@theoremjs/studio 0.3.4-next.413.gb7a2b75 → 0.3.4-next.415.g3597d47

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 (48) hide show
  1. package/README.md +1 -1
  2. package/mod.d.ts +1 -1
  3. package/mod.js +1 -1
  4. package/package.json +4 -4
  5. package/page/assets/Banner-8YJcS8-V.js +2 -0
  6. package/page/assets/CollapsibleGroup-PavPZ114.js +1 -0
  7. package/page/assets/ConsoleFrame-DU_VbYY8.js +3 -0
  8. package/page/assets/{ContextMenu-COkl-sLz.js → ContextMenu-BM8iCtOh.js} +2 -2
  9. package/page/assets/{ToolResultCharts-UmFnfSOp.js → ToolResultCharts-2ZnwTSBM.js} +1 -1
  10. package/page/assets/TraceInspector-BMPAqts9.js +1 -0
  11. package/page/assets/{TraceOverview-BiCh-eqR.js → TraceOverview-CAcG99rb.js} +1 -1
  12. package/page/assets/{browser-engine-BY6gYiHv.js → browser-engine-CZIMsNT9.js} +1 -1
  13. package/page/assets/{browser-transport-B4Or1xM7.js → browser-transport-1C-yqvF_.js} +4 -4
  14. package/page/assets/{export-agent-D8hLzzFn.js → export-agent-CtTlaPls.js} +1 -1
  15. package/page/assets/{guardrail-tester-C04dSfNv.js → guardrail-tester-DSqEFs8L.js} +2 -2
  16. package/page/assets/{index-Br0mAcH5.js → index-BCMKIa3t.js} +68 -68
  17. package/page/assets/index-Cbs7B46A.css +1 -0
  18. package/page/assets/labels-provider-BSiD6Lrb.js +3 -0
  19. package/page/assets/{studio-code-pane-B0BB_yGF.js → studio-code-pane-PK_Sla3w.js} +1 -1
  20. package/page/assets/{studio-console-BLHAfqNd.js → studio-console-CfDp9X-T.js} +1 -1
  21. package/page/assets/{studio-decision-Cfc9arc7.js → studio-decision-B4KZCD5V.js} +1 -1
  22. package/page/assets/{studio-embedding-CGSaA2dS.js → studio-embedding-CB4lSSWM.js} +1 -1
  23. package/page/assets/{studio-host-console-BidxEK6_.js → studio-host-console-C3PBxw_t.js} +2 -2
  24. package/page/assets/{studio-labels-CWLqOvKg.js → studio-labels-Jw1KQ1X1.js} +1 -1
  25. package/page/assets/{studio-live-C8XxDpXT.js → studio-live-BfBbeQVS.js} +2 -2
  26. package/page/assets/{studio-map-C2zIkdcp.js → studio-map-D3GkuREn.js} +1 -1
  27. package/page/assets/{studio-trace-replay-CpbB-zpo.js → studio-trace-replay-BSDZA9i6.js} +1 -1
  28. package/page/assets/{use-one-call-BJhdZLBt.js → use-one-call-CY-cA5FL.js} +1 -1
  29. package/page/assets/{SidePanel-OYp0bLNO.js → use-trace-records-BkAYJz0M.js} +1 -1
  30. package/page/assets/workspace-MlrGUH2-.js +1 -0
  31. package/page/index.html +10 -10
  32. package/ui/inspector-context.d.ts +5 -0
  33. package/ui/inspector-context.js +2 -0
  34. package/ui/profile-editor.js +10 -3
  35. package/ui/studio-run.js +2 -12
  36. package/ui/studio-screen.js +141 -158
  37. package/ui/studio.css +29 -13
  38. package/ui/use-phone.d.ts +2 -0
  39. package/ui/use-phone.js +13 -0
  40. package/workspace.d.ts +7 -0
  41. package/workspace.js +48 -0
  42. package/page/assets/Banner-CIcZcE2z.js +0 -2
  43. package/page/assets/CollapsibleGroup-CL8h1IzH.js +0 -1
  44. package/page/assets/ConsoleFrame-Ix0RXm_R.js +0 -3
  45. package/page/assets/TraceInspector-DWw6BGA5.js +0 -1
  46. package/page/assets/index-Oo9otEIT.css +0 -1
  47. package/page/assets/labels-provider-BqgbdxdT.js +0 -3
  48. package/page/assets/workspace-SpjcVXbF.js +0 -1
@@ -21,11 +21,11 @@ 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, IconDownload, IconFile, IconFileImport, IconFileSmile, IconFileZip, IconKey, IconMenu2, IconPlayerPlay, IconPlaylistX, IconPlus, IconRestore, IconRotateClockwise, IconSearch, IconShieldSearch, IconSitemap, IconSparkles, IconTool, IconX, } from '@tabler/icons-react';
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';
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";
28
- import { addAgent, addArchitectExample, addProvider, agentNodeId, compileWorkspace, createBlankDraft, createConsoleExampleDraft, createDecisionExampleDraft, createExampleDraft, createGeminiEmbeddingExampleDraft, createLiveExampleDraft, createNarratorExampleDraft, createStudioRunId, draftFacets, duplicateAgent, editedSince, excludeFacet, includableFacets, includeFacet, libraryDraft, localProviderAddress, mergedWithFiles, modelBindingNodeId, modelRoute, modelSharers, modelSlots, nodeOrigins, onStudioLocal, openOutcome, originLabel, originPlace, originWay, providerIdOf, providerNodeId, providerUse, rebased, removeAgent, removeLibraryTool, removeProvider, renameProvider, reopened, resetAgent, resetAll, resetLibraryTool, resetProvider, sampleToolInput, saveStudioRunPayload, scopedNodeId, setToolAllowed, sharedAsk, sharedAt, sharedEntries, studioNodeRef, studioSource, studioTree, toolSpecKeyOf, withAgentDraft, withFilesChosen, withProvider, workspaceNodeRef, workspaceRunAgent, workspaceTree, writeAsk, } from "../mod.js";
28
+ import { addAgent, addArchitectExample, addProvider, agentNodeId, compileWorkspace, createBlankDraft, createConsoleExampleDraft, createDecisionExampleDraft, createExampleDraft, createGeminiEmbeddingExampleDraft, createLiveExampleDraft, createNarratorExampleDraft, createStudioRunId, draftFacets, duplicateAgent, editedSince, excludeFacet, includableFacets, includeFacet, libraryDraft, localProviderAddress, mergedWithFiles, modelBindingNodeId, modelRoute, modelSharers, modelSlots, nodeOrigins, onStudioLocal, openOutcome, originLabel, originPlace, originWay, providerIdOf, providerNodeId, providerUse, rebased, removeAgent, removeLibraryTool, removeProvider, renameProvider, reopened, resetAgent, resetAgentNode, resetLibraryTool, resetProvider, sampleToolInput, saveStudioRunPayload, scopedNodeId, setToolAllowed, sharedAsk, sharedAt, sharedEntries, studioNodeRef, studioSource, studioTree, toolSpecKeyOf, withAgentDraft, withFilesChosen, withProvider, workspaceNodeRef, workspaceRunAgent, workspaceTree, writeAsk, } from "../mod.js";
29
29
  import { studioSurface } from "../surface.js";
30
30
  import './studio.css';
31
31
  import { createTraceFeed } from '@theoremjs/web/client';
@@ -35,7 +35,7 @@ import { ActingAsButton } from "./acting-as-button.js";
35
35
  import { AddProviderButton } from "./add-provider.js";
36
36
  import { IconTypeSafe, modelFamilyMark } from "./brand-icons.js";
37
37
  import { useConfirmWrite } from "./confirm-write.js";
38
- import { ConfirmWrite, ISSUE_ROW_ATTRIBUTE, LeavePage, ListBadges, LocalConnection, ProbedAgent, ProjectAsks, QuestionLink, RowOrigins, RunsOn, WorkspaceContext, } from "./inspector-context.js";
38
+ import { ConfirmWrite, GuardrailTest, ISSUE_ROW_ATTRIBUTE, LeavePage, ListBadges, LocalConnection, ProbedAgent, ProjectAsks, QuestionLink, RowOrigins, RunsOn, WorkspaceContext, } from "./inspector-context.js";
39
39
  import { ActingAsContext, chatKeyOf, useActingAsState } from "./lib/acting-as.js";
40
40
  import { FACET_ICON } from "./lib/facet-icons.js";
41
41
  import { movePanel } from "./lib/panel-motion.js";
@@ -65,6 +65,7 @@ import { StudioHostContext, useStudioHost, } from "./studio-host.js";
65
65
  import { NAV_WIDTH } from "./studio-nav.js";
66
66
  import { StudioRunner } from "./studio-runner.js";
67
67
  import { ProjectSave, ProjectTokens, useProjectSave } from "./studio-save.js";
68
+ import { usePhone } from "./use-phone.js";
68
69
  // The tester, Settings, the map and the code view load when they first open, or once the page is idle.
69
70
  const testerPanel = () => import("./guardrail-tester.js");
70
71
  const settingsPanel = () => import("./studio-settings.js");
@@ -101,26 +102,31 @@ function actionButton(label, icon, onPress) {
101
102
  return (_jsx(IconButton, { label: label, variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: icon, size: "sm" }), tooltip: label, onClick: (event) => {
102
103
  event.stopPropagation();
103
104
  onPress();
104
- } }));
105
+ } }, label));
105
106
  }
106
107
  function rowAction(label, icon, onPress) {
107
108
  return _jsx("span", { className: "studio-tree-action", children: actionButton(label, icon, onPress) });
108
109
  }
110
+ /** A row's hover actions: Reset, while it differs from its start, then the others, the x last. */
111
+ function rowActions(label, reset, ...actions) {
112
+ const shown = [reset && actionButton(`Reset ${label}`, IconRotateClockwise, reset), ...actions];
113
+ return shown.some(Boolean) ? _jsx("span", { className: "studio-tree-action", children: shown }) : undefined;
114
+ }
109
115
  function treeItem(tree, node, isTop = false) {
110
116
  const { draft, scope, selectedId, onSelect, setDraft } = tree;
111
117
  const facet = node.ref.facet;
112
118
  const id = scope(node.id);
113
119
  const isOptional = isTop && facet !== 'toolSpec' && profileGraphFacet(facet)?.optional === true;
120
+ const actions = rowActions(node.label, tree.resetOf(node.ref, node.label), isOptional &&
121
+ actionButton(`Remove ${node.label}`, IconX, () => {
122
+ setDraft((current) => excludeFacet(current, facet));
123
+ }));
114
124
  return {
115
125
  id,
116
126
  label: node.label,
117
127
  startContent: _jsx(Icon, { icon: nodeIcon(draft, node.ref), size: "sm", color: "secondary" }),
118
- endContent: isOptional
119
- ? rowAction(`Remove ${node.label}`, IconX, () => {
120
- setDraft((current) => excludeFacet(current, facet));
121
- })
122
- : undefined,
123
- className: isOptional ? 'studio-tree-row' : undefined,
128
+ endContent: actions,
129
+ className: actions ? 'studio-tree-row' : undefined,
124
130
  isSelected: id === selectedId,
125
131
  isExpanded: node.children.length > 0,
126
132
  onClick: () => {
@@ -171,7 +177,7 @@ function agentItem(tree, isOpen, actions) {
171
177
  };
172
178
  }
173
179
  function agentItems(state) {
174
- const { workspace, focus, selectedId, onSelect, update, setDraft } = state;
180
+ const { workspace, focus, selectedId, onSelect, update, setDraft, reset } = state;
175
181
  const isOnly = workspace.agents.length === 1;
176
182
  return workspace.agents.map((agent) => {
177
183
  const draft = libraryDraft(workspace, agent.key) ?? createBlankDraft();
@@ -182,16 +188,17 @@ function agentItems(state) {
182
188
  selectedId,
183
189
  onSelect,
184
190
  setDraft,
185
- }, agent.key === focus, _jsxs("span", { className: "studio-tree-action", children: [actionButton(`Duplicate ${name}`, IconCopyPlus, () => {
186
- update((current) => duplicateAgent(current, agent.key));
187
- }), !isOnly &&
188
- actionButton(`Remove ${name}`, IconX, () => {
189
- update((current) => removeAgent(current, agent.key));
190
- })] }));
191
+ resetOf: (ref, label) => reset(resetAgentNode(workspace, agent.key, ref), `Reset ${label} to how it started.`),
192
+ }, agent.key === focus, rowActions(name, reset(resetAgent(workspace, agent.key), `Reset ${name} to how it started.`), actionButton(`Duplicate ${name}`, IconCopyPlus, () => {
193
+ update((current) => duplicateAgent(current, agent.key));
194
+ }), !isOnly &&
195
+ actionButton(`Remove ${name}`, IconX, () => {
196
+ update((current) => removeAgent(current, agent.key));
197
+ })));
191
198
  });
192
199
  }
193
200
  /** The tool library, every agent picks its tools from these: those whose name or description holds `query`. */
194
- function toolItems({ workspace, selectedId, onSelect, update }, query) {
201
+ function toolItems({ workspace, selectedId, onSelect, update, reset }, query) {
195
202
  const needle = query.trim().toLowerCase();
196
203
  return workspaceTree(workspace).tools.flatMap((node) => {
197
204
  const tool = workspace.toolSpecs.find((spec) => spec.key === toolSpecKeyOf(node.id));
@@ -203,10 +210,11 @@ function toolItems({ workspace, selectedId, onSelect, update }, query) {
203
210
  id: node.id,
204
211
  label: node.label,
205
212
  startContent: (_jsx(Icon, { icon: tool ? toolTypeIcon(tool.toolType) : IconTool, size: "sm", color: "secondary" })),
206
- endContent: rowAction(`Remove ${node.label}`, IconX, () => {
213
+ endContent: rowActions(node.label, tool &&
214
+ reset(resetLibraryTool(workspace, tool.key), `Reset ${node.label} to how it started.`), actionButton(`Remove ${node.label}`, IconX, () => {
207
215
  if (tool)
208
216
  update((current) => removeLibraryTool(current, tool.key));
209
- }),
217
+ })),
210
218
  className: 'studio-tree-row',
211
219
  isSelected: node.id === selectedId,
212
220
  onClick: () => {
@@ -226,7 +234,7 @@ function declaredAt({ setting }) {
226
234
  * provider's row opens its own settings; an agent's opens its first model there.
227
235
  */
228
236
  function providerItems(tree) {
229
- const { workspace, selectedId, onSelect, update, ownsProviders } = tree;
237
+ const { workspace, selectedId, onSelect, update, ownsProviders, reset } = tree;
230
238
  return workspace.providers.map((provider) => {
231
239
  const id = providerNodeId(provider.id);
232
240
  // One a model runs on stays: its row lists the agents to move first.
@@ -246,19 +254,19 @@ function providerItems(tree) {
246
254
  },
247
255
  ];
248
256
  });
257
+ const actions = rowActions(provider.id, reset(resetProvider(workspace, provider.id), `Reset ${provider.id} to how it started.`), isFree &&
258
+ actionButton(`Remove ${provider.id}`, IconX, () => {
259
+ update((current) => removeProvider(current, provider.id));
260
+ const [first] = workspace.agents;
261
+ if (selectedId === id && first)
262
+ onSelect(agentNodeId(first.key));
263
+ }));
249
264
  return {
250
265
  id,
251
266
  label: provider.id,
252
267
  startContent: _jsx(Icon, { icon: IconCloud, size: "sm", color: "secondary" }),
253
- endContent: isFree
254
- ? rowAction(`Remove ${provider.id}`, IconX, () => {
255
- update((current) => removeProvider(current, provider.id));
256
- const [first] = workspace.agents;
257
- if (selectedId === id && first)
258
- onSelect(agentNodeId(first.key));
259
- })
260
- : undefined,
261
- className: isFree ? 'studio-tree-row' : undefined,
268
+ endContent: actions,
269
+ className: actions ? 'studio-tree-row' : undefined,
262
270
  isSelected: selectedId === id,
263
271
  onClick: () => {
264
272
  onSelect(id);
@@ -437,7 +445,7 @@ function WorkspaceTreeLists({ tree, draft, onAddAgent, listRef, }) {
437
445
  } }), list === 'agents' ? _jsx(AddAgentMenu, { onAddAgent: onAddAgent }) : null, list === 'tools' && (_jsx(ToolSearchInput, { query: query, onChange: (next) => {
438
446
  setQuery(next);
439
447
  revealSelected();
440
- } })), list === 'tools' ? _jsx(AddToolButton, { tree: tree, draft: draft }) : null, list === 'providers' && tree.ownsProviders ? (_jsx(AddProviderButton, { taken: tree.workspace.providers.map((provider) => provider.id), onAdd: (provider) => {
448
+ } })), list === 'tools' ? _jsx(AddToolButton, { tree: tree, draft: draft }) : null, list === 'providers' && tree.keys ? _jsx(ManageKeysButton, { keys: tree.keys }) : null, list === 'providers' && tree.ownsProviders ? (_jsx(AddProviderButton, { taken: tree.workspace.providers.map((provider) => provider.id), onAdd: (provider) => {
441
449
  tree.update((current) => addProvider(current, provider));
442
450
  tree.onSelect(providerNodeId(provider.id));
443
451
  } })) : null, _jsx(StackItem, { size: "fill", children: _jsx(WorkspaceListBody, { tree: tree, list: list, tools: tools, query: query, listRef: listRef }) })] }));
@@ -839,17 +847,24 @@ function useLeftIssues(issues, page, revealed) {
839
847
  const shown = useMemo(() => issues.filter((issue) => left.has(issueKey(issue))), [issues, left]);
840
848
  return { shown, leavePage };
841
849
  }
842
- /** Get code (a .zip, or copied) and open in a new tab, for the agent being chatted with; off while the workspace has issues. */
843
- function ExportActions({ store, compiled, chatted, chattedId, blocked, copy, connection, }) {
850
+ /** Open in a new tab, for the agent being chatted with; off while the workspace has issues. */
851
+ function OpenInNewTabButton({ compiled, chattedId, blocked, connection, }) {
852
+ const project = useProject();
853
+ const phone = usePhone();
854
+ return (_jsx(Button, { label: "Open in a new tab", isIconOnly: phone, variant: "ghost", icon: _jsx(Icon, { icon: IconBrowserShare, size: "sm" }), isDisabled: !compiled, tooltip: blocked ??
855
+ 'Run this agent on a full page of its own, in a new tab, the way a person using it would see it', onClick: () => {
856
+ const run = compiled && workspaceRunAgent(compiled, chattedId);
857
+ if (run)
858
+ openInNewTab({ ...runPayload(run), ...connection }, project);
859
+ } }));
860
+ }
861
+ /** Get code: a .zip, Copy, or Copy for LLM, for every agent; off while the workspace has issues. */
862
+ function ExportMenu({ store, compile, copy, }) {
844
863
  const project = useProject();
845
- return (_jsxs(_Fragment, { children: [_jsx(IconButton, { label: "Open in a new tab", variant: "ghost", icon: _jsx(Icon, { icon: IconBrowserShare, size: "sm" }), isDisabled: !compiled, tooltip: blocked ?? 'Open this agent on its own full page, in a new tab', onClick: () => {
846
- const run = compiled && workspaceRunAgent(compiled, chattedId);
847
- if (run)
848
- openInNewTab({ ...runPayload(run), ...connection }, project);
849
- } }), _jsx(ExportMenu, { code: () => projectCode(project, store.getWorkspace()), compiled: compiled, chatted: chatted, blocked: blocked, copy: copy })] }));
850
- }
851
- /** Get code: a .zip, Copy, or Copy for LLM. */
852
- function ExportMenu({ code, compiled, chatted, blocked, copy, }) {
864
+ const compiled = compile.compiled.ok ? compile.compiled : undefined;
865
+ const { chatted, blocked } = compile;
866
+ /** What the project's files hold that the code names, read when the builder asks for the code. */
867
+ const code = () => projectCode(project, store.getWorkspace());
853
868
  return (_jsx(DropdownMenu, { button: {
854
869
  label: 'Get code',
855
870
  variant: 'ghost',
@@ -1559,45 +1574,6 @@ function ViewToggleButton({ view }) {
1559
1574
  },
1560
1575
  ] }));
1561
1576
  }
1562
- /**
1563
- * Reset for what is open: a library tool, a provider, or else the agent. It goes back to how it joined the
1564
- * workspace, and nothing else changes. `run` is absent while it is still as it started. `all` does
1565
- * it for everything: every agent and tool back to its start, or on a project every unsaved edit
1566
- * gone once the builder says yes. It is absent while there is nothing to put back.
1567
- */
1568
- function useReset({ workspace, focus, update }) {
1569
- const toast = useToast();
1570
- const save = useProjectSave();
1571
- const toolKey = toolSpecKeyOf(workspace.selected);
1572
- const provider = providerIdOf(workspace.selected);
1573
- const what = provider !== undefined ? 'provider' : toolKey === undefined ? 'agent' : 'tool';
1574
- const next = useMemo(() => {
1575
- if (provider !== undefined)
1576
- return resetProvider(workspace, provider);
1577
- return toolKey === undefined
1578
- ? resetAgent(workspace, focus)
1579
- : resetLibraryTool(workspace, toolKey);
1580
- }, [workspace, focus, toolKey, provider]);
1581
- const everything = useMemo(() => resetAll(workspace), [workspace]);
1582
- const put = (to, body) => to === workspace
1583
- ? undefined
1584
- : () => {
1585
- update(() => to);
1586
- const dismiss = toast({
1587
- body,
1588
- endContent: (_jsx(Button, { label: "Undo", variant: "ghost", size: "sm", onClick: () => {
1589
- update(() => workspace);
1590
- dismiss();
1591
- } })),
1592
- });
1593
- };
1594
- return {
1595
- what,
1596
- run: put(next, `Reset the ${what} to how it started.`),
1597
- all: save ? save.discard : put(everything, 'Reset every agent and tool to how it started.'),
1598
- inProject: save !== null,
1599
- };
1600
- }
1601
1577
  /** Opens the project's files again in place of every unsaved edit. Undo brings the edits back. */
1602
1578
  function useDiscardAll({ store, filesChanged }) {
1603
1579
  const toast = useToast();
@@ -1621,47 +1597,13 @@ function useDiscardAll({ store, filesChanged }) {
1621
1597
  });
1622
1598
  }, [store, filesChanged, toast]);
1623
1599
  }
1624
- /** The editor column's header: its title, the next issue, Keys, what to start from, and the view toggle. */
1625
- function EditorToolbar({ heading, compile, selected, view, onIssue, reset, project, setSheet, }) {
1626
- return (_jsx(Section, { variant: "transparent", padding: 3, dividers: ['bottom'], children: _jsxs(HStack, { gap: 1, vAlign: "center", children: [_jsx(SheetButton, { label: "Sections", icon: IconMenu2, sheet: "tree", setSheet: setSheet }), _jsx(StackItem, { size: "fill", children: heading && (_jsx("span", { title: heading, children: _jsx(Heading, { level: 4, maxLines: 1, children: heading }) })) }), _jsx(StackItem, { size: "static", children: _jsxs(HStack, { gap: 1, vAlign: "center", children: [_jsx(IssueToken, { compile: compile, selected: selected, onIssue: onIssue }), project] }) }), _jsx(KeysButton, { view: view }), _jsx(DropdownMenu, { button: {
1627
- label: 'Reset',
1628
- variant: 'ghost',
1629
- isIconOnly: true,
1630
- icon: _jsx(Icon, { icon: IconRotateClockwise, size: "sm" }),
1631
- tooltip: reset.run || reset.all
1632
- ? `Reset this ${reset.what}, or everything`
1633
- : 'Everything is as it started',
1634
- isDisabled: !reset.run && !reset.all,
1635
- }, menuWidth: "15rem", hasChevron: false, placement: "below", alignment: "end", items: [
1636
- {
1637
- id: 'this',
1638
- label: `Reset this ${reset.what}`,
1639
- description: oneLine('Back to how it started. Undo brings your changes back.'),
1640
- icon: _jsx(Icon, { icon: IconRotateClockwise, size: "sm" }),
1641
- isDisabled: !reset.run,
1642
- onClick: reset.run,
1643
- },
1644
- {
1645
- id: 'all',
1646
- label: reset.inProject ? 'Discard all edits' : 'Reset everything',
1647
- description: oneLine(reset.inProject
1648
- ? 'Every unsaved edit goes, and the studio opens your files as they are.'
1649
- : 'Every agent and tool, back to how it started.'),
1650
- icon: _jsx(Icon, { icon: IconRestore, size: "sm" }),
1651
- isDisabled: !reset.all,
1652
- onClick: reset.all,
1653
- },
1654
- ] }), _jsx(ViewToggleButton, { view: view }), _jsx(SheetButton, { label: "Preview", icon: IconPlayerPlay, sheet: "preview", setSheet: setSheet })] }) }));
1600
+ /** The editor column's header: its title, the next issue, what to start from, Get code, and the view toggle. */
1601
+ function EditorToolbar({ heading, compile, selected, view, onIssue, project, getCode, setSheet, }) {
1602
+ return (_jsx(Section, { variant: "transparent", padding: 3, dividers: ['bottom'], children: _jsxs(HStack, { gap: 1, vAlign: "center", children: [_jsx(SheetButton, { label: "Sections", icon: IconMenu2, sheet: "tree", setSheet: setSheet }), _jsx(StackItem, { size: "fill", children: heading && (_jsx("span", { title: heading, children: _jsx(Heading, { level: 4, maxLines: 1, children: heading }) })) }), _jsx(StackItem, { size: "static", children: _jsxs(HStack, { gap: 1, vAlign: "center", children: [_jsx(IssueToken, { compile: compile, selected: selected, onIssue: onIssue }), project] }) }), getCode, _jsx(ViewToggleButton, { view: view }), _jsx(SheetButton, { label: "Preview", icon: IconPlayerPlay, sheet: "preview", setSheet: setSheet })] }) }));
1655
1603
  }
1656
- /** The door to the Keys panel in the editor's header. A host with Settings keeps Keys there, so it has none. */
1657
- function KeysButton({ view }) {
1658
- const { hasSettings } = useStudioHost();
1659
- if (hasSettings)
1660
- return null;
1661
- return (_jsx(IconButton, { label: "Keys", variant: "ghost", icon: _jsx(Icon, { icon: IconKey, size: "sm" }), "aria-pressed": view.keysOpen, tooltip: "Add your own model keys to run this agent with", onClick: () => {
1662
- view.setKeysOpen((open) => !open);
1663
- view.setEditorView('editor');
1664
- } }));
1604
+ /** Opens the Keys page, above the providers list, in the same form as Add provider. */
1605
+ function ManageKeysButton({ keys }) {
1606
+ return (_jsx(Button, { label: "Manage keys", variant: "ghost", size: "sm", width: "100%", icon: _jsx(Icon, { icon: IconKey, size: "sm" }), style: { height: 'calc(var(--size-element-sm) - 4px)' }, "aria-pressed": keys.open, onClick: keys.toggle }));
1665
1607
  }
1666
1608
  /** The Keys panel, in the editor's column. */
1667
1609
  function KeysBody({ connection, state, }) {
@@ -1716,7 +1658,7 @@ function useOpenKeys(setKeysOpen) {
1716
1658
  function SettingsPane({ state, connection, project, setConversation, }) {
1717
1659
  const { kernelVersion } = useStudioHost();
1718
1660
  const { store } = state;
1719
- const reset = useReset(state);
1661
+ const save = useProjectSave();
1720
1662
  const toast = useToast();
1721
1663
  const quiet = useSyncExternalStore(store.subscribe, store.isQuiet, store.isQuiet);
1722
1664
  const shared = useMemo(() => ({
@@ -1730,7 +1672,7 @@ function SettingsPane({ state, connection, project, setConversation, }) {
1730
1672
  setConversation((count) => count + 1);
1731
1673
  toast({ body: "Cleared this tab's chats." });
1732
1674
  }, [setConversation, toast]);
1733
- return (_jsx(Suspense, { fallback: null, children: _jsx(StudioSettings, { project: project, version: kernelVersion, watch: state.watchFiles, shared: shared, actions: { clearChats, discard: reset.inProject ? reset.all : undefined }, keys: _jsx(KeySlots, { connection: connection, state: state }) }) }));
1675
+ return (_jsx(Suspense, { fallback: null, children: _jsx(StudioSettings, { project: project, version: kernelVersion, watch: state.watchFiles, shared: shared, actions: { clearChats, discard: save?.discard }, keys: _jsx(KeySlots, { connection: connection, state: state }) }) }));
1734
1676
  }
1735
1677
  /** The open node's editor, keyed by node so each one opens at its top. */
1736
1678
  function EditorBody({ state, connection, selected, editing, issues, listBadges, editorRef, open, }) {
@@ -1760,30 +1702,26 @@ function ClearHistoryButton({ run }) {
1760
1702
  }
1761
1703
  /** The types whose conversation a trace can hold, and so open and replay. */
1762
1704
  const NO_CONVERSATION = new Set(['decision', 'host', 'embedding', 'live']);
1763
- /** Downloads the records of the conversation on screen, as a trace file the studio opens. */
1764
- function DownloadTraceButton({ feed, name }) {
1765
- const records = useSyncExternalStore(feed.subscribe, feed.records, feed.records);
1766
- return (_jsx(IconButton, { label: "Download trace", variant: "ghost", tooltip: records.length ? 'Download trace' : 'Nothing recorded yet', icon: _jsx(Icon, { icon: IconDownload, size: "sm" }), isDisabled: records.length === 0, onClick: () => {
1767
- const stamp = new Date().toISOString().replaceAll(':', '-').slice(0, 19);
1768
- download(`${name}-${stamp}.trace.json`, new Blob([JSON.stringify(records, null, 2)], { type: 'application/json' }));
1769
- } }));
1770
- }
1771
- /** Import trace, for a type whose conversation a trace holds, and Download trace, outside an imported one. */
1772
- function TraceFileButtons({ payload, tracing, feed, profileId, }) {
1773
- if (payload == null)
1705
+ /** Replay exchange, for a type whose conversation a trace holds. */
1706
+ function ReplayExchangeButton({ payload, tracing, }) {
1707
+ const phone = usePhone();
1708
+ if (payload == null || NO_CONVERSATION.has(payload.profile.type))
1774
1709
  return null;
1775
- const importLabel = tracing.open ? 'Close imported trace' : 'Import trace';
1776
- return (_jsxs(_Fragment, { children: [!NO_CONVERSATION.has(payload.profile.type) && (_jsx(IconButton, { label: importLabel, variant: tracing.open ? 'secondary' : 'ghost', tooltip: importLabel, icon: _jsx(Icon, { icon: IconFileImport, size: "sm" }), "aria-pressed": tracing.open, onClick: tracing.toggle })), !tracing.open && _jsx(DownloadTraceButton, { feed: feed, name: profileId ?? 'agent' })] }));
1710
+ return (_jsx(Button, { label: tracing.open ? 'Close replay' : 'Replay exchange', isIconOnly: phone, variant: tracing.open ? 'secondary' : 'ghost', tooltip: tracing.open
1711
+ ? 'Back to the live conversation'
1712
+ : 'Open a recorded conversation (a .trace.json file) to see what happened, then replay its messages through this agent as it is now and compare', icon: _jsx(Icon, { icon: IconFileImport, size: "sm" }), "aria-pressed": tracing.open, onClick: tracing.toggle }));
1777
1713
  }
1778
1714
  /** The preview's header: back to the editor, the agent to chat with, history, trace, and Export. */
1779
- function PreviewHeader({ store, chatAgents, chatWith, compile, run, connection, trace, testing, tracing, feed, profileId, copy, setSheet, }) {
1780
- const testLabel = testing.open ? 'Close guardrail test' : 'Test guardrails';
1715
+ function PreviewHeader({ store, chatAgents, chatWith, compile, run, connection, trace, testing, tracing, setSheet, }) {
1781
1716
  const traceLabel = trace.open ? 'Hide trace' : 'View trace';
1717
+ const phone = usePhone();
1782
1718
  const { compiled } = compile;
1783
- return (_jsx(Section, { variant: "transparent", paddingInline: 3, paddingBlock: 0, children: _jsxs(HStack, { gap: 2, vAlign: "center", children: [_jsx(SheetButton, { label: "Back to the editor", icon: IconArrowLeft, sheet: null, setSheet: setSheet }), _jsx(StackItem, { size: "fill", children: _jsx(ChatPicker, { agents: chatAgents, chatWith: chatWith, onChange: store.chatWith }) }), _jsx(StackItem, { size: "static", children: _jsxs(HStack, { gap: 1, vAlign: "center", children: [_jsx(ActingAsButton, {}), compile.payload && run.isUsed && !testing.open && !tracing.open && (_jsx(ClearHistoryButton, { run: run })), !testing.open && (_jsx(TraceFileButtons, { payload: compile.payload, tracing: tracing, feed: feed, profileId: profileId })), compile.payload && (_jsx(IconButton, { label: testLabel, variant: "ghost", tooltip: testLabel, icon: _jsx(Icon, { icon: IconShieldSearch, size: "sm" }), "aria-pressed": testing.open, onClick: testing.toggle })), _jsx(ExportActions, { store: store, compiled: compiled.ok ? compiled : undefined, chatted: compile.chatted, chattedId: compile.chattedId, blocked: compile.blocked, copy: copy, connection: {
1719
+ return (_jsx(Section, { variant: "transparent", paddingInline: 3, paddingBlock: 0, children: _jsxs(HStack, { gap: 2, vAlign: "center", children: [_jsx(SheetButton, { label: "Back to the editor", icon: IconArrowLeft, sheet: null, setSheet: setSheet }), _jsx(StackItem, { size: "fill", children: _jsx(ChatPicker, { agents: chatAgents, chatWith: chatWith, onChange: store.chatWith }) }), _jsx(StackItem, { size: "static", children: _jsxs(HStack, { gap: 1, vAlign: "center", children: [_jsx(ActingAsButton, {}), compile.payload && run.isUsed && !testing && !tracing.open && (_jsx(ClearHistoryButton, { run: run })), !testing && _jsx(ReplayExchangeButton, { payload: compile.payload, tracing: tracing }), (compile.traced || tracing.open) && !testing ? (_jsx(Button, { label: traceLabel, isIconOnly: phone, variant: trace.open ? 'secondary' : 'ghost', tooltip: trace.open
1720
+ ? 'Close the trace'
1721
+ : 'See how each reply was made, step by step: every model and tool call, guardrail check and gate, with its timing', icon: _jsx(Icon, { icon: IconActivity, size: "sm" }), "aria-pressed": trace.open, onClick: trace.toggle })) : null, _jsx(OpenInNewTabButton, { compiled: compiled.ok ? compiled : undefined, chattedId: compile.chattedId, blocked: compile.blocked, connection: {
1784
1722
  connectionMode: connection.mode,
1785
1723
  localBaseUrl: connection.local.baseUrl,
1786
- } }), (compile.traced || tracing.open) && !testing.open ? (_jsx(IconButton, { label: traceLabel, variant: trace.open ? 'secondary' : 'ghost', tooltip: traceLabel, icon: _jsx(Icon, { icon: IconActivity, size: "sm" }), "aria-pressed": trace.open, onClick: trace.toggle })) : null] }) })] }) }));
1724
+ } })] }) })] }) }));
1787
1725
  }
1788
1726
  /**
1789
1727
  * Why the agent to chat with does not run: the errors in its config, or the workspace's when its
@@ -1899,10 +1837,11 @@ function useSendAfterResume(chatRef, runKey) {
1899
1837
  * The compiled agent to chat with, under its header; while none compiles, why. It sits out renders
1900
1838
  * that change nothing it shows: typing in the editor, opening a node, Keys.
1901
1839
  */
1902
- const PreviewPane = memo(function PreviewPane({ store, chatAgents, chatWith, compile, run, connection, chatRef, copy, setSheet, onIssue, }) {
1840
+ const PreviewPane = memo(function PreviewPane({ store, chatAgents, chatWith, compile, run, connection, chatRef, setSheet, onIssue, testing, }) {
1903
1841
  const [traceOpen, setTraceOpen] = useState(false);
1904
- const [testing, setTesting] = useState(false);
1905
- const [tracing, setTracing] = useState(false);
1842
+ const [imported, setTracing] = useState(false);
1843
+ // why: The guardrail test covers an imported trace too; closing the test shows it again.
1844
+ const tracing = imported && !testing;
1906
1845
  const feedOf = useRunFeeds(run.generation);
1907
1846
  const profileId = chatAgents.find((agent) => agent.value === chatWith)?.label;
1908
1847
  const sendNext = useSendAfterResume(chatRef, run.runKey);
@@ -1917,18 +1856,12 @@ const PreviewPane = memo(function PreviewPane({ store, chatAgents, chatWith, com
1917
1856
  toggle: () => {
1918
1857
  setTraceOpen((open) => !open);
1919
1858
  },
1920
- }, testing: {
1921
- open: testing,
1922
- toggle: () => {
1923
- setTesting((open) => !open);
1924
- setTracing(false);
1925
- },
1926
- }, tracing: {
1859
+ }, testing: testing, tracing: {
1927
1860
  open: tracing,
1928
1861
  toggle: () => {
1929
1862
  setTracing((open) => !open);
1930
1863
  },
1931
- }, feed: feedOf(run.runKey), profileId: profileId, copy: copy, setSheet: setSheet }), _jsx(StackItem, { size: "fill", children: _jsx(PreviewBody, { compile: compile, run: run, connection: connection, chatRef: chatRef, chatWith: chatWith, profileId: profileId, traceOpen: traceOpen, testing: testing, tracing: traceView, feedOf: feedOf, onIssue: onIssue }) })] }) }));
1864
+ }, setSheet: setSheet }), _jsx(StackItem, { size: "fill", children: _jsx(PreviewBody, { compile: compile, run: run, connection: connection, chatRef: chatRef, chatWith: chatWith, profileId: profileId, traceOpen: traceOpen, testing: testing, tracing: traceView, feedOf: feedOf, onIssue: onIssue }) })] }) }));
1932
1865
  });
1933
1866
  /** Tells the host what is on screen as it changes, and that it closed. */
1934
1867
  function useScreenReport(onReport, { agent, type, issues, section }) {
@@ -2046,8 +1979,20 @@ function Studio({ opened }) {
2046
1979
  const closeMap = useCallback(() => {
2047
1980
  setMapOpen(false);
2048
1981
  }, [setMapOpen]);
2049
- return (_jsx(ProjectContext.Provider, { value: project, children: _jsx(ActingAsContext.Provider, { value: actingAs, children: _jsx(ConfirmWrite, { value: asking.confirm, children: _jsxs(ProjectAsks, { value: state.asks ?? NO_ASKS, children: [asking.dialog, _jsx(SharedChange, { store: state.store }), _jsx(SaveScope, { project: project, state: state, blocked: Boolean(compile.blocked), children: _jsx(QuestionLink, { value: questionLink, children: _jsx(Layout, { ref: frame.layoutCallbackRef, className: frameClass(sheet, view.mapOpen, view.settingsOpen), padding: 0, start: _jsx(SidePanel, { frame: frame, tree: tree, draft: draft, onAddAgent: addAgentFrom, setSheet: setSheet, settings: view.settingsOpen &&
2050
- project && (_jsx(SettingsPane, { state: state, connection: connection, project: project, setConversation: run.setConversation })), map: view.mapOpen && (_jsx(Suspense, { fallback: null, children: _jsx(StudioMap, { workspace: state.workspace, shared: state.shared, selected: view.keysOpen ? '' : selected, onOpen: view.open, onClose: closeMap }) })), children: _jsx(EditorColumn, { heading: view.keysOpen ? 'Keys' : headingOf(tree.list, editing, title), state: state, connection: connection, view: view, compile: compile, selected: selected, editing: editing, frame: frame, issueReveal: issueReveal, goToIssue: goToIssue, setSheet: setSheet }) }), content: _jsx(PreviewPane, { store: store, chatAgents: chatAgents, chatWith: state.workspace.chatWith, compile: compile, run: run, connection: connection, chatRef: chatRef, copy: copy, setSheet: setSheet, onIssue: goToIssue }) }) }) })] }) }) }) }));
1982
+ const [testing, setTesting] = useState(false);
1983
+ const guardrailTest = useMemo(() => compile.payload
1984
+ ? {
1985
+ open: testing,
1986
+ toggle: () => {
1987
+ // why: On a phone the test shows in the preview sheet, so opening it goes there.
1988
+ if (!testing)
1989
+ setSheet('preview');
1990
+ setTesting(!testing);
1991
+ },
1992
+ }
1993
+ : null, [compile.payload, testing, setSheet]);
1994
+ return (_jsx(ProjectContext.Provider, { value: project, children: _jsx(ActingAsContext.Provider, { value: actingAs, children: _jsx(ConfirmWrite, { value: asking.confirm, children: _jsxs(ProjectAsks, { value: state.asks ?? NO_ASKS, children: [asking.dialog, _jsx(SharedChange, { store: state.store }), _jsx(SaveScope, { project: project, state: state, blocked: Boolean(compile.blocked), children: _jsx(QuestionLink, { value: questionLink, children: _jsx(GuardrailTest.Provider, { value: guardrailTest, children: _jsx(Layout, { ref: frame.layoutCallbackRef, className: frameClass(sheet, view.mapOpen, view.settingsOpen), padding: 0, start: _jsx(SidePanel, { frame: frame, tree: tree, draft: draft, onAddAgent: addAgentFrom, setSheet: setSheet, settings: view.settingsOpen &&
1995
+ project && (_jsx(SettingsPane, { state: state, connection: connection, project: project, setConversation: run.setConversation })), map: view.mapOpen && (_jsx(Suspense, { fallback: null, children: _jsx(StudioMap, { workspace: state.workspace, shared: state.shared, selected: view.keysOpen ? '' : selected, onOpen: view.open, onClose: closeMap }) })), children: _jsx(EditorColumn, { heading: view.keysOpen ? 'Keys' : headingOf(tree.list, editing, title), state: state, connection: connection, view: view, compile: compile, selected: selected, editing: editing, frame: frame, issueReveal: issueReveal, goToIssue: goToIssue, copy: copy, setSheet: setSheet }) }), content: _jsx(PreviewPane, { store: store, chatAgents: chatAgents, chatWith: state.workspace.chatWith, compile: compile, run: run, connection: connection, chatRef: chatRef, setSheet: setSheet, onIssue: goToIssue, testing: testing && Boolean(compile.payload) }) }) }) }) })] }) }) }) }));
2051
1996
  }
2052
1997
  /** Save for the screen under it, when the studio is open on a project. */
2053
1998
  function SaveScope({ project, state, blocked, children, }) {
@@ -2074,10 +2019,34 @@ function useAddAgent(store, update, open) {
2074
2019
  /** What the tree reads of the page; opening a node closes the phone's sheet, and Keys selects nothing. */
2075
2020
  function useWorkspaceTree(state, view, selected, setSheet) {
2076
2021
  const { workspace, focus, update, setDraft } = state;
2077
- const { open } = view;
2078
- const selectedId = view.keysOpen ? '' : selected;
2022
+ const { open, keysOpen, setKeysOpen, setEditorView } = view;
2023
+ const selectedId = keysOpen ? '' : selected;
2079
2024
  const { list, setList } = useListShown(selectedId);
2080
2025
  const ownsProviders = useProject() !== null;
2026
+ const { hasSettings } = useStudioHost();
2027
+ const toast = useToast();
2028
+ const reset = useCallback((to, body) => to === workspace
2029
+ ? undefined
2030
+ : () => {
2031
+ update(() => to);
2032
+ const dismiss = toast({
2033
+ body,
2034
+ endContent: (_jsx(Button, { label: "Undo", variant: "ghost", size: "sm", onClick: () => {
2035
+ update(() => workspace);
2036
+ dismiss();
2037
+ } })),
2038
+ });
2039
+ }, [workspace, update, toast]);
2040
+ const keys = useMemo(() => hasSettings
2041
+ ? null
2042
+ : {
2043
+ open: keysOpen,
2044
+ toggle: () => {
2045
+ setKeysOpen((was) => !was);
2046
+ setEditorView('editor');
2047
+ setSheet(null);
2048
+ },
2049
+ }, [hasSettings, keysOpen, setKeysOpen, setEditorView, setSheet]);
2081
2050
  return useMemo(() => ({
2082
2051
  workspace,
2083
2052
  focus,
@@ -2091,7 +2060,22 @@ function useWorkspaceTree(state, view, selected, setSheet) {
2091
2060
  },
2092
2061
  update,
2093
2062
  setDraft,
2094
- }), [workspace, focus, selectedId, list, ownsProviders, setList, open, setSheet, update, setDraft]);
2063
+ reset,
2064
+ keys,
2065
+ }), [
2066
+ workspace,
2067
+ focus,
2068
+ selectedId,
2069
+ list,
2070
+ ownsProviders,
2071
+ setList,
2072
+ open,
2073
+ setSheet,
2074
+ update,
2075
+ setDraft,
2076
+ reset,
2077
+ keys,
2078
+ ]);
2095
2079
  }
2096
2080
  /** The resizable side panel: the tree beside the editor column (`children`), and its handle. */
2097
2081
  function SidePanel({ frame, tree, draft, onAddAgent, setSheet, settings, map, children, }) {
@@ -2120,13 +2104,12 @@ function OriginBanner({ origin, partly, onOpen, }) {
2120
2104
  return (_jsx(Section, { variant: "transparent", padding: 3, children: _jsx(Banner, { status: "info", title: originLabel(origin), description: `${effect} ${way}`, endContent: place && _jsx(Button, { label: "Open", variant: "ghost", size: "sm", onClick: onOpen }) }) }));
2121
2105
  }
2122
2106
  /** The editor column: its toolbar over the Keys panel, the open node's editor, or its code. */
2123
- function EditorColumn({ heading, state, connection, view, compile, selected, editing, frame, issueReveal, goToIssue, setSheet, }) {
2107
+ function EditorColumn({ heading, state, connection, view, compile, selected, editing, frame, issueReveal, goToIssue, copy, setSheet, }) {
2124
2108
  const { editorRef, revealed } = issueReveal;
2125
2109
  // Keys stands over the open node, so it doesn't say what sets it.
2126
2110
  const over = view.keysOpen;
2127
2111
  const shared = over ? undefined : sharedAt(state.shared, selected);
2128
2112
  const { shown, leavePage } = useLeftIssues(compile.editorIssues, selected, revealed);
2129
- const reset = useReset(state);
2130
2113
  const project = useProject();
2131
2114
  const openOrigin = useOpenOrigin(project);
2132
2115
  const rowOrigins = useMemo(() => {
@@ -2135,7 +2118,7 @@ function EditorColumn({ heading, state, connection, view, compile, selected, edi
2135
2118
  }, [state.origins, state.workspace, selected, openOrigin]);
2136
2119
  const whole = rowOrigins?.scope.section;
2137
2120
  const said = over ? undefined : (whole ?? rowOrigins?.scope.partly);
2138
- return (_jsxs(VStack, { height: "100%", children: [_jsx(EditorToolbar, { heading: heading, compile: compile, selected: selected, view: view, onIssue: goToIssue, reset: reset, project: project && (_jsxs(_Fragment, { children: [state.conflict && (_jsx(Token, { label: conflictsLabel(state.conflict.merge.conflicts.length), color: "orange", description: "Choose between studio edits and source code", onClick: () => {
2121
+ return (_jsxs(VStack, { height: "100%", children: [_jsx(EditorToolbar, { heading: heading, compile: compile, selected: selected, view: view, onIssue: goToIssue, getCode: _jsx(ExportMenu, { store: state.store, compile: compile, copy: copy }), project: project && (_jsxs(_Fragment, { children: [state.conflict && (_jsx(Token, { label: conflictsLabel(state.conflict.merge.conflicts.length), color: "orange", description: "Choose between studio edits and source code", onClick: () => {
2139
2122
  state.showConflict(true);
2140
2123
  } })), _jsx(ProjectTokens, { project: project, unloaded: state.files?.unloaded })] })), setSheet: setSheet }), shared && (_jsx(SharedLine, { entry: shared, workspace: state.workspace, selected: selected, onOpen: project
2141
2124
  ? () => {
package/ui/studio.css CHANGED
@@ -21,8 +21,8 @@
21
21
  }
22
22
 
23
23
  /*
24
- * A studio tree row's add or remove button shows on hover or keyboard focus, fading in,
25
- * so the tree reads as names at rest. Without hover (touch), it always shows.
24
+ * A studio tree row's buttons (reset, add, remove) show on hover or keyboard focus, fading in,
25
+ * so the tree reads as names at rest. Without hover (touch), they always show.
26
26
  */
27
27
  .studio-tree-row {
28
28
  position: relative;
@@ -51,26 +51,37 @@
51
51
  }
52
52
 
53
53
  /*
54
- * The button sits over the label's end rather than beside it, so it never widens the row; the
55
- * label fades out beneath it instead of being pushed along.
54
+ * The buttons sit over the label's end rather than beside it, so they never widen the row; the
55
+ * label fades out beneath them instead of being pushed along. Each button past the first fades a
56
+ * button's width further in.
56
57
  */
58
+ .studio-tree-row {
59
+ --studio-tree-fade: 0px;
60
+ }
61
+
62
+ .studio-tree-row:has(.studio-tree-action > :nth-child(2)) {
63
+ --studio-tree-fade: var(--spacing-8);
64
+ }
65
+
66
+ .studio-tree-row:has(.studio-tree-action > :nth-child(3)) {
67
+ --studio-tree-fade: calc(var(--spacing-8) * 2);
68
+ }
69
+
57
70
  @media (hover: none) {
58
71
  .studio-tree-row > button:not([data-tree-toggle]) {
59
- mask-image: linear-gradient(to left, transparent var(--spacing-6), #000 var(--spacing-10));
72
+ mask-image: linear-gradient(
73
+ to left,
74
+ transparent calc(var(--spacing-6) + var(--studio-tree-fade)),
75
+ #000 calc(var(--spacing-10) + var(--studio-tree-fade))
76
+ );
60
77
  }
61
78
  }
62
79
 
63
80
  .studio-tree-row:is(:hover, :has(:focus-visible)) > button:not([data-tree-toggle]) {
64
- mask-image: linear-gradient(to left, transparent var(--spacing-6), #000 var(--spacing-10));
65
- }
66
-
67
- /* A row with two buttons fades a button's width further in. */
68
- .studio-tree-row:has(.studio-tree-action > :nth-child(2)):is(:hover, :has(:focus-visible))
69
- > button:not([data-tree-toggle]) {
70
81
  mask-image: linear-gradient(
71
82
  to left,
72
- transparent calc(var(--spacing-6) + var(--spacing-8)),
73
- #000 calc(var(--spacing-10) + var(--spacing-8))
83
+ transparent calc(var(--spacing-6) + var(--studio-tree-fade)),
84
+ #000 calc(var(--spacing-10) + var(--studio-tree-fade))
74
85
  );
75
86
  }
76
87
 
@@ -221,6 +232,11 @@ button.inspector-origin:hover {
221
232
  }
222
233
 
223
234
  @media (width >= 768px) {
235
+ /* The page is already inset, so the preview's panes meet its end and bottom as the editor does. */
236
+ .studio-preview {
237
+ --theorem-pane-edge: 0px;
238
+ }
239
+
224
240
  .studio-frame:is(.studio-map-open, .studio-settings-open) .studio-side {
225
241
  width: 100%;
226
242
  max-width: none;
@@ -0,0 +1,2 @@
1
+ /** Whether the page is phone-wide. */
2
+ export declare function usePhone(): boolean;
@@ -0,0 +1,13 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ /** The width below which the studio's text buttons keep only their icon. */
3
+ const PHONE = '(width < 768px)';
4
+ /** Whether the page is phone-wide. */
5
+ export function usePhone() {
6
+ return useSyncExternalStore((change) => {
7
+ const query = globalThis.matchMedia(PHONE);
8
+ query.addEventListener('change', change);
9
+ return () => {
10
+ query.removeEventListener('change', change);
11
+ };
12
+ }, () => globalThis.matchMedia(PHONE).matches, () => false);
13
+ }
package/workspace.d.ts CHANGED
@@ -112,6 +112,13 @@ export declare function startedHere(workspace: StudioWorkspace): StudioWorkspace
112
112
  * Nothing else changes. An agent already at its start returns the same workspace.
113
113
  */
114
114
  export declare function resetAgent(workspace: StudioWorkspace, key: string): StudioWorkspace;
115
+ /**
116
+ * Puts one row of an agent's tree back to its start: a section, or one model under Models. The
117
+ * agent's other sections keep their edits. The agent's own row puts the whole agent back, as
118
+ * `resetAgent` does, and Tools brings back a tool that left the library as it does too. A row
119
+ * already at its start returns the same workspace.
120
+ */
121
+ export declare function resetAgentNode(workspace: StudioWorkspace, key: string, ref: StudioNodeRef): StudioWorkspace;
115
122
  /**
116
123
  * Puts every agent, every library tool, and every provider back to its start, in one step. A tool that left the
117
124
  * library comes back when an agent started out allowing it. An agent or a tool added here stays: it