@theoremjs/studio 0.3.4-next.433.gf6b4da5 → 0.3.4-next.452.gb2a8e30

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 (55) hide show
  1. package/README.md +4 -2
  2. package/example.d.ts +3 -1
  3. package/example.js +5 -2
  4. package/mod.d.ts +1 -1
  5. package/mod.js +1 -1
  6. package/package.json +6 -5
  7. package/page/assets/{Banner-DqFqeJx4.js → Banner-BtT5Jno9.js} +2 -2
  8. package/page/assets/{ConsoleFrame-CZvaA-nP.js → ConsoleFrame-DDA006j3.js} +1 -1
  9. package/page/assets/{ContextMenu-DX9Xmroi.js → ContextMenu-BC0hJ4Df.js} +2 -2
  10. package/page/assets/{ToolResultCharts-142qwvoG.js → ToolResultCharts-D-NF4Hm0.js} +1 -1
  11. package/page/assets/{TraceInspector-CiszHOn4.js → TraceInspector-BfzYgpuZ.js} +1 -1
  12. package/page/assets/{TraceOverview-DpGrlsvE.js → TraceOverview-BJXRMLrV.js} +1 -1
  13. package/page/assets/{_virtual_studio-type-sources-Cwv7y4JN.js → _virtual_studio-type-sources-Bu1ywN-k.js} +7 -2
  14. package/page/assets/browser-engine-Bfnox-N-.js +11 -0
  15. package/page/assets/{browser-transport-CYmonEpj.js → browser-transport-Bel-nXw2.js} +3 -3
  16. package/page/assets/{export-agent-DD3nkkXc.js → export-agent-BB1POilM.js} +1 -1
  17. package/page/assets/{guardrail-tester-CB_zfwu-.js → guardrail-tester-yHV-g5S_.js} +1 -1
  18. package/page/assets/{index-D3Myde0v.js → index-DV2pL-YD.js} +69 -68
  19. package/page/assets/{live-messages-CI-oP2dK.js → live-messages-dPuowG_o.js} +1 -1
  20. package/page/assets/{reader-CVFKFctz.js → reader-BB7aL5yX.js} +1 -1
  21. package/page/assets/{stub-Bf9DC2iQ.js → stub-DyzmGexy.js} +1 -1
  22. package/page/assets/{studio-code-pane-BEu6fsah.js → studio-code-pane-Bf6T13Ry.js} +3 -3
  23. package/page/assets/{studio-console-C1aZk0Nc.js → studio-console-DCCy7NQj.js} +1 -1
  24. package/page/assets/{studio-decision-B4Xv1U6i.js → studio-decision-HhKBScge.js} +1 -1
  25. package/page/assets/{studio-embedding-B88JqYyA.js → studio-embedding-BMxiUc_Y.js} +1 -1
  26. package/page/assets/{studio-host-console-CV6UmZhm.js → studio-host-console-BfKmY0JO.js} +2 -2
  27. package/page/assets/{studio-labels-W7ubfnI9.js → studio-labels-DT3Y_lVj.js} +1 -1
  28. package/page/assets/{studio-live-DsZTO9nf.js → studio-live-Db2A5dA4.js} +1 -1
  29. package/page/assets/{studio-map-B8KLLT-0.js → studio-map-CCd_2s_7.js} +1 -1
  30. package/page/assets/{studio-trace-replay-CNI1i-QJ.js → studio-trace-replay-C849YGo_.js} +1 -1
  31. package/page/assets/{studio-type-checker-xhy-9-yy.js → studio-type-checker-BaSH5B4-.js} +1 -1
  32. package/page/assets/{tool-schema-BlUbzOu9.js → tool-schema-E9NPuDcN.js} +1 -1
  33. package/page/assets/{tree-6F02tTxC.js → tree-2VeaKg0m.js} +1 -1
  34. package/page/assets/{turn-client-4GHmGV_4.js → turn-client-Dy9-U9_L.js} +1 -1
  35. package/page/assets/types-CMdljBjd.js +1 -0
  36. package/page/assets/{use-one-call-BDdS64oW.js → use-one-call-C0kYaLgu.js} +1 -1
  37. package/page/assets/{workspace-MlrGUH2-.js → workspace-Cno6Ge26.js} +1 -1
  38. package/page/index.html +12 -12
  39. package/server/test-connection.js +2 -4
  40. package/ui/brand-icons.js +20 -2
  41. package/ui/lib/type-icons.d.ts +3 -1
  42. package/ui/lib/type-icons.js +4 -2
  43. package/ui/node-icon.d.ts +4 -0
  44. package/ui/node-icon.js +23 -0
  45. package/ui/profile-editor.d.ts +3 -0
  46. package/ui/profile-editor.js +5 -2
  47. package/ui/read-map.js +3 -3
  48. package/ui/studio-save.js +45 -2
  49. package/ui/studio-screen.d.ts +4 -1
  50. package/ui/studio-screen.js +25 -26
  51. package/ui/tree-map.css +15 -0
  52. package/ui/tree-map.d.ts +7 -0
  53. package/ui/tree-map.js +190 -0
  54. package/page/assets/browser-engine-DhWIzxGh.js +0 -11
  55. package/page/assets/types-DN_oZAOJ.js +0 -1
@@ -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 }: {
@@ -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, IconFileSmile, IconFileZip, IconKey, IconMenu2, IconPlayerPlay, IconPlaylistX, IconPlus, IconRotateClockwise, IconSearch, IconSitemap, IconSparkles, IconTool, IconX, } from '@tabler/icons-react';
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 { addToolSpec, ProfileEditor } from "./profile-editor.js";
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: IconFileSmile },
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
@@ -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
+ }
@@ -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
+ }