Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,87 @@ describe('AgentActionRow — 行主文案', () => {
expect(document.querySelector('[data-agent-action-file-chip="true"]')).toBeTruthy();
});

it('pi bash:小写工具名照样解析出意图动词,不再是「调用 bash」', () => {
render(
createElement(AgentActionRow, {
message: mkTool('t1', 'bash', { command: 'git status' }),
}),
);
expect(screen.getByText('chat.agentActionRow.verb.gitStatus')).toBeTruthy();
expect(screen.queryByText('chat.agentActionRow.verb.used')).toBeNull();
expect(screen.queryByText('bash')).toBeNull();
});

it('pi bash:无法分类的命令回退为运行动词 + 命令原文', () => {
render(
createElement(AgentActionRow, {
message: mkTool('t1', 'bash', { command: 'docker ps' }),
}),
);
expect(screen.getByText('chat.agentActionRow.verb.ran')).toBeTruthy();
expect(screen.getByText('docker ps')).toBeTruthy();
});

it('pi read:path 字段渲染成文件 chip 与读取动词', () => {
render(
createElement(AgentActionRow, {
message: mkTool('t1', 'read', { path: '/repo/src/app.ts' }),
}),
);
expect(screen.getByText('chat.agentActionRow.verb.read')).toBeTruthy();
expect(screen.getByText('app.ts')).toBeTruthy();
expect(document.querySelector('[data-agent-action-file-chip="true"]')).toBeTruthy();
});

it('pi grep / find:搜索动词 + 搜索目标', () => {
const { rerender } = render(
createElement(AgentActionRow, {
message: mkTool('t1', 'grep', { pattern: 'TODO', path: 'src/' }),
}),
);
expect(screen.getByText('chat.agentActionRow.verb.searched')).toBeTruthy();
expect(screen.getByText('TODO')).toBeTruthy();
rerender(
createElement(AgentActionRow, {
message: mkTool('t1', 'find', { pattern: '**/*.spec.ts' }),
}),
);
expect(screen.getByText('chat.agentActionRow.verb.searched')).toBeTruthy();
expect(screen.getByText('**/*.spec.ts')).toBeTruthy();
});

it('pi write:创建动词 + 行内 +N 统计,点击进共享 diff lightbox', () => {
render(
createElement(AgentActionRow, {
message: mkTool('t1', 'write', { path: '/repo/src/new.ts', content: 'a\nb' }),
}),
);
expect(screen.getByText('chat.agentActionRow.verb.created')).toBeTruthy();
expect(screen.getByText('new.ts')).toBeTruthy();
expect(screen.getByText('+2')).toBeTruthy();
fireEvent.click(screen.getByRole('button'));
expect(document.body.textContent).toContain('"kind":"diff"');
expect(document.body.textContent).toContain('"filePath":"/repo/src/new.ts"');
});

it('pi edit:edits[].oldText/newText 汇成编辑动词与 diff lightbox', () => {
render(
createElement(AgentActionRow, {
message: mkTool('t1', 'edit', {
path: '/repo/src/app.ts',
edits: [{ oldText: 'old', newText: 'new' }],
}),
}),
);
expect(screen.getByText('chat.agentActionRow.verb.edited')).toBeTruthy();
expect(screen.getByText('app.ts')).toBeTruthy();
expect(screen.getByText('+1')).toBeTruthy();
expect(screen.getByText('-1')).toBeTruthy();
fireEvent.click(screen.getByRole('button'));
expect(document.body.textContent).toContain('"oldString":"old"');
expect(document.body.textContent).toContain('"newString":"new"');
});

it('状态图标:running / done 经 aria-label 可达,缺省为 done', () => {
const { rerender } = render(
createElement(AgentActionRow, {
Expand Down
24 changes: 24 additions & 0 deletions apps/desktop/src/renderer/__tests__/diffStats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,30 @@ describe('statsForToolCall', () => {
expect(statsForToolCall('MultiEdit', { edits: [] })).toEqual({ add: 0, del: 0 });
});

it('pi edit: sums edits[].oldText/newText like MultiEdit', () => {
const stats = statsForToolCall('edit', {
path: '/foo',
edits: [
{ oldText: 'a', newText: 'b' }, // +1 -1
{ oldText: '', newText: 'x\ny' }, // +2 -0
],
});
expect(stats).toEqual({ add: 3, del: 1 });
});

it('pi write: full content as +N -0', () => {
expect(statsForToolCall('write', { path: '/foo', content: 'a\nb' })).toEqual({
add: 2,
del: 0,
});
});

it('pi read-only tools stay null', () => {
expect(statsForToolCall('read', { path: '/foo' })).toBeNull();
expect(statsForToolCall('bash', { command: 'ls' })).toBeNull();
expect(statsForToolCall('grep', { pattern: 'foo' })).toBeNull();
});

it('file_change: sums unified diffs across all changed files', () => {
expect(statsForToolCall('file_change', {
changes: [
Expand Down
10 changes: 10 additions & 0 deletions apps/desktop/src/renderer/__tests__/verbAggregator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ describe('verbForTool', () => {
expect(verbForTool('web_search')).toBe('fetched');
});

it('maps pi builtin lowercase tools', () => {
expect(verbForTool('bash')).toBe('ran');
expect(verbForTool('read')).toBe('read');
expect(verbForTool('ls')).toBe('read');
expect(verbForTool('edit')).toBe('edited');
expect(verbForTool('write')).toBe('created');
expect(verbForTool('grep')).toBe('searched');
expect(verbForTool('find')).toBe('searched');
});

it('falls back to "used" for unknown tools', () => {
expect(verbForTool('FooBar')).toBe('used');
expect(verbForTool('')).toBe('used');
Expand Down
38 changes: 33 additions & 5 deletions apps/desktop/src/renderer/components/chat/AgentActionRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ import { TextLightbox } from './TextLightbox';
import { ToolPayloadLightbox, type ToolPayloadMode } from './ToolPayloadLightbox';
import { useFileChipContextMenu } from './useFileChipContextMenu';

const FILE_PATH_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'Read']);
// CC 大写 + pi 小写(pi 内置工具名全小写、文件字段为 path,见 toolUseDescriptor.ts)。
const FILE_PATH_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'Read', 'edit', 'write', 'read']);
Comment thread
zqchris marked this conversation as resolved.
Outdated

/**
* v10 (2026-04-20): 命令类工具(Bash/Grep/Glob/WebFetch/WebSearch/...)的
Expand Down Expand Up @@ -451,14 +452,16 @@ function formatInlineInput(
if (!inp) return '';
switch (toolName) {
case 'Bash':
case 'bash':
case 'exec': {
// description 已上移为行主文案(issue #450),这里只展示命令原文 + cwd,
// 避免同一句话在折叠行和展开区重复出现。
const cmd = commandDisplayText(inp);
const cwd = typeof inp.cwd === 'string' && inp.cwd ? `cwd: ${inp.cwd}` : '';
return cwd ? `${cmd}\n${cwd}` : cmd;
}
case 'Grep': {
case 'Grep':
case 'grep': {
const pattern = typeof inp.pattern === 'string' ? inp.pattern : '';
const path = typeof inp.path === 'string' ? inp.path : '';
const glob = typeof inp.glob === 'string' ? inp.glob : '';
Expand All @@ -474,11 +477,17 @@ function formatInlineInput(
.filter(Boolean)
.join('\n');
}
case 'Glob': {
case 'Glob':
case 'find': {
const pattern = typeof inp.pattern === 'string' ? inp.pattern : '';
const path = typeof inp.path === 'string' ? inp.path : '';
return path ? `${pattern}\nin: ${path}` : pattern;
}
case 'ls': {
// pi ls:path 可缺省(默认当前目录)。
const path = typeof inp.path === 'string' ? inp.path : '';
return path;
}
case 'WebFetch': {
const url = typeof inp.url === 'string' ? inp.url : '';
const prompt = typeof inp.prompt === 'string' ? inp.prompt : '';
Expand Down Expand Up @@ -536,7 +545,7 @@ function buildDiffPayload(
],
};
}
if (toolName === 'Write') {
if (toolName === 'Write' || toolName === 'write') {
const c = typeof inp.content === 'string' ? inp.content : '';
return {
kind: 'diff',
Expand All @@ -549,6 +558,25 @@ function buildDiffPayload(
],
};
}
// pi edit:edits[].oldText/newText,与 MultiEdit 同款多段 diff 呈现。
if (toolName === 'edit') {
const edits = Array.isArray(inp.edits) ? inp.edits : [];
Comment thread
zqchris marked this conversation as resolved.
Outdated
return {
kind: 'diff',
files: [
{
key: filePath,
filePath,
diffs: edits.map((e, index) => {
const er = e as Record<string, unknown> | null;
const o = er && typeof er.oldText === 'string' ? er.oldText : '';
const n = er && typeof er.newText === 'string' ? er.newText : '';
return { key: `edit:${index}`, oldString: String(o), newString: String(n) };
}),
},
],
};
}
if (toolName === 'MultiEdit') {
const edits = Array.isArray(inp.edits) ? inp.edits : [];
return {
Expand Down Expand Up @@ -719,7 +747,7 @@ export function AgentActionRow({
return;
}
triggerRef.current = anchor;
if (toolName === 'Read' && filePath) {
if ((toolName === 'Read' || toolName === 'read') && filePath) {
// 模型可能给相对路径(runtime 按会话工作目录解析后 Read 照样成功),而
// 预览 / 定位 IPC 一律要求绝对路径 —— 先按 workingDir 补齐,镜像 runtime
// 语义,保证 chip 打开的就是 agent 实际读到的那个文件。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

import type { ChatMessage } from '@/lib/makerChatStore';

const EDIT_TOOL_NAMES = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
// CC 大写(file_path 字段)+ pi 小写(path 字段,见 toolUseDescriptor.ts 数据来源约定)。
const EDIT_TOOL_NAMES = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'edit', 'write']);

function slashPath(p: string): string {
return p.replace(/\\/g, '/').replace(/\/+/g, '/');
Expand Down Expand Up @@ -75,7 +76,7 @@ function extractToolFilePaths(msg: ChatMessage): string[] {
const input = (msg.toolInput as Record<string, unknown> | null) ?? null;
if (msg.toolName === 'file_change') return collectCodexFileChangePaths(input);
if (!EDIT_TOOL_NAMES.has(msg.toolName)) return [];
const filePath = input?.file_path;
const filePath = input?.file_path ?? input?.path;
return typeof filePath === 'string' && filePath ? [filePath] : [];
}

Expand Down
18 changes: 17 additions & 1 deletion apps/desktop/src/renderer/lib/agent-actions/diffStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,28 @@ export function statsForToolCall(
return computeDiffStats(o, n);
}

if (toolName === 'Write') {
if (toolName === 'Write' || toolName === 'write') {
const c = typeof inp.content === 'string' ? inp.content : '';
// All-add: oldStr = ''. Surface as `+N -0` per ADR-5.
return computeDiffStats('', c);
}

// pi edit:edits[].oldText/newText,逐条求和(与 MultiEdit 同形态)。
if (toolName === 'edit') {
const edits = Array.isArray(inp.edits) ? inp.edits : [];
let add = 0;
let del = 0;
for (const e of edits) {
const er = e as Record<string, unknown> | null;
const o = er && typeof er.oldText === 'string' ? er.oldText : '';
const n = er && typeof er.newText === 'string' ? er.newText : '';
const s = computeDiffStats(o, n);
add += s.add;
del += s.del;
}
return { add, del };
}

if (toolName === 'MultiEdit') {
const edits = Array.isArray(inp.edits) ? inp.edits : [];
let add = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ const TOOL_TO_VERB: Record<string, Verb> = {
WebFetch: 'fetched',
WebSearch: 'fetched',
web_search: 'fetched',
// pi 内置工具(全小写,见 toolUseDescriptor.ts 数据来源约定)。
bash: 'ran',
read: 'read',
ls: 'read',
edit: 'edited',
write: 'created',
grep: 'searched',
find: 'searched',
};

const ORDER: Verb[] = [
Expand Down
62 changes: 62 additions & 0 deletions packages/maker-shared/src/__tests__/toolUseDescriptor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,68 @@ describe('describeToolUse — file tools', () => {
});
});

describe('describeToolUse — pi builtin tools (lowercase, path field)', () => {
it('maps pi bash to command with local intent (schema has no description field)', () => {
expect(describeToolUse('bash', { command: 'git status' })).toEqual({
kind: 'command',
toolName: 'bash',
command: 'git status',
intent: { action: 'gitStatus' },
});
expect(describeToolUse('bash', { command: 'docker ps' })).toEqual({
kind: 'command',
toolName: 'bash',
command: 'docker ps',
});
});

it('maps pi read/edit/write/ls to file actions via the path field', () => {
expect(describeToolUse('read', { path: '/repo/src/app.ts' })).toEqual({
kind: 'file',
toolName: 'read',
action: 'read',
filePath: '/repo/src/app.ts',
fileName: 'app.ts',
});
expect(describeToolUse('edit', {
path: '/repo/a.ts',
edits: [{ oldText: 'a', newText: 'b' }],
})).toMatchObject({ kind: 'file', action: 'edit', fileName: 'a.ts' });
expect(describeToolUse('write', { path: '/repo/new.ts', content: 'x' })).toMatchObject({
kind: 'file',
action: 'create',
fileName: 'new.ts',
});
expect(describeToolUse('ls', { path: '/repo/src' })).toMatchObject({
kind: 'file',
action: 'read',
fileName: 'src',
});
});

it('degrades pi ls without path (defaults to cwd) to generic', () => {
expect(describeToolUse('ls', {})).toEqual({ kind: 'generic', toolName: 'ls' });
});

it('maps pi grep to grep-mode and pi find (glob pattern) to glob-mode search', () => {
expect(describeToolUse('grep', { pattern: 'TODO', path: 'src/', glob: '*.ts' })).toEqual({
kind: 'search',
toolName: 'grep',
mode: 'grep',
pattern: 'TODO',
path: 'src/',
glob: '*.ts',
});
expect(describeToolUse('find', { pattern: '**/*.spec.ts' })).toEqual({
kind: 'search',
toolName: 'find',
mode: 'glob',
pattern: '**/*.spec.ts',
});
expect(describeToolUse('grep', {})).toEqual({ kind: 'generic', toolName: 'grep' });
});
});

describe('describeToolUse — Codex file_change', () => {
it('normalizes add/update/delete and rename changes', () => {
expect(describeToolUse('file_change', {
Expand Down
23 changes: 22 additions & 1 deletion packages/maker-shared/src/payloadSummary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,14 @@ export function formatPayloadToolUseSummary(toolName: string, input: unknown): s
Bash: ['command'],
Glob: ['pattern'],
Grep: ['pattern'],
// pi 内置工具:名字全小写、文件参数为 path(见 toolUseDescriptor.ts 数据来源约定)。
read: ['path'],
edit: ['path'],
write: ['path'],
ls: ['path'],
bash: ['command'],
grep: ['pattern'],
find: ['pattern'],
};
const keys = keyParamMap[toolName];
if (!keys) return `${toolName}()`;
Expand All @@ -240,10 +248,23 @@ export function buildPayloadToolDiff(toolName: string, input: unknown): PayloadT
const newString = typeof inp.new_string === 'string' ? inp.new_string : '';
return createPayloadToolDiff(filePath, [{ key: 'edit:0', oldString, newString }]);
}
if (toolName === 'Write') {
if (toolName === 'Write' || toolName === 'write') {
const newString = typeof inp.content === 'string' ? inp.content : '';
return createPayloadToolDiff(filePath, [{ key: 'write:0', oldString: '', newString }]);
}
// pi edit:edits[].oldText/newText(与 MultiEdit 同款多段 diff)。
if (toolName === 'edit') {
const edits = Array.isArray(inp.edits) ? inp.edits : [];
return createPayloadToolDiff(filePath, edits.map((edit, index) => {
const record = readPayloadRecord(edit);
return {
key: `edit:${index}`,
oldString: typeof record?.oldText === 'string' ? record.oldText : '',
newString: typeof record?.newText === 'string' ? record.newText : '',
label: `Edit ${index + 1}/${edits.length}`,
};
}));
}
if (toolName === 'MultiEdit') {
const edits = Array.isArray(inp.edits) ? inp.edits : [];
return createPayloadToolDiff(filePath, edits.map((edit, index) => {
Expand Down
Loading
Loading