feat(chat): display system-injected messages as collapsible cards#463
feat(chat): display system-injected messages as collapsible cards#463shikihane wants to merge 4 commits intositeboon:mainfrom
Conversation
Instead of silently hiding system-injected messages (system-reminders, command outputs, hooks, session continuations, etc.), classify them and render as muted collapsible cards with type badges and summaries. Background task results get a distinct emerald-styled card. - Add classifyUserMessage() to detect and categorize injected content - Replace binary skip logic with classification-based rendering - Add collapsible UI with type badge + summary for all categories - Add i18n translations (en, ja, ko, zh-CN)
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review infoConfiguration used: Repository UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds internal message classification and transforms to detect system-injected types (task-notification, system-reminder, background-task-result, hook, command, continuation, other), tags emitted messages with injection metadata, updates rendering for injected messages, and adds localization keys for those types. Changes
Sequence DiagramsequenceDiagram
participant Client as Client
participant Converter as convertSessionMessages
participant Classifier as classifyUserMessage
participant Renderer as MessageComponent
participant i18n as i18n Locales
Client->>Converter: send raw session messages
Converter->>Classifier: classifyUserMessage(rawContent)
Classifier-->>Converter: ClassifiedMessage { injectedType, injectedSummary, taskStatus? }
alt task-notification
Converter-->>Renderer: assistant message (isSystemInjected + injectedType + taskStatus)
else background-task-result
Converter-->>Renderer: assistant message (isSystemInjected + injectedSummary with result body)
else other injected
Converter-->>Renderer: user message (isSystemInjected + injectedSummary)
else unclassified
Converter-->>Renderer: regular user message (unescaped content)
end
Renderer->>i18n: lookup systemInjected[injectedType]
i18n-->>Renderer: localized label
Renderer-->>Client: render message (collapsed/system-injected UI or normal bubble)
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
src/components/chat/utils/messageTransforms.ts (2)
353-357: Consider using the ChatMessage type for injectedType.The
ClassifiedMessage.injectedTypeproperty is typed asChatMessage['injectedType'], butChatMessagedoesn't have an explicitinjectedTypeproperty defined. This resolves tounknownvia the index signature. Consider defining a union type for the allowed injected types.💡 Suggested improvement
+type InjectedType = 'task-notification' | 'system-reminder' | 'command' | 'hook' | 'background-task-result' | 'continuation' | 'other'; + interface ClassifiedMessage { - injectedType: ChatMessage['injectedType']; + injectedType: InjectedType; injectedSummary: string; taskStatus?: string; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/chat/utils/messageTransforms.ts` around lines 353 - 357, The ClassifiedMessage.injectedType currently uses ChatMessage['injectedType'] which resolves to unknown because ChatMessage has no explicit injectedType; define a concrete union type (e.g., type InjectedType = 'system' | 'tool' | 'assistant' | 'user' or whatever allowed values are) or add an explicit injectedType property to the ChatMessage interface, then replace ClassifiedMessage.injectedType's type with that union (or ChatMessage.injectedType after adding it) so the field is strongly typed; update any usages of ClassifiedMessage or ChatMessage that assume injectedType to use the new union or property name.
403-408: RedundantstartsWithcheck.The condition
content.startsWith('<user-prompt-submit-hook>') || content.includes('<user-prompt-submit-hook>')is redundant sinceincludesalready covers thestartsWithcase.🧹 Suggested simplification
- if (content.startsWith('<user-prompt-submit-hook>') || content.includes('<user-prompt-submit-hook>')) { + if (content.includes('<user-prompt-submit-hook>')) { return { injectedType: 'hook', injectedSummary: 'Hook output', }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/chat/utils/messageTransforms.ts` around lines 403 - 408, The conditional that checks content.startsWith('<user-prompt-submit-hook>') || content.includes('<user-prompt-submit-hook>') is redundant; update the logic in the messageTransforms code that inspects the content variable to use only content.includes('<user-prompt-submit-hook>') and keep the existing return payload ({ injectedType: 'hook', injectedSummary: 'Hook output' }) unchanged so behavior is preserved but the check is simplified.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/chat/view/subcomponents/MessageComponent.tsx`:
- Around line 102-123: Add explicit optional properties to the ChatMessage
interface so the component gets proper typings: add isSystemInjected?: boolean,
injectedType?: string, injectedSummary?: string, isTaskNotification?: boolean,
and taskStatus?: string to the existing ChatMessage interface (keep the existing
[key: string]: unknown index signature). Update the ChatMessage interface
declaration to include these fields so usages in MessageComponent (and
messageTransforms.ts) are strongly typed and enable autocomplete/type checking.
In `@src/i18n/locales/en/chat.json`:
- Around line 242-251: The locales define a "skill" systemInjected key that is
never used because classifyUserMessage in messageTransforms.ts never returns
injectedType === 'skill'; either remove the "skill" entry from the
systemInjected objects across locale files (including the en file shown) or add
classification logic in classifyUserMessage to detect skill-related messages and
return 'skill' as injectedType (update the function in messageTransforms.ts and
any related constants/union types to include 'skill' if missing). Ensure the
change keeps locale keys and TypeScript types in sync (remove unused key from
all locales and any type declarations, or implement detection in
classifyUserMessage and update relevant types to include 'skill').
---
Nitpick comments:
In `@src/components/chat/utils/messageTransforms.ts`:
- Around line 353-357: The ClassifiedMessage.injectedType currently uses
ChatMessage['injectedType'] which resolves to unknown because ChatMessage has no
explicit injectedType; define a concrete union type (e.g., type InjectedType =
'system' | 'tool' | 'assistant' | 'user' or whatever allowed values are) or add
an explicit injectedType property to the ChatMessage interface, then replace
ClassifiedMessage.injectedType's type with that union (or
ChatMessage.injectedType after adding it) so the field is strongly typed; update
any usages of ClassifiedMessage or ChatMessage that assume injectedType to use
the new union or property name.
- Around line 403-408: The conditional that checks
content.startsWith('<user-prompt-submit-hook>') ||
content.includes('<user-prompt-submit-hook>') is redundant; update the logic in
the messageTransforms code that inspects the content variable to use only
content.includes('<user-prompt-submit-hook>') and keep the existing return
payload ({ injectedType: 'hook', injectedSummary: 'Hook output' }) unchanged so
behavior is preserved but the check is simplified.
ℹ️ Review info
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/components/chat/utils/messageTransforms.tssrc/components/chat/view/subcomponents/MessageComponent.tsxsrc/i18n/locales/en/chat.jsonsrc/i18n/locales/ja/chat.jsonsrc/i18n/locales/ko/chat.jsonsrc/i18n/locales/zh-CN/chat.json
| "systemInjected": { | ||
| "system-reminder": "System Reminder", | ||
| "task-notification": "Task Notification", | ||
| "background-task-result": "Background Task Result", | ||
| "hook": "Hook Output", | ||
| "command": "Command", | ||
| "skill": "Skill", | ||
| "continuation": "Session Info", | ||
| "other": "System" | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for any usage of 'skill' as an injectedType value
rg -n "injectedType.*skill|'skill'" --type=ts --type=tsx -g '!*.json'Repository: siteboon/claudecodeui
Length of output: 92
🏁 Script executed:
# Also search in messageTransforms.ts to understand the context
fd messageTransforms.ts --type fRepository: siteboon/claudecodeui
Length of output: 111
🏁 Script executed:
# Search more broadly for "skill" in the codebase
rg -n "skill" --type=ts --type=tsx --type=js --type=json -g '!node_modules'Repository: siteboon/claudecodeui
Length of output: 92
🏁 Script executed:
# Read the messageTransforms.ts file to understand classifyUserMessage
cat -n src/components/chat/utils/messageTransforms.ts | head -100Repository: siteboon/claudecodeui
Length of output: 3594
🏁 Script executed:
# Search for 'skill' usage in TypeScript/JS files (without tsx type)
rg -n "skill" -t=ts -g '!node_modules' 2>/dev/null || rg -n "skill" -g '!node_modules' -g '!*.json'Repository: siteboon/claudecodeui
Length of output: 47
🏁 Script executed:
# Search for injectedType assignments to understand all possible values
rg -n "injectedType\s*:" src/components/chat/utils/messageTransforms.ts -A 2 -B 2Repository: siteboon/claudecodeui
Length of output: 3683
🏁 Script executed:
# Search for any reference to "skill" in the entire codebase
rg -n "skill" --max-count=20Repository: siteboon/claudecodeui
Length of output: 283
🏁 Script executed:
# Verify the structure of chat.json to see all keys
cat src/i18n/locales/en/chat.json | grep -A 10 "systemInjected"Repository: siteboon/claudecodeui
Length of output: 369
Remove the unused "skill" translation key or implement its classification logic.
The skill key is defined in all locale files (en, zh-CN, ko, ja) but classifyUserMessage in messageTransforms.ts never produces an injectedType of 'skill'. Either implement the classification logic for skill-related messages or remove this unused key.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/i18n/locales/en/chat.json` around lines 242 - 251, The locales define a
"skill" systemInjected key that is never used because classifyUserMessage in
messageTransforms.ts never returns injectedType === 'skill'; either remove the
"skill" entry from the systemInjected objects across locale files (including the
en file shown) or add classification logic in classifyUserMessage to detect
skill-related messages and return 'skill' as injectedType (update the function
in messageTransforms.ts and any related constants/union types to include 'skill'
if missing). Ensure the change keeps locale keys and TypeScript types in sync
(remove unused key from all locales and any type declarations, or implement
detection in classifyUserMessage and update relevant types to include 'skill').
- Remove redundant startsWith check in hook output detection (content.includes already covers the startsWith case) - Remove unused 'skill' i18n key from all 4 locale chat.json files
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/components/chat/utils/messageTransforms.ts (2)
403-408: Hook detection usesincludes()which may cause false positives.Unlike other checks that use
startsWith(), this usesincludes('<user-prompt-submit-hook>')which could match if this tag appears anywhere in user content, including quoted examples or documentation snippets.💡 Consider using startsWith for consistency
- if (content.includes('<user-prompt-submit-hook>')) { + if (content.startsWith('<user-prompt-submit-hook>')) { return { injectedType: 'hook', injectedSummary: 'Hook output', }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/chat/utils/messageTransforms.ts` around lines 403 - 408, Replace the broad contains check in messageTransforms (the block that returns injectedType: 'hook' / injectedSummary: 'Hook output') so it only detects the hook when it appears at the start of the message; change the condition from content.includes('<user-prompt-submit-hook>') to a start-of-message check (e.g., use content.trim().startsWith('<user-prompt-submit-hook>') or an equivalent regex) to avoid false positives when the tag appears later in quoted text or examples.
353-357: Consider adding an explicit type forinjectedTypevalues.
ChatMessage['injectedType']resolves tounknownbecauseChatMessageuses an index signature[key: string]: unknownwithout an explicitinjectedTypeproperty. This works at runtime but loses type safety.💡 Suggested type definition
Add to
src/components/chat/types/types.ts:export type InjectedMessageType = | 'task-notification' | 'system-reminder' | 'background-task-result' | 'hook' | 'command' | 'continuation' | 'other';Then update
ChatMessageandClassifiedMessage:+ injectedType?: InjectedMessageType; + injectedSummary?: string; [key: string]: unknown;interface ClassifiedMessage { - injectedType: ChatMessage['injectedType']; + injectedType: InjectedMessageType; injectedSummary: string; taskStatus?: string; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/chat/utils/messageTransforms.ts` around lines 353 - 357, Define a concrete union type InjectedMessageType (e.g., 'task-notification' | 'system-reminder' | 'background-task-result' | 'hook' | 'command' | 'continuation' | 'other') in your shared chat types file, update the ChatMessage type to include injectedType?: InjectedMessageType instead of relying on the index signature, and change ClassifiedMessage.injectedType to use InjectedMessageType so the compiler no longer resolves it to unknown; ensure any code creating or reading injectedType is adjusted to the new union type (or falls back to 'other') and update imports where ChatMessage or ClassifiedMessage are referenced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/components/chat/utils/messageTransforms.ts`:
- Around line 403-408: Replace the broad contains check in messageTransforms
(the block that returns injectedType: 'hook' / injectedSummary: 'Hook output')
so it only detects the hook when it appears at the start of the message; change
the condition from content.includes('<user-prompt-submit-hook>') to a
start-of-message check (e.g., use
content.trim().startsWith('<user-prompt-submit-hook>') or an equivalent regex)
to avoid false positives when the tag appears later in quoted text or examples.
- Around line 353-357: Define a concrete union type InjectedMessageType (e.g.,
'task-notification' | 'system-reminder' | 'background-task-result' | 'hook' |
'command' | 'continuation' | 'other') in your shared chat types file, update the
ChatMessage type to include injectedType?: InjectedMessageType instead of
relying on the index signature, and change ClassifiedMessage.injectedType to use
InjectedMessageType so the compiler no longer resolves it to unknown; ensure any
code creating or reading injectedType is adjusted to the new union type (or
falls back to 'other') and update imports where ChatMessage or ClassifiedMessage
are referenced.
ℹ️ Review info
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/components/chat/utils/messageTransforms.tssrc/i18n/locales/en/chat.jsonsrc/i18n/locales/ja/chat.jsonsrc/i18n/locales/ko/chat.jsonsrc/i18n/locales/zh-CN/chat.json
🚧 Files skipped from review as they are similar to previous changes (2)
- src/i18n/locales/ja/chat.json
- src/i18n/locales/zh-CN/chat.json
- Tighten TaskMaster detection to require subtasks JSON at start of content, preventing false positives on normal user messages - Add explicit optional properties to ChatMessage interface for system-injected message fields (isSystemInjected, injectedType, injectedSummary, isTaskNotification, skillName, skillDescription)
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/components/chat/utils/messageTransforms.ts (1)
454-455:⚠️ Potential issue | 🟡 MinorTaskMaster detection can still over-classify on the CRITICAL phrase alone.
The
includes(...)check will classify any message containing that phrase as injected, even when it is normal user content (e.g., quoted logs/snippets).💡 Proposed fix
- if (content.trimStart().startsWith('{"subtasks":') || content.includes('CRITICAL: You MUST respond with ONLY a JSON')) { + const trimmed = content.trimStart(); + const hasSubtasksJsonAtStart = trimmed.startsWith('{"subtasks":'); + const hasCriticalPromptAtStart = trimmed.startsWith('CRITICAL: You MUST respond with ONLY a JSON'); + if (hasSubtasksJsonAtStart || hasCriticalPromptAtStart) { return { injectedType: 'other', injectedSummary: 'TaskMaster system prompt', }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/chat/utils/messageTransforms.ts` around lines 454 - 455, The current if in messageTransforms.ts uses content.includes('CRITICAL: You MUST respond with ONLY a JSON') which over-classifies any appearance of that phrase; change the detection to only match when the phrase appears at the start of the message (or at the start of a line) like the existing '{"subtasks":' check — e.g., replace the condition with a startsWith check (use content.trimStart().startsWith('CRITICAL: You MUST respond with ONLY a JSON')) or a single regex anchored to line start (e.g., /^\s*(\{"subtasks":|CRITICAL: You MUST respond with ONLY a JSON)/) so the code in the if block only runs for injected prompts and not quoted/logged snippets. Ensure you update the if that references the content variable accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/chat/types/types.ts`:
- Around line 49-54: The ChatMessage type is missing an explicit taskStatus
property so values assigned in convertSessionMessages fall back to the index
signature and lose type safety; add an optional taskStatus?: string (or a more
specific enum/type if available) to the ChatMessage interface (alongside
isSystemInjected, injectedType, etc.) so convertSessionMessages, its callers,
and consumers have proper typing for taskStatus and IDE/typechecker validation.
In `@src/components/chat/utils/messageTransforms.ts`:
- Around line 359-446: The classifyUserMessage function sometimes misses
injected messages because many branches use content.startsWith(...) against the
raw content; normalize the input once at the top (e.g., compute a
trimmed/normalized string like normalized = content.trimStart() or similar) and
use that normalized value for all prefix-based checks (the various
content.startsWith(...) branches, the '<system-reminder>' and '<command-...>'
checks, the '[Background task completed]' check, 'Caveat:' and continuation
checks) while keeping regexes like taskNotifRegex applied to the original or
normalized text as appropriate; update references in classifyUserMessage so all
startsWith/match checks use the single normalized variable.
---
Duplicate comments:
In `@src/components/chat/utils/messageTransforms.ts`:
- Around line 454-455: The current if in messageTransforms.ts uses
content.includes('CRITICAL: You MUST respond with ONLY a JSON') which
over-classifies any appearance of that phrase; change the detection to only
match when the phrase appears at the start of the message (or at the start of a
line) like the existing '{"subtasks":' check — e.g., replace the condition with
a startsWith check (use content.trimStart().startsWith('CRITICAL: You MUST
respond with ONLY a JSON')) or a single regex anchored to line start (e.g.,
/^\s*(\{"subtasks":|CRITICAL: You MUST respond with ONLY a JSON)/) so the code
in the if block only runs for injected prompts and not quoted/logged snippets.
Ensure you update the if that references the content variable accordingly.
…classification - Add explicit taskStatus property to ChatMessage interface for type safety - Trim content at the start of classifyUserMessage to prevent whitespace from bypassing startsWith-based classification checks
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
Hey @shikihane, thanks for this PR! Can you share some example prompts to see the full effects of this change? |
Summary
classifyUserMessage()function detects system-reminders, command outputs, hook outputs, task notifications, session continuations, and other injected content.Before
System-injected messages were completely hidden from the chat view, making it impossible to see what Claude was responding to when system-reminders, hooks, or command outputs were injected.
After
System-injected messages appear as muted, collapsible inline cards. Users can expand them to see the full content when needed, providing context without cluttering the conversation.
Files Changed
messageTransforms.tsclassifyUserMessage(), replace binary skip logic with classificationMessageComponent.tsxchat.json(en/ja/ko/zh-CN)systemInjectedtranslation keysTest plan
npm run buildpassesnpm run typecheckpassesSummary by CodeRabbit
New Features
Improvements