feat(console): show the delivery budget and shed sections in the prompt inspector (ADR 0108 D6) - #3257
Conversation
…pt inspector The backend has returned the ADR 0108 D6 delivery budget since #3247 — the ceiling, chars used, and an `overflow` list naming what was shed to fit, plus `truncated` on every section the budget cut. Nothing consumed any of it: `api.promptPreview()` had zero callers, so the console never called the preview route at all, and the fields were absent from the TypeScript types. Two latent bugs had to be fixed before the preview could be wired up at all: - `promptText()` read only `stable + context`, but post-#3234 captures and the /preview synthesis carry an EMPTY `system.context` and put the whole projection in `projected_context` — so the dialog rendered a bare stable prefix for exactly the calls it is now most used to inspect. `splitLine()` reported a 0-char tail for the same reason. - A `"projected"`-scope section fell through to the stable tint, painting the per-turn projection as if it were the cacheable prefix — the one split the breakdown exists to show. The delivery budget renders as its own strip (ceiling / used / spare, with the shed list beneath) and is deliberately NOT merged into the existing section-size bars: those are a share-of-prompt breakdown, this is a ceiling, and labeling them alike would imply the sections were measured against it. The next-call preview is a tab that loads on selection, not with the dialog — it re-runs the dynamic layer including retrieval, so it is the one tab that costs something to open — and it is badged speculative because no model call was made and nothing was written to the injection log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WEMxBi71vjtmmmziFCMcby
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WEMxBi71vjtmmmziFCMcby
|
Warning Review limit reachedNext included review available in 25 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
QA panel review — FAIL
code-review-structural · head 253b6cd81068 · formal
⚠️ PR advanced 1 commit(s) during this round (253b6cd81068→7711165cdf7f); 1 finding(s) in the delta were demoted to possibly addressed.
The PR adds a prompt-preview feature to the inspector. The one thing to fix first is the stuck-loading cycle in PromptViewer.tsx: previewState is both a guard condition and a dep, so the effect's own setPreviewState("loading") triggers a re-run whose cleanup kills the in-flight fetch — the UI is permanently stuck on "Composing the next call…". The fix is to remove previewState from the deps array (the guard already prevents re-fetching). The panel disagreed on nothing; both findings were confirmed by the verifier without re-grading. The structural pass completed; no gaps.
Findings
| Severity | Location | Finding | Verified | |
|---|---|---|---|---|
| 🔴 | blocker | apps/web/src/chat/PromptViewer.tsx:125 |
The preview-fetching useEffect is permanently stuck in 'loading' state: setPreviewState("loading") mutates a value in the deps array, triggering a re-run whose… | confirmed |
| 🟡 | minor | changelog.d/3257.added.md:1 |
The changelog fragment's bold lead-in does not end in (#NNNN), deviating from the documented fragment shape in changelog.d/README.md and PROTO.md. |
⏳ possibly addressed |
findings JSON (machine-readable)
[
{
"file": "apps/web/src/chat/PromptViewer.tsx",
"line": 125,
"severity": "blocker",
"category": "correctness",
"claim": "The preview-fetching useEffect is permanently stuck in 'loading' state: setPreviewState(\"loading\") mutates a value in the deps array, triggering a re-run whose cleanup sets alive=false, causing the in-flight fetch's .then() to discard its result; no code path ever resets previewState back to 'idle', so the user sees 'Composing the next call\u2026' indefinitely.",
"evidence": "setPreviewState(\"loading\");\n api\n .promptPreview(sessionId ?? \"\")\n .then((res) => {\n if (!alive) return;\n if (res.call) {\n setPreview(res.call);\n setPreviewState(\"idle\");\n } else {\n setPreviewNote(res.reason || (res.enabled ? \"\" : \"prompt capture is off\"));\n setPreviewState(\"unavailable\");\n }\n })\n .catch((e) => {\n if (!alive) return;\n setPreviewNote(e instanceof Error ? e.message : String(e));\n setPreviewState(\"error\");\n });\n return () => {\n alive = false;\n };\n }, [active, preview, previewState, sessionId]);",
"verdict": "confirmed",
"note": "Traced the full cycle: setPreviewState('loading') changes a dep \u2192 cleanup sets alive=false \u2192 re-run's guard (previewState!=='idle') returns early \u2192 fetch .then() discards via !alive \u2192 no path resets to 'idle'. Permanently stuck."
},
{
"file": "changelog.d/3257.added.md",
"line": 1,
"severity": "minor",
"category": "conventions",
"claim": "The changelog fragment's bold lead-in does not end in `(#NNNN)`, deviating from the documented fragment shape in changelog.d/README.md and PROTO.md.",
"evidence": "changelog.d/README.md documents the shape as a \"bullet with a **bold lead-in** ending in `(#NNNN)`\" (example: `- **`POST /api/fleet/{name}/stop` no longer reports a stop it didn't achieve (#2286).**`). This fragment's lead-in is `- **The prompt inspector shows the delivery budget and what it shed.**` \u2014 no `(#3257)` at the end of the bold.",
"verdict": "possibly addressed",
"note": "README example (read from default branch) shows bold lead-in ending with (#2286); the fragment's lead-in ends with 'shed.' \u2014 no (#3257). Minor convention deviation. \u2014 the PR head advanced while this round ran and the new commits touch this finding's region \u2014 verified against the superseded head, so it may already be addressed"
}
]1 finding(s) excluded from the verdict by in-diff confinement (file not among this PR's changed paths):
changelog.d/3257.added.md(minor) — The changelog fragment's bold lead-in does not end in(#NNNN), deviating from the documented fragment shape in changelog.d/README.md and PROTO.md.
`previewState` was both SET inside the preview effect and listed in its dep
array. setPreviewState("loading") re-ran the effect, the cleanup flipped
`alive = false` so the in-flight response was discarded, and the re-run's own
guard returned early because the state was no longer "idle" — nothing ever set
it back, so the tab sat on "Composing the next call…" forever.
Dropping `previewState` from the deps is enough: React rebuilds the closure
every render, so a run triggered by `active`/`preview`/`sessionId` still reads
the current state and the guard still prevents a re-fetch. `preview` stays a
dep — it is set on the SUCCESS path after the work completes, so its re-run is
benign, which is exactly the distinction that made `previewState` a bug.
Every test on this branch targeted the pure formatters in promptView.ts, so
nothing exercised the component and this shipped green. Adds four component
tests (jsdom + react-dom/client, the NewAgentPanel precedent) driving the real
effect. They use a hand-resolved deferred rather than mockResolvedValue: an
immediately-resolved mock settles before React re-runs the effect, so the
self-cancel never happens and the tests pass against the broken code — with the
deferred, all four fail when the dep array is re-armed.
Also fixes the changelog fragment to the documented shape: changelog.d/README.md
puts the (#NNNN) and its period INSIDE the bold lead-in, which is what the
collation extracts as the release-note title.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEMxBi71vjtmmmziFCMcby
There was a problem hiding this comment.
QA panel review — PASS
code-review-structural · head 7711165cdf7f · formal
⚠️ PR advanced 1 commit(s) during this round (7711165cdf7f→0a09aef22e62); 1 finding(s) in the delta were demoted to possibly addressed.
Both prior findings are refuted by the verifier: the PromptViewer deps array deliberately excludes previewState (with an eslint-disable and explanatory comment), and the changelog fragment does carry the (#3257) suffix. The finders appear to have quoted an earlier revision. No blockers or minors survive; the PR is clean on this pass.
Prior requests
| Prior finding | Disposition | Why | |
|---|---|---|---|
| 🚫 | apps/web/src/chat/PromptViewer.tsx:125 |
refuted | The verifier confirms the actual deps array is [active, preview, sessionId] — previewState is deliberately excluded with an eslint-disable-next-line react-hook… |
| 🚫 | changelog.d/3257.added.md:1 |
refuted | The verifier confirms the actual file reads The prompt inspector shows the delivery budget and what it shed (#3257). — the suffix is present. The finding's… |
Findings
| Severity | Location | Finding | Verified | |
|---|---|---|---|---|
| 🔴 | blocker | apps/web/src/chat/PromptViewer.tsx:125 |
The preview-fetching useEffect is permanently stuck in 'loading' state: setPreviewState("loading") mutates a value in the deps array, triggering a re-run whose… | ⏳ possibly addressed |
findings JSON (machine-readable)
[
{
"file": "apps/web/src/chat/PromptViewer.tsx",
"line": 125,
"severity": "blocker",
"claim": "The preview-fetching useEffect is permanently stuck in 'loading' state: setPreviewState(\"loading\") mutates a value in the deps array, triggering a re-run whose cleanup sets alive=false, causing the in-flight fetch's .then() to discard its result; no code path ever resets previewState back to 'idle', so the user sees 'Composing the next call\u2026' indefinitely.",
"verdict": "possibly addressed",
"carried": true,
"note": "Traced the full cycle: setPreviewState('loading') changes a dep \u2192 cleanup sets alive=false \u2192 re-run's guard (previewState!=='idle') returns early \u2192 fetch .then() discards via !alive \u2192 no path resets to 'idle'. Permanently stuck. \u2014 carried from a prior round \u2014 a confirmed blocker/major this round neither fixed nor refuted (protoAgent#2283); it keeps gating until positively cleared \u2014 the PR head advanced while this round ran and the new commits touch this finding's region \u2014 verified against the superseded head, so it may already be addressed"
}
]Unaccounted prior finding(s). An earlier round of this panel confirmed the following, and this round neither reports them, nor says they were fixed, nor refutes them:
apps/web/src/chat/PromptViewer.tsx:125(blocker) — The preview-fetching useEffect is permanently stuck in 'loading' state: setPreviewState("loading") mutates a value in the deps array, triggering a re-run whose cleanup sets alive=false, causing the in-flight fetch's .the
A finding that disappears without a disposition is unproven, not resolved (issue #26). Any standing block stays up until the next round accounts for it — or an operator dismisses this review.
Refs #3187, #3184. Console-only — no Python changes.
The gap
#3247 shipped the ADR 0108 D6 delivery budget through the API:
GET /api/prompts/previewreturnsbudget={chars, used, overflow: [{label, dropped_items, dropped_chars}]}(null when delivery is unbounded), and_sectionsstampstruncated: trueon every section the budget shed. It is tested attests/test_prompt_routes.py:196-215.Nothing consumed any of it.
api.promptPreview()had zero callers anywhere inapps/web/src— the console never called the preview route — andbudget/projected_context/speculative/truncatedwere not in the TypeScript types at all. D6's observability existed only as JSON nobody fetched.Two latent bugs fixed first
Wiring the preview up naively would have rendered it wrong:
promptText()returnedstable + context, but post-feat(context): remove legacy context/context_sections state channel #3234 captures and the/previewsynthesis carry an emptysystem.contextand put the entire projection inprojected_context. The dialog showed a bare stable prefix for exactly the calls it is now most used to inspect.splitLine()reportedcontext tail 0 charsfor the same reason — reading as "nothing dynamic was delivered" when several kB had been.row.scope === "context" ? tint : ""sent a"projected"section down the else branch, colouring the per-turn projection identically to the cacheable prefix — the one distinction the breakdown exists to show."projected"was also missing from thescopeunion.What you get
Shed to fit: RAG hits −8 entries (−9.1k chars) · Prior sessions −1 entry (−1.2k chars)when the budget cut something. Rendered only when a budget was in force.The delivery budget is deliberately not merged into the existing section-size bars. Those are a share-of-prompt breakdown; this is a ceiling. Labeling them alike would imply the sections had been measured against it.
How to test
context tailis non-zero on a recent turn (it read 0 before this PR).mainthis tab would have rendered blank.knowledge.top_k(5 → 60) so the RAG leg balloons past the 16k floor, send a turn, reopen: the delivery-budget strip appears andShed to fit: RAG hits −N entriesnames what was dropped, with ✂ on the shed section.context.budget_pctto tighten it further and watch the shed order walk RAG hits → prior sessions → skills rows.Gates
tsc -p tsconfig.json --noEmit+tsc -p tsconfig.node.json --noEmit(both run insidenpm run build) — cleannpm run test:unit— 1369 passed (145 files); 8 new tests inpromptView.test.ts, and 7 of them fail against this branch's own pre-change source (verified by revertingpromptView.tsand re-running:7 failed | 28 passed)npm run build— clean (the chunk-size warnings are pre-existing)node scripts/check-css-comments.mjs— OKnpx playwright test e2e/prompt-viewer.spec.tsagainst a fresh build — 2 passed (the existing spec for this surface)Review
This is console UI and needs a human visual pass — please do not auto-merge. The behavioural claims are covered by unit tests, but the strip's layout, the shed marker's legibility, and the projected-vs-stable tint in both themes are judgement calls that only look right or wrong on screen.
🤖 Generated with Claude Code
https://claude.ai/code/session_01WEMxBi71vjtmmmziFCMcby