feat(agents): draft preview — chat against any revision before promoting (feature 29)#2744
Conversation
The Overview tab's Recent sessions widget only checked the query's `isLoading` flag and fell through to the empty state on anything else — so when the agent platform API errored, it pulsed skeletons forever instead of saying so. The full Sessions pane already surfaces the error correctly via AgentDetailEmptyState; this just matches that behaviour on Overview. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(feature 29)
Lets operators test a non-live revision via the real ingress before promoting
it. Closes the freeze→promote-to-test loop that previously forced shipping to
prod before you'd ever chatted with the agent.
## Wire
- POST .../agent_applications/{id}/preview-token/?revision_id=<uuid> mints a
short-lived HS256 JWT (15-min TTL); body can carry secret_overrides for
per-preview env-key values that never touch live.
- /run /send /listen /cancel attach the JWT via X-Agent-Preview-Token (rides
on `parameters.header` so the fetcher merges it without clobbering the auth
bearer).
- Server emits preview_token_required ~5s before expiry and closes the
stream; client mints fresh and reconnects to the same session_id. Indefinite
re-mint cycles for long author sessions; one-shot retry on [401] as a
safety net for the initial fetch.
## Surface
- AgentRevisionBar: new "Test draft" button on non-live, non-archived
revisions → navigates to chat?revision=<id>.
- AgentChatPane: accepts `revisionId` + `resumeSessionId` from the route's
search params; switches chat key to `preview:<slug>:<rev>` so draft + live
coexist in the store; amber "Draft preview" banner with rev tag.
- Chat history rail: entries tagged with rev pill; defaults to entries
matching the current target; "Show N from other revisions" expander
reveals the rest. Cross-revision rail click navigates with both
?revision=... and ?session=... so the receiving surface auto-resumes.
## Secret overrides
- Mint body carries { secret_overrides: { KEY: "value" } } — backed into the
JWT claim. Reference identity invalidates the cached token so a Save → new
mint cycle is automatic.
- useAgentMissingSecrets joins spec.secrets[] with useAgentEnvKeys.
- AgentChatSecretOverridesCard renders above the composer when missing.length
> 0 — amber edit form → compact saved pill.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The in-chat approval card stayed up for the full decide-roundtrip → invalidate → list-refetch cycle (~200-500ms), feeling laggy after Approve/Reject. Move the cache update to `onMutate`: snapshot every approvals shape under the shared prefix, clear the decided id (filter arrays, null the chatPendingApproval object), restore on error. - chatPendingApproval (`AgentApprovalRequest | null`) → null if id matches - approvals list (`AgentApprovalRequest[]`) → filtered `onSettled` still invalidates so the next pending approval for the same session surfaces and any approvals-page list view reconciles with server truth. `onError` rolls back the snapshot key-by-key and shows the existing error toast. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the "I just froze and want to keep editing" loop and tidies a few visual nits on the revision picker. ## Clone to draft - POST .../revisions/new_draft/ atomically creates a fresh draft seeded from any source revision (one round trip, no client-side stitching). - AgentRevisionBar gains a "Clone to draft" button on non-draft revisions (`ready` / `live` / `archived`). On success it calls `onSelectRevision` so the picker lands you on the new draft in edit mode. - The "Test draft" button now reads "Test draft" for `draft` and plain "Test" for `ready` — the bundle's frozen, calling it a draft was misleading. ## Picker polish - State filter pills: `rounded-full` → `rounded-(--radius-2)` (matches the Radix Badge style used by the row badges; no more squished ovals from short uppercase words). Added `leading-none` + `py-[3px]` so text centers vertically. - Revision list rows: `items-start` → `items-center` so the colored LIVE/DRAFT badge centers between the short-id and meta lines instead of hugging the top. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
React Doctor found no issues in the changed files. 🎉 Reviewed by React Doctor for commit |
|
Wrap the runStream try/finally with a catch so a `getPreviewToken` throw (initial mint or re-mint after `preview_token_required`) sets the chat error instead of becoming an unhandled rejection and silently ending the stream. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stacked on top of #2742 (
dylan/agent-builder). Merge order: #2742 → this.Adds the draft preview feature so operators can chat with a non-live revision through the real ingress before promoting it — closes the "freeze → promote-to-test in prod" loop. Bundles a few related polish items that surfaced while wiring it.
Summary
Draft preview (commit
e4123a23b)AgentRevisionBar(non-live, non-archived revisions) →chat?revision=<id>.AgentChatPaneacceptsrevisionId+resumeSessionIdfrom the route; switcheschatIdtopreview:<slug>:<rev>so draft + live coexist in the store; amber "Draft preview" banner with rev tag.rev abc12345pill; defaults to entries matching the current target; "Show N from other revisions" expander reveals the rest. Cross-revision rail click navigates with?revision=...&session=...so the receiving surface auto-resumes the session.Preview-token transport
POST .../agent_applications/{id}/preview-token/?revision_id=<uuid>mints a short-lived HS256 JWT (15-min TTL); body can carrysecret_overridesfor per-preview env-key values that never touch live./run/send/listen/cancelattach the JWT viaX-Agent-Preview-Token(rides onparameters.headerso the fetcher merges it without clobbering the auth bearer).preview_token_required~5s before expiry and closes the stream; client mints fresh and reconnects to the samesession_id. Indefinite re-mint cycles for long author sessions; one-shot retry on[401]as a safety net for the initial fetch.Secret overrides
{ secret_overrides: { KEY: "value" } }— backed into the JWT claim. Reference identity invalidates the cached token so a Save → new mint cycle is automatic.useAgentMissingSecretsjoinsspec.secrets[]withuseAgentEnvKeys.AgentChatSecretOverridesCardrenders above the composer whenmissing.length > 0— amber edit form → compact saved pill.Approval optimistic update (commit
01889d20a)The in-chat approval card stayed up for the full decide-roundtrip → invalidate → list-refetch cycle (~200–500ms), feeling laggy. Moved the cache update to
onMutate: snapshot every approvals shape under the shared prefix, clear the decided id (filter arrays, null thechatPendingApprovalobject), restore on error.onSettledstill invalidates so the next pending approval surfaces.Clone-to-draft + picker polish (commit
d240f9cdf)POST .../revisions/new_draft/atomically forks any source revision into a fresh editable draft (one round trip).ready/live/archived). On success it auto-selects the new draft so the picker lands in edit mode.Test drafton drafts, plainTeston ready (the bundle's frozen, calling it draft was misleading).rounded-full→rounded-(--radius-2),leading-none, balanced padding); revision rows useitems-centerso the colored badge sits between the short-id + meta lines instead of hugging the top.Open follow-ups (not blocking)
endpointsfield from the mint response is currently unused — we hit the live ingress URL with the preview token header and Django routes correctly in path mode. Needed when the prod ingress flips to domain mode.POST .../revisions/{id}/unfreeze/action (ready → draft, gated on "never been live") would be a cleaner exit than Clone-to-draft for the just-froze-and-changed-my-mind case. Filed as a follow-up to the agent_platform team.Test plan
chat?revision=<rev>with the amber "Draft preview" banner +rev <8 chars>tag in the header.rev abc12345pill.?revision=from the URL, banner goes gray, expander reads "Show 1 from other revision".readyrevision, the test button reads plain "Test", and the "Clone to draft" button appears next to it.🤖 Generated with Claude Code