Merge branch 'main' into dev
This commit is contained in:
commit
4be6578d96
5 changed files with 144 additions and 8 deletions
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://www.producthunt.com/products/claude-devtools?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-claude-devtools" target="_blank" rel="noopener noreferrer">
|
<a href="https://www.producthunt.com/products/claude-devtools?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-claude-devtools" target="_blank" rel="noopener noreferrer">
|
||||||
<img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1080673&theme=light&t=1771309988290" alt="claude-devtools - See everything Claude Code hides from your terminal | Product Hunt" width="250" height="54" />
|
<img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1080673&theme=light" alt="claude-devtools - See everything Claude Code hides from your terminal | Product Hunt" width="250" height="54" />
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,8 +161,7 @@
|
||||||
"pacman"
|
"pacman"
|
||||||
],
|
],
|
||||||
"icon": "resources/icons/png",
|
"icon": "resources/icons/png",
|
||||||
"category": "Development",
|
"category": "Development"
|
||||||
"afterInstall": "resources/afterInstall.sh"
|
|
||||||
},
|
},
|
||||||
"deb": {
|
"deb": {
|
||||||
"afterInstall": "resources/afterInstall.sh"
|
"afterInstall": "resources/afterInstall.sh"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { CodeBlockViewer } from '@renderer/components/chat/viewers';
|
import { CodeBlockViewer, MarkdownViewer } from '@renderer/components/chat/viewers';
|
||||||
|
|
||||||
import type { LinkedToolItem } from '@renderer/types/groups';
|
import type { LinkedToolItem } from '@renderer/types/groups';
|
||||||
|
|
||||||
|
|
@ -20,13 +20,47 @@ export const WriteToolViewer: React.FC<WriteToolViewerProps> = ({ linkedTool })
|
||||||
const filePath = (toolUseResult?.filePath as string) || (linkedTool.input.file_path as string);
|
const filePath = (toolUseResult?.filePath as string) || (linkedTool.input.file_path as string);
|
||||||
const content = (toolUseResult?.content as string) || (linkedTool.input.content as string) || '';
|
const content = (toolUseResult?.content as string) || (linkedTool.input.content as string) || '';
|
||||||
const isCreate = toolUseResult?.type === 'create';
|
const isCreate = toolUseResult?.type === 'create';
|
||||||
|
const isMarkdownFile = /\.mdx?$/i.test(filePath);
|
||||||
|
const [viewMode, setViewMode] = React.useState<'code' | 'preview'>('code');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="mb-1 text-xs text-zinc-500">
|
<div className="mb-1 text-xs text-zinc-500">
|
||||||
{isCreate ? 'Created file' : 'Wrote to file'}
|
{isCreate ? 'Created file' : 'Wrote to file'}
|
||||||
</div>
|
</div>
|
||||||
<CodeBlockViewer fileName={filePath} content={content} startLine={1} />
|
{isMarkdownFile && (
|
||||||
|
<div className="flex items-center justify-end gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setViewMode('code')}
|
||||||
|
className="rounded px-2 py-1 text-xs transition-colors"
|
||||||
|
style={{
|
||||||
|
backgroundColor: viewMode === 'code' ? 'var(--tag-bg)' : 'transparent',
|
||||||
|
color: viewMode === 'code' ? 'var(--tag-text)' : 'var(--color-text-muted)',
|
||||||
|
border: '1px solid var(--tag-border)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Code
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setViewMode('preview')}
|
||||||
|
className="rounded px-2 py-1 text-xs transition-colors"
|
||||||
|
style={{
|
||||||
|
backgroundColor: viewMode === 'preview' ? 'var(--tag-bg)' : 'transparent',
|
||||||
|
color: viewMode === 'preview' ? 'var(--tag-text)' : 'var(--color-text-muted)',
|
||||||
|
border: '1px solid var(--tag-border)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Preview
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isMarkdownFile && viewMode === 'preview' ? (
|
||||||
|
<MarkdownViewer content={content} label="Markdown Preview" copyable />
|
||||||
|
) : (
|
||||||
|
<CodeBlockViewer fileName={filePath} content={content} startLine={1} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -423,16 +423,20 @@ export function buildDisplayItemsFromMessages(
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Plain-text user message (subagent input prompt)
|
// Only treat as subagent input if there are NO tool_result blocks in this message
|
||||||
if (rawText.trim()) {
|
const hasToolResults =
|
||||||
|
Array.isArray(msg.content) &&
|
||||||
|
msg.content.some((b) => b.type === 'tool_result');
|
||||||
|
if (rawText.trim() && !hasToolResults) {
|
||||||
displayItems.push({
|
displayItems.push({
|
||||||
type: 'subagent_input',
|
type: 'subagent_input',
|
||||||
content: rawText.trim(),
|
content: rawText.trim(),
|
||||||
timestamp: msgTimestamp,
|
timestamp: msgTimestamp,
|
||||||
tokenCount: estimateTokens(rawText),
|
tokenCount: estimateTokens(rawText),
|
||||||
});
|
});
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
continue;
|
// Fall through to tool result processing below if message has tool_results
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.type === 'assistant' && Array.isArray(msg.content)) {
|
if (msg.type === 'assistant' && Array.isArray(msg.content)) {
|
||||||
|
|
|
||||||
99
test/renderer/utils/displayItemBuilder.test.ts
Normal file
99
test/renderer/utils/displayItemBuilder.test.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { buildDisplayItemsFromMessages } from '../../../src/renderer/utils/displayItemBuilder';
|
||||||
|
import type { ParsedMessage } from '../../../src/main/types/messages';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to create a minimal ParsedMessage for testing.
|
||||||
|
*/
|
||||||
|
function makeMessage(overrides: Partial<ParsedMessage> & Pick<ParsedMessage, 'type' | 'content'>): ParsedMessage {
|
||||||
|
return {
|
||||||
|
uuid: `msg-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
|
parentUuid: null,
|
||||||
|
timestamp: new Date('2025-01-01T00:00:00Z'),
|
||||||
|
isMeta: false,
|
||||||
|
isSidechain: false,
|
||||||
|
toolCalls: [],
|
||||||
|
toolResults: [],
|
||||||
|
...overrides,
|
||||||
|
} as ParsedMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('buildDisplayItemsFromMessages', () => {
|
||||||
|
describe('subagent tool results with isMeta=false', () => {
|
||||||
|
it('should collect tool results from user messages without isMeta field', () => {
|
||||||
|
// Simulates real subagent JSONL where user messages with tool_result
|
||||||
|
// blocks have isMeta absent (defaults to false after parsing).
|
||||||
|
const toolUseId = 'toolu_test123';
|
||||||
|
|
||||||
|
const assistantMsg = makeMessage({
|
||||||
|
uuid: 'assistant-1',
|
||||||
|
type: 'assistant',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'tool_use',
|
||||||
|
id: toolUseId,
|
||||||
|
name: 'Bash',
|
||||||
|
input: { command: 'echo hello' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: new Date('2025-01-01T00:00:00Z'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is the key scenario: user message with tool_result but isMeta: false
|
||||||
|
// (simulating subagent JSONL where isMeta field is absent)
|
||||||
|
const toolResultMsg = makeMessage({
|
||||||
|
uuid: 'user-result-1',
|
||||||
|
type: 'user',
|
||||||
|
isMeta: false,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'tool_result',
|
||||||
|
tool_use_id: toolUseId,
|
||||||
|
content: 'hello\n',
|
||||||
|
is_error: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
toolResults: [
|
||||||
|
{
|
||||||
|
toolUseId: toolUseId,
|
||||||
|
content: 'hello\n',
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: new Date('2025-01-01T00:00:01Z'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = buildDisplayItemsFromMessages([assistantMsg, toolResultMsg], []);
|
||||||
|
|
||||||
|
const toolItems = items.filter((item) => item.type === 'tool');
|
||||||
|
expect(toolItems).toHaveLength(1);
|
||||||
|
|
||||||
|
const tool = toolItems[0];
|
||||||
|
if (tool.type !== 'tool') throw new Error('Expected tool item');
|
||||||
|
|
||||||
|
// The critical assertion: result must be present, not orphaned
|
||||||
|
expect(tool.tool.isOrphaned).toBe(false);
|
||||||
|
expect(tool.tool.result).toBeDefined();
|
||||||
|
expect(tool.tool.result?.content).toBe('hello\n');
|
||||||
|
expect(tool.tool.name).toBe('Bash');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should still render subagent_input for plain text user messages without tool results', () => {
|
||||||
|
const userMsg = makeMessage({
|
||||||
|
uuid: 'user-input-1',
|
||||||
|
type: 'user',
|
||||||
|
isMeta: false,
|
||||||
|
content: 'Please run the tests',
|
||||||
|
toolResults: [],
|
||||||
|
timestamp: new Date('2025-01-01T00:00:00Z'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = buildDisplayItemsFromMessages([userMsg], []);
|
||||||
|
|
||||||
|
const inputItems = items.filter((item) => item.type === 'subagent_input');
|
||||||
|
expect(inputItems).toHaveLength(1);
|
||||||
|
if (inputItems[0].type !== 'subagent_input') throw new Error('Expected subagent_input');
|
||||||
|
expect(inputItems[0].content).toBe('Please run the tests');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue