Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6a56e1c
Simplify custom metric capture
Sep 30, 2026
966270d
feat: score replicates with a deterministic Tool Step and code-comput…
Sep 30, 2026
3b33c73
feat: enlarge Tool Step node, add main-edge stubs, chrome-white accent
Sep 30, 2026
47ddfbf
feat: drag-and-drop field for experiment definition import
Sep 30, 2026
df1c18b
Mask saved API keys in credential form
Sep 30, 2026
4334cf2
refactor: make the Tool Step generic
Sep 30, 2026
5d0718b
fix: name the old Tool Step config format in its validation error
Sep 30, 2026
31a193a
Make node output panels resizable
Sep 30, 2026
749c869
Show arrows for directed protocol strategies
Sep 30, 2026
63a1857
Make run overlays draggable and preserve collapse position
Sep 30, 2026
e3b9b11
Show non-test run conversations in a labeled results panel
Sep 30, 2026
1fa6946
Let Runs tab buttons publish and run an unpublished canvas
Oct 1, 2026
0db5349
Bring BDM myocardial experiments closer to the spinal notebook
Oct 1, 2026
be1b538
Record the notebook's critic, revision and recipe process metrics
Oct 1, 2026
09ef481
Let each main edge choose what it passes to the next agent
Oct 1, 2026
947cf9c
Let field references narrow a list to item keys
Oct 1, 2026
a6d15dc
Record the notebook's prompt hash and per-stage tokens and turns
Oct 1, 2026
2a9ae8d
Let custom metrics total runtime over chosen nodes
Oct 1, 2026
fcd0e31
Record the notebook's agent run and Reason+Act loop counts
Oct 1, 2026
46ad29e
Keep an edge's handoff when a node is inserted on it
Oct 1, 2026
62b1e5d
Handle duplicate skill registration conflicts
Oct 1, 2026
aba321f
Populate BDM runtime and hyperparameter results
Oct 2, 2026
270add3
Clarify MCP server name scope
Oct 2, 2026
3865bc9
Distinguish test and production run state
Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions frontend/src/components/CreateCredentialDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { llmSettingsApi } from '@/api/client'
import { CreateCredentialDialog } from './CreateCredentialDialog'

describe('CreateCredentialDialog', () => {
it('masks an Azure AI Foundry API key by default', async () => {
vi.spyOn(llmSettingsApi, 'list').mockResolvedValue([
{
provider: 'azure_foundry',
api_base: 'https://example.services.ai.azure.com',
azure_project_endpoint: 'https://example.services.ai.azure.com/api/projects/example',
},
])
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })

render(
<QueryClientProvider client={client}>
<CreateCredentialDialog open onOpenChange={vi.fn()} defaultProvider="azure_foundry" />
</QueryClientProvider>,
)

const apiKey = await screen.findByPlaceholderText('**********')

expect(apiKey).toHaveAttribute('type', 'password')
expect(apiKey).toHaveAttribute('placeholder', '**********')
expect(apiKey).toHaveValue('')
})
})
1 change: 1 addition & 0 deletions frontend/src/components/CreateCredentialDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export function CreateCredentialDialog({
<Label htmlFor="credential-api-key">API key{apiKeyOptional && ' (optional)'}</Label>
<PasswordInput
id="credential-api-key"
placeholder={existing ? '**********' : undefined}
value={apiKey}
onChange={(e) => {
setApiKey(e.target.value)
Expand Down
32 changes: 12 additions & 20 deletions frontend/src/components/CreateExperimentFromFileDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useEffect, useMemo, useRef, useState, type ChangeEvent } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { FileJson, Upload } from 'lucide-react'
import { Upload } from 'lucide-react'
import { ApiError, experimentsApi } from '@/api/client'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { FileDropInput } from '@/components/protocol/FileDropInput'
import type { DesignSpec, MeasurementPlan } from '@/types/experiments'
import type { ProtocolGraph } from '@/types/protocols'

Expand Down Expand Up @@ -89,9 +90,8 @@ export function CreateExperimentFromFileDialog({
const [definition, setDefinition] = useState<ImportedDefinition | null>(null)
const [name, setName] = useState('')
const [nameEdited, setNameEdited] = useState(false)
const [fileName, setFileName] = useState<string | null>(null)
const [file, setFile] = useState<File | null>(null)
const [fileError, setFileError] = useState<string | null>(null)
const inputRef = useRef<HTMLInputElement>(null)
const queryClient = useQueryClient()

const experimentsQuery = useQuery({
Expand Down Expand Up @@ -150,33 +150,30 @@ export function CreateExperimentFromFileDialog({
setDefinition(null)
setName('')
setNameEdited(false)
setFileName(null)
setFile(null)
setFileError(null)
if (inputRef.current) inputRef.current.value = ''
}

function handleOpenChange(next: boolean) {
if (next) reset()
onOpenChange(next)
}

async function handleFileSelected(event: ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0]
event.target.value = '' // selecting the same file again should still parse it
async function handleFileSelected(file: File | null) {
if (!file) return
importMutation.reset()
setFileError(null)
try {
const parsed = parseDefinition(await file.text())
setDefinition(parsed)
setFileName(file.name)
setFile(file)
setNameEdited(false)
// If the names request has not completed yet, the effect above fills in
// the collision-safe name as soon as it does.
setName(nextAvailableExperimentName(parsed.sourceName, existingNames))
} catch (error) {
setDefinition(null)
setFileName(null)
setFile(null)
setName('')
setFileError(error instanceof Error ? error.message : 'Could not read this file as JSON.')
}
Expand All @@ -197,18 +194,13 @@ export function CreateExperimentFromFileDialog({
<div className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="experiment-definition-file">Experiment definition</Label>
<Input
ref={inputRef}
<FileDropInput
id="experiment-definition-file"
type="file"
accept="application/json,.json"
onChange={(event) => void handleFileSelected(event)}
file={file}
onChange={(next) => void handleFileSelected(next)}
placeholder="Drop a JSON file or click to browse"
/>
{fileName && (
<p className="flex items-center gap-1.5 text-xs text-muted-foreground">
<FileJson className="size-3.5" /> {fileName}
</p>
)}
</div>
{definition && (
<div className="space-y-1.5">
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/components/protocol/AddNodePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react'
import { ArrowRight, Atom, BookMarked, Bot, Braces, BrainCircuit, Cloud, Code2, Database, FileText, HardDrive, Repeat2, Route, ScrollText, ShieldCheck, Server, Sparkles, X } from 'lucide-react'
import { ArrowRight, Atom, BookMarked, Bot, Braces, BrainCircuit, Cloud, Code2, Cog, Database, FileText, HardDrive, Repeat2, Route, ScrollText, ShieldCheck, Server, Sparkles, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { DATASET_BROWSE } from './datasetCatalog'
Expand Down Expand Up @@ -56,6 +56,13 @@ const NODE_CATALOG = [
description: "Reviews an upstream Agent's output, requests revisions",
icon: ShieldCheck,
},
{
type: 'tool_step',
category: 'tools',
label: 'Tool Step',
description: "Calls one MCP tool directly with the upstream node's payload -- deterministic, no model",
icon: Cog,
},
{ type: 'model_anthropic', category: 'models', label: 'Anthropic', description: "An Agent or Critic Gate's model, temperature, and parameters", icon: Sparkles },
{ type: 'model_openai', category: 'models', label: 'OpenAI', description: "An Agent or Critic Gate's model, temperature, and parameters", icon: Atom },
{
Expand Down
7 changes: 3 additions & 4 deletions frontend/src/components/protocol/ConnectMcpServerDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,10 @@ export function ConnectMcpServerDialog({
value={name}
onChange={(e) => setName(e.target.value)}
/>
{/* Unique across the whole deployment, not just this account --
worth saying, since the 409 that enforces it is otherwise a
surprise. */}
{/* User-created names are unique within this account. A system
server with the same name also reserves it for every account. */}
<p className="text-xs text-muted-foreground">
How this connection is identified everywhere else. Must not already be taken.
How this connection is identified elsewhere. Must be unique within your account.
</p>
</div>

Expand Down
59 changes: 30 additions & 29 deletions frontend/src/components/protocol/ConversationTranscript.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from 'react'
import { ChevronDown, ChevronUp } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { Conversation, ConversationMessage } from '@/types/protocols'

// The literal id the backend uses for the human on both ends of a
Expand Down Expand Up @@ -51,56 +52,56 @@ function MessageBody({ parts }: { parts: ConversationMessage['parts'] }) {
)
}

// The live transcript of a conversation-mode run, over the canvas. Deliberately
// The live transcript of a conversation-mode run, shown inside that run's
// results panel. Deliberately
// a flat, ordered list rather than a threaded tree: `sequence` is assigned by
// the runtime and one agent runs at a time, so the order things happened in IS
// the structure -- nesting would only re-derive what the reading order already
// shows.
export function ConversationTranscript({
conversation,
agentNames,
className,
}: {
conversation: Conversation
// Node id -> the label shown on that node's card, so the transcript names
// agents the way the canvas does. Ids that aren't on the canvas (a node
// deleted since the run) fall back to the raw id rather than disappearing.
agentNames: Map<string, string>
className?: string
}) {
const nameOf = (id: string) => (id === USER_PARTICIPANT ? 'You' : agentNames.get(id) ?? id)
const stateLabel = CONVERSATION_STATE_LABEL[conversation.state] ?? conversation.state
// Collapsible because the canvas now opens on the protocol's most recent run
// rather than only on one launched in this tab, so this panel is present the
// whole time you're editing a Peer Collaboration graph -- not just while
// watching a run finish. Collapsed keeps the header, which is the part that
// says a conversation happened at all.
// Collapsible so a long transcript doesn't bury the rest of the results
// panel. Collapsed keeps the header, which is the part that says a
// conversation happened at all.
const [collapsed, setCollapsed] = useState(false)

return (
// Positioned by the top-left overlay column in ProtocolCanvas, not by
// itself, so it can't collide with the lock badge it shares that corner
// with (nor with the MiniMap, whose corner it used to sit in). `min-h-0`
// lets it shrink inside that flex column rather than overflowing it, the
// scroll lives on the message list below so the header stays put while you
// read, and `pointer-events-auto` opts back in to the events that column
// waives.
<aside className="pointer-events-auto flex min-h-0 w-full flex-col rounded-lg border border-[color:var(--node-label)]/35 bg-card/95 p-3 shadow-[0_0_16px_-6px_var(--node-label)] backdrop-blur">
<button
type="button"
onClick={() => setCollapsed((c) => !c)}
className="flex w-full items-center justify-between gap-2 rounded text-left hover:opacity-80"
aria-expanded={!collapsed}
>
<p className="text-sm font-medium text-[color:var(--node-label)]">Agent conversation flow</p>
<span className="flex items-center gap-1.5">
<aside
className={cn(
'pointer-events-auto flex min-h-0 flex-col rounded-lg border border-[color:var(--node-label)]/35 bg-card/95 p-3 shadow-[0_0_16px_-6px_var(--node-label)] backdrop-blur',
'w-full',
className,
)}
>
<div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 flex-1 items-center gap-2">
<p className="truncate text-sm font-medium text-[color:var(--node-label)]">Agent conversation flow</p>
<span className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">{stateLabel}</span>
<span className="text-xs text-muted-foreground">{conversation.messages.length}</span>
{collapsed ? (
<ChevronUp className="size-3.5 text-muted-foreground" />
) : (
<ChevronDown className="size-3.5 text-muted-foreground" />
)}
</span>
</button>
</div>
<button
type="button"
onClick={() => setCollapsed((c) => !c)}
className="shrink-0 cursor-pointer rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
aria-label={`${collapsed ? 'Restore' : 'Minimize'} Agent conversation flow`}
aria-expanded={!collapsed}
title={`${collapsed ? 'Restore' : 'Minimize'} Agent conversation flow`}
>
{collapsed ? <ChevronUp className="size-3.5" /> : <ChevronDown className="size-3.5" />}
</button>
</div>
<div className={collapsed ? 'hidden' : 'mt-2 min-h-0 space-y-2 overflow-y-auto'}>
{conversation.messages.map((message) => {
const badge = message.state ? REPLY_BADGE[message.state] : undefined
Expand Down
116 changes: 0 additions & 116 deletions frontend/src/components/protocol/CreateCustomMetricDialog.tsx

This file was deleted.

Loading
Loading