mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-28 13:33:35 +08:00
fix(session-ui): group file tool diffs (#48932)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
This commit is contained in:
co-authored by
Brendonovich
parent
0ee9f61e3d
commit
f1149efee7
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -34,7 +34,7 @@ const scenarios = [
|
||||
title: false,
|
||||
},
|
||||
{
|
||||
name: "grouped edit with title",
|
||||
name: "grouped edit",
|
||||
placement: "grouped",
|
||||
tools: [
|
||||
toolPart(
|
||||
@@ -46,14 +46,14 @@ const scenarios = [
|
||||
),
|
||||
],
|
||||
files: ["a"],
|
||||
title: true,
|
||||
title: false,
|
||||
},
|
||||
{
|
||||
name: "running edit input fallback",
|
||||
placement: "grouped",
|
||||
tools: [toolPart("prt_sticky_edit", "edit", "running", { path: "src/a.ts", oldString: before, newString: after })],
|
||||
files: ["a"],
|
||||
title: true,
|
||||
title: false,
|
||||
},
|
||||
{
|
||||
name: "grouped write",
|
||||
|
||||
@@ -4,38 +4,25 @@ import { expect, story } from "../../storybook/playwright/story"
|
||||
story("renders every admitted tool family and hides timeline-only exclusions", async ({ mount }) => {
|
||||
const timeline = await mount("current-session-research-agents--agent-research", { args: { scenario: "workflow" } })
|
||||
const first = timeline.locator(
|
||||
'[data-timeline-part-ids="tool_family_read,tool_family_glob,tool_family_grep,tool_family_list,tool_family_webfetch,tool_family_websearch,tool_family_subagent,tool_family_shell,tool_family_edit,tool_family_write,tool_family_patch"]',
|
||||
'[data-timeline-part-ids="tool_family_read,tool_family_glob,tool_family_grep,tool_family_list,tool_family_webfetch,tool_family_websearch,tool_family_subagent,tool_family_shell,tool_family_edit,tool_family_write,tool_family_write_extra,tool_family_patch"]',
|
||||
)
|
||||
const second = timeline.locator('[data-timeline-part-ids="tool_family_skill,tool_family_custom"]')
|
||||
await expect(first).toBeVisible()
|
||||
await expect(second).toBeVisible()
|
||||
await first.getByRole("button").click()
|
||||
await second.getByRole("button").click()
|
||||
for (const id of [
|
||||
"webfetch",
|
||||
"websearch",
|
||||
"subagent",
|
||||
"shell",
|
||||
"edit",
|
||||
"write",
|
||||
"patch",
|
||||
"question",
|
||||
"skill",
|
||||
"custom",
|
||||
]) {
|
||||
for (const id of ["webfetch", "websearch", "subagent", "shell", "question", "skill", "custom"]) {
|
||||
await expect(timeline.locator(`[data-timeline-part-id="tool_family_${id}"]`), id).toBeVisible()
|
||||
}
|
||||
for (const name of ["edit", "write", "patch"]) {
|
||||
const tool = timeline.locator(`[data-timeline-part-id="tool_family_${name}"]`)
|
||||
await expect(tool.getByText("1 file", { exact: true })).toBeVisible()
|
||||
await expect(tool.getByRole("button")).toHaveCount(1)
|
||||
await expect(tool.locator('[data-scope="apply-patch"] button')).toHaveAttribute("aria-expanded", "false")
|
||||
await expect(tool.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("data-locked", "")
|
||||
await expect(tool.locator('[data-slot="message-part-title-filename"]')).toHaveCount(0)
|
||||
await expect(tool.locator('[data-slot="message-part-actions"]')).toHaveCount(0)
|
||||
await expect(tool.locator('[data-slot="basic-tool-tool-title"]')).toHaveCSS("font-size", "13px")
|
||||
await expect(tool.locator('[data-slot="basic-tool-tool-title"]')).toHaveCSS("line-height", "16px")
|
||||
}
|
||||
const files = timeline.locator(
|
||||
'[data-timeline-part-ids="tool_family_edit,tool_family_write,tool_family_write_extra,tool_family_patch"]',
|
||||
)
|
||||
await expect(files).toBeVisible()
|
||||
await expect(files.locator('[data-scope="apply-patch"]')).toHaveCount(1)
|
||||
await expect(files.locator('[data-slot="apply-patch-filename"]')).toHaveText(["a.ts", "new.ts", "extra.ts"])
|
||||
await expect(files.locator('[data-slot="basic-tool-tool-title"]')).toHaveCount(0)
|
||||
await expect(files.locator('[data-scope="apply-patch"] button')).toHaveCount(3)
|
||||
await expect(files.locator('[data-scope="apply-patch"] button[aria-expanded="false"]')).toHaveCount(3)
|
||||
await expect(timeline.locator('[data-timeline-part-id="tool_family_todo"]')).toHaveCount(0)
|
||||
})
|
||||
|
||||
|
||||
@@ -250,6 +250,10 @@ const CompleteAgentWorkflow = {
|
||||
path: "src/new.ts",
|
||||
content: "export const stable = true",
|
||||
}),
|
||||
storyTool("tool_family_write_extra", "write", "completed", {
|
||||
path: "src/extra.ts",
|
||||
content: "export const extra = true",
|
||||
}),
|
||||
storyTool(
|
||||
"tool_family_patch",
|
||||
"patch",
|
||||
|
||||
@@ -64,19 +64,24 @@ export function createSessionTimelineRowRenderer(input: {
|
||||
}) {
|
||||
const i18n = useI18n()
|
||||
const data = useData()
|
||||
// Cached timelines retain subgroup identities alongside their disclosure choices.
|
||||
// Cached timelines retain file-change subgroup identities alongside their disclosure choices.
|
||||
const patchGroupKeys = input.disclosure.patchGroupKeys ?? new Map<string, string>()
|
||||
const patchPartKeys = new WeakMap<SessionMessageAssistant["content"][number], string>()
|
||||
const patchOwners = createMemo(() => {
|
||||
const owners = new Map<string, string>()
|
||||
const rows = input.projection.rows()
|
||||
// Track status changes before a group is first opened: a failed patch can
|
||||
// Track status changes before a group is first opened: a failed file change can
|
||||
// split an existing group without changing the projection's row identities.
|
||||
rows.forEach((row) => {
|
||||
if (row._tag !== "AssistantPart" || row.group.type !== "context") return
|
||||
row.group.refs.forEach((ref) => {
|
||||
const content = Timeline.resolveContent(input.projection.messageByID().get(ref.messageID), ref.partID)
|
||||
if (content?.type !== "tool" || content.name !== "patch" || content.state.status === "error") return
|
||||
if (
|
||||
content?.type !== "tool" ||
|
||||
!["edit", "write", "patch"].includes(content.name) ||
|
||||
content.state.status === "error"
|
||||
)
|
||||
return
|
||||
const part = `${ref.messageID}:${ref.partID}`
|
||||
const key = patchGroupKeys.get(part)
|
||||
if (key && !owners.has(key)) owners.set(key, part)
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
type JSX,
|
||||
} from "solid-js"
|
||||
import stripAnsi from "strip-ansi"
|
||||
import { createTwoFilesPatch } from "diff"
|
||||
import { createTwoFilesPatch, diffLines } from "diff"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import { type SessionSummary, useData } from "../context"
|
||||
import { useFileComponent } from "@opencode/ui/context/file"
|
||||
@@ -566,13 +566,7 @@ export function CurrentContextToolGroup(props: {
|
||||
return groups
|
||||
}
|
||||
const previous = groups.at(-1)
|
||||
if (
|
||||
tool.name === "patch" &&
|
||||
tool.state.status !== "error" &&
|
||||
Array.isArray(previous) &&
|
||||
previous?.[0]?.name === "patch" &&
|
||||
previous[0].state.status !== "error"
|
||||
) {
|
||||
if (isFileChangeTool(tool) && Array.isArray(previous) && previous[0] && isFileChangeTool(previous[0])) {
|
||||
previous.push(tool)
|
||||
return groups
|
||||
}
|
||||
@@ -595,7 +589,7 @@ export function CurrentContextToolGroup(props: {
|
||||
const patchKeys = createMemo(() => {
|
||||
const keys = new Map<SessionMessageAssistantTool, string>()
|
||||
items().forEach((item) => {
|
||||
if (!Array.isArray(item) || item[0]?.name !== "patch" || item[0].state.status === "error") return
|
||||
if (!Array.isArray(item) || !item[0] || !isFileChangeTool(item[0])) return
|
||||
const key = props.patchGroupKey?.(item) ?? item[0].id
|
||||
item.forEach((tool) => keys.set(tool, key))
|
||||
})
|
||||
@@ -715,7 +709,7 @@ export function CurrentContextToolGroup(props: {
|
||||
when={tool().name === "skill" && group().length > 1 && skills().length === group().length}
|
||||
fallback={
|
||||
<Show
|
||||
when={tool().name === "patch" && tool().state.status !== "error"}
|
||||
when={isFileChangeTool(tool())}
|
||||
fallback={
|
||||
<ToolDisplay
|
||||
id={tool().id}
|
||||
@@ -844,9 +838,32 @@ export function CurrentFileToolGroup(props: {
|
||||
const files = currentToolMetadata(tool).files
|
||||
if (Array.isArray(files) && files.length > 0)
|
||||
return files.map((value, index) => ({ key: `${tool.id}:${index}`, toolID: tool.id, value }))
|
||||
if (tool.name !== "write") return []
|
||||
const input = currentToolInput(tool)
|
||||
if (typeof input.path !== "string" || typeof input.content !== "string" || !input.content) return []
|
||||
if (typeof input.path !== "string") return []
|
||||
if (tool.name === "edit" && typeof input.oldString === "string" && typeof input.newString === "string") {
|
||||
const changes = diffLines(input.oldString, input.newString)
|
||||
const additions = changes
|
||||
.filter((change) => change.added)
|
||||
.reduce((total, change) => total + (change.count ?? 0), 0)
|
||||
const deletions = changes
|
||||
.filter((change) => change.removed)
|
||||
.reduce((total, change) => total + (change.count ?? 0), 0)
|
||||
if (additions === 0 && deletions === 0) return []
|
||||
return [
|
||||
{
|
||||
key: `${tool.id}:0`,
|
||||
toolID: tool.id,
|
||||
value: {
|
||||
file: input.path,
|
||||
patch: createTwoFilesPatch(input.path, input.path, input.oldString, input.newString),
|
||||
additions,
|
||||
deletions,
|
||||
status: "modified",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
if (tool.name !== "write" || typeof input.content !== "string" || !input.content) return []
|
||||
return [
|
||||
{
|
||||
key: `${tool.id}:0`,
|
||||
@@ -912,6 +929,10 @@ export function CurrentFileToolGroup(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function isFileChangeTool(tool: SessionMessageAssistantTool) {
|
||||
return tool.state.status !== "error" && (tool.name === "edit" || tool.name === "write" || tool.name === "patch")
|
||||
}
|
||||
|
||||
function samePatchFile(a: unknown, b: unknown) {
|
||||
if (a === b) return true
|
||||
if (!record(a) || !record(b)) return false
|
||||
|
||||
Reference in New Issue
Block a user