@theoremjs/studio 0.3.4-next.482.g7e608f2 → 0.3.4-next.508.geda6db0

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 (79) hide show
  1. package/README.md +3 -3
  2. package/compile.d.ts +1 -1
  3. package/draft.d.ts +1 -1
  4. package/example.js +5 -2
  5. package/map-line.d.ts +12 -0
  6. package/map-line.js +98 -0
  7. package/map.d.ts +1 -1
  8. package/map.js +0 -1
  9. package/package.json +4 -4
  10. package/page/assets/{Banner-CrUhb26_.js → Banner-BcHZFaJb.js} +2 -2
  11. package/page/assets/{ConsoleFrame-6cEEvO4i.js → ConsoleFrame-Byq_hSpu.js} +3 -3
  12. package/page/assets/{ContextMenu-WnsgC14_.js → ContextMenu-C6S-GYrc.js} +2 -2
  13. package/page/assets/{ToolResultCharts-BEhuo0Z4.js → ToolResultCharts-CkEf5ub5.js} +1 -1
  14. package/page/assets/{TraceInspector-k1w1q0XO.js → TraceInspector-BsYHHhGv.js} +1 -1
  15. package/page/assets/{TraceOverview-Crxd-sTS.js → TraceOverview-CMFfbfGM.js} +2 -2
  16. package/page/assets/{_virtual_studio-type-sources-DAabvYbE.js → _virtual_studio-type-sources-CTIGQ210.js} +56 -48
  17. package/page/assets/browser-engine-DjYv346h.js +11 -0
  18. package/page/assets/browser-transport-Bm3uY9Fj.js +47 -0
  19. package/page/assets/{crypto-iT_LZ8Et.js → crypto-DAIn4qnM.js} +1 -1
  20. package/page/assets/{egress-automata-B5ZGE1mT.js → egress-automata-SiHQTP1i.js} +1 -1
  21. package/page/assets/{encode-files-8AFMw7Er.js → encode-files-oYzzs_iG.js} +1 -1
  22. package/page/assets/{export-agent-BedkRp13.js → export-agent-BeUTYowB.js} +3 -3
  23. package/page/assets/{guardrail-tester-XwnZWNsg.js → guardrail-tester-DLT99rnO.js} +2 -2
  24. package/page/assets/index-C9rhrCGT.css +1 -0
  25. package/page/assets/{index-BVHT-9mR.js → index-DNA-Y8Mp.js} +69 -69
  26. package/page/assets/{labels-provider-BSiD6Lrb.js → labels-provider-Gus3heEH.js} +1 -1
  27. package/page/assets/{live-messages-B65rUlNM.js → live-messages-BnBioidx.js} +1 -1
  28. package/page/assets/reader-BFYIhA73.js +7 -0
  29. package/page/assets/shaped-data-1Qn2111E.js +16 -0
  30. package/page/assets/{stub-DG5wxM-A.js → stub-B3GersQP.js} +2 -2
  31. package/page/assets/{studio-code-pane-p4fjtQhL.js → studio-code-pane-CCi7-lyE.js} +3 -3
  32. package/page/assets/{studio-console-BtZY4aUM.js → studio-console-CJT5FkWm.js} +1 -1
  33. package/page/assets/{studio-decision-BovTBIwV.js → studio-decision-DhHdHb7Z.js} +1 -1
  34. package/page/assets/{studio-embedding-BY6UQk0k.js → studio-embedding-B8vBhuZU.js} +1 -1
  35. package/page/assets/studio-host-console-CE_5_tEn.js +2 -0
  36. package/page/assets/{studio-labels-C3yQoEd0.js → studio-labels-C2SI_T2p.js} +1 -1
  37. package/page/assets/{studio-live-B_cVDiqs.js → studio-live-CKSSyYZj.js} +3 -3
  38. package/page/assets/studio-map-CKm_FCd9.js +1 -0
  39. package/page/assets/studio-trace-replay-BRMD6pex.js +2 -0
  40. package/page/assets/{studio-type-checker-Bljo3amv.js → studio-type-checker-DoaPHYNy.js} +1 -1
  41. package/page/assets/tool-schema-DXYwL5aE.js +18 -0
  42. package/page/assets/{trace-record-D4xAUHLZ.js → trace-record-DWxIfO7D.js} +1 -1
  43. package/page/assets/tree-Chi6mqzx.js +3 -0
  44. package/page/assets/{turn-client-CBWNoFgo.js → turn-client-DF0ije7A.js} +2 -2
  45. package/page/assets/types-2I4NfLiS.js +1 -0
  46. package/page/assets/{use-one-call-rh-5EjBB.js → use-one-call-Bhnpve1Y.js} +1 -1
  47. package/page/assets/{workspace-kAEf4Zn4.js → workspace-U3w2ELtN.js} +1 -1
  48. package/page/index.html +17 -17
  49. package/run-payload.d.ts +1 -1
  50. package/runtime.d.ts +1 -1
  51. package/runtime.js +1 -1
  52. package/server/draft-decide.d.ts +1 -4
  53. package/server/draft-decide.js +1 -1
  54. package/server/draft-replies.js +2 -2
  55. package/source.js +1 -1
  56. package/ui/brand-icons.d.ts +2 -2
  57. package/ui/brand-icons.js +6 -4
  58. package/ui/lib/tree-map-layout.d.ts +120 -18
  59. package/ui/lib/tree-map-layout.js +1292 -190
  60. package/ui/map-token.css +25 -0
  61. package/ui/node-icon.js +1 -4
  62. package/ui/profile-editor.js +1 -1
  63. package/ui/read-map.css +1 -24
  64. package/ui/read-map.js +52 -4
  65. package/ui/studio-map.js +1 -3
  66. package/ui/tree-map.css +9 -0
  67. package/ui/tree-map.d.ts +27 -6
  68. package/ui/tree-map.js +215 -75
  69. package/page/assets/browser-engine-C78edv3x.js +0 -11
  70. package/page/assets/browser-transport-mGxdvtBL.js +0 -47
  71. package/page/assets/index-Cbs7B46A.css +0 -1
  72. package/page/assets/reader-XebgyCzF.js +0 -7
  73. package/page/assets/shaped-data-Bc6u4pow.js +0 -16
  74. package/page/assets/studio-host-console-D9NFWXxV.js +0 -2
  75. package/page/assets/studio-map-iN6-MOzK.js +0 -1
  76. package/page/assets/studio-trace-replay-B3k21TSg.js +0 -2
  77. package/page/assets/tool-schema-BmScISma.js +0 -18
  78. package/page/assets/tree-BiZScKs7.js +0 -3
  79. package/page/assets/types-zRlFhC9U.js +0 -1
package/ui/tree-map.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { IconAdjustments, IconArrowsMinimize, IconChecks, IconCloud, IconEngine, IconKey, IconLock, IconPlugConnected, IconRepeat, IconSettingsAutomation, IconShieldCheck, IconStack2, IconTimeline, IconWorldCheck, } from '@tabler/icons-react';
3
- import { useMemo, useRef, useState } from 'react';
4
- import { mapCurve } from "../map-line.js";
2
+ import { Text } from '@astryxdesign/core/Text';
3
+ import { IconArrowsMinimize, IconCloud, IconCpu, IconDatabase, IconKey, IconLock, IconPlugConnected, IconRepeat, IconRotateClockwise, IconSettingsAutomation, IconShieldCheck, IconStack2, IconTimeline, IconVault, IconWorldCheck, } from '@tabler/icons-react';
4
+ import { useMemo, useRef, useState, } from 'react';
5
+ import { mapBracket, mapPolyline, mapRoute } from "../map-line.js";
5
6
  import { createBlankDraft, libraryDraft } from "../mod.js";
6
- import { drawnAt, HALF, INDENT, PAD, placeAt, rowId, treeMapLayout, } from "./lib/tree-map-layout.js";
7
+ import { modelFamilyMark } from "./brand-icons.js";
8
+ import { atRest, beside, drawnAt, goneAt, HALF, INDENT, lineAt, lineKey, PAD, placeAt, rowId, settledAt, settling, treeMapLayout, underKey, } from "./lib/tree-map-layout.js";
7
9
  import { AGENT_ICON, profileTypeIcon, toolTypeIcon } from "./lib/type-icons.js";
8
10
  import { MapToken, useTokenWidths } from "./map-token.js";
9
11
  import { nodeIcon } from "./node-icon.js";
@@ -15,19 +17,20 @@ const TRUNK = 13;
15
17
  const TURN = 8;
16
18
  /** Between a token's side and its line. */
17
19
  const LINE_GAP = 4;
18
- /** How far right of what it passes a line coming round a column runs. */
19
- const RAIL = 20;
20
+ /** How far right of the feeds it passes a line between two feeds of one column runs. */
21
+ const BOW = 16;
22
+ /** How far a feed going drifts off. */
23
+ const DRIFT = 8;
20
24
  /** A token's width until it is measured. */
21
25
  const GUESS = 120;
22
26
  const STEP_ICONS = {
23
- resolve: IconAdjustments,
24
27
  sanitize: IconShieldCheck,
25
28
  compaction: IconArrowsMinimize,
26
29
  stream: IconCloud,
27
30
  tools: IconRepeat,
28
- validation: IconChecks,
29
31
  egress: IconShieldCheck,
30
32
  trace: IconTimeline,
33
+ loop: IconRotateClockwise,
31
34
  };
32
35
  const CHECK_ICONS = {
33
36
  permission: IconLock,
@@ -37,8 +40,6 @@ const CHECK_ICONS = {
37
40
  };
38
41
  function feedIcon(feed) {
39
42
  const of = feed.of ?? '';
40
- if (feed.kind === 'runner')
41
- return IconEngine;
42
43
  if (feed.kind === 'step')
43
44
  return STEP_ICONS[of] ?? IconRepeat;
44
45
  if (feed.kind === 'provider')
@@ -47,20 +48,22 @@ function feedIcon(feed) {
47
48
  return IconPlugConnected;
48
49
  if (feed.kind === 'tool')
49
50
  return toolTypeIcon(of);
50
- if (feed.kind === 'branch')
51
+ if (feed.kind === 'branch' || feed.kind === 'stream')
51
52
  return IconStack2;
52
53
  if (feed.kind === 'subagent')
53
54
  return profileTypeIcon(of) ?? AGENT_ICON;
54
- if (feed.kind === 'tool-runner')
55
+ if (feed.kind === 'tool-runtime')
55
56
  return IconSettingsAutomation;
57
+ if (feed.kind === 'guardrail-runtime')
58
+ return IconShieldCheck;
59
+ if (feed.kind === 'destinations')
60
+ return IconDatabase;
61
+ if (feed.kind === 'vault')
62
+ return IconVault;
63
+ if (feed.kind === 'model')
64
+ return IconCpu;
56
65
  return CHECK_ICONS[of] ?? IconShieldCheck;
57
66
  }
58
- /** Right from `from` to `rail`, up or down it, and back left to `to`, its corners turned. */
59
- function sidePath(from, to, rail) {
60
- const up = Math.sign(to.y - from.y);
61
- return (`M ${from.x} ${from.y} H ${rail - TURN} Q ${rail} ${from.y} ${rail} ${from.y + up * TURN}` +
62
- ` V ${to.y - up * TURN} Q ${rail} ${to.y} ${rail - TURN} ${to.y} H ${to.x}`);
63
- }
64
67
  /** Down from under the parent's icon, then a turn across to the child. */
65
68
  function linePath(from, to) {
66
69
  const x = from.x + TRUNK;
@@ -77,9 +80,28 @@ function underPath(from, to) {
77
80
  return (`M ${from.x + TRUNK} ${from.y + HALF} V ${across} H ${trunk} V ${to.y - TURN}` +
78
81
  ` Q ${trunk} ${to.y} ${trunk + TURN} ${to.y} H ${to.x - LINE_GAP}`);
79
82
  }
80
- const clamp = (value) => Math.min(Math.max(value, 0), 1);
81
83
  const NONE = { x: 0, y: 0 };
82
- export function StudioTreeMap({ workspace, agents, grow = 0, fit, draggable = false, }) {
84
+ /**
85
+ * How large the map stands, and where in `room`, this far settled: closed and grown alike it fits
86
+ * the box it stands in, never larger than itself, in the middle of it, easing from the one to the
87
+ * other. Closed at given places, which are its own pixels, it stands at its own size from the top left.
88
+ */
89
+ function frameAt(closed, grown, room, settled, given) {
90
+ const fitting = (size) => room ? Math.min(1, room.width / size.width, room.height / size.height) : 1;
91
+ const middle = (size) => room
92
+ ? {
93
+ x: Math.max(0, room.width - size.width * fitting(size)) / 2,
94
+ y: Math.max(0, room.height - size.height * fitting(size)) / 2,
95
+ }
96
+ : NONE;
97
+ const closedScale = given ? 1 : fitting(closed);
98
+ const [from, to] = [given ? NONE : middle(closed), middle(grown)];
99
+ return {
100
+ scale: closedScale + (fitting(grown) - closedScale) * settled,
101
+ centre: { x: from.x + (to.x - from.x) * settled, y: from.y + (to.y - from.y) * settled },
102
+ };
103
+ }
104
+ export function StudioTreeMap({ workspace, agents, grow = 0, fit, room = fit, still, closed: given, draggable = false, }) {
83
105
  const root = useRef(null);
84
106
  const [moved, setMoved] = useState({});
85
107
  const [field, setField] = useState(NONE);
@@ -87,54 +109,157 @@ export function StudioTreeMap({ workspace, agents, grow = 0, fit, draggable = fa
87
109
  // why: Every token is drawn from the start, the undrawn ones unseen, so each is measured before it shows.
88
110
  const widths = useTokenWidths(root, workspace);
89
111
  const { width: fitWidth, height: fitHeight } = fit ?? { width: 0, height: 0 };
90
- const layout = useMemo(() => treeMapLayout(workspace, agents, (id) => widths[id] ?? GUESS, fitWidth > 0 && fitHeight > 0 ? { width: fitWidth, height: fitHeight } : undefined), [workspace, agents, widths, fitWidth, fitHeight]);
112
+ const layout = useMemo(() => treeMapLayout(workspace, agents, (id) => widths[id] ?? GUESS, fitWidth > 0 && fitHeight > 0 ? { width: fitWidth, height: fitHeight } : undefined, still, given), [workspace, agents, widths, fitWidth, fitHeight, still, given]);
91
113
  const width = (id) => widths[id] ?? GUESS;
92
114
  const drafts = new Map(workspace.agents.map((agent) => [
93
115
  agent.key,
94
116
  libraryDraft(workspace, agent.key) ?? createBlankDraft(),
95
117
  ]));
96
- const { rows, feeds } = layout;
118
+ const { rows, feeds, leaving } = layout;
119
+ const gone = goneAt(grow);
97
120
  const at = (id) => {
98
121
  const place = placeAt(layout, id, grow);
99
122
  const by = moved[id] ?? NONE;
100
123
  return { ...place, x: place.x + by.x, y: place.y + by.y };
101
124
  };
125
+ // why: What growing adds draws as the growing goes, and goes back as it comes back: no clock of its own.
102
126
  const drawn = (id) => drawnAt(layout, id, grow);
103
- const feedById = new Map(feeds.map((feed) => [feed.id, feed]));
104
- /** A line draws in its first half of the later of its two ends drawing, its token in the second. */
105
- const lineDrawn = (...ids) => {
106
- const parts = ids.flatMap((id) => drawn(id) ?? []);
107
- if (parts.length === 0)
108
- return {};
109
- const part = Math.min(...parts);
110
- return { pathLength: 1, style: { strokeDasharray: 1, strokeDashoffset: 1 - clamp(part * 2) } };
127
+ const on = (id) => (drawn(id) ?? 1) > 0;
128
+ /**
129
+ * A line drawing runs out from its source at the drawing's pace, as a pen draws it, the loader's
130
+ * map's way: the length of it drawn so far shows, and the rest is still to come.
131
+ */
132
+ const lineDrawn = (source, target) => {
133
+ const run = lineAt(layout, source, target, grow);
134
+ if (run <= 0)
135
+ return undefined;
136
+ if (run >= 1)
137
+ return { className: 'map-line' };
138
+ return {
139
+ className: 'map-line',
140
+ pathLength: 1,
141
+ style: { strokeDasharray: 1, strokeDashoffset: 1 - run },
142
+ };
143
+ };
144
+ /** A token, as solid as it has shown since the drawing reached it. */
145
+ const tokenDrawn = (id) => {
146
+ const out = drawn(id) ?? 1;
147
+ const zIndex = atRest(layout, id, grow) ? 2 : 1;
148
+ return { style: out < 1 ? { opacity: settling(out), zIndex } : { zIndex } };
111
149
  };
112
- const shown = feeds.filter((feed) => (drawn(feed.id) ?? 1) > 0);
150
+ const shown = feeds.filter((feed) => on(feed.id));
113
151
  const right = (id) => at(id).x + width(id);
114
- /** Right of every feed in its column from `to` to `feed`, where each stands now. */
115
- const railFor = (feed, to) => {
116
- const [top, bottom] = [at(to.id).y, at(feed.id).y].sort((a, b) => a - b);
117
- const lying = shown.filter((each) => {
118
- const { y } = at(each.id);
119
- return each.column === feed.column && y >= (top ?? 0) && y <= (bottom ?? 0);
120
- });
121
- return Math.max(...lying.map((each) => right(each.id))) + RAIL;
152
+ // why: A model wears its family's mark, the Gemini sparkle or the provider's, as the editor's tree does.
153
+ const iconOf = (feed) => (feed.kind === 'model' && feed.of !== undefined && modelFamilyMark(feed.of)) || feedIcon(feed);
154
+ /**
155
+ * The rail the lines from a source to feeds in its own column run down, right of it: clear of every
156
+ * token they pass, and one for all of them, so they leave the source as one line.
157
+ */
158
+ const railOf = (source) => {
159
+ const ends = feeds.filter((feed) => feed.from.includes(source) && beside(layout, source, feed));
160
+ const ys = [at(source).y, ...ends.map((feed) => at(feed.id).y)];
161
+ const [top, bottom] = [Math.min(...ys), Math.max(...ys)];
162
+ const column = ends[0]?.column;
163
+ const passed = feeds
164
+ .filter((each) => each.column === column)
165
+ .filter((each) => at(each.id).y >= top && at(each.id).y <= bottom)
166
+ .map((each) => right(each.id) + LINE_GAP);
167
+ return Math.max(right(source) + LINE_GAP, ...passed) + BOW;
122
168
  };
123
- const joins = shown.flatMap((feed) => {
124
- const to = feed.joins === undefined ? undefined : feedById.get(feed.joins);
125
- return to && (drawn(to.id) ?? 1) > 0 ? [{ feed, to }] : [];
169
+ // why: Closed, a row's lines run down one trunk to what it shows; grown, each runs down its feed's
170
+ // lane like the tree's, its steps aside growing out of its row.
171
+ const square = settledAt(grow);
172
+ const toward = (from, to, at) => ({
173
+ x: from.x + (to.x - from.x) * at,
174
+ y: from.y + (to.y - from.y) * at,
126
175
  });
127
- /** A join to a feed further right runs across to it; to one in its own column, round their right. */
128
- const joinPath = (feed, to) => {
129
- const from = { x: right(feed.id) + LINE_GAP, y: at(feed.id).y };
130
- if (to.column > feed.column) {
131
- return mapCurve(from, { x: at(to.id).x - LINE_GAP, y: at(to.id).y }).d;
132
- }
133
- return sidePath(from, { x: right(to.id) + LINE_GAP, y: at(to.id).y }, railFor(feed, to));
176
+ const lines = feeds.flatMap((feed) => {
177
+ return [...feed.from, ...(feed.late ?? [])].flatMap((source) => {
178
+ if (!on(source) || lineAt(layout, source, feed.id, grow) <= 0)
179
+ return [];
180
+ const end = at(feed.id);
181
+ const start = { x: right(source) + LINE_GAP, y: at(source).y };
182
+ if (beside(layout, source, feed)) {
183
+ const into = { x: end.x + width(feed.id) + LINE_GAP, y: end.y };
184
+ return [
185
+ {
186
+ key: lineKey(source, feed.id),
187
+ d: mapBracket(start, into, railOf(source), TURN),
188
+ target: feed.id,
189
+ source,
190
+ },
191
+ ];
192
+ }
193
+ const lane = layout.lanes.get(lineKey(source, feed.id));
194
+ const grownRail = lane ? at(lane.of).x - lane.by : (start.x + end.x) / 2;
195
+ const trunk = layout.trunks.get(source);
196
+ const steps = (layout.jogs.get(lineKey(source, feed.id)) ?? []).map((jog) => ({
197
+ x: at(jog.lane.of).x - jog.lane.by,
198
+ y: at(jog.near).y + (jog.side * layout.row) / 2,
199
+ }));
200
+ const into = { x: end.x - LINE_GAP, y: end.y };
201
+ const d = trunk === undefined
202
+ ? mapRoute(start, into, steps, grownRail, TURN, square)
203
+ : mapRoute(start, into, steps.map((step) => toward(start, step, square)), trunk + (grownRail - trunk) * square, TURN, 1);
204
+ return [{ key: lineKey(source, feed.id), d, source, target: feed.id }];
205
+ });
206
+ });
207
+ const agentsOf = new Set(layout.becomes.values());
208
+ /**
209
+ * The line from an agent tool to the agent of the still it grows into. why: Closed, the agent
210
+ * stands as its own in the tree, so the line leaves the tool's right, runs down right of what
211
+ * stands between, and comes back into the agent's right; grown, it is the line from under the
212
+ * tool's icon into the agent in its slot. Each corner eases from the one to the other.
213
+ */
214
+ const backPath = (holder, agent) => {
215
+ const [tool, to] = [at(holder), at(agent)];
216
+ const [top, bottom] = [Math.min(tool.y, to.y), Math.max(tool.y, to.y)];
217
+ const passed = [...shown.map((feed) => feed.id), ...leaving.feeds.map((feed) => feed.id)]
218
+ .filter((id) => at(id).y >= top && at(id).y <= bottom)
219
+ .map((id) => right(id) + LINE_GAP);
220
+ const rail = Math.max(right(holder) + LINE_GAP, right(agent) + LINE_GAP, ...passed) + BOW;
221
+ const closed = [
222
+ { x: right(holder) + LINE_GAP, y: tool.y },
223
+ { x: rail, y: tool.y },
224
+ { x: rail, y: (tool.y + to.y) / 2 },
225
+ { x: rail, y: to.y },
226
+ { x: right(agent) + LINE_GAP, y: to.y },
227
+ ];
228
+ const trunk = to.x - INDENT + TRUNK;
229
+ const across = tool.y + HALF + LINE_GAP;
230
+ const grown = [
231
+ { x: tool.x + TRUNK, y: tool.y + HALF },
232
+ { x: tool.x + TRUNK, y: across },
233
+ { x: trunk, y: across },
234
+ { x: trunk, y: to.y },
235
+ { x: to.x - LINE_GAP, y: to.y },
236
+ ];
237
+ return mapPolyline(closed.map((point, index) => toward(point, grown[index] ?? point, square)), TURN);
134
238
  };
135
- const places = [...rows.map(rowId), ...shown.map((feed) => feed.id)];
136
- const rails = joins.flatMap(({ feed, to }) => to.column === feed.column ? [railFor(feed, to)] : []);
137
- const mapWidth = Math.max(GUESS, ...places.map(right), ...rails) + PAD;
239
+ // What growing takes away goes under the rest: a row folding into its agent, a feed drifting off.
240
+ const going = gone < 1;
241
+ const leavingStyle = { opacity: 1 - gone, zIndex: 0 };
242
+ const parentOf = (id) => layout.becomes.get(id) ?? id;
243
+ const leavingLines = going
244
+ ? leaving.lines.map(({ source, feed }) => {
245
+ const end = at(feed);
246
+ const start = { x: right(source) + LINE_GAP, y: at(source).y };
247
+ const rail = layout.trunks.get(source) ?? (start.x + end.x) / 2;
248
+ return {
249
+ key: lineKey(source, feed),
250
+ d: mapRoute(start, { x: end.x - LINE_GAP, y: end.y }, [], rail, TURN, 1),
251
+ };
252
+ })
253
+ : [];
254
+ // why: What growing takes away holds the map open until it has gone.
255
+ const places = [
256
+ ...rows.map(rowId),
257
+ ...shown.map((feed) => feed.id),
258
+ ...(going ? [...leaving.rows.map(rowId), ...leaving.feeds.map((feed) => feed.id)] : []),
259
+ ];
260
+ // why: An agent tool's line back to its agent runs down right of the feeds.
261
+ const back = layout.becomes.size > 0 ? LINE_GAP + BOW : 0;
262
+ const mapWidth = Math.max(GUESS, ...places.map(right)) + back + PAD;
138
263
  const height = Math.max(...places.map((id) => at(id).y)) + HALF + PAD;
139
264
  const grown = useMemo(() => {
140
265
  const ids = [...layout.rows.map(rowId), ...layout.feeds.map((feed) => feed.id)];
@@ -144,15 +269,17 @@ export function StudioTreeMap({ workspace, agents, grow = 0, fit, draggable = fa
144
269
  height: Math.max(...ends.map((end) => end.y)) + HALF + PAD,
145
270
  };
146
271
  }, [layout, widths]);
147
- const fits = fit ? Math.min(1, fit.width / grown.width, fit.height / grown.height) : 1;
148
- const scale = 1 + (fits - 1) * clamp(grow);
149
- // why: Grown, the map stands in the middle of the box it fills; closed, at its top left.
150
- const centre = fit
151
- ? {
152
- x: (Math.max(0, fit.width - grown.width * fits) / 2) * clamp(grow),
153
- y: (Math.max(0, fit.height - grown.height * fits) / 2) * clamp(grow),
154
- }
155
- : NONE;
272
+ // why: The map eases to the size of the box it stands in with its nodes' move, so it never
273
+ // outgrows it, and grows with it after.
274
+ const closedSize = useMemo(() => {
275
+ const ends = [...layout.closed];
276
+ return {
277
+ width: Math.max(...ends.map(([id, end]) => end.x + (widths[id] ?? GUESS))) + back + PAD,
278
+ height: Math.max(...ends.map(([, end]) => end.y)) + HALF + PAD,
279
+ };
280
+ }, [layout, widths, back]);
281
+ const settled = settledAt(grow);
282
+ const { scale, centre } = frameAt(closedSize, grown, room, settled, given !== undefined);
156
283
  // why: The pointer moves in the page's pixels; the map is scaled, so a move is divided back into its own.
157
284
  const drag = (event) => {
158
285
  if (!draggable || !drags(event))
@@ -177,24 +304,37 @@ export function StudioTreeMap({ workspace, agents, grow = 0, fit, draggable = fa
177
304
  });
178
305
  };
179
306
  const shift = { x: field.x + centre.x, y: field.y + centre.y };
307
+ const rowToken = (row, look) => {
308
+ const draft = drafts.get(row.agent) ?? createBlankDraft();
309
+ const { x, y } = at(rowId(row));
310
+ return (_jsx(MapToken, { id: rowId(row), label: row.node.label, icon: nodeIcon(draft, row.node.ref), x: x, y: y, ...look }, rowId(row)));
311
+ };
312
+ /** A feed's token, `drift` right of where it stands. */
313
+ const feedToken = (feed, look, drift = 0) => {
314
+ const { x, y } = at(feed.id);
315
+ return (_jsx(MapToken, { id: feed.id, label: feed.label, icon: iconOf(feed), x: x + drift, y: y, ...look }, feed.id));
316
+ };
180
317
  const translate = shift.x || shift.y ? { translate: `${shift.x}px ${shift.y}px` } : {};
181
318
  return (_jsxs("div", { ref: root, className: "studio-tree-map", style: {
182
319
  width: mapWidth,
183
320
  height,
184
321
  ...translate,
185
322
  ...(scale < 1 ? { scale: String(scale) } : {}),
186
- }, "data-draggable": draggable ? '' : undefined, "data-dragging": dragging ? '' : undefined, onPointerDown: drag, children: [_jsxs("svg", { className: "studio-tree-map-lines", width: mapWidth, height: height, "aria-hidden": "true", children: [rows.map((row) => row.parent && (_jsx("path", { className: "map-line", d: linePath(at(row.parent), at(rowId(row))) }, rowId(row)))), shown.flatMap((feed) => feed.into.map((id) => {
187
- const from = at(feed.id);
188
- const { d } = mapCurve({ x: from.x - LINE_GAP, y: from.y }, { x: right(id) + LINE_GAP, y: at(id).y });
189
- return _jsx("path", { className: "map-line", d: d }, `${feed.id}>${id}`);
190
- })), shown.map((feed) => feed.under !== undefined && (_jsx("path", { className: "map-line", d: underPath(at(feed.under), at(feed.id)), ...lineDrawn(feed.id) }, `${feed.id}^`))), joins.map(({ feed, to }) => (_jsx("path", { className: "map-line", d: joinPath(feed, to), ...lineDrawn(feed.id, to.id) }, `${feed.id}~${to.id}`)))] }), rows.map((row) => {
191
- const draft = drafts.get(row.agent) ?? createBlankDraft();
192
- const { x, y } = at(rowId(row));
193
- return (_jsx(MapToken, { id: rowId(row), label: row.node.label, icon: nodeIcon(draft, row.node.ref), x: x, y: y }, rowId(row)));
194
- }), feeds.map((feed) => {
195
- const { x, y } = at(feed.id);
196
- const part = drawn(feed.id);
197
- const unseen = part === 0 ? { visibility: 'hidden' } : {};
198
- return (_jsx(MapToken, { id: feed.id, label: feed.label, icon: feedIcon(feed), x: x, y: y, ...(part === undefined ? {} : { style: { ...unseen, opacity: clamp(part * 2 - 1) } }) }, feed.id));
199
- })] }));
323
+ }, "data-draggable": draggable ? '' : undefined, "data-dragging": dragging ? '' : undefined, onPointerDown: drag, children: [_jsxs("svg", { className: "studio-tree-map-lines", width: mapWidth, height: height, "aria-hidden": "true", children: [rows.map((row) => {
324
+ const drawing = row.parent !== undefined && lineDrawn(row.parent, rowId(row));
325
+ return (row.parent &&
326
+ drawing && (_jsx("path", { d: linePath(at(row.parent), at(rowId(row))), ...drawing }, rowId(row))));
327
+ }), going &&
328
+ leaving.rows.map((row) => row.parent && (_jsx("path", { className: "map-line", d: linePath(at(parentOf(row.parent)), at(rowId(row))), style: leavingStyle }, rowId(row)))), leavingLines.map(({ key, d }) => (_jsx("path", { className: "map-line", d: d, style: leavingStyle }, key))), lines.map(({ key, d, source, target }) => {
329
+ const drawing = lineDrawn(source, target);
330
+ return drawing && _jsx("path", { d: d, ...drawing }, key);
331
+ }), feeds.map((feed) => {
332
+ const holder = feed.under;
333
+ const drawing = holder !== undefined && on(holder) && lineDrawn(holder, feed.id);
334
+ return (holder !== undefined &&
335
+ drawing && (_jsx("path", { d: agentsOf.has(feed.id)
336
+ ? backPath(holder, feed.id)
337
+ : underPath(at(holder), at(feed.id)), ...drawing }, underKey(feed.id))));
338
+ })] }), settled > 0 &&
339
+ layout.headings.map(({ label, note, x, y }) => (_jsxs("div", { className: "studio-tree-map-heading", style: { left: x, top: y, opacity: settled }, children: [_jsx(Text, { type: "supporting", color: "secondary", weight: "semibold", children: label }), _jsx(Text, { type: "supporting", color: "placeholder", children: note })] }, label))), rows.map((row) => rowToken(row, tokenDrawn(rowId(row)))), going && leaving.rows.map((row) => rowToken(row, { style: leavingStyle })), going && leaving.feeds.map((feed) => feedToken(feed, { style: leavingStyle }, DRIFT * gone)), feeds.map((feed) => feedToken(feed, tokenDrawn(feed.id)))] }));
200
340
  }