From 12500b0192955459b7c7eda127e750a9994ed890 Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sat, 6 Jun 2026 03:52:58 -0500 Subject: [PATCH 01/64] fix: verify Cloudflare trace receipts --- .../approval-trace/src/index.ts | 18 + .../approval-trace/src/ui.ts | 371 +++++++++++++++--- .../test/approval-trace.worker.test.ts | 17 + .../test/browser/approval-trace.ui.spec.ts | 3 + 4 files changed, 353 insertions(+), 56 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/index.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/index.ts index 7558c209..a7e3875b 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/index.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/index.ts @@ -19,6 +19,7 @@ import { hexEncode, sha256, signRecord, + verifyRecord as verifyAtribRecord, type AtribRecord, type OnRecordSidecar, type ProofBundle, @@ -1739,6 +1740,22 @@ export default { ) } + if (url.pathname === '/api/verify-record' && request.method === 'POST') { + const body = (await request.json()) as { record?: AtribRecord; expected_hash?: string } + if (!body.record) return json({ ok: false, error: 'missing record' }, { status: 400 }) + const actualHash = recordHash(body.record) + const signatureOk = await verifyAtribRecord(body.record) + return json({ + ok: signatureOk && (!body.expected_hash || body.expected_hash === actualHash), + signature_ok: signatureOk, + hash_ok: !body.expected_hash || body.expected_hash === actualHash, + record_hash: actualHash, + expected_hash: body.expected_hash ?? null, + creator_key: body.record.creator_key, + timestamp: body.record.timestamp, + }) + } + const runMatch = url.pathname.match(/^\/api\/runs\/([^/]+)$/u) if (runMatch && request.method === 'GET') { const runId = decodeURIComponent(runMatch[1]!) @@ -1793,6 +1810,7 @@ export default { endpoints: [ '/', '/api/runs', + '/api/verify-record', '/api/runs/:runId', '/api/runs/:runId/approve', '/api/runs/:runId/reject', diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index 573072c3..dc5990aa 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -733,10 +733,10 @@ export function renderApp(): string { } .github-mark { + color: #111827; display: block; flex: 0 0 auto; height: 24px; - overflow: visible; width: 24px; } @@ -907,6 +907,10 @@ export function renderApp(): string { min-width: 0; } + .header-meta > span:nth-child(3) { + margin-left: 0; + } + .header-meta .meta-code { display: inline-block; max-width: 150px; @@ -959,6 +963,9 @@ export function renderApp(): string { background: #fff; border-color: var(--line); border-radius: 8px; + display: flex; + gap: 22px; + justify-content: flex-start; margin-bottom: 8px; min-height: 62px; padding: 10px 28px; @@ -989,7 +996,13 @@ export function renderApp(): string { } .header-meta { + flex: 1 1 auto; gap: 12px; + justify-content: flex-start; + } + + .header-meta > span:nth-child(3) { + margin-left: auto; } .header-meta > span { @@ -1608,11 +1621,14 @@ export function renderApp(): string { } .risk-details-toggle { + align-items: center; background: transparent; border: 0; color: var(--muted); cursor: pointer; + display: inline-flex; font-size: 12px; + gap: 4px; padding: 0; white-space: nowrap; } @@ -2113,29 +2129,6 @@ export function renderApp(): string { background: #fff; } - .verify-row .pill { - justify-content: center; - min-width: 36px; - } - - .verify-row:nth-child(1) .pill { - background: #edf5ff; - border-color: #c7d6e8; - color: #0969da; - } - - .verify-row:nth-child(2) .pill { - background: #fff0dc; - border-color: #ffd09a; - color: #a44900; - } - - .verify-row:nth-child(3) .pill { - background: #e9f8ef; - border-color: #b9e5cb; - color: #078861; - } - .receipt-grid { display: block; margin-top: 10px; @@ -2308,15 +2301,192 @@ export function renderApp(): string { .verify-row { border-radius: 7px; - grid-template-columns: 34px minmax(0, 1fr) auto; + grid-template-columns: 34px minmax(0, 1fr) minmax(72px, auto); min-height: 44px; padding: 7px 9px; } + .verify-row > div { + min-width: 0; + } + + .verify-icon { + align-items: center; + border: 1px solid; + border-radius: 7px; + display: inline-flex; + height: 28px; + justify-content: center; + width: 28px; + } + + .verify-icon svg { + height: 15px; + width: 15px; + } + + .verify-icon.log { + background: #f3f8ff; + border-color: #d4e5fb; + color: #0969da; + } + + .verify-icon.sig { + background: #fff7ed; + border-color: #fed7aa; + color: #b35a00; + } + + .verify-icon.get { + background: #eefbf4; + border-color: #c7efd6; + color: #078861; + } + .verify-row .event-action { + align-items: center; + background: transparent; + border: 0; color: var(--blue); + display: inline-flex; font-size: 12px; font-weight: 750; + gap: 5px; + justify-content: flex-end; + min-width: 0; + padding: 0; + text-decoration: none; + white-space: nowrap; + } + + .verify-row .event-action:disabled { + color: var(--muted); + cursor: not-allowed; + } + + .verify-row .event-action svg { + height: 13px; + width: 13px; + } + + .verify-row .event-action.verified { + color: var(--green); + } + + .verify-row .event-action.failed { + color: var(--red); + } + + .verification-result { + background: #fbfdff; + border: 1px solid var(--line); + border-radius: 8px; + display: grid; + gap: 7px; + margin-top: 10px; + padding: 9px 10px; + } + + .verification-result.checking { + background: #f7fbff; + border-color: #cfe0f8; + } + + .verification-result.failed { + background: #fff7f7; + border-color: #ffc9c9; + } + + .verification-step { + align-items: center; + display: grid; + font-size: 12px; + gap: 7px; + grid-template-columns: 16px minmax(0, 1fr); + } + + .verification-step strong { + font-size: 12px; + } + + .verification-step span:last-child { + color: var(--muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .verification-dot { + align-items: center; + border: 1px solid #c9d5e5; + border-radius: 999px; + display: inline-flex; + height: 14px; + justify-content: center; + width: 14px; + } + + .verification-dot.checking { + animation: verifyPulse 900ms ease-in-out infinite; + background: var(--blue); + border-color: var(--blue); + box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.12); + } + + .verification-dot.ok { + background: var(--green); + border-color: var(--green); + color: #fff; + } + + .verification-dot.ok::after { + content: ""; + border: solid currentColor; + border-width: 0 1.5px 1.5px 0; + height: 6px; + transform: rotate(45deg) translate(-1px, -1px); + width: 3px; + } + + .verification-dot.fail { + background: var(--red); + border-color: var(--red); + color: #fff; + } + + .verification-dot.fail::before { + content: ""; + background: currentColor; + height: 8px; + transform: rotate(45deg); + width: 1.5px; + } + + .risk-heading { + align-items: center; + color: var(--ink); + display: flex; + font-size: 12px; + font-weight: 750; + justify-content: space-between; + margin: 2px 0 -4px; + } + + .risk-details-toggle svg { + height: 12px; + width: 12px; + } + + @keyframes verifyPulse { + 0%, + 100% { + opacity: 0.65; + transform: scale(0.86); + } + 50% { + opacity: 1; + transform: scale(1); + } } @media (max-width: 1450px) and (min-width: 1101px) { @@ -2387,8 +2557,19 @@ export function renderApp(): string { } @media (max-width: 720px) { + .hero { + align-items: flex-start; + flex-direction: column; + gap: 12px; + } + .header-meta { grid-template-columns: 1fr; + width: 100%; + } + + .header-meta > span:nth-child(3) { + margin-left: 0; } .event { @@ -2445,8 +2626,8 @@ export function renderApp(): string {
- GitHub issue webhook Verified @@ -2520,9 +2701,9 @@ export function renderApp(): string {
-
LOG
Verify in Cloudflare Integrity LogCheck inclusion and consistency proof
-
SIG
Verify receipt signatureValidate signer and record hashes
-
GET
Download transparency proofCT-style proof for this receipt
+
Verify in Cloudflare Integrity LogCheck inclusion and consistency proof
+
Verify receipt signatureValidate signer and record hashes
+
Download transparency proofCT-style proof for this receipt
@@ -2963,7 +3144,15 @@ export function renderApp(): string { const labels = new Set(run.trace_packet.timeline.map((entry) => entry.label)); const rows = []; if (!labels.has('approval') && !labels.has('rejection')) { - rows.push({ name: 'human.review.halted', detail: 'Awaiting human decision', marker: 'pending' }); + const proposal = run.records.find((record) => record.label === 'proposal'); + rows.push({ + name: 'human.review.halted', + detail: 'Awaiting human decision', + marker: 'pending', + record: proposal, + displayLabel: 'approval', + hash: proposal?.record_hash, + }); } if (!labels.has('execution')) { rows.push({ name: 'mcp.execution.resumed', detail: 'Pending approval', marker: 'future' }); @@ -3114,6 +3303,56 @@ export function renderApp(): string { return run.trace_packet.handoff?.public_context_url ?? '/api/runs/' + run.run_id; } + function verifyIcon(kind) { + const icons = { + log: '', + sig: '', + get: '', + }; + return ''; + } + + function actionGlyph(kind = 'external') { + if (kind === 'download') return ''; + if (kind === 'check') return ''; + return ''; + } + + function verificationRowsMarkup(run = currentRun, record = selectedReceiptRecord) { + if (!run || !record) { + return '
' + + '
' + verifyIcon('log') + '
Verify in Cloudflare Integrity LogCheck inclusion and consistency proof
' + + '
' + verifyIcon('sig') + '
Verify receipt signatureValidate signer and record hashes
' + + '
' + verifyIcon('get') + '
Download transparency proofCT-style proof for this receipt
' + + '
'; + } + const proofUrl = proofTargetForRun(run); + return '
' + + '
' + verifyIcon('log') + '
Verify in Cloudflare Integrity LogCheck inclusion and consistency proof
View proof ' + actionGlyph('external') + '
' + + '
' + verifyIcon('sig') + '
Verify receipt signatureValidate signer and record hashes
' + + '
' + verifyIcon('get') + '
Download transparency proofCT-style proof for this receipt
' + + '
'; + } + + function verificationCheckingMarkup(record = selectedReceiptRecord) { + return '
' + + '
Verifying receiptChecking ' + escapeHtml(shortHash(record?.record_hash)) + '
' + + '
Record hashWaiting for Worker verifier
' + + '
SignatureWaiting for Ed25519 check
' + + '
'; + } + + function verificationResultMarkup(result, record = selectedReceiptRecord) { + const ok = Boolean(result?.ok); + const hashOk = Boolean(result?.hash_ok); + const signatureOk = Boolean(result?.signature_ok); + return '
' + + '
Record hash ' + (hashOk ? 'matches' : 'mismatch') + '' + escapeHtml(shortHash(result?.record_hash ?? record?.record_hash)) + '
' + + '
Signature ' + (signatureOk ? 'valid' : 'failed') + 'Creator key ' + escapeHtml(shortHash(result?.creator_key ?? record?.record?.creator_key)) + '
' + + '
' + (ok ? 'Receipt verified' : 'Verification failed') + 'Checked by the Cloudflare Worker verifier just now
' + + '
'; + } + function updateTraceHeaderCopy() { const button = document.querySelector('[data-copy-source="#traceIdLabel"]'); if (!button) return; @@ -3121,24 +3360,47 @@ export function renderApp(): string { } function renderVerificationActions(run = currentRun, record = selectedReceiptRecord) { - if (!run || !record) { - verificationEl.innerHTML = \` -
-
LOG
Verify in Cloudflare Integrity LogCheck inclusion and consistency proof
-
SIG
Verify receipt signatureValidate signer and record hashes
-
GET
Download transparency proofCT-style proof for this receipt
-
- \`; - return; + verificationEl.innerHTML = verificationRowsMarkup(run, record); + } + + async function verifySelectedReceipt(button) { + const record = selectedReceiptRecord; + if (!record) return; + button.disabled = true; + button.innerHTML = 'Verifying...'; + button.classList.remove('verified', 'failed'); + const existingResult = verificationEl.querySelector('#verificationResult'); + if (existingResult) existingResult.remove(); + verificationEl.insertAdjacentHTML('beforeend', verificationCheckingMarkup(record)); + try { + await sleep(250); + const result = await post('/api/verify-record', { + record: record.record, + expected_hash: record.record_hash, + }); + const ok = Boolean(result?.ok); + const resultEl = verificationEl.querySelector('#verificationResult'); + if (resultEl) resultEl.outerHTML = verificationResultMarkup(result, record); + button.innerHTML = (ok ? 'Verified ' : 'Check failed ') + actionGlyph(ok ? 'check' : 'external'); + button.classList.toggle('verified', ok); + button.classList.toggle('failed', !ok); + } catch (error) { + const resultEl = verificationEl.querySelector('#verificationResult'); + const failed = { + ok: false, + hash_ok: false, + signature_ok: false, + record_hash: record.record_hash, + creator_key: record.record?.creator_key, + }; + if (resultEl) resultEl.outerHTML = verificationResultMarkup(failed, record); + const failedText = escapeHtml(String(error?.message ?? error)); + verificationEl.insertAdjacentHTML('beforeend', '

' + failedText + '

'); + button.innerHTML = 'Check failed ' + actionGlyph('external'); + button.classList.add('failed'); + } finally { + button.disabled = false; } - const proofUrl = proofTargetForRun(run); - verificationEl.innerHTML = \` -
-
LOG
Verify in Cloudflare Integrity LogCheck inclusion and consistency proof
View proof
-
SIG
Verify receipt signatureValidate signer and record hashes
-
GET
Download transparency proofCT-style proof for this receipt
-
- \`; } function updateReceiptControls(record = selectedReceiptRecord) { @@ -3236,11 +3498,12 @@ export function renderApp(): string {
\${renderDiff(diff)}
+
Risk assessment
Medium \${body.risk ?? 'requires_human_approval'} - +
@@ -3553,11 +3816,7 @@ export function renderApp(): string { const verifyButton = target.closest?.('[data-verify-receipt]'); if (verifyButton && !verifyButton.disabled && selectedReceiptRecord) { - const isVerifiable = selectedReceiptRecord.record_hash?.startsWith('sha256:') - && selectedReceiptRecord.record?.signature - && selectedReceiptRecord.record?.creator_key; - verifyButton.textContent = isVerifiable ? 'Verified' : 'Check failed'; - verifyButton.classList.toggle('verified', Boolean(isVerifiable)); + await verifySelectedReceipt(verifyButton); return; } diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/approval-trace.worker.test.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/approval-trace.worker.test.ts index 0d0718b3..eda8d8dc 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/approval-trace.worker.test.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/approval-trace.worker.test.ts @@ -215,6 +215,23 @@ describe('Cloudflare approval trace Worker', () => { expect(pending.status).toBe('pending_approval') expect(labels(pending)).toEqual(['trigger', 'triage', 'proposal']) + const pendingRecords = byLabel(pending) + const proposalRecord = pendingRecords.get('proposal')! + const verification = await postJson<{ + ok: boolean + signature_ok: boolean + hash_ok: boolean + record_hash: string + }>('/api/verify-record', { + record: proposalRecord.record, + expected_hash: proposalRecord.record_hash, + }) + expect(verification).toMatchObject({ + ok: true, + signature_ok: true, + hash_ok: true, + record_hash: proposalRecord.record_hash, + }) const trace = await approveRun(runId) const records = byLabel(trace) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index e02af9e3..96fcc8c7 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -87,6 +87,9 @@ test.describe('Cloudflare approval trace browser UI', () => { /log\.atrib\.dev|\/api\/runs\//, ) await page.locator('#verification').getByRole('button', { name: 'Verify' }).click() + await expect(page.locator('#verificationResult')).toContainText('Record hash matches') + await expect(page.locator('#verificationResult')).toContainText('Signature valid') + await expect(page.locator('#verificationResult')).toContainText('Receipt verified') await expect(page.locator('#verification').getByRole('button', { name: 'Verified' })).toBeVisible() const pendingDownload = page.waitForEvent('download') await page.getByRole('button', { name: 'Download receipt' }).click() From 3211c1df41088d3db8afe36f4d84733d89f57c33 Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 04:02:13 -0500 Subject: [PATCH 02/64] fix: polish Cloudflare approval trace demo --- .../approval-trace/src/ui.ts | 275 +++++++++++++----- .../test/browser/approval-trace.ui.spec.ts | 4 +- 2 files changed, 203 insertions(+), 76 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index dc5990aa..5a89ccef 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -736,8 +736,9 @@ export function renderApp(): string { color: #111827; display: block; flex: 0 0 auto; - height: 24px; - width: 24px; + height: 25px; + overflow: visible; + width: 25px; } .trigger-details { @@ -955,7 +956,8 @@ export function renderApp(): string { } .shell { - max-width: none; + margin: 0 auto; + max-width: 1536px; padding: 0 0 0; } @@ -1022,6 +1024,44 @@ export function renderApp(): string { gap: 8px; } + .meta-pill.live-run::after { + border-bottom: 1.5px solid currentColor; + border-right: 1.5px solid currentColor; + content: ""; + height: 5px; + margin-left: 2px; + transform: translateY(-2px) rotate(45deg); + width: 5px; + } + + .region-status-dot { + background: var(--green); + border-radius: 999px; + display: inline-block; + height: 8px; + margin-left: 6px; + vertical-align: 1px; + width: 8px; + } + + .header-menu { + align-items: center; + background: #fff; + border: 1px solid var(--line); + border-radius: 999px; + color: var(--ink); + display: inline-flex; + height: 32px; + justify-content: center; + padding: 0; + width: 32px; + } + + .header-menu svg { + height: 16px; + width: 16px; + } + .meta-pill, .meta-code { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); @@ -1034,7 +1074,7 @@ export function renderApp(): string { gap: 16px; grid-template-columns: 1fr; margin-bottom: 6px; - min-height: 64px; + min-height: 72px; padding: 0 30px 0; } @@ -1079,7 +1119,7 @@ export function renderApp(): string { display: flex; min-height: 52px; min-width: 0; - padding: 6px 9px; + padding: 6px 10px; position: relative; } @@ -1100,15 +1140,17 @@ export function renderApp(): string { .step.active, .step.halted, .step.error { - background: #fff; - border-color: #ccd7e5; - box-shadow: var(--shadow-tight); + background: transparent; + border-color: transparent; + box-shadow: none; } .step.halted { background: #fff8ed; border-color: #f3a64e; justify-self: center; + min-height: 58px; + padding: 8px 14px; width: 332px; } @@ -1134,8 +1176,9 @@ export function renderApp(): string { flex: 0 0 auto; font-size: 14px; font-weight: 800; - height: 34px; - width: 34px; + height: 38px; + margin-right: 11px; + width: 38px; z-index: 1; } @@ -1148,17 +1191,19 @@ export function renderApp(): string { content: ""; border-bottom: 3px solid #fff; border-right: 3px solid #fff; - height: 14px; + height: 15px; left: 50%; position: absolute; top: 50%; - transform: translate(-50%, -58%) rotate(45deg); - width: 7px; + transform: translate(-50%, -56%) rotate(45deg); + width: 8px; } .step.halted .step-index { font-size: 0; + height: 42px; position: relative; + width: 42px; } .step.halted .step-index::before, @@ -1168,16 +1213,17 @@ export function renderApp(): string { content: ""; height: 15px; position: absolute; - top: 9px; + top: 50%; + transform: translateY(-50%); width: 4px; } .step.halted .step-index::before { - left: 12px; + left: 14px; } .step.halted .step-index::after { - right: 12px; + right: 14px; } .step-copy strong { @@ -1200,6 +1246,33 @@ export function renderApp(): string { z-index: 1; } + .step.halted .step-copy { + display: grid; + gap: 3px; + } + + .step.halted .step-copy strong { + display: block; + font-size: 14px; + font-weight: 850; + line-height: 1.1; + } + + .step.halted .step-copy .step-number-label, + .step.halted .step-copy [data-step-title] { + display: inline; + margin-top: 0; + } + + .step.halted .step-copy .step-meta-line { + align-items: center; + display: flex; + gap: 10px; + line-height: 1; + margin-top: 0; + min-width: 0; + } + .step-badge { background: #fff0dc; border: 1px solid #ffd09a; @@ -1209,8 +1282,9 @@ export function renderApp(): string { font-size: 10px; font-weight: 850; line-height: 1; - margin-left: 7px; + margin-left: 0; padding: 3px 6px; + text-transform: uppercase; vertical-align: 1px; } @@ -1218,6 +1292,11 @@ export function renderApp(): string { display: inline; } + .step.halted + .step .step-index { + border-color: var(--blue); + color: var(--blue); + } + .step-badge.approved { background: #e9f8ef; border-color: #b9e5cb; @@ -1478,7 +1557,7 @@ export function renderApp(): string { left: 50%; position: absolute; top: 50%; - transform: translate(-50%, -58%) rotate(45deg); + transform: translate(-50%, -55%) rotate(45deg); width: 3px; } @@ -1662,18 +1741,18 @@ export function renderApp(): string { .danger { align-items: center; display: grid; - gap: 8px; - grid-template-columns: 20px minmax(0, 1fr); + gap: 10px; + grid-template-columns: 22px minmax(0, 1fr); justify-content: stretch; min-height: 58px; - padding: 9px 10px; + padding: 9px 12px; } .actions { display: grid; - grid-template-columns: minmax(186px, 1.05fr) minmax(156px, 0.9fr) minmax(168px, 0.96fr); - gap: 14px; - margin-top: 4px; + grid-template-columns: minmax(194px, 1.08fr) minmax(164px, 0.92fr) minmax(174px, 0.98fr); + gap: 16px; + margin-top: 6px; min-width: 0; } @@ -1686,33 +1765,33 @@ export function renderApp(): string { .action-copy small { color: inherit; - font-size: 9.5px; + font-size: 9px; font-weight: 650; line-height: 1.12; opacity: 0.78; - overflow: hidden; + overflow: visible; overflow-wrap: normal; - text-overflow: ellipsis; + text-overflow: clip; text-wrap: normal; } .button-label { display: block; - font-size: 12.5px; + font-size: 13px; font-weight: 850; line-height: 1.12; white-space: nowrap; } .primary .button-label { - font-size: 12.5px; + font-size: 13px; } .primary .action-copy small { font-size: 8px; letter-spacing: 0; - overflow: hidden; - text-overflow: ellipsis; + overflow: visible; + text-overflow: clip; white-space: nowrap; } @@ -1731,30 +1810,36 @@ export function renderApp(): string { .button-icon { align-items: center; - border: 1px solid currentColor; border-radius: 999px; display: inline-flex; - height: 18px; + height: 22px; justify-content: center; justify-self: start; line-height: 0; margin-left: 0; - width: 18px; + width: 22px; } .button-icon svg { display: block; - height: 11px; - width: 11px; + height: 14px; + width: 14px; } .primary .button-icon { - border-color: rgba(255, 255, 255, 0.78); + border: 0; + color: #fff; + } + + .danger .button-icon { + border: 1.5px solid currentColor; + color: var(--red); } - .danger .button-icon, .secondary .button-icon { - margin-top: -1px; + border: 1px solid #cbd5e1; + border-radius: 6px; + color: #334155; } .primary .action-copy, @@ -1781,6 +1866,7 @@ export function renderApp(): string { .event, .event-future { + align-items: center; animation-duration: 180ms; background: transparent; border: 0; @@ -1788,8 +1874,9 @@ export function renderApp(): string { display: grid; gap: 7px; grid-template-columns: 54px 20px minmax(0, 1fr) minmax(82px, 120px); + min-height: 44px; min-width: 0; - padding: 3px 0; + padding: 4px 0; } .event:hover, @@ -1836,7 +1923,7 @@ export function renderApp(): string { left: 50%; position: absolute; top: 50%; - transform: translate(-50%, -58%) rotate(45deg); + transform: translate(-50%, -55%) rotate(45deg); width: 4px; } @@ -1950,7 +2037,7 @@ export function renderApp(): string { .signer-list { gap: 0; - margin-top: 12px; + margin-top: 30px; } .signer-list .trace-section-label { @@ -1960,10 +2047,10 @@ export function renderApp(): string { .signer-row { background: #fff; border-radius: 0; - gap: 7px; - grid-template-columns: 24px 74px minmax(0, 1fr) 56px minmax(62px, 82px) 14px; - min-height: 30px; - padding: 4px 8px; + gap: 8px; + grid-template-columns: 26px 74px minmax(112px, 1fr) 62px minmax(84px, 108px) 16px; + min-height: 34px; + padding: 5px 9px; box-shadow: none; } @@ -2009,14 +2096,14 @@ export function renderApp(): string { align-items: center; border-radius: 7px; display: inline-flex; - height: 22px; + height: 24px; justify-content: center; - width: 22px; + width: 24px; } .signer-icon svg { - height: 15px; - width: 15px; + height: 16px; + width: 16px; } .signer-icon.agent { @@ -2220,7 +2307,7 @@ export function renderApp(): string { } .receipt-section { - padding: 12px 14px; + padding: 10px 14px; } .json pre { @@ -2228,17 +2315,35 @@ export function renderApp(): string { border: 1px solid var(--line); color: #102033; font-size: 12px; - line-height: 1.5; - max-height: 210px; - padding: 10px 12px 10px 36px; + line-height: 1.42; + max-height: 160px; + overflow: auto; + padding: 8px 10px; position: relative; } + .json-line { + display: grid; + grid-template-columns: 24px minmax(0, 1fr); + } + + .json-line-number { + color: #94a3b8; + padding-right: 10px; + text-align: right; + user-select: none; + } + + .json-line-code { + min-width: 0; + white-space: pre; + } + .receipt-tabs { border-bottom: 1px solid var(--line); display: flex; gap: 34px; - margin: -12px -14px 10px; + margin: -10px -14px 8px; padding: 0 26px; } @@ -2249,7 +2354,7 @@ export function renderApp(): string { cursor: pointer; font-size: 12px; font-weight: 700; - min-height: 34px; + min-height: 30px; padding: 0; position: relative; } @@ -2282,14 +2387,15 @@ export function renderApp(): string { .receipt-summary-grid, .verification-list { display: grid; - gap: 8px; + gap: 6px; } .summary-row { display: grid; font-size: 12px; - gap: 8px; + gap: 6px; grid-template-columns: 130px minmax(0, 1fr) auto; + line-height: 1.18; } .summary-row .hash, @@ -2302,8 +2408,8 @@ export function renderApp(): string { .verify-row { border-radius: 7px; grid-template-columns: 34px minmax(0, 1fr) minmax(72px, auto); - min-height: 44px; - padding: 7px 9px; + min-height: 42px; + padding: 6px 9px; } .verify-row > div { @@ -2596,10 +2702,11 @@ export function renderApp(): string {
- Live run + Live run Run ID pending - Region IAD + Region IAD Started waiting +
@@ -2614,7 +2721,7 @@ export function renderApp(): string {
1TriggerPending 2Autonomous triagePending - 3Human review halted Awaiting reviewPending + 33. Human review haltedPendingAwaiting review 4MCP execution resumedPending 5Audit readyPending
@@ -2626,7 +2733,7 @@ export function renderApp(): string {
- GitHub issue webhook @@ -2795,6 +2902,16 @@ export function renderApp(): string { handoff: 11200, }; + const progressDisplayOffsets = { + trigger: 0, + context: 1200, + policy: 2600, + proposal: 4800, + halt: 6200, + resume: 8600, + audit: 11200, + }; + function renderSteps(step, kind = 'pending') { currentStep = step; const order = ['trigger', 'autonomous', 'halt', 'resume', 'audit']; @@ -2802,8 +2919,9 @@ export function renderApp(): string { workflowSteps.querySelectorAll('.step').forEach((item) => { const itemIndex = order.indexOf(item.dataset.step); item.className = 'step'; - if (itemIndex < activeIndex) item.classList.add('done'); - if (item.dataset.step === step) { + const activeDone = kind === 'ok' && itemIndex === activeIndex; + if (itemIndex < activeIndex || activeDone) item.classList.add('done'); + if (item.dataset.step === step && !activeDone) { item.classList.add(step === 'halt' ? 'halted' : kind === 'error' ? 'error' : 'active'); } }); @@ -2926,7 +3044,7 @@ export function renderApp(): string { if (key && stageDisplayTimes[key]) return stageDisplayTimes[key]; if (!active) return '-'; const record = progressRecordFor(run, title); - return displayRecordTime(record, record?.label ?? key) + ' UTC'; + return formatRecordTime(record, progressDisplayOffsets[key] ?? recordDisplayOffsets[record?.label ?? key] ?? 0) + ' UTC'; } function formatRecordTime(record, offsetMs = 0) { @@ -3243,6 +3361,12 @@ export function renderApp(): string { return JSON.stringify(value, null, 2); } + function renderReceiptJson(value) { + return '
' + pretty(value).split('\\n').map((line, index) => (
+          '' + String(index + 1) + '' + escapeHtml(line) + ''
+        )).join('') + '
'; + } + async function writeClipboard(text) { const value = String(text ?? ''); if (!value) return false; @@ -3372,8 +3496,9 @@ export function renderApp(): string { const existingResult = verificationEl.querySelector('#verificationResult'); if (existingResult) existingResult.remove(); verificationEl.insertAdjacentHTML('beforeend', verificationCheckingMarkup(record)); + followElement(verificationEl.querySelector('#verificationResult'), 'nearest'); try { - await sleep(250); + await sleep(850); const result = await post('/api/verify-record', { record: record.record, expected_hash: record.record_hash, @@ -3381,6 +3506,7 @@ export function renderApp(): string { const ok = Boolean(result?.ok); const resultEl = verificationEl.querySelector('#verificationResult'); if (resultEl) resultEl.outerHTML = verificationResultMarkup(result, record); + followElement(verificationEl.querySelector('#verificationResult'), 'nearest'); button.innerHTML = (ok ? 'Verified ' : 'Check failed ') + actionGlyph(ok ? 'check' : 'external'); button.classList.toggle('verified', ok); button.classList.toggle('failed', !ok); @@ -3706,9 +3832,9 @@ export function renderApp(): string { const pendingSignatures = signers.filter((signer) => signer.status === 'Pending').length; const logEntry = run.trace_packet.handoff?.public_context_url ?? '/api/runs/' + run.run_id; const tabMarkup = \` -
- - +
+ +
\`; if (activeTab === 'details') { @@ -3743,7 +3869,7 @@ export function renderApp(): string { }; const selectRecord = (record) => { selectedReceiptRecord = record; - receiptsEl.innerHTML = '
' + pretty(record) + '
'; + receiptsEl.innerHTML = renderReceiptJson(record); renderReceiptSummary(record); renderVerificationActions(run, record); updateReceiptControls(record); @@ -3763,6 +3889,7 @@ export function renderApp(): string { function render(run) { currentRun = run; + stageDisplayTimes = {}; runIdLabel.textContent = run.run_id; traceIdLabel.textContent = traceIdForRun(run); updateTraceHeaderCopy(); @@ -3847,9 +3974,9 @@ export function renderApp(): string { const runPromise = post('/api/runs', { prompt: promptInput.value, }); - for (let index = 0; index < bootStages.length; index += 1) { + for (let index = 0; index < bootStages.length - 1; index += 1) { renderBootProgress(index); - await sleep(index === bootStages.length - 1 ? 1200 : 1700); + await sleep(1700); } const run = await runPromise; render(run); diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index 96fcc8c7..14072a88 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -73,10 +73,10 @@ test.describe('Cloudflare approval trace browser UI', () => { await expect(page.locator('#riskDetails')).toBeVisible() await expect(page.locator('#riskDetails')).toContainText('Human review gate') - await page.getByRole('button', { name: 'Record details' }).click() + await page.getByRole('tab', { name: 'Record details' }).click() await expect(page.locator('#receiptSummary')).toContainText('Record hash') await expect(page.locator('#receiptSummary')).toContainText('Timestamp') - await page.getByRole('button', { name: 'Summary' }).click() + await page.getByRole('tab', { name: 'Summary' }).click() await expectCopies(page.getByRole('button', { name: 'Copy trace ID' })) await expectCopies(page.getByRole('button', { name: 'Copy Agent signature' })) From 26bfeeb628ab3aa307a9acc3349a7cfd58a5fce4 Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 05:14:48 -0500 Subject: [PATCH 03/64] fix: align Cloudflare trace demo with mockup --- .../approval-trace/src/index.ts | 2 +- .../approval-trace/src/ui.ts | 310 +++++++++++++----- 2 files changed, 223 insertions(+), 89 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/index.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/index.ts index a7e3875b..0f27259b 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/index.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/index.ts @@ -380,7 +380,7 @@ function fixturePlan(prompt: string): PlannedAction { +}];` return { planner: 'fixture', - action: 'Update rate-limit middleware for the /v1/report route', + action: 'Update file in repository', summary: 'Respond to a GitHub issue webhook by preparing a small repository file update that adds request limiting to the reported route.', risk: 'Introduces rate limiting that changes production request handling.', diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index 5a89ccef..ded4dc91 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -777,7 +777,12 @@ export function renderApp(): string { } .diff { + display: block; + min-width: 0; + max-width: 100%; + overflow: hidden; grid-template-columns: 1fr; + width: 100%; } .diff pre { @@ -999,7 +1004,7 @@ export function renderApp(): string { .header-meta { flex: 1 1 auto; - gap: 12px; + gap: 18px; justify-content: flex-start; } @@ -1020,6 +1025,17 @@ export function renderApp(): string { white-space: nowrap; } + .run-id-meta { + align-items: center; + display: inline-flex; + gap: 7px; + } + + .run-id-meta .copy-icon { + height: 16px; + width: 16px; + } + .meta-pill { gap: 8px; } @@ -1148,6 +1164,7 @@ export function renderApp(): string { .step.halted { background: #fff8ed; border-color: #f3a64e; + color: var(--ink); justify-self: center; min-height: 58px; padding: 8px 14px; @@ -1240,13 +1257,14 @@ export function renderApp(): string { } .step-copy { - background: inherit; + background: #fff; min-width: 0; position: relative; z-index: 1; } .step.halted .step-copy { + background: #fff8ed; display: grid; gap: 3px; } @@ -1297,6 +1315,62 @@ export function renderApp(): string { color: var(--blue); } + @media (min-width: 1451px) { + .rail-stepper { + display: block; + height: 72px; + } + + .step { + position: absolute; + top: 7px; + } + + .step[data-step="trigger"] { + left: 10px; + width: 286px; + } + + .step[data-step="autonomous"] { + left: 300px; + width: 270px; + } + + .step[data-step="halt"] { + left: 569px; + } + + .step[data-step="resume"] { + left: 966px; + width: 300px; + } + + .step[data-step="audit"] { + left: 1288px; + width: 190px; + } + + .step[data-step="trigger"]::after { + left: 158px; + width: 128px; + } + + .step[data-step="autonomous"]::after { + left: 148px; + width: 121px; + } + + .step.halted:not(:last-child)::after { + left: 332px; + width: 74px; + } + + .step[data-step="resume"]::after { + left: 130px; + width: 154px; + } + } + .step-badge.approved { background: #e9f8ef; border-color: #b9e5cb; @@ -1312,7 +1386,7 @@ export function renderApp(): string { .grid { gap: 10px; align-items: start; - grid-template-columns: minmax(336px, 363px) minmax(620px, 1fr) minmax(438px, 524px); + grid-template-columns: 363px 610px 523px; margin: 0 10px; } @@ -1338,10 +1412,11 @@ export function renderApp(): string { border-bottom: 1px solid var(--line); color: var(--ink); display: flex; - font-size: 12px; - font-weight: 850; - justify-content: space-between; - letter-spacing: 0.05em; + font-size: 11.5px; + font-weight: 800; + gap: 7px; + justify-content: flex-start; + letter-spacing: 0.035em; margin: 0; min-height: 38px; padding: 0 14px; @@ -1382,7 +1457,7 @@ export function renderApp(): string { } .trigger-source { - font-size: 15px; + font-size: 14px; } .trigger-details { @@ -1402,9 +1477,17 @@ export function renderApp(): string { padding: 14px 16px; } + .proposal, + .timeline { + padding-left: 12px; + padding-right: 12px; + } + .proposal { display: grid; gap: 10px; + min-width: 0; + overflow: hidden; } .prompt { @@ -1468,7 +1551,7 @@ export function renderApp(): string { border: 0; border-radius: 0; column-gap: 10px; - grid-template-columns: 120px minmax(0, 1fr); + grid-template-columns: max-content minmax(0, 1fr); padding: 0; } @@ -1518,12 +1601,15 @@ export function renderApp(): string { } .progress-item .dot { - border: 2px solid #fff; - box-shadow: 0 0 0 1px #c7d2df; - height: 14px; - margin-left: 2px; + border: 0; + box-shadow: none; + box-sizing: border-box; + display: inline-flex; + height: 16px; + justify-self: center; + margin-left: 0; position: relative; - width: 14px; + width: 16px; z-index: 1; } @@ -1531,6 +1617,11 @@ export function renderApp(): string { background: #fff; } + .progress-item .dot.future { + border: 1.5px solid #a8b4c3; + box-shadow: none; + } + .progress-item.halted { background: transparent; } @@ -1541,7 +1632,6 @@ export function renderApp(): string { .progress-item.proposal .dot.ok { background: var(--blue); - box-shadow: 0 0 0 1px #b8c8f6; } .progress-item:not(.proposal) .dot.ok { @@ -1553,15 +1643,16 @@ export function renderApp(): string { border-bottom: 2px solid #fff; border-right: 2px solid #fff; content: ""; - height: 6px; - left: 50%; + height: 8px; + left: 5px; position: absolute; - top: 50%; - transform: translate(-50%, -55%) rotate(45deg); - width: 3px; + top: 2px; + transform: rotate(45deg); + width: 4px; } .progress-item.halted .dot.pending { + background: var(--orange); font-size: 0; } @@ -1570,18 +1661,19 @@ export function renderApp(): string { background: #fff; border-radius: 1px; content: ""; - height: 7px; + height: 8px; position: absolute; - top: 2px; + top: 50%; + transform: translateY(-50%); width: 2px; } .progress-item.halted .dot.pending::before { - left: 4px; + left: 5px; } .progress-item.halted .dot.pending::after { - right: 4px; + right: 5px; } .run-state { @@ -1605,7 +1697,9 @@ export function renderApp(): string { gap: 10px; justify-content: space-between; margin-bottom: 6px; + max-width: 100%; min-width: 0; + width: 100%; } .diff-tools { @@ -1618,9 +1712,9 @@ export function renderApp(): string { } .diff pre { - font-size: 12px; - line-height: 1.55; - max-height: 300px; + font-size: 11.3px; + line-height: 1.38; + max-height: 313px; } .diff-code { @@ -1629,18 +1723,22 @@ export function renderApp(): string { border-radius: 8px; color: #102033; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - font-size: 12px; - line-height: 1.55; - max-height: 302px; + font-size: 11.3px; + line-height: 1.38; + max-height: 313px; + max-width: 100%; overflow: auto; padding: 8px 0; + width: 100%; } .diff-line { display: block; - min-height: 18px; + min-height: 15.5px; + min-width: 0; padding: 0 12px; - white-space: pre-wrap; + white-space: pre; + width: auto; } .diff-line.add { @@ -1750,8 +1848,8 @@ export function renderApp(): string { .actions { display: grid; - grid-template-columns: minmax(194px, 1.08fr) minmax(164px, 0.92fr) minmax(174px, 0.98fr); - gap: 16px; + grid-template-columns: 190px 174px 170px; + gap: 20px; margin-top: 6px; min-width: 0; } @@ -1858,7 +1956,7 @@ export function renderApp(): string { background: #cfd8e5; bottom: 26px; content: ""; - left: 68px; + left: 70px; position: absolute; top: 24px; width: 2px; @@ -1901,17 +1999,19 @@ export function renderApp(): string { .event-marker { align-items: center; background: var(--green); - border: 2px solid #fff; + border: 0; border-radius: 999px; - box-shadow: 0 0 0 1px #b7d7c8; + box-shadow: none; + box-sizing: border-box; color: #fff; display: inline-flex; font-size: 10px; - height: 17px; + height: 18px; justify-content: center; + justify-self: center; margin-top: 3px; position: relative; - width: 17px; + width: 18px; z-index: 1; } @@ -1919,17 +2019,16 @@ export function renderApp(): string { border-bottom: 2px solid #fff; border-right: 2px solid #fff; content: ""; - height: 7px; - left: 50%; + height: 8px; + left: 6px; position: absolute; - top: 50%; - transform: translate(-50%, -55%) rotate(45deg); + top: 3px; + transform: rotate(45deg); width: 4px; } .event-marker.pending { background: var(--orange); - box-shadow: 0 0 0 1px #ffd09a; font-size: 0; } @@ -1940,22 +2039,23 @@ export function renderApp(): string { content: ""; height: 8px; position: absolute; - top: 3px; + top: 50%; + transform: translateY(-50%); width: 2px; } .event-marker.pending::before { - left: 5px; + left: 6px; } .event-marker.pending::after { - right: 5px; + right: 6px; } .event-marker.future { background: #fff; - box-shadow: 0 0 0 1px #a8b4c3; - border-color: #fff; + border: 1.5px solid #a8b4c3; + box-shadow: none; } .event-copy { @@ -2037,7 +2137,7 @@ export function renderApp(): string { .signer-list { gap: 0; - margin-top: 30px; + margin-top: 14px; } .signer-list .trace-section-label { @@ -2047,10 +2147,10 @@ export function renderApp(): string { .signer-row { background: #fff; border-radius: 0; - gap: 8px; - grid-template-columns: 26px 74px minmax(112px, 1fr) 62px minmax(84px, 108px) 16px; - min-height: 34px; - padding: 5px 9px; + gap: 7px; + grid-template-columns: 24px 72px minmax(112px, 1fr) 58px minmax(84px, 106px) 16px; + min-height: 31px; + padding: 3px 8px; box-shadow: none; } @@ -2096,14 +2196,14 @@ export function renderApp(): string { align-items: center; border-radius: 7px; display: inline-flex; - height: 24px; + height: 22px; justify-content: center; - width: 24px; + width: 22px; } .signer-icon svg { - height: 16px; - width: 16px; + height: 15px; + width: 15px; } .signer-icon.agent { @@ -2219,11 +2319,12 @@ export function renderApp(): string { .receipt-grid { display: block; margin-top: 10px; - padding: 0 10px; + padding: 0; } .receipt-panel { grid-column: auto; + min-height: 248px; scroll-margin-top: 12px; } @@ -2303,7 +2404,7 @@ export function renderApp(): string { } .receipt-shell { - grid-template-columns: minmax(420px, 1.05fr) minmax(360px, 0.9fr) minmax(420px, 1fr); + grid-template-columns: minmax(500px, 520px) minmax(430px, 458px) minmax(520px, 1fr); } .receipt-section { @@ -2315,10 +2416,10 @@ export function renderApp(): string { border: 1px solid var(--line); color: #102033; font-size: 12px; - line-height: 1.42; - max-height: 160px; + line-height: 1.38; + max-height: 194px; overflow: auto; - padding: 8px 10px; + padding: 6px 10px; position: relative; } @@ -2408,8 +2509,8 @@ export function renderApp(): string { .verify-row { border-radius: 7px; grid-template-columns: 34px minmax(0, 1fr) minmax(72px, auto); - min-height: 42px; - padding: 6px 9px; + min-height: 40px; + padding: 5px 9px; } .verify-row > div { @@ -2432,21 +2533,21 @@ export function renderApp(): string { } .verify-icon.log { - background: #f3f8ff; - border-color: #d4e5fb; - color: #0969da; + background: #fff; + border-color: var(--line); + color: #4b5563; } .verify-icon.sig { - background: #fff7ed; - border-color: #fed7aa; - color: #b35a00; + background: #fff; + border-color: var(--line); + color: #4b5563; } .verify-icon.get { - background: #eefbf4; - border-color: #c7efd6; - color: #078861; + background: #fff; + border-color: var(--line); + color: #4b5563; } .verify-row .event-action { @@ -2703,7 +2804,7 @@ export function renderApp(): string {
Live run - Run ID pending + Run ID pending Region IAD Started waiting @@ -2719,11 +2820,11 @@ export function renderApp(): string {
- 1TriggerPending - 2Autonomous triagePending + 11. TriggerPending + 22. Autonomous triagePending 33. Human review haltedPendingAwaiting review - 4MCP execution resumedPending - 5Audit readyPending + 44. MCP execution resumedPending + 55. Audit readyPending
@@ -2807,6 +2908,7 @@ export function renderApp(): string {

Summary appears after a signed record is selected.

+
Verify in Cloudflare Integrity LogCheck inclusion and consistency proof
Verify receipt signatureValidate signer and record hashes
@@ -3224,7 +3326,8 @@ export function renderApp(): string { function shortHash(hash) { if (!hash) return 'missing'; - return hash.slice(0, 18) + '...' + hash.slice(-8); + const normalized = String(hash).replace(/^sha256:/, ''); + return normalized.slice(0, 18) + '...' + normalized.slice(-8); } function recordDisplayId(hash) { @@ -3367,6 +3470,30 @@ export function renderApp(): string { )).join('') + ''; } + function traceReceiptPayload(run = currentRun) { + if (!run) return null; + const createdAt = run.records[0]?.record?.timestamp + ? new Date(run.records[0].record.timestamp).toISOString() + : null; + return { + trace_id: traceIdForRun(run), + run_id: run.run_id, + status: run.status === 'pending_approval' ? 'human_review_halted' : run.status, + current_step: run.status === 'pending_approval' ? 3 : ['succeeded', 'failed', 'rejected'].includes(run.status) ? 5 : 4, + created_at: createdAt, + records: run.trace_packet.timeline.map((entry) => { + const record = run.records.find((item) => item.record_hash === entry.record_hash); + return { + record_id: recordDisplayId(entry.record_hash), + timestamp: record?.record?.timestamp ? new Date(record.record.timestamp).toISOString() : null, + event: entry.event, + label: entry.label, + record_hash: entry.record_hash, + }; + }), + }; + } + async function writeClipboard(text) { const value = String(text ?? ''); if (!value) return false; @@ -3444,14 +3571,14 @@ export function renderApp(): string { function verificationRowsMarkup(run = currentRun, record = selectedReceiptRecord) { if (!run || !record) { - return '
' + return '
' + '
' + verifyIcon('log') + '
Verify in Cloudflare Integrity LogCheck inclusion and consistency proof
' + '
' + verifyIcon('sig') + '
Verify receipt signatureValidate signer and record hashes
' + '
' + verifyIcon('get') + '
Download transparency proofCT-style proof for this receipt
' + '
'; } const proofUrl = proofTargetForRun(run); - return '
' + return '
' + '
' + verifyIcon('log') + '
Verify in Cloudflare Integrity LogCheck inclusion and consistency proof
View proof ' + actionGlyph('external') + '
' + '
' + verifyIcon('sig') + '
Verify receipt signatureValidate signer and record hashes
' + '
' + verifyIcon('get') + '
Download transparency proofCT-style proof for this receipt
' @@ -3478,9 +3605,10 @@ export function renderApp(): string { } function updateTraceHeaderCopy() { - const button = document.querySelector('[data-copy-source="#traceIdLabel"]'); - if (!button) return; - button.disabled = !traceIdLabel.textContent || traceIdLabel.textContent === 'pending'; + const traceButton = document.querySelector('[data-copy-source="#traceIdLabel"]'); + if (traceButton) traceButton.disabled = !traceIdLabel.textContent || traceIdLabel.textContent === 'pending'; + const runButton = document.querySelector('[data-copy-source="#runIdLabel"]'); + if (runButton) runButton.disabled = !runIdLabel.textContent || runIdLabel.textContent === 'pending'; } function renderVerificationActions(run = currentRun, record = selectedReceiptRecord) { @@ -3770,6 +3898,7 @@ export function renderApp(): string { ]; timelineEl.innerHTML = run.trace_packet.timeline.length ? \` +
\${run.trace_packet.timeline.map((entry, index) => { const record = run.records.find((item) => item.record_hash === entry.record_hash); @@ -3867,9 +3996,9 @@ export function renderApp(): string { \`; bindReceiptTabs(record); }; - const selectRecord = (record) => { + const selectRecord = (record, options = {}) => { selectedReceiptRecord = record; - receiptsEl.innerHTML = renderReceiptJson(record); + receiptsEl.innerHTML = renderReceiptJson(options.showTrace ? traceReceiptPayload(run) : record); renderReceiptSummary(record); renderVerificationActions(run, record); updateReceiptControls(record); @@ -3884,7 +4013,12 @@ export function renderApp(): string { }); const preferredLabel = run.status === 'pending_approval' ? 'proposal' : run.status === 'rejected' ? 'rejection' : run.status === 'failed' ? 'outcome' : 'handoff'; const preferredButton = timelineEl.querySelector(\`.event[data-label="\${preferredLabel}"]\`) ?? timelineEl.querySelector('.event'); - preferredButton?.click(); + if (preferredButton) { + timelineEl.querySelectorAll('.event').forEach((item) => item.classList.remove('selected')); + preferredButton.classList.add('selected'); + const preferredRecord = run.records.find((item) => item.record_hash === preferredButton.dataset.hash); + if (preferredRecord) selectRecord(preferredRecord, { showTrace: true }); + } } function render(run) { From add659cda468b724d4e3b4bf01b210eb2874c790 Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 06:14:03 -0500 Subject: [PATCH 04/64] fix: polish Cloudflare trace demo layout --- .../approval-trace/src/ui.ts | 382 +++++++++++++----- .../test/browser/approval-trace.ui.spec.ts | 77 ++++ 2 files changed, 349 insertions(+), 110 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index ded4dc91..f0c3645e 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -38,6 +38,8 @@ export function renderApp(): string { var(--bg); color: var(--text); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + max-width: 100%; + overflow-x: hidden; } button, @@ -964,6 +966,7 @@ export function renderApp(): string { margin: 0 auto; max-width: 1536px; padding: 0 0 0; + width: 100%; } .hero { @@ -1006,6 +1009,7 @@ export function renderApp(): string { flex: 1 1 auto; gap: 18px; justify-content: flex-start; + position: relative; } .header-meta > span:nth-child(3) { @@ -1073,11 +1077,61 @@ export function renderApp(): string { width: 32px; } + .header-menu:hover, + .header-menu:focus-visible, + .header-menu[aria-expanded="true"] { + border-color: #b8c8f6; + color: var(--blue); + outline: 0; + } + .header-menu svg { height: 16px; width: 16px; } + .header-actions-menu { + background: #fff; + border: 1px solid var(--line); + border-radius: 8px; + box-shadow: 0 14px 32px rgba(18, 27, 42, 0.16); + display: grid; + min-width: 178px; + padding: 5px; + position: absolute; + right: 0; + top: 42px; + z-index: 20; + } + + .header-actions-menu[hidden] { + display: none; + } + + .header-actions-menu button, + .header-actions-menu a { + align-items: center; + background: transparent; + border-radius: 6px; + color: var(--ink); + display: flex; + font-size: 12px; + font-weight: 700; + min-height: 30px; + padding: 7px 9px; + text-align: left; + text-decoration: none; + white-space: nowrap; + } + + .header-actions-menu button:hover, + .header-actions-menu button:focus-visible, + .header-actions-menu a:hover, + .header-actions-menu a:focus-visible { + background: #f4f7fb; + outline: 0; + } + .meta-pill, .meta-code { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); @@ -1121,8 +1175,8 @@ export function renderApp(): string { .rail-stepper { align-items: center; display: grid; - gap: 12px; - grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 16px; + grid-template-columns: minmax(126px, 1fr) minmax(196px, 1.18fr) minmax(236px, 1.12fr) minmax(242px, 1.36fr) minmax(142px, 0.9fr); position: relative; width: 100%; } @@ -1132,20 +1186,23 @@ export function renderApp(): string { background: transparent; border: 1px solid transparent; color: var(--ink); - display: flex; - min-height: 52px; + display: grid; + gap: 10px; + grid-template-columns: 38px minmax(0, 1fr); + min-height: 64px; min-width: 0; - padding: 6px 10px; + padding: 6px 0; position: relative; } .step:not(:last-child)::after { border-top: 2px solid #c5cfdb; content: ""; - left: 168px; + left: 54px; position: absolute; - top: 26px; - width: calc(100% - 178px); + right: -16px; + top: 31px; + width: auto; z-index: 0; } @@ -1164,18 +1221,19 @@ export function renderApp(): string { .step.halted { background: #fff8ed; border-color: #f3a64e; + border-radius: 8px; color: var(--ink); - justify-self: center; - min-height: 58px; - padding: 8px 14px; - width: 332px; + min-height: 64px; + padding: 6px 10px; + width: auto; } .step.halted:not(:last-child)::after { border-color: #c5cfdb; border-top-style: dashed; - left: calc(100% + 8px); - width: 78px; + left: calc(100% + 1px); + right: -16px; + width: auto; } .step.done { @@ -1194,33 +1252,32 @@ export function renderApp(): string { font-size: 14px; font-weight: 800; height: 38px; - margin-right: 11px; width: 38px; z-index: 1; } .step.done .step-index { + color: #fff; font-size: 0; position: relative; } .step.done .step-index::after { content: ""; - border-bottom: 3px solid #fff; - border-right: 3px solid #fff; - height: 15px; + background: currentColor; + height: 22px; left: 50%; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.2 6.5 11 12 4.8' fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2.1'/%3E%3C/svg%3E") center / contain no-repeat; + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.2 6.5 11 12 4.8' fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2.1'/%3E%3C/svg%3E") center / contain no-repeat; position: absolute; top: 50%; - transform: translate(-50%, -56%) rotate(45deg); - width: 8px; + transform: translate(-50%, -50%); + width: 22px; } .step.halted .step-index { font-size: 0; - height: 42px; position: relative; - width: 42px; } .step.halted .step-index::before, @@ -1236,11 +1293,11 @@ export function renderApp(): string { } .step.halted .step-index::before { - left: 14px; + left: 13px; } .step.halted .step-index::after { - right: 14px; + right: 13px; } .step-copy strong { @@ -1258,31 +1315,38 @@ export function renderApp(): string { .step-copy { background: #fff; + display: grid; + gap: 3px; min-width: 0; + padding: 0 4px; position: relative; z-index: 1; } .step.halted .step-copy { background: #fff8ed; - display: grid; - gap: 3px; } - .step.halted .step-copy strong { + .step.done .step-copy, + .step.active .step-copy, + .step.error .step-copy { + background: #fff; + } + + .step[data-step="halt"] .step-copy strong { display: block; font-size: 14px; font-weight: 850; line-height: 1.1; } - .step.halted .step-copy .step-number-label, - .step.halted .step-copy [data-step-title] { + .step[data-step="halt"] .step-copy .step-number-label, + .step[data-step="halt"] .step-copy [data-step-title] { display: inline; margin-top: 0; } - .step.halted .step-copy .step-meta-line { + .step[data-step="halt"] .step-copy .step-meta-line { align-items: center; display: flex; gap: 10px; @@ -1291,6 +1355,10 @@ export function renderApp(): string { min-width: 0; } + .step[data-step="halt"] [data-step-time="halt"] { + white-space: nowrap; + } + .step-badge { background: #fff0dc; border: 1px solid #ffd09a; @@ -1306,6 +1374,10 @@ export function renderApp(): string { vertical-align: 1px; } + .step-badge[hidden] { + display: none; + } + .step-copy strong [data-step-title] { display: inline; } @@ -1315,62 +1387,6 @@ export function renderApp(): string { color: var(--blue); } - @media (min-width: 1451px) { - .rail-stepper { - display: block; - height: 72px; - } - - .step { - position: absolute; - top: 7px; - } - - .step[data-step="trigger"] { - left: 10px; - width: 286px; - } - - .step[data-step="autonomous"] { - left: 300px; - width: 270px; - } - - .step[data-step="halt"] { - left: 569px; - } - - .step[data-step="resume"] { - left: 966px; - width: 300px; - } - - .step[data-step="audit"] { - left: 1288px; - width: 190px; - } - - .step[data-step="trigger"]::after { - left: 158px; - width: 128px; - } - - .step[data-step="autonomous"]::after { - left: 148px; - width: 121px; - } - - .step.halted:not(:last-child)::after { - left: 332px; - width: 74px; - } - - .step[data-step="resume"]::after { - left: 130px; - width: 154px; - } - } - .step-badge.approved { background: #e9f8ef; border-color: #b9e5cb; @@ -1383,10 +1399,17 @@ export function renderApp(): string { color: #be123c; } + @media (min-width: 1250px) { + .rail-stepper { + grid-template-columns: minmax(170px, 223px) minmax(220px, 263px) minmax(280px, 332px) minmax(250px, 1fr) minmax(160px, 200px); + } + } + .grid { gap: 10px; align-items: start; - grid-template-columns: 363px 610px 523px; + grid-template-columns: minmax(318px, 363px) minmax(500px, 610px) minmax(340px, 523px); + justify-content: center; margin: 0 10px; } @@ -1601,11 +1624,14 @@ export function renderApp(): string { } .progress-item .dot { + align-items: center; border: 0; + border-radius: 999px; box-shadow: none; box-sizing: border-box; display: inline-flex; height: 16px; + justify-content: center; justify-self: center; margin-left: 0; position: relative; @@ -1636,19 +1662,18 @@ export function renderApp(): string { .progress-item:not(.proposal) .dot.ok { background: var(--green); + color: #fff; font-size: 0; } .progress-item:not(.proposal) .dot.ok::after { - border-bottom: 2px solid #fff; - border-right: 2px solid #fff; content: ""; - height: 8px; - left: 5px; + background: currentColor; + height: 10px; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.2 6.5 11 12 4.8' fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2.1'/%3E%3C/svg%3E") center / contain no-repeat; + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.2 6.5 11 12 4.8' fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2.1'/%3E%3C/svg%3E") center / contain no-repeat; position: absolute; - top: 2px; - transform: rotate(45deg); - width: 4px; + width: 10px; } .progress-item.halted .dot.pending { @@ -1711,6 +1736,28 @@ export function renderApp(): string { white-space: nowrap; } + .diff-tools select, + .diff-tools button { + background: transparent; + border: 0; + border-radius: 6px; + color: var(--muted); + font-size: 12px; + font-weight: 650; + min-height: 24px; + padding: 2px 4px; + } + + .diff-tools select:hover, + .diff-tools select:focus-visible, + .diff-tools button:hover, + .diff-tools button:focus-visible, + .diff-tools button[aria-pressed="true"] { + background: #f4f7fb; + color: var(--ink); + outline: 0; + } + .diff pre { font-size: 11.3px; line-height: 1.38; @@ -1741,6 +1788,11 @@ export function renderApp(): string { width: auto; } + .diff-code.wrap .diff-line { + overflow-wrap: anywhere; + white-space: pre-wrap; + } + .diff-line.add { background: #e9f8ef; color: #0b5138; @@ -2016,15 +2068,13 @@ export function renderApp(): string { } .event-marker.done::after { - border-bottom: 2px solid #fff; - border-right: 2px solid #fff; content: ""; - height: 8px; - left: 6px; + background: currentColor; + height: 11px; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.2 6.5 11 12 4.8' fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2.1'/%3E%3C/svg%3E") center / contain no-repeat; + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.2 6.5 11 12 4.8' fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2.1'/%3E%3C/svg%3E") center / contain no-repeat; position: absolute; - top: 3px; - transform: rotate(45deg); - width: 4px; + width: 11px; } .event-marker.pending { @@ -2091,6 +2141,7 @@ export function renderApp(): string { color: #44536a; font-size: 11px; max-width: 132px; + min-width: 0; overflow: hidden; overflow-wrap: normal; text-align: right; @@ -2148,7 +2199,7 @@ export function renderApp(): string { background: #fff; border-radius: 0; gap: 7px; - grid-template-columns: 24px 72px minmax(112px, 1fr) 58px minmax(84px, 106px) 16px; + grid-template-columns: 24px 78px minmax(88px, 1fr) minmax(52px, auto) minmax(58px, 92px) 16px; min-height: 31px; padding: 3px 8px; box-shadow: none; @@ -2177,6 +2228,8 @@ export function renderApp(): string { .signer-row strong { font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; white-space: nowrap; } @@ -2250,13 +2303,20 @@ export function renderApp(): string { align-self: center; color: var(--muted); font-size: 11px; + min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .signature-slot .hash { + display: inline-block; font-size: 10px; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: bottom; + white-space: nowrap; } .copy-icon { @@ -2298,7 +2358,8 @@ export function renderApp(): string { display: grid; font-size: 12px; gap: 8px; - grid-template-columns: 94px minmax(0, 1fr) auto; + grid-template-columns: 94px minmax(0, 1fr) 16px; + min-width: 0; } .integrity-row strong { @@ -2312,6 +2373,22 @@ export function renderApp(): string { white-space: nowrap; } + .integrity-row .value { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .integrity-row.proof-row { + grid-template-columns: 94px minmax(0, 1fr) auto; + } + + .trace-integrity .event-action { + justify-self: end; + white-space: nowrap; + } + .verify-row { background: #fff; } @@ -2404,7 +2481,7 @@ export function renderApp(): string { } .receipt-shell { - grid-template-columns: minmax(500px, 520px) minmax(430px, 458px) minmax(520px, 1fr); + grid-template-columns: minmax(360px, 520px) minmax(320px, 458px) minmax(360px, 1fr); } .receipt-section { @@ -2612,12 +2689,22 @@ export function renderApp(): string { grid-template-columns: 16px minmax(0, 1fr); } + .verification-step > div { + display: grid; + gap: 2px; + min-width: 0; + } + .verification-step strong { + display: block; font-size: 12px; + line-height: 1.25; } .verification-step span:last-child { color: var(--muted); + display: block; + line-height: 1.25; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -2722,7 +2809,7 @@ export function renderApp(): string { .signer-row { gap: 5px; - grid-template-columns: 22px 62px minmax(0, 1fr) 50px minmax(42px, 58px) 12px; + grid-template-columns: 22px 72px minmax(0, 1fr) minmax(46px, auto) minmax(30px, 44px) 12px; padding: 4px 6px; } @@ -2807,7 +2894,12 @@ export function renderApp(): string { Run ID pending Region IAD Started waiting - + +
@@ -2950,6 +3042,8 @@ export function renderApp(): string { const traceIdLabel = document.querySelector('#traceIdLabel'); const copyReceiptButton = document.querySelector('#copyReceipt'); const downloadReceiptButton = document.querySelector('#downloadReceipt'); + const headerMenuButton = document.querySelector('#headerMenu'); + const headerActionsMenu = document.querySelector('#headerActions'); const bootStages = [ { @@ -3060,9 +3154,17 @@ export function renderApp(): string { function updateHaltStepState(run = currentRun) { const title = workflowSteps.querySelector('[data-step-title="halt"]'); const badge = workflowSteps.querySelector('[data-step-badge="halt"]'); + const haltStep = workflowSteps.querySelector('[data-step="halt"]'); if (!title || !badge) return; badge.classList.remove('approved', 'rejected'); - if (!run || run.status === 'pending_approval') { + badge.hidden = false; + if (!run) { + title.textContent = 'Human review halted'; + badge.textContent = 'Awaiting review'; + badge.hidden = !haltStep?.classList.contains('halted'); + return; + } + if (run.status === 'pending_approval') { title.textContent = 'Human review halted'; badge.textContent = 'Awaiting review'; return; @@ -3245,7 +3347,7 @@ export function renderApp(): string {
Merkle rootpending\${copyIcon('', 'Merkle root')}
Log hashpending\${copyIcon('', 'log hash')}
-
Proof statusWaiting for first signed record
+
Proof statusWaiting for first signed record
\`; @@ -3305,6 +3407,7 @@ export function renderApp(): string { const label = reject.querySelector('.button-label'); if (label) label.textContent = busy && activeLabel === 'reject' ? 'Rejecting...' : 'Reject'; } + updateHeaderMenuControls(); } function setBusy(next, activeLabel = '') { @@ -3611,6 +3714,19 @@ export function renderApp(): string { if (runButton) runButton.disabled = !runIdLabel.textContent || runIdLabel.textContent === 'pending'; } + function setHeaderMenuOpen(open) { + if (!headerMenuButton || !headerActionsMenu) return; + headerMenuButton.setAttribute('aria-expanded', String(open)); + headerActionsMenu.hidden = !open; + } + + function updateHeaderMenuControls() { + if (!headerActionsMenu) return; + headerActionsMenu.querySelectorAll('[data-header-action="open-json"], [data-header-action="reset"]').forEach((button) => { + button.disabled = !currentRun || busy; + }); + } + function renderVerificationActions(run = currentRun, record = selectedReceiptRecord) { verificationEl.innerHTML = verificationRowsMarkup(run, record); } @@ -3747,7 +3863,10 @@ export function renderApp(): string {
Diff (unified) - Context3 linesWrap + + + +
\${renderDiff(diff)}
@@ -3778,6 +3897,16 @@ export function renderApp(): string { button.setAttribute('aria-expanded', String(!expanded)); details.hidden = expanded; }); + document.querySelector('#diffWrapToggle')?.addEventListener('click', (event) => { + const button = event.currentTarget; + const code = document.querySelector('.diff-code'); + const pressed = button.getAttribute('aria-pressed') === 'true'; + button.setAttribute('aria-pressed', String(!pressed)); + code?.classList.toggle('wrap', !pressed); + }); + document.querySelector('#diffContext')?.addEventListener('change', (event) => { + document.querySelector('.diff')?.setAttribute('data-context-lines', event.currentTarget.value); + }); document.querySelector('#approve')?.addEventListener('click', async () => { await transition({ title: 'Agent resumed', @@ -3944,7 +4073,7 @@ export function renderApp(): string {
Merkle root\${run.records[0]?.record_hash ?? 'pending'}\${copyIcon(run.records[0]?.record_hash ?? '', 'Merkle root')}
Log hash\${run.records[1]?.record_hash ?? 'pending'}\${copyIcon(run.records[1]?.record_hash ?? '', 'log hash')}
-
Proof statusIncluded in Cloudflare Integrity LogView proof
+
Proof statusIncluded in Cloudflare Integrity LogView proof
\` @@ -4062,6 +4191,34 @@ export function renderApp(): string { document.addEventListener('click', async (event) => { const target = event.target; + const menuButton = target.closest?.('#headerMenu'); + if (menuButton) { + setHeaderMenuOpen(menuButton.getAttribute('aria-expanded') !== 'true'); + return; + } + + const menuAction = target.closest?.('[data-header-action]'); + if (menuAction && !menuAction.disabled) { + const action = menuAction.dataset.headerAction; + setHeaderMenuOpen(false); + if (action === 'copy-link') { + if (await writeClipboard(window.location.href)) markCopied(menuAction); + return; + } + if (action === 'open-json' && currentRun) { + window.open('/api/runs/' + currentRun.run_id, '_blank', 'noreferrer'); + return; + } + if (action === 'reset') { + resetButton.click(); + return; + } + } + + if (headerActionsMenu && !target.closest?.('#headerActions')) { + setHeaderMenuOpen(false); + } + const copyButton = target.closest?.('[data-copy-value], [data-copy-source], #copyReceipt'); if (copyButton && !copyButton.disabled) { const source = copyButton.dataset.copySource @@ -4124,6 +4281,7 @@ export function renderApp(): string { resetButton.addEventListener('click', () => { if (busy) return; + setHeaderMenuOpen(false); currentRun = null; selectedReceiptRecord = null; stageDisplayTimes = {}; @@ -4140,6 +4298,10 @@ export function renderApp(): string { updateControls(); }); + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape') setHeaderMenuOpen(false); + }); + updateControls(); updateTraceHeaderCopy(); if (!autoStarted) { diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index 14072a88..1116d01d 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -48,11 +48,43 @@ async function expectCopies(button: Locator): Promise { await expect(button).toHaveAttribute('data-copy-state', 'copied') } +async function expectNoHorizontalOverflow(page: Page): Promise { + const overflow = await page.evaluate<{ + bodyWidth: number + documentWidth: number + nodes: Array<{ className: string; tagName: string; text: string }> + viewportWidth: number + }>(`(() => { + const nodes = Array.from(document.querySelectorAll('*')) + .map((element) => { + const rect = element.getBoundingClientRect() + if (rect.width === 0 || rect.height === 0) return null + if (rect.left >= -1 && rect.right <= window.innerWidth + 1) return null + return { + className: String(element.className), + tagName: element.tagName, + text: element.textContent?.trim().replace(/\s+/g, ' ').slice(0, 80) ?? '', + } + }) + .filter(Boolean) + return { + bodyWidth: document.body.scrollWidth, + documentWidth: document.documentElement.scrollWidth, + nodes, + viewportWidth: window.innerWidth, + } + })()`) + expect(overflow.bodyWidth).toBeLessThanOrEqual(overflow.viewportWidth) + expect(overflow.documentWidth).toBeLessThanOrEqual(overflow.viewportWidth) + expect(overflow.nodes).toEqual([]) +} + test.describe('Cloudflare approval trace browser UI', () => { test('clicks through approved execution and opens the signed receipt', async ({ page }) => { await expectCleanConsole(page, async () => { await page.context().grantPermissions(['clipboard-write']) await createProposal(page) + await expectNoHorizontalOverflow(page) const visibleTimes = await page.locator('#answer .progress-time').allTextContents() const populatedTimes = visibleTimes.filter((time) => time !== '-') @@ -73,6 +105,20 @@ test.describe('Cloudflare approval trace browser UI', () => { await expect(page.locator('#riskDetails')).toBeVisible() await expect(page.locator('#riskDetails')).toContainText('Human review gate') + await page.locator('#diffWrapToggle').click() + await expect(page.locator('#diffWrapToggle')).toHaveAttribute('aria-pressed', 'true') + await expect(page.locator('.diff-code')).toHaveClass(/wrap/) + await page.locator('#diffContext').selectOption('6') + await expect(page.locator('.diff')).toHaveAttribute('data-context-lines', '6') + + await page.locator('#headerMenu').click() + await expect(page.locator('#headerActions')).toBeVisible() + await expect(page.locator('[data-header-action="copy-link"]')).toBeEnabled() + await expect(page.locator('[data-header-action="open-json"]')).toBeEnabled() + await expect(page.locator('[data-header-action="reset"]')).toBeEnabled() + await page.keyboard.press('Escape') + await expect(page.locator('#headerActions')).toBeHidden() + await page.getByRole('tab', { name: 'Record details' }).click() await expect(page.locator('#receiptSummary')).toContainText('Record hash') await expect(page.locator('#receiptSummary')).toContainText('Timestamp') @@ -98,6 +144,7 @@ test.describe('Cloudflare approval trace browser UI', () => { await page.getByRole('button', { name: 'Approve and resume' }).click() await expect(page.locator('#statusTitle')).toHaveText('Trace complete', { timeout: 30_000 }) + await expectNoHorizontalOverflow(page) await expect(page.locator('[data-step="halt"]')).toContainText('Approved') await expect(page.locator('[data-step="halt"]')).not.toContainText('Awaiting review') await expect(page.locator('#answer')).toContainText('Agent resumed through MCP') @@ -122,6 +169,36 @@ test.describe('Cloudflare approval trace browser UI', () => { }) }) + test('keeps the desktop trace layout contained through the live stages', async ({ page }) => { + await expectCleanConsole(page, async () => { + await page.setViewportSize({ width: 1365, height: 768 }) + await page.goto('/') + await expect(page).toHaveTitle('Cloudflare Agent Trace') + await expect(page.getByTestId('approval-trace-app')).toBeVisible() + await expectNoHorizontalOverflow(page) + + await expect(page.locator('#statusTitle')).toHaveText('Halted for human review', { + timeout: 15_000, + }) + await expect(page.locator('[data-step="halt"]')).toContainText('Awaiting review') + await expectNoHorizontalOverflow(page) + + const signerSpacing = await page.evaluate(`Array.from(document.querySelectorAll('.signer-row')) + .map((row) => { + const cells = Array.from(row.children).map((child) => child.getBoundingClientRect()) + const nameCell = cells[1] + const detailCell = cells[2] + return Boolean(nameCell && detailCell && nameCell.right < detailCell.left) + })`) + expect(signerSpacing).toEqual([true, true, true]) + + await page.getByRole('button', { name: 'Approve and resume' }).click() + await expect(page.locator('#statusTitle')).toHaveText('Trace complete', { timeout: 30_000 }) + await expect(page.locator('[data-step="halt"]')).toContainText('Approved') + await expectNoHorizontalOverflow(page) + }) + }) + test('clicks through rejection and shows no action MCP record', async ({ page }) => { await expectCleanConsole(page, async () => { await createProposal(page) From b3071d4ce5c9c776b074d3d04a8fdd6900e47a20 Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 06:25:08 -0500 Subject: [PATCH 05/64] fix: keep Cloudflare trace menu above panels --- .../cloudflare-agents/approval-trace/src/ui.ts | 5 ++++- .../test/browser/approval-trace.ui.spec.ts | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index f0c3645e..92053512 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -978,7 +978,10 @@ export function renderApp(): string { justify-content: flex-start; margin-bottom: 8px; min-height: 62px; + overflow: visible; padding: 10px 28px; + position: relative; + z-index: 30; } .hero::after { @@ -1101,7 +1104,7 @@ export function renderApp(): string { position: absolute; right: 0; top: 42px; - z-index: 20; + z-index: 100; } .header-actions-menu[hidden] { diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index 1116d01d..abf2f269 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -79,6 +79,18 @@ async function expectNoHorizontalOverflow(page: Page): Promise { expect(overflow.nodes).toEqual([]) } +async function expectHeaderMenuAboveContent(page: Page): Promise { + const menuHit = await page.evaluate(`(() => { + const menu = document.querySelector('#headerActions') + if (!menu || menu.hidden) return false + const rect = menu.getBoundingClientRect() + const x = Math.floor(rect.left + rect.width / 2) + const y = Math.floor(rect.top + Math.min(rect.height - 2, 18)) + return Boolean(document.elementFromPoint(x, y)?.closest('#headerActions')) + })()`) + expect(menuHit).toBe(true) +} + test.describe('Cloudflare approval trace browser UI', () => { test('clicks through approved execution and opens the signed receipt', async ({ page }) => { await expectCleanConsole(page, async () => { @@ -116,6 +128,7 @@ test.describe('Cloudflare approval trace browser UI', () => { await expect(page.locator('[data-header-action="copy-link"]')).toBeEnabled() await expect(page.locator('[data-header-action="open-json"]')).toBeEnabled() await expect(page.locator('[data-header-action="reset"]')).toBeEnabled() + await expectHeaderMenuAboveContent(page) await page.keyboard.press('Escape') await expect(page.locator('#headerActions')).toBeHidden() From 6025912755148397302702756e9b2e05b23c1842 Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 07:03:11 -0500 Subject: [PATCH 06/64] fix: repair Cloudflare trace demo state controls --- .../approval-trace/src/ui.ts | 191 +++++++++++++----- .../test/browser/approval-trace.ui.spec.ts | 26 +++ 2 files changed, 164 insertions(+), 53 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index 92053512..725fbde9 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -37,7 +37,7 @@ export function renderApp(): string { linear-gradient(180deg, #f8fafc 0, #eef2f7 320px, #e8edf4 100%), var(--bg); color: var(--text); - font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", sans-serif; max-width: 100%; overflow-x: hidden; } @@ -2011,7 +2011,7 @@ export function renderApp(): string { background: #cfd8e5; bottom: 26px; content: ""; - left: 70px; + left: 9px; position: absolute; top: 24px; width: 2px; @@ -2026,7 +2026,7 @@ export function renderApp(): string { border-radius: 0; display: grid; gap: 7px; - grid-template-columns: 54px 20px minmax(0, 1fr) minmax(82px, 120px); + grid-template-columns: 20px 54px minmax(0, 1fr) minmax(82px, 120px); min-height: 44px; min-width: 0; padding: 4px 0; @@ -2436,6 +2436,20 @@ export function renderApp(): string { text-transform: none; } + .receipt-format { + appearance: auto; + background: #fff; + border: 1px solid var(--line); + border-radius: 6px; + color: var(--ink); + font: inherit; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 12px; + height: 26px; + max-width: 156px; + padding: 0 6px; + } + .icon-button { align-items: center; background: #fff; @@ -2799,11 +2813,11 @@ export function renderApp(): string { .event, .event-future { gap: 6px; - grid-template-columns: 48px 18px minmax(0, 1fr) minmax(66px, 92px); + grid-template-columns: 18px 48px minmax(0, 1fr) minmax(66px, 92px); } .record-timeline::before { - left: 62px; + left: 8px; } .event-hash { @@ -2870,7 +2884,7 @@ export function renderApp(): string { } .event { - grid-template-columns: 48px 20px minmax(0, 1fr); + grid-template-columns: 20px 48px minmax(0, 1fr); } .event-hash { @@ -2985,7 +2999,10 @@ export function renderApp(): string {

Receipt inspector

Format - JSON (pretty) + @@ -3023,6 +3040,8 @@ export function renderApp(): string { let autoFollow = true; let stageDisplayTimes = {}; let selectedReceiptRecord = null; + let selectedReceiptView = 'record'; + let selectedReceiptFormat = 'pretty'; const statusDot = document.querySelector('#statusDot'); const statusTitle = document.querySelector('#statusTitle'); @@ -3045,6 +3064,7 @@ export function renderApp(): string { const traceIdLabel = document.querySelector('#traceIdLabel'); const copyReceiptButton = document.querySelector('#copyReceipt'); const downloadReceiptButton = document.querySelector('#downloadReceipt'); + const receiptFormatSelect = document.querySelector('#receiptFormat'); const headerMenuButton = document.querySelector('#headerMenu'); const headerActionsMenu = document.querySelector('#headerActions'); @@ -3260,7 +3280,15 @@ export function renderApp(): string { return new Date(Date.now() + offsetMs).toISOString().slice(11, 19); } - function renderBootTimeline(activeIndex) { + function bootRecordFor(key, run = currentRun) { + if (!run) return null; + if (key === 'trigger') return run.records.find((record) => record.label === 'trigger'); + if (key === 'context') return run.records.find((record) => record.label === 'triage'); + if (key === 'proposal' || key === 'halt') return run.records.find((record) => record.label === 'proposal'); + return null; + } + + function renderBootTimeline(activeIndex, run = currentRun) { const activeStage = bootStages[activeIndex] ?? bootStages[bootStages.length - 1]; const reached = (key) => bootStages.findIndex((stage) => stage.key === key) <= activeIndex; const bootRows = [ @@ -3314,24 +3342,31 @@ export function renderApp(): string { }, ]; const bootSigners = [ - { kind: 'agent', name: 'Agent', detail: 'agents/triage@1.4.2', status: reached('proposal') ? 'Signed' : 'Pending', className: reached('proposal') ? 'signed' : 'pending mcp', sig: reached('proposal') ? '2fb06b13...' : '-' }, + { kind: 'agent', name: 'Agent', detail: 'agents/triage@1.4.2', signer: 'agent', status: reached('proposal') ? 'Signed' : 'Pending', className: reached('proposal') ? 'signed' : 'pending mcp', sig: reached('proposal') && run ? signerSignature(run, 'agent') : '-' }, { kind: 'human', name: 'Human', detail: 'alice@example.com', status: 'Pending', className: 'pending', sig: '-' }, { kind: 'mcp', name: 'Action MCP', detail: 'github.write@2.3.1', status: 'Pending', className: 'pending mcp', sig: '-' }, ]; + const merkleRoot = reached('trigger') ? run?.records[0]?.record_hash ?? '' : ''; + const logHash = reached('context') ? run?.records[1]?.record_hash ?? '' : ''; timelineEl.innerHTML = \`
- \${bootRows.map((row) => \` + \${bootRows.map((row) => { + const record = bootRecordFor(row.key, run); + const time = row.time ?? (record ? displayRecordTime(record, record.label) + ' UTC' : ''); + const hash = row.marker === 'future' ? '-' : record ? recordDisplayId(record.record_hash) : 'pending'; + return \`
- \${row.time ? row.time.slice(0, 8) : '-'} + \${time ? time.slice(0, 8) : '-'} \${row.name} \${row.detail} - \${row.marker === 'future' ? '-' : 'pending'} + \${hash}
- \`).join('')} + \`; + }).join('')}
@@ -3341,22 +3376,22 @@ export function renderApp(): string { \${signer.name} \${signer.detail} \${signer.status} - Sig: \${signer.sig}\${copyIcon('', signer.name + ' signature')} + Sig: \${signer.sig}\${copyIcon(run && signer.signer ? signerRecordHash(run, signer.signer) : '', signer.name + ' signature')}
\`).join('')}
-
Merkle rootpending\${copyIcon('', 'Merkle root')}
-
Log hashpending\${copyIcon('', 'log hash')}
-
Proof statusWaiting for first signed record
+
Merkle root\${merkleRoot || 'pending'}\${copyIcon(merkleRoot, 'Merkle root')}
+
Log hash\${logHash || 'pending'}\${copyIcon(logHash, 'log hash')}
+
Proof status\${merkleRoot ? 'Signed records available' : 'Waiting for first signed record'}
\`; } - function renderBootProgress(activeIndex) { + function renderBootProgress(activeIndex, run = currentRun) { const rows = bootStages.map((stage, index) => { const done = index < activeIndex; const active = index === activeIndex; @@ -3384,7 +3419,7 @@ export function renderApp(): string {
\`; } - renderBootTimeline(activeIndex); + renderBootTimeline(activeIndex, run); followElement(answerEl.querySelectorAll('.progress-item')[activeIndex], 'nearest'); if (activeStage.key === 'halt') followElement(proposalEl, 'nearest'); } @@ -3516,8 +3551,12 @@ export function renderApp(): string { return status + (signer.kind === 'mcp' ? ' mcp' : ''); } + function traceIdFromRunId(runId) { + return 'trc_' + String(runId).replaceAll('-', '').toUpperCase().slice(0, 18); + } + function traceIdForRun(run) { - return run.trace_packet.trace_id ?? 'trc_' + run.run_id.replaceAll('-', '').toUpperCase().slice(0, 18); + return run.trace_packet?.trace_id ?? traceIdFromRunId(run.run_id); } function copyIcon(value = '', label = 'value') { @@ -3526,8 +3565,24 @@ export function renderApp(): string { return ''; } - function renderDiff(diff) { - return String(diff).split('\\n').map((line) => { + function visibleDiffLines(diff, context = '3') { + const lines = String(diff).split('\\n'); + if (context === 'all') return lines; + const contextLines = Number.parseInt(context, 10); + if (!Number.isFinite(contextLines)) return lines; + let shownContext = 0; + return lines.filter((line) => { + const changed = (line.startsWith('+') && !line.startsWith('+++')) + || (line.startsWith('-') && !line.startsWith('---')) + || line.startsWith('@@'); + if (changed) return true; + shownContext += 1; + return shownContext <= contextLines; + }); + } + + function renderDiff(diff, context = '3') { + return visibleDiffLines(diff, context).map((line) => { const kind = line.startsWith('+') && !line.startsWith('+++') ? 'add' : line.startsWith('-') && !line.startsWith('---') @@ -3570,8 +3625,12 @@ export function renderApp(): string { return JSON.stringify(value, null, 2); } + function formatReceiptJson(value, format = selectedReceiptFormat) { + return format === 'compact' ? JSON.stringify(value) : pretty(value); + } + function renderReceiptJson(value) { - return '
' + pretty(value).split('\\n').map((line, index) => (
+        return '
' + formatReceiptJson(value).split('\\n').map((line, index) => (
           '' + String(index + 1) + '' + escapeHtml(line) + ''
         )).join('') + '
'; } @@ -3628,7 +3687,7 @@ export function renderApp(): string { } function downloadJson(filename, value) { - const blob = new Blob([pretty(value)], { type: 'application/json' }); + const blob = new Blob([formatReceiptJson(value)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; @@ -3647,6 +3706,7 @@ export function renderApp(): string { function selectedReceiptPayload(record = selectedReceiptRecord) { if (!record || !currentRun) return null; + if (selectedReceiptView === 'trace') return traceReceiptPayload(currentRun); return { trace_id: traceIdForRun(currentRun), run_id: currentRun.run_id, @@ -3782,8 +3842,21 @@ export function renderApp(): string { if (downloadReceiptButton) downloadReceiptButton.disabled = !hasRecord; } + function selectedReceiptJsonPayload() { + if (!selectedReceiptRecord || !currentRun) return null; + return selectedReceiptView === 'trace' + ? traceReceiptPayload(currentRun) + : selectedReceiptRecord; + } + + function rerenderSelectedReceiptJson() { + const payload = selectedReceiptJsonPayload(); + if (payload) receiptsEl.innerHTML = renderReceiptJson(payload); + } + function clearReceiptInspector() { selectedReceiptRecord = null; + selectedReceiptView = 'record'; receiptsEl.innerHTML = '

View signed record and proof after the first trace record is selected.

'; receiptSummaryEl.innerHTML = '

Summary appears after a signed record is selected.

'; renderVerificationActions(null, null); @@ -3862,7 +3935,7 @@ export function renderApp(): string { Target \${payload.target_file ?? 'missing'}
-
+
Diff (unified) @@ -3871,7 +3944,7 @@ export function renderApp(): string {
-
\${renderDiff(diff)}
+
\${renderDiff(diff, '3')}
Risk assessment
@@ -3908,7 +3981,15 @@ export function renderApp(): string { code?.classList.toggle('wrap', !pressed); }); document.querySelector('#diffContext')?.addEventListener('change', (event) => { - document.querySelector('.diff')?.setAttribute('data-context-lines', event.currentTarget.value); + const context = event.currentTarget.value; + const diffRoot = document.querySelector('.diff'); + const code = document.querySelector('.diff-code'); + const wrap = document.querySelector('#diffWrapToggle')?.getAttribute('aria-pressed') === 'true'; + diffRoot?.setAttribute('data-context-lines', context); + if (code) { + code.innerHTML = renderDiff(diff, context); + code.classList.toggle('wrap', wrap); + } }); document.querySelector('#approve')?.addEventListener('click', async () => { await transition({ @@ -4037,8 +4118,8 @@ export function renderApp(): string { const isPendingHuman = false; return \`
- \${auditReady ? \` + \${showReviewResult ? \`
- Execution result - \${answer.executed ? answer.outcome : 'not run'} + \${changesRequested ? 'Review result' : 'Execution result'} + \${changesRequested ? 'changes requested' : answer.executed ? answer.outcome : 'not run'}
- Changed rows - \${answer.changed.length ? answer.changed.join(', ') : 'none'} + \${changesRequested ? 'Next step' : 'Changed rows'} + \${changesRequested ? 'agent revision' : answer.changed.length ? answer.changed.join(', ') : 'none'}
\` : ''} @@ -4225,7 +4260,7 @@ export function renderApp(): string { selectRecord(record); }); }); - const preferredLabel = run.status === 'pending_approval' ? 'proposal' : run.status === 'rejected' ? 'rejection' : run.status === 'failed' ? 'outcome' : 'handoff'; + const preferredLabel = run.status === 'pending_approval' ? 'proposal' : run.status === 'changes_requested' ? 'change_request' : run.status === 'rejected' ? 'rejection' : run.status === 'failed' ? 'outcome' : 'handoff'; const preferredButton = timelineEl.querySelector(\`.event[data-label="\${preferredLabel}"]\`) ?? timelineEl.querySelector('.event'); if (preferredButton) { timelineEl.querySelectorAll('.event').forEach((item) => item.classList.remove('selected')); diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/approval-trace.worker.test.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/approval-trace.worker.test.ts index eda8d8dc..ea2c532a 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/approval-trace.worker.test.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/approval-trace.worker.test.ts @@ -21,6 +21,7 @@ type WorkflowStatus = | 'pending_approval' | 'approved' | 'rejected' + | 'changes_requested' | 'executing' | 'succeeded' | 'failed' @@ -43,9 +44,9 @@ interface TraceResponse { context_id: string trace_packet: { answer: { - decision: 'approved' | 'rejected' | null + decision: 'approved' | 'rejected' | 'changes_requested' | null executed: boolean - outcome: 'not_run' | 'success' | 'error' | 'pending' + outcome: 'not_run' | 'revision_requested' | 'success' | 'error' | 'pending' changed: string[] diagnostic: string | null } @@ -165,6 +166,12 @@ async function rejectRun(runId: string): Promise { }) } +async function requestChanges(runId: string): Promise { + return postJson(`/api/runs/${runId}/request-changes`, { + feedback: 'The reviewer requested a smaller repository file update.', + }) +} + async function getAgentRun(runId: string): Promise { const stub = testEnv.ApprovalTraceAgent.get(testEnv.ApprovalTraceAgent.idFromName(runId)) return runInDurableObject(stub, (instance) => (instance as unknown as RunReader).getRun(runId)) @@ -337,6 +344,34 @@ describe('Cloudflare approval trace Worker', () => { expect(await getTargetRows(runId, 'server/middleware/rate_limit.ts')).toEqual([]) }) + it('signs requested changes without rejecting or executing the action MCP path', async () => { + const runId = 'changes-requested-local-e2e' + await createRun(runId) + const trace = await requestChanges(runId) + const records = byLabel(trace) + const proposal = records.get('proposal')! + const feedback = records.get('change_request')! + + expect(trace.status).toBe('changes_requested') + expect(labels(trace)).toEqual(['trigger', 'triage', 'proposal', 'change_request']) + await expectSignedTrace(trace) + expect(sorted(feedback.record.informed_by)).toEqual([proposal.record_hash]) + expect(trace.records.some((record) => record.label === 'rejection')).toBe(false) + expect(trace.records.some((record) => record.signer === 'action_mcp')).toBe(false) + expect(feedback.body).toMatchObject({ + kind: 'human_review_feedback', + decision: 'changes_requested', + next_step: 'agent_revision', + }) + expect(trace.trace_packet.answer).toMatchObject({ + decision: 'changes_requested', + executed: false, + outcome: 'revision_requested', + changed: [], + }) + expect(await getTargetRows(runId, 'server/middleware/rate_limit.ts')).toEqual([]) + }) + it('records a diagnostic outcome when the approved action fails', async () => { const runId = 'error-local-e2e' await createRun(runId) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index 202fa8bf..70d5aa94 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -254,6 +254,27 @@ test.describe('Cloudflare approval trace browser UI', () => { }) }) + test('requests changes without signing a rejection or running MCP', async ({ page }) => { + await expectCleanConsole(page, async () => { + await createProposal(page) + await page.getByRole('button', { name: 'Request changes' }).click() + + await expect(page.locator('#statusTitle')).toHaveText('Changes requested') + await expect(page.locator('[data-step="halt"]')).toContainText('Needs revision') + await expect(page.locator('#answer')).toContainText('Revision requested') + await expect(page.locator('#answer')).toContainText('agent revision') + await expect(page.locator('#timeline .event')).toHaveCount(4) + await expect(page.locator('#timeline')).toContainText('human.change_request.signed') + await expect(page.locator('#timeline')).not.toContainText('human.rejection.signed') + await expect(page.locator('#timeline')).not.toContainText('action_mcp') + + await openTimelineRecord(page, 'change_request') + await expect(page.locator('#receipts pre')).toContainText('"kind": "human_review_feedback"') + await expect(page.locator('#receipts pre')).toContainText('"decision": "changes_requested"') + await expect(page.locator('#receipts pre')).toContainText('"next_step": "agent_revision"') + }) + }) + test('clicks through diagnostic error and opens the outcome receipt', async ({ page }) => { await expectCleanConsole(page, async () => { await createProposal(page, '/?simulate_error=1') From eab54e9b7601ca231bcfc1633fe0e4774e3b9ec2 Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 08:02:04 -0500 Subject: [PATCH 08/64] fix: wire Cloudflare run mode controls --- .../approval-trace/src/ui.ts | 122 ++++++++++++++++-- .../test/browser/approval-trace.ui.spec.ts | 61 +++++++++ 2 files changed, 169 insertions(+), 14 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index c27ed08e..8e56e4ec 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -1043,10 +1043,29 @@ export function renderApp(): string { width: 16px; } + .run-mode-wrap { + display: inline-flex; + position: relative; + white-space: nowrap; + } + .meta-pill { gap: 8px; } + button.meta-pill { + cursor: pointer; + font: inherit; + } + + button.meta-pill:hover, + button.meta-pill:focus-visible, + button.meta-pill[aria-expanded="true"] { + border-color: #b8c8f6; + color: var(--blue); + outline: 0; + } + .meta-pill.live-run::after { border-bottom: 1.5px solid currentColor; border-right: 1.5px solid currentColor; @@ -1107,12 +1126,31 @@ export function renderApp(): string { z-index: 100; } + .run-mode-menu { + background: #fff; + border: 1px solid var(--line); + border-radius: 8px; + box-shadow: 0 14px 32px rgba(18, 27, 42, 0.16); + display: grid; + left: 0; + min-width: 178px; + padding: 5px; + position: absolute; + top: 40px; + z-index: 100; + } + .header-actions-menu[hidden] { display: none; } + .run-mode-menu[hidden] { + display: none; + } + .header-actions-menu button, - .header-actions-menu a { + .header-actions-menu a, + .run-mode-menu button { align-items: center; background: transparent; border-radius: 6px; @@ -1130,11 +1168,17 @@ export function renderApp(): string { .header-actions-menu button:hover, .header-actions-menu button:focus-visible, .header-actions-menu a:hover, - .header-actions-menu a:focus-visible { + .header-actions-menu a:focus-visible, + .run-mode-menu button:hover, + .run-mode-menu button:focus-visible { background: #f4f7fb; outline: 0; } + .run-mode-menu button[aria-checked="true"] { + color: var(--green); + } + .meta-pill, .meta-code { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); @@ -1901,10 +1945,11 @@ export function renderApp(): string { align-items: center; display: grid; gap: 10px; - grid-template-columns: 22px auto; + grid-template-columns: 22px minmax(0, 1fr); justify-content: center; min-height: 58px; padding: 10px 12px; + text-align: center; } .actions { @@ -1916,11 +1961,14 @@ export function renderApp(): string { } .action-copy { + box-sizing: border-box; display: grid; gap: 2px; + justify-items: center; min-width: 0; - justify-items: start; - text-align: left; + padding: 0; + text-align: center; + width: 100%; } .action-copy small { @@ -1937,14 +1985,14 @@ export function renderApp(): string { .button-label { display: block; - font-size: 13px; + font-size: 12px; font-weight: 850; line-height: 1.12; white-space: nowrap; } .primary .button-label { - font-size: 13px; + font-size: 12px; } .primary .action-copy small { @@ -1952,7 +2000,7 @@ export function renderApp(): string { letter-spacing: 0; overflow: visible; text-overflow: clip; - white-space: nowrap; + white-space: normal; } .primary { @@ -1974,7 +2022,7 @@ export function renderApp(): string { display: inline-flex; height: 22px; justify-content: center; - justify-self: start; + justify-self: end; line-height: 0; margin-left: 0; width: 22px; @@ -2914,7 +2962,14 @@ export function renderApp(): string {
- Live run + + + + Run ID pending Region IAD Started waiting @@ -3074,6 +3129,8 @@ export function renderApp(): string { const receiptFormatSelect = document.querySelector('#receiptFormat'); const headerMenuButton = document.querySelector('#headerMenu'); const headerActionsMenu = document.querySelector('#headerActions'); + const runModeButton = document.querySelector('#runModeMenu'); + const runModeActionsMenu = document.querySelector('#runModeActions'); const bootStages = [ { @@ -3453,7 +3510,7 @@ export function renderApp(): string { if (approve) { approve.disabled = busy || !hasPendingApproval; const label = approve.querySelector('.button-label'); - if (label) label.textContent = busy && activeLabel === 'approve' ? 'Resuming agent...' : 'Approve and resume'; + if (label) label.textContent = busy && activeLabel === 'approve' ? 'Resuming agent...' : 'Approve & resume'; } if (reject) { reject.disabled = busy || !hasPendingApproval; @@ -3805,9 +3862,15 @@ export function renderApp(): string { headerActionsMenu.hidden = !open; } + function setRunModeMenuOpen(open) { + if (!runModeButton || !runModeActionsMenu) return; + runModeButton.setAttribute('aria-expanded', String(open)); + runModeActionsMenu.hidden = !open; + } + function updateHeaderMenuControls() { if (!headerActionsMenu) return; - headerActionsMenu.querySelectorAll('[data-header-action="open-json"], [data-header-action="reset"]').forEach((button) => { + document.querySelectorAll('[data-header-action="open-json"], [data-header-action="reset"], [data-run-mode-action="open-json"], [data-run-mode-action="reset"]').forEach((button) => { button.disabled = !currentRun || busy; }); } @@ -3991,7 +4054,7 @@ export function renderApp(): string { Approval signs the exact payload hash, connector id, and target file before execution resumes.
- +
@@ -4318,10 +4381,33 @@ export function renderApp(): string { const target = event.target; const menuButton = target.closest?.('#headerMenu'); if (menuButton) { + setRunModeMenuOpen(false); setHeaderMenuOpen(menuButton.getAttribute('aria-expanded') !== 'true'); return; } + const runModeTrigger = target.closest?.('#runModeMenu'); + if (runModeTrigger) { + setHeaderMenuOpen(false); + setRunModeMenuOpen(runModeTrigger.getAttribute('aria-expanded') !== 'true'); + return; + } + + const runModeAction = target.closest?.('[data-run-mode-action]'); + if (runModeAction && !runModeAction.disabled) { + const action = runModeAction.dataset.runModeAction; + setRunModeMenuOpen(false); + if (action === 'open-json' && currentRun) { + window.open('/api/runs/' + currentRun.run_id, '_blank', 'noreferrer'); + return; + } + if (action === 'reset') { + resetButton.click(); + return; + } + return; + } + const menuAction = target.closest?.('[data-header-action]'); if (menuAction && !menuAction.disabled) { const action = menuAction.dataset.headerAction; @@ -4344,6 +4430,10 @@ export function renderApp(): string { setHeaderMenuOpen(false); } + if (runModeActionsMenu && !target.closest?.('#runModeActions')) { + setRunModeMenuOpen(false); + } + const copyButton = target.closest?.('[data-copy-value], [data-copy-source], #copyReceipt'); if (copyButton && !copyButton.disabled) { const source = copyButton.dataset.copySource @@ -4418,11 +4508,15 @@ export function renderApp(): string { resetButton.addEventListener('click', () => { if (busy) return; setHeaderMenuOpen(false); + setRunModeMenuOpen(false); startTriggeredRun(); }); document.addEventListener('keydown', (event) => { - if (event.key === 'Escape') setHeaderMenuOpen(false); + if (event.key === 'Escape') { + setHeaderMenuOpen(false); + setRunModeMenuOpen(false); + } }); updateControls(); diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index 70d5aa94..5f90470c 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -97,12 +97,63 @@ async function expectHeaderMenuAboveContent(page: Page): Promise { expect(menuHit).toBe(true) } +async function expectRunModeMenuAboveContent(page: Page): Promise { + const menuHit = await page.evaluate(`(() => { + const menu = document.querySelector('#runModeActions') + if (!menu || menu.hidden) return false + const rect = menu.getBoundingClientRect() + const x = Math.floor(rect.left + rect.width / 2) + const y = Math.floor(rect.top + Math.min(rect.height - 2, 18)) + return Boolean(document.elementFromPoint(x, y)?.closest('#runModeActions')) + })()`) + expect(menuHit).toBe(true) +} + +async function expectActionButtonsCentered(page: Page): Promise { + const buttonGeometry = await page.evaluate< + Array<{ + groupDelta: number + iconCenterYDelta: number + labelFits: boolean + noLabelIconCollision: boolean + smallFits: boolean + textAlign: string + }> + >(`Array.from(document.querySelectorAll('.actions > button')).map((button) => { + const buttonRect = button.getBoundingClientRect() + const icon = button.querySelector('.button-icon')?.getBoundingClientRect() + const copy = button.querySelector('.action-copy')?.getBoundingClientRect() + const label = button.querySelector('.button-label')?.getBoundingClientRect() + const small = button.querySelector('.action-copy small')?.getBoundingClientRect() + const center = buttonRect.left + buttonRect.width / 2 + const groupLeft = Math.min(icon?.left ?? buttonRect.left, copy?.left ?? buttonRect.left) + const groupRight = Math.max(icon?.right ?? buttonRect.right, copy?.right ?? buttonRect.right) + return { + groupDelta: Math.abs((groupLeft + groupRight) / 2 - center), + iconCenterYDelta: icon ? Math.abs((icon.top + icon.height / 2) - (buttonRect.top + buttonRect.height / 2)) : 999, + labelFits: label ? label.left >= buttonRect.left && label.right <= buttonRect.right : false, + noLabelIconCollision: icon && label ? icon.right + 2 <= label.left : false, + smallFits: small ? small.left >= buttonRect.left && small.right <= buttonRect.right : false, + textAlign: copy ? getComputedStyle(button.querySelector('.action-copy')).textAlign : '', + } + })`) + for (const geometry of buttonGeometry) { + expect(geometry.textAlign).toBe('center') + expect(geometry.groupDelta).toBeLessThanOrEqual(8) + expect(geometry.iconCenterYDelta).toBeLessThanOrEqual(1) + expect(geometry.labelFits).toBe(true) + expect(geometry.noLabelIconCollision).toBe(true) + expect(geometry.smallFits).toBe(true) + } +} + test.describe('Cloudflare approval trace browser UI', () => { test('clicks through approved execution and opens the signed receipt', async ({ page }) => { await expectCleanConsole(page, async () => { await page.context().grantPermissions(['clipboard-write']) await createProposal(page) await expectNoHorizontalOverflow(page) + await expectActionButtonsCentered(page) const visibleTimes = await page.locator('#answer .progress-time').allTextContents() const populatedTimes = visibleTimes.filter((time) => time !== '-') @@ -141,6 +192,15 @@ test.describe('Cloudflare approval trace browser UI', () => { await page.keyboard.press('Escape') await expect(page.locator('#headerActions')).toBeHidden() + await page.locator('#runModeMenu').click() + await expect(page.locator('#runModeActions')).toBeVisible() + await expect(page.locator('[data-run-mode-action="live"]')).toHaveAttribute('aria-checked', 'true') + await expect(page.locator('[data-run-mode-action="open-json"]')).toBeEnabled() + await expect(page.locator('[data-run-mode-action="reset"]')).toBeEnabled() + await expectRunModeMenuAboveContent(page) + await page.keyboard.press('Escape') + await expect(page.locator('#runModeActions')).toBeHidden() + await page.getByRole('tab', { name: 'Record details' }).click() await expect(page.locator('#receiptSummary')).toContainText('Record hash') await expect(page.locator('#receiptSummary')).toContainText('Timestamp') @@ -210,6 +270,7 @@ test.describe('Cloudflare approval trace browser UI', () => { }) await expect(page.locator('[data-step="halt"]')).toContainText('Awaiting review') await expectNoHorizontalOverflow(page) + await expectActionButtonsCentered(page) const firstRunId = await page.locator('#runIdLabel').textContent() await page.locator('#headerMenu').click() From f0fc286fbf99869d61996323c6bd4093ffd7b70f Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 08:40:58 -0500 Subject: [PATCH 09/64] fix: polish Cloudflare trace timeline --- .../approval-trace/src/ui.ts | 40 ++++++++++++++++++- .../test/browser/approval-trace.ui.spec.ts | 39 +++++++++++++++++- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index 8e56e4ec..fb753fc3 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -765,7 +765,7 @@ export function renderApp(): string { .progress-item, .event { - animation: itemIn 360ms ease both; + transition: background-color 160ms ease, color 160ms ease; } .progress-item { @@ -1364,9 +1364,12 @@ export function renderApp(): string { background: #fff; display: grid; gap: 3px; + justify-self: start; + max-width: 100%; min-width: 0; padding: 0 4px; position: relative; + width: max-content; z-index: 1; } @@ -2087,6 +2090,10 @@ export function renderApp(): string { padding: 4px 0; } + .event { + grid-template-columns: 20px 54px minmax(0, 1fr) minmax(82px, 112px) 14px; + } + .event:hover, .event:focus-visible, .event.selected { @@ -2208,6 +2215,26 @@ export function renderApp(): string { word-break: normal; } + .event-cue { + align-self: center; + color: #6b778c; + display: inline-flex; + height: 14px; + justify-content: center; + opacity: 0.85; + width: 14px; + } + + .event-cue::before { + border-right: 1.5px solid currentColor; + border-top: 1.5px solid currentColor; + content: ""; + height: 6px; + margin-top: 3px; + transform: rotate(45deg); + width: 6px; + } + .trace-section-label { color: var(--ink); display: block; @@ -2871,6 +2898,10 @@ export function renderApp(): string { grid-template-columns: 18px 48px minmax(0, 1fr) minmax(66px, 92px); } + .event { + grid-template-columns: 18px 48px minmax(0, 1fr) minmax(58px, 82px) 12px; + } + .record-timeline::before { left: 8px; } @@ -2946,6 +2977,10 @@ export function renderApp(): string { grid-column: 3; justify-self: start; } + + .event-cue { + display: none; + } } @@ -4223,6 +4258,7 @@ export function renderApp(): string { \${timelineDetail(entry, run)} \${recordDisplayId(entry.record_hash)} + \`; }).join('')} @@ -4327,7 +4363,7 @@ export function renderApp(): string { const preferredButton = timelineEl.querySelector(\`.event[data-label="\${preferredLabel}"]\`) ?? timelineEl.querySelector('.event'); if (preferredButton) { timelineEl.querySelectorAll('.event').forEach((item) => item.classList.remove('selected')); - preferredButton.classList.add('selected'); + if (run.status !== 'pending_approval') preferredButton.classList.add('selected'); const preferredRecord = run.records.find((item) => item.record_hash === preferredButton.dataset.hash); if (preferredRecord) selectRecord(preferredRecord, { showTrace: true }); } diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index 5f90470c..562ab713 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -38,6 +38,10 @@ async function createProposal(page: Page, path = '/'): Promise { .map((child) => String(child.className))`) expect(timelineColumns[0]).toContain('event-marker') expect(timelineColumns[1]).toContain('event-time') + await expect(page.locator('#timeline .event .event-cue')).toHaveCount(3) + await expect(page.locator('#timeline .event-future .event-cue')).toHaveCount(0) + await expect(page.locator('#timeline .event.selected')).toHaveCount(0) + await expect(page.locator('#timeline .event-future.selected')).toContainText('human.review.halted') await expect(page.locator('#answer')).toContainText('Human review halted') await expect(page.locator('#answer')).toContainText('Execution is stopped') } @@ -147,6 +151,35 @@ async function expectActionButtonsCentered(page: Page): Promise { } } +async function expectWorkflowStepCopyHugsContent(page: Page): Promise { + const stepGeometry = await page.evaluate< + Array<{ copyWidth: number; rowWidth: number; step: string | null }> + >(`Array.from(document.querySelectorAll('.step')).map((step) => { + const row = step.getBoundingClientRect() + const copy = step.querySelector('.step-copy')?.getBoundingClientRect() + return { + copyWidth: copy ? copy.width : row.width, + rowWidth: row.width, + step: step.getAttribute('data-step'), + } + })`) + for (const geometry of stepGeometry) { + expect(geometry.copyWidth).toBeLessThanOrEqual(geometry.rowWidth - 40) + } +} + +async function expectTraceRowsReadable(page: Page): Promise { + const rowOpacity = await page.evaluate>( + `Array.from(document.querySelectorAll('.progress-item, #timeline .event, #timeline .event-future')).map((row) => ({ + opacity: Number(getComputedStyle(row).opacity), + selector: row.className, + }))`, + ) + for (const row of rowOpacity) { + expect(row.opacity).toBeGreaterThanOrEqual(0.98) + } +} + test.describe('Cloudflare approval trace browser UI', () => { test('clicks through approved execution and opens the signed receipt', async ({ page }) => { await expectCleanConsole(page, async () => { @@ -154,6 +187,8 @@ test.describe('Cloudflare approval trace browser UI', () => { await createProposal(page) await expectNoHorizontalOverflow(page) await expectActionButtonsCentered(page) + await expectWorkflowStepCopyHugsContent(page) + await expectTraceRowsReadable(page) const visibleTimes = await page.locator('#answer .progress-time').allTextContents() const populatedTimes = visibleTimes.filter((time) => time !== '-') @@ -245,7 +280,7 @@ test.describe('Cloudflare approval trace browser UI', () => { await openTimelineRecord(page, 'execution') await expect(page.locator('#receipts pre')).toContainText('"signer": "action_mcp"') await expect(page.locator('#receipts pre')).toContainText('"tool_name": "write_file"') - await expect(page.locator('#receipts pre')).toContainText('"proof": null') + await expect(page.locator('#receipts pre')).toContainText('"proof":') await expectCopies(page.getByRole('button', { name: 'Copy Action MCP signature' })) await expect(page.locator('#verification').getByRole('link', { name: 'View proof' })).toHaveAttribute( 'href', @@ -271,6 +306,8 @@ test.describe('Cloudflare approval trace browser UI', () => { await expect(page.locator('[data-step="halt"]')).toContainText('Awaiting review') await expectNoHorizontalOverflow(page) await expectActionButtonsCentered(page) + await expectWorkflowStepCopyHugsContent(page) + await expectTraceRowsReadable(page) const firstRunId = await page.locator('#runIdLabel').textContent() await page.locator('#headerMenu').click() From ed212dd3e974e7e9781a7d00bc952531d9b20c83 Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 09:18:26 -0500 Subject: [PATCH 10/64] fix: align Cloudflare trace action controls --- .../approval-trace/src/ui.ts | 39 ++++++++++++--- .../test/browser/approval-trace.ui.spec.ts | 49 ++++++++++++++++++- 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index fb753fc3..2ac60c9c 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -1946,18 +1946,17 @@ export function renderApp(): string { .secondary, .danger { align-items: center; - display: grid; - gap: 10px; - grid-template-columns: 22px minmax(0, 1fr); + display: flex; + gap: 9px; justify-content: center; min-height: 58px; - padding: 10px 12px; + padding: 10px 13px; text-align: center; } .actions { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-columns: minmax(0, 1.08fr) repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 6px; min-width: 0; @@ -1968,16 +1967,19 @@ export function renderApp(): string { display: grid; gap: 2px; justify-items: center; + max-width: calc(100% - 31px); min-width: 0; padding: 0; text-align: center; - width: 100%; + width: auto; } .action-copy small { color: inherit; + display: block; font-size: 9px; font-weight: 650; + justify-self: center; line-height: 1.15; opacity: 0.78; overflow: visible; @@ -1990,6 +1992,7 @@ export function renderApp(): string { display: block; font-size: 12px; font-weight: 850; + justify-self: center; line-height: 1.12; white-space: nowrap; } @@ -1999,7 +2002,7 @@ export function renderApp(): string { } .primary .action-copy small { - font-size: 8px; + font-size: 9px; letter-spacing: 0; overflow: visible; text-overflow: clip; @@ -2023,9 +2026,9 @@ export function renderApp(): string { align-items: center; border-radius: 999px; display: inline-flex; + flex: 0 0 22px; height: 22px; justify-content: center; - justify-self: end; line-height: 0; margin-left: 0; width: 22px; @@ -2053,6 +2056,26 @@ export function renderApp(): string { color: #334155; } + @media (min-width: 1451px) { + .primary, + .secondary, + .danger { + gap: 8px; + padding-left: 8px; + padding-right: 8px; + } + + .action-copy { + max-width: calc(100% - 30px); + width: max-content; + } + + .action-copy small, + .primary .action-copy small { + white-space: nowrap; + } + } + .primary .action-copy, .danger .action-copy, .secondary .action-copy { diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index 562ab713..944ac4ed 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -116,8 +116,12 @@ async function expectRunModeMenuAboveContent(page: Page): Promise { async function expectActionButtonsCentered(page: Page): Promise { const buttonGeometry = await page.evaluate< Array<{ + captionFontSize: number + copyCenterDelta: number groupDelta: number iconCenterYDelta: number + labelCenterDelta: number + labelFontSize: number labelFits: boolean noLabelIconCollision: boolean smallFits: boolean @@ -129,12 +133,21 @@ async function expectActionButtonsCentered(page: Page): Promise { const copy = button.querySelector('.action-copy')?.getBoundingClientRect() const label = button.querySelector('.button-label')?.getBoundingClientRect() const small = button.querySelector('.action-copy small')?.getBoundingClientRect() + const labelElement = button.querySelector('.button-label') + const smallElement = button.querySelector('.action-copy small') + const labelStyle = labelElement ? getComputedStyle(labelElement) : null + const smallStyle = smallElement ? getComputedStyle(smallElement) : null const center = buttonRect.left + buttonRect.width / 2 const groupLeft = Math.min(icon?.left ?? buttonRect.left, copy?.left ?? buttonRect.left) const groupRight = Math.max(icon?.right ?? buttonRect.right, copy?.right ?? buttonRect.right) + const copyCenter = copy ? copy.left + copy.width / 2 : center return { + captionFontSize: smallStyle ? Number.parseFloat(smallStyle.fontSize) : 0, + copyCenterDelta: copy && small ? Math.abs((small.left + small.width / 2) - copyCenter) : 999, groupDelta: Math.abs((groupLeft + groupRight) / 2 - center), iconCenterYDelta: icon ? Math.abs((icon.top + icon.height / 2) - (buttonRect.top + buttonRect.height / 2)) : 999, + labelCenterDelta: copy && label ? Math.abs((label.left + label.width / 2) - copyCenter) : 999, + labelFontSize: labelStyle ? Number.parseFloat(labelStyle.fontSize) : 0, labelFits: label ? label.left >= buttonRect.left && label.right <= buttonRect.right : false, noLabelIconCollision: icon && label ? icon.right + 2 <= label.left : false, smallFits: small ? small.left >= buttonRect.left && small.right <= buttonRect.right : false, @@ -143,14 +156,43 @@ async function expectActionButtonsCentered(page: Page): Promise { })`) for (const geometry of buttonGeometry) { expect(geometry.textAlign).toBe('center') - expect(geometry.groupDelta).toBeLessThanOrEqual(8) + expect(geometry.groupDelta).toBeLessThanOrEqual(4) expect(geometry.iconCenterYDelta).toBeLessThanOrEqual(1) + expect(geometry.labelCenterDelta).toBeLessThanOrEqual(2) + expect(geometry.copyCenterDelta).toBeLessThanOrEqual(2) + expect(geometry.labelFontSize).toBeGreaterThanOrEqual(12) + expect(geometry.captionFontSize).toBeGreaterThanOrEqual(9) expect(geometry.labelFits).toBe(true) expect(geometry.noLabelIconCollision).toBe(true) expect(geometry.smallFits).toBe(true) } } +async function expectReferenceDesktopPrimaryCaption(page: Page): Promise { + const captionGeometry = await page.evaluate<{ + fits: boolean + height: number + lineHeight: number + whiteSpace: string + }>(`(() => { + const button = document.querySelector('#approve') + const caption = button?.querySelector('.action-copy small') + if (!button || !caption) return { fits: false, height: 999, lineHeight: 0, whiteSpace: '' } + const buttonRect = button.getBoundingClientRect() + const captionRect = caption.getBoundingClientRect() + const style = getComputedStyle(caption) + return { + fits: captionRect.left >= buttonRect.left && captionRect.right <= buttonRect.right, + height: captionRect.height, + lineHeight: Number.parseFloat(style.lineHeight), + whiteSpace: style.whiteSpace, + } + })()`) + expect(captionGeometry.whiteSpace).toBe('nowrap') + expect(captionGeometry.fits).toBe(true) + expect(captionGeometry.height).toBeLessThanOrEqual(captionGeometry.lineHeight + 1) +} + async function expectWorkflowStepCopyHugsContent(page: Page): Promise { const stepGeometry = await page.evaluate< Array<{ copyWidth: number; rowWidth: number; step: string | null }> @@ -183,10 +225,12 @@ async function expectTraceRowsReadable(page: Page): Promise { test.describe('Cloudflare approval trace browser UI', () => { test('clicks through approved execution and opens the signed receipt', async ({ page }) => { await expectCleanConsole(page, async () => { + await page.setViewportSize({ width: 1536, height: 1024 }) await page.context().grantPermissions(['clipboard-write']) await createProposal(page) await expectNoHorizontalOverflow(page) await expectActionButtonsCentered(page) + await expectReferenceDesktopPrimaryCaption(page) await expectWorkflowStepCopyHugsContent(page) await expectTraceRowsReadable(page) @@ -227,7 +271,10 @@ test.describe('Cloudflare approval trace browser UI', () => { await page.keyboard.press('Escape') await expect(page.locator('#headerActions')).toBeHidden() + await expect(page.locator('#runModeMenu')).toHaveAttribute('aria-haspopup', 'menu') + await expect(page.locator('#runModeMenu')).toHaveAttribute('aria-expanded', 'false') await page.locator('#runModeMenu').click() + await expect(page.locator('#runModeMenu')).toHaveAttribute('aria-expanded', 'true') await expect(page.locator('#runModeActions')).toBeVisible() await expect(page.locator('[data-run-mode-action="live"]')).toHaveAttribute('aria-checked', 'true') await expect(page.locator('[data-run-mode-action="open-json"]')).toBeEnabled() From 821005e2201fb8f738b2bc9d6c66ba3177835b33 Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 09:36:42 -0500 Subject: [PATCH 11/64] fix: align Cloudflare trace rail geometry --- .../approval-trace/src/ui.ts | 14 +++++-- .../test/browser/approval-trace.ui.spec.ts | 39 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index 2ac60c9c..7bc97427 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -1270,7 +1270,7 @@ export function renderApp(): string { border-color: #f3a64e; border-radius: 8px; color: var(--ink); - min-height: 64px; + min-height: 58px; padding: 6px 10px; width: auto; } @@ -1455,9 +1455,17 @@ export function renderApp(): string { color: #a44900; } - @media (min-width: 1250px) { + @media (min-width: 1451px) { + .workflow-rail { + padding: 0 9px 0 51px; + } + .rail-stepper { - grid-template-columns: minmax(170px, 223px) minmax(220px, 263px) minmax(280px, 332px) minmax(250px, 1fr) minmax(160px, 200px); + grid-template-columns: 272px 244px 370px minmax(250px, 1fr) 200px; + } + + .step.halted { + width: 331px; } } diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index 944ac4ed..e89d8b1b 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -210,6 +210,44 @@ async function expectWorkflowStepCopyHugsContent(page: Page): Promise { } } +async function expectReferenceDesktopRailGeometry(page: Page): Promise { + const railGeometry = await page.evaluate< + Array<{ + indexX: number | null + rectH: number + rectW: number + rectX: number + step: string | null + }> + >(`Array.from(document.querySelectorAll('.step')).map((step) => { + const rect = step.getBoundingClientRect() + const index = step.querySelector('.step-index')?.getBoundingClientRect() + return { + indexX: index ? Math.round(index.x) : null, + rectH: Math.round(rect.height), + rectW: Math.round(rect.width), + rectX: Math.round(rect.x), + step: step.getAttribute('data-step'), + } + })`) + const byStep = Object.fromEntries( + railGeometry.map((geometry) => [geometry.step, geometry]), + ) + expect(byStep.trigger.indexX).toBeGreaterThanOrEqual(51) + expect(byStep.trigger.indexX).toBeLessThanOrEqual(53) + expect(byStep.autonomous.indexX).toBeGreaterThanOrEqual(339) + expect(byStep.autonomous.indexX).toBeLessThanOrEqual(341) + expect(byStep.halt.rectX).toBeGreaterThanOrEqual(598) + expect(byStep.halt.rectX).toBeLessThanOrEqual(600) + expect(byStep.halt.rectW).toBeGreaterThanOrEqual(330) + expect(byStep.halt.rectW).toBeLessThanOrEqual(332) + expect(byStep.halt.rectH).toBe(58) + expect(byStep.resume.indexX).toBeGreaterThanOrEqual(985) + expect(byStep.resume.indexX).toBeLessThanOrEqual(987) + expect(byStep.audit.indexX).toBeGreaterThanOrEqual(1326) + expect(byStep.audit.indexX).toBeLessThanOrEqual(1328) +} + async function expectTraceRowsReadable(page: Page): Promise { const rowOpacity = await page.evaluate>( `Array.from(document.querySelectorAll('.progress-item, #timeline .event, #timeline .event-future')).map((row) => ({ @@ -232,6 +270,7 @@ test.describe('Cloudflare approval trace browser UI', () => { await expectActionButtonsCentered(page) await expectReferenceDesktopPrimaryCaption(page) await expectWorkflowStepCopyHugsContent(page) + await expectReferenceDesktopRailGeometry(page) await expectTraceRowsReadable(page) const visibleTimes = await page.locator('#answer .progress-time').allTextContents() From 996077f521250a460ef26d37049e609107e13e3c Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 09:50:14 -0500 Subject: [PATCH 12/64] fix: tune Cloudflare trace review badge --- .../approval-trace/src/ui.ts | 12 +++++-- .../test/browser/approval-trace.ui.spec.ts | 36 ++++++++++++++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index 7bc97427..e122e804 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -1399,7 +1399,7 @@ export function renderApp(): string { .step[data-step="halt"] .step-copy .step-meta-line { align-items: center; display: flex; - gap: 10px; + gap: 16px; line-height: 1; margin-top: 0; min-width: 0; @@ -1419,11 +1419,19 @@ export function renderApp(): string { font-weight: 850; line-height: 1; margin-left: 0; - padding: 3px 6px; + padding: 3px 4px; text-transform: uppercase; vertical-align: 1px; } + .step[data-step="halt"] .step-copy .step-badge { + color: #a44900; + font-size: 10px; + font-weight: 850; + line-height: 1; + margin-top: 0; + } + .step-badge[hidden] { display: none; } diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index e89d8b1b..cf349450 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -211,15 +211,34 @@ async function expectWorkflowStepCopyHugsContent(page: Page): Promise { } async function expectReferenceDesktopRailGeometry(page: Page): Promise { - const railGeometry = await page.evaluate< - Array<{ + const railGeometry = await page.evaluate<{ + badge: { + color: string + fontSize: number + fontWeight: number + height: number + width: number + } | null + steps: Array<{ indexX: number | null rectH: number rectW: number rectX: number step: string | null }> - >(`Array.from(document.querySelectorAll('.step')).map((step) => { + }>(`(() => { + const badge = document.querySelector('[data-step-badge="halt"]') + const badgeRect = badge?.getBoundingClientRect() + const badgeStyle = badge ? getComputedStyle(badge) : null + return { + badge: badge && badgeRect && badgeStyle ? { + color: badgeStyle.color, + fontSize: Number.parseFloat(badgeStyle.fontSize), + fontWeight: Number.parseFloat(badgeStyle.fontWeight), + height: Math.round(badgeRect.height), + width: Math.round(badgeRect.width), + } : null, + steps: Array.from(document.querySelectorAll('.step')).map((step) => { const rect = step.getBoundingClientRect() const index = step.querySelector('.step-index')?.getBoundingClientRect() return { @@ -229,9 +248,11 @@ async function expectReferenceDesktopRailGeometry(page: Page): Promise { rectX: Math.round(rect.x), step: step.getAttribute('data-step'), } - })`) + }), + } + })()`) const byStep = Object.fromEntries( - railGeometry.map((geometry) => [geometry.step, geometry]), + railGeometry.steps.map((geometry) => [geometry.step, geometry]), ) expect(byStep.trigger.indexX).toBeGreaterThanOrEqual(51) expect(byStep.trigger.indexX).toBeLessThanOrEqual(53) @@ -246,6 +267,11 @@ async function expectReferenceDesktopRailGeometry(page: Page): Promise { expect(byStep.resume.indexX).toBeLessThanOrEqual(987) expect(byStep.audit.indexX).toBeGreaterThanOrEqual(1326) expect(byStep.audit.indexX).toBeLessThanOrEqual(1328) + expect(railGeometry.badge?.fontSize).toBe(10) + expect(railGeometry.badge?.fontWeight).toBeGreaterThanOrEqual(800) + expect(railGeometry.badge?.height).toBeLessThanOrEqual(18) + expect(railGeometry.badge?.width).toBeLessThanOrEqual(112) + expect(railGeometry.badge?.color).toBe('rgb(164, 73, 0)') } async function expectTraceRowsReadable(page: Page): Promise { From b4eb1afdf5de38e6827d55723a5597ee8ad8d0f7 Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 10:27:29 -0500 Subject: [PATCH 13/64] fix: center Cloudflare trace action controls --- .../approval-trace/src/index.ts | 2 +- .../approval-trace/src/ui.ts | 37 ++++++++++++------- .../test/browser/approval-trace.ui.spec.ts | 33 +++++++++++------ 3 files changed, 46 insertions(+), 26 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/index.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/index.ts index 97d02873..bb28dfd3 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/index.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/index.ts @@ -385,7 +385,7 @@ function fixturePlan(prompt: string): PlannedAction { action: 'Update file in repository', summary: 'Respond to a GitHub issue webhook by preparing a small repository file update that adds request limiting to the reported route.', - risk: 'Introduces rate limiting that changes production request handling.', + risk: 'Introduces rate limiting which may impact client traffic if misconfigured.', payload: { operation: 'write_file', issue_id: 'workers-issue-4821', diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index e122e804..1f973a28 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -1881,7 +1881,7 @@ export function renderApp(): string { .risk-bar { align-items: center; - background: #fff8ed; + background: #fff; border: 1px solid #ffd09a; border-radius: 8px; display: grid; @@ -1963,10 +1963,11 @@ export function renderApp(): string { .danger { align-items: center; display: flex; - gap: 9px; + gap: 0; justify-content: center; min-height: 58px; - padding: 10px 13px; + padding: 10px; + position: relative; text-align: center; } @@ -1983,11 +1984,11 @@ export function renderApp(): string { display: grid; gap: 2px; justify-items: center; - max-width: calc(100% - 31px); + max-width: 100%; min-width: 0; padding: 0; text-align: center; - width: auto; + width: 100%; } .action-copy small { @@ -2042,12 +2043,15 @@ export function renderApp(): string { align-items: center; border-radius: 999px; display: inline-flex; - flex: 0 0 22px; - height: 22px; + flex: 0 0 18px; + height: 18px; justify-content: center; + left: 7px; line-height: 0; margin-left: 0; - width: 22px; + position: absolute; + top: 13px; + width: 18px; } .button-icon svg { @@ -2076,14 +2080,14 @@ export function renderApp(): string { .primary, .secondary, .danger { - gap: 8px; - padding-left: 8px; - padding-right: 8px; + gap: 0; + padding-left: 10px; + padding-right: 10px; } .action-copy { - max-width: calc(100% - 30px); - width: max-content; + max-width: 100%; + width: 100%; } .action-copy small, @@ -2928,7 +2932,12 @@ export function renderApp(): string { .actions { gap: 10px; - grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-columns: minmax(0, 1.12fr) repeat(2, minmax(0, 1fr)); + } + + .action-copy small, + .primary .action-copy small { + white-space: nowrap; } .event, diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index cf349450..d4364e6e 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -117,9 +117,10 @@ async function expectActionButtonsCentered(page: Page): Promise { const buttonGeometry = await page.evaluate< Array<{ captionFontSize: number + captionCenterDelta: number copyCenterDelta: number - groupDelta: number - iconCenterYDelta: number + iconInsideButton: boolean + iconLabelYDelta: number labelCenterDelta: number labelFontSize: number labelFits: boolean @@ -138,15 +139,18 @@ async function expectActionButtonsCentered(page: Page): Promise { const labelStyle = labelElement ? getComputedStyle(labelElement) : null const smallStyle = smallElement ? getComputedStyle(smallElement) : null const center = buttonRect.left + buttonRect.width / 2 - const groupLeft = Math.min(icon?.left ?? buttonRect.left, copy?.left ?? buttonRect.left) - const groupRight = Math.max(icon?.right ?? buttonRect.right, copy?.right ?? buttonRect.right) const copyCenter = copy ? copy.left + copy.width / 2 : center return { captionFontSize: smallStyle ? Number.parseFloat(smallStyle.fontSize) : 0, - copyCenterDelta: copy && small ? Math.abs((small.left + small.width / 2) - copyCenter) : 999, - groupDelta: Math.abs((groupLeft + groupRight) / 2 - center), - iconCenterYDelta: icon ? Math.abs((icon.top + icon.height / 2) - (buttonRect.top + buttonRect.height / 2)) : 999, - labelCenterDelta: copy && label ? Math.abs((label.left + label.width / 2) - copyCenter) : 999, + captionCenterDelta: small ? Math.abs((small.left + small.width / 2) - center) : 999, + copyCenterDelta: copy ? Math.abs(copyCenter - center) : 999, + iconInsideButton: icon + ? icon.left >= buttonRect.left && icon.right <= buttonRect.right && icon.top >= buttonRect.top && icon.bottom <= buttonRect.bottom + : false, + iconLabelYDelta: icon && label + ? Math.abs((icon.top + icon.height / 2) - (label.top + label.height / 2)) + : 999, + labelCenterDelta: label ? Math.abs((label.left + label.width / 2) - center) : 999, labelFontSize: labelStyle ? Number.parseFloat(labelStyle.fontSize) : 0, labelFits: label ? label.left >= buttonRect.left && label.right <= buttonRect.right : false, noLabelIconCollision: icon && label ? icon.right + 2 <= label.left : false, @@ -156,10 +160,11 @@ async function expectActionButtonsCentered(page: Page): Promise { })`) for (const geometry of buttonGeometry) { expect(geometry.textAlign).toBe('center') - expect(geometry.groupDelta).toBeLessThanOrEqual(4) - expect(geometry.iconCenterYDelta).toBeLessThanOrEqual(1) + expect(geometry.copyCenterDelta).toBeLessThanOrEqual(1) expect(geometry.labelCenterDelta).toBeLessThanOrEqual(2) - expect(geometry.copyCenterDelta).toBeLessThanOrEqual(2) + expect(geometry.captionCenterDelta).toBeLessThanOrEqual(2) + expect(geometry.iconInsideButton).toBe(true) + expect(geometry.iconLabelYDelta).toBeLessThanOrEqual(1.5) expect(geometry.labelFontSize).toBeGreaterThanOrEqual(12) expect(geometry.captionFontSize).toBeGreaterThanOrEqual(9) expect(geometry.labelFits).toBe(true) @@ -295,6 +300,12 @@ test.describe('Cloudflare approval trace browser UI', () => { await expectNoHorizontalOverflow(page) await expectActionButtonsCentered(page) await expectReferenceDesktopPrimaryCaption(page) + await expect(page.locator('.risk-bar .value')).toHaveText( + 'Introduces rate limiting which may impact client traffic if misconfigured.', + ) + await expect( + page.locator('.risk-bar').evaluate((element) => getComputedStyle(element).backgroundColor), + ).resolves.toBe('rgb(255, 255, 255)') await expectWorkflowStepCopyHugsContent(page) await expectReferenceDesktopRailGeometry(page) await expectTraceRowsReadable(page) From 8614190591dd5fe3681c8f66e04f5d6f9289439a Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 11:00:55 -0500 Subject: [PATCH 14/64] fix: align Cloudflare trace diff stack --- .../approval-trace/src/index.ts | 4 +++ .../approval-trace/src/ui.ts | 23 +++++++----- .../test/browser/approval-trace.ui.spec.ts | 36 +++++++++++++++++++ 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/index.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/index.ts index bb28dfd3..81bcfa67 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/index.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/index.ts @@ -363,6 +363,10 @@ function fixturePlan(prompt: string): PlannedAction { import { NextFunction, Request, Response } from 'express'; import { getConfig } from '../config'; + import { logRequest } from '../observability/logging'; + import { reportMetrics } from '../observability/metrics'; + import { resolveTenant } from '../tenant'; + +import rateLimit from 'express-rate-limit'; + +const limiter = rateLimit({ diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index 1f973a28..aaf886b6 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -1831,8 +1831,8 @@ export function renderApp(): string { } .diff pre { - font-size: 11.3px; - line-height: 1.38; + font-size: 10px; + line-height: 1.2; max-height: 313px; } @@ -1840,12 +1840,14 @@ export function renderApp(): string { background: #fbfdff; border: 1px solid var(--line); border-radius: 8px; + box-sizing: border-box; color: #102033; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - font-size: 11.3px; - line-height: 1.38; + font-size: 10px; + line-height: 1.2; max-height: 313px; max-width: 100%; + height: 298px; overflow: auto; padding: 8px 0; width: 100%; @@ -1853,7 +1855,7 @@ export function renderApp(): string { .diff-line { display: block; - min-height: 15.5px; + min-height: 12px; min-width: 0; padding: 0 12px; white-space: pre; @@ -3732,14 +3734,17 @@ export function renderApp(): string { if (context === 'all') return lines; const contextLines = Number.parseInt(context, 10); if (!Number.isFinite(contextLines)) return lines; - let shownContext = 0; + let shownContextAfterChange = 0; return lines.filter((line) => { const changed = (line.startsWith('+') && !line.startsWith('+++')) || (line.startsWith('-') && !line.startsWith('---')) || line.startsWith('@@'); - if (changed) return true; - shownContext += 1; - return shownContext <= contextLines; + if (changed) { + shownContextAfterChange = 0; + return true; + } + shownContextAfterChange += 1; + return shownContextAfterChange <= contextLines; }); } diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index d4364e6e..7bee5c88 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -198,6 +198,37 @@ async function expectReferenceDesktopPrimaryCaption(page: Page): Promise { expect(captionGeometry.height).toBeLessThanOrEqual(captionGeometry.lineHeight + 1) } +async function expectReferenceDesktopCenterStack(page: Page): Promise { + const stackGeometry = await page.evaluate<{ + actionBottomGap: number + actionsY: number + diffCodeHeight: number + panelBottom: number + riskBarHeight: number + }>(`(() => { + const panel = document.querySelector('#proposal')?.closest('.panel')?.getBoundingClientRect() + const diffCode = document.querySelector('.diff-code')?.getBoundingClientRect() + const riskBar = document.querySelector('.risk-bar')?.getBoundingClientRect() + const actions = document.querySelector('.actions')?.getBoundingClientRect() + if (!panel || !diffCode || !riskBar || !actions) { + return { actionBottomGap: 999, actionsY: 0, diffCodeHeight: 0, panelBottom: 0, riskBarHeight: 0 } + } + return { + actionBottomGap: Math.round(panel.bottom - actions.bottom), + actionsY: Math.round(actions.y), + diffCodeHeight: Math.round(diffCode.height), + panelBottom: Math.round(panel.bottom), + riskBarHeight: Math.round(riskBar.height), + } + })()`) + expect(stackGeometry.diffCodeHeight).toBeGreaterThanOrEqual(298) + expect(stackGeometry.actionBottomGap).toBeGreaterThanOrEqual(12) + expect(stackGeometry.actionBottomGap).toBeLessThanOrEqual(18) + expect(stackGeometry.actionsY).toBeGreaterThanOrEqual(690) + expect(stackGeometry.actionsY).toBeLessThanOrEqual(698) + expect(stackGeometry.riskBarHeight).toBeGreaterThanOrEqual(38) +} + async function expectWorkflowStepCopyHugsContent(page: Page): Promise { const stepGeometry = await page.evaluate< Array<{ copyWidth: number; rowWidth: number; step: string | null }> @@ -300,6 +331,7 @@ test.describe('Cloudflare approval trace browser UI', () => { await expectNoHorizontalOverflow(page) await expectActionButtonsCentered(page) await expectReferenceDesktopPrimaryCaption(page) + await expectReferenceDesktopCenterStack(page) await expect(page.locator('.risk-bar .value')).toHaveText( 'Introduces rate limiting which may impact client traffic if misconfigured.', ) @@ -328,6 +360,9 @@ test.describe('Cloudflare approval trace browser UI', () => { await page.locator('#riskDetailsToggle').click() await expect(page.locator('#riskDetails')).toBeVisible() await expect(page.locator('#riskDetails')).toContainText('Human review gate') + await expect(page.locator('.diff-code')).toContainText('const config = getConfig();') + await expect(page.locator('.diff-code')).toContainText('next();') + await expect(page.locator('.diff-code')).not.toContainText('logRequest') await page.locator('#diffWrapToggle').click() await expect(page.locator('#diffWrapToggle')).toHaveAttribute('aria-pressed', 'true') @@ -337,6 +372,7 @@ test.describe('Cloudflare approval trace browser UI', () => { await expect.poll(async () => page.locator('.diff-line').count()).toBeGreaterThan(threeLineDiffCount) await page.locator('#diffContext').selectOption('6') await expect(page.locator('.diff')).toHaveAttribute('data-context-lines', '6') + await expect(page.locator('.diff-code')).toContainText('logRequest') await page.locator('#headerMenu').click() await expect(page.locator('#headerActions')).toBeVisible() From 9d1551ca783518e958284bdaff33fe18d08baf2f Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 11:35:41 -0500 Subject: [PATCH 15/64] fix: align Cloudflare trace controls --- .../approval-trace/src/ui.ts | 53 ++++++++++--------- .../test/browser/approval-trace.ui.spec.ts | 30 ++++++----- 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index aaf886b6..c98e3c1e 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -1066,14 +1066,16 @@ export function renderApp(): string { outline: 0; } - .meta-pill.live-run::after { - border-bottom: 1.5px solid currentColor; - border-right: 1.5px solid currentColor; - content: ""; - height: 5px; - margin-left: 2px; - transform: translateY(-2px) rotate(45deg); - width: 5px; + .meta-pill.live-run .menu-chevron { + color: #475569; + height: 12px; + margin-left: -2px; + transition: transform 160ms ease; + width: 12px; + } + + .meta-pill.live-run[aria-expanded="true"] .menu-chevron { + transform: rotate(180deg); } .region-status-dot { @@ -1123,7 +1125,7 @@ export function renderApp(): string { position: absolute; right: 0; top: 42px; - z-index: 100; + z-index: 1000; } .run-mode-menu { @@ -1137,7 +1139,7 @@ export function renderApp(): string { padding: 5px; position: absolute; top: 40px; - z-index: 100; + z-index: 1000; } .header-actions-menu[hidden] { @@ -1965,17 +1967,17 @@ export function renderApp(): string { .danger { align-items: center; display: flex; - gap: 0; + gap: 8px; justify-content: center; min-height: 58px; padding: 10px; position: relative; - text-align: center; + text-align: left; } .actions { display: grid; - grid-template-columns: minmax(0, 1.08fr) repeat(2, minmax(0, 1fr)); + grid-template-columns: minmax(190px, 1.08fr) repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 6px; min-width: 0; @@ -1985,12 +1987,12 @@ export function renderApp(): string { box-sizing: border-box; display: grid; gap: 2px; - justify-items: center; + justify-items: start; max-width: 100%; min-width: 0; padding: 0; - text-align: center; - width: 100%; + text-align: left; + width: auto; } .action-copy small { @@ -1998,7 +2000,7 @@ export function renderApp(): string { display: block; font-size: 9px; font-weight: 650; - justify-self: center; + justify-self: start; line-height: 1.15; opacity: 0.78; overflow: visible; @@ -2011,7 +2013,7 @@ export function renderApp(): string { display: block; font-size: 12px; font-weight: 850; - justify-self: center; + justify-self: start; line-height: 1.12; white-space: nowrap; } @@ -2045,14 +2047,14 @@ export function renderApp(): string { align-items: center; border-radius: 999px; display: inline-flex; + align-self: flex-start; flex: 0 0 18px; height: 18px; justify-content: center; - left: 7px; line-height: 0; margin-left: 0; - position: absolute; - top: 13px; + margin-top: 3px; + position: static; width: 18px; } @@ -2082,14 +2084,14 @@ export function renderApp(): string { .primary, .secondary, .danger { - gap: 0; + gap: 8px; padding-left: 10px; padding-right: 10px; } .action-copy { max-width: 100%; - width: 100%; + width: auto; } .action-copy small, @@ -2934,11 +2936,12 @@ export function renderApp(): string { .actions { gap: 10px; - grid-template-columns: minmax(0, 1.12fr) repeat(2, minmax(0, 1fr)); + grid-template-columns: minmax(190px, 1.12fr) repeat(2, minmax(0, 1fr)); } .action-copy small, .primary .action-copy small { + font-size: 8px; white-space: nowrap; } @@ -3048,7 +3051,7 @@ export function renderApp(): string {
- +
- - - + + +
\`; document.querySelector('#riskDetailsToggle')?.addEventListener('click', (event) => { diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index 7b422529..c47a84cf 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -118,10 +118,12 @@ async function expectActionButtonsCentered(page: Page): Promise { Array<{ buttonDisplay: string captionFontSize: number - groupCenterDelta: number + contentCenterDelta: number + contentDisplay: string + contentInsideButton: boolean iconCopyGap: number iconInsideButton: boolean - iconLabelYDelta: number + iconCopyYDelta: number labelFontSize: number labelFits: boolean noLabelIconCollision: boolean @@ -130,6 +132,7 @@ async function expectActionButtonsCentered(page: Page): Promise { }> >(`Array.from(document.querySelectorAll('.actions > button')).map((button) => { const buttonRect = button.getBoundingClientRect() + const content = button.querySelector('.button-content')?.getBoundingClientRect() const icon = button.querySelector('.button-icon')?.getBoundingClientRect() const copy = button.querySelector('.action-copy')?.getBoundingClientRect() const label = button.querySelector('.button-label')?.getBoundingClientRect() @@ -137,21 +140,24 @@ async function expectActionButtonsCentered(page: Page): Promise { const labelElement = button.querySelector('.button-label') const smallElement = button.querySelector('.action-copy small') const buttonStyle = getComputedStyle(button) + const contentElement = button.querySelector('.button-content') const labelStyle = labelElement ? getComputedStyle(labelElement) : null const smallStyle = smallElement ? getComputedStyle(smallElement) : null const center = buttonRect.left + buttonRect.width / 2 - const groupLeft = icon && copy ? Math.min(icon.left, copy.left) : buttonRect.left - const groupRight = icon && copy ? Math.max(icon.right, copy.right) : buttonRect.right return { buttonDisplay: buttonStyle.display, captionFontSize: smallStyle ? Number.parseFloat(smallStyle.fontSize) : 0, - groupCenterDelta: Math.abs((groupLeft + groupRight) / 2 - center), + contentCenterDelta: content ? Math.abs((content.left + content.width / 2) - center) : 999, + contentDisplay: contentElement ? getComputedStyle(contentElement).display : '', + contentInsideButton: content + ? content.left >= buttonRect.left && content.right <= buttonRect.right && content.top >= buttonRect.top && content.bottom <= buttonRect.bottom + : false, iconCopyGap: icon && copy ? copy.left - icon.right : 0, iconInsideButton: icon ? icon.left >= buttonRect.left && icon.right <= buttonRect.right && icon.top >= buttonRect.top && icon.bottom <= buttonRect.bottom : false, - iconLabelYDelta: icon && label - ? Math.abs((icon.top + icon.height / 2) - (label.top + label.height / 2)) + iconCopyYDelta: icon && copy + ? Math.abs((icon.top + icon.height / 2) - (copy.top + copy.height / 2)) : 999, labelFontSize: labelStyle ? Number.parseFloat(labelStyle.fontSize) : 0, labelFits: label ? label.left >= buttonRect.left && label.right <= buttonRect.right : false, @@ -162,12 +168,14 @@ async function expectActionButtonsCentered(page: Page): Promise { })`) for (const geometry of buttonGeometry) { expect(geometry.buttonDisplay).toBe('flex') + expect(geometry.contentDisplay).toBe('flex') expect(geometry.textAlign).toBe('left') - expect(geometry.groupCenterDelta).toBeLessThanOrEqual(2) + expect(geometry.contentCenterDelta).toBeLessThanOrEqual(1.5) + expect(geometry.contentInsideButton).toBe(true) expect(geometry.iconCopyGap).toBeGreaterThanOrEqual(8) expect(geometry.iconCopyGap).toBeLessThanOrEqual(10) expect(geometry.iconInsideButton).toBe(true) - expect(geometry.iconLabelYDelta).toBeLessThanOrEqual(3) + expect(geometry.iconCopyYDelta).toBeLessThanOrEqual(1.5) expect(geometry.labelFontSize).toBeGreaterThanOrEqual(12) expect(geometry.captionFontSize).toBeGreaterThanOrEqual(8) expect(geometry.labelFits).toBe(true) @@ -176,6 +184,38 @@ async function expectActionButtonsCentered(page: Page): Promise { } } +async function expectDiffLineGutter(page: Page): Promise { + const gutter = await page.evaluate<{ + allRowsNumbered: boolean + firstLine: string + gutterWidth: number + lineCount: number + numberCount: number + textAfterGutter: boolean + }>(`(() => { + const code = document.querySelector('.diff-code')?.getBoundingClientRect() + const rows = Array.from(document.querySelectorAll('.diff-line')) + const numbers = Array.from(document.querySelectorAll('.diff-line-no')) + const firstNumber = numbers[0]?.getBoundingClientRect() + const firstText = document.querySelector('.diff-line-text')?.getBoundingClientRect() + return { + allRowsNumbered: rows.every((row, index) => row.querySelector('.diff-line-no')?.textContent === String(index + 1)), + firstLine: numbers[0]?.textContent ?? '', + gutterWidth: firstNumber ? Math.round(firstNumber.width) : 0, + lineCount: rows.length, + numberCount: numbers.length, + textAfterGutter: Boolean(code && firstNumber && firstText && firstNumber.left >= code.left && firstText.left > firstNumber.right), + } + })()`) + expect(gutter.lineCount).toBeGreaterThan(1) + expect(gutter.numberCount).toBe(gutter.lineCount) + expect(gutter.firstLine).toBe('1') + expect(gutter.allRowsNumbered).toBe(true) + expect(gutter.gutterWidth).toBeGreaterThanOrEqual(20) + expect(gutter.gutterWidth).toBeLessThanOrEqual(24) + expect(gutter.textAfterGutter).toBe(true) +} + async function expectReferenceDesktopPrimaryCaption(page: Page): Promise { const captionGeometry = await page.evaluate<{ fits: boolean @@ -408,6 +448,7 @@ test.describe('Cloudflare approval trace browser UI', () => { await expect(page.locator('.diff-code')).toContainText('const config = getConfig();') await expect(page.locator('.diff-code')).toContainText('next();') await expect(page.locator('.diff-code')).not.toContainText('logRequest') + await expectDiffLineGutter(page) await page.locator('#diffWrapToggle').click() await expect(page.locator('#diffWrapToggle')).toHaveAttribute('aria-pressed', 'true') @@ -418,6 +459,7 @@ test.describe('Cloudflare approval trace browser UI', () => { await page.locator('#diffContext').selectOption('6') await expect(page.locator('.diff')).toHaveAttribute('data-context-lines', '6') await expect(page.locator('.diff-code')).toContainText('logRequest') + await expectDiffLineGutter(page) await page.locator('#headerMenu').click() await expect(page.locator('#headerActions')).toBeVisible() @@ -438,6 +480,11 @@ test.describe('Cloudflare approval trace browser UI', () => { await expect(page.locator('[data-run-mode-action="open-json"]')).toBeEnabled() await expect(page.locator('[data-run-mode-action="reset"]')).toBeEnabled() await expectRunModeMenuAboveContent(page) + await page.locator('[data-run-mode-action="live"]').click() + await expect(page.locator('#runModeMenu')).toHaveAttribute('aria-expanded', 'false') + await expect(page.locator('#runModeActions')).toBeHidden() + await page.locator('#runModeMenu').click() + await expect(page.locator('#runModeActions')).toBeVisible() await page.keyboard.press('Escape') await expect(page.locator('#runModeActions')).toBeHidden() @@ -511,6 +558,7 @@ test.describe('Cloudflare approval trace browser UI', () => { await expect(page.locator('[data-step="halt"]')).toContainText('Awaiting review') await expectNoHorizontalOverflow(page) await expectActionButtonsCentered(page) + await expectDiffLineGutter(page) await expectWorkflowStepCopyHugsContent(page) await expectConstrainedDesktopRailGeometry(page) await expectTraceRowsReadable(page) From 63331a002e53301218990841f1afa53bba1fa416 Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 12:40:50 -0500 Subject: [PATCH 18/64] fix: tune Cloudflare trace visual rhythm --- .../approval-trace/src/ui.ts | 19 ++++--- .../test/browser/approval-trace.ui.spec.ts | 54 +++++++++++++++++++ 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index e4fab62d..d9861e00 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -1245,8 +1245,15 @@ export function renderApp(): string { } .step:not(:last-child)::after { - border-top: 2px solid #c5cfdb; + background-image: repeating-linear-gradient( + to right, + #c5cfdb 0, + #c5cfdb 4px, + transparent 4px, + transparent 7px + ); content: ""; + height: 2px; left: 54px; position: absolute; right: -16px; @@ -1256,7 +1263,7 @@ export function renderApp(): string { } .step.done:not(:last-child)::after { - border-color: var(--green); + background: var(--green); } .step.active, @@ -1278,8 +1285,6 @@ export function renderApp(): string { } .step.halted:not(:last-child)::after { - border-color: #c5cfdb; - border-top-style: dashed; left: calc(100% + 1px); right: -16px; width: auto; @@ -1846,7 +1851,7 @@ export function renderApp(): string { .diff pre { font-size: 10px; - line-height: 1.2; + line-height: 13.4px; max-height: 313px; } @@ -1858,7 +1863,7 @@ export function renderApp(): string { color: #102033; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 10px; - line-height: 1.2; + line-height: 13.4px; max-height: 313px; max-width: 100%; height: 298px; @@ -1872,7 +1877,7 @@ export function renderApp(): string { column-gap: 8px; display: grid; grid-template-columns: 22px minmax(0, 1fr); - min-height: 12px; + min-height: 13.4px; min-width: 0; padding: 0 12px; width: auto; diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index c47a84cf..f67e82d7 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -216,6 +216,34 @@ async function expectDiffLineGutter(page: Page): Promise { expect(gutter.textAfterGutter).toBe(true) } +async function expectDiffRowsFillReferenceFrame(page: Page): Promise { + const rhythm = await page.evaluate<{ + bottomGap: number + lineHeight: number + topGap: number + }>(`(() => { + const code = document.querySelector('.diff-code')?.getBoundingClientRect() + const rows = Array.from(document.querySelectorAll('.diff-line')) + const first = rows[0]?.getBoundingClientRect() + const last = rows[rows.length - 1]?.getBoundingClientRect() + const style = document.querySelector('.diff-code') + ? getComputedStyle(document.querySelector('.diff-code')) + : null + if (!code || !first || !last || !style) return { bottomGap: 999, lineHeight: 0, topGap: 999 } + return { + bottomGap: Math.round((code.bottom - last.bottom) * 100) / 100, + lineHeight: Number.parseFloat(style.lineHeight), + topGap: Math.round((first.top - code.top) * 100) / 100, + } + })()`) + expect(rhythm.lineHeight).toBeGreaterThanOrEqual(13) + expect(rhythm.lineHeight).toBeLessThanOrEqual(14) + expect(rhythm.topGap).toBeGreaterThanOrEqual(7) + expect(rhythm.topGap).toBeLessThanOrEqual(11) + expect(rhythm.bottomGap).toBeGreaterThanOrEqual(7) + expect(rhythm.bottomGap).toBeLessThanOrEqual(11) +} + async function expectReferenceDesktopPrimaryCaption(page: Page): Promise { const captionGeometry = await page.evaluate<{ fits: boolean @@ -298,6 +326,12 @@ async function expectReferenceDesktopRailGeometry(page: Page): Promise { height: number width: number } | null + connectors: Array<{ + backgroundColor: string + backgroundImage: string + height: number + step: string | null + }> steps: Array<{ indexX: number | null rectH: number @@ -317,6 +351,15 @@ async function expectReferenceDesktopRailGeometry(page: Page): Promise { height: Math.round(badgeRect.height), width: Math.round(badgeRect.width), } : null, + connectors: Array.from(document.querySelectorAll('.step:not(:last-child)')).map((step) => { + const after = getComputedStyle(step, '::after') + return { + backgroundColor: after.backgroundColor, + backgroundImage: after.backgroundImage, + height: Number.parseFloat(after.height), + step: step.getAttribute('data-step'), + } + }), steps: Array.from(document.querySelectorAll('.step')).map((step) => { const rect = step.getBoundingClientRect() const index = step.querySelector('.step-index')?.getBoundingClientRect() @@ -351,6 +394,15 @@ async function expectReferenceDesktopRailGeometry(page: Page): Promise { expect(railGeometry.badge?.height).toBeLessThanOrEqual(18) expect(railGeometry.badge?.width).toBeLessThanOrEqual(112) expect(railGeometry.badge?.color).toBe('rgb(164, 73, 0)') + const connectors = Object.fromEntries( + railGeometry.connectors.map((connector) => [connector.step, connector]), + ) + expect(connectors.trigger.backgroundColor).toBe('rgb(7, 136, 97)') + expect(connectors.autonomous.backgroundColor).toBe('rgb(7, 136, 97)') + expect(connectors.halt.backgroundImage).toContain('repeating-linear-gradient') + expect(connectors.resume.backgroundImage).toContain('repeating-linear-gradient') + expect(connectors.halt.height).toBe(2) + expect(connectors.resume.height).toBe(2) } async function expectConstrainedDesktopRailGeometry(page: Page): Promise { @@ -449,6 +501,7 @@ test.describe('Cloudflare approval trace browser UI', () => { await expect(page.locator('.diff-code')).toContainText('next();') await expect(page.locator('.diff-code')).not.toContainText('logRequest') await expectDiffLineGutter(page) + await expectDiffRowsFillReferenceFrame(page) await page.locator('#diffWrapToggle').click() await expect(page.locator('#diffWrapToggle')).toHaveAttribute('aria-pressed', 'true') @@ -559,6 +612,7 @@ test.describe('Cloudflare approval trace browser UI', () => { await expectNoHorizontalOverflow(page) await expectActionButtonsCentered(page) await expectDiffLineGutter(page) + await expectDiffRowsFillReferenceFrame(page) await expectWorkflowStepCopyHugsContent(page) await expectConstrainedDesktopRailGeometry(page) await expectTraceRowsReadable(page) From 301584037b98bdfc7bbcdf942904d2ba03c3b8a3 Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 12:53:59 -0500 Subject: [PATCH 19/64] fix: align Cloudflare trace button icons --- .../examples/cloudflare-agents/approval-trace/src/ui.ts | 1 + .../approval-trace/test/browser/approval-trace.ui.spec.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index d9861e00..ee0b9bcc 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -2094,6 +2094,7 @@ export function renderApp(): string { margin-left: 0; margin-top: 0; position: static; + transform: translateY(-5px); width: 18px; } diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index f67e82d7..632edcec 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -123,7 +123,7 @@ async function expectActionButtonsCentered(page: Page): Promise { contentInsideButton: boolean iconCopyGap: number iconInsideButton: boolean - iconCopyYDelta: number + iconLabelYDelta: number labelFontSize: number labelFits: boolean noLabelIconCollision: boolean @@ -156,8 +156,8 @@ async function expectActionButtonsCentered(page: Page): Promise { iconInsideButton: icon ? icon.left >= buttonRect.left && icon.right <= buttonRect.right && icon.top >= buttonRect.top && icon.bottom <= buttonRect.bottom : false, - iconCopyYDelta: icon && copy - ? Math.abs((icon.top + icon.height / 2) - (copy.top + copy.height / 2)) + iconLabelYDelta: icon && label + ? Math.abs((icon.top + icon.height / 2) - (label.top + label.height / 2)) : 999, labelFontSize: labelStyle ? Number.parseFloat(labelStyle.fontSize) : 0, labelFits: label ? label.left >= buttonRect.left && label.right <= buttonRect.right : false, @@ -175,7 +175,7 @@ async function expectActionButtonsCentered(page: Page): Promise { expect(geometry.iconCopyGap).toBeGreaterThanOrEqual(8) expect(geometry.iconCopyGap).toBeLessThanOrEqual(10) expect(geometry.iconInsideButton).toBe(true) - expect(geometry.iconCopyYDelta).toBeLessThanOrEqual(1.5) + expect(geometry.iconLabelYDelta).toBeLessThanOrEqual(1.5) expect(geometry.labelFontSize).toBeGreaterThanOrEqual(12) expect(geometry.captionFontSize).toBeGreaterThanOrEqual(8) expect(geometry.labelFits).toBe(true) From b43d49255b32e9c50cd5764d7738d81685e5300c Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 13:10:30 -0500 Subject: [PATCH 20/64] fix: center Cloudflare trace action icons --- .../examples/cloudflare-agents/approval-trace/src/ui.ts | 1 - .../approval-trace/test/browser/approval-trace.ui.spec.ts | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index ee0b9bcc..d9861e00 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -2094,7 +2094,6 @@ export function renderApp(): string { margin-left: 0; margin-top: 0; position: static; - transform: translateY(-5px); width: 18px; } diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index 632edcec..4c4ca2c4 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -123,6 +123,7 @@ async function expectActionButtonsCentered(page: Page): Promise { contentInsideButton: boolean iconCopyGap: number iconInsideButton: boolean + iconTextBlockYDelta: number iconLabelYDelta: number labelFontSize: number labelFits: boolean @@ -156,6 +157,9 @@ async function expectActionButtonsCentered(page: Page): Promise { iconInsideButton: icon ? icon.left >= buttonRect.left && icon.right <= buttonRect.right && icon.top >= buttonRect.top && icon.bottom <= buttonRect.bottom : false, + iconTextBlockYDelta: icon && copy + ? Math.abs((icon.top + icon.height / 2) - (copy.top + copy.height / 2)) + : 999, iconLabelYDelta: icon && label ? Math.abs((icon.top + icon.height / 2) - (label.top + label.height / 2)) : 999, @@ -175,7 +179,8 @@ async function expectActionButtonsCentered(page: Page): Promise { expect(geometry.iconCopyGap).toBeGreaterThanOrEqual(8) expect(geometry.iconCopyGap).toBeLessThanOrEqual(10) expect(geometry.iconInsideButton).toBe(true) - expect(geometry.iconLabelYDelta).toBeLessThanOrEqual(1.5) + expect(geometry.iconTextBlockYDelta).toBeLessThanOrEqual(1.5) + expect(geometry.iconLabelYDelta).toBeGreaterThan(4) expect(geometry.labelFontSize).toBeGreaterThanOrEqual(12) expect(geometry.captionFontSize).toBeGreaterThanOrEqual(8) expect(geometry.labelFits).toBe(true) From 3078108bf14f77885959465d9b865623ec1109af Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 13:25:00 -0500 Subject: [PATCH 21/64] fix: fit Cloudflare trace risk copy --- .../approval-trace/src/ui.ts | 9 +++++++- .../test/browser/approval-trace.ui.spec.ts | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index d9861e00..79b9ef21 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -1920,7 +1920,7 @@ export function renderApp(): string { border: 1px solid #ffd09a; border-radius: 8px; display: grid; - gap: 8px; + gap: 7px; grid-template-columns: auto auto minmax(0, 1fr) minmax(42px, auto); min-width: 0; padding: 8px 10px; @@ -1956,6 +1956,13 @@ export function renderApp(): string { white-space: nowrap; } + @media (min-width: 1451px) { + .risk-bar .value { + overflow: visible; + text-overflow: clip; + } + } + .risk-details-toggle { align-items: center; background: transparent; diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index 4c4ca2c4..3955f472 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -274,6 +274,28 @@ async function expectReferenceDesktopPrimaryCaption(page: Page): Promise { expect(captionGeometry.height).toBeLessThanOrEqual(captionGeometry.lineHeight + 1) } +async function expectReferenceDesktopRiskTextFits(page: Page): Promise { + const riskGeometry = await page.evaluate<{ + clientWidth: number + gap: number + scrollWidth: number + textOverflow: string + }>(`(() => { + const bar = document.querySelector('.risk-bar') + const value = bar?.querySelector('.value') + const style = value ? getComputedStyle(value) : null + return { + clientWidth: value?.clientWidth ?? 0, + gap: bar ? Number.parseFloat(getComputedStyle(bar).columnGap) : 0, + scrollWidth: value?.scrollWidth ?? 999, + textOverflow: style?.textOverflow ?? '', + } + })()`) + expect(riskGeometry.gap).toBeLessThanOrEqual(7) + expect(riskGeometry.textOverflow).toBe('clip') + expect(riskGeometry.scrollWidth).toBeLessThanOrEqual(riskGeometry.clientWidth + 1) +} + async function expectReferenceDesktopCenterStack(page: Page): Promise { const stackGeometry = await page.evaluate<{ actionBottomGap: number @@ -477,6 +499,7 @@ test.describe('Cloudflare approval trace browser UI', () => { await expect(page.locator('.risk-bar .value')).toHaveText( 'Introduces rate limiting which may impact client traffic if misconfigured.', ) + await expectReferenceDesktopRiskTextFits(page) await expect( page.locator('.risk-bar').evaluate((element) => getComputedStyle(element).backgroundColor), ).resolves.toBe('rgb(255, 255, 255)') From ba1a43845b65ac55d8a91db0e1eb8c895aa4e27c Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 14:11:37 -0500 Subject: [PATCH 22/64] fix: align Cloudflare trace dropdown and review buttons --- .../approval-trace/src/ui.ts | 60 +++++++++++++++-- .../test/browser/approval-trace.ui.spec.ts | 64 +++++++++++++++++++ 2 files changed, 118 insertions(+), 6 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index 79b9ef21..a94450f7 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -1167,6 +1167,25 @@ export function renderApp(): string { white-space: nowrap; } + .run-mode-menu button { + display: grid; + gap: 7px; + grid-template-columns: 12px minmax(0, 1fr); + } + + .run-mode-menu button::before { + color: transparent; + content: ""; + font-size: 11px; + font-weight: 800; + line-height: 1; + } + + .run-mode-menu button[aria-checked="true"]::before { + color: var(--green); + content: "✓"; + } + .header-actions-menu button:hover, .header-actions-menu button:focus-visible, .header-actions-menu a:hover, @@ -2045,7 +2064,7 @@ export function renderApp(): string { color: inherit; display: block; font-size: 9px; - font-weight: 650; + font-weight: 600; justify-self: start; line-height: 1.15; opacity: 0.78; @@ -2053,12 +2072,13 @@ export function renderApp(): string { overflow-wrap: anywhere; text-overflow: clip; white-space: normal; + width: 100%; } .button-label { display: block; font-size: 12px; - font-weight: 850; + font-weight: 800; justify-self: start; line-height: 1.12; white-space: nowrap; @@ -2070,6 +2090,7 @@ export function renderApp(): string { .primary .action-copy small { font-size: 9px; + font-weight: 500; letter-spacing: 0; overflow: visible; text-overflow: clip; @@ -2089,6 +2110,13 @@ export function renderApp(): string { background: #fff; } + .danger .action-copy small, + .secondary .action-copy small { + color: #475569; + font-weight: 500; + opacity: 1; + } + .button-icon { align-self: center; align-items: center; @@ -2136,7 +2164,7 @@ export function renderApp(): string { .action-copy { max-width: 100%; - width: auto; + width: max-content; } .action-copy small, @@ -2759,6 +2787,10 @@ export function renderApp(): string { gap: 6px; } + .verify-list { + gap: 5px; + } + .summary-row { display: grid; font-size: 12px; @@ -2775,16 +2807,32 @@ export function renderApp(): string { } .verify-row { - border-radius: 7px; + background: transparent; + border: 0; + border-radius: 0; grid-template-columns: 34px minmax(0, 1fr) minmax(72px, auto); - min-height: 40px; - padding: 5px 9px; + min-height: 42px; + padding: 6px 0; } .verify-row > div { min-width: 0; } + .verify-row strong { + display: block; + font-size: 12px; + font-weight: 700; + line-height: 1.25; + } + + .verify-row .empty { + color: var(--muted); + font-size: 11px; + line-height: 1.25; + margin-top: 2px; + } + .verify-icon { align-items: center; border: 1px solid; diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index 3955f472..454a76f0 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -118,6 +118,8 @@ async function expectActionButtonsCentered(page: Page): Promise { Array<{ buttonDisplay: string captionFontSize: number + captionFitsCopy: boolean + captionMuted: boolean contentCenterDelta: number contentDisplay: string contentInsideButton: boolean @@ -127,6 +129,7 @@ async function expectActionButtonsCentered(page: Page): Promise { iconLabelYDelta: number labelFontSize: number labelFits: boolean + labelWeight: number noLabelIconCollision: boolean smallFits: boolean textAlign: string @@ -148,6 +151,8 @@ async function expectActionButtonsCentered(page: Page): Promise { return { buttonDisplay: buttonStyle.display, captionFontSize: smallStyle ? Number.parseFloat(smallStyle.fontSize) : 0, + captionFitsCopy: small && copy ? small.left >= copy.left && small.right <= copy.right + 1 : false, + captionMuted: button.id === 'approve' || (smallStyle ? smallStyle.color === 'rgb(71, 85, 105)' : false), contentCenterDelta: content ? Math.abs((content.left + content.width / 2) - center) : 999, contentDisplay: contentElement ? getComputedStyle(contentElement).display : '', contentInsideButton: content @@ -165,6 +170,7 @@ async function expectActionButtonsCentered(page: Page): Promise { : 999, labelFontSize: labelStyle ? Number.parseFloat(labelStyle.fontSize) : 0, labelFits: label ? label.left >= buttonRect.left && label.right <= buttonRect.right : false, + labelWeight: labelStyle ? Number.parseFloat(labelStyle.fontWeight) : 0, noLabelIconCollision: icon && label ? icon.right + 2 <= label.left : false, smallFits: small ? small.left >= buttonRect.left && small.right <= buttonRect.right : false, textAlign: copy ? getComputedStyle(button.querySelector('.action-copy')).textAlign : '', @@ -182,7 +188,10 @@ async function expectActionButtonsCentered(page: Page): Promise { expect(geometry.iconTextBlockYDelta).toBeLessThanOrEqual(1.5) expect(geometry.iconLabelYDelta).toBeGreaterThan(4) expect(geometry.labelFontSize).toBeGreaterThanOrEqual(12) + expect(geometry.labelWeight).toBeGreaterThanOrEqual(800) + expect(geometry.captionFitsCopy).toBe(true) expect(geometry.captionFontSize).toBeGreaterThanOrEqual(8) + expect(geometry.captionMuted).toBe(true) expect(geometry.labelFits).toBe(true) expect(geometry.noLabelIconCollision).toBe(true) expect(geometry.smallFits).toBe(true) @@ -296,6 +305,55 @@ async function expectReferenceDesktopRiskTextFits(page: Page): Promise { expect(riskGeometry.scrollWidth).toBeLessThanOrEqual(riskGeometry.clientWidth + 1) } +async function expectReferenceVerificationRows(page: Page): Promise { + const rows = await page.evaluate< + Array<{ + borderWidth: string + detailFontSize: number + iconHeight: number + minHeight: number + paddingLeft: number + paddingRight: number + radius: string + rowGap: number + strongFontSize: number + strongWeight: number + }> + >(`Array.from(document.querySelectorAll('#verification .verify-row')).map((row) => { + const style = getComputedStyle(row) + const icon = row.querySelector('.verify-icon')?.getBoundingClientRect() + const strong = row.querySelector('strong') + const detail = row.querySelector('.empty') + const strongStyle = strong ? getComputedStyle(strong) : null + const detailStyle = detail ? getComputedStyle(detail) : null + return { + borderWidth: style.borderTopWidth, + detailFontSize: detailStyle ? Number.parseFloat(detailStyle.fontSize) : 0, + iconHeight: icon ? Math.round(icon.height) : 0, + minHeight: Math.round(row.getBoundingClientRect().height), + paddingLeft: Number.parseFloat(style.paddingLeft), + paddingRight: Number.parseFloat(style.paddingRight), + radius: style.borderTopLeftRadius, + rowGap: Number.parseFloat(style.columnGap), + strongFontSize: strongStyle ? Number.parseFloat(strongStyle.fontSize) : 0, + strongWeight: strongStyle ? Number.parseFloat(strongStyle.fontWeight) : 0, + } + })`) + expect(rows).toHaveLength(3) + for (const row of rows) { + expect(row.borderWidth).toBe('0px') + expect(row.radius).toBe('0px') + expect(row.paddingLeft).toBe(0) + expect(row.paddingRight).toBe(0) + expect(row.detailFontSize).toBe(11) + expect(row.minHeight).toBeGreaterThanOrEqual(42) + expect(row.iconHeight).toBe(28) + expect(row.rowGap).toBe(8) + expect(row.strongFontSize).toBe(12) + expect(row.strongWeight).toBe(700) + } +} + async function expectReferenceDesktopCenterStack(page: Page): Promise { const stackGeometry = await page.evaluate<{ actionBottomGap: number @@ -558,6 +616,11 @@ test.describe('Cloudflare approval trace browser UI', () => { await expect(page.locator('#runModeMenu')).toHaveAttribute('aria-expanded', 'true') await expect(page.locator('#runModeActions')).toBeVisible() await expect(page.locator('[data-run-mode-action="live"]')).toHaveAttribute('aria-checked', 'true') + await expect( + page.locator('[data-run-mode-action="live"]').evaluate((element) => + getComputedStyle(element, '::before').content, + ), + ).resolves.toBe('"✓"') await expect(page.locator('[data-run-mode-action="open-json"]')).toBeEnabled() await expect(page.locator('[data-run-mode-action="reset"]')).toBeEnabled() await expectRunModeMenuAboveContent(page) @@ -588,6 +651,7 @@ test.describe('Cloudflare approval trace browser UI', () => { 'href', /log\.atrib\.dev|\/api\/runs\//, ) + await expectReferenceVerificationRows(page) await page.locator('#verification').getByRole('button', { name: 'Verify' }).click() await expect(page.locator('#verificationResult')).toContainText('Record hash matches') await expect(page.locator('#verificationResult')).toContainText('Signature valid') From fad373805709bfae5e1ef3e15f0973c375940e7a Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 14:39:28 -0500 Subject: [PATCH 23/64] fix: tune Cloudflare trace left panel typography --- .../approval-trace/src/ui.ts | 44 +++++++++++++--- .../test/browser/approval-trace.ui.spec.ts | 51 +++++++++++++++++++ 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index a94450f7..f1ea723c 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -1593,6 +1593,11 @@ export function renderApp(): string { font-size: 14px; } + .trigger-card .detail-row strong { + font-size: 13px; + font-weight: 400; + } + .trigger-details { gap: 8px; margin-top: 2px; @@ -1663,6 +1668,14 @@ export function renderApp(): string { text-transform: uppercase; } + #answer > .section-label { + font-size: 14px; + font-weight: 700; + letter-spacing: 0; + margin-bottom: 7px; + text-transform: none; + } + textarea { min-height: 86px; } @@ -1762,8 +1775,14 @@ export function renderApp(): string { background: transparent; } + .progress-item strong { + font-size: 13px; + font-weight: 400; + } + .progress-item.halted strong { color: #9a4a00; + font-weight: 700; } .progress-item.proposal .dot.ok { @@ -3502,11 +3521,24 @@ export function renderApp(): string { return new Date(Date.now() + offsetMs).toISOString().slice(11, 19) + ' UTC'; } + function formatHeaderDate(value) { + const date = value ? new Date(value) : new Date(); + const month = date.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' }); + const day = date.getUTCDate(); + const year = date.getUTCFullYear(); + const time = date.toISOString().slice(11, 19); + return month + ' ' + day + ', ' + year + ' ' + time + ' UTC'; + } + function displayRecordTime(record, label = record?.label, fallbackIndex = 0) { const offset = recordDisplayOffsets[label] ?? fallbackIndex * 1000; return formatRecordTime(record, offset); } + function progressTimeLabel(value) { + return String(value ?? '-').replace(/ UTC$/, ''); + } + function progressKeyForTitle(title) { if (title === 'Trigger received') return 'trigger'; if (title === 'Context gathered') return 'context'; @@ -3520,10 +3552,10 @@ export function renderApp(): string { function progressDisplayTime(run, title, active) { const key = progressKeyForTitle(title); - if (key && stageDisplayTimes[key]) return stageDisplayTimes[key]; + if (key && stageDisplayTimes[key]) return progressTimeLabel(stageDisplayTimes[key]); if (!active) return '-'; const record = progressRecordFor(run, title); - return formatRecordTime(record, progressDisplayOffsets[key] ?? recordDisplayOffsets[record?.label ?? key] ?? 0) + ' UTC'; + return formatRecordTime(record, progressDisplayOffsets[key] ?? recordDisplayOffsets[record?.label ?? key] ?? 0); } function formatRecordTime(record, offsetMs = 0) { @@ -3656,7 +3688,7 @@ export function renderApp(): string { \${stage.title} \${stage.detail}
- \${done || active ? stageDisplayTimes[stage.key] : '-'} + \${done || active ? progressTimeLabel(stageDisplayTimes[stage.key]) : '-'} \`; }).join(''); @@ -4524,7 +4556,7 @@ export function renderApp(): string { traceIdLabel.textContent = traceIdForRun(run); updateTraceHeaderCopy(); const started = run.records[0]?.record?.timestamp - ? new Date(run.records[0].record.timestamp).toISOString().replace('T', ' ').slice(0, 19) + ' UTC' + ? formatHeaderDate(run.records[0].record.timestamp) : 'pending'; startedLabel.textContent = started; receivedLabel.textContent = started; @@ -4666,8 +4698,8 @@ export function renderApp(): string { runIdLabel.textContent = runId; traceIdLabel.textContent = traceIdFromRunId(runId); updateTraceHeaderCopy(); - startedLabel.textContent = nowTime(0); - receivedLabel.textContent = nowTime(0); + startedLabel.textContent = formatHeaderDate(); + receivedLabel.textContent = formatHeaderDate(); clearReceiptInspector(); renderBootProgress(0); const run = await post('/api/runs', { diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index 454a76f0..d1f05a32 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -44,6 +44,7 @@ async function createProposal(page: Page, path = '/'): Promise { await expect(page.locator('#timeline .event-future.selected')).toContainText('human.review.halted') await expect(page.locator('#answer')).toContainText('Human review halted') await expect(page.locator('#answer')).toContainText('Execution is stopped') + await expectReferenceLeftProgressTypography(page) } async function openTimelineRecord(page: Page, label: string): Promise { @@ -58,6 +59,56 @@ async function expectCopies(button: Locator): Promise { await expect(button).toHaveAttribute('data-copy-state', 'copied') } +async function expectReferenceLeftProgressTypography(page: Page): Promise { + const progress = await page.evaluate<{ + detailWeights: number[] + receivedText: string + rowWeights: Array<{ text: string; weight: number }> + sectionLabel: { + fontSize: number + fontWeight: number + text: string + textTransform: string + } | null + times: string[] + }>(`(() => { + const sectionLabel = document.querySelector('#answer > .section-label') + const sectionStyle = sectionLabel ? getComputedStyle(sectionLabel) : null + return { + detailWeights: Array.from(document.querySelectorAll('.trigger-card .detail-row strong')) + .map((element) => Number.parseFloat(getComputedStyle(element).fontWeight)), + receivedText: document.querySelector('#receivedLabel')?.textContent?.trim() ?? '', + rowWeights: Array.from(document.querySelectorAll('#answer .progress-item strong')) + .map((element) => ({ text: element.textContent?.trim() ?? '', weight: Number.parseFloat(getComputedStyle(element).fontWeight) })), + sectionLabel: sectionLabel && sectionStyle ? { + fontSize: Number.parseFloat(sectionStyle.fontSize), + fontWeight: Number.parseFloat(sectionStyle.fontWeight), + text: sectionLabel.textContent?.trim() ?? '', + textTransform: sectionStyle.textTransform, + } : null, + times: Array.from(document.querySelectorAll('#answer .progress-time')) + .map((element) => element.textContent?.trim() ?? ''), + } + })()`) + expect(progress.sectionLabel?.text).toBe('Agent progress') + expect(progress.sectionLabel?.textTransform).toBe('none') + expect(progress.sectionLabel?.fontSize).toBe(14) + expect(progress.sectionLabel?.fontWeight).toBe(700) + expect(progress.receivedText).toMatch(/^[A-Z][a-z]{2} \d{1,2}, \d{4} \d{2}:\d{2}:\d{2} UTC$/) + expect(progress.detailWeights.every((weight) => weight <= 400)).toBe(true) + for (const row of progress.rowWeights) { + if (row.text === 'Human review halted') { + expect(row.weight).toBe(700) + } else { + expect(row.weight).toBe(400) + } + } + for (const time of progress.times.filter((value) => value !== '-')) { + expect(time).not.toContain('UTC') + expect(time).toMatch(/^\d{2}:\d{2}:\d{2}$/) + } +} + async function expectNoHorizontalOverflow(page: Page): Promise { const overflow = await page.evaluate<{ bodyWidth: number From 24ce56dd2a7fdcfdf9bbdd69966f3f75ac23df52 Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 15:02:41 -0500 Subject: [PATCH 24/64] fix: center Cloudflare trace review buttons --- .../cloudflare-agents/approval-trace/src/ui.ts | 18 +++++++++--------- .../test/browser/approval-trace.ui.spec.ts | 14 ++++++++++++-- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index f1ea723c..c44921c4 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -2071,12 +2071,12 @@ export function renderApp(): string { box-sizing: border-box; display: grid; gap: 2px; - justify-items: start; + justify-items: center; max-width: 100%; min-width: 0; padding: 0; - text-align: left; - width: auto; + text-align: center; + width: max-content; } .action-copy small { @@ -2084,7 +2084,7 @@ export function renderApp(): string { display: block; font-size: 9px; font-weight: 600; - justify-self: start; + justify-self: center; line-height: 1.15; opacity: 0.78; overflow: visible; @@ -2096,15 +2096,15 @@ export function renderApp(): string { .button-label { display: block; - font-size: 12px; + font-size: 13px; font-weight: 800; - justify-self: start; + justify-self: center; line-height: 1.12; white-space: nowrap; } .primary .button-label { - font-size: 12px; + font-size: 13px; } .primary .action-copy small { @@ -2153,8 +2153,8 @@ export function renderApp(): string { .button-icon svg { display: block; - height: 14px; - width: 14px; + height: 15px; + width: 15px; } .primary .button-icon { diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index d1f05a32..a7127ea8 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -178,10 +178,12 @@ async function expectActionButtonsCentered(page: Page): Promise { iconInsideButton: boolean iconTextBlockYDelta: number iconLabelYDelta: number + labelCopyCenterDelta: number labelFontSize: number labelFits: boolean labelWeight: number noLabelIconCollision: boolean + smallCopyCenterDelta: number smallFits: boolean textAlign: string }> @@ -219,10 +221,16 @@ async function expectActionButtonsCentered(page: Page): Promise { iconLabelYDelta: icon && label ? Math.abs((icon.top + icon.height / 2) - (label.top + label.height / 2)) : 999, + labelCopyCenterDelta: label && copy + ? Math.abs((label.left + label.width / 2) - (copy.left + copy.width / 2)) + : 999, labelFontSize: labelStyle ? Number.parseFloat(labelStyle.fontSize) : 0, labelFits: label ? label.left >= buttonRect.left && label.right <= buttonRect.right : false, labelWeight: labelStyle ? Number.parseFloat(labelStyle.fontWeight) : 0, noLabelIconCollision: icon && label ? icon.right + 2 <= label.left : false, + smallCopyCenterDelta: small && copy + ? Math.abs((small.left + small.width / 2) - (copy.left + copy.width / 2)) + : 999, smallFits: small ? small.left >= buttonRect.left && small.right <= buttonRect.right : false, textAlign: copy ? getComputedStyle(button.querySelector('.action-copy')).textAlign : '', } @@ -230,7 +238,7 @@ async function expectActionButtonsCentered(page: Page): Promise { for (const geometry of buttonGeometry) { expect(geometry.buttonDisplay).toBe('flex') expect(geometry.contentDisplay).toBe('flex') - expect(geometry.textAlign).toBe('left') + expect(geometry.textAlign).toBe('center') expect(geometry.contentCenterDelta).toBeLessThanOrEqual(1.5) expect(geometry.contentInsideButton).toBe(true) expect(geometry.iconCopyGap).toBeGreaterThanOrEqual(8) @@ -238,13 +246,15 @@ async function expectActionButtonsCentered(page: Page): Promise { expect(geometry.iconInsideButton).toBe(true) expect(geometry.iconTextBlockYDelta).toBeLessThanOrEqual(1.5) expect(geometry.iconLabelYDelta).toBeGreaterThan(4) - expect(geometry.labelFontSize).toBeGreaterThanOrEqual(12) + expect(geometry.labelCopyCenterDelta).toBeLessThanOrEqual(1) + expect(geometry.labelFontSize).toBeGreaterThanOrEqual(13) expect(geometry.labelWeight).toBeGreaterThanOrEqual(800) expect(geometry.captionFitsCopy).toBe(true) expect(geometry.captionFontSize).toBeGreaterThanOrEqual(8) expect(geometry.captionMuted).toBe(true) expect(geometry.labelFits).toBe(true) expect(geometry.noLabelIconCollision).toBe(true) + expect(geometry.smallCopyCenterDelta).toBeLessThanOrEqual(1) expect(geometry.smallFits).toBe(true) } } From a5f7350326818e43697ce40547fe3759f3daaa2a Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 15:18:10 -0500 Subject: [PATCH 25/64] fix: style Cloudflare trace receipt JSON --- .../approval-trace/src/ui.ts | 51 +++++++++++++++++-- .../test/browser/approval-trace.ui.spec.ts | 47 +++++++++++++++++ 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index c44921c4..72b27548 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -2728,11 +2728,11 @@ export function renderApp(): string { .json pre { background: #fff; - border: 1px solid var(--line); + border: 0; color: #102033; font-size: 12px; line-height: 1.38; - max-height: 194px; + max-height: 208px; overflow: auto; padding: 6px 10px; position: relative; @@ -2740,7 +2740,7 @@ export function renderApp(): string { .json-line { display: grid; - grid-template-columns: 24px minmax(0, 1fr); + grid-template-columns: 34px minmax(0, 1fr); } .json-line-number { @@ -2755,6 +2755,27 @@ export function renderApp(): string { white-space: pre; } + .json-token.key { + color: #c33a65; + font-weight: 650; + } + + .json-token.string { + color: #284f93; + } + + .json-token.number { + color: #1d7665; + } + + .json-token.literal { + color: #8b4c9b; + } + + .json-token.punctuation { + color: #6b7280; + } + .receipt-tabs { border-bottom: 1px solid var(--line); display: flex; @@ -3924,9 +3945,31 @@ export function renderApp(): string { return format === 'compact' ? JSON.stringify(value) : pretty(value); } + function highlightJsonValueFragment(fragment) { + const token = fragment.match(/^(\\s*)("(?:\\\\.|[^"\\\\])*"|-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?|true|false|null)(.*)$/); + if (!token) return escapeHtml(fragment); + const type = token[2].startsWith('"') + ? 'string' + : token[2] === 'true' || token[2] === 'false' || token[2] === 'null' + ? 'literal' + : 'number'; + return escapeHtml(token[1]) + + '' + escapeHtml(token[2]) + '' + + escapeHtml(token[3]); + } + + function highlightJsonLine(line) { + const key = line.match(/^(\\s*)("(?:\\\\.|[^"\\\\])*"):(.*)$/); + if (!key) return highlightJsonValueFragment(line); + return escapeHtml(key[1]) + + '' + escapeHtml(key[2]) + '' + + ':' + + highlightJsonValueFragment(key[3]); + } + function renderReceiptJson(value) { return '
' + formatReceiptJson(value).split('\\n').map((line, index) => (
-          '' + String(index + 1) + '' + escapeHtml(line) + ''
+          '' + String(index + 1) + '' + highlightJsonLine(line) + ''
         )).join('') + '
'; } diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts index a7127ea8..d8fdd455 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/test/browser/approval-trace.ui.spec.ts @@ -415,6 +415,51 @@ async function expectReferenceVerificationRows(page: Page): Promise { } } +async function expectReferenceReceiptJsonSyntax(page: Page): Promise { + const syntax = await page.evaluate<{ + borderWidth: string + gutterWidth: number + keyColor: string + keyCount: number + lineCount: number + maxHeight: number + numberColor: string + stringColor: string + stringCount: number + tenthLineNumber: string + }>(`(() => { + const pre = document.querySelector('#receipts pre') + const preStyle = pre ? getComputedStyle(pre) : null + const numbers = Array.from(document.querySelectorAll('#receipts .json-line-number')) + const firstNumber = numbers[0]?.getBoundingClientRect() + const key = document.querySelector('#receipts .json-token.key') + const string = document.querySelector('#receipts .json-token.string') + const number = document.querySelector('#receipts .json-token.number') + return { + borderWidth: preStyle?.borderTopWidth ?? '', + gutterWidth: firstNumber ? Math.round(firstNumber.width) : 0, + keyColor: key ? getComputedStyle(key).color : '', + keyCount: document.querySelectorAll('#receipts .json-token.key').length, + lineCount: document.querySelectorAll('#receipts .json-line').length, + maxHeight: preStyle ? Number.parseFloat(preStyle.maxHeight) : 0, + numberColor: number ? getComputedStyle(number).color : '', + stringColor: string ? getComputedStyle(string).color : '', + stringCount: document.querySelectorAll('#receipts .json-token.string').length, + tenthLineNumber: numbers[9]?.textContent ?? '', + } + })()`) + expect(syntax.borderWidth).toBe('0px') + expect(syntax.gutterWidth).toBeGreaterThanOrEqual(34) + expect(syntax.keyCount).toBeGreaterThan(4) + expect(syntax.lineCount).toBeGreaterThan(1) + expect(syntax.maxHeight).toBeGreaterThanOrEqual(208) + expect(syntax.stringCount).toBeGreaterThan(4) + expect(syntax.tenthLineNumber).toBe('10') + expect(syntax.keyColor).toBe('rgb(195, 58, 101)') + expect(syntax.stringColor).toBe('rgb(40, 79, 147)') + expect(syntax.numberColor).toBe('rgb(29, 118, 101)') +} + async function expectReferenceDesktopCenterStack(page: Page): Promise { const stackGeometry = await page.evaluate<{ actionBottomGap: number @@ -699,10 +744,12 @@ test.describe('Cloudflare approval trace browser UI', () => { await page.getByRole('tab', { name: 'Summary' }).click() const prettyReceiptLines = await page.locator('#receipts .json-line').count() expect(prettyReceiptLines).toBeGreaterThan(1) + await expectReferenceReceiptJsonSyntax(page) await page.locator('#receiptFormat').selectOption('compact') await expect.poll(async () => page.locator('#receipts .json-line').count()).toBe(1) await page.locator('#receiptFormat').selectOption('pretty') await expect.poll(async () => page.locator('#receipts .json-line').count()).toBeGreaterThan(1) + await expectReferenceReceiptJsonSyntax(page) await expectCopies(page.getByRole('button', { name: 'Copy trace ID' })) await expectCopies(page.getByRole('button', { name: 'Copy Agent signature' })) From 8a6fa9a7eb08066661c09a382302087dd5406d0a Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 7 Jun 2026 15:51:19 -0500 Subject: [PATCH 26/64] fix: align Cloudflare trace action controls --- .../approval-trace/src/ui.ts | 38 +++++++++++++------ .../test/browser/approval-trace.ui.spec.ts | 14 ++++--- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts index 72b27548..40755c22 100644 --- a/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts +++ b/packages/integration/examples/cloudflare-agents/approval-trace/src/ui.ts @@ -2052,7 +2052,7 @@ export function renderApp(): string { .actions { display: grid; - grid-template-columns: minmax(190px, 1.08fr) repeat(2, minmax(0, 1fr)); + grid-template-columns: minmax(190px, 1.08fr) repeat(2, minmax(180px, 1fr)); gap: 12px; margin-top: 6px; min-width: 0; @@ -2060,23 +2060,29 @@ export function renderApp(): string { .button-content { align-items: center; - display: inline-flex; - gap: 8px; + display: flex; justify-content: center; max-width: 100%; min-width: 0; + width: 100%; + } + + .button-content::after { + content: none; } .action-copy { + align-items: center; box-sizing: border-box; - display: grid; + display: flex; + flex-direction: column; gap: 2px; - justify-items: center; + justify-content: center; max-width: 100%; min-width: 0; - padding: 0; + padding: 0 18px; text-align: center; - width: max-content; + width: 100%; } .action-copy small { @@ -2100,7 +2106,9 @@ export function renderApp(): string { font-weight: 800; justify-self: center; line-height: 1.12; + max-width: 100%; white-space: nowrap; + width: max-content; } .primary .button-label { @@ -2145,9 +2153,11 @@ export function renderApp(): string { height: 18px; justify-content: center; line-height: 0; - margin-left: 0; - margin-top: 0; - position: static; + left: 12px; + margin: 0; + position: absolute; + top: 50%; + transform: translateY(-50%); width: 18px; } @@ -3069,7 +3079,7 @@ export function renderApp(): string { .actions { gap: 10px; - grid-template-columns: minmax(190px, 1.12fr) repeat(2, minmax(0, 1fr)); + grid-template-columns: minmax(190px, 1fr) repeat(2, minmax(180px, 0.95fr)); } .action-copy small, @@ -3159,6 +3169,10 @@ export function renderApp(): string { grid-template-columns: 20px 48px minmax(0, 1fr); } + .actions { + grid-template-columns: 1fr; + } + .event-hash { grid-column: 3; justify-self: start; @@ -3184,7 +3198,7 @@ export function renderApp(): string {
- +