diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5c1016db..727acfbf 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -55,3 +55,10 @@ jobs: uses: stateful/runme-action@v2 with: workflows: test + - name: Test agent widgets and key vault + run: >- + pnpm -C app exec vitest run + src/lib/agents src/lib/keyvault src/components/Agents src/components/KeyVault + src/lib/runtime/sandboxJsKernel.test.ts src/lib/notebookData.test.ts + src/components/Actions/Actions.test.tsx + --maxWorkers=2 --minWorkers=1 diff --git a/app/src/components/Actions/ActionOutputItems.tsx b/app/src/components/Actions/ActionOutputItems.tsx index ea8637a9..a365f4e1 100644 --- a/app/src/components/Actions/ActionOutputItems.tsx +++ b/app/src/components/Actions/ActionOutputItems.tsx @@ -1,3 +1,5 @@ +import { AgentMonitorOutput } from '../Agents/AgentMonitor' +import { AGENT_MONITOR_MIME } from '../../lib/agents/types' import { HtmlOutput } from './HtmlOutput' import React from 'react' import { LinkIcon } from '@heroicons/react/20/solid' @@ -86,7 +88,9 @@ export function ActionOutputItemView({ let content: React.ReactNode = null - if (mime === 'text/html') { + if (mime === AGENT_MONITOR_MIME) { + content = + } else if (mime === 'text/html') { content = ( { }) describe('Actions tabs', () => { + it('toggles agent widget source and render modes without executing or unmounting output', () => { + const cell = create(parser_pb.CellSchema, { + refId: 'widget-cell', kind: parser_pb.CellKind.CODE, languageId: 'javascript', value: 'agents.monitor("sess_test")', + outputs: [create(parser_pb.CellOutputSchema, { items: [create(parser_pb.CellOutputItemSchema, { + mime: 'application/vnd.runme.agent-monitor+json', + data: new TextEncoder().encode(JSON.stringify({version: 1, id: 'saved-widget', sessionId: 'sess_test', pageSize: 50})), + })] })], + }) + const stub = new StubCellData(cell) + stub.getRunnerName = () => APPKERNEL_RUNNER_NAME + const view = render() + const editor = view.container.querySelector('[data-cell-focus-role="editor"]') as HTMLElement + const output = view.container.querySelector('#widget-output-widget-cell') as HTMLElement + expect(editor.hidden).toBe(true) + const mounted = screen.getByTestId('agent-monitor') + fireEvent.click(screen.getByRole('button', {name: /^Edit$/})) + expect(editor.hidden).toBe(false) + expect(output.hidden).toBe(true) + expect(screen.getByTestId('agent-monitor')).toBe(mounted) + fireEvent.click(screen.getByRole('button', {name: /^Render$/})) + expect(output.hidden).toBe(false) + expect(stub.run).not.toHaveBeenCalled() + view.unmount() + }) + it.each([ ['url', true, true, true], @@ -972,7 +997,8 @@ describe('Actions tabs', () => { ) expect(await screen.findByText('Feedback from diff')).toBeTruthy() expect(screen.getByText(/Historical source/)).toBeTruthy() - }) + // Multiple rendered edit/reply/resolve cycles exceed 5s on shared CI CPUs. + }, 20_000) it.each(['rendered', 'code', 'markdown-source'])( 'submits a %s selection through real operation-log persistence after a pending flush and later edit', @@ -3808,7 +3834,9 @@ describe('Action component', () => { message: 'Selected text copied', tone: 'success', }) - expect(screen.queryByRole('button', { name: 'Copy' })).toBeNull() + await waitFor(() => { + expect(screen.queryByRole('button', { name: 'Copy' })).toBeNull() + }) selection.removeAllRanges() }) diff --git a/app/src/components/Actions/Actions.tsx b/app/src/components/Actions/Actions.tsx index d01f4981..dbcd85bc 100644 --- a/app/src/components/Actions/Actions.tsx +++ b/app/src/components/Actions/Actions.tsx @@ -1,3 +1,4 @@ +import { AGENT_MONITOR_MIME } from '../../lib/agents/types' import { outputReferenceSource } from '../../lib/outputReferenceRuntime' import { OutputReferenceCell } from './OutputReferenceCell' import { @@ -264,7 +265,8 @@ function syncIndicatorPresentation(state: NotebookSyncState | null): { switch (state?.status) { case 'not-downloaded': return { - label: 'Notebook has not been downloaded to this browser. Click to download now.', + label: + 'Notebook has not been downloaded to this browser. Click to download now.', className: 'border border-nb-text-faint bg-transparent', clickable: true, } @@ -805,6 +807,12 @@ export function Action({ }) const shareTargetUri = shareTarget.docUri === docUri ? shareTarget.targetUri : null + const [widgetEditing, setWidgetEditing] = useState(false) + const hasWidgetOutput = + cell?.outputs?.some((output) => + output.items.some((item) => item.mime === AGENT_MONITOR_MIME) + ) ?? false + const [widgetSourceChanged, setWidgetSourceChanged] = useState(false) const [htmlEditRequest, setHtmlEditRequest] = useState(0) const [markdownEditRequest, setMarkdownEditRequest] = useState(0) const [pid, setPid] = useState(null) @@ -914,6 +922,8 @@ export function Action({ if (readOnly) { return } + setWidgetEditing(false) + setWidgetSourceChanged(false) cellData.run() }, [cellData, readOnly]) @@ -2133,6 +2143,7 @@ export function Action({ {/* Minimal toolbar: language + runner selectors + run/trash buttons */} + {hasWidgetOutput && ( +
+ + + {widgetSourceChanged && ( + Source changed — Run to update the monitor. + )} +
+ )}