@theoremjs/studio 0.3.4-next.474.gdb1d5d7 → 0.3.4-next.482.g7e608f2

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 (50) hide show
  1. package/README.md +1 -1
  2. package/draft.js +1 -1
  3. package/package.json +4 -4
  4. package/page/assets/{Banner-TdHtbVjU.js → Banner-CrUhb26_.js} +2 -2
  5. package/page/assets/{ConsoleFrame-DgjR7mxh.js → ConsoleFrame-6cEEvO4i.js} +1 -1
  6. package/page/assets/{ContextMenu-D-6K0Fs2.js → ContextMenu-WnsgC14_.js} +2 -2
  7. package/page/assets/{ToolResultCharts-0ddlwgFH.js → ToolResultCharts-BEhuo0Z4.js} +1 -1
  8. package/page/assets/{TraceInspector-egLHbVIj.js → TraceInspector-k1w1q0XO.js} +1 -1
  9. package/page/assets/{TraceOverview-CRTN7VoT.js → TraceOverview-Crxd-sTS.js} +1 -1
  10. package/page/assets/{_virtual_studio-type-sources-DdSjKnMh.js → _virtual_studio-type-sources-DAabvYbE.js} +16 -6
  11. package/page/assets/{browser-engine-tXDx3yU4.js → browser-engine-C78edv3x.js} +7 -7
  12. package/page/assets/{browser-transport-ffGDz5MD.js → browser-transport-mGxdvtBL.js} +4 -4
  13. package/page/assets/{crypto-C5mfRMPv.js → crypto-iT_LZ8Et.js} +1 -1
  14. package/page/assets/encode-files-8AFMw7Er.js +1 -0
  15. package/page/assets/{export-agent-Y58EdieU.js → export-agent-BedkRp13.js} +1 -1
  16. package/page/assets/{guardrail-tester-CDawxhDM.js → guardrail-tester-XwnZWNsg.js} +1 -1
  17. package/page/assets/{index-DtLCmecg.js → index-BVHT-9mR.js} +9 -9
  18. package/page/assets/{live-messages-CcUq850L.js → live-messages-B65rUlNM.js} +1 -1
  19. package/page/assets/{reader-DkhgU7-Z.js → reader-XebgyCzF.js} +4 -4
  20. package/page/assets/{shaped-data-Hq2l-bCM.js → shaped-data-Bc6u4pow.js} +2 -2
  21. package/page/assets/{stub-9noS56tk.js → stub-DG5wxM-A.js} +1 -1
  22. package/page/assets/{studio-code-pane-CTGeGFc3.js → studio-code-pane-p4fjtQhL.js} +3 -3
  23. package/page/assets/{studio-console-bsXe3dk3.js → studio-console-BtZY4aUM.js} +1 -1
  24. package/page/assets/{studio-decision-CEcqOCMb.js → studio-decision-BovTBIwV.js} +1 -1
  25. package/page/assets/{studio-embedding-C1KeE3S4.js → studio-embedding-BY6UQk0k.js} +1 -1
  26. package/page/assets/{studio-host-console-mg-gOPJs.js → studio-host-console-D9NFWXxV.js} +2 -2
  27. package/page/assets/{studio-labels-DyUKoK3A.js → studio-labels-C3yQoEd0.js} +1 -1
  28. package/page/assets/{studio-live-D4qKxLHi.js → studio-live-B_cVDiqs.js} +1 -1
  29. package/page/assets/{studio-map-CsQoRSdx.js → studio-map-iN6-MOzK.js} +1 -1
  30. package/page/assets/studio-trace-replay-B3k21TSg.js +2 -0
  31. package/page/assets/{studio-type-checker-Ch0YO2TO.js → studio-type-checker-Bljo3amv.js} +1 -1
  32. package/page/assets/{tool-schema-DUAd6AXZ.js → tool-schema-BmScISma.js} +8 -8
  33. package/page/assets/{trace-record-Dfx7WS9n.js → trace-record-D4xAUHLZ.js} +1 -1
  34. package/page/assets/{tree-q4zfZBm3.js → tree-BiZScKs7.js} +2 -2
  35. package/page/assets/{turn-client-BHueOGtF.js → turn-client-CBWNoFgo.js} +2 -2
  36. package/page/assets/types-zRlFhC9U.js +1 -0
  37. package/page/assets/{use-one-call-BF7eEAPL.js → use-one-call-rh-5EjBB.js} +1 -1
  38. package/page/assets/{workspace-D8wB8WWL.js → workspace-kAEf4Zn4.js} +1 -1
  39. package/page/index.html +15 -15
  40. package/policy.js +2 -2
  41. package/replay.js +2 -3
  42. package/server/draft-decide.d.ts +4 -1
  43. package/server/draft-decide.js +1 -1
  44. package/ui/lib/tree-map-layout.d.ts +38 -14
  45. package/ui/lib/tree-map-layout.js +210 -142
  46. package/ui/tree-map.d.ts +10 -9
  47. package/ui/tree-map.js +60 -40
  48. package/page/assets/encode-files-CZXrfYAp.js +0 -1
  49. package/page/assets/studio-trace-replay-aP7TzJuO.js +0 -2
  50. package/page/assets/types-CbxtWiCN.js +0 -1
@@ -7,27 +7,24 @@
7
7
  * stands where it is.
8
8
  */
9
9
  import { workspaceTree } from "../../mod.js";
10
- /** A row's height closed, and grown. */
10
+ /** A row's height closed; grown, the least and the most it stretches to, to fill the box it fits. */
11
11
  const ROW = 32;
12
12
  const GROWN_ROW = 28;
13
+ const TALLEST_ROW = 40;
13
14
  /** How far in each level of the tree starts. */
14
15
  export const INDENT = 28;
15
16
  /** A token's half height. */
16
17
  export const HALF = 11;
17
18
  export const PAD = 16;
18
- /** Between the tree's widest row and the feeds. */
19
+ /** Closed, between the tree's widest row and the feeds; grown, the least and most between columns. */
19
20
  const GAP = 72;
20
- /** How many of the registry's tools sit under it closed: one of each type first. */
21
- const SHOWN_TOOLS = 4;
21
+ const LEAST_GAP = 56;
22
+ const MOST_GAP = 160;
23
+ /** The tools beside a Tools row: the first of each type. */
22
24
  const TOOL_TYPES = ['function', 'http', 'mcp', 'agent'];
23
- /** Grown, between one agent's zone and the next. */
24
- const AGENT_GAP = 16;
25
- /** Grown, a turn's steps in rows under its runner, so many to a row. */
26
- const STEPS_ACROSS = 3;
27
- const STEP_SLOT = 160;
28
- /** Grown, the registry's tools in columns this tall. */
29
- const TOOLS_DOWN = 8;
30
- const TOOL_SLOT = 210;
25
+ /** Grown, the runner's steps two across, down one column and then the next. */
26
+ const STEPS_DOWN = 4;
27
+ const STEP_GAP = 24;
31
28
  /** By how far grown every node has reached its zone; the rest is for drawing in. */
32
29
  const MOVE_TO = 0.7;
33
30
  /** Over what stretch of growing the added nodes draw, and how long each takes of it. */
@@ -38,25 +35,33 @@ const DRAW_SPAN = 0.1;
38
35
  const SAMPLES = 100;
39
36
  /** Space kept clear round a token. */
40
37
  const CLEAR = 2;
41
- export const REGISTRY = 'tool-registry';
38
+ export const RUNNER = 'runner';
39
+ export const TOOL_RUNNER = 'tool-runner';
42
40
  /**
43
- * What a turn runner does in one turn, in order.
44
- * why: The kernel contract's turn lifecycle (docs/contracts/kernel.md, "Turn lifecycle"), step for
45
- * step; a decision and a session have no such list there, so their runners open into nothing.
41
+ * What the runner does in a turn, in order: the steps of the kernel contract's turn lifecycle
42
+ * (docs/contracts/kernel.md, "Turn lifecycle") that do work.
43
+ * why: The canary bind runs only while a canary detector is on, and pre_turn, before_end and
44
+ * post_turn are hooks for the host's stages, so the map leaves those four out.
46
45
  */
47
46
  export const TURN_STEPS = [
48
47
  { id: 'resolve', label: 'Resolve' },
49
48
  { id: 'sanitize', label: 'Sanitize' },
50
49
  { id: 'compaction', label: 'Compaction' },
51
- { id: 'canary', label: 'Canary bind' },
52
- { id: 'pre_turn', label: 'pre_turn' },
53
50
  { id: 'stream', label: 'Provider stream' },
54
51
  { id: 'tools', label: 'Tool loop' },
55
- { id: 'before_end', label: 'before_end' },
56
52
  { id: 'validation', label: 'Validation' },
57
53
  { id: 'egress', label: 'Egress' },
58
54
  { id: 'trace', label: 'Trace' },
59
- { id: 'post_turn', label: 'post_turn' },
55
+ ];
56
+ /**
57
+ * What the tool runner checks of every call, in order (`executeRegisteredTool`): its permission,
58
+ * its sign-in, the network a remote call reaches, and the guard on its result.
59
+ */
60
+ export const TOOL_CHECKS = [
61
+ { id: 'permission', label: 'Permission gate' },
62
+ { id: 'sign-in', label: 'Sign-in' },
63
+ { id: 'network', label: 'Network check' },
64
+ { id: 'result', label: 'Result guard' },
60
65
  ];
61
66
  /** A row's token id: the tree's ids repeat from one agent to the next. */
62
67
  export function rowId(row) {
@@ -84,144 +89,195 @@ function rowsOf(workspace, only) {
84
89
  return rows;
85
90
  }
86
91
  /**
87
- * What runs a profile of this type: `runDecision`, `runSession`, `runEmbedding`, else `runTurn`. A
88
- * host has none of its own: the tool runner runs its calls.
89
- */
90
- function runnerLabel(type) {
91
- if (type === 'host')
92
- return undefined;
93
- if (type === 'decision')
94
- return 'Decision runner';
95
- if (type === 'live')
96
- return 'Session runner';
97
- if (type === 'embedding')
98
- return 'Embedding runner';
99
- return 'Turn runner';
100
- }
101
- /**
102
- * Each agent's runner (`runTurn`, or `runDecision` for a decision), beside its name; its providers,
103
- * beside its models; and the workspace's tool registry, beside its Tools row, with the tool runner
104
- * under it. A feed sits level with the middle of the rows it joins, or under the feed before it when
105
- * that is lower. With `detail`, every tool and each turn runner's steps.
92
+ * Beside the tree, shared by every agent drawn: the providers, each with the protocols the models
93
+ * it serves speak, and a line into each of those models; the first tool of each type the agents
94
+ * allow, with a line into each Tools row that allows it, an MCP tool's sessions under it and an
95
+ * agent tool's agent under it, joined to the runner; and the one runner, with a line into each
96
+ * agent but a host (whose calls the tool runner runs) and its steps under it. Past them, the tool
97
+ * runner the tool loop calls, its checks under it.
106
98
  */
107
- function feedsOf(workspace, rows, detail) {
99
+ function feedsOf(workspace, rows) {
100
+ const drawn = workspace.agents.filter((agent) => rows.some((row) => row.agent === agent.key));
101
+ const rowOf = (agent, facet, key) => rows.find((row) => row.agent === agent &&
102
+ row.node.ref.facet === facet &&
103
+ (key === undefined || ('key' in row.node.ref && row.node.ref.key === key)));
108
104
  const feeds = [];
109
- const add = (feed, into = [], depth = 0) => {
110
- const below = (feeds.at(-1)?.y ?? Number.NEGATIVE_INFINITY) + ROW;
111
- const middle = into.length ? into.reduce((sum, row) => sum + row.y, 0) / into.length : below;
112
- feeds.push({ ...feed, y: Math.max(middle, below), into: into.map(rowId), depth });
113
- };
114
- for (const agent of workspace.agents) {
115
- const mine = rows.filter((row) => row.agent === agent.key);
116
- const name = mine.find((row) => row.node.ref.facet === 'identity');
117
- if (!name)
118
- continue;
119
- const runner = runnerLabel(agent.identity.profileType);
120
- if (runner) {
121
- const id = `${agent.key}/runner`;
122
- add({ id, label: runner, kind: 'runner', agent: agent.key }, [name]);
123
- const steps = detail && runner === 'Turn runner' ? TURN_STEPS : [];
124
- for (const step of steps) {
125
- const of = step.id;
126
- add({ id: `${id}/${of}`, label: step.label, kind: 'step', agent: agent.key, of, under: id }, [], 1);
127
- }
128
- }
129
- const providers = new Map();
105
+ const providers = new Map();
106
+ for (const agent of drawn) {
130
107
  for (const binding of agent.modelBindings) {
131
- const row = mine.find((each) => each.node.ref.facet === 'modelBinding' && each.node.ref.key === binding.key);
132
- if (row)
133
- providers.set(binding.provider, [...(providers.get(binding.provider) ?? []), row]);
108
+ const row = rowOf(agent.key, 'modelBinding', binding.key);
109
+ if (!row)
110
+ continue;
111
+ const provider = providers.get(binding.provider) ?? { into: [], protocols: new Set() };
112
+ provider.into.push(rowId(row));
113
+ provider.protocols.add(binding.protocol);
114
+ providers.set(binding.provider, provider);
134
115
  }
135
- for (const [provider, into] of providers) {
136
- const id = `${agent.key}/provider:${provider}`;
137
- add({ id, label: provider, kind: 'provider', agent: agent.key }, into);
116
+ }
117
+ for (const [provider, { into, protocols }] of providers) {
118
+ const id = `provider:${provider}`;
119
+ feeds.push({ id, label: provider, kind: 'provider', into, column: 1, group: 'providers' });
120
+ for (const protocol of protocols) {
121
+ const nested = { id: `${id}/${protocol}`, label: protocol, kind: 'protocol' };
122
+ feeds.push({ ...nested, into: [], under: id, column: 1, group: 'providers' });
138
123
  }
139
- // The workspace's tools are one registry that each agent's tool policy picks from: drawn once,
140
- // beside the first Tools row, with a line into every other.
141
- const tools = mine.find((row) => row.node.ref.facet === 'tools');
142
- const drawn = feeds.find((feed) => feed.id === REGISTRY);
143
- if (tools && drawn)
144
- drawn.into.push(rowId(tools));
145
- else if (tools && workspace.toolSpecs.length > 0) {
146
- add({ id: REGISTRY, label: 'Tool registry', kind: 'registry', agent: agent.key }, [tools]);
147
- for (const tool of listedTools(workspace.toolSpecs, detail)) {
148
- const id = `${REGISTRY}/${tool.key}`;
149
- const feed = { id, label: tool.toolName, kind: 'tool', agent: agent.key };
150
- add({ ...feed, of: tool.toolType, under: REGISTRY }, [], 1);
151
- }
152
- const runs = { id: 'tool-runner', label: 'Tool runner', kind: 'tool-runner' };
153
- add({ ...runs, agent: agent.key, under: REGISTRY });
124
+ }
125
+ const allowing = (key) => drawn.flatMap((agent) => {
126
+ const row = agent.tools.allow.includes(key) ? rowOf(agent.key, 'tools') : undefined;
127
+ return row ? [rowId(row)] : [];
128
+ });
129
+ for (const type of TOOL_TYPES) {
130
+ const tool = workspace.toolSpecs.find((each) => each.toolType === type && allowing(each.key).length > 0);
131
+ if (!tool)
132
+ continue;
133
+ const id = `tool:${tool.key}`;
134
+ const into = allowing(tool.key);
135
+ feeds.push({
136
+ id,
137
+ label: tool.toolName,
138
+ kind: 'tool',
139
+ of: type,
140
+ into,
141
+ column: 1,
142
+ group: 'tools',
143
+ });
144
+ const nested = { into: [], under: id, column: 1, group: 'tools' };
145
+ if (type === 'mcp') {
146
+ feeds.push({ ...nested, id: `${id}/sessions`, label: 'MCP sessions', kind: 'branch' });
147
+ }
148
+ const agent = workspace.agents.find((each) => each.key === tool.agentKey);
149
+ if (type === 'agent' && agent) {
150
+ const label = agent.identity.agentId || agent.identity.handle;
151
+ feeds.push({
152
+ ...nested,
153
+ id: `${id}/agent`,
154
+ label,
155
+ kind: 'subagent',
156
+ of: agent.identity.profileType,
157
+ joins: RUNNER,
158
+ });
154
159
  }
155
160
  }
156
- return feeds;
157
- }
158
- /**
159
- * The registry's tools: the first of each type, then the rest in the registry's order. A few of
160
- * them, or with `detail` every one.
161
- */
162
- function listedTools(tools, detail) {
163
- const firsts = TOOL_TYPES.flatMap((type) => {
164
- const tool = tools.find((each) => each.toolType === type);
165
- return tool ? [tool] : [];
161
+ const runs = drawn.filter((agent) => agent.identity.profileType !== 'host');
162
+ const into = runs.flatMap((agent) => {
163
+ const row = rowOf(agent.key, 'identity');
164
+ return row ? [rowId(row)] : [];
166
165
  });
167
- const rest = tools.filter((tool) => !firsts.includes(tool));
168
- const listed = [...firsts, ...rest];
169
- return detail ? listed : listed.slice(0, SHOWN_TOOLS);
166
+ feeds.push({ id: RUNNER, label: 'Runner', kind: 'runner', into, column: 1, group: 'runner' });
167
+ for (const step of TURN_STEPS) {
168
+ feeds.push({
169
+ id: `${RUNNER}/${step.id}`,
170
+ label: step.label,
171
+ kind: 'step',
172
+ of: step.id,
173
+ into: [],
174
+ under: RUNNER,
175
+ column: 1,
176
+ group: 'runner',
177
+ ...(step.id === 'tools' ? { joins: TOOL_RUNNER } : {}),
178
+ });
179
+ }
180
+ const runner = { id: TOOL_RUNNER, label: 'Tool runner', kind: 'tool-runner' };
181
+ feeds.push({ ...runner, into: [], column: 2, group: 'tool-runner' });
182
+ for (const check of TOOL_CHECKS) {
183
+ const nested = { id: `${TOOL_RUNNER}/${check.id}`, label: check.label, of: check.id };
184
+ feeds.push({
185
+ ...nested,
186
+ kind: 'check',
187
+ into: [],
188
+ under: TOOL_RUNNER,
189
+ column: 2,
190
+ group: 'tool-runner',
191
+ });
192
+ }
193
+ return feeds;
170
194
  }
171
195
  const clamp = (value) => Math.min(Math.max(value, 0), 1);
172
196
  const between = (from, to, at) => from + (to - from) * at;
173
- /** Every other one from each list, while both last: the turn's steps and the tools draw by turns. */
174
- function byTurns(first, second) {
175
- return Array.from({ length: Math.max(first.length, second.length) }, (_, index) => [first[index], second[index]].filter((each) => each !== undefined)).flat();
176
- }
177
- /** A step under its runner in rows so many across; a tool under the registry in columns. */
178
- function slotsOf(feeds) {
197
+ /** A step under the runner two across, down one column and then the next; any other a level in. */
198
+ function slotsOf(feeds, width) {
179
199
  const slots = new Map();
200
+ const steps = feeds.filter((feed) => feed.kind === 'step');
201
+ const stepColumn = Math.max(0, ...steps.map((step) => width(step.id))) + STEP_GAP;
180
202
  const counts = new Map();
181
203
  for (const feed of feeds) {
182
- if (feed.under === undefined || feed.depth === 0)
204
+ if (feed.under === undefined)
183
205
  continue;
184
206
  const index = counts.get(feed.under) ?? 0;
185
207
  counts.set(feed.under, index + 1);
186
- const slot = feed.kind === 'tool'
187
- ? { across: Math.floor(index / TOOLS_DOWN) * TOOL_SLOT, down: (index % TOOLS_DOWN) + 1 }
188
- : {
189
- across: (index % STEPS_ACROSS) * STEP_SLOT,
190
- down: Math.floor(index / STEPS_ACROSS) + 1,
191
- };
208
+ const slot = feed.kind === 'step'
209
+ ? { across: Math.floor(index / STEPS_DOWN) * stepColumn, down: (index % STEPS_DOWN) + 1 }
210
+ : { across: 0, down: index + 1 };
192
211
  slots.set(feed.id, { holder: feed.under, across: INDENT + slot.across, down: slot.down });
193
212
  }
194
213
  return slots;
195
214
  }
196
- /**
197
- * The column grown: each agent a zone of its own, its rows restacked and its feeds in their order,
198
- * each level with its rows or under the room the one before it holds for its own.
199
- */
200
- function grownColumn(rows, feeds, slots, feedX) {
201
- const places = new Map();
215
+ /** Each column's groups of heads, and how many rows each group holds with what is under its heads. */
216
+ function stacksOf(rows, feeds, slots) {
202
217
  const held = new Map();
203
218
  for (const slot of slots.values()) {
204
219
  held.set(slot.holder, Math.max(held.get(slot.holder) ?? 0, slot.down));
205
220
  }
206
- let top = PAD + HALF;
207
- for (const agent of new Set(rows.map((row) => row.agent))) {
208
- const mine = rows.filter((row) => row.agent === agent);
209
- mine.forEach((row, index) => {
210
- places.set(rowId(row), { x: row.x, y: top + index * GROWN_ROW });
211
- });
212
- let cursor = top;
221
+ const stack = (groups) => {
222
+ const ids = [...groups.values()];
223
+ const count = ids.flat().reduce((sum, id) => sum + 1 + (held.get(id) ?? 0), 0);
224
+ return { ids, rows: count };
225
+ };
226
+ const tree = new Map();
227
+ for (const row of rows)
228
+ tree.set(row.agent, [...(tree.get(row.agent) ?? []), rowId(row)]);
229
+ const columns = [1, 2].map((column) => {
230
+ const groups = new Map();
213
231
  for (const feed of feeds) {
214
- if (feed.agent !== agent || slots.has(feed.id))
232
+ if (feed.column !== column || feed.under !== undefined)
215
233
  continue;
216
- const level = feed.into.flatMap((id) => places.get(id)?.y ?? []);
217
- const middle = level.length ? level.reduce((sum, y) => sum + y, 0) / level.length : cursor;
218
- const y = Math.max(middle, cursor);
219
- places.set(feed.id, { x: feedX, y });
220
- cursor = y + GROWN_ROW * (1 + (held.get(feed.id) ?? 0));
234
+ groups.set(feed.group, [...(groups.get(feed.group) ?? []), feed.id]);
221
235
  }
222
- top = Math.max(top + mine.length * GROWN_ROW, cursor) + AGENT_GAP;
223
- }
224
- return places;
236
+ return stack(groups);
237
+ });
238
+ return { held, columns: [stack(tree), ...columns] };
239
+ }
240
+ /**
241
+ * The map grown: the tree, each agent a zone of its own; beside it the providers, the tools and the
242
+ * runner; past them the tool runner. Rows stretch, up to a most, and the gaps between columns and
243
+ * between groups widen, so the map fills `box`; without one, rows and gaps are at their least.
244
+ */
245
+ function grownPlaces(rows, feeds, slots, width, box) {
246
+ const { held, columns } = stacksOf(rows, feeds, slots);
247
+ // In rows, from the top node's middle to the bottom one's, each gap at its least of a row.
248
+ const span = Math.max(...columns.map((each) => each.rows + each.ids.length - 2));
249
+ const room = box ? box.height - 2 * (PAD + HALF) : 0;
250
+ const row = Math.min(Math.max(room / Math.max(span, 1), GROWN_ROW), TALLEST_ROW);
251
+ const height = Math.max(room, span * row);
252
+ const right = (id, x) => x + width(id);
253
+ const treeRight = Math.max(0, ...rows.map((each) => right(rowId(each), each.x)));
254
+ const columnWidth = (column) => Math.max(0, ...feeds
255
+ .filter((feed) => feed.column === column)
256
+ .map((feed) => {
257
+ const slot = slots.get(feed.id);
258
+ return right(feed.id, slot ? slot.across : 0);
259
+ }));
260
+ const [first, second] = [columnWidth(1), columnWidth(2)];
261
+ const spare = box ? (box.width - 2 * PAD - (treeRight - PAD) - first - second) / 2 : LEAST_GAP;
262
+ const gap = Math.min(Math.max(spare, LEAST_GAP), MOST_GAP);
263
+ const lefts = [0, treeRight + gap, treeRight + gap + first + gap];
264
+ const places = new Map();
265
+ const tree = new Map(rows.map((each) => [rowId(each), each]));
266
+ columns.forEach((stack, column) => {
267
+ const free = height - (stack.rows - 1) * row;
268
+ // Past a group, the next stands a row and its share of what is free further down.
269
+ const extra = stack.ids.length > 1 ? free / (stack.ids.length - 1) : 0;
270
+ // why: A column of one group has no gap to widen, so it stands in the middle of the map.
271
+ let y = PAD + HALF + (stack.ids.length > 1 ? 0 : free / 2);
272
+ for (const group of stack.ids) {
273
+ for (const id of group) {
274
+ places.set(id, { x: tree.get(id)?.x ?? lefts[column] ?? 0, y });
275
+ y += row * (1 + (held.get(id) ?? 0));
276
+ }
277
+ y += extra;
278
+ }
279
+ });
280
+ return { places, row };
225
281
  }
226
282
  /** Where a node stands this far grown: a node under a feed keeps its slot under it on the way. */
227
283
  export function placeAt(layout, id, grow) {
@@ -229,7 +285,8 @@ export function placeAt(layout, id, grow) {
229
285
  const slot = layout.slots.get(id);
230
286
  if (slot) {
231
287
  const holder = placeAt(layout, slot.holder, grow);
232
- return { x: holder.x + slot.across, y: holder.y + slot.down * between(ROW, GROWN_ROW, moved) };
288
+ const row = between(ROW, layout.row, moved);
289
+ return { x: holder.x + slot.across, y: holder.y + slot.down * row };
233
290
  }
234
291
  const to = layout.grown.get(id) ?? { x: 0, y: 0 };
235
292
  const from = layout.closed.get(id) ?? to;
@@ -257,29 +314,40 @@ function clearFrom(layout, id, ids, width) {
257
314
  }
258
315
  return 0;
259
316
  }
260
- /** The tree and what feeds it, closed and grown, for tokens this wide. */
261
- export function treeMapLayout(workspace, agents, width) {
317
+ /**
318
+ * The tree and what feeds it, closed and grown, for tokens this wide: grown, it fills `box` when
319
+ * one is given.
320
+ */
321
+ export function treeMapLayout(workspace, agents, width, box) {
262
322
  const rows = rowsOf(workspace, agents);
263
- const fewFeeds = feedsOf(workspace, rows, false);
323
+ const feeds = feedsOf(workspace, rows);
324
+ // Closed, the first column's heads stand beside the tree, each level with the first row it
325
+ // joins or under the one before.
326
+ const fewFeeds = feeds.filter((feed) => feed.column === 1 && feed.under === undefined);
264
327
  const few = new Set(fewFeeds.map((feed) => feed.id));
265
- const feeds = feedsOf(workspace, rows, true);
266
328
  const feedX = Math.max(0, ...rows.map((row) => row.x + width(rowId(row)))) + GAP;
267
329
  const closed = new Map(rows.map((row) => [rowId(row), row]));
268
- for (const feed of fewFeeds)
269
- closed.set(feed.id, { x: feedX + feed.depth * INDENT, y: feed.y });
270
- const slots = slotsOf(feeds);
330
+ const rowY = new Map(rows.map((row) => [rowId(row), row.y]));
331
+ let below = Number.NEGATIVE_INFINITY;
332
+ for (const feed of fewFeeds) {
333
+ const y = Math.max(rowY.get(feed.into[0] ?? '') ?? below, below);
334
+ closed.set(feed.id, { x: feedX, y });
335
+ below = y + ROW;
336
+ }
337
+ const slots = slotsOf(feeds, width);
338
+ const { places, row } = grownPlaces(rows, feeds, slots, width, box);
271
339
  const layout = {
272
340
  rows,
273
341
  feeds,
274
342
  few,
275
343
  closed,
276
- grown: grownColumn(rows, feeds, slots, feedX),
344
+ grown: places,
345
+ row,
277
346
  slots,
278
347
  starts: new Map(),
279
348
  };
280
- // What growing adds draws in order, the turn's steps and the tools by turns, each once it stands clear.
281
- const added = feeds.filter((feed) => !few.has(feed.id));
282
- const order = byTurns(added.filter((feed) => feed.under !== REGISTRY), added.filter((feed) => feed.under === REGISTRY));
349
+ // What growing adds draws in its columns' order, each once it stands clear.
350
+ const order = feeds.filter((feed) => !few.has(feed.id));
283
351
  const ids = [...rows.map(rowId), ...feeds.map((feed) => feed.id)];
284
352
  order.forEach((feed, index) => {
285
353
  const turn = DRAW_FROM + ((DRAW_TO - DRAW_SPAN - DRAW_FROM) * index) / Math.max(order.length - 1, 1);
package/ui/tree-map.d.ts CHANGED
@@ -1,20 +1,21 @@
1
1
  import { type StudioWorkspace } from '../mod.ts';
2
+ import { type Box } from './lib/tree-map-layout.ts';
2
3
  export interface StudioTreeMapProps {
3
4
  workspace: StudioWorkspace;
4
5
  /** The keys of the agents to draw, in the workspace's order; every agent when unset. */
5
6
  agents?: readonly string[] | undefined;
6
7
  /**
7
- * How far the tree has grown, from 0 to 1. As it grows each agent drifts into a zone of its own,
8
- * its runner and providers level with its rows, and room opens under each feed for its own. What
9
- * growing adds draws in as its room opens, its line and then its token: the steps of a turn under
10
- * each turn runner, and every tool in the registry. Shrinking takes it all back the same way.
8
+ * How far the tree has grown, from 0 to 1. As it grows each agent drifts into a zone of its own
9
+ * and what feeds the tree spreads into its columns. What growing adds draws in as its room opens,
10
+ * its line and then its token: the protocols under each provider, what branches off a tool, the
11
+ * runner's steps, and the tool runner with its checks. Shrinking takes it all back the same way.
11
12
  */
12
13
  grow?: number | undefined;
13
- /** The box the map fits grown: growing, it eases smaller until all of it fits. */
14
- fit?: {
15
- width: number;
16
- height: number;
17
- } | undefined;
14
+ /**
15
+ * The box the map fills grown: its rows and gaps stretch to fill it, and it stands in the middle
16
+ * of it. When the map cannot fit, it eases smaller as it grows until all of it does.
17
+ */
18
+ fit?: Box | undefined;
18
19
  /** A token dragged moves to where it is dropped, its lines with it; the field dragged moves the whole map. */
19
20
  draggable?: boolean | undefined;
20
21
  }