Skip to content

[Design] Add copy actions and show more/less for long messages - #357

Open
Julia Shilova (juliashilovaa) wants to merge 9 commits into
mainfrom
copy-and-collapse-messages
Open

[Design] Add copy actions and show more/less for long messages #357
Julia Shilova (juliashilovaa) wants to merge 9 commits into
mainfrom
copy-and-collapse-messages

Conversation

@juliashilovaa

@juliashilovaa Julia Shilova (juliashilovaa) commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add copy buttons to chat messages and collapse very long user messages so the transcript stays
scannable. Copy uses the existing icon/control pattern and gives clear copied-state feedback; behavior
is consistent across the normal user and agent message content.

Changes

  • Copy actions (apps/web/src/chat/CopyButton.tsx, turns.tsx) — a hover-revealed copy affordance
    on plain user bubbles and the agent's answer, via one shared MessageWithCopy layout: the action sits
    below the message content, bottom-left under an assistant answer and bottom-right under a
    user bubble (never an overlay on the text, so long/multiline content can't collide with it). It copies
    the full message source (userText(...) / markdown text), never a collapsed preview or any UI
    chrome. CopyButton is self-contained and presentational: navigator.clipboard.writeText + a local
    ~1.2s CopyCheck icon flip for copied-state feedback (no store toast, keeping the renderers
    props-driven). Only the round's concluding agent answer carries copy — not the intermediate
    narration between tool steps (ChatView finalAnswerRowIdsisFinalAnswer).
  • Collapse large user messages (turns.tsx, ChatView.tsx) — a plain user message over 500 chars
    shows expanded right after send and auto-collapses to a line-clamp preview + an in-card
    Show more/Show less toggle (with chevrons) once the agent starts responding; a manual expand
    survives continued streaming (rides the shared foldState cache). Short messages are unchanged. The
    transcript list gets an off-screen render buffer (increaseViewportBy) so short transcripts stay fully
    mounted now that rows carry a per-message action.
  • Reused, not reinvented: Remix icon set (RiFileCopyLine/RiCheckLine), the established
    group/opacity-0 group-hover:opacity-100 hover-reveal, and semantic typography/color tokens — no
    hardcoded colors or parallel styles.
  • Spec: apps/web/src/chat/SPEC.md documents the copy affordance + large-message collapse.
  • Tests: e2e/message-actions.spec.ts (new). One-line adjustment in e2e/history-search.spec.ts's
    seed helper (.last() on a hasText filter) — the richer rows now keep a repeated older message
    mounted, so the helper targets the latest occurrence instead of strict-matching two.

State ownership is unchanged: chat/session state stays server-owned, collapse/expand is client-side view
state, hydrate-then-stream is untouched, and closing a tab remains a view action.

Testing

  • bun run lint — pass (only pre-existing website-CSS warnings)
  • turbo run typecheck --filter=@thinkrail/web — pass
  • turbo run test --filter=@thinkrail/web — 843 passed
  • bun run e2e (full no-agent suite) — all 6 shards passed
  • Focused: bun run e2e:serial -- e2e/message-actions.spec.ts e2e/history-search.spec.ts e2e/chat-order.spec.ts — 23 passed

Screenshots

Same scenario (a user message + the agent's answer), copy actions forced visible for the still.

Before After
before after

After: user copy sits bottom-right under the bubble, the agent's answer copy bottom-left under the message.

Demo

Large user message auto-collapsed after the agent replied → Show more/Show less, then the copy
action on the user message (bottom-right) and on the agent's answer (bottom-left), each showing the
CopyCheck feedback.

copy actions demo

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — please address the blocking inline finding.

Comment thread apps/web/src/chat/ChatView.tsx Outdated
Comment thread apps/web/src/chat/CopyButton.tsx Outdated
Julia Shilova (juliashilovaa) pushed a commit that referenced this pull request Aug 31, 2026
@juliashilovaa Julia Shilova (juliashilovaa) changed the title Add copy actions [Design] Add copy actions and show more/less for long messages Aug 31, 2026

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — please address the blocking inline findings.

Comment thread apps/web/src/chat/ChatView.tsx Outdated
@danyaberezun
danyaberezun force-pushed the copy-and-collapse-messages branch from b35d5df to 62750e9 Compare September 1, 2026 20:58

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — please address the blocking inline findings.

Comment thread apps/web/src/chat/turns.tsx Outdated
@danyaberezun
danyaberezun force-pushed the copy-and-collapse-messages branch from 62750e9 to 4543521 Compare September 1, 2026 21:19

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — please address the blocking inline findings.

totalListHeightChanged={handleContentHeight}
atBottomThreshold={50}
atTopThreshold={50}
// Keep a generous off-screen render buffer so a short transcript stays fully mounted

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking:
Problem: The current head reintroduces a multi-line narrative comment after the prior comment-invariant thread was resolved, again violating the repository’s explicit near-zero-comments invariant.
Failure scenario: The implementation or chat spec can change while this parallel explanation of why the virtualization buffer exists remains stale, recreating the drift path the prior review identified.
Suggested fix: Remove the two comment lines and keep the rationale only in the owning chat spec.

Comment thread apps/web/src/chat/SPEC.md
toggle **inside the card** (within its padding, directly below the message body, so line-clamp truncates
only the body's own element — never the control) once the agent has started responding to it: `UserTurn`
folds on `useFold(`${id}:user-collapse`,
agentResponded)`, so the fallback is *expanded* until the agent responds and *collapsed* after — with

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking:
Problem: The authoritative chat spec passes agentResponded as the useFold fallback, but the implemented and described behavior requires !agentResponded because useFold returns whether the row is expanded.
Failure scenario: A maintainer following this source-of-truth contract will make answered long messages expand and unanswered messages collapse, reversing the tested workflow.
Suggested fix: Record useFold(${id}:user-collapse, !agentResponded) so the spec matches the implementation and stated behavior.

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — please address the blocking inline findings.

aria-label={label}
title={label}
onClick={() => {
void navigator.clipboard.writeText(getText()).then(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking:
Problem: This bypasses the shared copyText() degradation contract and assumes navigator.clipboard exists; the chat spec also records this direct call despite lib/SPEC.md requiring one clipboard-write path.
Failure scenario: On supported plain-HTTP remote access, navigator.clipboard is unavailable, so clicking Copy throws instead of degrading quietly and the new action cannot copy the message.
Suggested fix: Call copyText(getText()) from the @/lib barrel and set the copied state only when it returns true; update the chat spec to describe that shared behavior.

onClick={toggle}
className="mt-4 flex items-center gap-4 tr-text-metadata text-text-subtle hover:text-text-default"
>
{expanded ? (

@jetbrains-air jetbrains-air Bot Sep 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking:
Problem: Both new disclosure chevrons use size-12, contrary to the web spec's explicit rule that every expand/disclosure chevron is size-16 regardless of context.
Failure scenario: Long-message disclosure renders a 12px glyph while every conforming disclosure renders 16px, introducing a visibly undersized variant into the shared design system.
Suggested fix: Change both ChevronUp and ChevronDown classes in this toggle to size-16.

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — please address the blocking inline findings.

Julia.Shilova and others added 9 commits September 2, 2026 00:09
…messages

The agent answer's last markdown block carried the shared prose 8px bottom
margin, so its copy action sat ~10px below the text — noticeably farther than
on a user bubble. Zero only that last block's trailing margin at the agent
content wrapper (its owning layout; inter-block and leading prose margins
untouched) and make the shared MessageWithCopy gap the single source of the
message→copy spacing at 2px, so agent and user copy actions align. No negative
margins or copy-specific positioning.
Move CopyButton from a row below the message to an absolutely positioned
overlay anchored to MessageWithCopy's bottom-right corner, for both agent
and user messages, instead of below-left/below-right placement.
main's bounded passive auto-open (756a4b8) now opens a workspace's single
newest disk chat directly instead of leaving it in closed-chat history, so
openChatFromHistory's wait for the chat-history button hung forever. Match
the fix already applied to the other affected specs in that commit.
…tion

CopyButton, MessageWithCopy, and the collapse/copy-action logic in turns.tsx
and its e2e spec carried multi-line JSDoc/inline prose duplicating rationale
chat/SPEC.md already owns. Address the review finding by deleting them and
relying on the spec + names/types, per the repo's near-zero-comments rule.
userResponded and finalAnswerRowIds scanned the display-projected rows with
an O(n) inner loop per row, which is quadratic in transcript size and, under
newest-first order, scans in the wrong direction since projectRows reverses
row order within each group — misclassifying which user row got answered
and which markdown row is the round's true final answer. Derive both from
chronologicalRows in one O(n) backward pass instead, keyed by row id so
either display projection can look them up correctly.
The corner-overlay CopyButton had no reserved space, so a message whose
last line reached the right edge had the button sit on top of that text.
Give both message content wrappers pb-32 (24px hit target + inset) so the
button's footprint is always empty padding, never the message's own text.
pb-32 tripped spacingUsage.test.ts's CI-caught guard: 32/40/64 are
deliberately reserved primitives in spacing.json with no rhythm call sites
(styles/SPACING.md), never to be spent by p/m/gap utilities even though
they're valid scale steps. Swap to the canonical pb-24, sized to match
CopyButton's own size-24 exactly, and flush it to the wrapper's edge with
bottom-0 (position offsets aren't spacing-gated) so the reserved band and
the button's footprint still line up with zero overlap.
Rebasing onto main's assistant-workspace-links feature (8e0fb7d) auto-merged
cleanly but silently scoped AssistantMarkdown to only the non-final markdown
branch, since that's where the corresponding line landed after this branch
split assistant rendering into a final/non-final ternary for the copy
button. A round's single/last answer is exactly the common "final" case, so
it silently lost safe in-app link handling entirely. Render both branches
through AssistantMarkdown; the final/non-final split is only about the copy
action and is orthogonal to link safety.
@danyaberezun
danyaberezun force-pushed the copy-and-collapse-messages branch from acdc07d to a31305d Compare September 1, 2026 22:27

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — please address the blocking inline findings.

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