Skip to content

feat: render selected context as quote cards#4380

Open
santastabber wants to merge 1 commit into
nesquena:masterfrom
santastabber:feat/selected-reply-card
Open

feat: render selected context as quote cards#4380
santastabber wants to merge 1 commit into
nesquena:masterfrom
santastabber:feat/selected-reply-card

Conversation

@santastabber

@santastabber santastabber commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Thinking Path

  • Reply with selection should feel like composed reply context, not raw Markdown dumped into the textarea or thread.
  • A textarea cannot render rich context well, so pending selected text belongs in a quiet composer card.
  • After send, the same selected-context payload should remain readable in the thread instead of showing **Name:** / > quote syntax literally.
  • The model-facing payload stays plain text/Markdown-compatible; the browser render gets the richer visual treatment.

What Changed

  • Replaced the tiny pending selected-context chips with readable quote cards above the composer.
  • Each pending card includes:
    • a subtle accent rail,
    • editable context name,
    • clipped multi-line quote preview,
    • contextual remove control.
  • Pending card content is built with DOM nodes and textContent, avoiding user-content interpolation through innerHTML.
  • Rename is accessible by click, Enter, Space, and F2.
  • Rename inputs are labelled, length-bounded, and restore focus to the card after commit/cancel.
  • Remove buttons include the context name in their aria label.
  • The card stack is capped and scrollable so multiple selected contexts do not consume the whole composer area.
  • Card width aligns with the widened composer at large desktop breakpoints.
  • Sent user messages now render generated selected-context payloads as semantic styled blocks:
    • <figure class="sent-selection-context">
    • <figcaption> for the context label
    • <blockquote> for the selected passage
  • Normal user text/Markdown remains escaped; only the generated labelled-blockquote shape gets the styled treatment.
  • Context labels and quote text are HTML-escaped in the sent-thread renderer.
  • Updated i18n keys, changelog, and selected-text regression tests.

Why It Matters

Selected text context is easier to read, rename, remove, and trust before sending. After sending, the thread remains readable too: the user’s actual reply stays normal, and the selected context appears as a clean referenced block instead of raw Markdown syntax.

Verification

  • node --check static/ui.js
  • node --check static/messages.js
  • node --check static/i18n.js
  • git diff --check origin/master...HEAD
  • ./scripts/test.sh tests/test_issue2481_selected_text_reply.py tests/test_selected_context_user_render_runtime.py tests/test_issue2543_named_context_session_switch.py -q12 passed
  • Browser verification in isolated WebUI state:
    • pending selected text renders as a quote card above the composer;
    • textarea remains focused on the user’s own reply instead of receiving raw context text;
    • multiple cards use a bounded scrollable stack;
    • rename works by keyboard and restores focus;
    • remove/rename controls have contextual accessible labels;
    • send flush preserves the full selected text, not just the clipped preview;
    • sent user bubbles render selected context as a styled figure/blockquote block;
    • sent user bubbles do not show raw **Name:** or > quote syntax for generated selected context.
  • Independent implementation-quality review completed; follow-up fixes were applied for stack height, touch targets, rename accessibility/focus restoration, wide-screen alignment, parser/name consistency, semantic sent-thread markup, behavioral tests, and blocked-send guard ordering.
  • Independent public-upload/sterility review completed; no committed-diff upload blockers found.

Risks / Follow-ups

  • The visual preview intentionally clips long selections; the full selected text is still sent and displayed after send.
  • Existing DOM ids/function names retain “chips” terminology in a few places for compatibility with the existing named-context plumbing.
  • No backend routes, upload storage, persistent settings, or send payload schema are changed.
  • The sent-thread renderer intentionally recognizes the generated **Name:** + > quote shape in user bubbles. User content remains escaped, but manually typed text with that exact shape will also get the same visual treatment.

Contract Routing

Task type: UI/UX behavior improvement.

Touched areas:

  • static/messages.js selected-text / named-context composer behavior
  • static/ui.js sent user-message selected-context rendering
  • static/style.css composer and sent-thread selected-context styling
  • static/i18n.js selected-context labels
  • selected-text regression tests
  • CHANGELOG.md

Relevant public docs:

  • AGENTS.md
  • CONTRIBUTING.md
  • docs/CONTRACTS.md
  • docs/UIUX-GUIDE.md

Scope boundaries:

  • No backend endpoint changes.
  • No send payload/schema changes.
  • No new dependencies, build step, framework, or persistent settings.

Disclosure

AI-assisted implementation and review support was used. No secrets or private runtime state are required by this change.

@santastabber santastabber force-pushed the feat/selected-reply-card branch 2 times, most recently from cd91f2d to 0e017c7 Compare June 17, 2026 21:39
nesquena-hermes added a commit to nesquena-hermes/hermes-webui that referenced this pull request Jun 17, 2026
@santastabber santastabber force-pushed the feat/selected-reply-card branch from 0e017c7 to 45718cc Compare June 18, 2026 00:24
@santastabber santastabber marked this pull request as ready for review June 18, 2026 00:30
@greptile-apps

greptile-apps Bot commented Jun 18, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces inline selected-text chips in the composer with richer quote cards (accent rail, editable label, remove button, clipped preview) and renders the generated **Name:**\n> quote Markdown pattern as styled <figure>/<blockquote> blocks in sent user bubbles. The send queue path is also fixed to capture pending selection context via _composerTextWithPendingSelections() instead of the flushed textarea value.

  • Composer UI: _renderSelectionChips now builds cards with DOM APIs and textContent, avoiding innerHTML for user content; rename is fully keyboard-accessible with focus restoration via requestAnimationFrame.
  • Sent-thread rendering: _stashUserSelectedContextBlocks + _sentSelectionContextBlockHtml run inside _renderUserFencedBlocks, stashing labelled blockquote shapes before esc() and restoring them with HTML-escaped content as <figure class="sent-selection-context"> cards.
  • Queue path: _composerTextWithPendingSelections() builds the queued text (user input + pending blocks) without first mutating the composer; _clearComposerAfterQueuedSelectionSend() atomically clears both textarea and _pendingSelections after the text is captured.

Confidence Score: 5/5

Safe to merge — no backend, payload, or schema changes; all user content goes through DOM textContent or esc() before touching HTML; queue-path selection capture is now correct.

The send queue path now correctly captures pending selection blocks via _composerTextWithPendingSelections before clearing them, fixing the prior gap. Sent-thread rendering stashes labelled-blockquote shapes before esc() and restores them with fully escaped content; the stash tokens (null-byte-delimited) are unaffected by esc(). Composer cards are built entirely with DOM APIs and textContent — no user content reaches innerHTML. Rename accessibility (Enter/Space/F2, focus restoration, aria labels) and touch targets (44 px via coarse-pointer media query) are addressed. The runtime test file exercises XSS escaping in an actual Node.js subprocess.

No files require special attention. static/messages.js carries the most logic but the stash/flush/queue split is clean and the previous threading issue is resolved.

Important Files Changed

Filename Overview
static/messages.js Core composer logic rewritten: chips become quote cards built entirely with DOM APIs; _composerTextWithPendingSelections and _clearComposerAfterQueuedSelectionSend fix the queued-send path; _flushSelectionBlocksToComposer correctly placed after the _sendInProgress guard.
static/ui.js _stashUserSelectedContextBlocks + _sentSelectionContextBlockHtml added to _renderUserFencedBlocks; stash tokens survive esc() correctly; both label and quote content pass through esc() before insertion into HTML.
static/style.css Chips replaced with full-width card styles; composer stack is capped with max-height/overflow-y; touch targets meet 44px minimum via coarse-pointer media query; sent-thread figure styles added.
static/i18n.js Three new i18n keys (rename hint, rename aria, remove label) added consistently across all 13 locales.
tests/test_selected_context_user_render_runtime.py New runtime test file that executes the renderer logic in a real Node.js subprocess; covers XSS escaping, edge-case labels, and code-fence protection.
tests/test_issue2481_selected_text_reply.py Existing regression tests updated and expanded: covers card DOM structure, focus restoration, aria labels, queue-path helpers, and sent-thread renderer contract.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User selects text\n'Reply with selection'] --> B[_addNamedContextBlock]
    B --> C[_pendingSelections array]
    C --> D[_renderSelectionChips\nbuilds quote cards via DOM APIs]
    D --> E[Composer: card stack\naccent rail + label button + remove]

    E --> F{Send clicked}

    F -->|_sendInProgress = true\nqueue path| G[_composerTextWithPendingSelections\ncaptures text + blocks]
    G --> H[queueSessionMessage with full text]
    H --> I[_clearComposerAfterQueuedSelectionSend\nclears textarea + _pendingSelections]

    F -->|normal path| J[check empty: text + files + _pendingSelections]
    J --> K[_flushSelectionBlocksToComposer\nwrites **Name:**\\n> quote to textarea]
    K --> L[text = $msg.value.trim\nsend to model]
    L --> M[_clearPendingSelections]

    L --> N[Thread: renderUserMessage\n_renderUserFencedBlocks]
    N --> O[_stashUserSelectedContextBlocks\nstash **Name:**\\n> quote to UC token]
    O --> P[esc remaining text\nnewlines to br]
    P --> Q[restore UC tokens\nfigure.sent-selection-context\nfigcaption + blockquote via esc]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[User selects text\n'Reply with selection'] --> B[_addNamedContextBlock]
    B --> C[_pendingSelections array]
    C --> D[_renderSelectionChips\nbuilds quote cards via DOM APIs]
    D --> E[Composer: card stack\naccent rail + label button + remove]

    E --> F{Send clicked}

    F -->|_sendInProgress = true\nqueue path| G[_composerTextWithPendingSelections\ncaptures text + blocks]
    G --> H[queueSessionMessage with full text]
    H --> I[_clearComposerAfterQueuedSelectionSend\nclears textarea + _pendingSelections]

    F -->|normal path| J[check empty: text + files + _pendingSelections]
    J --> K[_flushSelectionBlocksToComposer\nwrites **Name:**\\n> quote to textarea]
    K --> L[text = $msg.value.trim\nsend to model]
    L --> M[_clearPendingSelections]

    L --> N[Thread: renderUserMessage\n_renderUserFencedBlocks]
    N --> O[_stashUserSelectedContextBlocks\nstash **Name:**\\n> quote to UC token]
    O --> P[esc remaining text\nnewlines to br]
    P --> Q[restore UC tokens\nfigure.sent-selection-context\nfigcaption + blockquote via esc]
Loading

Reviews (2): Last reviewed commit: "feat: render selected context as quote c..." | Re-trigger Greptile

Comment thread static/messages.js
Comment thread static/messages.js Outdated
@santastabber santastabber force-pushed the feat/selected-reply-card branch from 45718cc to e63929c Compare June 18, 2026 01:02
@santastabber

Copy link
Copy Markdown
Contributor Author

Updated in e63929c: pending selections are now included in the concurrent queued-send path via _composerTextWithPendingSelections(), and the queued path clears the composer/cards consistently after queueing. Also changed the pending-card tooltip to expose the full selected text instead of the clipped preview. Focused verification: node --check static/messages.js static/ui.js and ./scripts/test.sh tests/test_issue2481_selected_text_reply.py tests/test_selected_context_user_render_runtime.py tests/test_issue2543_named_context_session_switch.py -q => 13 passed.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Pulled e63929cf into a read-only worktree and read the three render-path changes against origin/master. This is clean — the queue-path fix is correct and the sent-thread renderer is XSS-safe. One low-severity heuristic note below, not a blocker.

Queue-path fix — correct

The real bug the prior round left open was that the concurrent (_sendInProgress) branch read the flushed textarea instead of the pending blocks. The new split fixes it: send() no longer flushes up front, and the queue branch now captures via _composerTextWithPendingSelections() (static/messages.js):

if (_sendInProgress) {
    const _text=_composerTextWithPendingSelections().trim();
    ...
    queueSessionMessage(_targetSid,{text:_text, ...});
    _clearComposerAfterQueuedSelectionSend();

_composerTextWithPendingSelections() builds current + blocks without mutating the composer, and _clearComposerAfterQueuedSelectionSend() clears textarea + _pendingSelections atomically after capture. The normal path flushes once (_flushSelectionBlocksToComposer() → re-read text=$('msg').value.trim()), so blocks land exactly once with no double-append. The empty-guard correctly gained !_pendingSelections.length so a selection-only send isn't dropped.

Sent-thread renderer — XSS-safe

_sentSelectionContextBlockHtml (static/ui.js:298) routes both fields through esc():

return `<figure class="sent-selection-context" data-selected-context="1"><figcaption ...>${esc(safeLabel)}</figcaption><blockquote ...>${esc(safeQuote)}</blockquote></figure>`;

and the \x00UC\x00 stash tokens are immune to esc() (which only rewrites & < > " ', static/ui.js:269), so the restore at s.replace(/\x00UC(\d+)\x00/g,...) is sound. Ordering is right too: fenced code is stashed first (static/ui.js:340, the \x00UF\x00 pass) before _stashUserSelectedContextBlocks runs, so a **X:** + blockquote inside a code fence stays literal rather than being mis-rendered as a card. Composer cards are built entirely with DOM APIs + textContent (no innerHTML for user content), and s.text only reaches HTML via title/textContent. Good.

One heuristic note (low severity, your call)

_stashUserSelectedContextBlocks keys purely on the textual shape — a label line matching /^\*\*([^\n]{1,200}):\*\*\s*$/ immediately followed by > lines:

const labelMatch=lines[i].match(/^\*\*([^\n]{1,200}):\*\*\s*$/);
...
while(j<lines.length&&/^>/.test(lines[j])){ quoteLines.push(...); j++; }

There's no provenance sentinel distinguishing "this came from Reply-with-selection" from "the user hand-typed this exact shape." So a user who manually types **Note:** on its own line followed by a > blockquote will now get the styled figure.sent-selection-context card instead of plain escaped markdown. It's purely cosmetic (still escaped, no security impact) and arguably a nice upgrade, but it's an undocumented side effect on all user messages, not just selection replies. If you want to scope it strictly to real selections, the persisted block would need a marker (e.g. a zero-width sentinel or a distinct fence) the matcher checks for — though that adds round-trip complexity. Acceptable as-is IMO; just flagging so it's a known behavior.

i18n looks complete (all three new keys present 13×, one per locale). Nice work on the keyboard a11y (Enter/Space/F2 rename + requestAnimationFrame focus restoration) and the 44px coarse-pointer touch targets. LGTM pending the maintainer's gate.

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