Skip to content

feat(agents): draft preview — chat against any revision before promoting (feature 29)#2744

Merged
dmarticus merged 6 commits into
dylan/agent-builderfrom
dylan/draft-preview
Jun 18, 2026
Merged

feat(agents): draft preview — chat against any revision before promoting (feature 29)#2744
dmarticus merged 6 commits into
dylan/agent-builderfrom
dylan/draft-preview

Conversation

@dmarticus

Copy link
Copy Markdown
Contributor

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)

  • New "Test draft" button on AgentRevisionBar (non-live, non-archived revisions) → chat?revision=<id>.
  • AgentChatPane accepts revisionId + resumeSessionId from the route; switches chatId to preview:<slug>:<rev> so draft + live coexist in the store; amber "Draft preview" banner with rev tag.
  • Chat history rail: entries tagged with a rev abc12345 pill; 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 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.

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.

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 the chatPendingApproval object), restore on error. onSettled still 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).
  • New "Clone to draft" button on non-draft revisions (ready / live / archived). On success it auto-selects the new draft so the picker lands in edit mode.
  • "Test draft" → conditional label: Test draft on drafts, plain Test on ready (the bundle's frozen, calling it draft was misleading).
  • Picker visual tweaks: state filter pills no longer squish into ovals (rounded-fullrounded-(--radius-2), leading-none, balanced padding); revision rows use items-center so the colored badge sits between the short-id + meta lines instead of hugging the top.

Open follow-ups (not blocking)

  • endpoints field 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.
  • A backend 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

  • On Approval Demo agent → Configuration → revision picker shows LIVE + DRAFT; selecting the DRAFT reveals Test draft button next to Freeze.
  • Click Test draft → lands on chat?revision=<rev> with the amber "Draft preview" banner + rev <8 chars> tag in the header.
  • Send a message → SSE stream lands; if the agent fires an approval, the inline card appears and clicking Approve clears it instantly (no spinner lag).
  • Rail entries from the draft session show the rev abc12345 pill.
  • After sending one message in draft + at least one in live, switching back to the live chat tab (rail click) drops ?revision= from the URL, banner goes gray, expander reads "Show 1 from other revision".
  • On a ready revision, the test button reads plain "Test", and the "Clone to draft" button appears next to it.
  • Click Clone to draft → picker auto-selects the freshly-minted draft.
  • Open the revision picker → state filter pills (LIVE/READY/DRAFT/ARCHIVED) are rectangular with centered text, no more ovals; row badges sit centered between short-id and meta.

🤖 Generated with Claude Code

dmarticus and others added 4 commits June 17, 2026 14:33
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>
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 61557d6.

@greptile-apps

greptile-apps Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Comments Outside Diff (1)

  1. packages/ui/src/features/agent-applications/components/AgentRevisionBar.tsx, line 999-1015 (link)

    P1 Clone-to-draft failures are silently swallowed

    cloneToDraft.mutate(...) only provides an onSuccess callback. If the mutation throws (e.g. a 4xx/5xx from POST .../revisions/new_draft/), the call-site error callback is absent and useCreateAgentDraftFromRevision has no onError handler either. The spinner stops, the picker stays on the original revision, and the user receives no feedback about why nothing happened. A toast.error(...) in onError (either in the hook or on the call site) would match the pattern used in useDecideAgentApproval.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/ui/src/features/agent-applications/components/AgentRevisionBar.tsx
    Line: 999-1015
    
    Comment:
    **Clone-to-draft failures are silently swallowed**
    
    `cloneToDraft.mutate(...)` only provides an `onSuccess` callback. If the mutation throws (e.g. a 4xx/5xx from `POST .../revisions/new_draft/`), the call-site error callback is absent and `useCreateAgentDraftFromRevision` has no `onError` handler either. The spinner stops, the picker stays on the original revision, and the user receives no feedback about why nothing happened. A `toast.error(...)` in `onError` (either in the hook or on the call site) would match the pattern used in `useDecideAgentApproval`.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
packages/ui/src/features/agent-applications/hooks/useAgentChat.ts:309-346
**Token remint failure silently ends the stream**

The outer `try { … } finally { … }` block has no `catch`. If `getPreviewToken(true)` throws (e.g. a network failure while reminting after a `preview_token_required` signal), the exception propagates straight through the `finally` — which only sets the status to `"done"` — and then becomes an unhandled promise rejection because `runStream` is called with `void`. The user sees the stream quietly stop with no error in the chat UI. The same risk applies to the initial `await getPreviewToken()` call on line 310.

Adding a `catch` around the outer block (or wrapping the `getPreviewToken` calls inside the loop in their own try/catch) and calling `store.setError(chatId, ...)` on throw would surface the failure.

### Issue 2 of 3
packages/ui/src/features/agent-applications/components/AgentRevisionBar.tsx:999-1015
**Clone-to-draft failures are silently swallowed**

`cloneToDraft.mutate(...)` only provides an `onSuccess` callback. If the mutation throws (e.g. a 4xx/5xx from `POST .../revisions/new_draft/`), the call-site error callback is absent and `useCreateAgentDraftFromRevision` has no `onError` handler either. The spinner stops, the picker stays on the original revision, and the user receives no feedback about why nothing happened. A `toast.error(...)` in `onError` (either in the hook or on the call site) would match the pattern used in `useDecideAgentApproval`.

### Issue 3 of 3
packages/ui/src/features/agent-applications/components/AgentChatSecretOverridesCard.tsx:35-37
**Stale draft values for keys that leave `missingSecrets`**

`draft` is initialised once from `missingSecrets` via a lazy `useState` initialiser, so its key-set is frozen at mount time. If the live agent's env-keys are updated while this card is open (removing a key from `missingSecrets`), the old key's value stays in `draft`. On the next save, line 138 iterates `Object.entries(draft)` and emits every non-empty value — including the now-removed key. That stale override rides on the next mint's `secret_overrides` claim, potentially shadowing the freshly-set live value for that key.

Filtering the `clean` map to only include keys still in `missingSecrets` (i.e. iterating `missingSecrets` instead of `Object.entries(draft)`) would prevent the leak.

Reviews (1): Last reviewed commit: "feat(agents): clone-to-draft + revision-..." | Re-trigger Greptile

Comment thread packages/ui/src/features/agent-applications/hooks/useAgentChat.ts
benjackwhite and others added 2 commits June 18, 2026 10:29
)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
@dmarticus dmarticus merged commit cb35ba0 into dylan/agent-builder Jun 18, 2026
19 checks passed
@dmarticus dmarticus deleted the dylan/draft-preview branch June 18, 2026 18:42
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.

2 participants