Skip to content

Commit

Permalink
add listbuffers command
Browse files Browse the repository at this point in the history
  • Loading branch information
dlants committed Dec 18, 2024
1 parent 6ab17f1 commit 91ebf43
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 19 deletions.
9 changes: 6 additions & 3 deletions rplugin/node/magenta/src/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ class AnthropicClient {
messages,
model: "claude-3-5-sonnet-20241022",
max_tokens: 1024,
system: `You are a coding assistant to a software engineer, inside a neovim plugin called Magenta. Be concise.`,
system: `You are a coding assistant to a software engineer, inside a neovim plugin called Magenta.
Be concise. You can use multiple tools at once, so try to minimize round trips.`,
tool_choice: {
type: "auto",
disable_parallel_tool_use: false,
Expand All @@ -57,10 +58,12 @@ class AnthropicClient {
flushBuffer();
})
.on("error", (error: Error) => {
context.logger.error(error)
context.logger.error(error);
})
.on("inputJson", (_delta, snapshot) => {
context.logger.debug(`anthropic stream inputJson: ${JSON.stringify(snapshot)}`);
context.logger.debug(
`anthropic stream inputJson: ${JSON.stringify(snapshot)}`,
);
});

const response = await stream.finalMessage();
Expand Down
5 changes: 3 additions & 2 deletions rplugin/node/magenta/src/tea/tea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ export function createApp<Model, Msg, SubscriptionType extends string>({
const [nextModel, thunk] = update(msg, currentState.model);

if (thunk) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
thunk(dispatch);
thunk(dispatch).catch((err) => {
context.logger.error(err as Error);
});
}

currentState = { status: "running", model: nextModel };
Expand Down
12 changes: 6 additions & 6 deletions rplugin/node/magenta/src/tools/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,28 +81,28 @@ export async function displayDiffs(
}

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

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

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

break;
}
Expand Down
9 changes: 6 additions & 3 deletions rplugin/node/magenta/src/tools/insert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,17 +170,20 @@ export function getToolResult(model: Model): ToolResultBlockParam {

export const spec: Anthropic.Anthropic.Tool = {
name: "insert",
description: "Insert content after a specified string in a file",
description:
"Insert content after the specified string in a file. You can also use this tool to create new files.",
input_schema: {
type: "object",
properties: {
filePath: {
type: "string",
description: "Path to the file to modify",
description:
"Path to the file to modify. The file will be created if it does not exist yet.",
},
insertAfter: {
type: "string",
description: "String after which to insert the content",
description:
"String after which to insert the content. This text will not be changed. This should be the literal text of the file - regular expressions are not allowed. Provide just enough text to uniquely identify a location in the file. Provide the empty string to insert at the beginning of the file.",
},
content: {
type: "string",
Expand Down
124 changes: 124 additions & 0 deletions rplugin/node/magenta/src/tools/listBuffers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import * as Anthropic from "@anthropic-ai/sdk";
import path from "path";
import { assertUnreachable } from "../utils/assertUnreachable.ts";
import { ToolResultBlockParam } from "@anthropic-ai/sdk/resources/index.mjs";
import { Thunk, Update } from "../tea/tea.ts";
import { d, VDOMNode } from "../tea/view.ts";
import { context } from "../context.ts";
import { ToolRequestId } from "./toolManager.ts";

export type Model = {
type: "list-buffers";
autoRespond: boolean;
request: ListBuffersToolRequest;
state:
| {
state: "processing";
}
| {
state: "done";
result: ToolResultBlockParam;
};
};

export type Msg = {
type: "finish";
result: ToolResultBlockParam;
};

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

export function initModel(
request: ListBuffersToolRequest,
): [Model, Thunk<Msg>] {
const model: Model = {
type: "list-buffers",
autoRespond: true,
request,
state: {
state: "processing",
},
};
return [
model,
async (dispatch) => {
const { nvim } = context;
const buffers = await nvim.buffers;
const cwd = (await nvim.call("getcwd")) as string;
const bufferPaths = await Promise.all(
buffers.map(async (buffer) => {
const fullPath = await buffer.name;
return path.relative(cwd, fullPath);
}),
);

dispatch({
type: "finish",
result: {
type: "tool_result",
tool_use_id: request.id,
content: bufferPaths.join("\n"),
},
});
},
];
}

export function view({ model }: { model: Model }): VDOMNode {
switch (model.state.state) {
case "processing":
return d`⚙️ Grabbing buffers...`;
case "done":
return d`✅ Finished getting buffers.`;
default:
assertUnreachable(model.state);
}
}

export function getToolResult(model: Model): ToolResultBlockParam {
switch (model.state.state) {
case "processing":
return {
type: "tool_result",
tool_use_id: model.request.id,
content: `This tool use is being processed. 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: "list_buffers",
description: `List the file paths of all the buffers the user currently has open. This can be useful to understand the context of what the user is trying to do.`,
input_schema: {
type: "object",
properties: {},
required: [],
},
};

export type ListBuffersToolRequest = {
type: "tool_use";
id: ToolRequestId; //"toolu_01UJtsBsBED9bwkonjqdxji4"
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
input: {};
name: "list_buffers";
};
4 changes: 2 additions & 2 deletions rplugin/node/magenta/src/tools/replace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,12 @@ export const spec: Anthropic.Anthropic.Tool = {
start: {
type: "string",
description:
"Replace content starting with this text. This text is included in what will be replaced. Please provide just enough text to uniquely identify a location in the file.",
"Replace content starting with this text. This should be the literal text of the file - regular expressions are not supported. This text is included in what will be replaced. Please provide just enough text to uniquely identify a location in the file.",
},
end: {
type: "string",
description:
"Replace content until we encounter this text. This text is included in what will be replaced. Please provide just enough text to uniquely identify a location in the file.",
"Replace content until we encounter this text. This should be the literal text of the file - regular expressions are not supported. This text is included in what will be replaced. Please provide just enough text to uniquely identify a location in the file.",
},
content: {
type: "string",
Expand Down
80 changes: 77 additions & 3 deletions rplugin/node/magenta/src/tools/toolManager.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import * as GetFile from "./getFile.ts";
import * as Insert from "./insert.ts";
import * as Replace from "./replace.ts";
import * as ListBuffers from "./listBuffers.ts";
import { Dispatch, Update } from "../tea/tea.ts";
import { assertUnreachable } from "../utils/assertUnreachable.ts";
import { ToolResultBlockParam } from "@anthropic-ai/sdk/resources/messages.mjs";

export type ToolRequest =
| GetFile.GetFileToolUseRequest
| Insert.InsertToolUseRequest
| Replace.ReplaceToolRequest;
| Replace.ReplaceToolRequest
| ListBuffers.ListBuffersToolRequest;

export type ToolModel = GetFile.Model | Insert.Model | Replace.Model;
export type ToolModel =
| GetFile.Model
| Insert.Model
| Replace.Model
| ListBuffers.Model;

export type ToolRequestId = string & { __toolRequestId: true };

export const TOOL_SPECS = [GetFile.spec, Insert.spec, Replace.spec];
export const TOOL_SPECS = [
GetFile.spec,
Insert.spec,
Replace.spec,
ListBuffers.spec,
];

export type Model = {
toolModels: {
Expand All @@ -30,6 +41,8 @@ export function getToolResult(model: ToolModel): ToolResultBlockParam {
return Insert.getToolResult(model);
case "replace":
return Replace.getToolResult(model);
case "list-buffers":
return ListBuffers.getToolResult(model);

default:
return assertUnreachable(model);
Expand All @@ -40,6 +53,8 @@ export function renderTool(model: ToolModel, dispatch: Dispatch<Msg>) {
switch (model.type) {
case "get-file":
return GetFile.view({ model });
case "list-buffers":
return ListBuffers.view({ model });
case "insert":
return Insert.view({
model,
Expand Down Expand Up @@ -71,6 +86,7 @@ export type Msg =
type: "init-tool-use";
request:
| GetFile.GetFileToolUseRequest
| ListBuffers.ListBuffersToolRequest
| Insert.InsertToolUseRequest
| Replace.ReplaceToolRequest;
}
Expand All @@ -82,6 +98,10 @@ export type Msg =
type: "get-file";
msg: GetFile.Msg;
}
| {
type: "list-buffers";
msg: ListBuffers.Msg;
}
| {
type: "insert";
msg: Insert.Msg;
Expand Down Expand Up @@ -127,6 +147,30 @@ export const update: Update<Msg, Model> = (msg, model) => {
];
}

case "list_buffers": {
const [listBuffersModel, thunk] = ListBuffers.initModel(request);
return [
{
...model,
toolModels: {
...model.toolModels,
[request.id]: listBuffersModel,
},
},
(dispatch) =>
thunk((msg) =>
dispatch({
type: "tool-msg",
id: request.id,
msg: {
type: "list-buffers",
msg,
},
}),
),
];
}

case "insert": {
const [insertModel] = Insert.initModel(request);
return [
Expand Down Expand Up @@ -195,6 +239,36 @@ export const update: Update<Msg, Model> = (msg, model) => {
];
}

case "list-buffers": {
const [nextToolModel, thunk] = ListBuffers.update(
msg.msg.msg,
toolModel as ListBuffers.Model,
);

return [
{
...model,
toolModels: {
...model.toolModels,
[msg.id]: nextToolModel,
},
},
thunk
? (dispatch) =>
thunk((innerMsg) =>
dispatch({
type: "tool-msg",
id: msg.id,
msg: {
type: "list-buffers",
msg: innerMsg,
},
}),
)
: undefined,
];
}

case "insert": {
const [nextToolModel, thunk] = Insert.update(
msg.msg.msg,
Expand Down

0 comments on commit 91ebf43

Please sign in to comment.