@theoremjs/studio 0.3.4-next.433.gf6b4da5 → 0.3.4-next.454.gaa3f549
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/README.md +4 -2
- package/example.d.ts +3 -1
- package/example.js +5 -2
- package/mod.d.ts +1 -1
- package/mod.js +1 -1
- package/package.json +6 -5
- package/page/assets/{Banner-DqFqeJx4.js → Banner-QCiHVb5K.js} +2 -2
- package/page/assets/{ConsoleFrame-CZvaA-nP.js → ConsoleFrame-DqN62wL7.js} +1 -1
- package/page/assets/{ContextMenu-DX9Xmroi.js → ContextMenu-DIzJdlQ5.js} +2 -2
- package/page/assets/{ToolResultCharts-142qwvoG.js → ToolResultCharts-CjuviquY.js} +1 -1
- package/page/assets/{TraceInspector-CiszHOn4.js → TraceInspector-CwGSe8Oe.js} +1 -1
- package/page/assets/{TraceOverview-DpGrlsvE.js → TraceOverview-isg4ubrd.js} +1 -1
- package/page/assets/{_virtual_studio-type-sources-Cwv7y4JN.js → _virtual_studio-type-sources-Bu1ywN-k.js} +7 -2
- package/page/assets/browser-engine-DA4ZywPN.js +11 -0
- package/page/assets/{browser-transport-CYmonEpj.js → browser-transport-B0zaH_Aw.js} +3 -3
- package/page/assets/{export-agent-DD3nkkXc.js → export-agent-vV5u7tzN.js} +1 -1
- package/page/assets/{guardrail-tester-CB_zfwu-.js → guardrail-tester-D70vg-Zv.js} +1 -1
- package/page/assets/{index-D3Myde0v.js → index-CvrD0wqP.js} +69 -68
- package/page/assets/{live-messages-CI-oP2dK.js → live-messages-dPuowG_o.js} +1 -1
- package/page/assets/{reader-CVFKFctz.js → reader-BB7aL5yX.js} +1 -1
- package/page/assets/{stub-Bf9DC2iQ.js → stub-DyzmGexy.js} +1 -1
- package/page/assets/{studio-code-pane-BEu6fsah.js → studio-code-pane-CPon5udY.js} +3 -3
- package/page/assets/{studio-console-C1aZk0Nc.js → studio-console-BSV-XHe9.js} +1 -1
- package/page/assets/{studio-decision-B4Xv1U6i.js → studio-decision-Dk1Tsfah.js} +1 -1
- package/page/assets/{studio-embedding-B88JqYyA.js → studio-embedding-BkBuVy1T.js} +1 -1
- package/page/assets/{studio-host-console-CV6UmZhm.js → studio-host-console-C6f389Xp.js} +2 -2
- package/page/assets/{studio-labels-W7ubfnI9.js → studio-labels-DEEMSKuk.js} +1 -1
- package/page/assets/{studio-live-DsZTO9nf.js → studio-live-uDkVyN2h.js} +1 -1
- package/page/assets/{studio-map-B8KLLT-0.js → studio-map-BVIl4fqd.js} +1 -1
- package/page/assets/{studio-trace-replay-CNI1i-QJ.js → studio-trace-replay-DG90ujMk.js} +1 -1
- package/page/assets/{studio-type-checker-xhy-9-yy.js → studio-type-checker-BaSH5B4-.js} +1 -1
- package/page/assets/{tool-schema-BlUbzOu9.js → tool-schema-E9NPuDcN.js} +1 -1
- package/page/assets/{tree-6F02tTxC.js → tree-2VeaKg0m.js} +1 -1
- package/page/assets/{turn-client-4GHmGV_4.js → turn-client-Dy9-U9_L.js} +1 -1
- package/page/assets/types-CMdljBjd.js +1 -0
- package/page/assets/{use-one-call-BDdS64oW.js → use-one-call-DIEEQ4qF.js} +1 -1
- package/page/assets/{workspace-MlrGUH2-.js → workspace-Cno6Ge26.js} +1 -1
- package/page/index.html +12 -12
- package/server/test-connection.js +2 -4
- package/ui/boot-mark.d.ts +10 -2
- package/ui/boot-mark.js +44 -8
- package/ui/brand-icons.js +20 -2
- package/ui/lib/boot-hold.d.ts +21 -0
- package/ui/lib/boot-hold.js +38 -0
- package/ui/lib/type-icons.d.ts +3 -1
- package/ui/lib/type-icons.js +4 -2
- package/ui/node-icon.d.ts +4 -0
- package/ui/node-icon.js +23 -0
- package/ui/profile-editor.d.ts +3 -0
- package/ui/profile-editor.js +5 -2
- package/ui/read-map.js +3 -3
- package/ui/studio-save.js +45 -2
- package/ui/studio-screen.d.ts +4 -1
- package/ui/studio-screen.js +25 -26
- package/ui/tree-map.css +15 -0
- package/ui/tree-map.d.ts +7 -0
- package/ui/tree-map.js +190 -0
- package/page/assets/browser-engine-DhWIzxGh.js +0 -11
- package/page/assets/types-DN_oZAOJ.js +0 -1
package/ui/profile-editor.js
CHANGED
|
@@ -26,7 +26,7 @@ import { VStack } from '@astryxdesign/core/VStack';
|
|
|
26
26
|
import { IconAbc, IconAdjustmentsHorizontal, IconAlertTriangle, IconAlignLeft, IconAntennaBars1, IconAntennaBars2, IconAntennaBars3, IconAntennaBars4, IconAntennaBars5, IconAntennaBarsOff, IconArrowDown, IconArrowLeft, IconArrowUp, IconBan, IconBandage, IconBiohazard, IconBolt, IconBraces, IconBulb, IconBulbOff, IconCertificate, IconCheck, IconChevronRight, IconCircleDashed, IconDatabase, IconDeviceDesktop, IconEraser, IconEye, IconEyeOff, IconFeather, IconFlag, IconFlame, IconFlask, IconGauge, IconHandOff, IconHandStop, IconHeadset, IconHourglass, IconHttpDelete, IconHttpGet, IconHttpPatch, IconHttpPost, IconHttpPut, IconInfoCircle, IconInputAi, IconKey, IconLayersUnion, IconLockOpen, IconMessage, IconMicrophone, IconPackage, IconPaperclip, IconPencil, IconPlayerPause, IconPlayerPlay, IconPlayerStop, IconPlayerTrackNext, IconPlugConnected, IconPlus, IconRefresh, IconRegex, IconSearch, IconSend, IconServer, IconShieldLock, IconShieldOff, IconShieldSearch, IconSquareRoundedNumber0, IconSquareRoundedNumber2, IconTool, IconTrash, IconUser, IconUserCheck, IconWaveSine, IconWriting, IconX, } from '@tabler/icons-react';
|
|
27
27
|
import { ATTACHMENT_ACCEPT_MIMES, CONTINUE_STOP_KINDS, fieldMeta, GOOGLE_BUILTIN_TOOLS, GOOGLE_IMAGE_ASPECT_RATIOS, GOOGLE_IMAGE_OUTPUT_MIMES, GOOGLE_IMAGE_RESOLUTIONS, GOOGLE_SPEECH_VOICES, HTTP_METHODS, IMAGE_ATTACHMENT_ACCEPT_MIMES, LABEL_KEYS, labelDefault, PROFILE_HANDLE_MAX_CHARS, PROFILE_ID_MAX_CHARS, profileGraphFacet, THINKING_LEVELS, VOICE_ACCEPT_MIMES, } from '@theoremjs/agents';
|
|
28
28
|
import { BOUNDARIES, BOUNDARY_META, DETECT_ACTION_META, DETECT_ACTIONS, DETECTOR_BOUNDARIES, DETECTOR_GROUP_META, DETECTOR_GROUPS, DETECTOR_META, DETECTORS, TOOL_BOUNDARIES, TOOL_KINDS, } from '@theoremjs/agents/guardrails';
|
|
29
|
-
import { lazy, Suspense, useContext, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore, } from 'react';
|
|
29
|
+
import { createContext, lazy, Suspense, useContext, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore, } from 'react';
|
|
30
30
|
import { acceptSections, agentToolTarget, bindingSlots, COMPACTION_DRAFT_DEFAULTS, defaultEffortRequired, defaultModelRequired, definitionOf, draftAllows, draftKey, expandAccept, INLINE_LABELS, includableFacets, includeFacet, inputLimitsRequired, isGoogleRoute, isOpenRouterRoute, kernelDefinitions, LOCAL_PROVIDER_ID, lentCompression, modelBindingNodeId, modelRoute, modelSlots, newCriteria, newDecisionImage, newDecisionQuestion, newModelBinding, newOwnDetector, newPattern, newToolSpec, nextAccept, onStudioLocal, optionAt, plantsCanary, protocolOptionFields, removeModelBinding, STUDIO_TRACE_DESTINATION, sampleToolInput, sectionNote, servedProtocols, servesProfileType, setProfileType, studioNetworkNote, studioNodeRef, studioRunsProvider, studioTaintNote, takesContinueInstruction, toolSpecNodeId, updateModelBinding, withOption, writeAsk, } from "../mod.js";
|
|
31
31
|
import { AddProviderButton } from "./add-provider.js";
|
|
32
32
|
import { IconGoogle, IconOpenAi, IconOpenRouter, IconTypeSafe } from "./brand-icons.js";
|
|
@@ -1833,6 +1833,8 @@ function SampleInputRow({ value, onChange }) {
|
|
|
1833
1833
|
function TestConnectionButton({ isLoading, onClick }) {
|
|
1834
1834
|
return (_jsx(HStack, { children: _jsx(Button, { label: "Test connection", variant: "secondary", size: "sm", icon: _jsx(Icon, { icon: IconPlugConnected, size: "sm" }), isLoading: isLoading, onClick: onClick }) }));
|
|
1835
1835
|
}
|
|
1836
|
+
/** A test already run, which a still preview of the editor shows done. */
|
|
1837
|
+
export const ShownProbe = createContext(undefined);
|
|
1836
1838
|
/**
|
|
1837
1839
|
* Tries the tool's endpoint or MCP server once, from the server, which reaches public hosts only.
|
|
1838
1840
|
* The sample input and credential live here only: neither is saved to the draft.
|
|
@@ -1846,7 +1848,8 @@ function ToolTest({ tool, guardrails }) {
|
|
|
1846
1848
|
setToolCredential(tool.key, value);
|
|
1847
1849
|
};
|
|
1848
1850
|
const [pending, setPending] = useState(false);
|
|
1849
|
-
const
|
|
1851
|
+
const shown = useContext(ShownProbe);
|
|
1852
|
+
const [result, setResult] = useState(shown);
|
|
1850
1853
|
const http = tool.toolType === 'http';
|
|
1851
1854
|
const method = tool.method ?? HTTP_METHODS[0];
|
|
1852
1855
|
// A result answers the request as it was: once what it reaches or sends changes, it's dropped.
|
package/ui/read-map.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Text } from '@astryxdesign/core/Text';
|
|
3
|
-
import { IconFileCode,
|
|
3
|
+
import { IconFileCode, IconFolder } from '@tabler/icons-react';
|
|
4
4
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
5
5
|
import { mapCurve } from "../map-line.js";
|
|
6
6
|
import { readMap } from "./lib/read-map.js";
|
|
7
|
-
import { profileTypeIcon, toolTypeIcon } from "./lib/type-icons.js";
|
|
7
|
+
import { AGENT_ICON, profileTypeIcon, toolTypeIcon } from "./lib/type-icons.js";
|
|
8
8
|
import { MapToken, useTokenWidths } from "./map-token.js";
|
|
9
9
|
import './read-map.css';
|
|
10
10
|
/** The slowest the map draws: one step in this long. */
|
|
@@ -27,7 +27,7 @@ const LABEL = 30;
|
|
|
27
27
|
const NODE_ICON = {
|
|
28
28
|
entry: IconFileCode,
|
|
29
29
|
folder: IconFolder,
|
|
30
|
-
profile:
|
|
30
|
+
profile: AGENT_ICON,
|
|
31
31
|
tool: undefined,
|
|
32
32
|
more: undefined,
|
|
33
33
|
};
|
package/ui/studio-save.js
CHANGED
|
@@ -6,6 +6,7 @@ import { CodeBlock } from '@astryxdesign/core/CodeBlock';
|
|
|
6
6
|
import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog';
|
|
7
7
|
import { HoverCard } from '@astryxdesign/core/HoverCard';
|
|
8
8
|
import { HStack } from '@astryxdesign/core/HStack';
|
|
9
|
+
import { useClipboard } from '@astryxdesign/core/hooks';
|
|
9
10
|
import { Icon } from '@astryxdesign/core/Icon';
|
|
10
11
|
import { IconButton } from '@astryxdesign/core/IconButton';
|
|
11
12
|
import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout';
|
|
@@ -15,11 +16,47 @@ import { Text } from '@astryxdesign/core/Text';
|
|
|
15
16
|
import { useToast } from '@astryxdesign/core/Toast';
|
|
16
17
|
import { Token } from '@astryxdesign/core/Token';
|
|
17
18
|
import { VStack } from '@astryxdesign/core/VStack';
|
|
18
|
-
import { IconFiles, IconListDetails } from '@tabler/icons-react';
|
|
19
|
+
import { IconCheck, IconCopy, IconFiles, IconListDetails } from '@tabler/icons-react';
|
|
19
20
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
20
21
|
import { atStart, removeAgent, resetAgent, startedHere } from "../mod.js";
|
|
21
22
|
import { reviewSave, undoSave, writeSave } from "./lib/studio-project.js";
|
|
22
23
|
import { RefusalDetail } from "./refusal-detail.js";
|
|
24
|
+
/** The areas `settings` fall in, most first: `guardrails.detect (310)`, not every dotted path. */
|
|
25
|
+
function settingAreas(settings, shown = 4) {
|
|
26
|
+
const counts = new Map();
|
|
27
|
+
for (const setting of settings) {
|
|
28
|
+
const parts = setting.split('.');
|
|
29
|
+
const area = parts.length > 2 ? parts.slice(0, 2).join('.') : parts[0];
|
|
30
|
+
counts.set(area, (counts.get(area) ?? 0) + 1);
|
|
31
|
+
}
|
|
32
|
+
const areas = [...counts].sort((a, b) => b[1] - a[1]);
|
|
33
|
+
const named = areas
|
|
34
|
+
.slice(0, shown)
|
|
35
|
+
.map(([area, count]) => (count > 1 ? `${area} (${count})` : area));
|
|
36
|
+
const more = areas.length - named.length;
|
|
37
|
+
return more > 0 ? `${named.join(', ')}, and ${more} more` : named.join(', ');
|
|
38
|
+
}
|
|
39
|
+
/** Every alert as plain text, for pasting into a message: all names, the areas of each difference. */
|
|
40
|
+
function alertsReport(project, unloaded) {
|
|
41
|
+
const { problems, modelProblems, differences, unheld, unregistered } = project;
|
|
42
|
+
const lines = [`Theorem studio: ${project.name}`];
|
|
43
|
+
if (unloaded !== undefined)
|
|
44
|
+
lines.push('', 'The studio cannot load your files:', unloaded.trim());
|
|
45
|
+
for (const each of problems)
|
|
46
|
+
lines.push('', `Cannot run ${each.profile}: ${each.message}`);
|
|
47
|
+
for (const each of modelProblems) {
|
|
48
|
+
lines.push('', `${each.profile}'s model '${each.model}' cannot run: ${each.message}`);
|
|
49
|
+
}
|
|
50
|
+
if (unregistered.length) {
|
|
51
|
+
lines.push('', `Defined in files but not registered at start (${String(unregistered.length)}): ${unregistered.map((each) => each.name).join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
if (unheld.length)
|
|
54
|
+
lines.push('', `Cannot check against files: ${[...new Set(unheld)].join(' ')}`);
|
|
55
|
+
for (const each of differences) {
|
|
56
|
+
lines.push('', `${each.of} shows ${String(each.settings.length)} settings differently from files: ${settingAreas(each.settings, 12)}`);
|
|
57
|
+
}
|
|
58
|
+
return lines.join('\n');
|
|
59
|
+
}
|
|
23
60
|
/** Wide enough for a reason's sentence to read in a few lines. */
|
|
24
61
|
const ALERTS_WIDTH = 320;
|
|
25
62
|
/** The most of the alerts a hover shows before it scrolls: a project can have one for every agent. */
|
|
@@ -167,9 +204,15 @@ function ProjectProblems({ project, unloaded }) {
|
|
|
167
204
|
(unheld.length ? 1 : 0) +
|
|
168
205
|
(unregistered.length ? 1 : 0) +
|
|
169
206
|
(unloaded === undefined ? 0 : 1);
|
|
207
|
+
const { copy, isCopied } = useClipboard();
|
|
170
208
|
if (!count)
|
|
171
209
|
return null;
|
|
172
|
-
return (_jsx(HoverCard, { label: "What the studio cannot run or shows differently", touchTrigger: "tap", content: _jsx(ScrollableArea, { label: "What the studio cannot run or shows differently", width: ALERTS_WIDTH, style: { maxHeight: ALERTS_MAX_HEIGHT }, children: _jsxs(VStack, { gap: 3, children: [
|
|
210
|
+
return (_jsx(HoverCard, { label: "What the studio cannot run or shows differently", touchTrigger: "tap", content: _jsx(ScrollableArea, { label: "What the studio cannot run or shows differently", width: ALERTS_WIDTH, style: { maxHeight: ALERTS_MAX_HEIGHT }, children: _jsxs(VStack, { gap: 3, children: [_jsx(HStack, { hAlign: "end", children: _jsx(IconButton, { label: isCopied ? 'Copied' : 'Copy alerts', variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: isCopied ? IconCheck : IconCopy, size: "sm" }), tooltip: "Copy these alerts to send", onClick: () => {
|
|
211
|
+
copy(alertsReport(project, unloaded));
|
|
212
|
+
} }) }), unloaded !== undefined && (_jsxs(VStack, { gap: 1, children: [_jsx(Text, { weight: "semibold", children: "The studio cannot load your files." }), _jsx(Text, { type: "supporting", color: "secondary", children: "It runs them as they last loaded, until they load again." }), _jsx(CodeBlock, { code: unloaded.trim(), size: "sm", isWrapped: true, maxHeight: 240 })] })), problems.map((problem) => (_jsxs(VStack, { gap: 1, children: [_jsx(Text, { weight: "semibold", children: `The studio cannot run ${problem.profile}.` }), _jsx(Text, { type: "supporting", color: "secondary", children: problem.message })] }, problem.profile))), modelProblems.map((problem) => (_jsxs(VStack, { gap: 1, children: [_jsx(Text, { weight: "semibold", children: `As your files set it, ${problem.profile}'s model '${problem.model}' cannot run.` }), _jsx(Text, { type: "supporting", color: "secondary", children: problem.message })] }, `${problem.profile}:${problem.model}`))), unregistered.length > 0 && (_jsxs(VStack, { gap: 1, children: [_jsx(Text, { weight: "semibold", children: "Your project did not register everything its files define." }), _jsx(Text, { type: "supporting", color: "secondary", children: `${unregistered.length} defined in your files but not registered when the project started, so the studio cannot show them: ${unregistered
|
|
213
|
+
.slice(0, 5)
|
|
214
|
+
.map((each) => each.name)
|
|
215
|
+
.join(', ')}${unregistered.length > 5 ? `, and ${unregistered.length - 5} more` : ''}. A run and Save still use your files. If none of them load, the studio is probably reading a second copy of the Theorem package.` })] })), unheld.length > 0 && (_jsxs(VStack, { gap: 1, children: [_jsx(Text, { weight: "semibold", children: "The studio cannot check what it shows against your files." }), _jsx(Text, { type: "supporting", color: "secondary", children: `A run and Save still use your files. ${[...new Set(unheld)].join(' ')}${unregistered.length > 0 ? ' This usually follows from the profiles above not loading.' : ''}` })] })), differences.map((difference) => (_jsxs(VStack, { gap: 1, children: [_jsx(Text, { weight: "semibold", children: `The studio shows ${difference.of} differently from your files.` }), _jsx(Text, { type: "supporting", color: "secondary", children: `${difference.settings.length} ${difference.settings.length === 1 ? 'setting differs' : 'settings differ'}: ${settingAreas(difference.settings)}. A run and Save still use your files.` })] }, `${difference.kind}:${difference.of}`)))] }) }), children: _jsx(Token, { label: count === 1 ? '1 alert' : `${String(count)} alerts`, color: "orange" }) }));
|
|
173
216
|
}
|
|
174
217
|
const ProjectSaveContext = createContext(null);
|
|
175
218
|
/** Save for the open project, or null on a page that has none. */
|
package/ui/studio-screen.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type FilesMerge, type SharedEntry, type StudioDraft, type StudioRunsOn,
|
|
|
2
2
|
import { type Dispatch, type SetStateAction } from 'react';
|
|
3
3
|
import { type ProjectFiles, type ProjectSession } from './lib/studio-project.ts';
|
|
4
4
|
import { type RestoredStudio } from './lib/studio-session.ts';
|
|
5
|
+
import type { ProbeResult } from './lib/tool-probe.ts';
|
|
5
6
|
import { type StudioHost, type StudioOpened } from './studio-host.ts';
|
|
6
7
|
/** The workspace's store, kept in this tab's sessionStorage, and the open agent's draft from it. */
|
|
7
8
|
declare function useStudioWorkspace(start: RestoredStudio, project: ProjectSession | undefined): {
|
|
@@ -58,10 +59,12 @@ export type StudioWorkspaceState = ReturnType<typeof useStudioWorkspace>;
|
|
|
58
59
|
* count, over the inspector, with each issue on its row. Nothing on it changes anything: a picture
|
|
59
60
|
* of the studio for a page that is not the studio, such as a home page. It writes no storage and
|
|
60
61
|
* reports nothing. `on` is where the page's studio runs, so the issues are the ones it would show.
|
|
62
|
+
* `probe`, on a tool, is a connection test already run: the inspector shows it, scrolled to the end.
|
|
61
63
|
*/
|
|
62
|
-
export declare function StudioEditorPreview({ workspace, on, }: {
|
|
64
|
+
export declare function StudioEditorPreview({ workspace, on, probe, }: {
|
|
63
65
|
workspace: StudioWorkspace;
|
|
64
66
|
on: StudioRunsOn;
|
|
67
|
+
probe?: ProbeResult;
|
|
65
68
|
}): import("react").JSX.Element;
|
|
66
69
|
/** The studio, opened on `opened`, with what the page showing it provides. */
|
|
67
70
|
export declare function StudioScreen({ opened, host }: {
|
package/ui/studio-screen.js
CHANGED
|
@@ -21,7 +21,7 @@ import { Token } from '@astryxdesign/core/Token';
|
|
|
21
21
|
import { Tooltip } from '@astryxdesign/core/Tooltip';
|
|
22
22
|
import { TreeList } from '@astryxdesign/core/TreeList';
|
|
23
23
|
import { VStack } from '@astryxdesign/core/VStack';
|
|
24
|
-
import { IconActivity, IconAdjustmentsHorizontal, IconArrowBarToDown, IconArrowLeft, IconBrowserShare, IconCheck, IconCloud, IconCode, IconCopy, IconCopyPlus, IconFile, IconFileImport,
|
|
24
|
+
import { IconActivity, IconAdjustmentsHorizontal, IconArrowBarToDown, IconArrowLeft, IconBrowserShare, IconCheck, IconCloud, IconCode, IconCopy, IconCopyPlus, IconFile, IconFileImport, IconFileZip, IconKey, IconMenu2, IconPlayerPlay, IconPlaylistX, IconPlus, IconRotateClockwise, IconSearch, IconSitemap, IconSparkles, IconTool, IconX, } from '@tabler/icons-react';
|
|
25
25
|
import { profileGraphFacet } from '@theoremjs/agents';
|
|
26
26
|
import { InPlace, PaneFailure, RaisedPane, useDisclosureMotion, } from '@theoremjs/react/ui';
|
|
27
27
|
import { canonical } from "../canonical.js";
|
|
@@ -33,7 +33,6 @@ import { lazy, memo, Suspense, useCallback, useEffect, useLayoutEffect, useMemo,
|
|
|
33
33
|
import { flushSync } from 'react-dom';
|
|
34
34
|
import { ActingAsButton } from "./acting-as-button.js";
|
|
35
35
|
import { AddProviderButton } from "./add-provider.js";
|
|
36
|
-
import { IconTypeSafe, modelFamilyMark } from "./brand-icons.js";
|
|
37
36
|
import { useConfirmWrite } from "./confirm-write.js";
|
|
38
37
|
import { ConfirmWrite, GuardrailTest, ISSUE_ROW_ATTRIBUTE, LeavePage, ListBadges, LocalConnection, ProbedAgent, ProjectAsks, QuestionLink, RowOrigins, RunsOn, WorkspaceContext, } from "./inspector-context.js";
|
|
39
38
|
import { ActingAsContext, chatKeyOf, useActingAsState } from "./lib/acting-as.js";
|
|
@@ -54,9 +53,10 @@ import { createStudioStore } from "./lib/studio-store.js";
|
|
|
54
53
|
import { subscribeToFiles } from "./lib/studio-watch.js";
|
|
55
54
|
import { toolCredential } from "./lib/tool-credentials.js";
|
|
56
55
|
import { runToolProbe } from "./lib/tool-probe.js";
|
|
57
|
-
import { PROFILE_TYPE_ICON, toolTypeIcon } from "./lib/type-icons.js";
|
|
56
|
+
import { AGENT_ICON, PROFILE_TYPE_ICON, toolTypeIcon } from "./lib/type-icons.js";
|
|
58
57
|
import { warmWhenIdle } from "./lib/when-idle.js";
|
|
59
|
-
import {
|
|
58
|
+
import { nodeIcon } from "./node-icon.js";
|
|
59
|
+
import { addToolSpec, ProfileEditor, ShownProbe } from "./profile-editor.js";
|
|
60
60
|
import { ProviderEditor } from "./provider-editor.js";
|
|
61
61
|
import { SharedChange } from "./shared-change.js";
|
|
62
62
|
import { ChooseConflictsContext, FileConflicts } from "./studio-conflicts.js";
|
|
@@ -78,25 +78,6 @@ const GuardrailTester = lazy(() => testerPanel().then((tester) => ({ default: te
|
|
|
78
78
|
const traceReplay = () => import("./studio-trace-replay.js");
|
|
79
79
|
const TraceReplay = lazy(() => traceReplay().then((pane) => ({ default: pane.TraceReplay })));
|
|
80
80
|
const StudioSettings = lazy(() => settingsPanel().then((settings) => ({ default: settings.StudioSettings })));
|
|
81
|
-
/** A node's icon; Identity shows the profile type's once one is picked. */
|
|
82
|
-
function nodeIcon(draft, ref) {
|
|
83
|
-
const type = draft.identity.profileType;
|
|
84
|
-
if (ref.facet === 'identity' && type)
|
|
85
|
-
return PROFILE_TYPE_ICON[type];
|
|
86
|
-
if (ref.facet === 'modelBinding') {
|
|
87
|
-
const binding = draft.modelBindings.find((each) => each.key === ref.key);
|
|
88
|
-
if (!binding)
|
|
89
|
-
return FACET_ICON.modelBinding;
|
|
90
|
-
const definition = draft.providers.find((each) => each.id === binding.provider)?.definition;
|
|
91
|
-
if (definition === 'typesafe')
|
|
92
|
-
return IconTypeSafe;
|
|
93
|
-
return modelFamilyMark(binding.apiId) ?? FACET_ICON.modelBinding;
|
|
94
|
-
}
|
|
95
|
-
if (ref.facet !== 'toolSpec')
|
|
96
|
-
return FACET_ICON[ref.facet];
|
|
97
|
-
const tool = draft.toolSpecs.find((spec) => spec.key === ref.key);
|
|
98
|
-
return tool ? toolTypeIcon(tool.toolType) : IconTool;
|
|
99
|
-
}
|
|
100
81
|
/** A row's hover action: stops at the button so the row itself isn't selected. */
|
|
101
82
|
function actionButton(label, icon, onPress) {
|
|
102
83
|
return (_jsx(IconButton, { label: label, variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: icon, size: "sm" }), tooltip: label, onClick: (event) => {
|
|
@@ -329,7 +310,7 @@ function WorkspaceListToggle({ workspace, list, onChange, }) {
|
|
|
329
310
|
}
|
|
330
311
|
/** The sidebar's lists, as icons: a name and its count would not fit the nav's width. */
|
|
331
312
|
const WORKSPACE_LISTS = [
|
|
332
|
-
{ value: 'agents', name: 'Agents', icon:
|
|
313
|
+
{ value: 'agents', name: 'Agents', icon: AGENT_ICON },
|
|
333
314
|
{ value: 'tools', name: 'Tools', icon: IconTool },
|
|
334
315
|
{ value: 'providers', name: 'Providers', icon: IconCloud },
|
|
335
316
|
];
|
|
@@ -1527,8 +1508,9 @@ const TreeColumn = memo(function TreeColumn({ tree, draft, onAddAgent, listRef,
|
|
|
1527
1508
|
* count, over the inspector, with each issue on its row. Nothing on it changes anything: a picture
|
|
1528
1509
|
* of the studio for a page that is not the studio, such as a home page. It writes no storage and
|
|
1529
1510
|
* reports nothing. `on` is where the page's studio runs, so the issues are the ones it would show.
|
|
1511
|
+
* `probe`, on a tool, is a connection test already run: the inspector shows it, scrolled to the end.
|
|
1530
1512
|
*/
|
|
1531
|
-
export function StudioEditorPreview({ workspace, on, }) {
|
|
1513
|
+
export function StudioEditorPreview({ workspace, on, probe, }) {
|
|
1532
1514
|
const selected = workspace.selected;
|
|
1533
1515
|
const at = workspaceNodeRef(workspace, selected);
|
|
1534
1516
|
const focus = at && 'agent' in at ? at.agent : (workspace.agents[0]?.key ?? '');
|
|
@@ -1536,6 +1518,23 @@ export function StudioEditorPreview({ workspace, on, }) {
|
|
|
1536
1518
|
const compiled = useMemo(() => compileWorkspace(workspace, on), [workspace, on]);
|
|
1537
1519
|
const editing = innerNodeId(selected, focus) ?? 'identity';
|
|
1538
1520
|
const still = () => { };
|
|
1521
|
+
const scroller = useRef(null);
|
|
1522
|
+
// why: A tool's test sits at the foot of its page: the editor stays at its end as the page settles.
|
|
1523
|
+
useLayoutEffect(() => {
|
|
1524
|
+
const node = scroller.current;
|
|
1525
|
+
const content = node?.firstElementChild;
|
|
1526
|
+
if (!probe || !node || !content)
|
|
1527
|
+
return;
|
|
1528
|
+
const toEnd = () => {
|
|
1529
|
+
node.scrollTop = node.scrollHeight;
|
|
1530
|
+
};
|
|
1531
|
+
toEnd();
|
|
1532
|
+
const watch = new ResizeObserver(toEnd);
|
|
1533
|
+
watch.observe(content);
|
|
1534
|
+
return () => {
|
|
1535
|
+
watch.disconnect();
|
|
1536
|
+
};
|
|
1537
|
+
}, [probe]);
|
|
1539
1538
|
const view = {
|
|
1540
1539
|
keysOpen: false,
|
|
1541
1540
|
setKeysOpen: still,
|
|
@@ -1546,7 +1545,7 @@ export function StudioEditorPreview({ workspace, on, }) {
|
|
|
1546
1545
|
settingsOpen: false,
|
|
1547
1546
|
open: still,
|
|
1548
1547
|
};
|
|
1549
|
-
return (_jsxs(VStack, { height: "100%", children: [_jsx(EditorToolbar, { heading: editorTitle(draft, editing), compiled: compiled, selected: selected, view: view, onIssue: still, getCode: null, project: null, setSheet: still }), _jsx(StackItem, { size: "fill", children: _jsx(RunsOn.Provider, { value: on, children: _jsx(ProfileEditor, { draft: draft, setDraft: still, selectedId: editing, onSelect: still, issues: editorIssuesOf(compiled, focus) }) }) })] }));
|
|
1548
|
+
return (_jsxs(VStack, { height: "100%", children: [_jsx(EditorToolbar, { heading: editorTitle(draft, editing), compiled: compiled, selected: selected, view: view, onIssue: still, getCode: null, project: null, setSheet: still }), _jsx(StackItem, { size: "fill", children: _jsx(ScrollableArea, { label: "Editor", height: "100%", ref: scroller, children: _jsx(RunsOn.Provider, { value: on, children: _jsx(ShownProbe.Provider, { value: probe, children: _jsx(ProfileEditor, { draft: draft, setDraft: still, selectedId: editing, onSelect: still, issues: editorIssuesOf(compiled, focus) }) }) }) }) })] }));
|
|
1550
1549
|
}
|
|
1551
1550
|
/**
|
|
1552
1551
|
* The issue count, which goes to the next issue after the selected node and shows it there. Hidden
|
package/ui/tree-map.css
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* The tree map: the lines are one drawing under the tokens (map-token.css), as on the map. It has no
|
|
2
|
+
background of its own: it sits on whatever holds it. */
|
|
3
|
+
.studio-tree-map {
|
|
4
|
+
position: relative;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.studio-tree-map-lines {
|
|
8
|
+
position: absolute;
|
|
9
|
+
inset: 0 auto auto 0;
|
|
10
|
+
pointer-events: none;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.studio-tree-map-lines .map-line {
|
|
14
|
+
stroke: color-mix(in srgb, var(--color-border-emphasized) 60%, var(--color-background-surface));
|
|
15
|
+
}
|
package/ui/tree-map.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type StudioWorkspace } from '../mod.ts';
|
|
2
|
+
export interface StudioTreeMapProps {
|
|
3
|
+
workspace: StudioWorkspace;
|
|
4
|
+
/** The keys of the agents to draw, in the workspace's order; every agent when unset. */
|
|
5
|
+
agents?: readonly string[] | undefined;
|
|
6
|
+
}
|
|
7
|
+
export declare function StudioTreeMap({ workspace, agents }: StudioTreeMapProps): import("react").JSX.Element;
|
package/ui/tree-map.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { IconCloud, IconEngine, IconSettingsAutomation, IconTool } from '@tabler/icons-react';
|
|
3
|
+
import { useRef } from 'react';
|
|
4
|
+
import { mapCurve } from "../map-line.js";
|
|
5
|
+
import { createBlankDraft, libraryDraft, workspaceTree, } from "../mod.js";
|
|
6
|
+
import { toolTypeIcon } from "./lib/type-icons.js";
|
|
7
|
+
import { MapToken, useTokenWidths } from "./map-token.js";
|
|
8
|
+
import { nodeIcon } from "./node-icon.js";
|
|
9
|
+
import './tree-map.css';
|
|
10
|
+
/** A row's height, and how far in each level of the tree starts. */
|
|
11
|
+
const ROW = 32;
|
|
12
|
+
const INDENT = 28;
|
|
13
|
+
/** Where under a token its children's line runs: the middle of its icon. */
|
|
14
|
+
const TRUNK = 13;
|
|
15
|
+
/** A token's half height, and the radius of the line's turn. */
|
|
16
|
+
const HALF = 11;
|
|
17
|
+
const TURN = 8;
|
|
18
|
+
const PAD = 16;
|
|
19
|
+
/** Between the tree's widest row and the feeds, and between a token's side and its line. */
|
|
20
|
+
const GAP = 72;
|
|
21
|
+
const LINE_GAP = 4;
|
|
22
|
+
/** How far right of the widest feed a line coming round them runs. */
|
|
23
|
+
const RAIL = 20;
|
|
24
|
+
/** How many of the registry's tools sit under it: one of each type first. */
|
|
25
|
+
const SHOWN_TOOLS = 4;
|
|
26
|
+
const TOOL_TYPES = ['function', 'http', 'mcp', 'agent'];
|
|
27
|
+
/** A token's width until it is measured. */
|
|
28
|
+
const GUESS = 120;
|
|
29
|
+
function rowsOf(workspace, only) {
|
|
30
|
+
const rows = [];
|
|
31
|
+
const walk = (node, agent, depth, parent) => {
|
|
32
|
+
const row = {
|
|
33
|
+
node,
|
|
34
|
+
agent,
|
|
35
|
+
x: PAD + depth * INDENT,
|
|
36
|
+
y: PAD + HALF + rows.length * ROW,
|
|
37
|
+
...(parent ? { parent: { x: parent.x, y: parent.y } } : {}),
|
|
38
|
+
};
|
|
39
|
+
rows.push(row);
|
|
40
|
+
for (const child of node.children)
|
|
41
|
+
walk(child, agent, depth + 1, row);
|
|
42
|
+
};
|
|
43
|
+
workspaceTree(workspace).agents.forEach((root, index) => {
|
|
44
|
+
const key = workspace.agents[index]?.key ?? '';
|
|
45
|
+
if (only === undefined || only.includes(key))
|
|
46
|
+
walk(root, key, 0);
|
|
47
|
+
});
|
|
48
|
+
return rows;
|
|
49
|
+
}
|
|
50
|
+
/** A row's token id: the tree's ids repeat from one agent to the next. */
|
|
51
|
+
function rowId(row) {
|
|
52
|
+
return `${row.agent}/${row.node.id}`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Each agent's runner (`runTurn`, or `runDecision` for a decision), beside its name; its providers, beside its models; and the
|
|
56
|
+
* workspace's tool registry, beside its Tools row, with the tool runner under it. A feed sits level
|
|
57
|
+
* with the middle of the rows it joins, or under the feed before it when that is lower.
|
|
58
|
+
*/
|
|
59
|
+
const REGISTRY = 'tool-registry';
|
|
60
|
+
/**
|
|
61
|
+
* What runs a profile of this type: `runDecision`, `runSession`, `runEmbedding`, else `runTurn`. A
|
|
62
|
+
* host has none of its own: the tool runner runs its calls.
|
|
63
|
+
*/
|
|
64
|
+
function runnerLabel(type) {
|
|
65
|
+
if (type === 'host')
|
|
66
|
+
return undefined;
|
|
67
|
+
if (type === 'decision')
|
|
68
|
+
return 'Decision runner';
|
|
69
|
+
if (type === 'live')
|
|
70
|
+
return 'Session runner';
|
|
71
|
+
if (type === 'embedding')
|
|
72
|
+
return 'Embedding runner';
|
|
73
|
+
return 'Turn runner';
|
|
74
|
+
}
|
|
75
|
+
function feedsOf(workspace, rows) {
|
|
76
|
+
const feeds = [];
|
|
77
|
+
const add = (id, label, icon, into, under, depth = 0) => {
|
|
78
|
+
const below = (feeds.at(-1)?.y ?? Number.NEGATIVE_INFINITY) + ROW;
|
|
79
|
+
const middle = into.length ? into.reduce((sum, row) => sum + row.y, 0) / into.length : below;
|
|
80
|
+
feeds.push({
|
|
81
|
+
id,
|
|
82
|
+
label,
|
|
83
|
+
icon,
|
|
84
|
+
y: Math.max(middle, below),
|
|
85
|
+
into: into.map(rowId),
|
|
86
|
+
...(under ? { under } : {}),
|
|
87
|
+
depth,
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
for (const agent of workspace.agents) {
|
|
91
|
+
const mine = rows.filter((row) => row.agent === agent.key);
|
|
92
|
+
const name = mine.find((row) => row.node.ref.facet === 'identity');
|
|
93
|
+
if (!name)
|
|
94
|
+
continue;
|
|
95
|
+
const runner = runnerLabel(agent.identity.profileType);
|
|
96
|
+
if (runner)
|
|
97
|
+
add(`${agent.key}/runner`, runner, IconEngine, [name]);
|
|
98
|
+
const providers = new Map();
|
|
99
|
+
for (const binding of agent.modelBindings) {
|
|
100
|
+
const row = mine.find((each) => each.node.ref.facet === 'modelBinding' && each.node.ref.key === binding.key);
|
|
101
|
+
if (row)
|
|
102
|
+
providers.set(binding.provider, [...(providers.get(binding.provider) ?? []), row]);
|
|
103
|
+
}
|
|
104
|
+
for (const [provider, into] of providers) {
|
|
105
|
+
add(`${agent.key}/provider:${provider}`, provider, IconCloud, into);
|
|
106
|
+
}
|
|
107
|
+
// The workspace's tools are one registry that each agent's tool policy picks from: drawn once,
|
|
108
|
+
// beside the first Tools row, with a line into every other.
|
|
109
|
+
const tools = mine.find((row) => row.node.ref.facet === 'tools');
|
|
110
|
+
const drawn = feeds.find((feed) => feed.id === REGISTRY);
|
|
111
|
+
if (tools && drawn)
|
|
112
|
+
drawn.into.push(rowId(tools));
|
|
113
|
+
else if (tools && workspace.toolSpecs.length > 0) {
|
|
114
|
+
const registry = REGISTRY;
|
|
115
|
+
add(registry, 'Tool registry', IconTool, [tools]);
|
|
116
|
+
for (const tool of shownTools(workspace.toolSpecs)) {
|
|
117
|
+
const icon = toolTypeIcon(tool.toolType);
|
|
118
|
+
add(`${registry}/${tool.key}`, tool.toolName, icon, [], registry, 1);
|
|
119
|
+
}
|
|
120
|
+
add('tool-runner', 'Tool runner', IconSettingsAutomation, [], registry);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return feeds;
|
|
124
|
+
}
|
|
125
|
+
/** A few of the registry's tools: the first of each type, then the next ones, in the registry's order. */
|
|
126
|
+
function shownTools(tools) {
|
|
127
|
+
const firsts = TOOL_TYPES.flatMap((type) => {
|
|
128
|
+
const tool = tools.find((each) => each.toolType === type);
|
|
129
|
+
return tool ? [tool] : [];
|
|
130
|
+
});
|
|
131
|
+
const rest = tools.filter((tool) => !firsts.includes(tool));
|
|
132
|
+
return [...firsts, ...rest].slice(0, SHOWN_TOOLS);
|
|
133
|
+
}
|
|
134
|
+
/** Right from `from` to `rail`, up or down it, and back left to `to`, its corners turned. */
|
|
135
|
+
function sidePath(from, to, rail) {
|
|
136
|
+
const up = Math.sign(to.y - from.y);
|
|
137
|
+
return (`M ${from.x} ${from.y} H ${rail - TURN} Q ${rail} ${from.y} ${rail} ${from.y + up * TURN}` +
|
|
138
|
+
` V ${to.y - up * TURN} Q ${rail} ${to.y} ${rail - TURN} ${to.y} H ${to.x}`);
|
|
139
|
+
}
|
|
140
|
+
/** Down from under the parent's icon, then a turn across to the child. */
|
|
141
|
+
function linePath(from, to) {
|
|
142
|
+
const x = from.x + TRUNK;
|
|
143
|
+
return `M ${x} ${from.y + HALF} V ${to.y - TURN} Q ${x} ${to.y} ${x + TURN} ${to.y} H ${to.x - LINE_GAP}`;
|
|
144
|
+
}
|
|
145
|
+
export function StudioTreeMap({ workspace, agents }) {
|
|
146
|
+
const root = useRef(null);
|
|
147
|
+
const rows = rowsOf(workspace, agents);
|
|
148
|
+
const feeds = feedsOf(workspace, rows);
|
|
149
|
+
const widths = useTokenWidths(root, workspace);
|
|
150
|
+
const drafts = new Map(workspace.agents.map((agent) => [
|
|
151
|
+
agent.key,
|
|
152
|
+
libraryDraft(workspace, agent.key) ?? createBlankDraft(),
|
|
153
|
+
]));
|
|
154
|
+
const right = (row) => row.x + (widths[rowId(row)] ?? GUESS);
|
|
155
|
+
const feedX = Math.max(0, ...rows.map(right)) + GAP;
|
|
156
|
+
const byId = new Map(rows.map((row) => [rowId(row), row]));
|
|
157
|
+
const feedById = new Map(feeds.map((feed) => [feed.id, feed]));
|
|
158
|
+
const height = PAD * 2 + Math.max(rows.length * ROW, ...feeds.map((feed) => feed.y + HALF - PAD));
|
|
159
|
+
const feedLeft = (feed) => feedX + feed.depth * INDENT;
|
|
160
|
+
const feedRight = (feed) => feedLeft(feed) + (widths[feed.id] ?? GUESS);
|
|
161
|
+
/** Right of every feed from `above` down to `feed`. */
|
|
162
|
+
const railFor = (feed, above) => Math.max(...feeds.filter((each) => each.y >= above.y && each.y <= feed.y).map(feedRight)) +
|
|
163
|
+
RAIL;
|
|
164
|
+
const rails = feeds.flatMap((feed) => {
|
|
165
|
+
const above = feed.under === undefined ? undefined : feedById.get(feed.under);
|
|
166
|
+
return above && feed.depth <= above.depth ? [railFor(feed, above)] : [];
|
|
167
|
+
});
|
|
168
|
+
const width = Math.max(GUESS, ...feeds.map(feedRight), ...rails) + PAD;
|
|
169
|
+
return (_jsxs("div", { ref: root, className: "studio-tree-map", style: { width, height }, children: [_jsxs("svg", { className: "studio-tree-map-lines", width: width, height: height, "aria-hidden": "true", children: [rows.map((row) => row.parent && (_jsx("path", { className: "map-line", d: linePath(row.parent, row) }, rowId(row)))), feeds.flatMap((feed) => feed.into.map((id) => {
|
|
170
|
+
const row = byId.get(id);
|
|
171
|
+
if (!row)
|
|
172
|
+
return null;
|
|
173
|
+
const { d } = mapCurve({ x: feedX - LINE_GAP, y: feed.y }, { x: right(row) + LINE_GAP, y: row.y });
|
|
174
|
+
return _jsx("path", { className: "map-line", d: d }, `${feed.id}>${id}`);
|
|
175
|
+
})), feeds.map((feed) => {
|
|
176
|
+
const above = feed.under === undefined ? undefined : feedById.get(feed.under);
|
|
177
|
+
if (above === undefined)
|
|
178
|
+
return null;
|
|
179
|
+
const from = { x: feedLeft(above), y: above.y };
|
|
180
|
+
// One of its own turns in from the trunk, as a tree row does; one level with it comes round
|
|
181
|
+
// the right of what lies between, so it reads as joining the feed, not as one of its own.
|
|
182
|
+
const d = feed.depth > above.depth
|
|
183
|
+
? linePath(from, { x: feedLeft(feed), y: feed.y })
|
|
184
|
+
: sidePath({ x: feedRight(feed) + LINE_GAP, y: feed.y }, { x: feedRight(above) + LINE_GAP, y: above.y }, railFor(feed, above));
|
|
185
|
+
return _jsx("path", { className: "map-line", d: d }, `${feed.id}^`);
|
|
186
|
+
})] }), rows.map((row) => {
|
|
187
|
+
const draft = drafts.get(row.agent) ?? createBlankDraft();
|
|
188
|
+
return (_jsx(MapToken, { id: rowId(row), label: row.node.label, icon: nodeIcon(draft, row.node.ref), x: row.x, y: row.y }, rowId(row)));
|
|
189
|
+
}), feeds.map((feed) => (_jsx(MapToken, { id: feed.id, label: feed.label, icon: feed.icon, x: feedLeft(feed), y: feed.y }, feed.id)))] }));
|
|
190
|
+
}
|