Skip to content

DRU-622 - Show an in-app gate the question its app wrote - #676

Merged
czpython merged 1 commit into
mainfrom
agent/DRU-622
Sep 20, 2026
Merged

czpython merged 1 commit into
mainfrom
agent/DRU-622

Conversation

@druks-operator

Copy link
Copy Markdown
Contributor

What changed

Plan

Goal

Show the question an app supplied for an in-app gate. The binding example is KeepQuote.wait(input_request={"presentation": "in_app", "label": "Keep this quote?", "controls": ["keep", "skip"]}).

Implementation

  1. Update frontend/src/components/RunControls.tsx so the shared InAppReview reads the existing optional ask.label and renders it as the review prompt before the artifact, questions, note box, and controls. Keep unlabeled requests supported because InputRequest.label is optional and the component is also lent to installed apps.
  2. Add focused coverage in frontend/src/components/RunControls.test.tsx proving a declared in-app label is visible with its controls. Existing unlabeled fixtures continue to cover the optional-label path.
  3. Keep the backend fallback in backend/druks/durable/models.py: backend/druks/mcp/gateway/services.py already returns await run.get_ask(), so once InAppReview consumes the label, both an app-declared label and the existing "Review: <artifact title>" / "Review" fallback serve the view. No API or type-shape change is needed.

Scope boundaries

  • Do not duplicate the in-app label in SubjectPage; it already delegates in-app gates through GateControls to the shared review component.
  • External-gate rendering, dashboard request summaries, answer submission, and artifact loading remain unchanged.

Ruled out

  • Render the label only in SubjectPage: that would leave other GateControls and exported InAppReview consumers without the app's question and duplicate presentation logic outside the shared component.
  • Remove the Run.get_ask() fallback: existing unlabeled in-app asks would lose the artifact-derived review context even though the gate endpoint already transports it to the client.
  • Derive the prompt from the artifact in the frontend: artifact-less gates would still lack context, and an app's declared label could be displaced by client-side inference.

Acceptance Criteria

  • AC1: InAppReview renders the in-app ask's label before its artifact, questions, note field, and controls; an ask declaring "Keep this quote?" therefore displays that question alongside keep / skip.
    • Verification: Inspect frontend/src/components/RunControls.tsx and its component test in frontend/src/components/RunControls.test.tsx, which must assert the supplied label and controls are rendered in the review.
  • AC2: Labels returned by Run.get_ask() reach the shared in-app review: an app-declared label wins, while an unlabeled ask retains the existing "Review: <artifact title>" or "Review" fallback.
    • Verification: Trace backend/druks/mcp/gateway/services.py from Run.get_ask() through GateControls to InAppReview; the existing backend fallback coverage and the new frontend label coverage jointly exercise the path.

🤖 Generated with Claude Code

The shared InAppReview never read ask.label, so an in-app gate showed
its buttons with no question above them. Render the optional label as
the review prompt, keeping unlabeled asks supported for lent apps.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

@druks-reviewer druks-reviewer 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.

Verification — pass (round 1)

The diff is a minimal, three-file change matching the plan's scope exactly: RunControls.tsx gains one line ({ask.label && <div className="review-prompt">{ask.label}</div>}) placed as the first child of .ins-needs, i.e. before the artifact-loading state, critique block, artifact block, questions, note field, and controls. RunControls.test.tsx adds a focused test (shows the app-declared label with its controls) asserting the label and both custom control labels (keep/skip) render together. styles.css adds one .review-prompt rule for spacing.

AC2's backend wiring was traced end to end: Run.get_ask() in backend/druks/durable/models.py and backend/druks/mcp/gateway/services.py are byte-identical to base — get_ask()'s dict-merge ({"label": fallback, ...} | ask semantics) already means an app-declared label overrides the "Review: <title>"/"Review" fallback, so no backend change was needed, matching the plan. GateControls.tsx passes gate.data.ask straight into InAppReview with no transformation, and InAppReview is the sole render path (including the re-exported version installed apps use), so no other consumer needed touching — matching the plan's stated scope boundaries (SubjectPage untouched).

Acceptance criteria

  • AC1 — pass. InAppReview renders ask.label as the first element inside .ins-needs, strictly before the artifact/loading block, critique, questions, note field, and controls. New test asserts "Keep this quote?" and controls keep/skip render together.
  • AC2 — pass. Backend fallback (models.py/services.py) is unchanged and traced through GateControls to InAppReview; the dict-merge guarantees an app-declared label wins while the unlabeled path keeps its existing fallback. Existing backend test (test_get_ask_resolves_the_review_artifact) plus the new frontend test jointly exercise the path, per the plan's own verification method.

Verification profile

Check Result
uv run ruff check backend pass (local)
uv run ruff format --check backend pass (local, 452 files clean)
uv run pytest backend/ pass (local, 2151 passed)
npm --prefix frontend run lint pass (On Pull Request Frontend / checks, green on head SHA)
npm --prefix frontend run build pass (On Pull Request Frontend / checks, green on head SHA)
npm --prefix frontend test pass (On Pull Request Frontend / checks, green on head SHA)

No blocking findings. One observational note: no backend test directly asserts an in-app ask's own label survives get_ask()'s dict merge unmodified — coverage relies on Python dict-literal override semantics plus the new frontend test. Consistent with the plan's explicit backend-untouched scope and its own AC2 verification method, so not a gate failure — noting for awareness only.

Code review

Advisory, clean-room pass over the diff (no access to plan/AC/ticket). Summary: small, clean, additive change. ask.label already existed on InputRequest and was already rendered for the external presentation elsewhere (SubjectPage.tsx, EventsPage.tsx) — this closes the same gap for in_app. The conditional-render pattern matches the file's existing style ({critique && (...)}, {artifact && (...)}). The new test asserts rendered text for input it defines itself, not implementation details or pinned production prose, so it survives refactors. No regressions, no scope creep, no dead branches, no unsafe casts or swallowed exceptions found.

One non-blocking finding (left as an inline comment on frontend/src/styles.css:1638): the new .review-prompt CSS rule duplicates .ins-needs-body's five declarations verbatim instead of composing with it, which the file already does elsewhere for a similar case (review-btn review-btn-${control}). Not required for this PR; flagged for awareness.

Comment thread frontend/src/styles.css
.ins-needs-k { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--bucket-human); margin-bottom: 6px; display: flex; align-items: center; gap: 7px; }
.ins-needs-body { font-size: 13px; line-height: 1.55; color: var(--text-mid); max-width: 76ch; text-wrap: pretty; }

.review-prompt { font-size: 13px; line-height: 1.55; color: var(--text-mid); max-width: 76ch; text-wrap: pretty; margin-bottom: 10px; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Advisory (medium, non-blocking): .review-prompt copies .ins-needs-body's five declarations verbatim and adds only margin-bottom: 10px. Both render body copy inside the same .ins-needs wrapper (the wrapper InAppReview returns, and the one SubjectPage.tsx:189 uses for the external presentation's ask.label). Left as two separate rules, they'll drift independently over time — e.g. a future line-height/contrast tweak to .ins-needs-body won't reach .review-prompt.

Consider reusing the pattern already in this file (RunControls.tsx:218, `review-btn review-btn-${control}`): className="ins-needs-body review-prompt" with .review-prompt trimmed to just margin-bottom: 10px. Not required for this PR — flagging for awareness, happy to hear if there's a reason to keep them separate.

@druks-operator
druks-operator Bot marked this pull request as ready for review September 20, 2026 17:35
@druks-operator
druks-operator Bot requested a review from czpython as a code owner September 20, 2026 17:35
@czpython
czpython merged commit a65729d into main Sep 20, 2026
3 checks passed
@czpython
czpython deleted the agent/DRU-622 branch September 20, 2026 22:31
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