fix(ui): let chat review requests open the document they ask about (#20590)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - In chat, an agent can end its turn on a confirmation card that asks
the person to review a saved document
> - The full task view links the card's document, but the compact chat
card showed the document key as plain text unless it was the plan
> - A person who is asked to review a draft in chat could not open the
draft from the card
> - This pull request links a document on the same chat to its
`#document-<key>` panel, the same link a reply uses
> - The benefit is that a review request in chat lets the person read
what they are asked to approve

## Linked Issues or Issue Description

**What happened?**
In a chat eval run on master `f4337bb5fb`
(`questions-and-approvals.reject-and-revise`), the agent saved a `draft`
document and ended its turn on a confirmation card "Review research
notes introduction draft" with `target: { type: "issue_document", key:
"draft", revisionId }`. In the chat composer takeover, the card showed
`draft` as a plain chip with no link.

**Expected behavior**
A confirmation card in chat opens the document it asks the person to
review, like the full card in the task view does.

**Steps to reproduce**
Create a pending `request_confirmation` on a chat task with
`payload.target = { type: "issue_document", key: "draft", revisionId }`.
Open the chat. The takeover shows the `draft` chip without a link.

## What Changed

- `CompactTarget` in `TaskChatCompactInteractionCard.tsx` gives an
`issue_document` target on the same chat the href `#document-<key>`.
Plan targets, targets with an explicit `href`, attachments and documents
on other tasks keep their current behavior.
- Added a `TaskChatThread` test: a pending draft review request in the
takeover links `#document-draft`.

## Verification

- `npx vitest run src/components/TaskChatThread.test.tsx -t "links a
review request"` in `ui`: red before the fix, green after.
- `npx vitest run src/components/TaskChatThread.test.tsx
src/components/coordinator/CoordinatorTaskCommitment.test.tsx
src/lib/issue-thread-interactions.test.ts`: 457 passed.
- `node scripts/check-token-gates.mjs`: all gates clean.
- Paid chat eval on a temporary branch with this change: see the comment
on this PR.

## Risks

- Low risk. Plan review cards are unchanged; their plan preview already
opens the plan.
- No API, schema or stored state changes.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), agentic coding with
tool use in Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 5.5 authored and GitHub committed 2026-11-10 12:28:37 -06:00
1 parent 9816d2d25c
commit ed3ffc3bc7
2 files changed
+15 -1

No files matched your search

@@ -4218,6 +4218,14 @@ describe.each([true, false])("Unanswered question history (conversationMode=%s)"
expect(pendingIndicator()).toBeNull();
});
it("links a review request's own document so the person can open what it asks them to review", () => {
const review = { ...planReviewInteraction(), id: "draft-review", title: "Review draft", payload: { version: 1, prompt: "Review the unpublished draft.",
target: { type: "issue_document", key: "draft", revisionId: "draft-revision-1" } } } as IssueThreadInteraction;
render(<TaskChatThread {...props} comments={[]} interactions={[review]} />);
const link = takeover()?.querySelector<HTMLAnchorElement>('a[href="#document-draft"]');
expect(link?.textContent).toContain("draft");
});
it("keeps unrelated approvals and accepted plan history when a plan changes", () => {
planState.data = planDocument({ latestRevisionId: "revision-4", latestRevisionNumber: 4, updatedAt: new Date("2026-08-23T10:03:00Z") });
const review = { ...planReviewInteraction(), payload: { version: 1, prompt: "Review another task", target: { type: "issue_document", issueId: "other-task", key: "plan", revisionId: "revision-3" } } } as IssueThreadInteraction;
@@ -306,7 +306,13 @@ function CompactTarget({
if (!target || !label) return null;
const href = target.type === "issue_attachment"
? `/api/attachments/${encodeURIComponent(target.attachmentId)}/content`
: target.href ? normalizeRequestConfirmationTargetHref(target.href) : null;
: target.href ? normalizeRequestConfirmationTargetHref(target.href)
// A document on this chat opens in its document panel, like a reply's
// `#document-<key>` link, so the person can read what they are asked to review.
// Plan reviews keep their header chip; their plan preview opens the plan.
: target.type === "issue_document" && target.key !== "plan" && (target.issueId ?? interaction.issueId) === interaction.issueId
? `#document-${encodeURIComponent(target.key)}`
: null;
if (href) {
return (
<a