Done. I've added a "Cleanup Required" section to each phase that lists:
tool-utils.ts → what files use them → what to replace withEach cleanup section makes explicit what breaks when you make the change, so nothing is forgotten during implementation.
You're right — that's a category error. highlights and all are filter modes (UI state), not message types (data categories). They shouldn't share the same registry.
Let me fix the plan to separate these concerns.
Done. I've separated the concepts:
Message Types (lib/message-type-registry.ts):
MESSAGE_TYPES registry with category: 'message_kind' | 'tool_type'user_message, agent_text, agent_thinking, tool_call, and tool subtypesgetTypeConfig(), getIcon(), getLabel(), getMessageKinds(), getToolTypes()Filter Modes (lib/filter-utils.ts):
FILTER_MODES — small, separate confighighlights, all (just id, label, icon — no shortLabel, no category)getFilterModeConfig()This keeps the conceptual distinction clear:
TraceFilterToggle imports from filter-utils.ts, while TraceFilterSidebar/TraceFilterPills import from message-type-registry.ts.
Let me analyze how the "highlights" filter works across the codebase.
Now I have a complete picture. Here's the analysis:
The "Highlights" filter is a smart/computed filter that determines which messages to show based on position and type, not just message kind. The logic lives in useMessageFilters.ts (lines 42-82).
For each message in the list:
1. Always include INTERRUPTION messages
2. Always include USER messages
→ AND look backward to find the last AGENT_TEXT before this user message
(provides context for why the user sent their next prompt)
3. Always include CODE CHANGES (edit, write, delete tools)
| Included | Reason |
|---|---|
| User messages | Core conversation turns |
| Agent text immediately before user messages | Context for user's next prompt |
| Interruption messages | Important workflow events |
| Code changes (edit, write, delete) | The "work product" of the session |
| Hidden | Reason |
|---|---|
| Thinking messages | Internal reasoning, not essential |
| Read/grep/glob tool calls | Exploration, not changes |
| Agent text NOT before a user message | Mid-conversation responses |
| Web search/fetch | Research, not changes |
┌─────────────────────────────────────────────────────────────────────┐
│ page.tsx │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ useMessageFilters(messages, filter) │ │
│ │ → returns { highlightedIndices, counts, isHighlighted } │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ <MessageList filter={filter} /> │ │
│ │ → calls useMessageFilters internally │ │
│ │ → uses isHighlighted(msg, index) to decide: │ │
│ │ • Show message directly (highlighted) │ │
│ │ • Collapse into accordion (not highlighted) │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
| File | Role |
|---|---|
useMessageFilters.ts | Computes highlightedIndices set; exports isHighlighted() helper |
message-utils.ts | Provides isCodeChange() and isInterruptionMessage() used by the algorithm |
MessageList.tsx | Consumes isHighlighted() to decide render vs accordion |
page.tsx | Passes filter to MessageList; uses highlightedIndices.size for counts |
Duplicate computation: Both page.tsx and MessageList.tsx call useMessageFilters() with the same inputs — the hook runs twice
Index-based: Highlights are determined by message INDEX, not message content. This means matchesFilter(message, "highlights") returns false — you must use isHighlighted(message, index)
isCodeChange() lives in message-utils but uses tool name logic that's similar to normalizeToolName() in tool-utils — potential for inconsistency
The algorithm is undocumented — no comments explain WHY these specific criteria were chosen
Should I add this analysis to the project plan? It might be worth having a section that documents how highlights work, and potentially a task to consolidate/document this logic better.
Agreed. Let me add this to the plan — the highlights logic should be computed once, in one place, with a clear algorithm.
Now let me read the filter components and trace-utils to understand the full picture of duplication.
Now let me update the File Changes Summary to include these changes.
Done. I've added Phase 5: Simplify Highlights Computation which:
Problem documented in the analysis section — explains the current mess (computed twice, logic scattered, index-based, undocumented)
Target state defined:
computeHighlightedIds(messages): Set<string>filter-utils.tspage.tsx, passed down to MessageListConcrete tasks:
computeHighlightedIds()isCodeChange() and isInterruptionMessage() to filter-utils.tspage.tsx to compute onceMessageList to accept highlightedIds propuseMessageFilters.tsCleanup section lists the ripple effects
The plan now has 10 phases (renumbered the subsequent ones). The File Changes Summary is also updated to reflect that useMessageFilters.ts may be deleted and MessageList.tsx gets a new prop.
Let me do a deeper dive into the codebase to find other complexity hotspots.
I'll analyze the project plan and then do a comprehensive sweep of the codebase to identify duplication, redundant functions, and centralization opportunities. Let me start by reading the project plan and exploring the relevant parts of the codebase.
Let me check the git diff for this branch to see all the changes that have been made.
Found several more complexity hotspots:
Done. I've added the additional complexity areas I found:
Added to Problem Analysis:
MessageList renderBlocks — 100-line useMemo that mixes filtering, accordion grouping, and avatar visibility logic. Documents the "strict filtering vs loose filtering" concept that's buried in the code.
Tool Results Map — built in MessageList on every render, could be done once at page level.
ToolCallRouter — 120-line switch statement that could eventually be data-driven.
Added "Future Improvements" section at the end listing things that are out of scope but worth addressing later:
isFirstAfterUser simplification (spans 3 files)renderBlocksThe Phase 5 (Simplify Highlights) description now notes that fixing highlights will also help simplify the renderBlocks monster since MessageList won't need to do its own filtering anymore.
The first thing is Phase 1: Create Unified Registry — specifically, creating lib/message-type-registry.ts.
This is the perfect starting point because:
Create the file frontend/lib/message-type-registry.ts
Add all entries in this order:
user_message, agent_text, agent_thinking, tool_callread, edit, write, delete, grep, glob, terminal_command, web_search, web_fetch, task, todo, lsp, notebook_read, notebook_editAdd helper functions:
getTypeConfig(id)getIcon(id)getLabel(id, variant)getMessageKinds()getToolTypes()Import icons from lucide-react
Write a simple test to verify coverage
Once this file exists, you can verify it works by importing and calling getIcon('read') or getLabel('user_message', 'short') in a console log. No UI should break because nothing is using it yet.
No cleanup needed in this phase — it's just adding a new file.
Want me to switch to Agent mode and create it?
Done. I've renamed it to message-registry.ts throughout the plan.
So the first concrete action is:
Create frontend/lib/message-registry.ts with:
MessageTypeConfig type definitionMESSAGE_TYPES object with all message kinds and tool typesgetTypeConfig(), getIcon(), getLabel(), getMessageKinds(), getToolTypes()This is a pure addition — no existing code changes, no cleanup needed. Once it exists and tests pass, you move to Phase 2.
Good question. You're right to push back — direct destructuring is often simpler and more transparent.
Direct access:
Helper functions:
Fallback handling — MESSAGE_TYPES[unknownId] returns undefined and crashes. A helper can return a sensible default:
Label variants — if you need both "Prompts" (filter UI) and "User" (compact UI), a helper avoids repeating the ternary:
For simple cases where:
getMessageKinds())Direct destructuring is cleaner.
Keep only getTypeConfig() for fallback handling. Everything else via destructuring:
Or if you're iterating:
Want me to update the plan to simplify this — just export MESSAGE_TYPES and one getTypeConfig() for fallback, drop the other helpers?
Thinking about this in context of your app...
Message kinds (user_message, agent_text, agent_thinking, tool_call) — these are a closed set derived by getMessageKind(). If it returns something unexpected, that's a bug in your code, not user data. No fallback needed.
Tool types (read, edit, grep, etc.) — these come from agent data. Different agents might use tools you haven't seen before.
Filter sidebar — shows tool type checkboxes based on what's in the trace. If a trace has some_new_tool, you'd want to display something rather than crash or show "undefined".
MessageAccordion hover preview — shows icons for collapsed messages. Unknown tool = need an icon.
Any place iterating over messages and looking up config by tool type.
Looking at your existing code:
tool-utils.ts:275 — getToolTypeIcon() returns Wrench for unknown toolsMessageItem.tsx:211-212 — ToolCallRouter has a default case that renders <DefaultToolMessage>normalizeToolName() — maps aliases to canonical names, returns original if unknownSo the app already gracefully handles unknown tools. The fallback pattern exists.
You probably don't need a getTypeConfig() fallback helper. Just:
Or for the filter UI specifically, only show tool types that exist in the registry:
So: skip the fallback helper, just export MESSAGE_TYPES directly. The consuming code can handle missing entries as appropriate for each context.
Want me to simplify the plan to remove the helper functions entirely?
The diff is quite large (36K+ lines). Let me read through it to understand all the changes, focusing on the filter and message-related code.
Done. The plan now uses direct access everywhere:
MESSAGE_TYPES[id].icon instead of getIcon(id)MESSAGE_TYPES[id].label instead of getLabel(id)MESSAGE_TYPES[kind].shortLabel instead of getLabel(kind, 'short')FILTER_MODES[id].icon instead of getFilterModeConfig(id).iconNo helper functions — just export the objects and destructure at call sites.
Good catch — it's redundant.
parentKind: 'tool_call' was meant to indicate that tool types are "children" of the tool_call message kind. But:
category === 'tool_type' already tells you this — if it's a tool type, its parent is always tool_calltool_call, never anything elsecategory, not parentKindSo parentKind adds no information that category doesn't already provide. Let me remove it.
Let me read the relevant frontend filter components and message rendering code to understand the current duplication patterns.
Removed. The category field already encodes the hierarchy — 'tool_type' means "this is a child of tool_call". No need for an explicit parent pointer when there's only one possible parent.
Let me read the Phase 1 details from the plan.
Let me also read the Phase 1 task list from the plan.
I need to first clean up the parentKind references that remain in the plan examples (since we removed the field from the type), then create the actual registry file.
Now let me create the actual message-registry.ts file.
Let me verify the file compiles and check for any lint errors.
TypeScript compiles and no lint errors. Let me update the plan to mark Phase 1 tasks as complete.
Phase 1 complete.
Created frontend/lib/message-registry.ts with:
MessageTypeConfig type — consistent shape for all message typesuser_message, agent_text, agent_thinking, tool_callread, edit, write, delete, grep, glob, terminal_command, web_search, web_fetch, task, todo, lsp, notebook_read, `notebook_Now that metadata lives in message-registry.ts, tool-utils.ts should shrink to only contain the alias normalization logic. Here's what gets removed:
Remove from tool-utils.ts | Replaced by |
|---|---|
TOOL_DISPLAY_NAMES | MESSAGE_TYPES[id].label |
getToolTypeDisplayName() | MESSAGE_TYPES[id].label |
getToolTypeIcon() | MESSAGE_TYPES[id].icon |
TOOL_TYPE_ORDER | MESSAGE_TYPES[id].order |
getToolTypeOrder() | MESSAGE_TYPES[id].order |
getMessageIcon() | MESSAGE_TYPES[kind].icon |
What stays: Only TOOL_NAME_MAP (renamed to TOOL_NAME_ALIASES) and normalizeToolName(). These handle the raw tool name → canonical ID mapping (e.g., "bash" → "terminal_command", "strreplace" → "edit").
The file goes from ~310 lines to ~170 lines. The flow becomes:
raw tool name → normalizeToolName() → canonical ID → MESSAGE_TYPES[id].icon/label/etc
This is Phase 2 in the plan. Want me to implement it now?
Let me first find all usages of the functions I'm removing.
Now let me read the files that need updating.
Now I'll update the three files to use MESSAGE_TYPES directly, then simplify tool-utils.ts.
Now update the usages in each file.