Skip to content

Commit 2c36518

Browse files
abossardCopilot
andcommitted
refactor: extract shared parseRunOutput, add delete-all-runs
- Extracted parseRunOutput (fence-stripping + JSON parsing) into outputUtils.js — shared by RunsSidePanel and AgentRunPage - Fixed AgentRunPage (show_in_menu): renders markdown instead of raw JSON - Added DELETE /api/workbench/runs endpoint + trash button in Runs panel - Runs panel: min-height 500px so content is visible on load Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ed9e745 commit 2c36518

8 files changed

Lines changed: 93 additions & 35 deletions

File tree

backend/agent_builder/persistence/repository.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ def update_run(self, run_id: str, **fields) -> Optional[AgentRun]:
9494
session.refresh(db_run)
9595
return db_run
9696

97+
def delete_all_runs(self) -> int:
98+
"""Delete all runs. Returns count deleted."""
99+
from sqlmodel import delete as sql_delete
100+
with Session(self._engine) as session:
101+
count = session.exec(select(AgentRun)).all().__len__()
102+
session.exec(sql_delete(AgentRun))
103+
session.commit()
104+
return count
105+
97106
# ----- Evaluations -----
98107

99108
def get_evaluation(self, run_id: str) -> Optional[AgentEvaluation]:

backend/agent_builder/routes.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,12 @@ async def workbench_list_all_runs():
243243
return jsonify({"runs": [r.to_dict() for r in runs]})
244244

245245

246+
@agent_builder_bp.route("/api/workbench/runs", methods=["DELETE"])
247+
async def workbench_delete_all_runs():
248+
count = _workbench_service.delete_all_runs()
249+
return jsonify({"deleted": count})
250+
251+
246252
@agent_builder_bp.route("/api/workbench/runs/<run_id>", methods=["GET"])
247253
async def workbench_get_run(run_id: str):
248254
run = _workbench_service.get_run(run_id)

backend/agent_builder/service.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,10 @@ def get_run(self, run_id: str) -> Optional[AgentRun]:
417417
def list_runs(self, agent_id: Optional[str] = None, limit: int = 50) -> list[AgentRun]:
418418
return self._repo.list_runs(agent_id=agent_id, limit=limit)
419419

420+
def delete_all_runs(self) -> int:
421+
"""Delete all runs from the database. Returns count deleted."""
422+
return self._repo.delete_all_runs()
423+
420424
# ------------------------------------------------------------------
421425
# Core: run an agent
422426
# ------------------------------------------------------------------

frontend/src/features/workbench/AgentRunPage.jsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from '@fluentui/react-components'
1919
import { useEffect, useState } from 'react'
2020
import { runWorkbenchAgent } from '../../services/api'
21+
import { parseRunOutput } from './outputUtils'
2122
import SchemaRenderer from './SchemaRenderer'
2223

2324
const useStyles = makeStyles({
@@ -26,7 +27,7 @@ const useStyles = makeStyles({
2627
display: 'flex',
2728
flexDirection: 'column',
2829
gap: tokens.spacingVerticalL,
29-
maxWidth: '800px',
30+
maxWidth: '900px',
3031
},
3132
outputContainer: {
3233
border: `1px solid ${tokens.colorNeutralStroke1}`,
@@ -50,14 +51,7 @@ export default function AgentRunPage({ agent }) {
5051
return <Spinner label="Loading agent..." />
5152
}
5253

53-
const parsedOutput = (() => {
54-
if (!output) return null
55-
try {
56-
const parsed = JSON.parse(output)
57-
if (typeof parsed === 'object' && parsed !== null) return parsed
58-
} catch { /* not JSON */ }
59-
return { message: output }
60-
})()
54+
const parsedOutput = parseRunOutput(output)
6155

6256
const handleRun = async () => {
6357
setError('')

frontend/src/features/workbench/RunsSidePanel.jsx

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import {
2222
makeStyles,
2323
tokens,
2424
} from '@fluentui/react-components'
25-
import { ArrowClockwise24Regular, Dismiss24Regular } from '@fluentui/react-icons'
25+
import { ArrowClockwise24Regular, Delete24Regular, Dismiss24Regular } from '@fluentui/react-icons'
26+
import { parseRunOutput } from './outputUtils'
2627
import SchemaRenderer from './SchemaRenderer'
2728

2829
const useStyles = makeStyles({
@@ -148,22 +149,6 @@ function resolveAgentName(agentMap, run) {
148149
return agentMap[run.agent_id]?.name ?? run.agent_snapshot?.name ?? 'Unknown Agent'
149150
}
150151

151-
function parseRunOutput(output) {
152-
if (!output) return null
153-
if (typeof output !== 'string') return output
154-
155-
// Strip markdown code fences (```json ... ```) that LLMs often wrap output in
156-
let cleaned = output.trim()
157-
const fenceMatch = cleaned.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```\s*$/)
158-
if (fenceMatch) cleaned = fenceMatch[1].trim()
159-
160-
try {
161-
const parsed = JSON.parse(cleaned)
162-
if (typeof parsed === 'object' && parsed !== null) return parsed
163-
} catch { /* not JSON */ }
164-
return { message: output }
165-
}
166-
167152
function formatRelativeTime(dateStr, now = Date.now()) {
168153
if (!dateStr) return ''
169154
const diffSec = Math.max(0, Math.floor((now - new Date(dateStr).getTime()) / 1000))
@@ -192,7 +177,7 @@ function StatusBadge({ status }) {
192177
)
193178
}
194179

195-
export default function RunsSidePanel({ runs = [], agents = [], selectedRunId, onSelectRun, onRefresh }) {
180+
export default function RunsSidePanel({ runs = [], agents = [], selectedRunId, onSelectRun, onRefresh, onDeleteAll }) {
196181
const styles = useStyles()
197182

198183
const agentMap = buildAgentMap(agents)
@@ -208,14 +193,26 @@ export default function RunsSidePanel({ runs = [], agents = [], selectedRunId, o
208193
<div className={styles.panel} data-testid="runs-side-panel">
209194
<div className={styles.header}>
210195
<Text weight="semibold" size={400}>Runs</Text>
211-
<Button
212-
appearance="subtle"
213-
icon={<ArrowClockwise24Regular />}
214-
size="small"
215-
onClick={onRefresh}
216-
data-testid="runs-panel-refresh"
217-
title="Refresh runs"
218-
/>
196+
<div style={{ display: 'flex', gap: '4px' }}>
197+
{sortedRuns.length > 0 && (
198+
<Button
199+
appearance="subtle"
200+
icon={<Delete24Regular />}
201+
size="small"
202+
onClick={onDeleteAll}
203+
data-testid="runs-panel-delete-all"
204+
title="Delete all runs"
205+
/>
206+
)}
207+
<Button
208+
appearance="subtle"
209+
icon={<ArrowClockwise24Regular />}
210+
size="small"
211+
onClick={onRefresh}
212+
data-testid="runs-panel-refresh"
213+
title="Refresh runs"
214+
/>
215+
</div>
219216
</div>
220217

221218
<div className={styles.runList}>

frontend/src/features/workbench/WorkbenchPage.jsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Apps24Regular, Add24Regular } from '@fluentui/react-icons'
1212
import { useEffect, useState, useCallback } from 'react'
1313
import {
1414
deleteWorkbenchAgent,
15+
deleteAllRuns,
1516
getWorkbenchUiConfig,
1617
listWorkbenchAgents,
1718
listWorkbenchTools,
@@ -92,6 +93,14 @@ export default function WorkbenchPage() {
9293
} catch { /* ignore */ }
9394
}, [])
9495

96+
const handleDeleteAllRuns = useCallback(async () => {
97+
try {
98+
await deleteAllRuns()
99+
setRuns([])
100+
setSelectedRunId(null)
101+
} catch { /* ignore */ }
102+
}, [])
103+
95104
const handleRunStarted = useCallback((run) => {
96105
setRuns((prev) => [run, ...prev])
97106
setSelectedRunId(run.id)
@@ -175,6 +184,7 @@ export default function WorkbenchPage() {
175184
selectedRunId={selectedRunId}
176185
onSelectRun={setSelectedRunId}
177186
onRefresh={refreshRuns}
187+
onDeleteAll={handleDeleteAllRuns}
178188
/>
179189
</div>
180190
</div>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Workbench output utilities — pure calculations (no side effects).
3+
*
4+
* Following "Grokking Simplicity": these are calculations that transform
5+
* data without I/O. Shared across RunsSidePanel and AgentRunPage.
6+
*/
7+
8+
/**
9+
* Parse raw LLM output into an object suitable for SchemaRenderer.
10+
*
11+
* Handles:
12+
* - Already-parsed objects (returned as-is)
13+
* - JSON strings (parsed to object)
14+
* - JSON wrapped in markdown fences (```json ... ```) — fences stripped first
15+
* - Plain text / markdown (wrapped as { message: text })
16+
*
17+
* @param {*} output - Raw output from agent run (string or object)
18+
* @returns {object|null} Parsed object for SchemaRenderer, or null if empty
19+
*/
20+
export function parseRunOutput(output) {
21+
if (!output) return null
22+
if (typeof output !== 'string') return output
23+
24+
let cleaned = output.trim()
25+
const fenceMatch = cleaned.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```\s*$/)
26+
if (fenceMatch) cleaned = fenceMatch[1].trim()
27+
28+
try {
29+
const parsed = JSON.parse(cleaned)
30+
if (typeof parsed === 'object' && parsed !== null) return parsed
31+
} catch { /* not JSON */ }
32+
33+
return { message: output }
34+
}

frontend/src/services/api.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,10 @@ export async function getRun(runId) {
378378
return fetchJSON(`${API_BASE_URL}/workbench/runs/${runId}`);
379379
}
380380

381+
export async function deleteAllRuns() {
382+
return fetchJSON(`${API_BASE_URL}/workbench/runs`, { method: "DELETE" });
383+
}
384+
381385
// ============================================================================
382386
// KBA Drafter APIs
383387
// ============================================================================

0 commit comments

Comments
 (0)