Show OpenCode tool activity consistently (#1171)
* fix(server): map OpenCode tool details * test(server): update OpenCode history client stub
This commit is contained in:
@@ -1082,6 +1082,171 @@ describe("OpenCode adapter startTurn error handling", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("streamHistory maps persisted OpenCode tool parts through canonical detail branches", async () => {
|
||||
const patchText = [
|
||||
"*** Begin Patch",
|
||||
"*** Delete File: /tmp/repo/src/App.tsx",
|
||||
"*** End Patch",
|
||||
].join("\n");
|
||||
|
||||
const fakeClient = {
|
||||
session: {
|
||||
get: vi.fn().mockResolvedValue({
|
||||
data: { revert: undefined },
|
||||
error: undefined,
|
||||
}),
|
||||
messages: vi.fn().mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
info: {
|
||||
id: "msg_assistant",
|
||||
sessionID: "ses_unit_test",
|
||||
role: "assistant",
|
||||
},
|
||||
parts: [
|
||||
{
|
||||
id: "part-grep",
|
||||
sessionID: "ses_unit_test",
|
||||
messageID: "msg_assistant",
|
||||
type: "tool",
|
||||
tool: "grep",
|
||||
callID: "call-grep",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { pattern: "sendCorrelatedSessionRequest" },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "part-skill",
|
||||
sessionID: "ses_unit_test",
|
||||
messageID: "msg_assistant",
|
||||
type: "tool",
|
||||
tool: "skill",
|
||||
callID: "call-skill",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { name: "diagnose" },
|
||||
output: '<skill_content name="diagnose"># Skill: diagnose</skill_content>',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "part-apply-patch",
|
||||
sessionID: "ses_unit_test",
|
||||
messageID: "msg_assistant",
|
||||
type: "tool",
|
||||
tool: "apply_patch",
|
||||
callID: "call-apply-patch",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { patchText },
|
||||
output: "Success. Updated the following files:\nD /tmp/repo/src/App.tsx",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "part-todowrite",
|
||||
sessionID: "ses_unit_test",
|
||||
messageID: "msg_assistant",
|
||||
type: "tool",
|
||||
tool: "todowrite",
|
||||
callID: "call-todowrite",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: {
|
||||
todos: [
|
||||
{
|
||||
content: "Inspect current directory and existing files",
|
||||
status: "completed",
|
||||
priority: "high",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
error: undefined,
|
||||
}),
|
||||
},
|
||||
} as never;
|
||||
|
||||
const session = new __openCodeInternals.OpenCodeAgentSession(
|
||||
{ provider: "opencode", cwd: "/tmp/repo" },
|
||||
fakeClient,
|
||||
"ses_unit_test",
|
||||
createTestLogger(),
|
||||
);
|
||||
|
||||
const history: AgentStreamEvent[] = [];
|
||||
for await (const event of session.streamHistory()) {
|
||||
history.push(event);
|
||||
}
|
||||
|
||||
expect(history).toEqual([
|
||||
{
|
||||
type: "timeline",
|
||||
provider: "opencode",
|
||||
item: {
|
||||
type: "tool_call",
|
||||
callId: "call-grep",
|
||||
name: "grep",
|
||||
status: "completed",
|
||||
detail: {
|
||||
type: "search",
|
||||
query: "sendCorrelatedSessionRequest",
|
||||
toolName: "grep",
|
||||
},
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "timeline",
|
||||
provider: "opencode",
|
||||
item: {
|
||||
type: "tool_call",
|
||||
callId: "call-skill",
|
||||
name: "skill",
|
||||
status: "completed",
|
||||
detail: {
|
||||
type: "plain_text",
|
||||
label: "diagnose",
|
||||
icon: "sparkles",
|
||||
text: '<skill_content name="diagnose"># Skill: diagnose</skill_content>',
|
||||
},
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "timeline",
|
||||
provider: "opencode",
|
||||
item: {
|
||||
type: "tool_call",
|
||||
callId: "call-apply-patch",
|
||||
name: "apply_patch",
|
||||
status: "completed",
|
||||
detail: {
|
||||
type: "edit",
|
||||
filePath: "/tmp/repo/src/App.tsx",
|
||||
unifiedDiff: [
|
||||
"diff --git a//tmp/repo/src/App.tsx b//tmp/repo/src/App.tsx",
|
||||
"--- a//tmp/repo/src/App.tsx",
|
||||
"+++ /dev/null",
|
||||
].join("\n"),
|
||||
},
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "timeline",
|
||||
provider: "opencode",
|
||||
item: {
|
||||
type: "todo",
|
||||
items: [{ text: "Inspect current directory and existing files", completed: true }],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("emits turn_failed when client.session.promptAsync throws synchronously", async () => {
|
||||
// Yield the server-connected event, then park forever. The adapter waits
|
||||
// for that first event before sending the prompt.
|
||||
|
||||
@@ -884,6 +884,17 @@ function buildOpenCodeReplayPartTimelineEvent(params: {
|
||||
if (part.type !== "tool") {
|
||||
return null;
|
||||
}
|
||||
if (isOpenCodeTodoWriteToolPart(part)) {
|
||||
const todos = readOpenCodeTodoItemsFromToolPart(part);
|
||||
if (!todos) {
|
||||
return null;
|
||||
}
|
||||
return buildOpenCodeReplayTimelineEvent({
|
||||
item: mapOpenCodeTodosToTimelineItems(todos),
|
||||
message,
|
||||
part,
|
||||
});
|
||||
}
|
||||
const parsedToolPart = OpencodeToolPartToTimelineItemSchema.safeParse(part);
|
||||
if (!parsedToolPart.success || !parsedToolPart.data) {
|
||||
return null;
|
||||
@@ -1537,6 +1548,56 @@ function readNonEmptyString(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
||||
}
|
||||
|
||||
function isOpenCodeTodoWriteToolPart(part: OpenCodeToolPartEventPart | OpenCodePart): boolean {
|
||||
return part.type === "tool" && part.tool.trim().toLowerCase() === "todowrite";
|
||||
}
|
||||
|
||||
function readOpenCodeTodoItems(
|
||||
value: unknown,
|
||||
): Array<{ content?: string | null; status?: string | null }> | null {
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
return readOpenCodeTodoItems(JSON.parse(value));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.flatMap((entry) => {
|
||||
const record = readOpenCodeRecord(entry);
|
||||
if (!record) {
|
||||
return [];
|
||||
}
|
||||
const content = readNonEmptyString(record.content);
|
||||
if (!content) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
content,
|
||||
status: readNonEmptyString(record.status),
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
const record = readOpenCodeRecord(value);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
return readOpenCodeTodoItems(record.todos);
|
||||
}
|
||||
|
||||
function readOpenCodeTodoItemsFromToolPart(
|
||||
part: Extract<OpenCodePart, { type: "tool" }>,
|
||||
): Array<{ content?: string | null; status?: string | null }> | null {
|
||||
const state = readOpenCodeRecord(part.state);
|
||||
return (
|
||||
readOpenCodeTodoItems(state?.input) ??
|
||||
readOpenCodeTodoItems(state?.output) ??
|
||||
readOpenCodeTodoItems(state?.metadata)
|
||||
);
|
||||
}
|
||||
|
||||
function mapOpenCodeTodosToTimelineItems(
|
||||
todos: Array<{ content?: string | null; status?: string | null }>,
|
||||
): Extract<AgentTimelineItem, { type: "todo" }> {
|
||||
@@ -2093,6 +2154,9 @@ function appendOpenCodeMessagePartUpdated(
|
||||
events: AgentStreamEvent[],
|
||||
): void {
|
||||
const part = event.properties.part;
|
||||
if (part.type === "tool" && isOpenCodeTodoWriteToolPart(part)) {
|
||||
return;
|
||||
}
|
||||
if (part.sessionID !== state.sessionId) {
|
||||
if (part.type === "tool") {
|
||||
appendOpenCodeSubAgentChildToolPart(part, state, events);
|
||||
|
||||
@@ -541,6 +541,143 @@ describe("translateOpenCodeEvent", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("suppresses live todowrite tool parts because OpenCode emits todo.updated separately", () => {
|
||||
const state = createState();
|
||||
|
||||
const events = translateOpenCodeEvent(
|
||||
{
|
||||
type: "message.part.updated",
|
||||
properties: {
|
||||
part: {
|
||||
id: "part-todowrite",
|
||||
sessionID: "session-1",
|
||||
messageID: "message-1",
|
||||
type: "tool",
|
||||
tool: "todowrite",
|
||||
callID: "call-todowrite",
|
||||
state: {
|
||||
status: "running",
|
||||
input: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
state,
|
||||
);
|
||||
|
||||
expect(events).toEqual([]);
|
||||
});
|
||||
|
||||
it("maps live OpenCode tool parts through canonical detail branches", () => {
|
||||
const state = createState();
|
||||
|
||||
const patchText = [
|
||||
"*** Begin Patch",
|
||||
"*** Delete File: /tmp/repo/src/App.tsx",
|
||||
"*** End Patch",
|
||||
].join("\n");
|
||||
|
||||
const events = [
|
||||
{
|
||||
id: "part-grep",
|
||||
tool: "grep",
|
||||
callID: "call-grep",
|
||||
state: { status: "completed", input: { pattern: "sendCorrelatedSessionRequest" } },
|
||||
},
|
||||
{
|
||||
id: "part-skill",
|
||||
tool: "skill",
|
||||
callID: "call-skill",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { name: "diagnose" },
|
||||
output: '<skill_content name="diagnose"># Skill: diagnose</skill_content>',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "part-apply-patch",
|
||||
tool: "apply_patch",
|
||||
callID: "call-apply-patch",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { patchText },
|
||||
output: "Success. Updated the following files:\nD /tmp/repo/src/App.tsx",
|
||||
},
|
||||
},
|
||||
].flatMap((part) =>
|
||||
translateOpenCodeEvent(
|
||||
{
|
||||
type: "message.part.updated",
|
||||
properties: {
|
||||
part: {
|
||||
...part,
|
||||
sessionID: "session-1",
|
||||
messageID: "message-1",
|
||||
type: "tool",
|
||||
},
|
||||
},
|
||||
},
|
||||
state,
|
||||
),
|
||||
);
|
||||
|
||||
expect(events).toEqual([
|
||||
{
|
||||
type: "timeline",
|
||||
provider: "opencode",
|
||||
item: {
|
||||
type: "tool_call",
|
||||
callId: "call-grep",
|
||||
name: "grep",
|
||||
status: "completed",
|
||||
detail: {
|
||||
type: "search",
|
||||
query: "sendCorrelatedSessionRequest",
|
||||
toolName: "grep",
|
||||
},
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "timeline",
|
||||
provider: "opencode",
|
||||
item: {
|
||||
type: "tool_call",
|
||||
callId: "call-skill",
|
||||
name: "skill",
|
||||
status: "completed",
|
||||
detail: {
|
||||
type: "plain_text",
|
||||
label: "diagnose",
|
||||
icon: "sparkles",
|
||||
text: '<skill_content name="diagnose"># Skill: diagnose</skill_content>',
|
||||
},
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "timeline",
|
||||
provider: "opencode",
|
||||
item: {
|
||||
type: "tool_call",
|
||||
callId: "call-apply-patch",
|
||||
name: "apply_patch",
|
||||
status: "completed",
|
||||
detail: {
|
||||
type: "edit",
|
||||
filePath: "/tmp/repo/src/App.tsx",
|
||||
unifiedDiff: [
|
||||
"diff --git a//tmp/repo/src/App.tsx b//tmp/repo/src/App.tsx",
|
||||
"--- a//tmp/repo/src/App.tsx",
|
||||
"+++ /dev/null",
|
||||
].join("\n"),
|
||||
},
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits compaction loading timeline items from compaction parts", () => {
|
||||
const state = createState();
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import type { ToolCallDetail } from "../../agent-sdk-types.js";
|
||||
import { nonEmptyString } from "../tool-call-mapper-utils.js";
|
||||
import { nonEmptyString, truncateDiffText } from "../tool-call-mapper-utils.js";
|
||||
import {
|
||||
ToolEditInputSchema,
|
||||
ToolEditOutputSchema,
|
||||
ToolGrepOutputSchema,
|
||||
ToolGlobOutputSchema,
|
||||
ToolReadInputSchema,
|
||||
ToolReadOutputSchema,
|
||||
@@ -49,6 +50,109 @@ function readOutputText(value: unknown): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseApplyPatchDirective(
|
||||
line: string,
|
||||
): { kind: "add" | "update" | "delete"; path: string } | null {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith("*** Add File:")) {
|
||||
return { kind: "add", path: trimmed.replace("*** Add File:", "").trim() };
|
||||
}
|
||||
if (trimmed.startsWith("*** Update File:")) {
|
||||
return { kind: "update", path: trimmed.replace("*** Update File:", "").trim() };
|
||||
}
|
||||
if (trimmed.startsWith("*** Delete File:")) {
|
||||
return { kind: "delete", path: trimmed.replace("*** Delete File:", "").trim() };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractApplyPatchPrimaryFilePath(patchText: string): string | undefined {
|
||||
for (const line of patchText.split(/\r?\n/)) {
|
||||
const directive = parseApplyPatchDirective(line);
|
||||
if (directive?.path) {
|
||||
return directive.path;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function normalizeDiffHeaderPath(rawPath: string): string {
|
||||
return rawPath.trim().replace(/^["']+|["']+$/g, "");
|
||||
}
|
||||
|
||||
function applyPatchToUnifiedDiff(patchText: string): string {
|
||||
const lines = patchText.replace(/\r\n/g, "\n").split("\n");
|
||||
const output: string[] = [];
|
||||
let sawDiffContent = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const directive = parseApplyPatchDirective(line);
|
||||
if (directive) {
|
||||
const filePath = normalizeDiffHeaderPath(directive.path);
|
||||
if (filePath.length > 0) {
|
||||
if (output.length > 0 && output[output.length - 1] !== "") {
|
||||
output.push("");
|
||||
}
|
||||
const left = directive.kind === "add" ? "/dev/null" : `a/${filePath}`;
|
||||
const right = directive.kind === "delete" ? "/dev/null" : `b/${filePath}`;
|
||||
output.push(`diff --git a/${filePath} b/${filePath}`);
|
||||
output.push(`--- ${left}`);
|
||||
output.push(`+++ ${right}`);
|
||||
sawDiffContent = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const trimmed = line.trim();
|
||||
if (
|
||||
trimmed === "*** Begin Patch" ||
|
||||
trimmed === "*** End Patch" ||
|
||||
trimmed === "*** End of File" ||
|
||||
trimmed.startsWith("*** Move to:")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
line.startsWith("@@") ||
|
||||
line.startsWith("+") ||
|
||||
line.startsWith("-") ||
|
||||
line.startsWith(" ") ||
|
||||
line.startsWith("\\ No newline at end of file")
|
||||
) {
|
||||
output.push(line);
|
||||
sawDiffContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sawDiffContent) {
|
||||
return patchText;
|
||||
}
|
||||
|
||||
const normalized = output.join("\n").trim();
|
||||
return normalized.length > 0 ? normalized : patchText;
|
||||
}
|
||||
|
||||
const OpencodeApplyPatchTextInputSchema = z
|
||||
.object({ patchText: z.string() })
|
||||
.passthrough()
|
||||
.transform((value) => {
|
||||
const filePath = extractApplyPatchPrimaryFilePath(value.patchText);
|
||||
return {
|
||||
filePath: filePath ?? "",
|
||||
oldString: undefined,
|
||||
newString: undefined,
|
||||
unifiedDiff: truncateDiffText(applyPatchToUnifiedDiff(value.patchText)),
|
||||
};
|
||||
});
|
||||
|
||||
const OpencodeGrepOutputSchema = z
|
||||
.union([
|
||||
ToolGrepOutputSchema,
|
||||
z.string().transform((output) => ({ numFiles: 0, filenames: [], content: output })),
|
||||
])
|
||||
.nullable();
|
||||
|
||||
function formatLogEntry(value: unknown): string | undefined {
|
||||
const outputText = readOutputText(value);
|
||||
if (outputText) {
|
||||
@@ -167,6 +271,12 @@ const OpencodeKnownToolDetailSchema = z.union([
|
||||
ToolEditOutputSchema,
|
||||
toEditToolDetail,
|
||||
),
|
||||
toolDetailBranchByToolName(
|
||||
"apply_patch",
|
||||
OpencodeApplyPatchTextInputSchema,
|
||||
z.unknown(),
|
||||
(input) => toEditToolDetail(input, null),
|
||||
),
|
||||
toolDetailBranchByToolName(
|
||||
"apply_diff",
|
||||
ToolEditInputSchema,
|
||||
@@ -176,6 +286,12 @@ const OpencodeKnownToolDetailSchema = z.union([
|
||||
toolDetailBranchByToolName("search", ToolSearchInputSchema, z.unknown(), (input) =>
|
||||
toSearchToolDetail({ input, toolName: "search" }),
|
||||
),
|
||||
toolDetailBranchByToolName(
|
||||
"grep",
|
||||
ToolSearchInputSchema,
|
||||
OpencodeGrepOutputSchema,
|
||||
(input, output) => toSearchToolDetail({ input, output, toolName: "grep" }),
|
||||
),
|
||||
toolDetailBranchByToolName("glob", ToolSearchInputSchema, z.unknown(), (input, output) => {
|
||||
const parsedOutput = ToolGlobOutputSchema.safeParse(output);
|
||||
return toSearchToolDetail({
|
||||
@@ -187,6 +303,31 @@ const OpencodeKnownToolDetailSchema = z.union([
|
||||
toolDetailBranchByToolName("web_search", ToolSearchInputSchema, z.unknown(), (input) =>
|
||||
toSearchToolDetail({ input, toolName: "web_search" }),
|
||||
),
|
||||
toolDetailBranchByToolName(
|
||||
"skill",
|
||||
z.object({ name: z.string() }).passthrough(),
|
||||
z
|
||||
.union([
|
||||
z
|
||||
.object({ output: z.string() })
|
||||
.passthrough()
|
||||
.transform((value) => value.output),
|
||||
z.string(),
|
||||
])
|
||||
.nullable(),
|
||||
(input, output) => {
|
||||
const skillName = input?.name.trim();
|
||||
if (!skillName) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
type: "plain_text" as const,
|
||||
label: skillName,
|
||||
icon: "sparkles" as const,
|
||||
...(output ? { text: output } : {}),
|
||||
} satisfies ToolCallDetail;
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
export function deriveOpencodeToolDetail(
|
||||
|
||||
@@ -103,6 +103,21 @@ describe("opencode tool-call mapper", () => {
|
||||
query: "**/*.md",
|
||||
toolName: "glob",
|
||||
});
|
||||
|
||||
const grepItem = expectMapped(
|
||||
mapOpencodeToolCall({
|
||||
toolName: "grep",
|
||||
callId: "opencode-running-grep",
|
||||
status: "running",
|
||||
input: { pattern: "sendCorrelatedSessionRequest" },
|
||||
output: null,
|
||||
}),
|
||||
);
|
||||
expect(grepItem.detail).toEqual({
|
||||
type: "search",
|
||||
query: "sendCorrelatedSessionRequest",
|
||||
toolName: "grep",
|
||||
});
|
||||
});
|
||||
|
||||
it("maps completed read calls", () => {
|
||||
@@ -230,6 +245,74 @@ describe("opencode tool-call mapper", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("maps skill calls to plain text detail with the loaded skill name", () => {
|
||||
const item = expectMapped(
|
||||
mapOpencodeToolCall({
|
||||
toolName: "skill",
|
||||
callId: "opencode-skill-1",
|
||||
status: "completed",
|
||||
input: { name: "diagnose" },
|
||||
output: '<skill_content name="diagnose"># Skill: diagnose</skill_content>',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(item.detail).toEqual({
|
||||
type: "plain_text",
|
||||
label: "diagnose",
|
||||
icon: "sparkles",
|
||||
text: '<skill_content name="diagnose"># Skill: diagnose</skill_content>',
|
||||
});
|
||||
});
|
||||
|
||||
it("maps completed grep calls with string output into search detail", () => {
|
||||
const item = expectMapped(
|
||||
mapOpencodeToolCall({
|
||||
toolName: "grep",
|
||||
callId: "opencode-grep-string-output-1",
|
||||
status: "completed",
|
||||
input: { pattern: "todowrite" },
|
||||
output: "Found 2 matches\nsrc/file.ts:\n Line 1: todowrite",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(item.detail).toEqual({
|
||||
type: "search",
|
||||
query: "todowrite",
|
||||
toolName: "grep",
|
||||
content: "Found 2 matches\nsrc/file.ts:\n Line 1: todowrite",
|
||||
numFiles: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("maps apply_patch patchText payloads into edit detail", () => {
|
||||
const patchText = [
|
||||
"*** Begin Patch",
|
||||
"*** Delete File: /tmp/repo/src/App.tsx",
|
||||
"*** End Patch",
|
||||
].join("\n");
|
||||
|
||||
const item = expectMapped(
|
||||
mapOpencodeToolCall({
|
||||
toolName: "apply_patch",
|
||||
callId: "opencode-apply-patch-text-1",
|
||||
status: "completed",
|
||||
input: { patchText },
|
||||
output: "Success. Updated the following files:\nD /tmp/repo/src/App.tsx",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(item.detail.type).toBe("edit");
|
||||
expect(item.detail).toEqual({
|
||||
type: "edit",
|
||||
filePath: "/tmp/repo/src/App.tsx",
|
||||
unifiedDiff: [
|
||||
"diff --git a//tmp/repo/src/App.tsx b//tmp/repo/src/App.tsx",
|
||||
"--- a//tmp/repo/src/App.tsx",
|
||||
"+++ /dev/null",
|
||||
].join("\n"),
|
||||
});
|
||||
});
|
||||
|
||||
it("maps unknown tools to unknown detail with raw payloads", () => {
|
||||
const item = expectMapped(
|
||||
mapOpencodeToolCall({
|
||||
|
||||
Reference in New Issue
Block a user