feat(ui): group reasoning and tool calls into collapsible Processing blocks - #1320

Merged
Astro-Han merged 7 commits into
mainfrom
feat/1307-processing-collapse
Jul 22, 2026
Merged

feat(ui): group reasoning and tool calls into collapsible Processing blocks#1320
Astro-Han merged 7 commits into
mainfrom
feat/1307-processing-collapse

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes#1307.

During longer tasks, every reasoning step and tool call rendered as its own timeline item, pushing the actual answer far down the conversation. Now each maximal run of thinking + tool activity between two answer texts folds into one collapsed "Processing" block; answer text always renders in place, and the expanded panel replays the full original timeline (the same 深度思考 disclosures and tool trows, nothing is lost).

Behavior decisions, where they differ from the issue's suggested behavior:

  • Answer text is the grouping boundary, so a turn can hold several Processing blocks (e.g. reasoning above a short interim answer, tools below it) instead of one section per turn. This keeps every user-facing text visible in chronological place.
  • Failures do not force the block open, deviating from the issue's "keep failures visible": recovered tool errors are not actionable, and the collapsed summary line already carries the failure signal (red icon + 「N 个失败」count), consistent with the existing tool-trow design locked by tool-trow-summary.test.ts. Only waiting_permission forces the block open; permission prompts themselves live outside the timeline and are unaffected.
  • While running, the block stays collapsed and its summary line shows the current activity live (「正在读取 foo.ts…」, plus the failed count as soon as a tool errors), then settles into the tool-activity roll-up (「读取 15 个文件,搜索 8 次」). Folded reasoning is not counted in the summary — thinking is not tool activity — and a pure-thinking run (no tools) stays a bare 深度思考 disclosure instead of folding into a Processing block. Manual expand/collapse sticks, per the existing disclosure-collapsible-contract (no defaultOpen).

The timeline model stays flat: the fold is a render-layer derivation (packages/ui/src/timeline-fold.ts, applied via useMemo in TurnView), so timeline-rewriting passes (overlayLiveTurn, projectTurnTools, shell-run folding) never maintain a nesting invariant — an invalid "tools-less Processing block" is unrepresentable by construction. Each block's React key derives from its preceding answer-text boundary, so projection removing a block's first tool cannot remount the disclosure or drop a manual toggle. Rendering reuses the existing Collapsible primitive, useToolDisclosure state machine, and SETTLE_FADE; no new CSS.

Verification

  • @maka/ui tests 223/223, @maka/desktop tests 2761/2761 (both re-run independently of the implementation pass), npm run lint, npm run format:check, workspace typecheck + build — all green.
  • An external adversarial review round (Codex + Kimi K3, independent) drove the final shape: their confirmed findings (a projection edge that could leave an empty Processing block, content-derived key instability, live-summary gaps) were resolved by moving the fold out of the timeline model rather than patching each site; regression tests for all three now lock the behavior.
  • Unit coverage: fold rules and block-id stability in timeline-fold.test.ts (pure thinking stays bare, interleaved runs, text-boundary multi-block, id survives first-tool projection); shell-run-fold regression in materialize.test.ts (a turn whose only tool is projected away yields a flat ['thinking'] timeline); render-level waiting_permission force-open in processing-block.test.tsx; live-summary cases in tool-trow-summary.test.ts (failed-count while running, last-live-activity pick, connector-tool localization).
  • New Processing story in chat-surface.stories.tsx (storybook contract updated) covers the collapsed block with a failed tool ahead of the answer text.
  • Before/after evidence below: both states captured from the live app (Playwright e2e fixture mode, fake backend, identical injected session on main vs this branch), composed with ImageMagick only.

Before / after (live app)

image

Expanded state — the same block manually opened, replaying the full timeline:

image

Review focus

  • packages/ui/src/timeline-fold.ts — the single place the fold rule lives; materialize.ts differs from main only by a doc comment stating the model stays flat.
  • trow-summary.ts now owns resolveToolDisplayName/isConnectorTool (moved from presentation.ts, which re-exports them) so the live Processing summary shares the localized name fallback without an import cycle.
  • streaming-handoff.test.ts asserts the folded render; materialize-turns.test.ts and live-turn-projection.test.ts are restored byte-identical to main.

…ing blocks
Group each maximal run of thinking + tool-call timeline entries between two
assistant answer texts into one collapsed "Processing" block (#1307). Answer
text stays a grouping boundary and always renders in place, so a turn can hold
several Processing blocks; the expanded block replays the full timeline (the
same 深度思考 disclosures and tool trows, nested one indent in).
- materialize: add a `processing` TurnTimelineItem kind and group in a shared
finalize pass used by both the settled (buildTurnTimeline) and live-overlay
paths, so streaming and replayed history fold identically; projectTurnTools
descends into blocks while preserving turn identity.
- trow-summary/copy: summarizeProcessing / isProcessingRunning /
processingNeedsAttention — reasoning count + tool buckets + red failed count,
live current-activity line, waiting_permission force-open (errors stay
collapsed), reusing the existing trow summary + disclosure state machine.
- chat-turn: ProcessingBlock renders via the shared disclosure/SETTLE_FADE
seams, collapsed by default with sticky manual toggle.
Show a collapsed "Processing" block (#1307): two think-then-call steps with a
failing tool fold into one summary (思考 2 次 + tool counts + a red failed
count) ahead of the assistant answer. Enumerate it in the chat-surface
Storybook contract.
…inking runs bare
Review feedback on #1307:
- The collapsed summary line now rolls up tool activity only (「读取 1 个文件,
搜索 1 次,1 个失败」) — folded reasoning stays inside the block but is no
longer counted; the settled `summary.thinking` copy is removed (zh/en). The
live `thinkingActivity` fallback (「正在深度思考」 when the block's tools are
done and thinking still streams) stays.
- A run between two answer texts folds into a Processing block only when it
contains at least one tools group; a pure-thinking run renders as the bare
深度思考 disclosure (including the live streaming path). groupProcessing
applies the rule identically on the settled and live-overlay paths.
… render layer
External review (Codex + Kimi) adjudication on #1307: baking the 'processing'
kind into the shared TurnTimelineItem model forced every timeline-rewriting
pass (overlayLiveTurn's flatten/refold, projectTurnTools' descent) to maintain
the nesting invariant, and projectTurnTools missed it — shell-run folding could
strip a block's every tool and strand an illegal thinking-only Processing block
with an empty summary (P1).
- materialize.ts reverts to main's flat TurnTimelineItem model; the
processing kind, its child types, groupProcessing/flattenProcessing/
finalizeTimeline, overlayLiveTurn's flatten→refold, and projectTurnTools'
processing branch are all removed.
- New pure module timeline-fold.ts derives the folded view at render time:
foldTimeline keeps answer text in place as the boundary, folds a maximal
thinking+tools run into one block only when it contains a tools group, and
gives each block a stable id from the preceding text's messageId ('start'
at turn head) — so a block's React key survives its first tool being
projected away without remounting or dropping a manual toggle (P2).
- TurnView folds via useMemo(foldTimeline); ProcessingBlock consumes fold
children and loses its dead onStreamingSettled prop; TurnTimelineEntry,
timelineEntryKey, and the live-content check return to their flat forms.
- Tests: materialize-turns.test.ts and live-turn-projection.test.ts restore
their original stronger assertions (flatten helpers removed); the grouping
cases move to timeline-fold.test.ts with a block-id stability case; new
P1 regression (shell-run fold leaves a flat thinking-only timeline) and a
render-level waiting_permission force-open test.
…y pick, and localization
External review (Codex + Kimi) adjudication on #1307, live-summary findings:
- The live line now appends the failed clause whenever the block already holds
an errored tool (「正在运行测试,1 个失败」) — errored tools stay collapsed,
so the summary must carry the failure signal before settle, matching the
trow (P2).
- The current activity is the LAST live entry in timeline order: children are
walked in reverse and a still-streaming thinking block or a group's active
tool wins, instead of flattening all tools and letting an earlier running
tool outrank a later streaming reasoning block (P3).
- The no-intent/no-displayName fallback routes through resolveToolDisplayName,
so a bare load_tools call reads as the localized 「加载工具组」 (P3). The
resolver and isConnectorTool move down into trow-summary.ts (the leaf
module) to avoid an import cycle; presentation.ts re-exports them for its
existing consumers.
…collapse
# Conflicts:
#	packages/ui/src/chat-turn.tsx
@Astro-Han
Astro-Han merged commit 17cc81d into mainJul 22, 2026
3 checks passed
@Astro-Han
Astro-Han deleted the feat/1307-processing-collapse branch July 22, 2026 11:36
Sign up for freeto 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.

feat(ui): group reasoning and tool calls into a collapsible section

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(ui): group reasoning and tool calls into collapsible Processing blocks - #1320

Merged
Astro-Han merged 7 commits into
mainfrom
feat/1307-processing-collapse
Jul 22, 2026
Merged

feat(ui): group reasoning and tool calls into collapsible Processing blocks#1320
Astro-Han merged 7 commits into
mainfrom
feat/1307-processing-collapse

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes#1307.

During longer tasks, every reasoning step and tool call rendered as its own timeline item, pushing the actual answer far down the conversation. Now each maximal run of thinking + tool activity between two answer texts folds into one collapsed "Processing" block; answer text always renders in place, and the expanded panel replays the full original timeline (the same 深度思考 disclosures and tool trows, nothing is lost).

Behavior decisions, where they differ from the issue's suggested behavior:

  • Answer text is the grouping boundary, so a turn can hold several Processing blocks (e.g. reasoning above a short interim answer, tools below it) instead of one section per turn. This keeps every user-facing text visible in chronological place.
  • Failures do not force the block open, deviating from the issue's "keep failures visible": recovered tool errors are not actionable, and the collapsed summary line already carries the failure signal (red icon + 「N 个失败」count), consistent with the existing tool-trow design locked by tool-trow-summary.test.ts. Only waiting_permission forces the block open; permission prompts themselves live outside the timeline and are unaffected.
  • While running, the block stays collapsed and its summary line shows the current activity live (「正在读取 foo.ts…」, plus the failed count as soon as a tool errors), then settles into the tool-activity roll-up (「读取 15 个文件,搜索 8 次」). Folded reasoning is not counted in the summary — thinking is not tool activity — and a pure-thinking run (no tools) stays a bare 深度思考 disclosure instead of folding into a Processing block. Manual expand/collapse sticks, per the existing disclosure-collapsible-contract (no defaultOpen).

The timeline model stays flat: the fold is a render-layer derivation (packages/ui/src/timeline-fold.ts, applied via useMemo in TurnView), so timeline-rewriting passes (overlayLiveTurn, projectTurnTools, shell-run folding) never maintain a nesting invariant — an invalid "tools-less Processing block" is unrepresentable by construction. Each block's React key derives from its preceding answer-text boundary, so projection removing a block's first tool cannot remount the disclosure or drop a manual toggle. Rendering reuses the existing Collapsible primitive, useToolDisclosure state machine, and SETTLE_FADE; no new CSS.

Verification

  • @maka/ui tests 223/223, @maka/desktop tests 2761/2761 (both re-run independently of the implementation pass), npm run lint, npm run format:check, workspace typecheck + build — all green.
  • An external adversarial review round (Codex + Kimi K3, independent) drove the final shape: their confirmed findings (a projection edge that could leave an empty Processing block, content-derived key instability, live-summary gaps) were resolved by moving the fold out of the timeline model rather than patching each site; regression tests for all three now lock the behavior.
  • Unit coverage: fold rules and block-id stability in timeline-fold.test.ts (pure thinking stays bare, interleaved runs, text-boundary multi-block, id survives first-tool projection); shell-run-fold regression in materialize.test.ts (a turn whose only tool is projected away yields a flat ['thinking'] timeline); render-level waiting_permission force-open in processing-block.test.tsx; live-summary cases in tool-trow-summary.test.ts (failed-count while running, last-live-activity pick, connector-tool localization).
  • New Processing story in chat-surface.stories.tsx (storybook contract updated) covers the collapsed block with a failed tool ahead of the answer text.
  • Before/after evidence below: both states captured from the live app (Playwright e2e fixture mode, fake backend, identical injected session on main vs this branch), composed with ImageMagick only.

Before / after (live app)

image

Expanded state — the same block manually opened, replaying the full timeline:

image

Review focus

  • packages/ui/src/timeline-fold.ts — the single place the fold rule lives; materialize.ts differs from main only by a doc comment stating the model stays flat.
  • trow-summary.ts now owns resolveToolDisplayName/isConnectorTool (moved from presentation.ts, which re-exports them) so the live Processing summary shares the localized name fallback without an import cycle.
  • streaming-handoff.test.ts asserts the folded render; materialize-turns.test.ts and live-turn-projection.test.ts are restored byte-identical to main.

…ing blocks
Group each maximal run of thinking + tool-call timeline entries between two
assistant answer texts into one collapsed "Processing" block (#1307). Answer
text stays a grouping boundary and always renders in place, so a turn can hold
several Processing blocks; the expanded block replays the full timeline (the
same 深度思考 disclosures and tool trows, nested one indent in).
- materialize: add a `processing` TurnTimelineItem kind and group in a shared
finalize pass used by both the settled (buildTurnTimeline) and live-overlay
paths, so streaming and replayed history fold identically; projectTurnTools
descends into blocks while preserving turn identity.
- trow-summary/copy: summarizeProcessing / isProcessingRunning /
processingNeedsAttention — reasoning count + tool buckets + red failed count,
live current-activity line, waiting_permission force-open (errors stay
collapsed), reusing the existing trow summary + disclosure state machine.
- chat-turn: ProcessingBlock renders via the shared disclosure/SETTLE_FADE
seams, collapsed by default with sticky manual toggle.
Show a collapsed "Processing" block (#1307): two think-then-call steps with a
failing tool fold into one summary (思考 2 次 + tool counts + a red failed
count) ahead of the assistant answer. Enumerate it in the chat-surface
Storybook contract.
…inking runs bare
Review feedback on #1307:
- The collapsed summary line now rolls up tool activity only (「读取 1 个文件,
搜索 1 次,1 个失败」) — folded reasoning stays inside the block but is no
longer counted; the settled `summary.thinking` copy is removed (zh/en). The
live `thinkingActivity` fallback (「正在深度思考」 when the block's tools are
done and thinking still streams) stays.
- A run between two answer texts folds into a Processing block only when it
contains at least one tools group; a pure-thinking run renders as the bare
深度思考 disclosure (including the live streaming path). groupProcessing
applies the rule identically on the settled and live-overlay paths.
… render layer
External review (Codex + Kimi) adjudication on #1307: baking the 'processing'
kind into the shared TurnTimelineItem model forced every timeline-rewriting
pass (overlayLiveTurn's flatten/refold, projectTurnTools' descent) to maintain
the nesting invariant, and projectTurnTools missed it — shell-run folding could
strip a block's every tool and strand an illegal thinking-only Processing block
with an empty summary (P1).
- materialize.ts reverts to main's flat TurnTimelineItem model; the
processing kind, its child types, groupProcessing/flattenProcessing/
finalizeTimeline, overlayLiveTurn's flatten→refold, and projectTurnTools'
processing branch are all removed.
- New pure module timeline-fold.ts derives the folded view at render time:
foldTimeline keeps answer text in place as the boundary, folds a maximal
thinking+tools run into one block only when it contains a tools group, and
gives each block a stable id from the preceding text's messageId ('start'
at turn head) — so a block's React key survives its first tool being
projected away without remounting or dropping a manual toggle (P2).
- TurnView folds via useMemo(foldTimeline); ProcessingBlock consumes fold
children and loses its dead onStreamingSettled prop; TurnTimelineEntry,
timelineEntryKey, and the live-content check return to their flat forms.
- Tests: materialize-turns.test.ts and live-turn-projection.test.ts restore
their original stronger assertions (flatten helpers removed); the grouping
cases move to timeline-fold.test.ts with a block-id stability case; new
P1 regression (shell-run fold leaves a flat thinking-only timeline) and a
render-level waiting_permission force-open test.
…y pick, and localization
External review (Codex + Kimi) adjudication on #1307, live-summary findings:
- The live line now appends the failed clause whenever the block already holds
an errored tool (「正在运行测试,1 个失败」) — errored tools stay collapsed,
so the summary must carry the failure signal before settle, matching the
trow (P2).
- The current activity is the LAST live entry in timeline order: children are
walked in reverse and a still-streaming thinking block or a group's active
tool wins, instead of flattening all tools and letting an earlier running
tool outrank a later streaming reasoning block (P3).
- The no-intent/no-displayName fallback routes through resolveToolDisplayName,
so a bare load_tools call reads as the localized 「加载工具组」 (P3). The
resolver and isConnectorTool move down into trow-summary.ts (the leaf
module) to avoid an import cycle; presentation.ts re-exports them for its
existing consumers.
…collapse
# Conflicts:
#	packages/ui/src/chat-turn.tsx
@Astro-Han
Astro-Han merged commit 17cc81d into mainJul 22, 2026
3 checks passed
@Astro-Han
Astro-Han deleted the feat/1307-processing-collapse branch July 22, 2026 11:36
Sign up for freeto 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.

feat(ui): group reasoning and tool calls into a collapsible section

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(ui): group reasoning and tool calls into collapsible Processing blocks - #1320

Merged
Astro-Han merged 7 commits into
mainfrom
feat/1307-processing-collapse
Jul 22, 2026
Merged

feat(ui): group reasoning and tool calls into collapsible Processing blocks#1320
Astro-Han merged 7 commits into
mainfrom
feat/1307-processing-collapse

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes#1307.

During longer tasks, every reasoning step and tool call rendered as its own timeline item, pushing the actual answer far down the conversation. Now each maximal run of thinking + tool activity between two answer texts folds into one collapsed "Processing" block; answer text always renders in place, and the expanded panel replays the full original timeline (the same 深度思考 disclosures and tool trows, nothing is lost).

Behavior decisions, where they differ from the issue's suggested behavior:

  • Answer text is the grouping boundary, so a turn can hold several Processing blocks (e.g. reasoning above a short interim answer, tools below it) instead of one section per turn. This keeps every user-facing text visible in chronological place.
  • Failures do not force the block open, deviating from the issue's "keep failures visible": recovered tool errors are not actionable, and the collapsed summary line already carries the failure signal (red icon + 「N 个失败」count), consistent with the existing tool-trow design locked by tool-trow-summary.test.ts. Only waiting_permission forces the block open; permission prompts themselves live outside the timeline and are unaffected.
  • While running, the block stays collapsed and its summary line shows the current activity live (「正在读取 foo.ts…」, plus the failed count as soon as a tool errors), then settles into the tool-activity roll-up (「读取 15 个文件,搜索 8 次」). Folded reasoning is not counted in the summary — thinking is not tool activity — and a pure-thinking run (no tools) stays a bare 深度思考 disclosure instead of folding into a Processing block. Manual expand/collapse sticks, per the existing disclosure-collapsible-contract (no defaultOpen).

The timeline model stays flat: the fold is a render-layer derivation (packages/ui/src/timeline-fold.ts, applied via useMemo in TurnView), so timeline-rewriting passes (overlayLiveTurn, projectTurnTools, shell-run folding) never maintain a nesting invariant — an invalid "tools-less Processing block" is unrepresentable by construction. Each block's React key derives from its preceding answer-text boundary, so projection removing a block's first tool cannot remount the disclosure or drop a manual toggle. Rendering reuses the existing Collapsible primitive, useToolDisclosure state machine, and SETTLE_FADE; no new CSS.

Verification

  • @maka/ui tests 223/223, @maka/desktop tests 2761/2761 (both re-run independently of the implementation pass), npm run lint, npm run format:check, workspace typecheck + build — all green.
  • An external adversarial review round (Codex + Kimi K3, independent) drove the final shape: their confirmed findings (a projection edge that could leave an empty Processing block, content-derived key instability, live-summary gaps) were resolved by moving the fold out of the timeline model rather than patching each site; regression tests for all three now lock the behavior.
  • Unit coverage: fold rules and block-id stability in timeline-fold.test.ts (pure thinking stays bare, interleaved runs, text-boundary multi-block, id survives first-tool projection); shell-run-fold regression in materialize.test.ts (a turn whose only tool is projected away yields a flat ['thinking'] timeline); render-level waiting_permission force-open in processing-block.test.tsx; live-summary cases in tool-trow-summary.test.ts (failed-count while running, last-live-activity pick, connector-tool localization).
  • New Processing story in chat-surface.stories.tsx (storybook contract updated) covers the collapsed block with a failed tool ahead of the answer text.
  • Before/after evidence below: both states captured from the live app (Playwright e2e fixture mode, fake backend, identical injected session on main vs this branch), composed with ImageMagick only.

Before / after (live app)

image

Expanded state — the same block manually opened, replaying the full timeline:

image

Review focus

  • packages/ui/src/timeline-fold.ts — the single place the fold rule lives; materialize.ts differs from main only by a doc comment stating the model stays flat.
  • trow-summary.ts now owns resolveToolDisplayName/isConnectorTool (moved from presentation.ts, which re-exports them) so the live Processing summary shares the localized name fallback without an import cycle.
  • streaming-handoff.test.ts asserts the folded render; materialize-turns.test.ts and live-turn-projection.test.ts are restored byte-identical to main.

…ing blocks
Group each maximal run of thinking + tool-call timeline entries between two
assistant answer texts into one collapsed "Processing" block (#1307). Answer
text stays a grouping boundary and always renders in place, so a turn can hold
several Processing blocks; the expanded block replays the full timeline (the
same 深度思考 disclosures and tool trows, nested one indent in).
- materialize: add a `processing` TurnTimelineItem kind and group in a shared
finalize pass used by both the settled (buildTurnTimeline) and live-overlay
paths, so streaming and replayed history fold identically; projectTurnTools
descends into blocks while preserving turn identity.
- trow-summary/copy: summarizeProcessing / isProcessingRunning /
processingNeedsAttention — reasoning count + tool buckets + red failed count,
live current-activity line, waiting_permission force-open (errors stay
collapsed), reusing the existing trow summary + disclosure state machine.
- chat-turn: ProcessingBlock renders via the shared disclosure/SETTLE_FADE
seams, collapsed by default with sticky manual toggle.
Show a collapsed "Processing" block (#1307): two think-then-call steps with a
failing tool fold into one summary (思考 2 次 + tool counts + a red failed
count) ahead of the assistant answer. Enumerate it in the chat-surface
Storybook contract.
…inking runs bare
Review feedback on #1307:
- The collapsed summary line now rolls up tool activity only (「读取 1 个文件,
搜索 1 次,1 个失败」) — folded reasoning stays inside the block but is no
longer counted; the settled `summary.thinking` copy is removed (zh/en). The
live `thinkingActivity` fallback (「正在深度思考」 when the block's tools are
done and thinking still streams) stays.
- A run between two answer texts folds into a Processing block only when it
contains at least one tools group; a pure-thinking run renders as the bare
深度思考 disclosure (including the live streaming path). groupProcessing
applies the rule identically on the settled and live-overlay paths.
… render layer
External review (Codex + Kimi) adjudication on #1307: baking the 'processing'
kind into the shared TurnTimelineItem model forced every timeline-rewriting
pass (overlayLiveTurn's flatten/refold, projectTurnTools' descent) to maintain
the nesting invariant, and projectTurnTools missed it — shell-run folding could
strip a block's every tool and strand an illegal thinking-only Processing block
with an empty summary (P1).
- materialize.ts reverts to main's flat TurnTimelineItem model; the
processing kind, its child types, groupProcessing/flattenProcessing/
finalizeTimeline, overlayLiveTurn's flatten→refold, and projectTurnTools'
processing branch are all removed.
- New pure module timeline-fold.ts derives the folded view at render time:
foldTimeline keeps answer text in place as the boundary, folds a maximal
thinking+tools run into one block only when it contains a tools group, and
gives each block a stable id from the preceding text's messageId ('start'
at turn head) — so a block's React key survives its first tool being
projected away without remounting or dropping a manual toggle (P2).
- TurnView folds via useMemo(foldTimeline); ProcessingBlock consumes fold
children and loses its dead onStreamingSettled prop; TurnTimelineEntry,
timelineEntryKey, and the live-content check return to their flat forms.
- Tests: materialize-turns.test.ts and live-turn-projection.test.ts restore
their original stronger assertions (flatten helpers removed); the grouping
cases move to timeline-fold.test.ts with a block-id stability case; new
P1 regression (shell-run fold leaves a flat thinking-only timeline) and a
render-level waiting_permission force-open test.
…y pick, and localization
External review (Codex + Kimi) adjudication on #1307, live-summary findings:
- The live line now appends the failed clause whenever the block already holds
an errored tool (「正在运行测试,1 个失败」) — errored tools stay collapsed,
so the summary must carry the failure signal before settle, matching the
trow (P2).
- The current activity is the LAST live entry in timeline order: children are
walked in reverse and a still-streaming thinking block or a group's active
tool wins, instead of flattening all tools and letting an earlier running
tool outrank a later streaming reasoning block (P3).
- The no-intent/no-displayName fallback routes through resolveToolDisplayName,
so a bare load_tools call reads as the localized 「加载工具组」 (P3). The
resolver and isConnectorTool move down into trow-summary.ts (the leaf
module) to avoid an import cycle; presentation.ts re-exports them for its
existing consumers.
…collapse
# Conflicts:
#	packages/ui/src/chat-turn.tsx
@Astro-Han
Astro-Han merged commit 17cc81d into mainJul 22, 2026
3 checks passed
@Astro-Han
Astro-Han deleted the feat/1307-processing-collapse branch July 22, 2026 11:36
Sign up for freeto 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.

feat(ui): group reasoning and tool calls into a collapsible section

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(ui): group reasoning and tool calls into collapsible Processing blocks - #1320

Merged
Astro-Han merged 7 commits into
mainfrom
feat/1307-processing-collapse
Jul 22, 2026
Merged

feat(ui): group reasoning and tool calls into collapsible Processing blocks#1320
Astro-Han merged 7 commits into
mainfrom
feat/1307-processing-collapse

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes#1307.

During longer tasks, every reasoning step and tool call rendered as its own timeline item, pushing the actual answer far down the conversation. Now each maximal run of thinking + tool activity between two answer texts folds into one collapsed "Processing" block; answer text always renders in place, and the expanded panel replays the full original timeline (the same 深度思考 disclosures and tool trows, nothing is lost).

Behavior decisions, where they differ from the issue's suggested behavior:

  • Answer text is the grouping boundary, so a turn can hold several Processing blocks (e.g. reasoning above a short interim answer, tools below it) instead of one section per turn. This keeps every user-facing text visible in chronological place.
  • Failures do not force the block open, deviating from the issue's "keep failures visible": recovered tool errors are not actionable, and the collapsed summary line already carries the failure signal (red icon + 「N 个失败」count), consistent with the existing tool-trow design locked by tool-trow-summary.test.ts. Only waiting_permission forces the block open; permission prompts themselves live outside the timeline and are unaffected.
  • While running, the block stays collapsed and its summary line shows the current activity live (「正在读取 foo.ts…」, plus the failed count as soon as a tool errors), then settles into the tool-activity roll-up (「读取 15 个文件,搜索 8 次」). Folded reasoning is not counted in the summary — thinking is not tool activity — and a pure-thinking run (no tools) stays a bare 深度思考 disclosure instead of folding into a Processing block. Manual expand/collapse sticks, per the existing disclosure-collapsible-contract (no defaultOpen).

The timeline model stays flat: the fold is a render-layer derivation (packages/ui/src/timeline-fold.ts, applied via useMemo in TurnView), so timeline-rewriting passes (overlayLiveTurn, projectTurnTools, shell-run folding) never maintain a nesting invariant — an invalid "tools-less Processing block" is unrepresentable by construction. Each block's React key derives from its preceding answer-text boundary, so projection removing a block's first tool cannot remount the disclosure or drop a manual toggle. Rendering reuses the existing Collapsible primitive, useToolDisclosure state machine, and SETTLE_FADE; no new CSS.

Verification

  • @maka/ui tests 223/223, @maka/desktop tests 2761/2761 (both re-run independently of the implementation pass), npm run lint, npm run format:check, workspace typecheck + build — all green.
  • An external adversarial review round (Codex + Kimi K3, independent) drove the final shape: their confirmed findings (a projection edge that could leave an empty Processing block, content-derived key instability, live-summary gaps) were resolved by moving the fold out of the timeline model rather than patching each site; regression tests for all three now lock the behavior.
  • Unit coverage: fold rules and block-id stability in timeline-fold.test.ts (pure thinking stays bare, interleaved runs, text-boundary multi-block, id survives first-tool projection); shell-run-fold regression in materialize.test.ts (a turn whose only tool is projected away yields a flat ['thinking'] timeline); render-level waiting_permission force-open in processing-block.test.tsx; live-summary cases in tool-trow-summary.test.ts (failed-count while running, last-live-activity pick, connector-tool localization).
  • New Processing story in chat-surface.stories.tsx (storybook contract updated) covers the collapsed block with a failed tool ahead of the answer text.
  • Before/after evidence below: both states captured from the live app (Playwright e2e fixture mode, fake backend, identical injected session on main vs this branch), composed with ImageMagick only.

Before / after (live app)

image

Expanded state — the same block manually opened, replaying the full timeline:

image

Review focus

  • packages/ui/src/timeline-fold.ts — the single place the fold rule lives; materialize.ts differs from main only by a doc comment stating the model stays flat.
  • trow-summary.ts now owns resolveToolDisplayName/isConnectorTool (moved from presentation.ts, which re-exports them) so the live Processing summary shares the localized name fallback without an import cycle.
  • streaming-handoff.test.ts asserts the folded render; materialize-turns.test.ts and live-turn-projection.test.ts are restored byte-identical to main.

…ing blocks
Group each maximal run of thinking + tool-call timeline entries between two
assistant answer texts into one collapsed "Processing" block (#1307). Answer
text stays a grouping boundary and always renders in place, so a turn can hold
several Processing blocks; the expanded block replays the full timeline (the
same 深度思考 disclosures and tool trows, nested one indent in).
- materialize: add a `processing` TurnTimelineItem kind and group in a shared
finalize pass used by both the settled (buildTurnTimeline) and live-overlay
paths, so streaming and replayed history fold identically; projectTurnTools
descends into blocks while preserving turn identity.
- trow-summary/copy: summarizeProcessing / isProcessingRunning /
processingNeedsAttention — reasoning count + tool buckets + red failed count,
live current-activity line, waiting_permission force-open (errors stay
collapsed), reusing the existing trow summary + disclosure state machine.
- chat-turn: ProcessingBlock renders via the shared disclosure/SETTLE_FADE
seams, collapsed by default with sticky manual toggle.
Show a collapsed "Processing" block (#1307): two think-then-call steps with a
failing tool fold into one summary (思考 2 次 + tool counts + a red failed
count) ahead of the assistant answer. Enumerate it in the chat-surface
Storybook contract.
…inking runs bare
Review feedback on #1307:
- The collapsed summary line now rolls up tool activity only (「读取 1 个文件,
搜索 1 次,1 个失败」) — folded reasoning stays inside the block but is no
longer counted; the settled `summary.thinking` copy is removed (zh/en). The
live `thinkingActivity` fallback (「正在深度思考」 when the block's tools are
done and thinking still streams) stays.
- A run between two answer texts folds into a Processing block only when it
contains at least one tools group; a pure-thinking run renders as the bare
深度思考 disclosure (including the live streaming path). groupProcessing
applies the rule identically on the settled and live-overlay paths.
… render layer
External review (Codex + Kimi) adjudication on #1307: baking the 'processing'
kind into the shared TurnTimelineItem model forced every timeline-rewriting
pass (overlayLiveTurn's flatten/refold, projectTurnTools' descent) to maintain
the nesting invariant, and projectTurnTools missed it — shell-run folding could
strip a block's every tool and strand an illegal thinking-only Processing block
with an empty summary (P1).
- materialize.ts reverts to main's flat TurnTimelineItem model; the
processing kind, its child types, groupProcessing/flattenProcessing/
finalizeTimeline, overlayLiveTurn's flatten→refold, and projectTurnTools'
processing branch are all removed.
- New pure module timeline-fold.ts derives the folded view at render time:
foldTimeline keeps answer text in place as the boundary, folds a maximal
thinking+tools run into one block only when it contains a tools group, and
gives each block a stable id from the preceding text's messageId ('start'
at turn head) — so a block's React key survives its first tool being
projected away without remounting or dropping a manual toggle (P2).
- TurnView folds via useMemo(foldTimeline); ProcessingBlock consumes fold
children and loses its dead onStreamingSettled prop; TurnTimelineEntry,
timelineEntryKey, and the live-content check return to their flat forms.
- Tests: materialize-turns.test.ts and live-turn-projection.test.ts restore
their original stronger assertions (flatten helpers removed); the grouping
cases move to timeline-fold.test.ts with a block-id stability case; new
P1 regression (shell-run fold leaves a flat thinking-only timeline) and a
render-level waiting_permission force-open test.
…y pick, and localization
External review (Codex + Kimi) adjudication on #1307, live-summary findings:
- The live line now appends the failed clause whenever the block already holds
an errored tool (「正在运行测试,1 个失败」) — errored tools stay collapsed,
so the summary must carry the failure signal before settle, matching the
trow (P2).
- The current activity is the LAST live entry in timeline order: children are
walked in reverse and a still-streaming thinking block or a group's active
tool wins, instead of flattening all tools and letting an earlier running
tool outrank a later streaming reasoning block (P3).
- The no-intent/no-displayName fallback routes through resolveToolDisplayName,
so a bare load_tools call reads as the localized 「加载工具组」 (P3). The
resolver and isConnectorTool move down into trow-summary.ts (the leaf
module) to avoid an import cycle; presentation.ts re-exports them for its
existing consumers.
…collapse
# Conflicts:
#	packages/ui/src/chat-turn.tsx
@Astro-Han
Astro-Han merged commit 17cc81d into mainJul 22, 2026
3 checks passed
@Astro-Han
Astro-Han deleted the feat/1307-processing-collapse branch July 22, 2026 11:36
Sign up for freeto 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.

feat(ui): group reasoning and tool calls into a collapsible section

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(ui): group reasoning and tool calls into collapsible Processing blocks - #1320

Merged
Astro-Han merged 7 commits into
mainfrom
feat/1307-processing-collapse
Jul 22, 2026
Merged

feat(ui): group reasoning and tool calls into collapsible Processing blocks#1320
Astro-Han merged 7 commits into
mainfrom
feat/1307-processing-collapse

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes#1307.

During longer tasks, every reasoning step and tool call rendered as its own timeline item, pushing the actual answer far down the conversation. Now each maximal run of thinking + tool activity between two answer texts folds into one collapsed "Processing" block; answer text always renders in place, and the expanded panel replays the full original timeline (the same 深度思考 disclosures and tool trows, nothing is lost).

Behavior decisions, where they differ from the issue's suggested behavior:

  • Answer text is the grouping boundary, so a turn can hold several Processing blocks (e.g. reasoning above a short interim answer, tools below it) instead of one section per turn. This keeps every user-facing text visible in chronological place.
  • Failures do not force the block open, deviating from the issue's "keep failures visible": recovered tool errors are not actionable, and the collapsed summary line already carries the failure signal (red icon + 「N 个失败」count), consistent with the existing tool-trow design locked by tool-trow-summary.test.ts. Only waiting_permission forces the block open; permission prompts themselves live outside the timeline and are unaffected.
  • While running, the block stays collapsed and its summary line shows the current activity live (「正在读取 foo.ts…」, plus the failed count as soon as a tool errors), then settles into the tool-activity roll-up (「读取 15 个文件,搜索 8 次」). Folded reasoning is not counted in the summary — thinking is not tool activity — and a pure-thinking run (no tools) stays a bare 深度思考 disclosure instead of folding into a Processing block. Manual expand/collapse sticks, per the existing disclosure-collapsible-contract (no defaultOpen).

The timeline model stays flat: the fold is a render-layer derivation (packages/ui/src/timeline-fold.ts, applied via useMemo in TurnView), so timeline-rewriting passes (overlayLiveTurn, projectTurnTools, shell-run folding) never maintain a nesting invariant — an invalid "tools-less Processing block" is unrepresentable by construction. Each block's React key derives from its preceding answer-text boundary, so projection removing a block's first tool cannot remount the disclosure or drop a manual toggle. Rendering reuses the existing Collapsible primitive, useToolDisclosure state machine, and SETTLE_FADE; no new CSS.

Verification

  • @maka/ui tests 223/223, @maka/desktop tests 2761/2761 (both re-run independently of the implementation pass), npm run lint, npm run format:check, workspace typecheck + build — all green.
  • An external adversarial review round (Codex + Kimi K3, independent) drove the final shape: their confirmed findings (a projection edge that could leave an empty Processing block, content-derived key instability, live-summary gaps) were resolved by moving the fold out of the timeline model rather than patching each site; regression tests for all three now lock the behavior.
  • Unit coverage: fold rules and block-id stability in timeline-fold.test.ts (pure thinking stays bare, interleaved runs, text-boundary multi-block, id survives first-tool projection); shell-run-fold regression in materialize.test.ts (a turn whose only tool is projected away yields a flat ['thinking'] timeline); render-level waiting_permission force-open in processing-block.test.tsx; live-summary cases in tool-trow-summary.test.ts (failed-count while running, last-live-activity pick, connector-tool localization).
  • New Processing story in chat-surface.stories.tsx (storybook contract updated) covers the collapsed block with a failed tool ahead of the answer text.
  • Before/after evidence below: both states captured from the live app (Playwright e2e fixture mode, fake backend, identical injected session on main vs this branch), composed with ImageMagick only.

Before / after (live app)

image

Expanded state — the same block manually opened, replaying the full timeline:

image

Review focus

  • packages/ui/src/timeline-fold.ts — the single place the fold rule lives; materialize.ts differs from main only by a doc comment stating the model stays flat.
  • trow-summary.ts now owns resolveToolDisplayName/isConnectorTool (moved from presentation.ts, which re-exports them) so the live Processing summary shares the localized name fallback without an import cycle.
  • streaming-handoff.test.ts asserts the folded render; materialize-turns.test.ts and live-turn-projection.test.ts are restored byte-identical to main.

…ing blocks
Group each maximal run of thinking + tool-call timeline entries between two
assistant answer texts into one collapsed "Processing" block (#1307). Answer
text stays a grouping boundary and always renders in place, so a turn can hold
several Processing blocks; the expanded block replays the full timeline (the
same 深度思考 disclosures and tool trows, nested one indent in).
- materialize: add a `processing` TurnTimelineItem kind and group in a shared
finalize pass used by both the settled (buildTurnTimeline) and live-overlay
paths, so streaming and replayed history fold identically; projectTurnTools
descends into blocks while preserving turn identity.
- trow-summary/copy: summarizeProcessing / isProcessingRunning /
processingNeedsAttention — reasoning count + tool buckets + red failed count,
live current-activity line, waiting_permission force-open (errors stay
collapsed), reusing the existing trow summary + disclosure state machine.
- chat-turn: ProcessingBlock renders via the shared disclosure/SETTLE_FADE
seams, collapsed by default with sticky manual toggle.
Show a collapsed "Processing" block (#1307): two think-then-call steps with a
failing tool fold into one summary (思考 2 次 + tool counts + a red failed
count) ahead of the assistant answer. Enumerate it in the chat-surface
Storybook contract.
…inking runs bare
Review feedback on #1307:
- The collapsed summary line now rolls up tool activity only (「读取 1 个文件,
搜索 1 次,1 个失败」) — folded reasoning stays inside the block but is no
longer counted; the settled `summary.thinking` copy is removed (zh/en). The
live `thinkingActivity` fallback (「正在深度思考」 when the block's tools are
done and thinking still streams) stays.
- A run between two answer texts folds into a Processing block only when it
contains at least one tools group; a pure-thinking run renders as the bare
深度思考 disclosure (including the live streaming path). groupProcessing
applies the rule identically on the settled and live-overlay paths.
… render layer
External review (Codex + Kimi) adjudication on #1307: baking the 'processing'
kind into the shared TurnTimelineItem model forced every timeline-rewriting
pass (overlayLiveTurn's flatten/refold, projectTurnTools' descent) to maintain
the nesting invariant, and projectTurnTools missed it — shell-run folding could
strip a block's every tool and strand an illegal thinking-only Processing block
with an empty summary (P1).
- materialize.ts reverts to main's flat TurnTimelineItem model; the
processing kind, its child types, groupProcessing/flattenProcessing/
finalizeTimeline, overlayLiveTurn's flatten→refold, and projectTurnTools'
processing branch are all removed.
- New pure module timeline-fold.ts derives the folded view at render time:
foldTimeline keeps answer text in place as the boundary, folds a maximal
thinking+tools run into one block only when it contains a tools group, and
gives each block a stable id from the preceding text's messageId ('start'
at turn head) — so a block's React key survives its first tool being
projected away without remounting or dropping a manual toggle (P2).
- TurnView folds via useMemo(foldTimeline); ProcessingBlock consumes fold
children and loses its dead onStreamingSettled prop; TurnTimelineEntry,
timelineEntryKey, and the live-content check return to their flat forms.
- Tests: materialize-turns.test.ts and live-turn-projection.test.ts restore
their original stronger assertions (flatten helpers removed); the grouping
cases move to timeline-fold.test.ts with a block-id stability case; new
P1 regression (shell-run fold leaves a flat thinking-only timeline) and a
render-level waiting_permission force-open test.
…y pick, and localization
External review (Codex + Kimi) adjudication on #1307, live-summary findings:
- The live line now appends the failed clause whenever the block already holds
an errored tool (「正在运行测试,1 个失败」) — errored tools stay collapsed,
so the summary must carry the failure signal before settle, matching the
trow (P2).
- The current activity is the LAST live entry in timeline order: children are
walked in reverse and a still-streaming thinking block or a group's active
tool wins, instead of flattening all tools and letting an earlier running
tool outrank a later streaming reasoning block (P3).
- The no-intent/no-displayName fallback routes through resolveToolDisplayName,
so a bare load_tools call reads as the localized 「加载工具组」 (P3). The
resolver and isConnectorTool move down into trow-summary.ts (the leaf
module) to avoid an import cycle; presentation.ts re-exports them for its
existing consumers.
…collapse
# Conflicts:
#	packages/ui/src/chat-turn.tsx
@Astro-Han
Astro-Han merged commit 17cc81d into mainJul 22, 2026
3 checks passed
@Astro-Han
Astro-Han deleted the feat/1307-processing-collapse branch July 22, 2026 11:36
Sign up for freeto 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.

feat(ui): group reasoning and tool calls into a collapsible section

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(ui): group reasoning and tool calls into collapsible Processing blocks - #1320

Merged
Astro-Han merged 7 commits into
mainfrom
feat/1307-processing-collapse
Jul 22, 2026
Merged

feat(ui): group reasoning and tool calls into collapsible Processing blocks#1320
Astro-Han merged 7 commits into
mainfrom
feat/1307-processing-collapse

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes#1307.

During longer tasks, every reasoning step and tool call rendered as its own timeline item, pushing the actual answer far down the conversation. Now each maximal run of thinking + tool activity between two answer texts folds into one collapsed "Processing" block; answer text always renders in place, and the expanded panel replays the full original timeline (the same 深度思考 disclosures and tool trows, nothing is lost).

Behavior decisions, where they differ from the issue's suggested behavior:

  • Answer text is the grouping boundary, so a turn can hold several Processing blocks (e.g. reasoning above a short interim answer, tools below it) instead of one section per turn. This keeps every user-facing text visible in chronological place.
  • Failures do not force the block open, deviating from the issue's "keep failures visible": recovered tool errors are not actionable, and the collapsed summary line already carries the failure signal (red icon + 「N 个失败」count), consistent with the existing tool-trow design locked by tool-trow-summary.test.ts. Only waiting_permission forces the block open; permission prompts themselves live outside the timeline and are unaffected.
  • While running, the block stays collapsed and its summary line shows the current activity live (「正在读取 foo.ts…」, plus the failed count as soon as a tool errors), then settles into the tool-activity roll-up (「读取 15 个文件,搜索 8 次」). Folded reasoning is not counted in the summary — thinking is not tool activity — and a pure-thinking run (no tools) stays a bare 深度思考 disclosure instead of folding into a Processing block. Manual expand/collapse sticks, per the existing disclosure-collapsible-contract (no defaultOpen).

The timeline model stays flat: the fold is a render-layer derivation (packages/ui/src/timeline-fold.ts, applied via useMemo in TurnView), so timeline-rewriting passes (overlayLiveTurn, projectTurnTools, shell-run folding) never maintain a nesting invariant — an invalid "tools-less Processing block" is unrepresentable by construction. Each block's React key derives from its preceding answer-text boundary, so projection removing a block's first tool cannot remount the disclosure or drop a manual toggle. Rendering reuses the existing Collapsible primitive, useToolDisclosure state machine, and SETTLE_FADE; no new CSS.

Verification

  • @maka/ui tests 223/223, @maka/desktop tests 2761/2761 (both re-run independently of the implementation pass), npm run lint, npm run format:check, workspace typecheck + build — all green.
  • An external adversarial review round (Codex + Kimi K3, independent) drove the final shape: their confirmed findings (a projection edge that could leave an empty Processing block, content-derived key instability, live-summary gaps) were resolved by moving the fold out of the timeline model rather than patching each site; regression tests for all three now lock the behavior.
  • Unit coverage: fold rules and block-id stability in timeline-fold.test.ts (pure thinking stays bare, interleaved runs, text-boundary multi-block, id survives first-tool projection); shell-run-fold regression in materialize.test.ts (a turn whose only tool is projected away yields a flat ['thinking'] timeline); render-level waiting_permission force-open in processing-block.test.tsx; live-summary cases in tool-trow-summary.test.ts (failed-count while running, last-live-activity pick, connector-tool localization).
  • New Processing story in chat-surface.stories.tsx (storybook contract updated) covers the collapsed block with a failed tool ahead of the answer text.
  • Before/after evidence below: both states captured from the live app (Playwright e2e fixture mode, fake backend, identical injected session on main vs this branch), composed with ImageMagick only.

Before / after (live app)

image

Expanded state — the same block manually opened, replaying the full timeline:

image

Review focus

  • packages/ui/src/timeline-fold.ts — the single place the fold rule lives; materialize.ts differs from main only by a doc comment stating the model stays flat.
  • trow-summary.ts now owns resolveToolDisplayName/isConnectorTool (moved from presentation.ts, which re-exports them) so the live Processing summary shares the localized name fallback without an import cycle.
  • streaming-handoff.test.ts asserts the folded render; materialize-turns.test.ts and live-turn-projection.test.ts are restored byte-identical to main.

…ing blocks
Group each maximal run of thinking + tool-call timeline entries between two
assistant answer texts into one collapsed "Processing" block (#1307). Answer
text stays a grouping boundary and always renders in place, so a turn can hold
several Processing blocks; the expanded block replays the full timeline (the
same 深度思考 disclosures and tool trows, nested one indent in).
- materialize: add a `processing` TurnTimelineItem kind and group in a shared
finalize pass used by both the settled (buildTurnTimeline) and live-overlay
paths, so streaming and replayed history fold identically; projectTurnTools
descends into blocks while preserving turn identity.
- trow-summary/copy: summarizeProcessing / isProcessingRunning /
processingNeedsAttention — reasoning count + tool buckets + red failed count,
live current-activity line, waiting_permission force-open (errors stay
collapsed), reusing the existing trow summary + disclosure state machine.
- chat-turn: ProcessingBlock renders via the shared disclosure/SETTLE_FADE
seams, collapsed by default with sticky manual toggle.
Show a collapsed "Processing" block (#1307): two think-then-call steps with a
failing tool fold into one summary (思考 2 次 + tool counts + a red failed
count) ahead of the assistant answer. Enumerate it in the chat-surface
Storybook contract.
…inking runs bare
Review feedback on #1307:
- The collapsed summary line now rolls up tool activity only (「读取 1 个文件,
搜索 1 次,1 个失败」) — folded reasoning stays inside the block but is no
longer counted; the settled `summary.thinking` copy is removed (zh/en). The
live `thinkingActivity` fallback (「正在深度思考」 when the block's tools are
done and thinking still streams) stays.
- A run between two answer texts folds into a Processing block only when it
contains at least one tools group; a pure-thinking run renders as the bare
深度思考 disclosure (including the live streaming path). groupProcessing
applies the rule identically on the settled and live-overlay paths.
… render layer
External review (Codex + Kimi) adjudication on #1307: baking the 'processing'
kind into the shared TurnTimelineItem model forced every timeline-rewriting
pass (overlayLiveTurn's flatten/refold, projectTurnTools' descent) to maintain
the nesting invariant, and projectTurnTools missed it — shell-run folding could
strip a block's every tool and strand an illegal thinking-only Processing block
with an empty summary (P1).
- materialize.ts reverts to main's flat TurnTimelineItem model; the
processing kind, its child types, groupProcessing/flattenProcessing/
finalizeTimeline, overlayLiveTurn's flatten→refold, and projectTurnTools'
processing branch are all removed.
- New pure module timeline-fold.ts derives the folded view at render time:
foldTimeline keeps answer text in place as the boundary, folds a maximal
thinking+tools run into one block only when it contains a tools group, and
gives each block a stable id from the preceding text's messageId ('start'
at turn head) — so a block's React key survives its first tool being
projected away without remounting or dropping a manual toggle (P2).
- TurnView folds via useMemo(foldTimeline); ProcessingBlock consumes fold
children and loses its dead onStreamingSettled prop; TurnTimelineEntry,
timelineEntryKey, and the live-content check return to their flat forms.
- Tests: materialize-turns.test.ts and live-turn-projection.test.ts restore
their original stronger assertions (flatten helpers removed); the grouping
cases move to timeline-fold.test.ts with a block-id stability case; new
P1 regression (shell-run fold leaves a flat thinking-only timeline) and a
render-level waiting_permission force-open test.
…y pick, and localization
External review (Codex + Kimi) adjudication on #1307, live-summary findings:
- The live line now appends the failed clause whenever the block already holds
an errored tool (「正在运行测试,1 个失败」) — errored tools stay collapsed,
so the summary must carry the failure signal before settle, matching the
trow (P2).
- The current activity is the LAST live entry in timeline order: children are
walked in reverse and a still-streaming thinking block or a group's active
tool wins, instead of flattening all tools and letting an earlier running
tool outrank a later streaming reasoning block (P3).
- The no-intent/no-displayName fallback routes through resolveToolDisplayName,
so a bare load_tools call reads as the localized 「加载工具组」 (P3). The
resolver and isConnectorTool move down into trow-summary.ts (the leaf
module) to avoid an import cycle; presentation.ts re-exports them for its
existing consumers.
…collapse
# Conflicts:
#	packages/ui/src/chat-turn.tsx
@Astro-Han
Astro-Han merged commit 17cc81d into mainJul 22, 2026
3 checks passed
@Astro-Han
Astro-Han deleted the feat/1307-processing-collapse branch July 22, 2026 11:36
Sign up for freeto 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.

feat(ui): group reasoning and tool calls into a collapsible section

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(ui): group reasoning and tool calls into collapsible Processing blocks - #1320

Merged
Astro-Han merged 7 commits into
mainfrom
feat/1307-processing-collapse
Jul 22, 2026
Merged

feat(ui): group reasoning and tool calls into collapsible Processing blocks#1320
Astro-Han merged 7 commits into
mainfrom
feat/1307-processing-collapse

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes#1307.

During longer tasks, every reasoning step and tool call rendered as its own timeline item, pushing the actual answer far down the conversation. Now each maximal run of thinking + tool activity between two answer texts folds into one collapsed "Processing" block; answer text always renders in place, and the expanded panel replays the full original timeline (the same 深度思考 disclosures and tool trows, nothing is lost).

Behavior decisions, where they differ from the issue's suggested behavior:

  • Answer text is the grouping boundary, so a turn can hold several Processing blocks (e.g. reasoning above a short interim answer, tools below it) instead of one section per turn. This keeps every user-facing text visible in chronological place.
  • Failures do not force the block open, deviating from the issue's "keep failures visible": recovered tool errors are not actionable, and the collapsed summary line already carries the failure signal (red icon + 「N 个失败」count), consistent with the existing tool-trow design locked by tool-trow-summary.test.ts. Only waiting_permission forces the block open; permission prompts themselves live outside the timeline and are unaffected.
  • While running, the block stays collapsed and its summary line shows the current activity live (「正在读取 foo.ts…」, plus the failed count as soon as a tool errors), then settles into the tool-activity roll-up (「读取 15 个文件,搜索 8 次」). Folded reasoning is not counted in the summary — thinking is not tool activity — and a pure-thinking run (no tools) stays a bare 深度思考 disclosure instead of folding into a Processing block. Manual expand/collapse sticks, per the existing disclosure-collapsible-contract (no defaultOpen).

The timeline model stays flat: the fold is a render-layer derivation (packages/ui/src/timeline-fold.ts, applied via useMemo in TurnView), so timeline-rewriting passes (overlayLiveTurn, projectTurnTools, shell-run folding) never maintain a nesting invariant — an invalid "tools-less Processing block" is unrepresentable by construction. Each block's React key derives from its preceding answer-text boundary, so projection removing a block's first tool cannot remount the disclosure or drop a manual toggle. Rendering reuses the existing Collapsible primitive, useToolDisclosure state machine, and SETTLE_FADE; no new CSS.

Verification

  • @maka/ui tests 223/223, @maka/desktop tests 2761/2761 (both re-run independently of the implementation pass), npm run lint, npm run format:check, workspace typecheck + build — all green.
  • An external adversarial review round (Codex + Kimi K3, independent) drove the final shape: their confirmed findings (a projection edge that could leave an empty Processing block, content-derived key instability, live-summary gaps) were resolved by moving the fold out of the timeline model rather than patching each site; regression tests for all three now lock the behavior.
  • Unit coverage: fold rules and block-id stability in timeline-fold.test.ts (pure thinking stays bare, interleaved runs, text-boundary multi-block, id survives first-tool projection); shell-run-fold regression in materialize.test.ts (a turn whose only tool is projected away yields a flat ['thinking'] timeline); render-level waiting_permission force-open in processing-block.test.tsx; live-summary cases in tool-trow-summary.test.ts (failed-count while running, last-live-activity pick, connector-tool localization).
  • New Processing story in chat-surface.stories.tsx (storybook contract updated) covers the collapsed block with a failed tool ahead of the answer text.
  • Before/after evidence below: both states captured from the live app (Playwright e2e fixture mode, fake backend, identical injected session on main vs this branch), composed with ImageMagick only.

Before / after (live app)

image

Expanded state — the same block manually opened, replaying the full timeline:

image

Review focus

  • packages/ui/src/timeline-fold.ts — the single place the fold rule lives; materialize.ts differs from main only by a doc comment stating the model stays flat.
  • trow-summary.ts now owns resolveToolDisplayName/isConnectorTool (moved from presentation.ts, which re-exports them) so the live Processing summary shares the localized name fallback without an import cycle.
  • streaming-handoff.test.ts asserts the folded render; materialize-turns.test.ts and live-turn-projection.test.ts are restored byte-identical to main.

…ing blocks
Group each maximal run of thinking + tool-call timeline entries between two
assistant answer texts into one collapsed "Processing" block (#1307). Answer
text stays a grouping boundary and always renders in place, so a turn can hold
several Processing blocks; the expanded block replays the full timeline (the
same 深度思考 disclosures and tool trows, nested one indent in).
- materialize: add a `processing` TurnTimelineItem kind and group in a shared
finalize pass used by both the settled (buildTurnTimeline) and live-overlay
paths, so streaming and replayed history fold identically; projectTurnTools
descends into blocks while preserving turn identity.
- trow-summary/copy: summarizeProcessing / isProcessingRunning /
processingNeedsAttention — reasoning count + tool buckets + red failed count,
live current-activity line, waiting_permission force-open (errors stay
collapsed), reusing the existing trow summary + disclosure state machine.
- chat-turn: ProcessingBlock renders via the shared disclosure/SETTLE_FADE
seams, collapsed by default with sticky manual toggle.
Show a collapsed "Processing" block (#1307): two think-then-call steps with a
failing tool fold into one summary (思考 2 次 + tool counts + a red failed
count) ahead of the assistant answer. Enumerate it in the chat-surface
Storybook contract.
…inking runs bare
Review feedback on #1307:
- The collapsed summary line now rolls up tool activity only (「读取 1 个文件,
搜索 1 次,1 个失败」) — folded reasoning stays inside the block but is no
longer counted; the settled `summary.thinking` copy is removed (zh/en). The
live `thinkingActivity` fallback (「正在深度思考」 when the block's tools are
done and thinking still streams) stays.
- A run between two answer texts folds into a Processing block only when it
contains at least one tools group; a pure-thinking run renders as the bare
深度思考 disclosure (including the live streaming path). groupProcessing
applies the rule identically on the settled and live-overlay paths.
… render layer
External review (Codex + Kimi) adjudication on #1307: baking the 'processing'
kind into the shared TurnTimelineItem model forced every timeline-rewriting
pass (overlayLiveTurn's flatten/refold, projectTurnTools' descent) to maintain
the nesting invariant, and projectTurnTools missed it — shell-run folding could
strip a block's every tool and strand an illegal thinking-only Processing block
with an empty summary (P1).
- materialize.ts reverts to main's flat TurnTimelineItem model; the
processing kind, its child types, groupProcessing/flattenProcessing/
finalizeTimeline, overlayLiveTurn's flatten→refold, and projectTurnTools'
processing branch are all removed.
- New pure module timeline-fold.ts derives the folded view at render time:
foldTimeline keeps answer text in place as the boundary, folds a maximal
thinking+tools run into one block only when it contains a tools group, and
gives each block a stable id from the preceding text's messageId ('start'
at turn head) — so a block's React key survives its first tool being
projected away without remounting or dropping a manual toggle (P2).
- TurnView folds via useMemo(foldTimeline); ProcessingBlock consumes fold
children and loses its dead onStreamingSettled prop; TurnTimelineEntry,
timelineEntryKey, and the live-content check return to their flat forms.
- Tests: materialize-turns.test.ts and live-turn-projection.test.ts restore
their original stronger assertions (flatten helpers removed); the grouping
cases move to timeline-fold.test.ts with a block-id stability case; new
P1 regression (shell-run fold leaves a flat thinking-only timeline) and a
render-level waiting_permission force-open test.
…y pick, and localization
External review (Codex + Kimi) adjudication on #1307, live-summary findings:
- The live line now appends the failed clause whenever the block already holds
an errored tool (「正在运行测试,1 个失败」) — errored tools stay collapsed,
so the summary must carry the failure signal before settle, matching the
trow (P2).
- The current activity is the LAST live entry in timeline order: children are
walked in reverse and a still-streaming thinking block or a group's active
tool wins, instead of flattening all tools and letting an earlier running
tool outrank a later streaming reasoning block (P3).
- The no-intent/no-displayName fallback routes through resolveToolDisplayName,
so a bare load_tools call reads as the localized 「加载工具组」 (P3). The
resolver and isConnectorTool move down into trow-summary.ts (the leaf
module) to avoid an import cycle; presentation.ts re-exports them for its
existing consumers.
…collapse
# Conflicts:
#	packages/ui/src/chat-turn.tsx
@Astro-Han
Astro-Han merged commit 17cc81d into mainJul 22, 2026
3 checks passed
@Astro-Han
Astro-Han deleted the feat/1307-processing-collapse branch July 22, 2026 11:36
Sign up for freeto 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.

feat(ui): group reasoning and tool calls into a collapsible section

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(ui): group reasoning and tool calls into collapsible Processing blocks - #1320

Merged
Astro-Han merged 7 commits into
mainfrom
feat/1307-processing-collapse
Jul 22, 2026
Merged

feat(ui): group reasoning and tool calls into collapsible Processing blocks#1320
Astro-Han merged 7 commits into
mainfrom
feat/1307-processing-collapse

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes#1307.

During longer tasks, every reasoning step and tool call rendered as its own timeline item, pushing the actual answer far down the conversation. Now each maximal run of thinking + tool activity between two answer texts folds into one collapsed "Processing" block; answer text always renders in place, and the expanded panel replays the full original timeline (the same 深度思考 disclosures and tool trows, nothing is lost).

Behavior decisions, where they differ from the issue's suggested behavior:

  • Answer text is the grouping boundary, so a turn can hold several Processing blocks (e.g. reasoning above a short interim answer, tools below it) instead of one section per turn. This keeps every user-facing text visible in chronological place.
  • Failures do not force the block open, deviating from the issue's "keep failures visible": recovered tool errors are not actionable, and the collapsed summary line already carries the failure signal (red icon + 「N 个失败」count), consistent with the existing tool-trow design locked by tool-trow-summary.test.ts. Only waiting_permission forces the block open; permission prompts themselves live outside the timeline and are unaffected.
  • While running, the block stays collapsed and its summary line shows the current activity live (「正在读取 foo.ts…」, plus the failed count as soon as a tool errors), then settles into the tool-activity roll-up (「读取 15 个文件,搜索 8 次」). Folded reasoning is not counted in the summary — thinking is not tool activity — and a pure-thinking run (no tools) stays a bare 深度思考 disclosure instead of folding into a Processing block. Manual expand/collapse sticks, per the existing disclosure-collapsible-contract (no defaultOpen).

The timeline model stays flat: the fold is a render-layer derivation (packages/ui/src/timeline-fold.ts, applied via useMemo in TurnView), so timeline-rewriting passes (overlayLiveTurn, projectTurnTools, shell-run folding) never maintain a nesting invariant — an invalid "tools-less Processing block" is unrepresentable by construction. Each block's React key derives from its preceding answer-text boundary, so projection removing a block's first tool cannot remount the disclosure or drop a manual toggle. Rendering reuses the existing Collapsible primitive, useToolDisclosure state machine, and SETTLE_FADE; no new CSS.

Verification

  • @maka/ui tests 223/223, @maka/desktop tests 2761/2761 (both re-run independently of the implementation pass), npm run lint, npm run format:check, workspace typecheck + build — all green.
  • An external adversarial review round (Codex + Kimi K3, independent) drove the final shape: their confirmed findings (a projection edge that could leave an empty Processing block, content-derived key instability, live-summary gaps) were resolved by moving the fold out of the timeline model rather than patching each site; regression tests for all three now lock the behavior.
  • Unit coverage: fold rules and block-id stability in timeline-fold.test.ts (pure thinking stays bare, interleaved runs, text-boundary multi-block, id survives first-tool projection); shell-run-fold regression in materialize.test.ts (a turn whose only tool is projected away yields a flat ['thinking'] timeline); render-level waiting_permission force-open in processing-block.test.tsx; live-summary cases in tool-trow-summary.test.ts (failed-count while running, last-live-activity pick, connector-tool localization).
  • New Processing story in chat-surface.stories.tsx (storybook contract updated) covers the collapsed block with a failed tool ahead of the answer text.
  • Before/after evidence below: both states captured from the live app (Playwright e2e fixture mode, fake backend, identical injected session on main vs this branch), composed with ImageMagick only.

Before / after (live app)

image

Expanded state — the same block manually opened, replaying the full timeline:

image

Review focus

  • packages/ui/src/timeline-fold.ts — the single place the fold rule lives; materialize.ts differs from main only by a doc comment stating the model stays flat.
  • trow-summary.ts now owns resolveToolDisplayName/isConnectorTool (moved from presentation.ts, which re-exports them) so the live Processing summary shares the localized name fallback without an import cycle.
  • streaming-handoff.test.ts asserts the folded render; materialize-turns.test.ts and live-turn-projection.test.ts are restored byte-identical to main.

…ing blocks
Group each maximal run of thinking + tool-call timeline entries between two
assistant answer texts into one collapsed "Processing" block (#1307). Answer
text stays a grouping boundary and always renders in place, so a turn can hold
several Processing blocks; the expanded block replays the full timeline (the
same 深度思考 disclosures and tool trows, nested one indent in).
- materialize: add a `processing` TurnTimelineItem kind and group in a shared
finalize pass used by both the settled (buildTurnTimeline) and live-overlay
paths, so streaming and replayed history fold identically; projectTurnTools
descends into blocks while preserving turn identity.
- trow-summary/copy: summarizeProcessing / isProcessingRunning /
processingNeedsAttention — reasoning count + tool buckets + red failed count,
live current-activity line, waiting_permission force-open (errors stay
collapsed), reusing the existing trow summary + disclosure state machine.
- chat-turn: ProcessingBlock renders via the shared disclosure/SETTLE_FADE
seams, collapsed by default with sticky manual toggle.
Show a collapsed "Processing" block (#1307): two think-then-call steps with a
failing tool fold into one summary (思考 2 次 + tool counts + a red failed
count) ahead of the assistant answer. Enumerate it in the chat-surface
Storybook contract.
…inking runs bare
Review feedback on #1307:
- The collapsed summary line now rolls up tool activity only (「读取 1 个文件,
搜索 1 次,1 个失败」) — folded reasoning stays inside the block but is no
longer counted; the settled `summary.thinking` copy is removed (zh/en). The
live `thinkingActivity` fallback (「正在深度思考」 when the block's tools are
done and thinking still streams) stays.
- A run between two answer texts folds into a Processing block only when it
contains at least one tools group; a pure-thinking run renders as the bare
深度思考 disclosure (including the live streaming path). groupProcessing
applies the rule identically on the settled and live-overlay paths.
… render layer
External review (Codex + Kimi) adjudication on #1307: baking the 'processing'
kind into the shared TurnTimelineItem model forced every timeline-rewriting
pass (overlayLiveTurn's flatten/refold, projectTurnTools' descent) to maintain
the nesting invariant, and projectTurnTools missed it — shell-run folding could
strip a block's every tool and strand an illegal thinking-only Processing block
with an empty summary (P1).
- materialize.ts reverts to main's flat TurnTimelineItem model; the
processing kind, its child types, groupProcessing/flattenProcessing/
finalizeTimeline, overlayLiveTurn's flatten→refold, and projectTurnTools'
processing branch are all removed.
- New pure module timeline-fold.ts derives the folded view at render time:
foldTimeline keeps answer text in place as the boundary, folds a maximal
thinking+tools run into one block only when it contains a tools group, and
gives each block a stable id from the preceding text's messageId ('start'
at turn head) — so a block's React key survives its first tool being
projected away without remounting or dropping a manual toggle (P2).
- TurnView folds via useMemo(foldTimeline); ProcessingBlock consumes fold
children and loses its dead onStreamingSettled prop; TurnTimelineEntry,
timelineEntryKey, and the live-content check return to their flat forms.
- Tests: materialize-turns.test.ts and live-turn-projection.test.ts restore
their original stronger assertions (flatten helpers removed); the grouping
cases move to timeline-fold.test.ts with a block-id stability case; new
P1 regression (shell-run fold leaves a flat thinking-only timeline) and a
render-level waiting_permission force-open test.
…y pick, and localization
External review (Codex + Kimi) adjudication on #1307, live-summary findings:
- The live line now appends the failed clause whenever the block already holds
an errored tool (「正在运行测试,1 个失败」) — errored tools stay collapsed,
so the summary must carry the failure signal before settle, matching the
trow (P2).
- The current activity is the LAST live entry in timeline order: children are
walked in reverse and a still-streaming thinking block or a group's active
tool wins, instead of flattening all tools and letting an earlier running
tool outrank a later streaming reasoning block (P3).
- The no-intent/no-displayName fallback routes through resolveToolDisplayName,
so a bare load_tools call reads as the localized 「加载工具组」 (P3). The
resolver and isConnectorTool move down into trow-summary.ts (the leaf
module) to avoid an import cycle; presentation.ts re-exports them for its
existing consumers.
…collapse
# Conflicts:
#	packages/ui/src/chat-turn.tsx
@Astro-Han
Astro-Han merged commit 17cc81d into mainJul 22, 2026
3 checks passed
@Astro-Han
Astro-Han deleted the feat/1307-processing-collapse branch July 22, 2026 11:36
Sign up for freeto 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.

feat(ui): group reasoning and tool calls into a collapsible section

1 participant

@Astro-Han