Skip to content

feat(chat): collapsible turn sections, auto-folded on a new turn#40

Merged
dnviti merged 1 commit into
chore/version-bump-5.1.2from
feat/issue-34-collapsible-turns
Jul 26, 2026
Merged

feat(chat): collapsible turn sections, auto-folded on a new turn#40
dnviti merged 1 commit into
chore/version-bump-5.1.2from
feat/issue-34-collapsible-turns

Conversation

@dnviti

@dnviti dnviti commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Summary

Test plan

  • npm test (1324 passing)
  • typecheck clean
  • Manual browser check: toggle a turn, expand-all/collapse-all buttons, turn-rail jump still scrolls to the right turn

A long conversation used to be one unbroken wall of fully-expanded turns.
Each turn's strip now discloses/hides its own body; only the newest turn
opens by default, a turn the user has explicitly opened or closed stays
that way across new turns, and jumping to a turn via the rail or search
force-opens it. Expand-all/collapse-all lives in the turn index header.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 26, 2026 14:32

Copilot AI 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.

Pull request overview

Implements per-turn collapsing/expanding in the chat transcript, wiring fold state through ChatView into the transcript (MessageList) and navigation (TurnIndex) so older turns auto-fold when a new turn starts, while preserving per-turn actions and deep-link/jump behavior.

Changes:

  • Add a disclosure control to TurnStrip and render each turn body as a hideable unit in MessageList.
  • Introduce fold-state management in ChatView (default: only newest open; user overrides persist) and “expand all / collapse all” hooks for TurnIndex.
  • Add unit/integration tests covering fold defaults and isTurnOpen behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/chat-view.test.js Adds integration coverage for folded/expanded rendering and accessibility attributes.
test/chat-turns.test.js Adds unit coverage for the new isTurnOpen default/override behavior.
src/client/shell/chat/TurnStrip.tsx Adds per-turn disclosure button and collapsed-header summary content.
src/client/shell/chat/TurnIndex.tsx Uses shared status glyphs and adds optional expand/collapse-all controls.
src/client/shell/chat/MessageList.tsx Switches to turn-based rendering and hides/shows a whole turn body as a unit.
src/client/shell/chat/ChatView.tsx Owns fold state, opens turns on index/search jumps, and wires expand/collapse-all.
src/client/chat/turns.ts Centralizes STATUS_GLYPH and adds isTurnOpen helper for fold defaults/overrides.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +105 to +116
{/* Icon-rail width has no room for these — the row is already tight
around the count at 44px. */}
{!collapsed && (onExpandAll || onCollapseAll) ? (
<span style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{onExpandAll ? (
<FoldAllButton label="Expand every turn" icon="maximize-2" onClick={onExpandAll} />
) : null}
{onCollapseAll ? (
<FoldAllButton label="Collapse every turn" icon="fold-vertical" onClick={onCollapseAll} />
) : null}
</span>
) : null}
Comment on lines +81 to +101
<button
type="button"
aria-expanded={open}
aria-controls={bodyId}
aria-label={`${open ? 'Collapse' : 'Expand'} turn ${turn.index}`}
title={open ? 'Collapse this turn' : 'Expand this turn'}
onClick={onToggleOpen}
style={{
flex: '0 0 auto',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 16,
height: 16,
padding: 0,
margin: '0 -2px 0 -4px',
background: 'transparent',
border: 0,
color: 'var(--muted-foreground)',
cursor: 'pointer',
}}
@dnviti
dnviti merged commit 2d0a6e3 into chore/version-bump-5.1.2 Jul 26, 2026
5 checks passed
@dnviti
dnviti deleted the feat/issue-34-collapsible-turns branch July 26, 2026 15:21
dnviti added a commit that referenced this pull request Jul 26, 2026
…l /clear (v5.1.2) (#37)

* chore: bump version to 5.1.2

* feat(chat): let users override a conversation's model, independent of profile default (#39)

Adds a per-conversation model override so any chat session can switch or
type a custom model at any time, regardless of whether the underlying
runtime already supports it. Persisted on the session (mirrors the
chatBypassPermissions pattern), applied via a three-tier fallback
(live adapter.setModel -> best-effort /model slash command -> saved for
next launch), and always beats the runtime profile's default for that
conversation only. Composer's model chip becomes an always-enabled
combobox with free-text entry and honest per-outcome feedback, replacing
the old dead-end disabled state.

Closes #38

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* feat(chat): collapsible turn sections, auto-folded on a new turn (#34) (#40)

A long conversation used to be one unbroken wall of fully-expanded turns.
Each turn's strip now discloses/hides its own body; only the newest turn
opens by default, a turn the user has explicitly opened or closed stays
that way across new turns, and jumping to a turn via the rail or search
force-opens it. Expand-all/collapse-all lives in the turn index header.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* fix(chat): show Claude's slash commands from the moment a session opens (#41)

Claude's CLI does not report `slash_commands` until it has processed a
first turn's `system/init`, which only arrives after the first message
is written to its stdin. The chat session spawns the process as soon as
a chat is opened, well before any message is sent, so the command menu
and its composer button stayed empty — looking broken — until a
throwaway message unlocked them.

The adapter now advertises a static baseline built from this app's own
table of Claude's built-in commands as soon as it starts, so the menu
and button are populated immediately. The real `init` list — including
any project or plugin commands — still arrives with the first turn and
replaces this baseline outright, exactly as before.

ACP runtimes (kimi, omp) already report commands during their handshake,
before any message is sent, so they were never affected. codex, grok and
pi have no command support at all; the composer already hides the button
entirely rather than showing an empty menu, which is the honest behavior
for a runtime that offers none.

Closes #30

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* fix(chat): make /clear and /new actually reset the conversation (#47)

`/clear` and `/new` forwarded their text to the still-running agent
process like any other message, then only added a cosmetic marker
that hid prior messages in the UI. The next message sent it right
back into the same process, so the agent kept whatever context it
already had — the reset never really happened, only its display did.

Route clearing commands through the same restart path a manual
"start fresh" relaunch already uses: stop the live adapter and start
a new one with no resume id, so the next turn talks to a process that
was never handed the prior conversation. This also fixes the id
persisted for later reconnects, so a rejoin can't resurrect it either.

Fixes #43

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* fix(chat): show the answer as it streams, and keep a model override across /clear (#49)

Five defects introduced by the four PRs already merged onto the 5.1.2 branch.
Each one's own tests passed; all but one lived where two of them met.

The chat stopped rendering. MessageList looked messages up in a map memoised
on the messages array, but the transcript appends to that array in place, so
its identity never changes: the map was built once at mount and every message
after it resolved to nothing. The strips above kept ticking while the bodies
stayed empty, and reloading "fixed" it because reloading replaces the array.
Keyed on the transcript version instead, which is the cadence the neighbouring
memo already used — the list still does not re-render per token.

A model override no longer survives /clear. The restart replays the options
the session was launched with, and the model is the one thing in them that can
change while the session is alive, so the conversation quietly went back to
the model it opened with after the browser had been told the switch applied.
The choice now reaches those options too, by either door: the picker, and a
/model typed straight into the composer, which was forwarded untouched and hit
the same reversion.

A collapsed turn's title escaped its bar. It set neither a size nor nowrap
while every sibling in that fixed-height row sets both, so it inherited the
page default and wrapped — and without nowrap the ellipsis was inert.

An override could not be undone. The server had a "cleared" path, message and
all, that nothing in the UI could reach: the text field refuses to submit
empty and every listed model carries a name. A typo therefore stayed in force
for every later launch of that conversation.

Model names are now capped and stripped of control characters, which would
otherwise ride into the best-effort /model turn as extra lines.

The browser checks gated nothing: npm test runs only the unit suite, and the
runner exited 0 when Chrome was absent. With the rendering bug restored, npm
test passes 1357/1357 while the browser check fails. They now run in CI and
refuse to skip there. Their fixture also grew a second turn, without which no
turn is ever collapsed and the broken title was never rendered at all.

Every fix has a test confirmed to fail against the unfixed code.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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