Skip to content

Commit

Permalink
add replace tool
Browse files Browse the repository at this point in the history
  • Loading branch information
dlants committed Dec 18, 2024
1 parent e8fe02c commit 786024c
Show file tree
Hide file tree
Showing 4 changed files with 365 additions and 23 deletions.
75 changes: 64 additions & 11 deletions rplugin/node/magenta/src/tools/diff.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
import { context } from "../context.ts";
import { Buffer } from "neovim";
import { WIDTH } from "../sidebar.ts";
import { assertUnreachable } from "../utils/assertUnreachable.ts";
import { Dispatch } from "../tea/tea.ts";

type Edit = {
type: "insert-after";
insertAfter: string;
content: string;
type Edit =
| {
type: "insert-after";
insertAfter: string;
content: string;
}
| {
type: "replace";
start: string;
end: string;
content: string;
};

type Msg = {
type: "error";
error: string;
};

/** Helper to bring up an editing interface for the given file path.
*/
export async function displayDiffs(filePath: string, edits: Edit[]) {
export async function displayDiffs(
filePath: string,
edits: Edit[],
dispatch: Dispatch<Msg>,
) {
const { nvim } = context;

// first, check to see if any windows *other than* the magenta plugin windows are open, and close them.
Expand Down Expand Up @@ -48,12 +66,47 @@ export async function displayDiffs(filePath: string, edits: Edit[]) {
let content: string = lines.join("\n");

for (const edit of edits) {
const insertLocation =
content.indexOf(edit.insertAfter) + edit.insertAfter.length;
content =
content.slice(0, insertLocation) +
edit.content +
content.slice(insertLocation);
switch (edit.type) {
case "insert-after": {
const insertLocation =
content.indexOf(edit.insertAfter) + edit.insertAfter.length;
content =
content.slice(0, insertLocation) +
edit.content +
content.slice(insertLocation);
break;
}

case "replace": {
const insertStart = content.indexOf(edit.start);
const insertEnd = content.indexOf(edit.end);

if (insertStart == -1) {
dispatch({
type: "error",
error: `Unable to find start location of string ${edit.start} in file ${filePath}`,
});
continue;
}

if (insertEnd == -1) {
dispatch({
type: "error",
error: `Unable to find end location of string ${edit.end} in file ${filePath}`,
});
continue;
}
content =
content.slice(0, insertStart) +
edit.content +
content.slice(insertEnd + edit.end.length);

break;
}

default:
assertUnreachable(edit);
}
}

const scratchBuffer = (await nvim.createBuffer(false, true)) as Buffer;
Expand Down
27 changes: 20 additions & 7 deletions rplugin/node/magenta/src/tools/insert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,26 @@ export function insertThunk(model: Model) {
const request = model.request;
return async (dispatch: Dispatch<Msg>) => {
try {
await displayDiffs(request.input.filePath, [
{
type: "insert-after",
insertAfter: request.input.insertAfter,
content: request.input.content,
},
]);
await displayDiffs(
request.input.filePath,
[
{
type: "insert-after",
insertAfter: request.input.insertAfter,
content: request.input.content,
},
],
(msg) =>
dispatch({
type: "finish",
result: {
type: "tool_result",
tool_use_id: model.request.id,
content: msg.error,
is_error: true,
},
}),
);
} catch (error) {
dispatch({
type: "finish",
Expand Down
209 changes: 209 additions & 0 deletions rplugin/node/magenta/src/tools/replace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import * as Anthropic from "@anthropic-ai/sdk";
import { assertUnreachable } from "../utils/assertUnreachable.ts";
import { ToolResultBlockParam } from "@anthropic-ai/sdk/resources/index.mjs";
import { Dispatch, Update } from "../tea/tea.ts";
import { d, VDOMNode, withBindings } from "../tea/view.ts";
import { ToolRequestId } from "./toolManager.ts";
import { displayDiffs } from "./diff.ts";

export type Model = {
type: "replace";
autoRespond: boolean;
request: ReplaceToolRequest;
state:
| {
state: "pending-user-action";
}
| {
state: "editing-diff";
}
| {
state: "done";
result: ToolResultBlockParam;
};
};

export type Msg =
| {
type: "finish";
result: ToolResultBlockParam;
}
| {
type: "display-diff";
};

export const update: Update<Msg, Model> = (msg, model) => {
switch (msg.type) {
case "finish":
return [
{
...model,
state: {
state: "done",
result: msg.result,
},
},
];
case "display-diff":
return [
{
...model,
state: {
state: "pending-user-action",
},
},
insertThunk(model),
];
default:
assertUnreachable(msg);
}
};

export function initModel(request: ReplaceToolRequest): [Model] {
const model: Model = {
type: "replace",
autoRespond: false,
request,
state: {
state: "pending-user-action",
},
};

return [model];
}

export function insertThunk(model: Model) {
const request = model.request;
return async (dispatch: Dispatch<Msg>) => {
try {
await displayDiffs(
request.input.filePath,
[
{
type: "replace",
start: request.input.start,
end: request.input.end,
content: request.input.content,
},
],
(msg) =>
dispatch({
type: "finish",
result: {
type: "tool_result",
tool_use_id: model.request.id,
content: msg.error,
is_error: true,
},
}),
);
} catch (error) {
dispatch({
type: "finish",
result: {
type: "tool_result",
tool_use_id: request.id,
content: `Error: ${(error as Error).message}`,
is_error: true,
},
});
}
};
}

export function view({
model,
dispatch,
}: {
model: Model;
dispatch: Dispatch<Msg>;
}): VDOMNode {
return d`Insert ${(
model.request.input.content.match(/\n/g) || []
).length.toString()} into file ${model.request.input.filePath}
${toolStatusView({ model, dispatch })}`;
}

function toolStatusView({
model,
dispatch,
}: {
model: Model;
dispatch: Dispatch<Msg>;
}): VDOMNode {
switch (model.state.state) {
case "pending-user-action":
return withBindings(d`[review diff]`, {
Enter: () =>
dispatch({
type: "display-diff",
}),
});
case "editing-diff":
return d`Editing diff`;
case "done":
return d`Done`;
}
}

export function getToolResult(model: Model): ToolResultBlockParam {
switch (model.state.state) {
case "editing-diff":
return {
type: "tool_result",
tool_use_id: model.request.id,
content: `The user is reviewing the change. Please proceed with your answer or address other parts of the question.`,
};
case "pending-user-action":
return {
type: "tool_result",
tool_use_id: model.request.id,
content: `Waiting for a user action to finish processing this tool use. Please proceed with your answer or address other parts of the question.`,
};
case "done":
return model.state.result;
default:
assertUnreachable(model.state);
}
}

export const spec: Anthropic.Anthropic.Tool = {
name: "insert",
description: "Replace text between two strings in a file.",
input_schema: {
type: "object",
properties: {
filePath: {
type: "string",
description: "Path of the file to modify.",
},
start: {
type: "string",
description:
"We will replace text starting with this string. This string is included in the text that is replaced. Please provide a minimal string that uniquely identifies a location in the file.",
},
end: {
type: "string",
description:
"We will replace text until we encounter this string. This string is included in the text that is replaced. Please provide a minimal string that uniquely identifies a location in the file.",
},
content: {
type: "string",
description: "Content to insert",
},
},
required: ["filePath", "start", "end", "content"],
},
};

export type ReplaceToolRequest = {
type: "tool_use";
id: ToolRequestId;
name: "replace";
input: {
filePath: string;
start: string;
end: string;
content: string;
};
};
Loading

0 comments on commit 786024c

Please sign in to comment.