From 11e639e12dd93e33262d370ad424b87d7d779d6e Mon Sep 17 00:00:00 2001 From: Denis Lantsman Date: Fri, 13 Dec 2024 21:11:34 -0800 Subject: [PATCH] fixup imports, finish tea --- rplugin/node/magenta/src/anthropic.ts | 4 +- rplugin/node/magenta/src/chat.ts | 12 +- .../node/magenta/src/debug/call-anthropic.ts | 56 +++--- rplugin/node/magenta/src/magenta.ts | 18 +- rplugin/node/magenta/src/moderator.ts | 7 +- rplugin/node/magenta/src/part.ts | 14 +- rplugin/node/magenta/src/sidebar.ts | 126 ++++++++------ rplugin/node/magenta/src/tea/tea.ts | 164 +++++++++--------- rplugin/node/magenta/src/tea/view.spec.ts | 2 - rplugin/node/magenta/src/tea/view.ts | 14 +- rplugin/node/magenta/src/tools/getFile.ts | 10 +- rplugin/node/magenta/src/tools/index.ts | 4 +- rplugin/node/magenta/src/tools/insert.ts | 9 +- rplugin/node/magenta/src/tools/types.ts | 6 +- rplugin/node/magenta/src/types.ts | 2 +- rplugin/node/magenta/src/utils/buffers.ts | 2 +- 16 files changed, 243 insertions(+), 207 deletions(-) diff --git a/rplugin/node/magenta/src/anthropic.ts b/rplugin/node/magenta/src/anthropic.ts index 89d8c74..894c018 100644 --- a/rplugin/node/magenta/src/anthropic.ts +++ b/rplugin/node/magenta/src/anthropic.ts @@ -1,6 +1,6 @@ import Anthropic from "@anthropic-ai/sdk"; -import { Logger } from "./logger"; -import { TOOLS, ToolRequest } from "./tools/index"; +import { Logger } from "./logger.js"; +import { TOOLS, ToolRequest } from "./tools/index.js"; export class AnthropicClient { private client: Anthropic; diff --git a/rplugin/node/magenta/src/chat.ts b/rplugin/node/magenta/src/chat.ts index 32a206d..539543f 100644 --- a/rplugin/node/magenta/src/chat.ts +++ b/rplugin/node/magenta/src/chat.ts @@ -1,13 +1,13 @@ import Anthropic from "@anthropic-ai/sdk"; import { Buffer } from "neovim"; -import { Context } from "./types"; +import { Context } from "./types.js"; import { createMarkedSpaces, getExtMark, Mark, replaceBetweenMarks, -} from "./utils/extmarks"; -import { ToolResultBlockParam } from "@anthropic-ai/sdk/resources"; +} from "./utils/extmarks.js"; +import { ToolResultBlockParam } from "@anthropic-ai/sdk/resources/index.mjs"; import { Line, Part, @@ -15,9 +15,9 @@ import { TextPart, ToolResultPart, ToolUsePart, -} from "./part"; -import { ToolRequest } from "./tools"; -import { ToolProcess } from "./tools/types"; +} from "./part.js"; +import { ToolProcess } from "./tools/types.js"; +import { ToolRequest } from "./tools/index.js"; type Role = "user" | "assistant"; diff --git a/rplugin/node/magenta/src/debug/call-anthropic.ts b/rplugin/node/magenta/src/debug/call-anthropic.ts index ea8bb4b..0407083 100644 --- a/rplugin/node/magenta/src/debug/call-anthropic.ts +++ b/rplugin/node/magenta/src/debug/call-anthropic.ts @@ -1,28 +1,40 @@ -import { AnthropicClient } from '../anthropic' -import { Logger } from '../logger' +import { AnthropicClient } from "../anthropic.js"; +import { Logger } from "../logger.js"; -const logger = new Logger({ - outWriteLine: () => Promise.resolve(undefined), - errWriteLine: () => Promise.resolve(undefined) -}, { - level: 'trace' -}) +const logger = new Logger( + { + outWriteLine: () => Promise.resolve(undefined), + errWrite: () => Promise.resolve(undefined), + errWriteLine: () => Promise.resolve(undefined), + }, + { + level: "trace", + }, +); async function run() { - const client = new AnthropicClient(logger) + const client = new AnthropicClient(logger); - await client.sendMessage([{ - role: 'user', - content: 'try reading the contents of the file ./src/index.js' - }], (text) => { - return Promise.resolve(console.log('text: ' + text)) - }) + await client.sendMessage( + [ + { + role: "user", + content: "try reading the contents of the file ./src/index.js", + }, + ], + (text) => { + return Promise.resolve(console.log("text: " + text)); + }, + ); } -run().then(() => { - console.log('success'); - process.exit(0) -}, (err) => { - console.error(err); - process.exit(1) -}) +run().then( + () => { + console.log("success"); + process.exit(0); + }, + (err) => { + console.error(err); + process.exit(1); + }, +); diff --git a/rplugin/node/magenta/src/magenta.ts b/rplugin/node/magenta/src/magenta.ts index 15503f7..e11465e 100644 --- a/rplugin/node/magenta/src/magenta.ts +++ b/rplugin/node/magenta/src/magenta.ts @@ -1,13 +1,13 @@ -import { AnthropicClient } from "./anthropic"; +import { AnthropicClient } from "./anthropic.js"; import { NvimPlugin } from "neovim"; -import { Sidebar } from "./sidebar"; -import { Chat } from "./chat"; -import { Logger } from "./logger"; -import { Context } from "./types"; -import { TOOLS } from "./tools/index"; -import { assertUnreachable } from "./utils/assertUnreachable"; -import { ToolProcess } from "./tools/types"; -import { Moderator } from "./moderator"; +import { Sidebar } from "./sidebar.js"; +import { Chat } from "./chat.js"; +import { Logger } from "./logger.js"; +import { Context } from "./types.js"; +import { TOOLS } from "./tools/index.js"; +import { assertUnreachable } from "./utils/assertUnreachable.js"; +import { ToolProcess } from "./tools/types.js"; +import { Moderator } from "./moderator.js"; class Magenta { private anthropicClient: AnthropicClient; diff --git a/rplugin/node/magenta/src/moderator.ts b/rplugin/node/magenta/src/moderator.ts index 792b597..e704c12 100644 --- a/rplugin/node/magenta/src/moderator.ts +++ b/rplugin/node/magenta/src/moderator.ts @@ -1,9 +1,8 @@ // as in a debate, moderator keeps track of tool state and manages turn taking in the conversation -import { ToolResultBlockParam } from "@anthropic-ai/sdk/resources"; -import { ToolRequest } from "./tools"; -import { ToolProcess } from "./tools/types"; -import { Context } from "./types"; +import { ToolResultBlockParam } from "@anthropic-ai/sdk/resources/index.mjs"; +import { ToolProcess, ToolRequest } from "./tools/types.js"; +import { Context } from "./types.js"; export class Moderator { private toolProcesses: { diff --git a/rplugin/node/magenta/src/part.ts b/rplugin/node/magenta/src/part.ts index 57b2df4..1b3a3bb 100644 --- a/rplugin/node/magenta/src/part.ts +++ b/rplugin/node/magenta/src/part.ts @@ -1,10 +1,14 @@ import Anthropic from "@anthropic-ai/sdk"; -import { Mark, insertBeforeMark, replaceBetweenMarks } from "./utils/extmarks"; -import { ToolRequest } from "./tools/index"; +import { + Mark, + insertBeforeMark, + replaceBetweenMarks, +} from "./utils/extmarks.js"; +import { ToolRequest } from "./tools/index.js"; import { Buffer } from "neovim"; -import { assertUnreachable } from "./utils/assertUnreachable"; -import { ToolProcess } from "./tools/types"; -import { Context } from "./types"; +import { assertUnreachable } from "./utils/assertUnreachable.js"; +import { ToolProcess } from "./tools/types.js"; +import { Context } from "./types.js"; /** A line that's meant to be sent to neovim. Should not contain newlines */ diff --git a/rplugin/node/magenta/src/sidebar.ts b/rplugin/node/magenta/src/sidebar.ts index e377539..e4bb5a3 100644 --- a/rplugin/node/magenta/src/sidebar.ts +++ b/rplugin/node/magenta/src/sidebar.ts @@ -1,28 +1,33 @@ import { Neovim, Buffer, Window } from "neovim"; -import { Logger } from "./logger"; +import { Logger } from "./logger.js"; /** This will mostly manage the window toggle */ export class Sidebar { - private state: { - state: 'not-loaded' - } | { - state: 'loaded'; - visible: boolean; - displayBuffer: Buffer; - inputBuffer: Buffer; - displayWindow: Window; - inputWindow: Window; - } - - constructor(private nvim: Neovim, private logger: Logger) { - this.state = { state: 'not-loaded' } + private state: + | { + state: "not-loaded"; + } + | { + state: "loaded"; + visible: boolean; + displayBuffer: Buffer; + inputBuffer: Buffer; + displayWindow: Window; + inputWindow: Window; + }; + + constructor( + private nvim: Neovim, + private logger: Logger, + ) { + this.state = { state: "not-loaded" }; } /** returns the input buffer when it was created */ async toggle(displayBuffer: Buffer): Promise { - if (this.state.state == 'not-loaded') { + if (this.state.state == "not-loaded") { await this.create(displayBuffer); } else { if (this.state.visible) { @@ -35,31 +40,35 @@ export class Sidebar { private async create(displayBuffer: Buffer): Promise { const { nvim, logger } = this; - logger.trace(`sidebar.create`) - const totalHeight = await nvim.getOption('lines') as number; - const cmdHeight = await nvim.getOption('cmdheight') as number; + logger.trace(`sidebar.create`); + const totalHeight = (await nvim.getOption("lines")) as number; + const cmdHeight = (await nvim.getOption("cmdheight")) as number; const width = 80; const displayHeight = Math.floor((totalHeight - cmdHeight) * 0.8); const inputHeight = totalHeight - displayHeight - 2; - await nvim.command('leftabove vsplit') + await nvim.command("leftabove vsplit"); const displayWindow = await nvim.window; displayWindow.width = width; - await nvim.lua(`vim.api.nvim_win_set_buf(${displayWindow.id}, ${displayBuffer.id})`) + await nvim.lua( + `vim.api.nvim_win_set_buf(${displayWindow.id}, ${displayBuffer.id})`, + ); - const inputBuffer = await this.nvim.createBuffer(false, true) as Buffer; + const inputBuffer = (await this.nvim.createBuffer(false, true)) as Buffer; - await nvim.command('below split') + await nvim.command("below split"); const inputWindow = await nvim.window; inputWindow.height = inputHeight; - await nvim.lua(`vim.api.nvim_win_set_buf(${inputWindow.id}, ${inputBuffer.id})`) + await nvim.lua( + `vim.api.nvim_win_set_buf(${inputWindow.id}, ${inputBuffer.id})`, + ); - await inputBuffer.setOption('buftype', 'nofile'); - await inputBuffer.setOption('swapfile', false); - await inputBuffer.setLines(['> '], { + await inputBuffer.setOption("buftype", "nofile"); + await inputBuffer.setOption("swapfile", false); + await inputBuffer.setLines(["> "], { start: 0, end: -1, - strictIndexing: false + strictIndexing: false, }); const winOptions = { @@ -74,30 +83,30 @@ export class Sidebar { await inputWindow.setOption(key, value); } - await inputBuffer.request('nvim_buf_set_keymap', [inputBuffer, - 'n', - '', - ':Magenta send', - { silent: true, noremap: true } + await inputBuffer.request("nvim_buf_set_keymap", [ + inputBuffer, + "n", + "", + ":Magenta send", + { silent: true, noremap: true }, ]); - logger.trace(`sidebar.create setting state`) + logger.trace(`sidebar.create setting state`); this.state = { - state: 'loaded', + state: "loaded", visible: true, displayBuffer, inputBuffer, displayWindow, - inputWindow - } - + inputWindow, + }; return inputBuffer; } - async hide() { } + async hide() {} - async show() { } + async show() {} async scrollTop() { // const { displayWindow } = await this.getWindowIfVisible(); @@ -112,42 +121,47 @@ export class Sidebar { // } } - async getWindowIfVisible(): Promise<{ displayWindow?: Window, inputWindow?: Window }> { - if (this.state.state != 'loaded') { + async getWindowIfVisible(): Promise<{ + displayWindow?: Window; + inputWindow?: Window; + }> { + if (this.state.state != "loaded") { return {}; } const { displayWindow, inputWindow } = this.state; - const displayWindowValid = await displayWindow.valid - const inputWindowValid = await inputWindow.valid + const displayWindowValid = await displayWindow.valid; + const inputWindowValid = await inputWindow.valid; return { displayWindow: displayWindowValid ? displayWindow : undefined, - inputWindow: inputWindowValid ? inputWindow : undefined - } + inputWindow: inputWindowValid ? inputWindow : undefined, + }; } async getMessage(): Promise { - if (this.state.state != 'loaded') { - this.logger.trace(`sidebar state is ${this.state.state} in getMessage`) - return ''; + if (this.state.state != "loaded") { + this.logger.trace(`sidebar state is ${this.state.state} in getMessage`); + return ""; } - const { inputBuffer } = this.state + const { inputBuffer } = this.state; const lines = await inputBuffer.getLines({ start: 0, end: -1, - strictIndexing: false - }) + strictIndexing: false, + }); - this.logger.trace(`sidebar got lines ${JSON.stringify(lines)} from inputBuffer`) - const message = lines.join('\n'); - await inputBuffer.setLines([''], { + this.logger.trace( + `sidebar got lines ${JSON.stringify(lines)} from inputBuffer`, + ); + const message = lines.join("\n"); + await inputBuffer.setLines([""], { start: 0, end: -1, - strictIndexing: false - }) + strictIndexing: false, + }); return message; } diff --git a/rplugin/node/magenta/src/tea/tea.ts b/rplugin/node/magenta/src/tea/tea.ts index 5e76cd9..b6d7d2d 100644 --- a/rplugin/node/magenta/src/tea/tea.ts +++ b/rplugin/node/magenta/src/tea/tea.ts @@ -1,24 +1,19 @@ -import { createSignal } from './signal'; -import {View as RenderView } from './view' +import { d, MountedView, MountPoint, mountView, VDOMNode } from "./view.js"; +export type Dispatch = (msg: Msg) => void; export type Update = ( msg: Msg, model: Model, ) => [Model] | [Model, Thunk | undefined]; -export type Dispatch = (msg: Msg) => void; - export type View = ({ model, dispatch, }: { model: Model; dispatch: Dispatch; -}) => RenderView<{ - model: Accessor; - dispatch: Dispatch -}>; +}) => VDOMNode; export interface Subscription { /** Must be unique! @@ -57,90 +52,101 @@ export function createApp({ subscriptionManager: SubscriptionManager; }; }) { - let dispatchRef: { current: Dispatch | undefined } = { - current: undefined, + let currentState: AppState = { + status: "running", + model: initialModel, }; + let root: + | MountedView<{ currentState: AppState; dispatch: Dispatch }> + | undefined; - function App() { - const [appState, setAppState] = createSignal({ - status: "running", - model: initialModel, - }); + const dispatch = (msg: Msg) => { + if (currentState.status == "error") { + return currentState; + } - const subs: { - [id: string]: Subscription; - } = {}; - - const dispatch = useCallback((msg: Msg) => { - setAppState((currentState) => { - if (currentState.status == "error") { - return currentState; - } - - try { - const [nextModel, thunk] = update(msg, currentState.model); - - if (thunk) { - // purposefully do not await - thunk(dispatch); - } - - return { status: "running", model: nextModel }; - } catch (e) { - console.error(e); - return { status: "error", error: (e as Error).message }; - } - }); - }, []); + try { + const [nextModel, thunk] = update(msg, currentState.model); - dispatchRef.current = dispatch; + if (thunk) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + thunk(dispatch); + } - React.useEffect(() => { - if (!sub) return; - if (appState.status != "running") return; + currentState = { status: "running", model: nextModel }; + updateSubs(currentState); - const subscriptionManager = sub.subscriptionManager; - const currentSubscriptions = subs.current; + if (root) { + // schedule a re-render + root.render({ currentState, dispatch }).catch((err) => { + console.error(err); + throw err; + }); + } + } catch (e) { + console.error(e); + currentState = { status: "error", error: (e as Error).message }; + } + }; - const nextSubs = sub.subscriptions(appState.model); - const nextSubsMap: { [id: string]: Subscription } = {}; + const subs: { + [id: string]: Subscription; + } = {}; - // Add new subs - nextSubs.forEach((sub) => { - nextSubsMap[sub.id] = sub; - if (!subscriptionManager[sub.id]) { - subscriptionManager[sub.id].subscribe(dispatch); - currentSubscriptions[sub.id] = sub; - } - }); + function updateSubs(currentState: AppState) { + if (!sub) return; + if (currentState.status != "running") return; - // Remove old subs - Object.keys(currentSubscriptions).forEach((id) => { - if (!nextSubsMap[id]) { - subscriptionManager[id as SubscriptionType].unsubscribe(); - delete subs.current[id]; - } - }); + const subscriptionManager = sub.subscriptionManager; + const currentSubscriptions = subs; + + const nextSubs = sub.subscriptions(currentState.model); + const nextSubsMap: { [id: string]: Subscription } = {}; + + // Add new subs + nextSubs.forEach((sub) => { + nextSubsMap[sub.id] = sub; + if (!subscriptionManager[sub.id]) { + subscriptionManager[sub.id].subscribe(dispatch); + currentSubscriptions[sub.id] = sub; + } + }); - return () => {}; - }, [appState]); - - return ( -
- {appState.status == "running" ? ( - - ) : ( -
Error: {appState.error}
- )} -
- ); + // Remove old subs + Object.keys(currentSubscriptions).forEach((id) => { + if (!nextSubsMap[id]) { + subscriptionManager[id as SubscriptionType].unsubscribe(); + delete subs[id]; + } + }); + + return () => {}; + } + + updateSubs(currentState); + + function App({ + currentState, + dispatch, + }: { + currentState: AppState; + dispatch: Dispatch; + }) { + return d`${ + currentState.status == "running" + ? View({ model: currentState.model, dispatch }) + : d`Error: ${currentState.error}` + }`; } return { - mount(element: Element) { - const root = createRoot(element); - flushSync(() => root.render()); - return { root, dispatchRef }; + async mount(mount: MountPoint) { + root = await mountView({ + view: App, + mount, + props: { currentState, dispatch }, + }); + return { root, dispatch }; }, }; } diff --git a/rplugin/node/magenta/src/tea/view.spec.ts b/rplugin/node/magenta/src/tea/view.spec.ts index 95089b8..c07590e 100644 --- a/rplugin/node/magenta/src/tea/view.spec.ts +++ b/rplugin/node/magenta/src/tea/view.spec.ts @@ -24,7 +24,6 @@ await test.describe("Neovim Plugin Tests", async () => { await test("basic rendering & update", async () => { const buffer = (await nvim.createBuffer(false, true)) as Buffer; await buffer.setLines([""], { start: 0, end: 0, strictIndexing: false }); - const namespace = await nvim.createNamespace("test"); await buffer.setOption("modifiable", false); @@ -35,7 +34,6 @@ await test.describe("Neovim Plugin Tests", async () => { mount: { nvim, buffer, - namespace, startPos: { row: 0, col: 0 }, endPos: { row: 0, col: 0 }, }, diff --git a/rplugin/node/magenta/src/tea/view.ts b/rplugin/node/magenta/src/tea/view.ts index 0832f11..22c4e50 100644 --- a/rplugin/node/magenta/src/tea/view.ts +++ b/rplugin/node/magenta/src/tea/view.ts @@ -41,6 +41,13 @@ export type MountedComponentNode = { export type MountedVDOM = MountedStringNode | MountedComponentNode; +export type MountedView

= { + render(props: P): Promise; + /** for testing + */ + _getMountedNode(): MountedVDOM; +}; + export async function mountView

({ view, mount, @@ -49,12 +56,7 @@ export async function mountView

({ view: View

; mount: MountPoint; props: P; -}): Promise<{ - render(props: P): Promise; - /** for testing - */ - _getMountedNode(): MountedVDOM; -}> { +}): Promise> { let mountedNode = await render({ vdom: view(props), mount }); return { async render(props) { diff --git a/rplugin/node/magenta/src/tools/getFile.ts b/rplugin/node/magenta/src/tools/getFile.ts index cbb2a86..e04b1a2 100644 --- a/rplugin/node/magenta/src/tools/getFile.ts +++ b/rplugin/node/magenta/src/tools/getFile.ts @@ -1,11 +1,11 @@ import * as Anthropic from "@anthropic-ai/sdk"; -import { Context } from "../types"; -import { getBufferIfOpen } from "../utils/buffers"; -import { ToolResultBlockParam } from "@anthropic-ai/sdk/resources"; +import { Context } from "../types.js"; +import { getBufferIfOpen } from "../utils/buffers.js"; import fs from "fs"; import path from "path"; -import { Line } from "../part"; -import { assertUnreachable } from "../utils/assertUnreachable"; +import { Line } from "../part.js"; +import { assertUnreachable } from "../utils/assertUnreachable.js"; +import { ToolResultBlockParam } from "@anthropic-ai/sdk/resources/index.mjs"; type State = | { diff --git a/rplugin/node/magenta/src/tools/index.ts b/rplugin/node/magenta/src/tools/index.ts index 93337ab..d5b7bb3 100644 --- a/rplugin/node/magenta/src/tools/index.ts +++ b/rplugin/node/magenta/src/tools/index.ts @@ -1,5 +1,5 @@ -import { FileTool, GetFileToolUseRequest } from "./getFile"; -import { InsertTool, InsertToolUseRequest } from "./insert"; +import { FileTool, GetFileToolUseRequest } from "./getFile.js"; +import { InsertTool, InsertToolUseRequest } from "./insert.js"; export const TOOLS = { get_file: new FileTool(), diff --git a/rplugin/node/magenta/src/tools/insert.ts b/rplugin/node/magenta/src/tools/insert.ts index 4af07c4..654a093 100644 --- a/rplugin/node/magenta/src/tools/insert.ts +++ b/rplugin/node/magenta/src/tools/insert.ts @@ -1,9 +1,10 @@ import * as Anthropic from "@anthropic-ai/sdk"; -import { Context } from "../types"; -import { ToolResultBlockParam } from "@anthropic-ai/sdk/resources"; +import { Context } from "../types.js"; +import {} from "@anthropic-ai/sdk"; import { Buffer } from "neovim"; -import { Line } from "../part"; -import { assertUnreachable } from "../utils/assertUnreachable"; +import { Line } from "../part.js"; +import { assertUnreachable } from "../utils/assertUnreachable.js"; +import { ToolResultBlockParam } from "@anthropic-ai/sdk/resources/index.mjs"; type State = | { diff --git a/rplugin/node/magenta/src/tools/types.ts b/rplugin/node/magenta/src/tools/types.ts index 868f8ff..6774506 100644 --- a/rplugin/node/magenta/src/tools/types.ts +++ b/rplugin/node/magenta/src/tools/types.ts @@ -1,6 +1,6 @@ -import { FileToolProcess, GetFileToolUseRequest } from "./getFile"; -import { InsertProcess, InsertToolUseRequest } from "./insert"; -import { Context } from "../types"; +import { FileToolProcess, GetFileToolUseRequest } from "./getFile.js"; +import { InsertProcess, InsertToolUseRequest } from "./insert.js"; +import { Context } from "../types.js"; export type ToolRequest = GetFileToolUseRequest | InsertToolUseRequest; diff --git a/rplugin/node/magenta/src/types.ts b/rplugin/node/magenta/src/types.ts index ae84b88..c727746 100644 --- a/rplugin/node/magenta/src/types.ts +++ b/rplugin/node/magenta/src/types.ts @@ -1,5 +1,5 @@ import { Neovim } from "neovim" -import { Logger } from "./logger" +import { Logger } from "./logger.js" export type Context = { nvim: Neovim, diff --git a/rplugin/node/magenta/src/utils/buffers.ts b/rplugin/node/magenta/src/utils/buffers.ts index 81a5c36..736d56d 100644 --- a/rplugin/node/magenta/src/utils/buffers.ts +++ b/rplugin/node/magenta/src/utils/buffers.ts @@ -1,6 +1,6 @@ import { Buffer } from "neovim"; import * as path from "path"; -import { Context } from "../types"; +import { Context } from "../types.js"; export async function getBufferIfOpen({ context,