From 91ebf430aa7fbaca7ad8a309f94dab00f8dda728 Mon Sep 17 00:00:00 2001 From: Denis Lantsman Date: Wed, 18 Dec 2024 15:57:34 -0800 Subject: [PATCH] add listbuffers command --- rplugin/node/magenta/src/anthropic.ts | 9 +- rplugin/node/magenta/src/tea/tea.ts | 5 +- rplugin/node/magenta/src/tools/diff.ts | 12 +- rplugin/node/magenta/src/tools/insert.ts | 9 +- rplugin/node/magenta/src/tools/listBuffers.ts | 124 ++++++++++++++++++ rplugin/node/magenta/src/tools/replace.ts | 4 +- rplugin/node/magenta/src/tools/toolManager.ts | 80 ++++++++++- 7 files changed, 224 insertions(+), 19 deletions(-) create mode 100644 rplugin/node/magenta/src/tools/listBuffers.ts diff --git a/rplugin/node/magenta/src/anthropic.ts b/rplugin/node/magenta/src/anthropic.ts index ae8f733..ce6f210 100644 --- a/rplugin/node/magenta/src/anthropic.ts +++ b/rplugin/node/magenta/src/anthropic.ts @@ -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, @@ -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(); diff --git a/rplugin/node/magenta/src/tea/tea.ts b/rplugin/node/magenta/src/tea/tea.ts index 7673e2f..574cecd 100644 --- a/rplugin/node/magenta/src/tea/tea.ts +++ b/rplugin/node/magenta/src/tea/tea.ts @@ -96,8 +96,9 @@ export function createApp({ 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 }; diff --git a/rplugin/node/magenta/src/tools/diff.ts b/rplugin/node/magenta/src/tools/diff.ts index 51ea9e5..d949917 100644 --- a/rplugin/node/magenta/src/tools/diff.ts +++ b/rplugin/node/magenta/src/tools/diff.ts @@ -81,10 +81,10 @@ 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}`, @@ -92,7 +92,7 @@ export async function displayDiffs( continue; } - if (insertEnd == -1) { + if (replaceEnd == -1) { dispatch({ type: "error", error: `Unable to find end location of string ${edit.end} in file ${filePath}`, @@ -100,9 +100,9 @@ export async function displayDiffs( 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; } diff --git a/rplugin/node/magenta/src/tools/insert.ts b/rplugin/node/magenta/src/tools/insert.ts index 6911435..7c9f4ff 100644 --- a/rplugin/node/magenta/src/tools/insert.ts +++ b/rplugin/node/magenta/src/tools/insert.ts @@ -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", diff --git a/rplugin/node/magenta/src/tools/listBuffers.ts b/rplugin/node/magenta/src/tools/listBuffers.ts new file mode 100644 index 0000000..698268d --- /dev/null +++ b/rplugin/node/magenta/src/tools/listBuffers.ts @@ -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) => { + switch (msg.type) { + case "finish": + return [ + { + ...model, + state: { + state: "done", + result: msg.result, + }, + }, + ]; + default: + assertUnreachable(msg.type); + } +}; + +export function initModel( + request: ListBuffersToolRequest, +): [Model, Thunk] { + 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"; +}; diff --git a/rplugin/node/magenta/src/tools/replace.ts b/rplugin/node/magenta/src/tools/replace.ts index e10d705..b64058a 100644 --- a/rplugin/node/magenta/src/tools/replace.ts +++ b/rplugin/node/magenta/src/tools/replace.ts @@ -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", diff --git a/rplugin/node/magenta/src/tools/toolManager.ts b/rplugin/node/magenta/src/tools/toolManager.ts index 16b4a0c..19373bb 100644 --- a/rplugin/node/magenta/src/tools/toolManager.ts +++ b/rplugin/node/magenta/src/tools/toolManager.ts @@ -1,6 +1,7 @@ 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"; @@ -8,13 +9,23 @@ 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: { @@ -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); @@ -40,6 +53,8 @@ export function renderTool(model: ToolModel, dispatch: Dispatch) { switch (model.type) { case "get-file": return GetFile.view({ model }); + case "list-buffers": + return ListBuffers.view({ model }); case "insert": return Insert.view({ model, @@ -71,6 +86,7 @@ export type Msg = type: "init-tool-use"; request: | GetFile.GetFileToolUseRequest + | ListBuffers.ListBuffersToolRequest | Insert.InsertToolUseRequest | Replace.ReplaceToolRequest; } @@ -82,6 +98,10 @@ export type Msg = type: "get-file"; msg: GetFile.Msg; } + | { + type: "list-buffers"; + msg: ListBuffers.Msg; + } | { type: "insert"; msg: Insert.Msg; @@ -127,6 +147,30 @@ export const update: Update = (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 [ @@ -195,6 +239,36 @@ export const update: Update = (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,