Skip to content

feat(chat): adaptive annotation pill for the Quote action - #7336

Open
solnikhil wants to merge 1 commit into
kirodotdev:mainfrom
solnikhil:feat/quote-annotation-pill
Open

feat(chat): adaptive annotation pill for the Quote action#7336
solnikhil wants to merge 1 commit into
kirodotdev:mainfrom
solnikhil:feat/quote-annotation-pill

Conversation

@solnikhil

@solnikhil solnikhil commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Problem / Motivation

Selecting assistant text and choosing Quote injected raw > markdown
straight into the composer. That leaves the user editing a quotation as if it
were their own prose: the > prefix has no boundary, so a stray backspace
silently turns the quote into ordinary text; it carries no attribution, so
neither the reader nor the agent can tell who said it or when; and quoting a
second passage appends into the same run-on blockquote with no way to drop just
one of them.

Why it matters

Quote is the cheapest way to make a follow-up unambiguous — "this line, not the
whole answer" — so its output is exactly what the next turn is grounded in.
Today that grounding is fragile: the reference is indistinguishable from the
instruction, half-deletions are invisible, and multi-quote replies degrade into
one merged block. Users respond by re-typing the passage by hand, which
defeats the action.

What changed (motivation → approach → change)

Goal — make a quote a first-class staged object with a boundary, provenance,
and individual removal, without changing what the agent receives.

Approach — the composer is a plain <textarea> that renders text/plain
only, so true inline chips would need a mirrored overlay or a contenteditable
swap (the app's only composer — an outsized blast radius). The chip pattern
already established for staged session refs and file attachments is the right
seam, so quotes are staged above the composer and folded into the payload at
send time, exactly like appendSessionRefLinks does for session refs.

What was built — a QuoteAnnotationPill whose resting label is adaptive
rather than a bare count: at one quote it shows the role plus a ~50-char excerpt
(the common case, where "1 quote" would hide the only thing worth confirming),
and at two or more it collapses to a count plus the distinct roles. Hover opens
the disclosure transiently; click, tap, Enter and Space pin it — the path
that works on touch and with a keyboard — exposing per-row remove, clear all,
and jump to source. Escape closes and returns focus to the pill.

On the wire a quote serializes as an attributed blockquote,
> [Role · time] text — deliberately the same > form the agent already
parses, just carrying provenance, so this is a presentation upgrade with zero
agent-side change
. Code selections are wrapped in a blockquoted fence so
indentation survives. Display truncation never reaches the wire: the full text
is always sent.

Lifecycle details that took most of the care: quotes are per chat slot and
survive a slot switch; they clear atomically with the composer they annotated;
and they are merged back into their originating slot when a send is refused
or a mid-turn steer is rejected, so a rejected request never silently discards
the reference. A quote-only composer (no typed prose) is sendable.

Tests

  • website/src/test/quoteRefs.test.ts — the wire contract: multi-line prose as
    one attributed blockquote, code indentation preserved inside a blockquoted
    fence, empty collection serializes to nothing, and typed text is preserved
    byte-for-byte when no quote is staged.
  • website/src/test/QuoteAnnotationPill.test.tsx — adaptive label, per-row
    remove by stable key, clear all, jump-to-source passing the right quote, the
    Escape and second-activation dismissal contracts, and that an emptied
    collection does not retain pinned-open state.
  • website/src/utils/chatQuoteDrafts.test.ts — per-slot isolation, snapshot
    independence from later mutation, malformed-record rejection, and the
    merge-behind-newer ordering used by send recovery.
  • website/src/test/ChatInput.quoteAnnotation.test.tsx — a quote-only composer
    enables Send, and the measured strip height is added to the composer floor.
  • website/src/test/ChatPageMoreCoverage.test.tsx — staging instead of composer
    injection, stacking, per-slot ownership across switches, quote-only send then
    clear, restore on a rejected transport, steer serialization and consumption,
    and merge-behind-newer on a rejected steer.

Three pre-existing specs asserted toHaveBeenCalledWith(text, rect) against
the SelectionToolbar callback this PR widens to carry an optional source
descriptor. They now read mock.calls so they are arity-tolerant, rather than
the component contorting to preserve the old arity.

Manual verification

Driven in a real browser against the dashboard, and it caught a defect no unit
test could: the popover was originally an absolute bottom-full child of
ChatInput's input-wrapper, which owns overflow-hidden for its rounded
frame and drag-resize. The panel opens above the strip, so it was clipped to
zero visible pixels while remaining present and focusable — every assertion
passed because jsdom performs no layout. Measured popover.top = 632 against
wrapper.top = 724 with overflow: hidden to confirm it, then moved the
popover into a createPortal with a viewport-anchored fixed position (the
pattern SelectionToolbar already uses here), re-measured on open, resize and
scroll, and re-armed the hover handlers on the portaled node so the pointer
crossing into it no longer dismisses it. Re-captured to confirm it renders.

The composer value was read directly at each step, which is the sharpest
statement of the change: on main it ends up holding
"> Once it is exhausted … \n\n", and on this branch it is "" with the quote
staged as a pill instead.

Screenshots / video

Before — Quote injects raw > markdown as editable text, no attribution:

Before: the composer holds raw quoted markdown as editable text

After — the quote is staged as an adaptive pill; the composer stays empty:

After: the quote is staged as an adaptive pill above an empty composer

Pinned popover — row attribution (jump to source), quote text, per-row remove

The pinned popover showing row attribution, the quoted text and a per-row remove control

Related Issues

Fixes #6983

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title (feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable)
  • No secrets, credentials, or internal references in the diff

@solnikhil
solnikhil requested review from a team and a lite review from Copilot August 31, 2026 18:51
@solnikhil
solnikhil requested a review from a team as a code owner August 31, 2026 18:51
@solnikhil
solnikhil requested a review from patrigao August 31, 2026 18:51

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added merge conflict Branch has merge conflicts with its base — author must resolve before merge fork Pull request from a fork (external contributor) readiness: checking Automated validation is still running labels Aug 31, 2026
@bolichen97 bolichen97 added needs-pr-triage PR scanner: awaiting automated triage drive-to-green PR claimed by drive-to-green pipeline and removed needs-pr-triage PR scanner: awaiting automated triage labels Sep 3, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator

🤖 Kiro Crew [operator: bolichen97#66809557]: This PR has been inactive for 7+ days with a merge conflict against main. I've assessed the blockers and they appear resolvable — I'll push fixes directly to this branch as a co-author.

Assessment: The branch is behind main and conflicts in three files: website/src/pages/ChatPage.tsx and website/src/pages/chat/AssistantMessage.tsx (base drift where main touched the same composer/Quote-action seam this PR extends — resolve by taking main's structure + this PR's quote-pill logic) and website/src/i18n/locales/en-XA.json (pseudo-locale, regenerated deterministically from en.manual.json). All other 29 files auto-merge. Plan: rebase onto origin/main, resolve the two .tsx conflicts guided by the PR intent, regenerate en-XA, run the frontend + backend gates, force-push with lease, and drive readiness to green. No design/scope changes.

If you'd prefer I don't touch this PR, add the pr-no-autofix label.

Quote injected raw '> markdown' straight into the composer. That text had no boundary the user could not half-delete, carried no attribution, and merged several quotes into one run-on blockquote with no way to drop just one.

Stage each selection as a structured quote instead, drawn as an adaptive pill above the composer: role plus excerpt at one quote, count plus distinct roles beyond that. Click pins a popover offering per-row remove, clear all, and jump to source; Escape closes it and returns focus to the pill.

Quotes are per chat slot, survive a slot switch, and serialize on the wire as attributed blockquotes ('> [Role - time] text'), so the agent contract is unchanged. They clear atomically with the composer they annotated and are restored into their originating slot when a send is refused or steering is rejected. A quote-only composer is sendable.

Rebased onto main and reconciled by Kiro Crew (operator bolichen97): merge conflicts in ChatPage.tsx, AssistantMessage.tsx resolved keeping main's structure; en-XA.json regenerated; an unused eslint-disable directive removed. Original feature authored by Nikhil Solanki.

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
@bolichen97
bolichen97 force-pushed the feat/quote-annotation-pill branch from b8c6aa4 to 0268caa Compare September 4, 2026 00:43
@bolichen97

Copy link
Copy Markdown
Collaborator

🤖 Kiro Crew [operator: bolichen97#66809557]: Rebased this branch onto current main and pushed the reconciled head 0268caafe (force-with-lease, previous head b8c6aa4d). Changes made:

  1. Rebase onto main (was ~230 commits behind, 3 conflicting files):
    • website/src/pages/ChatPage.tsx — kept main's structure and re-applied this PR's feature logic on top:
      • imports: kept main's refactored pinnedPrompt import (nextPinnedPromptState/PinnedPromptState/pinHidesRow; main removed promptPreview/promptImages/promptBody) and added this PR's quoteRefs + SelectionSource imports.
      • steerMutation: kept main's shape — the slot param name and the new onSuccess optimistic-steer receipt handler (Stranded thinking chip when a steer races chat_done onto the new-turn path #6075) — and layered this PR's quote-restore onError logic and the quotes: QuoteRef[] field on top (the PR's originSlot rename was dropped in favor of main's slot to minimize divergence; logic is identical).
      • <AssistantMessage> render site: kept main's new props (onSessionOpen/sessions/activeSession/prevUserText) and added this PR's quoteSourceTime + messageMid.
    • website/src/pages/chat/AssistantMessage.tsx — merged the component signature: main's new props (incl. forkMessageId) plus this PR's quoteSourceTime, messageMid, and the source?: SelectionSource parameter on onQuote/onAsk.
    • website/src/i18n/locales/en-XA.json — regenerated deterministically from the merged en.manual.json via node scripts/gen-pseudolocale.mjs (12439 keys), not hand-merged.
  2. Removed one unused eslint-disable directive the PR introduced (ChatPage.tsx react-hooks/exhaustive-deps above the quote-draft persist effect) — eslint reports the rule has no complaint there (refs are not required deps), and it would have added a warning to the eslint ratchet.
  3. Re-pinned the PR body's screenshot URLs to the new head SHA (images unchanged; the old pinned commit is now orphaned by the force-push).

No design, approach, or scope changes — the feature is exactly as @solnikhil authored it. Local gates before push: tsc -b clean, all 8 diff-affected test files pass (66 tests), eslint clean on changed files, i18n key checks OK.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: checking Automated validation is still running labels Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5, fork) — ✅ PASS

Design-level review of 0268caafe6c8ea72c4b3a445f1461d2e25485a27 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: PASS

Right seam (the established staged-chip pattern), unchanged wire contract, and failure/slot lifecycle mirroring the session-ref machinery — a sound, proportionate design.

Suggestions

  • This PR removes FlyingQuote's only consumer; delete the component, its test, and the ChatPageW3Coverage/ChatPageMoreCoverage mocks here rather than leaving an orphan (website/src/components/FlyingQuote.tsx).

[DESIGN-REVIEWED] 0268caa

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5, fork) — 🟡 CONCERNS

Premise-level review of 0268caafe6c8ea72c4b3a445f1461d2e25485a27 via the fork AI-review pipeline — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All evidence gathered. The one raw->-injection sibling (SideChat.tsx:481) and the orphaned FlyingQuote module are confirmed against the base tree; temp-screenshots/ is an established convention (791 files) and the draft store is genuine reuse. Final review:

First-Principles-Verdict: CONCERNS

Every core item earns its place, but the change orphans FlyingQuote.tsx without deleting it and leaves SideChat injecting the exact raw > markdown it condemns.

What this change ships

Intent: make a quoted selection a bounded, attributed, individually removable object instead of raw editable markdown — an ADDITION (declared feat, replacing prior behavior).

  1. Quote stages a removable pill above the composer instead of injecting > text — justified (feat(chat): adaptive annotation pill for the Quote action #6983's named harms)
  2. One quote shows role + excerpt; several collapse to count + roles — justified
  3. Pinned popover with per-row remove, clear all, jump to source — justified
  4. Wire quotes gain [Role · time] tags and now PREPEND before typed text (was appended after) — order flip undeclared
  5. Code selections wrapped in a blockquoted fence — justified
  6. Quotes persist per slot and restore on refused send/steer — justified; reuses createSlotDraftStore (9 sibling stores)
  7. Quote-only composer is sendable — justified
  8. Mid-turn steer serializes and consumes staged quotes — justified
  9. Flying-quote transit animation removed — rides along, undeclared, and its module is left dead
  10. SelectionToolbar callbacks widened with an optional source descriptor — justified; every field consumed

Watch

  • The named root cause — a composer seeded with boundary-less > lines — has exactly 1 unfixed sibling: grepping map(line => `> ${line}`) finds 2 composer-seeding sites; ChatPage.tsx:5904 is fixed, website/src/pages/chat/SideChat.tsx:481 (the Ask seed) still ships the old form. Accepted-and-deferred is fine, but the description's "zero agent-side change" framing never says the fix is partial.
  • The prepend-vs-append flip (item 4) changes what every existing quote user sends; the description declares the attribution tag but not the reordering.

Subtractions

  • Delete website/src/components/FlyingQuote.tsx, website/src/test/FlyingQuote.test.tsx, and the stale mock at website/src/test/ChatPageW3Coverage.test.tsx:141 — this PR removes the component's only production consumer (ChatPage.tsx), leaving 0.
  • Drop the export on sanitizeQuoteRefs (chatQuoteDrafts.ts) and QUOTE_EXCERPT_MAX (quoteRefs.ts) — 0 consumers outside their defining files, tests included.

[FIRST-PRINCIPLES-REVIEWED] 0268caa

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5, fork) — 🟡 CONCERNS

UX-level review of 0268caafe6c8ea72c4b3a445f1461d2e25485a27 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All evidence is gathered. The feature itself is well-built (staged pill matches the existing strip pattern, keyboard/a11y contract is solid, quote-only sends and failure restores are handled), but two error paths added by this diff carry real UX risk. Final review:

UX-Verdict: CONCERNS

Solid staged-quote flow, but a failed jump speaks in the Pins feature's voice, and a failed steer restores only half the user's message.

Watch

  • Quote jump failure wears pin wording. onJumpToQuoteSource={(q) => handleJumpToPin(q.ts || '', q.mid)} reuses origin 'pin', so when the quoted message isn't in loaded history the banner says "This pinned message is no longer available in the loaded history" — a user who never pinned anything is told about a feature they didn't use, right after clicking "Jump to the quoted message". Low frequency × comprehension failure × every failed jump. Fix: add a 'quote' origin to PendingJumpOrigin with its own string (the single-writer jumpUnavailableNotice exists for exactly this).
  • Failed steer restores the quote but not the instruction. steerMutation's new onError re-stages quotes, while the typed text cleared at steer time stays discarded — the pill silently reappears holding context with no instruction, so a re-send delivers a bare quote or forces the user to retype what they said. Rare × half-lost work × per incident. Fix: restore the text alongside the quotes (mirror the send-failure mergeRecoveredDraft path), or neither.

Suggestions

  • bn/hi strings use informal imperatives (সরাও, हटाओ, দেখো) while the adjacent sessionRefStrip strings in the same composer strip use formal register (সরান, हटाएँ) — align the new quoteAnnotationPill entries to the sibling register.

[UX-REVIEWED] 0268caa

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review (fork) — ✅ no blocking findings

Reviewed 0268caafe6c8ea72c4b3a445f1461d2e25485a27 via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.

The sole candidate — a staged-quote key collision from key: q-${Date.now()}-${prev.length}`` — is not reachable in practice. handleQuote is invoked only through a human gesture on `SelectionToolbar` (select text → toolbar appears → click Quote); a collision needs stage-A, stage-B, remove-A, and stage-C all inside a single `Date.now()` millisecond, which no sequence of human selection-and-click actions can produce, and there is no batch/loop caller. (a) has no concrete input that occurs in practice, so the candidate fails Step 1.

[OPUS-REVIEWED] 0268caa

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review (fork) — 🔴 changes requested (blocking)

Reviewed 0268caafe6c8ea72c4b3a445f1461d2e25485a27 via the fork AI-review pipeline; updated in place on each push.

BLOCKING -- website/src/utils/quoteRefs.ts:35 -- Fixed fence corrupts quoted Markdown examples
const CODE_FENCE = '```'
Code containing a triple-backtick line -> serialization closes the wrapper early -> subsequent code loses its fenced structure.
Anchor: residual/crash-data-loss-corruption
Fix: Size the wrapper fence longer than the longest backtick run in q.text.
[BLOCK-MERGE] 0268caa
[GPT-REVIEWED] 0268caa

@bolichen97

Copy link
Copy Markdown
Collaborator

Open PR relationship audit

This is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion.

Relationship findings

  • PR #7255 is OVERLAPPING relative to this PR. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #7255: REBASE. 7336 threads a fifth draft store through exactly the ref/effect cluster 7255 relocates; the two diffs cannot both apply to ChatPage.tsx. Files: website/src/pages/ChatPage.tsx.

No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit.

@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

drive-to-green PR claimed by drive-to-green pipeline fork Pull request from a fork (external contributor) merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: action required A blocking check or review needs attention

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(chat): adaptive annotation pill for the Quote action

3 participants