feat(mcp): preview-card confirm rail + fix ForEach history templating#83
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the StatusPro MCP server’s Prefab UI builders to improve mutation-preview UX in MCP Apps clients (notably Claude Desktop) by adding a confirm/apply “rail” with pending/applied/error/cancelled states, and fixes templating in order-history rendering inside ForEach.
Changes:
- Add shared preview-card state initialization plus reusable confirm/cancel action builders and a footer state machine (Preview → Pending → Applied/Retry/Cancelled).
- Render inline apply errors via a destructive
Alertand push apply results into agent context viaUpdateContext. - Fix
build_order_detail_uihistory row bindings by capturingForEach(... ) as entryand binding cells to the loop item.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
statuspro_mcp_server/src/statuspro_mcp/tools/prefab_ui.py |
Adds confirm/footer state machine, inline error rendering, context updates, and fixes ForEach history bindings. |
statuspro_mcp_server/tests/tools/test_prefab_ui.py |
Updates toolCall assertions for the new UI structure and adds tests for the new preview-card guard/state seeding behavior. |
Comments suppressed due to low confidence (2)
statuspro_mcp_server/src/statuspro_mcp/tools/prefab_ui.py:268
Cancelcan’t reach the intended terminal cancelled state after an apply error: the cancel action only setscancelled=True, but the footer checkserrorbeforecancelled, so the UI will keep rendering the Retry state as long aserroris set. Either clearerrorin the cancel action or make the cancelled branch take precedence over the error branch so cancelling reliably locks out further actions.
with Elif("applied"):
Button(
label=applied_label,
variant="success",
icon="check",
disabled=True,
)
with Elif("error"):
Button(
label="Retry",
variant="warning",
icon="rotate-cw",
on_click=confirm_action,
)
with Elif("cancelled"):
Button(label="Cancelled", variant="outline", disabled=True)
with Else():
Button(
label=confirm_label,
variant="default",
on_click=confirm_action,
disabled=Rx("pending") | Rx("cancelled"),
)
Button(
label="Cancel",
variant="outline",
on_click=cancel_action,
disabled=Rx("pending") | Rx("applied") | Rx("cancelled"),
)
statuspro_mcp_server/src/statuspro_mcp/tools/prefab_ui.py:252
- The Retry button (error state) is not disabled based on
pending/cancelled, unlike the initial Confirm button. This undermines the double-click safety goal in the error/retry path because the button can still be clicked rapidly while a retry call is in-flight. Binddisabledfor Retry as well (and/or rely on an immediateSetState(pending, True)plus UI disable) to prevent duplicate retries.
with Elif("error"):
Button(
label="Retry",
variant="warning",
icon="rotate-cw",
on_click=confirm_action,
)
515649f to
5b75d4b
Compare
Two related issues surfaced by Claude Desktop rendering:
**Confirm-button click feedback + double-click safety.** The four preview
cards (status change, comment, due date, bulk status) now render a footer
state machine that morphs through Preview → Pending… → Applied / Retry /
Cancelled. `SetState("pending", True)` fires synchronously before the
apply CallTool so a rapid second click can't trigger a duplicate
mutation, with `disabled=Rx("pending") | Rx("cancelled")` as the
belt-and-suspenders binding. `on_success` pushes the structured result
into the agent's model context via `UpdateContext`; `on_error` surfaces
the failure inline via a shadcn `Alert` (matching katana's pattern) and
swaps the primary button to Retry.
**ForEach history templating.** `build_order_detail_ui` was emitting raw
`{{ created_at }}` literals — the renderer can't resolve them because
inside `ForEach` the binding scope is `$item`, not parent state. Capture
the loop via `as entry:` and bind to `entry.created_at` etc.
Ports the confirmation pattern from katana-openapi-client's prefab_ui.
Co-Authored-By: Claude <noreply@anthropic.com>
5b75d4b to
b5f6a8a
Compare
|
Addressing the two findings from the suppressed-low-confidence section of @copilot-pull-request-reviewer's review: 1. Cancel after error doesn't lock out further actions — fixed by reordering the footer If/Elif chain so 2. Retry has no Both in b5f6a8a. |
…sage on Cancel Closes #54, closes #53. **#54 — Typed Rx refs for CallTool template args.** The four Confirm-button ``CallTool`` actions used bare-string templates like ``"{{ preview.order_id }}"`` to bind iframe state into tool arguments. A typo (``"{{ preivew.order_id }}"``) would silently expand to empty and the host would call the tool with a missing arg, with no signal on our side. Added a ``_preview_ref(field) -> Rx`` helper next to the existing action builders; the four builders now go through it: "order_id": _preview_ref("order_id"), "status_code": _preview_ref("new_status_code"), ... Wire shape is identical (``Rx`` serializes to the same template string), so all existing tests continue to pass. The win is a single namespaced choke-point and refactor-safe references — the ``preview.`` prefix can no longer be typo'd, and field names are greppable. **#53 — Pin no-SendMessage on Cancel.** PR #83 already replaced the ``SendMessage("Cancel the X")`` chain with ``[SetState("cancelled", True), ShowToast(...)]``, but no test enforced that contract. Added ``test_cancel_buttons_do_not_send_chat_messages`` which walks every preview card's envelope, finds each Cancel button, and asserts no ``sendMessage`` action in the on_click chain. Pinning so a future regression that re-introduces SendMessage on Cancel surfaces here instead of as chat-noise in production. Co-Authored-By: Claude <noreply@anthropic.com>
…sage on Cancel Closes #54, closes #53. **#54 — Typed Rx refs for CallTool template args.** The four Confirm-button ``CallTool`` actions used bare-string templates like ``"{{ preview.order_id }}"`` to bind iframe state into tool arguments. A typo (``"{{ preivew.order_id }}"``) would silently expand to empty and the host would call the tool with a missing arg, with no signal on our side. Added a ``_preview_ref(field) -> Rx`` helper next to the existing action builders; the four builders now go through it: "order_id": _preview_ref("order_id"), "status_code": _preview_ref("new_status_code"), ... Wire shape is identical (``Rx`` serializes to the same template string), so all existing tests continue to pass. The win is a single namespaced choke-point and refactor-safe references — the ``preview.`` prefix can no longer be typo'd, and field names are greppable. **#53 — Pin no-SendMessage on Cancel.** PR #83 already replaced the ``SendMessage("Cancel the X")`` chain with ``[SetState("cancelled", True), ShowToast(...)]``, but no test enforced that contract. Added ``test_cancel_buttons_do_not_send_chat_messages`` which walks every preview card's envelope, finds each Cancel button, and asserts no ``sendMessage`` action in the on_click chain. Pinning so a future regression that re-introduces SendMessage on Cancel surfaces here instead of as chat-noise in production. Co-Authored-By: Claude <noreply@anthropic.com>
…sage on Cancel (#85) Closes #54, closes #53. **#54 — Typed Rx refs for CallTool template args.** The four Confirm-button ``CallTool`` actions used bare-string templates like ``"{{ preview.order_id }}"`` to bind iframe state into tool arguments. A typo (``"{{ preivew.order_id }}"``) would silently expand to empty and the host would call the tool with a missing arg, with no signal on our side. Added a ``_preview_ref(field) -> Rx`` helper next to the existing action builders; the four builders now go through it: "order_id": _preview_ref("order_id"), "status_code": _preview_ref("new_status_code"), ... Wire shape is identical (``Rx`` serializes to the same template string), so all existing tests continue to pass. The win is a single namespaced choke-point and refactor-safe references — the ``preview.`` prefix can no longer be typo'd, and field names are greppable. **#53 — Pin no-SendMessage on Cancel.** PR #83 already replaced the ``SendMessage("Cancel the X")`` chain with ``[SetState("cancelled", True), ShowToast(...)]``, but no test enforced that contract. Added ``test_cancel_buttons_do_not_send_chat_messages`` which walks every preview card's envelope, finds each Cancel button, and asserts no ``sendMessage`` action in the on_click chain. Pinning so a future regression that re-introduces SendMessage on Cancel surfaces here instead of as chat-noise in production. Co-authored-by: Claude <noreply@anthropic.com>
Summary
Fixes two issues surfaced by Claude Desktop rendering of MCP-Apps preview cards.
Confirm-button feedback + double-click safety. The four preview cards (status change, comment, due date, bulk status) now render a footer state machine: Preview → Pending… → Applied / Retry / Cancelled.
SetState("pending", True)flips synchronously before the applyCallToolfires, so a rapid second click can't trigger a duplicate mutation.disabled=Rx("pending") | Rx("cancelled")on the button is the belt-and-suspenders binding.on_successpushes the structured result into the agent's model context viaUpdateContext;on_errorsurfaces the failure inline via a shadcnAlert(matching katana's pattern) and swaps the primary button to a Retry affordance.ForEach history templating.
build_order_detail_uiwas rendering raw{{ created_at }}text literals because insideForEachthe binding scope is$item, not parent state. Capture the loop viaas entry:so each cell binds to the row.Ports the direct-apply confirmation rail from sibling katana-openapi-client.
Test plan
uv run poe check— full validation (test + lint + typecheck + format) passestest_prefab_ui.pypass, including two new ones:test_preview_cards_wire_double_click_guard— pins the SetState guard and seeded state across all four preview cardstest_status_change_preview_invalid_does_not_seed_apply_state— invalid-transition path doesn't seed pending/appliedget_orderon an order with history — history rows render with actual timestamps/events, not raw{{ field }}literals🤖 Generated with Claude Code