refactor: formalize safety rules, extract shared styles, reconcile docs (P2)

- Add backend/app/safety.py with explicit confirmation policy, multi-intent
  semantics, and MCP error taxonomy with retry classification
- Add 26 unit tests for safety module (confirmation rules, error taxonomy)
- Extract repeated inline styles into shared CSS classes in index.css
  (section-card, stat-label, status-badge, data-table, empty/error-state,
  pagination-bar)
- Refactor DashboardPage, ReplayListPage, ReplayPage to use shared classes
- Update README: add missing API endpoints, document safety/confirmation rules
- Use proper HTML entities for arrow/dash characters to fix encoding glitches
This commit is contained in:
Yaojia Wang
2026-04-05 23:10:50 +02:00
parent e0931daece
commit 036e12349d
7 changed files with 448 additions and 120 deletions

View File

@@ -28,23 +28,21 @@ export function ReplayPage() {
return (
<div className="page-container">
<div className="page-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: "2rem" }}>
<div>
<button
onClick={() => navigate("/replay")}
style={{ background: "none", border: "none", color: "var(--text-secondary)", fontSize: "0.875rem", cursor: "pointer", padding: "0 0 0.5rem 0", display: "flex", alignItems: "center", gap: "0.25rem" }}
>
&larr; Back to All Replays
</button>
<h2>Audit Trail: <span style={{ fontFamily: "monospace", color: "var(--brand-primary)" }}>{threadId}</span></h2>
<p>Detailed temporal log of agent reflections, MCP tool calls, and human overrides.</p>
</div>
<div className="page-header" style={{ marginBottom: "2rem" }}>
<button
onClick={() => navigate("/replay")}
style={{ background: "none", border: "none", color: "var(--text-secondary)", fontSize: "0.875rem", cursor: "pointer", padding: "0 0 0.5rem 0", display: "flex", alignItems: "center", gap: "0.25rem" }}
>
&larr; Back to All Replays
</button>
<h2>Audit Trail: <span style={{ fontFamily: "monospace", color: "var(--brand-primary)" }}>{threadId}</span></h2>
<p>Detailed temporal log of agent reflections, MCP tool calls, and human overrides.</p>
</div>
{error ? (
<div style={{ padding: "3rem", textAlign: "center", color: "var(--text-secondary)" }}>
<p style={{ fontSize: "1.125rem", fontWeight: 600, color: "var(--brand-accent)" }}>Failed to load replay</p>
<p style={{ marginTop: "0.5rem" }}>{error}</p>
<div className="error-state">
<p className="error-state__title">Failed to load replay</p>
<p className="error-state__description">{error}</p>
</div>
) : isLoading ? (
<div style={{ display: "grid", gridTemplateColumns: "1fr 3fr", gap: "2rem" }}>
@@ -52,29 +50,29 @@ export function ReplayPage() {
<div className="skeleton-box" style={{ height: "400px", borderRadius: "var(--radius-xl)", background: "var(--bg-surface)" }}></div>
</div>
) : steps.length === 0 ? (
<div style={{ padding: "3rem", textAlign: "center", color: "var(--text-secondary)" }}>
<p style={{ fontSize: "1.125rem", fontWeight: 600 }}>No replay steps found</p>
<p style={{ marginTop: "0.5rem" }}>This conversation has no recorded checkpoints.</p>
<div className="empty-state">
<p className="empty-state__title">No replay steps found</p>
<p className="empty-state__description">This conversation has no recorded checkpoints.</p>
</div>
) : (
<div style={{ display: "grid", gridTemplateColumns: "1fr 3fr", gap: "2rem" }}>
{/* Sidebar Summary Info */}
<div style={{ backgroundColor: "var(--bg-surface)", padding: "1.5rem", borderRadius: "var(--radius-xl)", border: "1px solid var(--border-light)", alignSelf: "start" }}>
<div className="section-card" style={{ alignSelf: "start" }}>
<h3 style={{ fontSize: "1rem", marginBottom: "1.25rem", color: "var(--text-primary)" }}>Session Context</h3>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<div>
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)", fontWeight: 600 }}>Thread ID</div>
<div style={{ fontWeight: 600, fontSize: "0.8125rem", fontFamily: "monospace", wordBreak: "break-all" }}>{threadId}</div>
<div className="stat-label">Thread ID</div>
<div className="stat-value" style={{ fontSize: "0.8125rem", fontFamily: "monospace", wordBreak: "break-all" }}>{threadId}</div>
</div>
<div>
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)", fontWeight: 600 }}>Total Steps</div>
<div style={{ fontSize: "0.9375rem" }}>{totalSteps}</div>
<div className="stat-label">Total Steps</div>
<div className="stat-value">{totalSteps}</div>
</div>
<div>
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", color: "var(--text-secondary)", fontWeight: 600 }}>Time Range</div>
<div className="stat-label">Time Range</div>
<div style={{ fontSize: "0.8125rem" }}>
{steps[0]?.timestamp ? new Date(steps[0].timestamp).toLocaleString() : "N/A"}
{" - "}
{" \u2013 "}
{steps[steps.length - 1]?.timestamp ? new Date(steps[steps.length - 1].timestamp).toLocaleString() : "N/A"}
</div>
</div>
@@ -82,7 +80,7 @@ export function ReplayPage() {
</div>
{/* Timeline */}
<div style={{ backgroundColor: "var(--bg-surface)", padding: "2rem", borderRadius: "var(--radius-xl)", border: "1px solid var(--border-light)" }}>
<div className="section-card" style={{ padding: "2rem" }}>
<ReplayTimeline steps={steps as any} />
</div>
</div>