Ver código fonte

fix(app): collapse deletion-only edit parts (#40536)

Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
opencode-agent[bot] 3 dias atrás
pai
commit
98dd65cd60

+ 2 - 9
packages/session-ui/src/components/message-part.tsx

@@ -61,6 +61,7 @@ import { TextShimmer } from "@opencode-ai/ui/text-shimmer"
 import { AnimatedCountList } from "./tool-count-summary"
 import { ToolStatusTitle } from "./tool-status-title"
 import { patchFiles } from "./apply-patch-file"
+import { partDefaultOpen } from "./part-default-open"
 import { animate } from "motion"
 import { attached, inline, kind, typeLabel } from "./message-file"
 import { readPartText } from "./message-part-text"
@@ -718,15 +719,7 @@ export function renderable(part: PartType, showReasoningSummaries = true) {
   return !!PART_MAPPING[part.type]
 }
 
-function toolDefaultOpen(tool: string, shell = false, edit = false) {
-  if (tool === "bash" || tool === "shell") return shell
-  if (tool === "edit" || tool === "write" || tool === "patch" || tool === "apply_patch") return edit
-}
-
-export function partDefaultOpen(part: PartType, shell = false, edit = false) {
-  if (part.type !== "tool") return
-  return toolDefaultOpen(part.tool, shell, edit)
-}
+export { partDefaultOpen } from "./part-default-open"
 
 export function AssistantParts(props: {
   messages: AssistantMessage[]

+ 66 - 0
packages/session-ui/src/components/part-default-open.test.ts

@@ -0,0 +1,66 @@
+import { describe, expect, test } from "bun:test"
+import type { Part as PartType } from "@opencode-ai/sdk/v2"
+import { partDefaultOpen } from "./part-default-open"
+
+describe("partDefaultOpen", () => {
+  test("keeps edited files expanded when enabled", () => {
+    expect(partDefaultOpen(tool("edit", { filediff: { additions: 1, deletions: 1 } }), false, true)).toBe(true)
+  })
+
+  test("collapses deletion-only edits when enabled", () => {
+    expect(partDefaultOpen(tool("edit", { filediff: { additions: 0, deletions: 1_200 } }), false, true)).toBe(false)
+  })
+
+  test("collapses patches containing only deleted files when enabled", () => {
+    expect(
+      partDefaultOpen(
+        tool("apply_patch", {
+          files: [
+            { filePath: "one.ts", type: "delete" },
+            { filePath: "two.ts", type: "delete" },
+          ],
+        }),
+        false,
+        true,
+      ),
+    ).toBe(false)
+  })
+
+  test("keeps mixed patches expanded when enabled", () => {
+    expect(
+      partDefaultOpen(
+        tool("apply_patch", {
+          files: [
+            { filePath: "one.ts", type: "delete" },
+            { filePath: "two.ts", type: "update" },
+          ],
+        }),
+        false,
+        true,
+      ),
+    ).toBe(true)
+  })
+
+  test("preserves shell defaults", () => {
+    expect(partDefaultOpen(tool("shell", {}), true, false)).toBe(true)
+  })
+})
+
+function tool(name: string, metadata: Record<string, unknown>): PartType {
+  return {
+    id: `part_${name}`,
+    sessionID: "session",
+    messageID: "message",
+    type: "tool",
+    callID: `call_${name}`,
+    tool: name,
+    state: {
+      status: "completed",
+      input: {},
+      output: "",
+      title: name,
+      metadata,
+      time: { start: 0, end: 1 },
+    },
+  }
+}

+ 26 - 0
packages/session-ui/src/components/part-default-open.ts

@@ -0,0 +1,26 @@
+import type { Part as PartType, ToolPart } from "@opencode-ai/sdk/v2"
+
+function deletionOnly(part: ToolPart) {
+  if (!("metadata" in part.state)) return false
+  const metadata = part.state.metadata
+  if (!metadata) return false
+
+  const files = metadata.files
+  if (Array.isArray(files) && files.length > 0) {
+    return files.every((file) => !!file && typeof file === "object" && "type" in file && file.type === "delete")
+  }
+
+  const filediff = metadata.filediff
+  if (!filediff || typeof filediff !== "object") return false
+  if (!("additions" in filediff) || !("deletions" in filediff)) return false
+  return filediff.additions === 0 && typeof filediff.deletions === "number" && filediff.deletions > 0
+}
+
+export function partDefaultOpen(part: PartType, shell = false, edit = false) {
+  if (part.type !== "tool") return
+  if (part.tool === "bash" || part.tool === "shell") return shell
+  if (part.tool === "edit" || part.tool === "write" || part.tool === "patch" || part.tool === "apply_patch") {
+    if (!edit) return false
+    return !deletionOnly(part)
+  }
+}