Skip to content

feat(ui): auto-collapse cross-message process groups, result first (PR 0001) - #166

Closed
tokg-venv wants to merge 3 commits into
deer-flow:mainfrom
tokg-venv:pr/0001-collapsible-process-groups
Closed

tokg-venv wants to merge 3 commits into
deer-flow:mainfrom
tokg-venv:pr/0001-collapsible-process-groups

Conversation

@tokg-venv

Copy link
Copy Markdown
Contributor

Summary

Implements spec 0001: after an agent run finishes, its intermediate steps (thinking + tool calls spread across consecutive messages) collapse into a single expandable header row, so the final answer leads the conversation.

Grouping rule (derived view layer, no store/runtime changes):

  • member = assistant message with no body text and (thinking and/or tool calls)
  • a group ends at a user message or at an assistant message with body text (the result)
  • groups only wrap runs that finished with a result; the live streaming message is never wrapped

Details

  • process-groups.ts: grouping rule, span detection, master switch hook (localStorage llm-space-collapse-process-groups, on by default)
  • display-messages.ts: resolveDisplayRows folds settled runs into group rows
  • process-group-header.tsx: "N tool calls · Last: " with a warning state when a step failed
  • message-list-view.tsx: virtualizer treats a collapsed group as one fixed header row; an id→display-index map keeps jump/autofocus/validation targets correct while member rows are hidden; message navigator anchors to the group's first member
  • thread-store: expandedProcessGroupIds + toggleProcessGroupExpanded (store-only, never persisted); removeMessage cleans up stale ids
  • Settings → General: "Collapse process steps" toggle (en/zh)

Test plan

  • process-groups.test.ts (grouping boundaries, expand/collapse, virtualization row mapping) — passes
  • full packages/ui suite (121 tests) and i18n tests pass
  • bun run typecheck:changed / bun run lint:changed clean

Notes

This PR is a dependency for the planned agent-layout PR (run timeline jumps reference result rows).

…R 0001)

When an agent run finishes, its intermediate steps (thinking + tool calls
across consecutive messages) now collapse into a single expandable header
row, so the final answer leads the conversation.

- process-groups.ts: grouping rule (assistant messages without body text,
  with thinking and/or tool calls), span detection, master switch hook
  (localStorage 'llm-space-collapse-process-groups', on by default)
- display-messages.ts: resolveDisplayRows folds settled runs into group
  rows; live streaming messages are never wrapped
- process-group-header.tsx: collapsed header shows 'N tool calls',
  last tool name, and a warning state when a step failed
- message-list-view.tsx: virtualizer treats a collapsed group as one
  fixed header row; id->display-index map keeps jump/autofocus/validation
  targets correct while member rows are hidden; navigator anchors to the
  group's first member
- thread-store: expandedProcessGroupIds + toggleProcessGroupExpanded
  (store-only, never persisted); removeMessage cleans up stale ids
- Settings → General: 'Collapse process steps' toggle (en/zh)
- i18n: playground-labels processGroups block (en/zh + type)

Tests: process-groups.test.ts (grouping boundaries, expand/collapse,
virtualization row mapping).

@MagicCube MagicCube 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.

Thank you for working on this! Collapsing completed process steps is a useful direction for long agent conversations, and keeping the grouping in the display layer preserves the underlying messages. I noticed three interaction cases worth adjusting before we merge; I've left examples inline to make them easier to check. Since LLM Space is also a debugging workbench, keeping unfinished steps and validation targets easy to find is especially helpful.

const readonly = readonlyFromProps || isSnapshotView;
// Groups wrap only once a run has settled: while running/preparing the
// in-flight process steps stay expanded so streaming doesn't jump.
const groupingEnabled = collapseProcessGroups && status === "idle";

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.

Could we keep completed historical groups collapsed when a new run starts? This condition disables grouping for the entire conversation whenever the thread is preparing/running, so all previously collapsed groups become full message rows and then collapse again at the end. In a long conversation that changes the list height substantially and can disturb the reading position. Keeping the current run's steps visible while preserving older groups would make this much more stable. A regression test with a completed group followed by a new run would help cover it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in c0517de. Grouping is now purely structural instead of gated on status === "idle": a new run's in-flight steps form a trailing, unterminated span so they never wrap, while every older completed group keeps its collapsed header row across preparing/running. List height and reading position no longer shift when a run starts. Added the regression test you suggested (keeps older groups collapsed while a new run is in flight in process-groups.test.ts: completed group + running=true still yields one group row).

? (displayIndexByMessageId.get(autoFocusMessageId) ?? -1)
: -1;
const validationMessageId = runValidationIssue?.messageId ?? null;
const validationMessageIndex = validationMessageId

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.

Could we also expand a group when a validation or autofocus target is inside it, then scroll to the actual message? Mapping hidden members to the header fixes the display index, but the target message is still unmounted. The existing effects therefore scroll to the header without revealing the field/error, and autofocus is consumed before its target can receive focus. Revealing the member first would preserve the existing error-fixing workflow.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c0517de. displayIndexByMessageId now maps only rendered rows, so a member hidden in a collapsed group resolves to nothing instead of its header — the effects used to scroll to the header and consume autoFocusMessageId before the target could ever mount. A new small effect expands the owning group (via findCollapsedGroupIdForMessage) and the existing scroll/autofocus effects then run against the real row on the next render, preserving the error-fixing workflow. Group ids are deduped per pass so two targets hiding in the same group don't cancel each other's toggle. Covered by the findCollapsedGroupIdForMessage tests.

// A non-member message closes the run; the group is only kept when the
// closer is a user message or an assistant message with a result body.
const isTerminator =
message.role === "user" ||

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.

Could a user message end the candidate span without making it eligible for automatic collapse? For example, if a run is interrupted after a tool call and the user sends a follow-up, this branch now groups the unfinished steps even though no final answer was produced. That seems to differ from the stated goal of leaving runs without a result visible. Keeping those steps expanded would preserve the failure context; it would be useful to cover an interrupted run followed by a user message in the tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — tightened in c0517de. A user message now only ends the candidate span; only an assistant message with a result body makes it eligible for automatic collapse, so an interrupted run's steps (tool call, no answer, user follows up) stay expanded as failure context — same treatment as a trailing run at the end of the list. Added both cases you asked for: a user message ends the span without collapsing unfinished steps and a later successful run groups without pulling in the interrupted one (the follow-up run still groups normally).

zcai7675-bot added 2 commits September 9, 2026 12:58
- Keep historical process groups collapsed while a new run is in flight:
  grouping is now purely structural instead of disabled for the whole
  conversation while preparing/running, so a long thread's list height and
  reading position no longer shift mid-run. A new run's steps stay expanded
  because they form a trailing, unterminated span.
- Reveal a collapsed group before scrolling/focusing a run-validation or
  autofocus target inside it. Hidden members no longer map to their header
  row (they map to nothing), and a small effect expands the owning group so
  the target row mounts before the scroll/focus effects run.
- A user message now only ends a candidate span instead of qualifying it:
  only an assistant message with a result body makes a group collapsible, so
  steps from an interrupted run (followed by a user message) stay expanded
  as failure context, matching the stated goal.

Tests: interrupted-run grouping, older-group stability during a run, and
findCollapsedGroupIdForMessage lookup.
@tokg-venv

Copy link
Copy Markdown
Contributor Author

This PR was auto-closed on 2026-09-13 when its head fork was deleted during a fork-network repair (the fork had been detached from upstream by an earlier ownership transfer, which silently broke PR creation; deleting and re-forking was the fix, but I failed to check for open PRs riding on that fork first — sorry, that was my miss). Re-submitted as #171 from the exact head SHA GitHub retained for this PR: the diff and review state are unchanged.

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