Skip to content

add copyable code snippets#8292

Open
FedeCuci wants to merge 1 commit into
janhq:mainfrom
FedeCuci:copyable-code-snippets
Open

add copyable code snippets#8292
FedeCuci wants to merge 1 commit into
janhq:mainfrom
FedeCuci:copyable-code-snippets

Conversation

@FedeCuci

@FedeCuci FedeCuci commented Jun 7, 2026

Copy link
Copy Markdown

Description

Inline code spans (`like this`) in assistant chat messages are now click-to-copy. Clicking an inline code snippet copies its contents to the clipboard and shows a brief "Copied!" badge at the cursor.

Previously, only fenced code blocks (triple-backtick) were copyable via Streamdown's built-in block copy button. Short inline snippets — like a setting name, flag, or command referenced mid-sentence — had no way to be copied other than manual text selection.

Changes

  • New CopyableInlineCode wrapper (web-app/src/components/CopyableInlineCode.tsx) that makes inline code click-to-copy and renders a transient "Copied!" badge at the click position.
  • RenderMarkdown gains an opt-in copyableInlineCode prop; when set, it wraps the rendered markdown in CopyableInlineCode. Off by default.
  • MessageItem enables the prop only on the assistant-message render path, scoping the feature to assistant messages.
  • markdown.css adds a cursor: pointer and subtle hover tint on inline code, scoped to the copyable wrapper so the affordance only appears where copying is enabled.

Implementation notes

  • Event delegation instead of a component override. Streamdown tags single-line inline code with data-streamdown="inline-code". The wrapper listens for clicks and matches that attribute, rather than overriding React-Markdown's code component. This is deliberate: overriding code would replace Streamdown's block renderer and lose syntax highlighting and the existing copy button on fenced code blocks. Fenced blocks are completely untouched.
  • No layout impact. The wrapper uses display: contents, so it adds no box to the DOM and doesn't affect markdown spacing/layout.
  • Respects text selection. If the user drags to select text inside an inline code span, the non-collapsed selection is detected and the copy does not fire — only a clean click triggers it.
  • Isolated re-renders. Badge state lives in the wrapper, so showing/hiding "Copied!" doesn't re-render the markdown body (relevant during streaming).
  • Graceful degradation. If the Clipboard API is unavailable or denied, it silently no-ops.

Testing

  • Existing RenderMarkdown test suite passes (23/23).
  • Typecheck (tsc -b) and lint (eslint) clean on changed files.
  • Manually verified: clicking inline code copies + shows the badge; fenced blocks keep their own copy button and highlighting; text selection inside inline code still works.

@tokamak-pm tokamak-pm Bot 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.

PR Review: add copyable code snippets (#8292)

Summary

Despite the title focusing on copyable code snippets, this PR bundles four distinct features into one:

  1. Click-to-copy inline code in assistant messages (new CopyableInlineCode component)
  2. Model name + provider logo labels above each assistant response
  3. SSE custom event filter to fix #8280 (OpenAI-compatible servers sending non-standard event types)
  4. Emoji-prefixed chat titles + emoji font fallback in --font-sans

Each feature is individually well-implemented, but bundling them makes the PR harder to review and bisect. Consider splitting into separate PRs for independent features (especially SSE filter vs UI changes).


Code Quality Assessment

CopyableInlineCode (CopyableInlineCode.tsx) -- Well designed.

  • Event delegation via data-streamdown="inline-code" attribute is the right call -- avoids disrupting Streamdown's fenced-code-block renderer.
  • display: contents wrapper avoids layout side effects.
  • Text-selection guard (window.getSelection().isCollapsed) is a nice touch.
  • Timer cleanup in useEffect is correct.
  • Graceful clipboard API degradation (.catch(() => {})) is appropriate.
  • Portal-based badge avoids re-rendering the markdown body.

SSE Event Filter (model-factory.ts) -- Solid and well-tested.

  • The TransformStream approach with remainder buffering handles split chunks correctly.
  • CRLF normalization is handled.
  • The flush() method processes any leftover data.
  • Test coverage (202 lines in sse-event-filter.test.ts) is thorough -- covers split chunks, CRLF, empty event types, integration with createCustomFetch.

Model labels (MessageItem.tsx) -- Functional but has a concern.

  • The useMemo dependency array is correct and complete.
  • The fallback logic (per-message metadata -> current selection for last message) is reasonable.
  • Non-null assertion modelProviderForDisplay! on line ~598 is safe (guarded by modelProviderLogo truthiness which requires modelProviderForDisplay to be truthy), but a more defensive approach would be cleaner.

Thread title emoji (thread-title-summarizer.ts) -- Clean.

  • The regex [^\p{L}\p{N}\p{Extended_Pictographic}\uFE0F\u200D\s] correctly preserves emoji sequences including ZWJ.
  • Good test coverage for emoji edge cases.

Issues & Observations

  1. Scope creep / PR title mismatch: The title says "add copyable code snippets" but ~60% of the diff is unrelated (model labels, SSE filter, emoji titles). The PR description is excellent and thorough, but the title should reflect the actual scope or, better yet, this should be multiple PRs.

  2. Commit history is messy: Commit 1 (ab6c2d2) contains model labels, SSE filter, AND emoji titles. Commit 2 is review feedback for model labels. Commit 3 is the inline code feature. Consider squashing or reorganizing before merge.

  3. $threadId.tsx mutation pattern: The code mutates messageMetadata directly:

    if (!messageMetadata.modelId && currentModelState.selectedModel) {
      messageMetadata.modelId = currentModelState.selectedModel.id
      messageMetadata.modelProvider = currentModelState.selectedProvider
    }

    This mutates the object cast from message.metadata. If message.metadata is shared or frozen upstream, this could be fragile. Consider creating a new object ({ ...messageMetadata, modelId: ... }) instead.

  4. Missing useMemo for modelProviderForDisplay and modelProviderLogo: These are computed inline on every render of MessageItem. While cheap, for consistency with the memoized modelDisplayName, wrapping them in useMemo would be more idiomatic (especially since MessageItem is wrapped in memo).

  5. No unit tests for CopyableInlineCode: The new component has no dedicated test file. The PR description mentions existing RenderMarkdown tests pass, but there are no tests for the click-to-copy behavior, badge positioning, or text-selection guard. Consider adding tests (possibly using @testing-library/user-event with a mocked navigator.clipboard).

  6. getModelDisplayName, getProviderTitle, getProviderLogo imports: The diff changes the import from @/lib/utils to include these helpers. The current main branch exports them from utils.ts, confirmed. This is fine.

  7. CSS scoping looks correct: .copyable-inline-code [data-streamdown='inline-code'] ensures the hover/pointer styles only apply within the copyable wrapper.


Minor Nits

  • The FEEDBACK_MS = 1200 constant is reasonable but could be documented as to why 1.2s was chosen (it's a UX taste call, not a bug).
  • The badge z-50 is high -- if there are other z-indexed elements (modals, dropdowns), this could conflict. Verify it doesn't overlap with other UI elements.

Recommendation: improve needed

The code quality per-feature is good, but the PR would benefit from: (a) splitting into separate PRs for independent features (SSE filter fix alone is worth its own PR), (b) adding unit tests for CopyableInlineCode, and (c) avoiding direct mutation of messageMetadata in $threadId.tsx. None of these are blocking bugs, but they improve maintainability and reviewability.

@FedeCuci FedeCuci force-pushed the copyable-code-snippets branch from fa972dd to b55bb79 Compare June 8, 2026 08:54
@FedeCuci

FedeCuci commented Jun 8, 2026

Copy link
Copy Markdown
Author

I have made a mistake in the original PR containing edits from other branches as well. I have now removed those and kept only the click-to-copy inline code feature:

  • Rebased onto a fresh main, dropping the three unrelated commits (model labels, SSE filter, emoji titles). The branch is now a single commit touching only the copyable-code files.

I also addressed the feature-specific feedback:

  • Added a dedicated test file for CopyableInlineCode (CopyableInlineCode.test.tsx) covering click-to-copy, the "Copied!" badge, the text-selection guard (drag-select doesn't trigger a copy), clicks outside inline code, and graceful degradation when the clipboard write is rejected.
  • Documented the FEEDBACK_MS = 1200 constant explaining the ~1.2s badge duration is a UX choice.

@tokamak-pm

tokamak-pm Bot commented Jun 9, 2026

Copy link
Copy Markdown

Follow-up Review — All Prior Feedback Addressed

The author has fully addressed the concerns raised in the initial review:

  • Scope creep resolved: The branch was rebased onto fresh `main`, dropping the three unrelated commits (model labels, SSE filter, emoji titles). The PR is now a single focused commit touching only the copyable-code feature (5 files).
  • Tests added: `CopyableInlineCode.test.tsx` covers click-to-copy, badge display, text-selection guard, out-of-bounds clicks, and clipboard rejection degradation.
  • `FEEDBACK_MS` documented: The constant now has a comment explaining the 1.2s UX choice.

The remaining nits from the first review (mutation of `messageMetadata`, `useMemo` for provider fields) were in the removed commits and no longer apply.

The current diff is minimal and well-scoped. Event delegation, `display: contents`, portal badge, timer cleanup, and graceful degradation are all correctly implemented.

Recommendation: can merge

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant