feat(node-ui): chat panel PR5 — My-projects picker + hover timestamps + inline streaming caret#528
Merged
Conversation
The composer's project <Select> listed every non-system context graph while the left sidebar's "My projects" only shows ones the agent created/joined. Reuse the sidebar's membership predicate so they agree. - Extract `toSidebarIdentity` from PanelLeft into the shared contextGraphSidebar lib (one source of truth; decoupled structural param so the lib stays independent of the api layer). - Add pure `computeSelectableProjects(available, identity, activeId)`: membership filter + a coherence rule that still surfaces an active project that isn't a member so the trigger never silently falls back to the placeholder. The local "hidden from sidebar" dismissal is intentionally NOT applied (hiding ≠ can't post chat there). - PanelRight container derives the list (currentAgent already loaded) and passes it to the picker; prop is optional and defaults to the full list so unrelated ConnectedAgentsTab renderers/tests are unaffected. Other availableProjects consumers (display name, context entries) keep the full list. - Note: this commit also carries the PanelRight render-path wiring for the streaming-caret change (same file; see follow-up commit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-message timestamps were always visible, adding transcript noise now that PR4 made assistant turns full-width with no bubble. Hide by default and reveal on message hover / keyboard focus-within, reusing the exact `.v10-md-copy` recipe (opacity + 120ms transition). Touch (hover:none) and prefers-reduced-motion users always see it. Colour/contrast (AA per Task #68) is unchanged — only visibility toggles. Pure CSS, no JSX. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The caret rendered as a block sibling after the whole MarkdownMessage, so it orphaned onto its own line below code/tables/lists. Inject it inline right after the last rendered text node via a small rehype tree transform, structure-independent. Chosen over the originally-planned sentinel character: a tree transform cannot leak a stray glyph into visible text if parsing splits unexpectedly. Whitespace-only hast text nodes (the `\n` inserted between block elements) and fenced-code text are skipped so the caret lands at the true end of streamed prose. PanelRight passes `streaming` through renderMessageContent (wiring committed alongside Item A — same file) and keeps an inline caret for the plaintext path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The streaming-caret skip matched `tag === 'code'`, which also matches inline code (a bare <code> with no <pre> ancestor), so a message ending in `` `inline` `` never got a caret and looked finished early. Only propagate the code-skip through `pre` (fenced/preformatted, whose text is rebuilt by CodeBlock). Adds a regression test for an inline-code-ending streaming message. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
qa-lead nit: the "no caret in fenced code" test used a `pre, code` selector which, after the inline-code fix, would also match inline <code> (which legitimately can hold the caret). Scope it to the CodeBlock container `.v10-md-pre` so the assertion stays precise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-token wait
Item C moved the streaming caret inside the last text node, so an
assistant turn that starts as `{ streaming: true, content: '' }` had
nothing to anchor the caret to and rendered a blank row until the first
token arrived (user-reported regression). Add a ChatGPT-style shimmer
"Thinking…" indicator for the empty-streaming state; it falls through to
the markdown path (whose inline caret takes over) the moment content
arrives. Pure CSS sheen with a solid `--text-secondary` fallback if
`background-clip:text` is unsupported, a static `prefers-reduced-motion`
state, and `role=status`/`aria-live` for AT. User picked the
shimmer-text option (over a brand-logo pulse) after UX/UI consultation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A streamed message whose current content has no HAST text node (e.g. ends in an image ``) found no per-text-node anchor, so the turn looked finished while still streaming. Add a trailing-caret fallback for that case. It deliberately does NOT fire for a code-only stream: `sawCodeText` distinguishes "text exists but is all code" (caret intentionally suppressed — ChatGPT parity, ux-lead-approved) from "no text node anywhere" (image-only — needs the caret). Adds two regression tests: image-ending stream gets exactly one caret; code-only stream still gets none. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rose When a streaming message was prose followed by a trailing fenced code block, the last non-code text target was the *earlier* prose, so the caret was spliced mid-message before the code block (stale position — looked like it was streaming in the wrong place). Track `trailingCode`: set when a `<pre>` is entered, reset whenever a new non-code text target is recorded. If a `<pre>` follows the last target, suppress the caret (same outcome as a code-only stream — ChatGPT parity, ux-lead-approved) instead of parking it on stale prose. Code-earlier- then-prose still correctly anchors to the trailing prose. Two regression tests added (trailing-code suppression; inverse ordering). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The stale-anchor guard only covered a trailing <pre>; the same bug applied to any non-text tail (trailing image, hr, hard <br>) — the caret stayed on earlier prose, spliced before the final block. Replace the `<pre>`-only boolean with a document-order `tail` model: 'text' → splice the inline caret at the last text node; 'code' → suppress (CodeBlock rebuilds the subtree; ChatGPT parity, ux-lead-approved); 'leaf' (img/hr/br) → append a trailing caret so the turn still reads as streaming; 'none' → nothing (empty content is the upstream "Thinking…" indicator's job). Subsumes the prior code-only / image-only / trailing-code cases. Adds regression tests: image-after- prose, hr-after-prose, mid-paragraph <br> stays inline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three deferred chat-panel follow-ups (post-#516). Per user direction, #3 assistant-turn separators and #1 Select typeahead were dropped.
<Select>listed every context graph; now it mirrors the left sidebar's membership predicate (belongsInMyProjectsSidebar). Extracted sharedtoSidebarIdentity+ new purecomputeSelectableProjectsintocontextGraphSidebar.ts. The local "hidden from sidebar" dismissal is intentionally NOT applied. An active-but-non-member project is still surfaced so the trigger never silently retargets. OtheravailableProjectsconsumers keep the full list.:focus-within, reusing the.v10-md-copyrecipe. Touch + reduced-motion always show it. AA contrast unchanged. Pure CSS.Tests
computeSelectableProjectsunit cases (membership, active-but-non-member inclusion, no-dup, null identity) incontextGraphSidebar.test.ts.markdown-message.test.ts(one caret in last text node, none when not streaming, end-of-paragraph, not inside fenced code).Test plan
🤖 Generated with Claude Code