Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 148 additions & 23 deletions src/components/chat/utils/messageTransforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ const toAbsolutePath = (projectPath: string, filePath?: string) => {
};

export const calculateDiff = (oldStr: string, newStr: string): DiffLine[] => {
const oldLines = oldStr.split('\n');
const newLines = newStr.split('\n');
const oldLines = (oldStr ?? '').split('\n');
const newLines = (newStr ?? '').split('\n');

// Use LCS alignment so insertions/deletions don't cascade into a full-file "changed" diff.
const lcsTable: number[][] = Array.from({ length: oldLines.length + 1 }, () =>
Expand Down Expand Up @@ -350,6 +350,116 @@ export const convertCursorSessionMessages = (blobs: CursorBlob[], projectPath: s
return converted;
};

interface ClassifiedMessage {
injectedType: ChatMessage['injectedType'];
injectedSummary: string;
taskStatus?: string;
}

const classifyUserMessage = (content: string): ClassifiedMessage | null => {
// 1. <task-notification> — parse status/summary
const taskNotifRegex = /<task-notification>\s*<task-id>[^<]*<\/task-id>\s*(?:<tool-use-id>[^<]*<\/tool-use-id>\s*)?<output-file>[^<]*<\/output-file>\s*<status>([^<]*)<\/status>\s*<summary>([^<]*)<\/summary>\s*<\/task-notification>/;
const taskMatch = taskNotifRegex.exec(content);
if (taskMatch) {
return {
injectedType: 'task-notification',
injectedSummary: taskMatch[2]?.trim() || 'Background task finished',
taskStatus: taskMatch[1]?.trim() || 'completed',
};
}

// 2. <system-reminder> — extract first meaningful line as summary
if (content.startsWith('<system-reminder>')) {
const innerMatch = content.match(/<system-reminder>\s*\n?(.*)/s);
const firstLine = innerMatch?.[1]?.split('\n').find(l => l.trim())?.trim() || 'System reminder';
return {
injectedType: 'system-reminder',
injectedSummary: firstLine.length > 80 ? firstLine.slice(0, 77) + '...' : firstLine,
};
}

// 3. Command-related tags
if (content.startsWith('<command-name>')) {
const nameMatch = content.match(/<command-name>([^<]*)<\/command-name>/);
return {
injectedType: 'command',
injectedSummary: nameMatch?.[1]?.trim() || 'Command',
};
}
if (content.startsWith('<command-message>') || content.startsWith('<command-args>')) {
return {
injectedType: 'command',
injectedSummary: 'Command arguments',
};
}
if (content.startsWith('<local-command-stdout>') || content.startsWith('<local-command-caveat>')) {
return {
injectedType: 'command',
injectedSummary: 'Command output',
};
}

// 4. Hook output
if (content.includes('<user-prompt-submit-hook>')) {
return {
injectedType: 'hook',
injectedSummary: 'Hook output',
};
}

// 5. Background task completion results (injected by server after subagent finishes)
if (content.startsWith('[Background task completed]')) {
const agentMatch = content.match(/Agent\s+(\S+)\s+finished/);
const agentId = agentMatch?.[1] || '';
const summary = agentId ? `Agent ${agentId} completed` : 'Background task completed';
return {
injectedType: 'background-task-result',
injectedSummary: summary,
};
}

// 6. Session continuation / interruption / errors
if (content.startsWith('Caveat:')) {
return {
injectedType: 'continuation',
injectedSummary: content.length > 80 ? content.slice(0, 77) + '...' : content,
};
}
if (content.startsWith('This session is being continued from a previous') || content.startsWith('This session is a continuation')) {
return {
injectedType: 'continuation',
injectedSummary: 'Session continuation',
};
}
if (content.startsWith('[Request interrupted')) {
return {
injectedType: 'continuation',
injectedSummary: 'Request interrupted',
};
}
if (content.startsWith('Invalid API key')) {
return {
injectedType: 'other',
injectedSummary: 'Invalid API key',
};
}
if (content === 'Warmup') {
return {
injectedType: 'other',
injectedSummary: 'Warmup',
};
}
// TaskMaster system prompts (should be hidden)
if (content.includes('{"subtasks":') || content.includes('CRITICAL: You MUST respond with ONLY a JSON')) {
return {
injectedType: 'other',
injectedSummary: 'TaskMaster system prompt',
};
}

return null;
};

export const convertSessionMessages = (rawMessages: any[]): ChatMessage[] => {
const converted: ChatMessage[] = [];
const toolResults = new Map<
Expand Down Expand Up @@ -391,38 +501,53 @@ export const convertSessionMessages = (rawMessages: any[]): ChatMessage[] => {
content = decodeHtmlEntities(String(message.message.content));
}

const shouldSkip =
!content ||
content.startsWith('<command-name>') ||
content.startsWith('<command-message>') ||
content.startsWith('<command-args>') ||
content.startsWith('<local-command-stdout>') ||
content.startsWith('<system-reminder>') ||
content.startsWith('Caveat:') ||
content.startsWith('This session is being continued from a previous') ||
content.startsWith('[Request interrupted');

if (!shouldSkip) {
// Parse <task-notification> blocks into compact system messages
const taskNotifRegex = /<task-notification>\s*<task-id>[^<]*<\/task-id>\s*<output-file>[^<]*<\/output-file>\s*<status>([^<]*)<\/status>\s*<summary>([^<]*)<\/summary>\s*<\/task-notification>/g;
const taskNotifMatch = taskNotifRegex.exec(content);
if (taskNotifMatch) {
const status = taskNotifMatch[1]?.trim() || 'completed';
const summary = taskNotifMatch[2]?.trim() || 'Background task finished';
if (!content) {
return;
}

const classified = classifyUserMessage(content);
if (classified) {
// Task notifications keep existing compact pill rendering (backward compat)
if (classified.injectedType === 'task-notification') {
converted.push({
type: 'assistant',
content: summary,
content: classified.injectedSummary || content,
timestamp: message.timestamp || new Date().toISOString(),
isTaskNotification: true,
taskStatus: status,
taskStatus: classified.taskStatus || 'completed',
isSystemInjected: true,
injectedType: 'task-notification',
injectedSummary: classified.injectedSummary,
});
} else if (classified.injectedType === 'background-task-result') {
// Background task result: collapsible card, collapsed by default
const resultIdx = content.indexOf('\n\nResult:\n');
const resultBody = resultIdx >= 0 ? content.slice(resultIdx + '\n\nResult:\n'.length) : content;
converted.push({
type: 'assistant',
content: resultBody,
timestamp: message.timestamp || new Date().toISOString(),
isSystemInjected: true,
injectedType: 'background-task-result',
injectedSummary: classified.injectedSummary,
});
} else {
// All other system-injected messages: show with muted styling
converted.push({
type: 'user',
content: unescapeWithMathProtection(content),
content,
timestamp: message.timestamp || new Date().toISOString(),
isSystemInjected: true,
injectedType: classified.injectedType,
injectedSummary: classified.injectedSummary,
});
}
} else {
converted.push({
type: 'user',
content: unescapeWithMathProtection(content),
timestamp: message.timestamp || new Date().toISOString(),
});
}
return;
}
Expand Down
55 changes: 53 additions & 2 deletions src/components/chat/view/subcomponents/MessageComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,31 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
return (
<div
ref={messageRef}
className={`chat-message ${message.type} ${isGrouped ? 'grouped' : ''} ${message.type === 'user' ? 'flex justify-end px-3 sm:px-0' : 'px-3 sm:px-0'}`}
className={`chat-message ${message.type} ${isGrouped ? 'grouped' : ''} ${message.type === 'user' && !message.isSystemInjected ? 'flex justify-end px-3 sm:px-0' : 'px-3 sm:px-0'}`}
>
{message.type === 'user' ? (
{message.isSystemInjected && !message.isTaskNotification && message.injectedType !== 'background-task-result' ? (
/* System-injected message — muted, collapsed by default */
<div className="w-full">
<details className="group">
<summary className="cursor-pointer flex items-center gap-2 py-1 px-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800/60 transition-colors select-none">
<svg className="w-3 h-3 text-gray-400 dark:text-gray-500 transition-transform group-open:rotate-90 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300">
{t(`systemInjected.${message.injectedType || 'other'}`)}
</span>
<span className="text-xs text-gray-500 dark:text-gray-400 truncate">
{String(message.injectedSummary || '')}
</span>
</summary>
<div className="mt-1 ml-5 border-l-2 border-gray-300 dark:border-gray-600 pl-3">
<pre className="text-xs text-gray-600 dark:text-gray-400 whitespace-pre-wrap break-words font-mono bg-gray-50 dark:bg-gray-800/40 rounded p-2 max-h-64 overflow-y-auto">
{message.content}
</pre>
</div>
</details>
</div>
) : message.type === 'user' ? (
/* User message bubble on the right */
<div className="flex items-end space-x-0 sm:space-x-3 w-full sm:w-auto sm:max-w-[85%] md:max-w-md lg:max-w-lg xl:max-w-xl">
<div className="bg-blue-600 text-white rounded-2xl rounded-br-md px-3 sm:px-4 py-2 shadow-sm flex-1 sm:flex-initial group">
Expand Down Expand Up @@ -166,6 +188,35 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
</div>
)}
</div>
) : message.injectedType === 'background-task-result' ? (
/* Background task result — collapsible card, collapsed by default */
<div className="w-full">
<details className="group border border-emerald-200 dark:border-emerald-800/60 rounded-lg overflow-hidden bg-emerald-50/50 dark:bg-emerald-900/10">
<summary className="cursor-pointer flex items-center gap-3 py-2.5 px-3 hover:bg-emerald-100/60 dark:hover:bg-emerald-900/20 transition-colors select-none">
<div className="flex items-center justify-center w-6 h-6 bg-emerald-500 dark:bg-emerald-600 rounded-full flex-shrink-0">
<svg className="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<div className="flex-1 min-w-0">
<span className="text-sm font-medium text-emerald-900 dark:text-emerald-100">
{String(message.injectedSummary || '') || t('systemInjected.background-task-result')}
</span>
</div>
<span className="text-[10px] text-emerald-600/70 dark:text-emerald-400/60 flex-shrink-0">
{formattedTime}
</span>
<svg className="w-4 h-4 text-emerald-500 dark:text-emerald-400 transition-transform group-open:rotate-90 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</summary>
<div className="border-t border-emerald-200 dark:border-emerald-800/60 bg-white dark:bg-gray-900 p-3">
<pre className="text-xs text-gray-700 dark:text-gray-300 whitespace-pre-wrap break-words font-mono max-h-96 overflow-y-auto">
{message.content}
</pre>
</div>
</details>
</div>
) : message.isTaskNotification ? (
/* Compact task notification on the left */
<div className="w-full">
Expand Down
9 changes: 9 additions & 0 deletions src/i18n/locales/en/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -238,5 +238,14 @@
},
"tasks": {
"nextTaskPrompt": "Start the next task"
},
"systemInjected": {
"system-reminder": "System Reminder",
"task-notification": "Task Notification",
"background-task-result": "Background Task Result",
"hook": "Hook Output",
"command": "Command",
"continuation": "Session Info",
"other": "System"
}
Comment on lines +242 to 250
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 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 f

Repository: 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 -100

Repository: 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 2

Repository: siteboon/claudecodeui

Length of output: 3683


🏁 Script executed:

# Search for any reference to "skill" in the entire codebase
rg -n "skill" --max-count=20

Repository: 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').

}
9 changes: 9 additions & 0 deletions src/i18n/locales/ja/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -205,5 +205,14 @@
"runCommand": "{{projectName}}で{{command}}を実行",
"startCli": "{{projectName}}でClaude CLIを起動しています",
"defaultCommand": "コマンド"
},
"systemInjected": {
"system-reminder": "システムリマインダー",
"task-notification": "タスク通知",
"background-task-result": "バックグラウンドタスク結果",
"hook": "フック出力",
"command": "コマンド",
"continuation": "セッション情報",
"other": "システム"
}
}
9 changes: 9 additions & 0 deletions src/i18n/locales/ko/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,5 +220,14 @@
},
"tasks": {
"nextTaskPrompt": "다음 작업 시작"
},
"systemInjected": {
"system-reminder": "시스템 알림",
"task-notification": "작업 알림",
"background-task-result": "백그라운드 작업 결과",
"hook": "Hook 출력",
"command": "명령어",
"continuation": "세션 정보",
"other": "시스템"
}
}
9 changes: 9 additions & 0 deletions src/i18n/locales/zh-CN/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,5 +220,14 @@
},
"tasks": {
"nextTaskPrompt": "开始下一个任务"
},
"systemInjected": {
"system-reminder": "系统提醒",
"task-notification": "任务通知",
"background-task-result": "后台任务结果",
"hook": "Hook 输出",
"command": "命令",
"continuation": "会话信息",
"other": "系统"
}
}