Skip to content

feat: introduce MessageGroup model replacing flat Message[] for grouped assistant rendering#27

Merged
TuYv merged 2 commits into
TuYv:masterfrom
genni613:feat/message-grouping
Apr 21, 2026
Merged

feat: introduce MessageGroup model replacing flat Message[] for grouped assistant rendering#27
TuYv merged 2 commits into
TuYv:masterfrom
genni613:feat/message-grouping

Conversation

@genni613

@genni613 genni613 commented Apr 20, 2026

Copy link
Copy Markdown
Collaborator

User description

Summary

  • 引入 MessageGroup[] 替代扁平 Message[] 渲染模型,将执行卡片和最终回答归入同一 assistant 组
  • 消除后端 bridgeNotifiedStart / notifyRemoveMessage 排序 hack,改为无条件的 notifyStart + notifyRoundEnd

Changes

  • 新增 groupReducer.ts 替代 eventReducer.ts,用 GroupState + MessageGroup 管理状态
  • 新增 AssistantGroup.tsxAssistantMarkdown.tsx 组件,分别渲染分组和 markdown
  • 更新 App.tsx:从 messages[] 切换到 groups[],简化加载指示器(仅在无流式文本时显示 spinner)
  • 简化 ChatService.kt:移除 bridgeNotifiedStart 集合,startStreamingRound() 无条件发送 notifyStartonFinishReason("tool_calls") 改发
    notifyRoundEnd
  • 新增 29 个 groupReducer 测试用例(含工具调用流程、多轮执行、会话恢复等场景)

Test plan

  • 65 个前端测试全部通过(含 29 个新增 groupReducer 测试)
  • 工具调用流程:执行卡片在最终文本之前,中间 token 被 round_end 正确丢弃
  • 多轮工具调用:正确分组在同一个 assistant 组内
  • 执行卡片自动折叠:运行时展开,完成后自动折叠
  • 会话恢复:历史消息正确转换为分组结构
  • 正文流式输出时底部不显示多余 spinner

PR Type

Enhancement


Description

  • Replace flat Message[] with MessageGroup[] for grouped assistant rendering (execution cards + final answer in same group)

  • Remove backend bridgeNotifiedStart hack; send unconditional notifyStart + notifyRoundEnd for token discarding

  • Add groupReducer.ts with GroupState, AssistantGroup, HumanGroup types; handle 'round_end' to discard intermediate tokens

  • Add AssistantGroup.tsx and AssistantMarkdown.tsx components; update App.tsx to use groups and simplify spinner logic

  • Add 29 groupReducer tests covering tool calls, multi-round flows, session restore


Diagram Walkthrough

flowchart LR
  A[Kotlin ChatService] -->|"notifyStart"| B[Frontend groupReducer]
  A -->|"notifyToken"| B
  A -->|"execution_card"| B
  A -->|"notifyRoundEnd"| B
  B --> C[GroupState with groups[]]
  C --> D[AssistantGroup component]
  C --> E[HumanGroup component]
Loading

File Walkthrough

Relevant files
Enhancement
5 files
ChatService.kt
Remove bridgeNotifiedStart hack, add notifyRoundEnd           
+14/-44 
App.tsx
Switch from messages[] to groups[] with groupReducer         
+29/-22 
AssistantGroup.tsx
New component for grouped assistant rendering                       
+81/-0   
AssistantMarkdown.tsx
New component for markdown rendering with code highlighting
+83/-0   
groupReducer.ts
New reducer with GroupState and MessageGroup types             
+255/-0 
Documentation
1 files
unified-tool-design.md
Update design doc for Phase 2 MessageGroup architecture   
+157/-57
Configuration changes
2 files
package.json
Add groupReducer test to test script                                         
+1/-1     
tsconfig.test.json
Add groupReducer.ts to test build include                               
+2/-1     
Tests
1 files
groupReducer.test.mjs
New test file with 29 groupReducer test cases                       
+436/-0 
Additional files
1 files
index.html +60/-60 

@github-actions

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected

@github-actions

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Use crypto.randomUUID() instead of Date.now() for unique IDs

Using Date.now() for text child IDs can cause collisions if multiple rapid tokens
arrive within the same millisecond. Consider using a counter or UUID for unique IDs.

webview/src/groupReducer.ts [90-111]

 case 'token': {
   const lastGroup = state.groups[state.groups.length - 1]
   if (lastGroup?.type !== 'assistant') return state
 
   const group = lastGroup as AssistantGroup
-  let newTextIndex = state.currentRoundTextIndex
+  const newTextIndex = state.currentRoundTextIndex
 
   if (newTextIndex !== null && group.children[newTextIndex]?.kind === 'text') {
     const existing = group.children[newTextIndex] as AssistantChild & { kind: 'text' }
     const updated = [...group.children]
     updated[newTextIndex] = { kind: 'text', id: existing.id, content: existing.content + payload.text, isStreaming: existing.isStreaming }
     const groups = [...state.groups]
     groups[groups.length - 1] = { ...group, children: updated }
     return { ...state, groups }
   }
 
-  const textChild: AssistantChild = { kind: 'text', id: `text-${Date.now()}`, content: payload.text, isStreaming: true }
-  newTextIndex = group.children.length
+  const textChild: AssistantChild = { kind: 'text', id: `text-${crypto.randomUUID()}`, content: payload.text, isStreaming: true }
+  const appendIndex = group.children.length
   const groups = [...state.groups]
   groups[groups.length - 1] = { ...group, children: [...group.children, textChild] }
-  return { ...state, groups, currentRoundTextIndex: newTextIndex }
+  return { ...state, groups, currentRoundTextIndex: appendIndex }
 }
Suggestion importance[1-10]: 4

__

Why: The suggestion correctly identifies a potential ID collision issue with Date.now(). While the practical risk is low (tokens arrive rapidly within the same round and are appended sequentially), using crypto.randomUUID() provides more robust uniqueness guarantees. This is a minor improvement to code quality rather than a critical bug fix.

Low

@genni613 genni613 requested a review from TuYv April 20, 2026 04:45
@TuYv TuYv merged commit 0da308a into TuYv:master Apr 21, 2026
4 checks passed
TuYv pushed a commit that referenced this pull request Apr 21, 2026
* refactor: unify 15 bridge callbacks into single onEvent channel (#26)

* feat: add landing page for beta onboarding

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update GitHub links to genni613/CodePlanGUI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(execution): stream assistant response after execution cards with correct ordering

* fix: cancel flushTimer on dispose and synchronize flushPendingBuffer to prevent race condition and resource leak

* docs: add unified event system and message grouping design spec

Covers two-phase migration: Phase 1 unifies 13 bridge callbacks into
a single onEvent channel with eventReducer; Phase 2 introduces
MessageGroup-based rendering to eliminate backend ordering hacks
(notifyRemoveMessage, bridgeNotifiedStart, lazy bubble creation).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: fix spec review issues in unified events & grouping design

- Correct dispatch strategies for notifyContextFile/notifyTheme (pushJS, not flushAndPush)
- Fix flushAndPush signature (String param, not lambda)
- Use kotlinx.serialization instead of non-existent JSONObject.quote
- Preserve useBridge bridge_ready lifecycle, don't overwrite window.__bridge
- Add action field to structured_error event payload
- Complete error/structured_error cases in groupReducer
- Replace pseudocode token index tracking with concrete logic
- Add msgId invariant note across API rounds
- Handle double-encoding in restore_messages payload
- Preserve debug log side effects in event handler
- Add notifyStart timing consideration note for Phase 2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: fix buildEventJS contract and notifyRoundEnd safety

- Change buildEventJS payload param from String to Map<String, Any?>
  to prevent double-encoding when callers pass pre-encoded JSON
- Use mapOf() for notifyRoundEnd payload instead of string
  interpolation to avoid JSON injection from special characters

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: translate unified events & message grouping spec to Chinese

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: split unified events & grouping spec into two separate docs

- Phase 1: unified-event-channel-design.md — single onEvent channel,
  eventReducer, BridgeHandler buildEventJS refactor
- Phase 2: message-grouping-design.md — MessageGroup rendering,
  groupReducer, ChatService hack removal

Each doc is self-contained for independent PR review.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: unify 15 bridge callbacks into single onEvent channel

* refactor: unify 15 bridge callbacks into single onEvent channel

---------

Co-authored-by: yuang.peng <yuang.peng@yaduo.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: introduce MessageGroup model replacing flat Message[] for grouped assistant rendering (#27)

* feat: introduce MessageGroup model replacing flat Message[] for grouped assistant rendering

* fix: code review

---------

Co-authored-by: yuang.peng <yuang.peng@yaduo.com>

* feat: add tool execution engine with inline step visualization

* fix: cr

---------

Co-authored-by: yuang.peng <yuang.peng@yaduo.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants