[Design] Add copy actions and show more/less for long messages - #357
[Design] Add copy actions and show more/less for long messages #357Julia Shilova (juliashilovaa) wants to merge 9 commits into
Conversation
b35d5df to
62750e9
Compare
62750e9 to
4543521
Compare
| totalListHeightChanged={handleContentHeight} | ||
| atBottomThreshold={50} | ||
| atTopThreshold={50} | ||
| // Keep a generous off-screen render buffer so a short transcript stays fully mounted |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| aria-label={label} | ||
| title={label} | ||
| onClick={() => { | ||
| void navigator.clipboard.writeText(getText()).then(() => { |
There was a problem hiding this comment.
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 ? ( |
There was a problem hiding this comment.
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.
…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.
acdc07d to
a31305d
Compare
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
apps/web/src/chat/CopyButton.tsx,turns.tsx) — a hover-revealed copy affordanceon plain user bubbles and the agent's answer, via one shared
MessageWithCopylayout: the action sitsbelow 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 UIchrome.
CopyButtonis self-contained and presentational:navigator.clipboard.writeText+ a local~1.2s
Copy→Checkicon flip for copied-state feedback (no store toast, keeping the renderersprops-driven). Only the round's concluding agent answer carries copy — not the intermediate
narration between tool steps (
ChatViewfinalAnswerRowIds→isFinalAnswer).turns.tsx,ChatView.tsx) — a plain user message over 500 charsshows expanded right after send and auto-collapses to a
line-clamppreview + an in-cardShow more/Show lesstoggle (with chevrons) once the agent starts responding; a manual expandsurvives continued streaming (rides the shared
foldStatecache). Short messages are unchanged. Thetranscript list gets an off-screen render buffer (
increaseViewportBy) so short transcripts stay fullymounted now that rows carry a per-message action.
RiFileCopyLine/RiCheckLine), the establishedgroup/opacity-0 group-hover:opacity-100hover-reveal, and semantic typography/color tokens — nohardcoded colors or parallel styles.
apps/web/src/chat/SPEC.mddocuments the copy affordance + large-message collapse.e2e/message-actions.spec.ts(new). One-line adjustment ine2e/history-search.spec.ts'sseed helper (
.last()on ahasTextfilter) — the richer rows now keep a repeated older messagemounted, 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— passturbo run test --filter=@thinkrail/web— 843 passedbun run e2e(full no-agent suite) — all 6 shards passedbun run e2e:serial -- e2e/message-actions.spec.ts e2e/history-search.spec.ts e2e/chat-order.spec.ts— 23 passedScreenshots
Same scenario (a user message + the agent's answer), copy actions forced visible for the still.
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 copyaction on the user message (bottom-right) and on the agent's answer (bottom-left), each showing the
Copy→Checkfeedback.