Skip to content

feat(console): show the delivery budget and shed sections in the prompt inspector (ADR 0108 D6) - #3257

Merged
mabry1985 merged 3 commits into
mainfrom
feat/3187-preview-budget-ui
Aug 28, 2026
Merged

feat(console): show the delivery budget and shed sections in the prompt inspector (ADR 0108 D6)#3257
mabry1985 merged 3 commits into
mainfrom
feat/3187-preview-budget-ui

Conversation

@mabry1985

Copy link
Copy Markdown
Member

Refs #3187, #3184. Console-only — no Python changes.

The gap

#3247 shipped the ADR 0108 D6 delivery budget through the API: GET /api/prompts/preview returns budget = {chars, used, overflow: [{label, dropped_items, dropped_chars}]} (null when delivery is unbounded), and _sections stamps truncated: true on every section the budget shed. It is tested at tests/test_prompt_routes.py:196-215.

Nothing consumed any of it. api.promptPreview() had zero callers anywhere in apps/web/src — the console never called the preview route — and budget / projected_context / speculative / truncated were 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:

  1. An empty body. promptText() returned stable + context, but post-feat(context): remove legacy context/context_sections state channel #3234 captures and the /preview synthesis carry an empty system.context and put the entire projection in projected_context. The dialog showed a bare stable prefix for exactly the calls it is now most used to inspect. splitLine() reported context tail 0 chars for the same reason — reading as "nothing dynamic was delivered" when several kB had been.
  2. The projection painted as stable. 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 the scope union.

What you get

  • A delivery-budget strip — ceiling, used, spare, a fill bar, and 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.
  • Shed sections marked in the section list (✂ plus a striped bar, so it doesn't depend on the glyph alone).
  • A "Next call (preview)" tab, loaded on selection rather than with the dialog — it re-runs the dynamic layer including retrieval, so it is the one tab that costs something to open. Badged speculative: no model call was made and nothing was written to the injection log.

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

  1. Chat ▸ any assistant message ▸ "View prompt". The existing section bars now tint the per-turn projection differently from the stable prefix; the meta line's context tail is non-zero on a recent turn (it read 0 before this PR).
  2. Click the "Next call (preview)" tab. Spinner, then the speculative projection for the next call with the banner above it. The prompt body is non-empty — on main this tab would have rendered blank.
  3. Make the budget bite. On a large-window model the default 8% budget is ~63k chars against a ~8k projection, so nothing sheds and no strip appears — that is correct. Raise knowledge.top_k (5 → 60) so the RAG leg balloons past the 16k floor, send a turn, reopen: the delivery-budget strip appears and Shed to fit: RAG hits −N entries names what was dropped, with ✂ on the shed section.
  4. Lower context.budget_pct to 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 inside npm run build) — clean
  • npm run test:unit1369 passed (145 files); 8 new tests in promptView.test.ts, and 7 of them fail against this branch's own pre-change source (verified by reverting promptView.ts and re-running: 7 failed | 28 passed)
  • npm run build — clean (the chunk-size warnings are pre-existing)
  • node scripts/check-css-comments.mjs — OK
  • npx playwright test e2e/prompt-viewer.spec.ts against 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

mabry1985 and others added 2 commits August 28, 2026 15:25
…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
@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 25 minutes.

View limit details

Limit 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.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 07058225-8c98-4349-99c0-d6fbc6e489f9

📥 Commits

Reviewing files that changed from the base of the PR and between dcb1c4f and 0a09aef.

📒 Files selected for processing (7)
  • apps/web/src/chat/PromptViewer.tsx
  • apps/web/src/chat/__tests__/PromptViewer.preview.test.ts
  • apps/web/src/chat/promptView.test.ts
  • apps/web/src/chat/promptView.ts
  • apps/web/src/chat/promptviewer.css
  • apps/web/src/lib/types.ts
  • changelog.d/3257.added.md

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@protoreview protoreview Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA panel review — FAIL

code-review-structural · head 253b6cd81068 · formal

⚠️ PR advanced 1 commit(s) during this round (253b6cd810687711165cdf7f); 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

@protoreview protoreview Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA panel review — PASS

code-review-structural · head 7711165cdf7f · formal

⚠️ PR advanced 1 commit(s) during this round (7711165cdf7f0a09aef22e62); 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.

@mabry1985
mabry1985 merged commit f884be9 into main Aug 28, 2026
15 checks passed
@mabry1985
mabry1985 deleted the feat/3187-preview-budget-ui branch August 28, 2026 23:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant