@theoremjs/studio 0.3.4-next.403.g5de27f3 → 0.3.4-next.407.gd93ec2c

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/browser-engine.d.ts +1 -1
  2. package/browser-engine.js +1 -1
  3. package/browser-transport.d.ts +12 -0
  4. package/browser-transport.js +26 -7
  5. package/browser.d.ts +1 -1
  6. package/browser.js +1 -1
  7. package/mod.d.ts +2 -1
  8. package/mod.js +2 -1
  9. package/package.json +4 -4
  10. package/page/assets/{Banner-CLcFr8r9.js → Banner-B1Vat7i_.js} +2 -2
  11. package/page/assets/{ContextMenu-CArbXxnu.js → ContextMenu-BreVLY9J.js} +2 -2
  12. package/page/assets/{ToolResultCharts-DG0GfEe-.js → ToolResultCharts-BV2tbzow.js} +1 -1
  13. package/page/assets/{TraceInspector-DlcrrHNa.js → TraceInspector-B8NufbVX.js} +1 -1
  14. package/page/assets/{TraceOverview-BRgK49q4.js → TraceOverview-DoQOfiAe.js} +2 -2
  15. package/page/assets/{browser-engine-yDBh7tWN.js → browser-engine-BvsLwtiv.js} +6 -6
  16. package/page/assets/browser-transport-DxpDmXGb.js +47 -0
  17. package/page/assets/{export-agent-DuPZHGrj.js → export-agent-D2Vo-OSL.js} +3 -3
  18. package/page/assets/{guardrail-tester-xtRSrTXh.js → guardrail-tester-CuqamClw.js} +2 -2
  19. package/page/assets/{index-BVTCzZhh.js → index-C3mDKoGG.js} +69 -69
  20. package/page/assets/{live-messages-CaI0Oqem.js → live-messages-BZGmMo_v.js} +1 -1
  21. package/page/assets/{reader-CaGeZoFz.js → reader-paEmSrWc.js} +4 -4
  22. package/page/assets/{studio-code-pane-_XzNjN6P.js → studio-code-pane-B9bG0xzf.js} +1 -1
  23. package/page/assets/{studio-console-Da8ZVuKx.js → studio-console-1qIWsDtV.js} +3 -3
  24. package/page/assets/studio-decision-BdJR3HSN.js +1 -0
  25. package/page/assets/{studio-embedding-BaWl7liT.js → studio-embedding-CFzitRLj.js} +1 -1
  26. package/page/assets/studio-host-console-BzpDE2TK.js +2 -0
  27. package/page/assets/{studio-live-ON74y8sM.js → studio-live-BucphGSr.js} +2 -2
  28. package/page/assets/{studio-map-W8XbAypV.js → studio-map-BfUMbnZK.js} +1 -1
  29. package/page/assets/studio-trace-replay-C5cBxYG_.js +2 -0
  30. package/page/assets/turn-client-BZUECwPn.js +2 -0
  31. package/page/assets/{use-one-call-BD3tlw64.js → use-one-call-BVmuHLSm.js} +1 -1
  32. package/page/index.html +6 -6
  33. package/replay.d.ts +97 -0
  34. package/replay.js +387 -0
  35. package/server/draft-handler.js +2 -1
  36. package/transport.d.ts +5 -1
  37. package/transport.js +7 -2
  38. package/ui/inspector-context.d.ts +1 -1
  39. package/ui/lib/send-when-ready.d.ts +7 -0
  40. package/ui/lib/send-when-ready.js +14 -0
  41. package/ui/studio-connection.d.ts +1 -1
  42. package/ui/studio-runner.d.ts +4 -1
  43. package/ui/studio-runner.js +3 -2
  44. package/ui/studio-screen.js +111 -11
  45. package/ui/studio-trace-replay.d.ts +23 -0
  46. package/ui/studio-trace-replay.js +216 -0
  47. package/page/assets/browser-transport-B0GPqGzb.js +0 -47
  48. package/page/assets/studio-decision-CHaFsz_1.js +0 -1
  49. package/page/assets/studio-host-console-BuPmNkLs.js +0 -2
  50. package/page/assets/turn-client-BBi2anJM.js +0 -2
@@ -21,13 +21,14 @@ import { Token } from '@astryxdesign/core/Token';
21
21
  import { Tooltip } from '@astryxdesign/core/Tooltip';
22
22
  import { TreeList } from '@astryxdesign/core/TreeList';
23
23
  import { VStack } from '@astryxdesign/core/VStack';
24
- import { IconActivity, IconAdjustmentsHorizontal, IconArrowBarToDown, IconArrowLeft, IconBrowserShare, IconCheck, IconCloud, IconCode, IconCopy, IconCopyPlus, IconFile, 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, IconDownload, IconFile, IconFileImport, IconFileSmile, IconFileZip, IconKey, IconMenu2, IconPlayerPlay, IconPlaylistX, IconPlus, IconRestore, IconRotateClockwise, IconSearch, IconShieldSearch, 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
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";
29
29
  import { studioSurface } from "../surface.js";
30
30
  import './studio.css';
31
+ import { createTraceFeed } from '@theoremjs/web/client';
31
32
  import { lazy, memo, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
32
33
  import { flushSync } from 'react-dom';
33
34
  import { ActingAsButton } from "./acting-as-button.js";
@@ -40,6 +41,7 @@ import { FACET_ICON } from "./lib/facet-icons.js";
40
41
  import { movePanel } from "./lib/panel-motion.js";
41
42
  import { screenEndpoint } from "./lib/project-address.js";
42
43
  import { recordsTraces } from "./lib/project-traces.js";
44
+ import { sendWhenReady } from "./lib/send-when-ready.js";
43
45
  import { clearConversation, restoreConversation, saveConversation, } from "./lib/studio-conversation.js";
44
46
  import { STUDIO_EXAMPLES } from "./lib/studio-examples.js";
45
47
  import { pageInputsOf, sentPageValues, usePageValues } from "./lib/studio-page.js";
@@ -72,6 +74,8 @@ const StudioMap = lazy(() => mapPanel().then((map) => ({ default: map.StudioMap
72
74
  const CodeBody = lazy(() => codePane().then((code) => ({ default: code.CodeBody })));
73
75
  const ProviderCode = lazy(() => codePane().then((code) => ({ default: code.ProviderCode })));
74
76
  const GuardrailTester = lazy(() => testerPanel().then((tester) => ({ default: tester.GuardrailTester })));
77
+ const traceReplay = () => import("./studio-trace-replay.js");
78
+ const TraceReplay = lazy(() => traceReplay().then((pane) => ({ default: pane.TraceReplay })));
75
79
  const StudioSettings = lazy(() => settingsPanel().then((settings) => ({ default: settings.StudioSettings })));
76
80
  /** A node's icon; Identity shows the profile type's once one is picked. */
77
81
  function nodeIcon(draft, ref) {
@@ -1240,7 +1244,11 @@ function useConversationRun(mode, sealer, chatWith, actingAs, question) {
1240
1244
  firstRuns.set(chatKey, runKey);
1241
1245
  const isFirstRun = firstRuns.get(chatKey) === runKey;
1242
1246
  const [seeded] = useState(chatWith);
1243
- const conversation = useMemo(() => (isFirstRun ? restoreConversation(chatKey, sealer) : undefined), [isFirstRun, chatKey, sealer]);
1247
+ /** A conversation started for a chat's next runner: what it resumes and draws at once. */
1248
+ const [resumed, setResumed] = useState();
1249
+ const resumes = resumed?.runKey === runKey ? resumed : undefined;
1250
+ const conversation = useMemo(() => (resumes ? resumes.ref : isFirstRun ? restoreConversation(chatKey, sealer) : undefined), [resumes, isFirstRun, chatKey, sealer]);
1251
+ const conversationView = resumes?.view;
1244
1252
  /** A docs seed's question waits in its agent's first composer, when that resumes nothing. */
1245
1253
  const initialText = isFirstRun && chatWith === seeded && !conversation ? question : undefined;
1246
1254
  /**
@@ -1255,6 +1263,7 @@ function useConversationRun(mode, sealer, chatWith, actingAs, question) {
1255
1263
  generation,
1256
1264
  sealer,
1257
1265
  conversation,
1266
+ conversationView,
1258
1267
  initialText,
1259
1268
  isUsed,
1260
1269
  markUsed: () => {
@@ -1265,8 +1274,25 @@ function useConversationRun(mode, sealer, chatWith, actingAs, question) {
1265
1274
  clearConversation(chatKey);
1266
1275
  setCleared((counts) => ({ ...counts, [chatKey]: (counts[chatKey] ?? 0) + 1 }));
1267
1276
  },
1277
+ /** Goes on in a new runner with the conversation under `ref`, drawn as `view` at once. */
1278
+ resume: (ref, view) => {
1279
+ saveConversation(chatKey, sealer, ref);
1280
+ const next = (cleared[chatKey] ?? 0) + 1;
1281
+ setResumed({ runKey: `${generation}:${chatKey}:${String(next)}`, ref, view });
1282
+ setCleared((counts) => ({ ...counts, [chatKey]: next }));
1283
+ },
1268
1284
  setConversation,
1269
- }), [runKey, chatKey, generation, sealer, conversation, initialText, isUsed]);
1285
+ }), [
1286
+ runKey,
1287
+ chatKey,
1288
+ generation,
1289
+ sealer,
1290
+ conversation,
1291
+ conversationView,
1292
+ initialText,
1293
+ isUsed,
1294
+ cleared,
1295
+ ]);
1270
1296
  }
1271
1297
  /**
1272
1298
  * Once, on arrival: say when a kept draft was set aside for a docs seed, when a project's files
@@ -1732,15 +1758,32 @@ function EditorBody({ state, connection, selected, editing, issues, listBadges,
1732
1758
  function ClearHistoryButton({ run }) {
1733
1759
  return (_jsx(IconButton, { label: "Clear history", variant: "ghost", icon: _jsx(Icon, { icon: IconPlaylistX, size: "sm" }), tooltip: "Clear the conversation and its traces. Your profile stays.", onClick: run.clear }));
1734
1760
  }
1761
+ /** The types whose conversation a trace can hold, and so open and replay. */
1762
+ 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
+ /** Open trace, for a type whose conversation a trace holds, and Download trace, outside an opened one. */
1772
+ function TraceFileButtons({ payload, tracing, feed, profileId, }) {
1773
+ if (payload == null)
1774
+ return null;
1775
+ const openLabel = tracing.open ? 'Close trace' : 'Open trace';
1776
+ return (_jsxs(_Fragment, { children: [!NO_CONVERSATION.has(payload.profile.type) && (_jsx(IconButton, { label: openLabel, variant: tracing.open ? 'secondary' : 'ghost', tooltip: openLabel, icon: _jsx(Icon, { icon: IconFileImport, size: "sm" }), "aria-pressed": tracing.open, onClick: tracing.toggle })), !tracing.open && _jsx(DownloadTraceButton, { feed: feed, name: profileId ?? 'agent' })] }));
1777
+ }
1735
1778
  /** The preview's header: back to the editor, the agent to chat with, history, trace, and Export. */
1736
- function PreviewHeader({ store, chatAgents, chatWith, compile, run, connection, trace, testing, copy, setSheet, }) {
1779
+ function PreviewHeader({ store, chatAgents, chatWith, compile, run, connection, trace, testing, tracing, feed, profileId, copy, setSheet, }) {
1737
1780
  const testLabel = testing.open ? 'Close guardrail test' : 'Test guardrails';
1738
1781
  const traceLabel = trace.open ? 'Hide trace' : 'View trace';
1739
1782
  const { compiled } = compile;
1740
- 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 && _jsx(ClearHistoryButton, { run: run }), 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: {
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: {
1741
1784
  connectionMode: connection.mode,
1742
1785
  localBaseUrl: connection.local.baseUrl,
1743
- } }), compile.traced && !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] }) })] }) }));
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] }) })] }) }));
1744
1787
  }
1745
1788
  /**
1746
1789
  * Why the agent to chat with does not run: the errors in its config, or the workspace's when its
@@ -1762,7 +1805,7 @@ function ConfigIssues({ compile, chatWith, onIssue, }) {
1762
1805
  * The runner for the compiled agent, its conversation saved as it goes, or the guardrail tester
1763
1806
  * over it; while none compiles, why.
1764
1807
  */
1765
- function PreviewBody({ compile, run, connection, chatRef, chatWith, profileId, traceOpen, testing, onIssue, }) {
1808
+ function PreviewBody({ compile, run, connection, chatRef, chatWith, profileId, traceOpen, testing, tracing, feedOf, onIssue, }) {
1766
1809
  const { payload, traced, tested } = compile;
1767
1810
  /** Why the project's server does not run this profile: its files say, not the editor. */
1768
1811
  const unserved = useProject()?.problems.find((problem) => problem.profile === profileId);
@@ -1777,6 +1820,7 @@ function PreviewBody({ compile, run, connection, chatRef, chatWith, profileId, t
1777
1820
  payload,
1778
1821
  onActivity: run.markUsed,
1779
1822
  conversation: run.conversation,
1823
+ conversationView: run.conversationView,
1780
1824
  initialText: run.initialText,
1781
1825
  onConversationChange: (ref) => {
1782
1826
  if (ref)
@@ -1792,9 +1836,9 @@ function PreviewBody({ compile, run, connection, chatRef, chatWith, profileId, t
1792
1836
  : null;
1793
1837
  const kept = useKeptRuns(run, shown);
1794
1838
  const blocked = unserved ? (_jsx(PaneFailure, { title: `${unserved.profile} does not run here yet`, description: unserved.message })) : payload ? null : (_jsx(ConfigIssues, { compile: compile, chatWith: chatWith, onIssue: onIssue }));
1795
- return (_jsx(InPlace, { view: testing && payload ? (_jsx(RaisedPane, { children: (isWide) => (_jsx(Suspense, { fallback: null, children: _jsx(GuardrailTester, { payload: payload, isWide: isWide }) })) })) : null, children: _jsx(VStack, { height: "100%", children: _jsxs(StackItem, { size: "fill", children: [blocked, kept.map(([key, { props }]) => {
1839
+ return (_jsx(InPlace, { view: testing && payload ? (_jsx(RaisedPane, { children: (isWide) => (_jsx(Suspense, { fallback: null, children: _jsx(GuardrailTester, { payload: payload, isWide: isWide }) })) })) : (tracing), children: _jsx(VStack, { height: "100%", children: _jsxs(StackItem, { size: "fill", children: [blocked, kept.map(([key, { props }]) => {
1796
1840
  const isShown = shown !== null && key === run.runKey;
1797
- return (_jsx("div", { style: { display: isShown ? 'contents' : 'none' }, children: _jsx(StudioRunner, { ...props, mode: connection.mode, runtime: connection.runtime, trace: traced ? traceOpen : undefined, chatRef: isShown ? chatRef : undefined }) }, key));
1841
+ return (_jsx("div", { style: { display: isShown ? 'contents' : 'none' }, children: _jsx(StudioRunner, { ...props, mode: connection.mode, runtime: connection.runtime, trace: traced ? traceOpen : undefined, chatRef: isShown ? chatRef : undefined, traces: feedOf(key) }) }, key));
1798
1842
  })] }) }) }));
1799
1843
  }
1800
1844
  /**
@@ -1813,6 +1857,44 @@ function useKeptRuns(run, shown) {
1813
1857
  kept.set(run.runKey, shown);
1814
1858
  return [...kept];
1815
1859
  }
1860
+ /**
1861
+ * The trace feed of each runner, by its run key: the header downloads the one on screen. Another
1862
+ * mode or a restart drops them all, as it drops the runners.
1863
+ */
1864
+ function useRunFeeds(generation) {
1865
+ const [feeds] = useState(() => new Map());
1866
+ return useCallback((runKey) => {
1867
+ for (const key of feeds.keys())
1868
+ if (!key.startsWith(`${generation}:`))
1869
+ feeds.delete(key);
1870
+ let feed = feeds.get(runKey);
1871
+ if (!feed) {
1872
+ feed = createTraceFeed();
1873
+ feeds.set(runKey, feed);
1874
+ }
1875
+ return feed;
1876
+ }, [feeds, generation]);
1877
+ }
1878
+ /** Sends a message once the runner for the next run key is ready: a conversation goes on live. */
1879
+ function useSendAfterResume(chatRef, runKey) {
1880
+ const [next, setNext] = useState();
1881
+ useEffect(() => {
1882
+ if (!next?.text || next.after === runKey)
1883
+ return;
1884
+ const controller = new AbortController();
1885
+ // why: cleared once sent, not before: clearing re-renders, and the cleanup would abort the send.
1886
+ void sendWhenReady(chatRef, next.text, controller.signal).then(() => {
1887
+ if (!controller.signal.aborted)
1888
+ setNext(undefined);
1889
+ });
1890
+ return () => {
1891
+ controller.abort();
1892
+ };
1893
+ }, [next, runKey, chatRef]);
1894
+ return useCallback((text) => {
1895
+ setNext({ text, after: runKey });
1896
+ }, [runKey]);
1897
+ }
1816
1898
  /**
1817
1899
  * The compiled agent to chat with, under its header; while none compiles, why. It sits out renders
1818
1900
  * that change nothing it shows: typing in the editor, opening a node, Keys.
@@ -1820,6 +1902,18 @@ function useKeptRuns(run, shown) {
1820
1902
  const PreviewPane = memo(function PreviewPane({ store, chatAgents, chatWith, compile, run, connection, chatRef, copy, setSheet, onIssue, }) {
1821
1903
  const [traceOpen, setTraceOpen] = useState(false);
1822
1904
  const [testing, setTesting] = useState(false);
1905
+ const [tracing, setTracing] = useState(false);
1906
+ const feedOf = useRunFeeds(run.generation);
1907
+ const profileId = chatAgents.find((agent) => agent.value === chatWith)?.label;
1908
+ const sendNext = useSendAfterResume(chatRef, run.runKey);
1909
+ const { payload } = compile;
1910
+ const traceView = tracing && payload ? (_jsx(Suspense, { fallback: null, children: _jsx(TraceReplay, { payload: payload, runtime: connection.runtime, trace: traceOpen, onClose: () => {
1911
+ setTracing(false);
1912
+ }, onLive: ({ conversation, view, text }) => {
1913
+ run.resume(conversation, view);
1914
+ setTracing(false);
1915
+ sendNext(text);
1916
+ } }) })) : null;
1823
1917
  return (_jsx(LayoutContent, { className: "studio-preview shell-fade", isScrollable: false, padding: 0, children: _jsxs(VStack, { height: "100%", children: [_jsx(PreviewHeader, { store: store, chatAgents: chatAgents, chatWith: chatWith, compile: compile, run: run, connection: connection, trace: {
1824
1918
  open: traceOpen,
1825
1919
  toggle: () => {
@@ -1829,8 +1923,14 @@ const PreviewPane = memo(function PreviewPane({ store, chatAgents, chatWith, com
1829
1923
  open: testing,
1830
1924
  toggle: () => {
1831
1925
  setTesting((open) => !open);
1926
+ setTracing(false);
1927
+ },
1928
+ }, tracing: {
1929
+ open: tracing,
1930
+ toggle: () => {
1931
+ setTracing((open) => !open);
1832
1932
  },
1833
- }, copy: copy, setSheet: setSheet }), _jsx(StackItem, { size: "fill", children: _jsx(PreviewBody, { compile: compile, run: run, connection: connection, chatRef: chatRef, chatWith: chatWith, profileId: chatAgents.find((agent) => agent.value === chatWith)?.label, traceOpen: traceOpen, testing: testing, onIssue: onIssue }) })] }) }));
1933
+ }, 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 }) })] }) }));
1834
1934
  });
1835
1935
  /** Tells the host what is on screen as it changes, and that it closed. */
1836
1936
  function useScreenReport(onReport, { agent, type, issues, section }) {
@@ -2074,7 +2174,7 @@ function EditorColumnBody({ state, connection, view, compile, rowIssues, selecte
2074
2174
  /** The studio, opened on `opened`, with what the page showing it provides. */
2075
2175
  export function StudioScreen({ opened, host }) {
2076
2176
  useEffect(() => {
2077
- warmWhenIdle(testerPanel, mapPanel, codePane, exportKit);
2177
+ warmWhenIdle(testerPanel, traceReplay, mapPanel, codePane, exportKit);
2078
2178
  }, []);
2079
2179
  return (_jsx(StudioHostContext.Provider, { value: host, children: _jsx(Studio, { opened: opened }) }));
2080
2180
  }
@@ -0,0 +1,23 @@
1
+ import type { TraceRecord } from '@theoremjs/agents/observability';
2
+ import { type ConversationView } from '@theoremjs/web/client';
3
+ import { type StudioBrowserRuntime } from '../browser.ts';
4
+ import { type StudioRunPayload } from '../mod.ts';
5
+ /** A trace file the builder opened. */
6
+ export interface OpenedTrace {
7
+ name: string;
8
+ records: TraceRecord[];
9
+ }
10
+ /** A live conversation started where a replay stopped: its ref, its view, and the message to send next. */
11
+ export interface LiveStart {
12
+ conversation: string;
13
+ view: ConversationView;
14
+ text: string;
15
+ }
16
+ /** The trace pane: a place to open one, then what it recorded and its replay. */
17
+ export declare function TraceReplay({ payload, trace, runtime, onClose, onLive, }: {
18
+ payload: StudioRunPayload;
19
+ trace: boolean | undefined;
20
+ onClose: () => void;
21
+ runtime: StudioBrowserRuntime | null;
22
+ onLive: (start: LiveStart) => void;
23
+ }): import("react").JSX.Element;
@@ -0,0 +1,216 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * An opened trace: the conversation it recorded, as the person saw it, and a replay of it through
4
+ * the profile as it is now. A replay reaches no model and, unless Live tools is on, no tool: the
5
+ * recording answers both. Where the profile asks for something the recording does not hold, the
6
+ * replay stops and says why; Continue live goes on from there with the real model.
7
+ *
8
+ * @module
9
+ */
10
+ import { Banner } from '@astryxdesign/core/Banner';
11
+ import { Button } from '@astryxdesign/core/Button';
12
+ import { FileInput } from '@astryxdesign/core/FileInput';
13
+ import { HStack } from '@astryxdesign/core/HStack';
14
+ import { Icon } from '@astryxdesign/core/Icon';
15
+ import { IconButton } from '@astryxdesign/core/IconButton';
16
+ import { Section } from '@astryxdesign/core/Section';
17
+ import { SegmentedControl, SegmentedControlItem } from '@astryxdesign/core/SegmentedControl';
18
+ import { StackItem } from '@astryxdesign/core/Stack';
19
+ import { Switch } from '@astryxdesign/core/Switch';
20
+ import { Text } from '@astryxdesign/core/Text';
21
+ import { VStack } from '@astryxdesign/core/VStack';
22
+ import { IconFileImport, IconPlayerPlay, IconX } from '@tabler/icons-react';
23
+ import { PaneState, TheoremChat } from '@theoremjs/react/ui';
24
+ import { createTraceFeed, } from '@theoremjs/web/client';
25
+ import { useEffect, useMemo, useRef, useState } from 'react';
26
+ import { createBrowserStudioTransport, startBrowserStudioConversation, } from "../browser.js";
27
+ import { createReplay, parseTraceFile, recordedPayload, recordedTranscript, replayScript, startStudioConversation, studioInterface, studioPageTools, } from "../mod.js";
28
+ import { sendWhenReady } from "./lib/send-when-ready.js";
29
+ import { pageConversationKey } from "./lib/studio-conversation.js";
30
+ import { STUDIO_LABELS } from "./lib/studio-labels.js";
31
+ /**
32
+ * A live conversation on `blocks`, kept where the live chat keeps its own: sealed in the page
33
+ * when it runs there, else in this visitor's session on the server.
34
+ */
35
+ function startLive(payload, runtime, blocks) {
36
+ return runtime
37
+ ? startBrowserStudioConversation(payload, runtime, {
38
+ conversationKey: pageConversationKey(),
39
+ blocks,
40
+ })
41
+ : startStudioConversation(payload, blocks);
42
+ }
43
+ const ordinal = (turn) => `turn ${String(turn + 1)}`;
44
+ /** A chat that only shows: the conversation a trace recorded, and its records beside it. */
45
+ function recordedTransport(payload, transcript, traces) {
46
+ const refused = () => Promise.reject(new Error('A recorded conversation cannot change. Replay it, then continue live.'));
47
+ return {
48
+ describe: () => Promise.resolve(studioInterface(payload)),
49
+ turn: refused,
50
+ invoke: refused,
51
+ steer: refused,
52
+ rewind: refused,
53
+ conversation: () => Promise.resolve({ transcript }),
54
+ traces,
55
+ };
56
+ }
57
+ /** The pane to open a trace in: a place to drop it, and why the last one did not open. */
58
+ function OpenTrace({ onOpen }) {
59
+ const [problem, setProblem] = useState();
60
+ const [reading, setReading] = useState(false);
61
+ const changeAction = async (files) => {
62
+ const file = Array.isArray(files) ? files[0] : files;
63
+ if (!file)
64
+ return;
65
+ setReading(true);
66
+ try {
67
+ onOpen({ name: file.name, records: parseTraceFile(await file.text()) });
68
+ setProblem(undefined);
69
+ }
70
+ catch (error) {
71
+ setProblem(error instanceof Error ? error.message : String(error));
72
+ }
73
+ finally {
74
+ setReading(false);
75
+ }
76
+ };
77
+ return (_jsx(PaneState, { icon: _jsx(Icon, { icon: IconFileImport, size: "lg", color: "secondary" }), title: "Open a trace", description: "See how a conversation went, then replay it through this profile as it is now. A replay answers from the recording, so it needs no keys.", children: _jsxs(VStack, { gap: 2, children: [_jsx(FileInput, { label: "Trace file", isLabelHidden: true, mode: "dropzone", accept: "application/json,.json", isLoading: reading, placeholder: "Drop a trace (.json) or choose a file", value: null, changeAction: changeAction, onChange: () => undefined }), problem ? _jsx(Banner, { status: "error", title: problem }) : null] }) }));
78
+ }
79
+ /** What the replay is doing, or where and why it stopped, with the way on. */
80
+ function StatusBanner({ status, turns, onContinue, onLive, }) {
81
+ const [starting, setStarting] = useState(false);
82
+ const [problem, setProblem] = useState();
83
+ const live = (turn) => (_jsx(Button, { label: "Continue live", variant: "secondary", size: "sm", isLoading: starting, onClick: async () => {
84
+ setStarting(true);
85
+ setProblem(undefined);
86
+ try {
87
+ await onLive(turn);
88
+ }
89
+ catch (error) {
90
+ setProblem(error instanceof Error ? error.message : String(error));
91
+ setStarting(false);
92
+ }
93
+ } }));
94
+ const banner = (() => {
95
+ switch (status.kind) {
96
+ case 'running':
97
+ return (_jsx(Banner, { status: "info", title: `Replaying ${ordinal(status.turn)} of ${String(turns)}` }));
98
+ case 'waiting':
99
+ return (_jsx(Banner, { status: "info", title: `${ordinal(status.turn - 1)} waits on you`, description: "Answer it in the conversation, then continue the replay.", endContent: _jsx(Button, { label: "Continue replay", variant: "secondary", size: "sm", onClick: () => {
100
+ onContinue(status.turn);
101
+ } }) }));
102
+ case 'missing':
103
+ return (_jsx(Banner, { status: "warning", title: `Replay stopped before ${ordinal(status.turn)}`, description: "It was sent with files, and a trace never holds a file's bytes. Continue live to send it again with yours.", endContent: live(status.turn) }));
104
+ case 'diverged': {
105
+ const { turn, call, reason } = status.divergence;
106
+ return (_jsx(Banner, { status: "warning", title: `Replay stopped at ${ordinal(turn)}, model call ${String(call)}`, description: reason, endContent: live(turn) }));
107
+ }
108
+ case 'failed':
109
+ return (_jsx(Banner, { status: "error", title: `Replay stopped at ${ordinal(status.turn)}`, description: status.message, endContent: live(status.turn) }));
110
+ case 'done':
111
+ return (_jsx(Banner, { status: "success", title: `Replayed ${String(turns)} ${turns === 1 ? 'turn' : 'turns'}`, description: "Every model call this profile made was one the recording answered." }));
112
+ }
113
+ })();
114
+ return (_jsxs(VStack, { gap: 1, children: [banner, problem ? _jsx(Banner, { status: "error", title: problem }) : null] }));
115
+ }
116
+ /** The replay of `script`: the chat it runs in, driven turn by turn, and where it stands. */
117
+ function ReplayRun({ payload, script, liveTools, trace, runtime, onLive, }) {
118
+ const chatRef = useRef(null);
119
+ const diverged = useRef(undefined);
120
+ const [status, setStatus] = useState({ kind: 'running', turn: 0 });
121
+ const [traces] = useState(createTraceFeed);
122
+ const replay = useMemo(() => createReplay(script, payload.profile.id, (divergence) => {
123
+ diverged.current ??= divergence;
124
+ }), [script, payload.profile.id]);
125
+ const ran = useMemo(() => (liveTools ? payload : recordedPayload(payload)), [liveTools, payload]);
126
+ const pageTools = useMemo(() => (liveTools ? studioPageTools(payload) : undefined), [liveTools, payload]);
127
+ const transport = useMemo(() => createBrowserStudioTransport(ran, {
128
+ mode: 'byok',
129
+ provider: replay.provider,
130
+ ...(liveTools ? {} : { toolHandlers: replay.toolHandlers }),
131
+ }, { conversationKey: pageConversationKey(), traces }), [ran, replay, liveTools, traces]);
132
+ const [from, setFrom] = useState(0);
133
+ useEffect(() => {
134
+ const controller = new AbortController();
135
+ const { signal } = controller;
136
+ void (async () => {
137
+ for (let turn = from; turn < script.turns.length; turn += 1) {
138
+ if (signal.aborted)
139
+ return;
140
+ const { text, attachments } = script.turns[turn];
141
+ if (attachments > 0)
142
+ return setStatus({ kind: 'missing', turn });
143
+ replay.startTurn(turn);
144
+ setStatus({ kind: 'running', turn });
145
+ try {
146
+ // why: The chat takes nothing until it has described its agent; later turns find it ready.
147
+ const sent = turn === 0
148
+ ? await sendWhenReady(chatRef, text, signal)
149
+ : ((await chatRef.current?.send(text)) ?? null);
150
+ if (signal.aborted)
151
+ return;
152
+ if (diverged.current)
153
+ return setStatus({ kind: 'diverged', divergence: diverged.current });
154
+ if (!sent) {
155
+ // The turn before waits on a gate: its reply goes on with the calls it has left.
156
+ replay.startTurn(turn - 1);
157
+ return setStatus({ kind: 'waiting', turn });
158
+ }
159
+ }
160
+ catch (error) {
161
+ const message = error instanceof Error ? error.message : String(error);
162
+ return setStatus({ kind: 'failed', turn, message });
163
+ }
164
+ }
165
+ setStatus({ kind: 'done' });
166
+ })();
167
+ return () => {
168
+ controller.abort();
169
+ chatRef.current?.stop();
170
+ };
171
+ }, [from, replay, script]);
172
+ const live = async (turn) => {
173
+ const blocks = script.turns.slice(0, turn).flatMap((each) => each.blocks);
174
+ const started = await startLive(payload, runtime, blocks);
175
+ onLive({ ...started, text: script.turns[turn]?.text ?? '' });
176
+ };
177
+ return (_jsxs(VStack, { height: "100%", children: [_jsx(Section, { variant: "transparent", paddingInline: 3, paddingBlock: 2, children: _jsx(StatusBanner, { status: status, turns: script.turns.length, onContinue: setFrom, onLive: live }) }), _jsx(StackItem, { size: "fill", children: _jsx(TheoremChat, { detectCodeLanguage: true, labels: STUDIO_LABELS, transport: transport, trace: trace, composer: false, chatRef: chatRef, pageTools: pageTools }) })] }));
178
+ }
179
+ const recordedAt = (script) => script.recordedAt === undefined
180
+ ? undefined
181
+ : new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(script.recordedAt);
182
+ /** An opened trace: its recorded conversation, or a replay of it, and the controls between them. */
183
+ function OpenedTraceView({ payload, opened, trace, runtime, onClose, onLive, }) {
184
+ const script = useMemo(() => replayScript(opened.records), [opened.records]);
185
+ const [view, setView] = useState('recorded');
186
+ const [attempt, setAttempt] = useState(0);
187
+ const [liveTools, setLiveTools] = useState(false);
188
+ const recorded = useMemo(() => {
189
+ const traces = createTraceFeed();
190
+ for (const record of opened.records)
191
+ traces.push(record);
192
+ return recordedTransport(payload, recordedTranscript(script), traces);
193
+ }, [payload, opened.records, script]);
194
+ const turns = script.turns.length;
195
+ const facts = [
196
+ script.agent || 'No agent',
197
+ `${String(turns)} ${turns === 1 ? 'turn' : 'turns'}`,
198
+ recordedAt(script),
199
+ ].filter(Boolean);
200
+ const replayOnce = () => {
201
+ setAttempt((count) => count + 1);
202
+ setView('replayed');
203
+ };
204
+ return (_jsxs(VStack, { height: "100%", children: [_jsx(Section, { variant: "transparent", paddingInline: 3, paddingBlock: 2, children: _jsxs(VStack, { gap: 2, children: [_jsxs(HStack, { gap: 2, vAlign: "center", children: [_jsx(StackItem, { size: "fill", children: _jsxs(VStack, { gap: 0, children: [_jsx(Text, { type: "label", maxLines: 1, children: opened.name }), _jsx(Text, { type: "supporting", color: "secondary", maxLines: 1, children: facts.join(' · ') })] }) }), attempt > 0 ? (_jsxs(SegmentedControl, { label: "Show", size: "sm", value: view, onChange: (next) => {
205
+ setView(next === 'replayed' ? 'replayed' : 'recorded');
206
+ }, children: [_jsx(SegmentedControlItem, { value: "recorded", label: "Recorded" }), _jsx(SegmentedControlItem, { value: "replayed", label: "Replayed" })] })) : null, _jsx(Switch, { label: "Live tools", size: "sm", value: liveTools, onChange: (next) => {
207
+ setLiveTools(next);
208
+ if (attempt > 0)
209
+ replayOnce();
210
+ } }), _jsx(Button, { label: attempt > 0 ? 'Replay again' : 'Replay', variant: "primary", size: "sm", icon: _jsx(Icon, { icon: IconPlayerPlay, size: "sm" }), isDisabled: turns === 0, onClick: replayOnce }), _jsx(IconButton, { label: "Close trace", variant: "ghost", tooltip: "Close trace", icon: _jsx(Icon, { icon: IconX, size: "sm" }), onClick: onClose })] }), script.agent && script.agent !== payload.profile.id ? (_jsx(Banner, { status: "info", title: `Recorded with ${script.agent}; a replay runs through ${payload.profile.id}.` })) : null, turns === 0 ? (_jsx(Banner, { status: "info", title: "This trace holds no conversation to replay." })) : null] }) }), _jsx(StackItem, { size: "fill", children: view === 'replayed' && attempt > 0 ? (_jsx(ReplayRun, { payload: payload, script: script, liveTools: liveTools, trace: trace, runtime: runtime, onLive: onLive }, attempt)) : (_jsx(TheoremChat, { detectCodeLanguage: true, labels: STUDIO_LABELS, transport: recorded, trace: trace, composer: false, conversation: "recorded", conversationView: { transcript: recordedTranscript(script) } }, opened.name)) })] }));
211
+ }
212
+ /** The trace pane: a place to open one, then what it recorded and its replay. */
213
+ export function TraceReplay({ payload, trace, runtime, onClose, onLive, }) {
214
+ const [opened, setOpened] = useState();
215
+ return opened ? (_jsx(OpenedTraceView, { payload: payload, opened: opened, trace: trace, runtime: runtime, onClose: onClose, onLive: onLive })) : (_jsx(OpenTrace, { onOpen: setOpened }));
216
+ }