diff --git a/extensions/spectacular/package-lock.json b/extensions/spectacular/package-lock.json
index 7a5c03f3..e241c3ab 100644
--- a/extensions/spectacular/package-lock.json
+++ b/extensions/spectacular/package-lock.json
@@ -15,6 +15,7 @@
"axios": "^1.7.3",
"diff": "^6.0.0-beta",
"dotenv": "^16.4.5",
+ "lru-cache": "^11.0.1",
"node-tree-sitter": "^0.0.1",
"posthog-js": "^1.157.2",
"python-ast": "^0.1.0",
@@ -2649,10 +2650,12 @@
}
},
"node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "dev": true
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz",
+ "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==",
+ "engines": {
+ "node": "20 || >=22"
+ }
},
"node_modules/make-dir": {
"version": "4.0.0",
@@ -3267,6 +3270,12 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true
+ },
"node_modules/path-to-regexp": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz",
diff --git a/extensions/spectacular/package.json b/extensions/spectacular/package.json
index 23065b5f..725037d9 100644
--- a/extensions/spectacular/package.json
+++ b/extensions/spectacular/package.json
@@ -149,6 +149,7 @@
"axios": "^1.7.3",
"diff": "^6.0.0-beta",
"dotenv": "^16.4.5",
+ "lru-cache": "^11.0.1",
"node-tree-sitter": "^0.0.1",
"posthog-js": "^1.157.2",
"python-ast": "^0.1.0",
diff --git a/extensions/spectacular/src/HelloWorldPanel.ts b/extensions/spectacular/src/HelloWorldPanel.ts
index b9bd173a..36d627bb 100644
--- a/extensions/spectacular/src/HelloWorldPanel.ts
+++ b/extensions/spectacular/src/HelloWorldPanel.ts
@@ -7,20 +7,19 @@ import {
} from "vscode";
import * as vscode from "vscode";
import { getUri, getNonce } from "./util/utils";
-import { Conversation, TaskMode } from "./types";
+import { TaskMode } from "./types";
import { MeltyExtension } from "./extension";
import { createNewDehydratedTask } from "./backend/tasks";
import * as config from "./util/config";
import { WebviewNotifier } from "./services/WebviewNotifier";
import { FileManager } from "./services/FileManager";
-import { DehydratedTask, RpcMethod } from "./types";
+import { DehydratedTask, RpcMethod, UserAttachedImage } from "./types";
import { Coder } from "./backend/assistants/coder";
import { Vanilla } from "./backend/assistants/vanilla";
import { GitManager } from "./services/GitManager";
import { GitHubManager } from './services/GitHubManager';
import { TaskManager } from './services/TaskManager';
import posthog from "posthog-js";
-import { create } from 'domain';
/**
* This class manages the state and behavior of HelloWorld webview panels.
@@ -135,7 +134,7 @@ export class HelloWorldPanel implements WebviewViewProvider {
-
+
Melty
@@ -200,7 +199,7 @@ export class HelloWorldPanel implements WebviewViewProvider {
}
task.addErrorJoule(message);
await WebviewNotifier.getInstance().sendNotification("updateTask", {
- task: task.dehydrateForWire(),
+ task: await task.dehydrateForWire(),
});
}
@@ -229,7 +228,7 @@ export class HelloWorldPanel implements WebviewViewProvider {
return this.rpcGetLatestCommit();
case "chatMessage":
return await this.rpcStartResponse(
- params.text, params.taskId
+ params.text, params.taskId, params.images
);
case "createTask":
return await this.rpcCreateTask(
@@ -257,6 +256,13 @@ export class HelloWorldPanel implements WebviewViewProvider {
return this.rpcCheckOnboardingComplete();
case "setOnboardingComplete":
return this.rpcSetOnboardingComplete();
+ case "showNotification":
+ if (params.notificationType === 'error') {
+ vscode.window.showErrorMessage(params.message);
+ } else {
+ vscode.window.showInformationMessage(params.message);
+ }
+ return true;
default:
throw new Error(`Unknown RPC method: ${method}`);
}
@@ -336,7 +342,7 @@ export class HelloWorldPanel implements WebviewViewProvider {
// { uri: newFolderUri }
// );
- const openResult: any = await vscode.commands.executeCommand('vscode.openFolder', newFolderUri, false)
+ const openResult: any = await vscode.commands.executeCommand('vscode.openFolder', newFolderUri, false);
return openResult === undefined;
} else {
return false;
@@ -368,12 +374,12 @@ export class HelloWorldPanel implements WebviewViewProvider {
}
}
- private async rpcGetActiveTask(taskId: string): Promise {
+ private async rpcGetActiveTask(taskId: string) {
const task = this._taskManager.getActiveTask(taskId);
if (!task) {
vscode.window.showErrorMessage(`Failed to get active task ${taskId}`);
}
- return task!.dehydrate();
+ return task!.dehydrateForWire();
}
private async rpcListMeltyFiles(): Promise {
@@ -463,12 +469,12 @@ export class HelloWorldPanel implements WebviewViewProvider {
return vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark ? 'dark' : 'light';
}
- private async rpcStartResponse(text: string, taskId: string): Promise {
+ private async rpcStartResponse(text: string, taskId: string, images?: UserAttachedImage[]): Promise {
const task = this._taskManager.getActiveTask(taskId)!;
if (!task) {
throw new Error(`Tried to chat with an inactive task ${taskId} (active task is ${this._taskManager.getActiveTaskId()})`);
}
- return task.startResponse(text);
+ return task.startResponse(text, images);
}
private async rpcStopResponse(taskId: string): Promise {
diff --git a/extensions/spectacular/src/backend/assistants/baseAssistant.ts b/extensions/spectacular/src/backend/assistants/baseAssistant.ts
index 683dc778..7b227605 100644
--- a/extensions/spectacular/src/backend/assistants/baseAssistant.ts
+++ b/extensions/spectacular/src/backend/assistants/baseAssistant.ts
@@ -12,10 +12,10 @@ export abstract class BaseAssistant {
abstract respond(
conversation: Conversation,
contextPaths: ContextPaths,
- processPartial: (partialConversation: Conversation) => void
+ processPartial: (partialConversation: Conversation) => Promise,
): Promise;
- protected encodeMessages(conversation: Conversation): ClaudeMessage[] {
+ protected async encodeMessages(conversation: Conversation): Promise {
const userPrompt = getUserPrompt();
const messages: ClaudeMessage[] = [];
@@ -35,12 +35,12 @@ export abstract class BaseAssistant {
return author === "human" ? "user" : "assistant";
}
- messages.push(
- ...conversation.joules.map((joule: Joule) => ({
+ for (const joule of conversation.joules) {
+ messages.push({
role: authorToRole(joule.author),
- content: joules.formatMessageForClaude(joule),
- }))
- );
+ content: await joules.formatMessageForClaude(joule),
+ });
+ }
return messages;
}
diff --git a/extensions/spectacular/src/backend/assistants/coder.ts b/extensions/spectacular/src/backend/assistants/coder.ts
index 8e97de5c..325e1bc5 100644
--- a/extensions/spectacular/src/backend/assistants/coder.ts
+++ b/extensions/spectacular/src/backend/assistants/coder.ts
@@ -41,7 +41,7 @@ export class Coder extends BaseAssistant {
async respond(
conversation: Conversation,
contextPaths: ContextPaths,
- processPartial: (partialConversation: Conversation) => void,
+ processPartial: (partialConversation: Conversation) => Promise,
cancellationToken?: vscode.CancellationToken
) {
webviewNotifier.updateStatusMessage("Preparing context");
@@ -83,7 +83,7 @@ export class Coder extends BaseAssistant {
messages: [
// TODOV2 user system info
...this.codebaseView(contextPaths, repoMapString),
- ...this.encodeMessages(conversation),
+ ...(await this.encodeMessages(conversation)),
],
};
@@ -110,7 +110,7 @@ export class Coder extends BaseAssistant {
contextPaths,
true // ignore changes
);
- processPartial(newConversation);
+ await processPartial(newConversation);
}
}
);
diff --git a/extensions/spectacular/src/backend/conversations.ts b/extensions/spectacular/src/backend/conversations.ts
index 3229cfbc..eb985662 100644
--- a/extensions/spectacular/src/backend/conversations.ts
+++ b/extensions/spectacular/src/backend/conversations.ts
@@ -1,4 +1,4 @@
-import { Joule, Conversation } from "../types";
+import { Joule, Conversation, JouleBot, DehydratedConversation } from "../types";
import * as vscode from "vscode";
import * as joules from "./joules";
export function create(): Conversation {
@@ -74,11 +74,16 @@ function removeLeadingBotJoules(conversation: Conversation): Conversation {
}
function removeEmptyJoules(conversation: Conversation): Conversation {
- return {
- joules: conversation.joules.filter((joule) =>
- joules.formatMessageForClaude(joule).length > 0
- ),
- };
+ const newJoules: Joule[] = [];
+ for (const joule of conversation.joules) {
+ if (joule.author === 'human' && (joule.message.trim().length > 0 || (joule.images && joule.images.length > 0))) {
+ newJoules.push(joule);
+ }
+ if (joule.author === 'bot' && (joule as JouleBot)?.botExecInfo?.rawOutput?.trim()?.length > 0) {
+ newJoules.push(joule);
+ }
+ }
+ return { joules: newJoules };
}
/**
@@ -126,3 +131,10 @@ function removeFinalJoulesFrom(
};
}
}
+
+export async function dehydrate(conversation: Conversation): Promise {
+ return {
+ ...conversation,
+ joules: await Promise.all(conversation.joules.map(j => joules.dehydrate(j))),
+ };
+}
diff --git a/extensions/spectacular/src/backend/datastores.ts b/extensions/spectacular/src/backend/datastores.ts
index 2f69f94c..fd1003ec 100644
--- a/extensions/spectacular/src/backend/datastores.ts
+++ b/extensions/spectacular/src/backend/datastores.ts
@@ -1,8 +1,11 @@
-import * as fs from "fs";
+import fs from "fs/promises";
import * as path from "path";
import * as vscode from "vscode";
-import { resolveTildePath } from "../util/utils";
-import { DehydratedTask } from "types";
+import { isPathInside, resolveTildePath, isDirectory, isFile, pathExists } from "../util/utils";
+import { DehydratedTask, JouleImage, UserAttachedImage } from "types";
+import { v4 as uuidv4 } from "uuid";
+import { LRUCache } from 'lru-cache';
+import { IMAGE_CACHE_TTL_MS, MAX_IMAGES_TO_CACHE } from '../constants';
function getMeltyDir(): string {
const config = vscode.workspace.getConfiguration('melty');
@@ -11,17 +14,22 @@ function getMeltyDir(): string {
return resolvedPath;
}
-export function loadTasksFromDisk(): Map {
+export async function loadTasksFromDisk(): Promise