summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src/components
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2026-01-17 22:35:09 -0800
committerGitHub <[email protected]>2026-01-18 00:35:09 -0600
commitb7ad6bd83922e2259a467fe59f27806af8060629 (patch)
tree0e1412ec1941f5a7c72731b53f4503b1619e811f /packages/ui/src/components
parent10433cb45b6ed932368fb147032d671eaed0d273 (diff)
downloadopencode-b7ad6bd83922e2259a467fe59f27806af8060629.tar.gz
opencode-b7ad6bd83922e2259a467fe59f27806af8060629.zip
feat: apply_patch tool for openai models (#9127)
Diffstat (limited to 'packages/ui/src/components')
-rw-r--r--packages/ui/src/components/message-part.css72
-rw-r--r--packages/ui/src/components/message-part.tsx94
2 files changed, 166 insertions, 0 deletions
diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css
index 4a249ec4f..184565e9c 100644
--- a/packages/ui/src/components/message-part.css
+++ b/packages/ui/src/components/message-part.css
@@ -689,3 +689,75 @@
}
}
}
+
+[data-component="apply-patch-files"] {
+ display: flex;
+ flex-direction: column;
+}
+
+[data-component="apply-patch-file"] {
+ display: flex;
+ flex-direction: column;
+ border-top: 1px solid var(--border-weaker-base);
+
+ &:first-child {
+ border-top: 1px solid var(--border-weaker-base);
+ }
+
+ [data-slot="apply-patch-file-header"] {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ background-color: var(--surface-inset-base);
+ }
+
+ [data-slot="apply-patch-file-action"] {
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-small);
+ font-weight: var(--font-weight-medium);
+ line-height: var(--line-height-large);
+ color: var(--text-base);
+ flex-shrink: 0;
+
+ &[data-type="delete"] {
+ color: var(--text-critical-base);
+ }
+
+ &[data-type="add"] {
+ color: var(--text-success-base);
+ }
+
+ &[data-type="move"] {
+ color: var(--text-warning-base);
+ }
+ }
+
+ [data-slot="apply-patch-file-path"] {
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-small);
+ color: var(--text-weak);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex-grow: 1;
+ }
+
+ [data-slot="apply-patch-deletion-count"] {
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-small);
+ color: var(--text-critical-base);
+ flex-shrink: 0;
+ }
+}
+
+[data-component="apply-patch-file-diff"] {
+ max-height: 420px;
+ overflow-y: auto;
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+}
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx
index 165f46f6c..47403786b 100644
--- a/packages/ui/src/components/message-part.tsx
+++ b/packages/ui/src/components/message-part.tsx
@@ -233,6 +233,12 @@ export function getToolInfo(tool: string, input: any = {}): ToolInfo {
title: "Write",
subtitle: input.filePath ? getFilename(input.filePath) : undefined,
}
+ case "apply_patch":
+ return {
+ icon: "code-lines",
+ title: "Patch",
+ subtitle: input.files?.length ? `${input.files.length} file${input.files.length > 1 ? "s" : ""}` : undefined,
+ }
case "todowrite":
return {
icon: "checklist",
@@ -1027,6 +1033,94 @@ ToolRegistry.register({
},
})
+interface ApplyPatchFile {
+ filePath: string
+ relativePath: string
+ type: "add" | "update" | "delete" | "move"
+ diff: string
+ before: string
+ after: string
+ additions: number
+ deletions: number
+ movePath?: string
+}
+
+ToolRegistry.register({
+ name: "apply_patch",
+ render(props) {
+ const diffComponent = useDiffComponent()
+ const files = createMemo(() => (props.metadata.files ?? []) as ApplyPatchFile[])
+
+ const subtitle = createMemo(() => {
+ const count = files().length
+ if (count === 0) return ""
+ return `${count} file${count > 1 ? "s" : ""}`
+ })
+
+ return (
+ <BasicTool
+ {...props}
+ icon="code-lines"
+ trigger={{
+ title: "Patch",
+ subtitle: subtitle(),
+ }}
+ >
+ <Show when={files().length > 0}>
+ <div data-component="apply-patch-files">
+ <For each={files()}>
+ {(file) => (
+ <div data-component="apply-patch-file">
+ <div data-slot="apply-patch-file-header">
+ <Switch>
+ <Match when={file.type === "delete"}>
+ <span data-slot="apply-patch-file-action" data-type="delete">
+ Deleted
+ </span>
+ </Match>
+ <Match when={file.type === "add"}>
+ <span data-slot="apply-patch-file-action" data-type="add">
+ Created
+ </span>
+ </Match>
+ <Match when={file.type === "move"}>
+ <span data-slot="apply-patch-file-action" data-type="move">
+ Moved
+ </span>
+ </Match>
+ <Match when={file.type === "update"}>
+ <span data-slot="apply-patch-file-action" data-type="update">
+ Patched
+ </span>
+ </Match>
+ </Switch>
+ <span data-slot="apply-patch-file-path">{file.relativePath}</span>
+ <Show when={file.type !== "delete"}>
+ <DiffChanges changes={{ additions: file.additions, deletions: file.deletions }} />
+ </Show>
+ <Show when={file.type === "delete"}>
+ <span data-slot="apply-patch-deletion-count">-{file.deletions}</span>
+ </Show>
+ </div>
+ <Show when={file.type !== "delete"}>
+ <div data-component="apply-patch-file-diff">
+ <Dynamic
+ component={diffComponent}
+ before={{ name: file.filePath, contents: file.before }}
+ after={{ name: file.filePath, contents: file.after }}
+ />
+ </div>
+ </Show>
+ </div>
+ )}
+ </For>
+ </div>
+ </Show>
+ </BasicTool>
+ )
+ },
+})
+
ToolRegistry.register({
name: "todowrite",
render(props) {