mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
Merge branch 'main' into pr/w1gs/87
This commit is contained in:
commit
e2e6cb0327
11 changed files with 447 additions and 403 deletions
57
extensions/void/package-lock.json
generated
57
extensions/void/package-lock.json
generated
|
|
@ -9,9 +9,6 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.27.1",
|
"@anthropic-ai/sdk": "^0.27.1",
|
||||||
"diff-match-patch": "^1.0.5",
|
|
||||||
"diff": "^7.0.0",
|
|
||||||
"ollama": "^0.5.9",
|
|
||||||
"openai": "^4.57.0"
|
"openai": "^4.57.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -23,7 +20,7 @@
|
||||||
"@types/node": "^22.5.1",
|
"@types/node": "^22.5.1",
|
||||||
"@types/react": "^18.3.4",
|
"@types/react": "^18.3.4",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/vscode": "1.89.0",
|
"@types/vscode": "1.92.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
||||||
"@typescript-eslint/parser": "^8.3.0",
|
"@typescript-eslint/parser": "^8.3.0",
|
||||||
"@vscode/test-cli": "^0.0.10",
|
"@vscode/test-cli": "^0.0.10",
|
||||||
|
|
@ -42,7 +39,8 @@
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"tailwindcss": "^3.4.10",
|
"tailwindcss": "^3.4.10",
|
||||||
"typescript": "5.5.4",
|
"typescript": "5.5.4",
|
||||||
"typescript-eslint": "^8.3.0"
|
"typescript-eslint": "^8.3.0",
|
||||||
|
"uuid": "^10.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.89.0"
|
"vscode": "^1.89.0"
|
||||||
|
|
@ -769,9 +767,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/vscode": {
|
"node_modules/@types/vscode": {
|
||||||
"version": "1.89.0",
|
"version": "1.92.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.89.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.92.0.tgz",
|
||||||
"integrity": "sha512-TMfGKLSVxfGfoO8JfIE/neZqv7QLwS4nwPwL/NwMvxtAY2230H2I4Z5xx6836pmJvMAzqooRQ4pmLm7RUicP3A==",
|
"integrity": "sha512-DcZoCj17RXlzB4XJ7IfKdPTcTGDLYvTOcTNkvtjXWF+K2TlKzHHkBEXNWQRpBIXixNEUgx39cQeTFunY0E2msw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
|
@ -2153,20 +2151,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/diff": {
|
|
||||||
"version": "7.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
|
|
||||||
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.3.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/diff-match-patch": {
|
|
||||||
"version": "1.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
|
|
||||||
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="
|
|
||||||
},
|
|
||||||
"node_modules/diff-sequences": {
|
"node_modules/diff-sequences": {
|
||||||
"version": "29.6.3",
|
"version": "29.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
|
||||||
|
|
@ -5573,15 +5557,6 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ollama": {
|
|
||||||
"version": "0.5.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.9.tgz",
|
|
||||||
"integrity": "sha512-F/KZuDRC+ZsVCuMvcOYuQ6zj42/idzCkkuknGyyGVmNStMZ/sU3jQpvhnl4SyC0+zBzLiKNZJnJeuPFuieWZvQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"whatwg-fetch": "^3.6.20"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/once": {
|
"node_modules/once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
|
@ -7720,6 +7695,20 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/v8-to-istanbul": {
|
"node_modules/v8-to-istanbul": {
|
||||||
"version": "9.3.0",
|
"version": "9.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
|
||||||
|
|
@ -7780,12 +7769,6 @@
|
||||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/whatwg-fetch": {
|
|
||||||
"version": "3.6.20",
|
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
|
|
||||||
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/whatwg-url": {
|
"node_modules/whatwg-url": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -363,7 +363,7 @@
|
||||||
"@types/node": "^22.5.1",
|
"@types/node": "^22.5.1",
|
||||||
"@types/react": "^18.3.4",
|
"@types/react": "^18.3.4",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/vscode": "1.89.0",
|
"@types/vscode": "1.92.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
||||||
"@typescript-eslint/parser": "^8.3.0",
|
"@typescript-eslint/parser": "^8.3.0",
|
||||||
"@vscode/test-cli": "^0.0.10",
|
"@vscode/test-cli": "^0.0.10",
|
||||||
|
|
@ -382,13 +382,11 @@
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"tailwindcss": "^3.4.10",
|
"tailwindcss": "^3.4.10",
|
||||||
"typescript": "5.5.4",
|
"typescript": "5.5.4",
|
||||||
"typescript-eslint": "^8.3.0"
|
"typescript-eslint": "^8.3.0",
|
||||||
|
"uuid": "^10.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.27.1",
|
"@anthropic-ai/sdk": "^0.27.1",
|
||||||
"diff-match-patch": "^1.0.5",
|
"openai": "^4.57.0"
|
||||||
"ollama": "^0.5.9",
|
|
||||||
"openai": "^4.57.0",
|
|
||||||
"diff": "^7.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
|
||||||
const openAICompatibleEndpoint: string | undefined = vscode.workspace.getConfiguration('void.openAICompatible').get('endpoint');
|
const openAICompatibleEndpoint: string | undefined = vscode.workspace.getConfiguration('void.openAICompatible').get('endpoint');
|
||||||
this._webviewDeps.push('void.openAICompatible.endpoint');
|
this._webviewDeps.push('void.openAICompatible.endpoint');
|
||||||
if (openAICompatibleEndpoint)
|
if (openAICompatibleEndpoint)
|
||||||
allowed_urls.push(openAICompatibleEndpoint);
|
allowed_urls.push(openAICompatibleEndpoint+'/chat/completions');
|
||||||
|
|
||||||
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/index.js'));
|
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/index.js'));
|
||||||
const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/styles.css'));
|
const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/styles.css'));
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { DisplayChangesProvider } from './DisplayChangesProvider';
|
import { DisplayChangesProvider } from './DisplayChangesProvider';
|
||||||
import { BaseDiffArea, ChatThreads, WebviewMessage } from './shared_types';
|
import { BaseDiffArea, ChatThreads, MessageFromSidebar, MessageToSidebar } from './shared_types';
|
||||||
import { SidebarWebviewProvider } from './SidebarWebviewProvider';
|
import { SidebarWebviewProvider } from './SidebarWebviewProvider';
|
||||||
import { ApiConfig } from './common/sendLLMMessage';
|
import { ApiConfig } from './common/sendLLMMessage';
|
||||||
|
|
||||||
|
|
@ -79,7 +79,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
const filePath = editor.document.uri;
|
const filePath = editor.document.uri;
|
||||||
|
|
||||||
// send message to the webview (Sidebar.tsx)
|
// send message to the webview (Sidebar.tsx)
|
||||||
webviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, selectionRange, filePath } } satisfies WebviewMessage));
|
webviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, selectionRange, filePath } } satisfies MessageToSidebar));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -105,23 +105,23 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
|
|
||||||
// top navigation bar commands
|
// top navigation bar commands
|
||||||
context.subscriptions.push(vscode.commands.registerCommand('void.startNewThread', async () => {
|
context.subscriptions.push(vscode.commands.registerCommand('void.startNewThread', async () => {
|
||||||
webview.postMessage({ type: 'startNewThread' } satisfies WebviewMessage)
|
webview.postMessage({ type: 'startNewThread' } satisfies MessageToSidebar)
|
||||||
}))
|
}))
|
||||||
context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => {
|
context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => {
|
||||||
webview.postMessage({ type: 'toggleThreadSelector' } satisfies WebviewMessage)
|
webview.postMessage({ type: 'toggleThreadSelector' } satisfies MessageToSidebar)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// when config changes, send it to the sidebar
|
// when config changes, send it to the sidebar
|
||||||
vscode.workspace.onDidChangeConfiguration(e => {
|
vscode.workspace.onDidChangeConfiguration(e => {
|
||||||
if (e.affectsConfiguration('void')) {
|
if (e.affectsConfiguration('void')) {
|
||||||
const apiConfig = getApiConfig()
|
const apiConfig = getApiConfig()
|
||||||
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage)
|
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies MessageToSidebar)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
// Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`)
|
// Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`)
|
||||||
webview.onDidReceiveMessage(async (m: WebviewMessage) => {
|
webview.onDidReceiveMessage(async (m: MessageFromSidebar) => {
|
||||||
|
|
||||||
if (m.type === 'requestFiles') {
|
if (m.type === 'requestFiles') {
|
||||||
|
|
||||||
|
|
@ -131,7 +131,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
)
|
)
|
||||||
|
|
||||||
// send contents to webview
|
// send contents to webview
|
||||||
webview.postMessage({ type: 'files', files, } satisfies WebviewMessage)
|
webview.postMessage({ type: 'files', files, } satisfies MessageToSidebar)
|
||||||
|
|
||||||
} else if (m.type === 'applyChanges') {
|
} else if (m.type === 'applyChanges') {
|
||||||
|
|
||||||
|
|
@ -168,11 +168,11 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
}
|
}
|
||||||
else if (m.type === 'getApiConfig') {
|
else if (m.type === 'getApiConfig') {
|
||||||
const apiConfig = getApiConfig()
|
const apiConfig = getApiConfig()
|
||||||
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage)
|
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies MessageToSidebar)
|
||||||
}
|
}
|
||||||
else if (m.type === 'getAllThreads') {
|
else if (m.type === 'getAllThreads') {
|
||||||
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
|
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
|
||||||
webview.postMessage({ type: 'allThreads', threads } satisfies WebviewMessage)
|
webview.postMessage({ type: 'allThreads', threads } satisfies MessageToSidebar)
|
||||||
}
|
}
|
||||||
else if (m.type === 'persistThread') {
|
else if (m.type === 'persistThread') {
|
||||||
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
|
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
|
||||||
|
|
@ -180,7 +180,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
context.workspaceState.update('allThreads', updatedThreads)
|
context.workspaceState.update('allThreads', updatedThreads)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.error('unrecognized command', m.type, m)
|
console.error('unrecognized command', m)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,45 +38,25 @@ type Diff = {
|
||||||
lenses: vscode.CodeLens[],
|
lenses: vscode.CodeLens[],
|
||||||
} & BaseDiff
|
} & BaseDiff
|
||||||
|
|
||||||
type WebviewMessage = (
|
// editor -> sidebar
|
||||||
|
type MessageToSidebar = (
|
||||||
// editor -> sidebar
|
|
||||||
| { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor
|
| { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor
|
||||||
|
|
||||||
// sidebar -> editor
|
|
||||||
| { type: 'applyChanges', code: string } // user clicks "apply" in the sidebar
|
|
||||||
|
|
||||||
// sidebar -> editor
|
|
||||||
| { type: 'requestFiles', filepaths: vscode.Uri[] }
|
|
||||||
|
|
||||||
// editor -> sidebar
|
|
||||||
| { type: 'files', files: { filepath: vscode.Uri, content: string }[] }
|
| { type: 'files', files: { filepath: vscode.Uri, content: string }[] }
|
||||||
|
|
||||||
// sidebar -> editor
|
|
||||||
| { type: 'getApiConfig' }
|
|
||||||
|
|
||||||
// editor -> sidebar
|
|
||||||
| { type: 'apiConfig', apiConfig: ApiConfig }
|
| { type: 'apiConfig', apiConfig: ApiConfig }
|
||||||
|
|
||||||
// sidebar -> editor
|
|
||||||
| { type: 'getAllThreads' }
|
|
||||||
|
|
||||||
// editor -> sidebar
|
|
||||||
| { type: 'allThreads', threads: ChatThreads }
|
| { type: 'allThreads', threads: ChatThreads }
|
||||||
|
|
||||||
// sidebar -> editor
|
|
||||||
| { type: 'persistThread', thread: ChatThreads[string] }
|
|
||||||
|
|
||||||
// editor -> sidebar
|
|
||||||
| { type: 'startNewThread' }
|
| { type: 'startNewThread' }
|
||||||
|
|
||||||
// editor -> sidebar
|
|
||||||
| { type: 'toggleThreadSelector' }
|
| { type: 'toggleThreadSelector' }
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// sidebar -> editor
|
||||||
|
type MessageFromSidebar = (
|
||||||
|
| { type: 'applyChanges', code: string } // user clicks "apply" in the sidebar
|
||||||
|
| { type: 'requestFiles', filepaths: vscode.Uri[] }
|
||||||
|
| { type: 'getApiConfig' }
|
||||||
|
| { type: 'getAllThreads' }
|
||||||
|
| { type: 'persistThread', thread: ChatThreads[string] }
|
||||||
|
)
|
||||||
|
|
||||||
type Command = WebviewMessage['type']
|
|
||||||
|
|
||||||
type ChatThreads = {
|
type ChatThreads = {
|
||||||
[id: string]: {
|
[id: string]: {
|
||||||
|
|
@ -105,8 +85,8 @@ export {
|
||||||
Diff, DiffArea,
|
Diff, DiffArea,
|
||||||
CodeSelection,
|
CodeSelection,
|
||||||
File,
|
File,
|
||||||
WebviewMessage,
|
MessageFromSidebar,
|
||||||
Command,
|
MessageToSidebar,
|
||||||
ChatThreads,
|
ChatThreads,
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,330 +1,44 @@
|
||||||
import React, { useState, useEffect, useRef, useCallback, FormEvent } from "react"
|
import React, { useState, useEffect, useRef, useCallback, FormEvent } from "react"
|
||||||
import { ApiConfig, sendLLMMessage } from "../common/sendLLMMessage"
|
import { ApiConfig, sendLLMMessage } from "../common/sendLLMMessage"
|
||||||
import { File, CodeSelection, WebviewMessage, ChatMessage } from "../shared_types"
|
import { CodeSelection, ChatMessage, MessageToSidebar } from "../shared_types"
|
||||||
import { awaitVSCodeResponse, getVSCodeAPI, resolveAwaitingVSCodeResponse } from "./getVscodeApi"
|
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode } from "./getVscodeApi"
|
||||||
|
|
||||||
import { marked } from 'marked';
|
import { SidebarThreadSelector } from "./SidebarThreadSelector";
|
||||||
import MarkdownRender from "./markdown/MarkdownRender";
|
|
||||||
import BlockCode from "./markdown/BlockCode";
|
|
||||||
|
|
||||||
import * as vscode from 'vscode'
|
|
||||||
import { SelectedFiles } from "./components/SelectedFiles";
|
|
||||||
import { useThreads } from "./threadsContext";
|
import { useThreads } from "./threadsContext";
|
||||||
|
import { SidebarChat } from "./SidebarChat";
|
||||||
|
|
||||||
const filesStr = (fullFiles: File[]) => {
|
|
||||||
return fullFiles.map(({ filepath, content }) =>
|
|
||||||
`
|
|
||||||
${filepath.fsPath}
|
|
||||||
\`\`\`
|
|
||||||
${content}
|
|
||||||
\`\`\``).join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
const userInstructionsStr = (instructions: string, files: File[], selection: CodeSelection | null) => {
|
|
||||||
return `
|
|
||||||
${filesStr(files)}
|
|
||||||
|
|
||||||
${!selection ? '' : `
|
|
||||||
I am currently selecting this code:
|
|
||||||
\`\`\`${selection.selectionStr}\`\`\`
|
|
||||||
`}
|
|
||||||
|
|
||||||
Please edit the code following these instructions (or, if appropriate, answer my question instead):
|
|
||||||
${instructions}
|
|
||||||
|
|
||||||
If you make a change, rewrite the entire file.
|
|
||||||
`; // TODO don't rewrite the whole file on prompt, instead rewrite it when click Apply
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
|
|
||||||
|
|
||||||
const role = chatMessage.role
|
|
||||||
const children = chatMessage.displayContent
|
|
||||||
|
|
||||||
if (!children)
|
|
||||||
return null
|
|
||||||
|
|
||||||
let chatbubbleContents: React.ReactNode
|
|
||||||
|
|
||||||
if (role === 'user') {
|
|
||||||
chatbubbleContents = <>
|
|
||||||
<SelectedFiles files={chatMessage.files} setFiles={null} />
|
|
||||||
{chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} hideToolbar />}
|
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
else if (role === 'assistant') {
|
|
||||||
|
|
||||||
chatbubbleContents = <MarkdownRender string={children} /> // sectionsHTML
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return <div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
|
|
||||||
<div className={`inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full`}>
|
|
||||||
{chatbubbleContents}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
const ThreadSelector = ({ onClose }: { onClose: () => void }) => {
|
|
||||||
const { allThreads, currentThread, switchToThread } = useThreads()
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col space-y-1">
|
|
||||||
<div className="text-right">
|
|
||||||
<button className="btn btn-sm" onClick={onClose}>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
className="size-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M6 18 18 6M6 6l12 12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/* iterate through all past threads */}
|
|
||||||
{Object.keys(allThreads ?? {}).map((threadId) => {
|
|
||||||
const pastThread = (allThreads ?? {})[threadId];
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={pastThread.id}
|
|
||||||
className={`btn btn-sm btn-secondary ${pastThread.id === currentThread?.id ? "btn-primary" : ""}`}
|
|
||||||
onClick={() => switchToThread(pastThread.id)}
|
|
||||||
>
|
|
||||||
{new Date(pastThread.createdAt).toLocaleString()}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
const { allThreads, currentThread, addMessageToHistory, startNewThread, } = useThreads()
|
|
||||||
|
|
||||||
// state of current message
|
|
||||||
const [selection, setSelection] = useState<CodeSelection | null>(null) // the code the user is selecting
|
|
||||||
const [files, setFiles] = useState<vscode.Uri[]>([]) // the names of the files in the chat
|
|
||||||
const [instructions, setInstructions] = useState('') // the user's instructions
|
|
||||||
|
|
||||||
// state of chat
|
|
||||||
const [messageStream, setMessageStream] = useState('')
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [isThreadSelectorOpen, setIsThreadSelectorOpen] = useState(false)
|
const [isThreadSelectorOpen, setIsThreadSelectorOpen] = useState(false)
|
||||||
const [requestFailed, setRequestFailed] = useState(false)
|
const [requestFailed, setRequestFailed] = useState(false)
|
||||||
const [requestFailedReason, setRequestFailedReason] = useState('')
|
const [requestFailedReason, setRequestFailedReason] = useState('')
|
||||||
|
|
||||||
const abortFnRef = useRef<(() => void) | null>(null)
|
|
||||||
|
|
||||||
const [apiConfig, setApiConfig] = useState<ApiConfig | null>(null)
|
|
||||||
|
|
||||||
// get Api Config on mount
|
// get Api Config on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getVSCodeAPI().postMessage({ type: 'getApiConfig' })
|
getVSCodeAPI().postMessage({ type: 'getApiConfig' })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Receive messages from the extension
|
// Receive messages from the VSCode extension
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = (event: MessageEvent) => {
|
const listener = (event: MessageEvent) => {
|
||||||
|
const m = event.data as MessageToSidebar;
|
||||||
const m = event.data as WebviewMessage;
|
onMessageFromVSCode(m)
|
||||||
// resolve any awaiting promises
|
|
||||||
// eg. it will resolve the promise below for `await VSCodeResponse('files')`
|
|
||||||
resolveAwaitingVSCodeResponse(m)
|
|
||||||
|
|
||||||
// if user pressed ctrl+l, add their selection to the sidebar
|
|
||||||
if (m.type === 'ctrl+l') {
|
|
||||||
setSelection(m.selection)
|
|
||||||
const filepath = m.selection.filePath
|
|
||||||
|
|
||||||
// add current file to the context if it's not already in the files array
|
|
||||||
if (!files.find(f => f.fsPath === filepath.fsPath))
|
|
||||||
setFiles(files => [...files, filepath])
|
|
||||||
|
|
||||||
}
|
|
||||||
// when get apiConfig, set
|
|
||||||
else if (m.type === 'apiConfig') {
|
|
||||||
setApiConfig(m.apiConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if they pressed the + to add a new chat
|
|
||||||
else if (m.type === 'startNewThread') {
|
|
||||||
setIsThreadSelectorOpen(false)
|
|
||||||
if (currentThread?.messages.length !== 0)
|
|
||||||
startNewThread()
|
|
||||||
}
|
|
||||||
|
|
||||||
// if they opened thread selector
|
|
||||||
else if (m.type === 'toggleThreadSelector') {
|
|
||||||
setIsThreadSelectorOpen(v => !v)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
window.addEventListener('message', listener);
|
window.addEventListener('message', listener);
|
||||||
return () => { window.removeEventListener('message', listener) }
|
return () => { window.removeEventListener('message', listener) }
|
||||||
}, [files, selection, startNewThread, currentThread])
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
const formRef = useRef<HTMLFormElement | null>(null)
|
|
||||||
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
|
||||||
|
|
||||||
e.preventDefault()
|
|
||||||
if (isLoading) return
|
|
||||||
|
|
||||||
// Reset any error messages from previous submit
|
|
||||||
setRequestFailed(false)
|
|
||||||
setRequestFailedReason('')
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
setInstructions('');
|
|
||||||
formRef.current?.reset(); // reset the form's text
|
|
||||||
setSelection(null)
|
|
||||||
setFiles([])
|
|
||||||
|
|
||||||
// request file content from vscode and await response
|
|
||||||
getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
|
|
||||||
const relevantFiles = await awaitVSCodeResponse('files')
|
|
||||||
|
|
||||||
// add message to chat history
|
|
||||||
const content = userInstructionsStr(instructions, relevantFiles.files, selection)
|
|
||||||
// console.log('prompt:\n', content)
|
|
||||||
const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files }
|
|
||||||
addMessageToHistory(newHistoryElt)
|
|
||||||
|
|
||||||
// send message to LLM
|
|
||||||
let { abort } = sendLLMMessage({
|
|
||||||
messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })), { role: 'user', content }],
|
|
||||||
onText: (newText, fullText) => setMessageStream(fullText),
|
|
||||||
onFinalMessage: (content) => {
|
|
||||||
// add assistant's message to chat history, and clear selection
|
|
||||||
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
|
|
||||||
addMessageToHistory(newHistoryElt)
|
|
||||||
|
|
||||||
// clear selection
|
|
||||||
setMessageStream('')
|
|
||||||
setIsLoading(false)
|
|
||||||
},
|
|
||||||
onError: (message) => { onStop(); setRequestFailed(true); setRequestFailedReason(message)},
|
|
||||||
apiConfig: apiConfig
|
|
||||||
})
|
|
||||||
abortFnRef.current = abort
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const onStop = useCallback(() => {
|
|
||||||
// abort claude
|
|
||||||
abortFnRef.current?.()
|
|
||||||
|
|
||||||
// if messageStream was not empty, add it to the history
|
|
||||||
const llmContent = messageStream || '(canceled)'
|
|
||||||
const newHistoryElt: ChatMessage = { role: 'assistant', displayContent: messageStream, content: llmContent }
|
|
||||||
addMessageToHistory(newHistoryElt)
|
|
||||||
|
|
||||||
setMessageStream('')
|
|
||||||
setIsLoading(false)
|
|
||||||
|
|
||||||
}, [addMessageToHistory, messageStream])
|
|
||||||
|
|
||||||
//Clear code selection
|
|
||||||
const clearSelection = () => {
|
|
||||||
setSelection(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div className="flex flex-col h-screen w-full">
|
<div className="flex flex-col h-screen w-full">
|
||||||
{isThreadSelectorOpen && (
|
{isThreadSelectorOpen && (
|
||||||
<div className="mb-2 max-h-[30vh] overflow-y-auto">
|
<div className="mb-2 max-h-[30vh] overflow-y-auto">
|
||||||
<ThreadSelector onClose={() => setIsThreadSelectorOpen(false)} />
|
<SidebarThreadSelector onClose={() => setIsThreadSelectorOpen(false)} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="overflow-y-auto overflow-x-hidden space-y-4">
|
|
||||||
{/* previous messages */}
|
|
||||||
{currentThread !== null && currentThread.messages.map((message, i) =>
|
|
||||||
<ChatBubble key={i} chatMessage={message} />
|
|
||||||
)}
|
|
||||||
{/* message stream */}
|
|
||||||
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream }} />
|
|
||||||
</div>
|
|
||||||
{/* chatbar */}
|
|
||||||
<div className="shrink-0 py-4">
|
|
||||||
{/* selection */}
|
|
||||||
<div className="text-left">
|
|
||||||
|
|
||||||
<div className="relative">
|
<SidebarChat setIsThreadSelectorOpen={setIsThreadSelectorOpen} />
|
||||||
<div className="input">
|
|
||||||
{/* selection */}
|
|
||||||
{(files.length || selection?.selectionStr) && <div className="p-2 pb-0 space-y-2">
|
|
||||||
{/* selected files */}
|
|
||||||
<SelectedFiles files={files} setFiles={setFiles} />
|
|
||||||
{/* selected code */}
|
|
||||||
{!!selection?.selectionStr && (
|
|
||||||
<BlockCode className="rounded bg-vscode-sidebar-bg" text={selection.selectionStr} toolbar={(
|
|
||||||
<button
|
|
||||||
onClick={clearSelection}
|
|
||||||
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
)} />
|
|
||||||
)}
|
|
||||||
</div>}
|
|
||||||
{/* error message */}
|
|
||||||
{requestFailed && (
|
|
||||||
<div className="bg-gray-800 text-red-500 text-center p-4 mb-4 rounded-md shadow-md">
|
|
||||||
<div className="text-lg">{`${requestFailedReason}`}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<form
|
|
||||||
ref={formRef}
|
|
||||||
className="flex flex-row items-center rounded-md p-2"
|
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }}
|
|
||||||
|
|
||||||
onSubmit={(e) => {
|
|
||||||
console.log('submit!')
|
|
||||||
e.preventDefault();
|
|
||||||
onSubmit(e)
|
|
||||||
}}>
|
|
||||||
{/* input */}
|
|
||||||
|
|
||||||
<textarea
|
|
||||||
onChange={(e) => { setInstructions(e.target.value) }}
|
|
||||||
className="w-full p-2 leading-tight resize-none max-h-[50vh] overflow-hidden bg-transparent border-none !outline-none"
|
|
||||||
placeholder="Ctrl+L to select"
|
|
||||||
rows={1}
|
|
||||||
onInput={e => { e.currentTarget.style.height = 'auto'; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px' }} // Adjust height dynamically
|
|
||||||
/>
|
|
||||||
{/* submit button */}
|
|
||||||
{isLoading ?
|
|
||||||
<button
|
|
||||||
onClick={onStop}
|
|
||||||
className="btn btn-primary rounded-r-lg max-h-10 p-2"
|
|
||||||
type='button'
|
|
||||||
>Stop</button>
|
|
||||||
: <button
|
|
||||||
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
|
|
||||||
disabled={!instructions}
|
|
||||||
type='submit'
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<line x1="12" y1="19" x2="12" y2="5"></line>
|
|
||||||
<polyline points="5 12 12 5 19 12"></polyline>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
280
extensions/void/src/sidebar/SidebarChat.tsx
Normal file
280
extensions/void/src/sidebar/SidebarChat.tsx
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
import React, { FormEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
import { marked } from 'marked';
|
||||||
|
import MarkdownRender from "./markdown/MarkdownRender";
|
||||||
|
import BlockCode from "./markdown/BlockCode";
|
||||||
|
import { SelectedFiles } from "./components/SelectedFiles";
|
||||||
|
import { File, ChatMessage, CodeSelection } from "../shared_types";
|
||||||
|
import * as vscode from 'vscode'
|
||||||
|
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "./getVscodeApi";
|
||||||
|
import { useThreads } from "./threadsContext";
|
||||||
|
import { ApiConfig, sendLLMMessage } from "../common/sendLLMMessage";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const filesStr = (fullFiles: File[]) => {
|
||||||
|
return fullFiles.map(({ filepath, content }) =>
|
||||||
|
`
|
||||||
|
${filepath.fsPath}
|
||||||
|
\`\`\`
|
||||||
|
${content}
|
||||||
|
\`\`\``).join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userInstructionsStr = (instructions: string, files: File[], selection: CodeSelection | null) => {
|
||||||
|
let str = '';
|
||||||
|
|
||||||
|
if (files.length > 0) {
|
||||||
|
str += filesStr(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selection) {
|
||||||
|
str += `
|
||||||
|
I am currently selecting this code:
|
||||||
|
\t\`\`\`${selection.selectionStr}\`\`\`
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length > 0 && selection) {
|
||||||
|
str += `
|
||||||
|
Please edit the selected code or the entire file following these instructions:
|
||||||
|
`;
|
||||||
|
} else if (files.length > 0) {
|
||||||
|
str += `
|
||||||
|
Please edit the file following these instructions:
|
||||||
|
`;
|
||||||
|
} else if (selection) {
|
||||||
|
str += `
|
||||||
|
Please edit the selected code following these instructions:
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
str += `
|
||||||
|
\t${instructions}
|
||||||
|
`;
|
||||||
|
if (files.length > 0) {
|
||||||
|
str += `
|
||||||
|
\tIf you make a change, rewrite the entire file.
|
||||||
|
`; // TODO don't rewrite the whole file on prompt, instead rewrite it when click Apply
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
|
||||||
|
|
||||||
|
const role = chatMessage.role
|
||||||
|
const children = chatMessage.displayContent
|
||||||
|
|
||||||
|
if (!children)
|
||||||
|
return null
|
||||||
|
|
||||||
|
let chatbubbleContents: React.ReactNode
|
||||||
|
|
||||||
|
if (role === 'user') {
|
||||||
|
chatbubbleContents = <>
|
||||||
|
<SelectedFiles files={chatMessage.files} setFiles={null} />
|
||||||
|
{chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} hideToolbar />}
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
else if (role === 'assistant') {
|
||||||
|
|
||||||
|
chatbubbleContents = <MarkdownRender string={children} /> // sectionsHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return <div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
|
||||||
|
<div className={`inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full`}>
|
||||||
|
{chatbubbleContents}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const SidebarChat = ({ setIsThreadSelectorOpen }: { setIsThreadSelectorOpen: (v: boolean | ((v: boolean) => boolean)) => void }) => {
|
||||||
|
|
||||||
|
|
||||||
|
// state of current message
|
||||||
|
const [selection, setSelection] = useState<CodeSelection | null>(null) // the code the user is selecting
|
||||||
|
const [files, setFiles] = useState<vscode.Uri[]>([]) // the names of the files in the chat
|
||||||
|
const [instructions, setInstructions] = useState('') // the user's instructions
|
||||||
|
|
||||||
|
// state of chat
|
||||||
|
const [messageStream, setMessageStream] = useState('')
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const abortFnRef = useRef<(() => void) | null>(null)
|
||||||
|
|
||||||
|
// higher level state
|
||||||
|
const { allThreads, currentThread, addMessageToHistory, startNewThread, } = useThreads()
|
||||||
|
const [apiConfig, setApiConfig] = useState<ApiConfig | null>(null)
|
||||||
|
|
||||||
|
|
||||||
|
// if user pressed ctrl+l, add their selection to the sidebar
|
||||||
|
useOnVSCodeMessage('ctrl+l', (m) => {
|
||||||
|
setSelection(m.selection)
|
||||||
|
const filepath = m.selection.filePath
|
||||||
|
|
||||||
|
// add current file to the context if it's not already in the files array
|
||||||
|
if (!files.find(f => f.fsPath === filepath.fsPath))
|
||||||
|
setFiles(files => [...files, filepath])
|
||||||
|
})
|
||||||
|
|
||||||
|
// when get apiConfig, set
|
||||||
|
useOnVSCodeMessage('apiConfig', (m) => {
|
||||||
|
setApiConfig(m.apiConfig)
|
||||||
|
})
|
||||||
|
|
||||||
|
// if they pressed the + to add a new chat
|
||||||
|
useOnVSCodeMessage('startNewThread', (m) => {
|
||||||
|
setIsThreadSelectorOpen(false)
|
||||||
|
if (currentThread?.messages.length !== 0)
|
||||||
|
startNewThread()
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// if they opened thread selector
|
||||||
|
useOnVSCodeMessage('toggleThreadSelector', (m) => {
|
||||||
|
setIsThreadSelectorOpen(v => !v)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const formRef = useRef<HTMLFormElement | null>(null)
|
||||||
|
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
if (isLoading) return
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setInstructions('');
|
||||||
|
formRef.current?.reset(); // reset the form's text
|
||||||
|
setSelection(null)
|
||||||
|
setFiles([])
|
||||||
|
|
||||||
|
// request file content from vscode and await response
|
||||||
|
getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
|
||||||
|
const relevantFiles = await awaitVSCodeResponse('files')
|
||||||
|
|
||||||
|
// add message to chat history
|
||||||
|
const content = userInstructionsStr(instructions, relevantFiles.files, selection)
|
||||||
|
// console.log('prompt:\n', content)
|
||||||
|
const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files }
|
||||||
|
addMessageToHistory(newHistoryElt)
|
||||||
|
|
||||||
|
// send message to LLM
|
||||||
|
let { abort } = sendLLMMessage({
|
||||||
|
messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })), { role: 'user', content }],
|
||||||
|
onText: (newText, fullText) => setMessageStream(fullText),
|
||||||
|
onFinalMessage: (content) => {
|
||||||
|
// add assistant's message to chat history, and clear selection
|
||||||
|
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
|
||||||
|
addMessageToHistory(newHistoryElt)
|
||||||
|
|
||||||
|
// clear selection
|
||||||
|
setMessageStream('')
|
||||||
|
setIsLoading(false)
|
||||||
|
},
|
||||||
|
apiConfig: apiConfig
|
||||||
|
})
|
||||||
|
abortFnRef.current = abort
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const onStop = useCallback(() => {
|
||||||
|
// abort claude
|
||||||
|
abortFnRef.current?.()
|
||||||
|
|
||||||
|
// if messageStream was not empty, add it to the history
|
||||||
|
const llmContent = messageStream || '(canceled)'
|
||||||
|
const newHistoryElt: ChatMessage = { role: 'assistant', displayContent: messageStream, content: llmContent }
|
||||||
|
addMessageToHistory(newHistoryElt)
|
||||||
|
|
||||||
|
setMessageStream('')
|
||||||
|
setIsLoading(false)
|
||||||
|
|
||||||
|
}, [addMessageToHistory, messageStream])
|
||||||
|
|
||||||
|
//Clear code selection
|
||||||
|
const clearSelection = () => {
|
||||||
|
setSelection(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div className="overflow-y-auto overflow-x-hidden space-y-4">
|
||||||
|
{/* previous messages */}
|
||||||
|
{currentThread !== null && currentThread.messages.map((message, i) =>
|
||||||
|
<ChatBubble key={i} chatMessage={message} />
|
||||||
|
)}
|
||||||
|
{/* message stream */}
|
||||||
|
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream }} />
|
||||||
|
</div>
|
||||||
|
{/* chatbar */}
|
||||||
|
<div className="shrink-0 py-4">
|
||||||
|
{/* selection */}
|
||||||
|
<div className="text-left">
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<div className="input">
|
||||||
|
{/* selection */}
|
||||||
|
{(files.length || selection?.selectionStr) && <div className="p-2 pb-0 space-y-2">
|
||||||
|
{/* selected files */}
|
||||||
|
<SelectedFiles files={files} setFiles={setFiles} />
|
||||||
|
{/* selected code */}
|
||||||
|
{!!selection?.selectionStr && (
|
||||||
|
<BlockCode className="rounded bg-vscode-sidebar-bg" text={selection.selectionStr} toolbar={(
|
||||||
|
<button
|
||||||
|
onClick={clearSelection}
|
||||||
|
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
)} />
|
||||||
|
)}
|
||||||
|
</div>}
|
||||||
|
<form
|
||||||
|
ref={formRef}
|
||||||
|
className="flex flex-row items-center rounded-md p-2"
|
||||||
|
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }}
|
||||||
|
|
||||||
|
onSubmit={(e) => {
|
||||||
|
console.log('submit!')
|
||||||
|
e.preventDefault();
|
||||||
|
onSubmit(e)
|
||||||
|
}}>
|
||||||
|
{/* input */}
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
onChange={(e) => { setInstructions(e.target.value) }}
|
||||||
|
className="w-full p-2 leading-tight resize-none max-h-[50vh] overflow-hidden bg-transparent border-none !outline-none"
|
||||||
|
placeholder="Ctrl+L to select"
|
||||||
|
rows={1}
|
||||||
|
onInput={e => { e.currentTarget.style.height = 'auto'; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px' }} // Adjust height dynamically
|
||||||
|
/>
|
||||||
|
{/* submit button */}
|
||||||
|
{isLoading ?
|
||||||
|
<button
|
||||||
|
onClick={onStop}
|
||||||
|
className="btn btn-primary rounded-r-lg max-h-10 p-2"
|
||||||
|
type='button'
|
||||||
|
>Stop</button>
|
||||||
|
: <button
|
||||||
|
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
|
||||||
|
disabled={!instructions}
|
||||||
|
type='submit'
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<line x1="12" y1="19" x2="12" y2="5"></line>
|
||||||
|
<polyline points="5 12 12 5 19 12"></polyline>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
40
extensions/void/src/sidebar/SidebarThreadSelector.tsx
Normal file
40
extensions/void/src/sidebar/SidebarThreadSelector.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import React from "react";
|
||||||
|
import { ThreadsProvider, useThreads } from "./threadsContext";
|
||||||
|
|
||||||
|
export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => {
|
||||||
|
const { allThreads, currentThread, switchToThread } = useThreads()
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<div className="text-right">
|
||||||
|
<button className="btn btn-sm" onClick={onClose}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
className="size-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M6 18 18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* iterate through all past threads */}
|
||||||
|
{Object.keys(allThreads ?? {}).map((threadId) => {
|
||||||
|
const pastThread = (allThreads ?? {})[threadId];
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={pastThread.id}
|
||||||
|
className={`btn btn-sm btn-secondary ${pastThread.id === currentThread?.id ? "btn-primary" : ""}`}
|
||||||
|
onClick={() => switchToThread(pastThread.id)}
|
||||||
|
>
|
||||||
|
{new Date(pastThread.createdAt).toLocaleString()}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,48 +1,77 @@
|
||||||
import { Command, WebviewMessage } from "../shared_types";
|
import { useEffect } from "react";
|
||||||
|
import { MessageFromSidebar, MessageToSidebar, } from "../shared_types";
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
|
||||||
|
type Command = MessageToSidebar['type']
|
||||||
|
|
||||||
// message -> res[]
|
// messageType -> res[]
|
||||||
const awaiting: { [c in Command]: ((res: any) => void)[] } = {
|
const onetimeCallbacks: { [C in Command]: ((res: any) => void)[] } = {
|
||||||
"ctrl+l": [],
|
"ctrl+l": [],
|
||||||
"applyChanges": [],
|
|
||||||
"requestFiles": [],
|
|
||||||
"files": [],
|
"files": [],
|
||||||
"apiConfig": [],
|
"apiConfig": [],
|
||||||
"getApiConfig": [],
|
|
||||||
"startNewThread": [],
|
"startNewThread": [],
|
||||||
"getAllThreads": [],
|
|
||||||
"allThreads": [],
|
"allThreads": [],
|
||||||
"persistThread": [],
|
|
||||||
"toggleThreadSelector": []
|
"toggleThreadSelector": []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// messageType -> id -> res
|
||||||
|
const callbacks: { [C in Command]: { [id: string]: ((res: any) => void) } } = {
|
||||||
|
"ctrl+l": {},
|
||||||
|
"files": {},
|
||||||
|
"apiConfig": {},
|
||||||
|
"startNewThread": {},
|
||||||
|
"allThreads": {},
|
||||||
|
"toggleThreadSelector": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// use this function to await responses
|
// use this function to await responses
|
||||||
export const awaitVSCodeResponse = <C extends Command>(c: C) => {
|
export const awaitVSCodeResponse = <C extends Command>(c: C) => {
|
||||||
let result: Promise<WebviewMessage & { type: C }> = new Promise((res, rej) => {
|
let result: Promise<MessageToSidebar & { type: C }> = new Promise((res, rej) => {
|
||||||
awaiting[c].push(res)
|
onetimeCallbacks[c].push(res)
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export const resolveAwaitingVSCodeResponse = (m: WebviewMessage) => {
|
|
||||||
// resolve all promises for this message
|
// use this function to add a listener to a certain type of message
|
||||||
for (let res of awaiting[m.type]) {
|
export const useOnVSCodeMessage = <C extends Command>(messageType: C, fn: (e: MessageToSidebar & { type: C }) => void) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const mType = messageType
|
||||||
|
const callbackId: string = uuidv4();
|
||||||
|
// @ts-ignore
|
||||||
|
callbacks[mType][callbackId] = fn;
|
||||||
|
return () => { delete callbacks[mType][callbackId] }
|
||||||
|
}, [messageType, fn])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// this function gets called whenever sidebar receives a message - it should only mount once
|
||||||
|
export const onMessageFromVSCode = (m: MessageToSidebar) => {
|
||||||
|
// resolve all promises for this message type
|
||||||
|
for (let res of onetimeCallbacks[m.type]) {
|
||||||
|
res(m)
|
||||||
|
onetimeCallbacks[m.type].splice(0) // clear the array
|
||||||
|
}
|
||||||
|
// call the listener for this message type
|
||||||
|
for (let res of Object.values(callbacks[m.type])) {
|
||||||
res(m)
|
res(m)
|
||||||
awaiting[m.type].splice(0) // clear the array
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// VS Code exposes the function acquireVsCodeApi() to us, it should only get called once
|
|
||||||
let vsCodeApi: ReturnType<AcquireVsCodeApiType> | undefined;
|
|
||||||
|
|
||||||
type AcquireVsCodeApiType = () => {
|
type AcquireVsCodeApiType = () => {
|
||||||
postMessage(message: WebviewMessage): void;
|
postMessage(message: MessageFromSidebar): void;
|
||||||
// setState(state: any): void; // getState and setState are made obsolete by us using { retainContextWhenHidden: true }
|
// setState(state: any): void; // getState and setState are made obsolete by us using { retainContextWhenHidden: true }
|
||||||
// getState(): any;
|
// getState(): any;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// VS Code exposes the function acquireVsCodeApi() to us, this variable makes sure it only gets called once
|
||||||
|
let vsCodeApi: ReturnType<AcquireVsCodeApiType> | undefined;
|
||||||
|
|
||||||
export function getVSCodeAPI(): ReturnType<AcquireVsCodeApiType> {
|
export function getVSCodeAPI(): ReturnType<AcquireVsCodeApiType> {
|
||||||
if (vsCodeApi)
|
if (vsCodeApi)
|
||||||
return vsCodeApi;
|
return vsCodeApi;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import React, { ReactNode, useCallback, useEffect, useState } from "react"
|
import React, { ReactNode, useCallback, useEffect, useState } from "react"
|
||||||
import { getVSCodeAPI } from "../getVscodeApi"
|
import { getVSCodeAPI } from "../getVscodeApi"
|
||||||
|
|
||||||
|
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||||
|
import { atomOneDarkReasonable } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||||
|
|
||||||
enum CopyButtonState {
|
enum CopyButtonState {
|
||||||
Copy = "Copy",
|
Copy = "Copy",
|
||||||
Copied = "Copied!",
|
Copied = "Copied!",
|
||||||
|
|
@ -12,17 +15,27 @@ const COPY_FEEDBACK_TIMEOUT = 1000
|
||||||
// code block with toolbar (Apply, Copy, etc) at top
|
// code block with toolbar (Apply, Copy, etc) at top
|
||||||
const BlockCode = ({
|
const BlockCode = ({
|
||||||
text,
|
text,
|
||||||
|
language,
|
||||||
toolbar,
|
toolbar,
|
||||||
hideToolbar = false,
|
hideToolbar = false,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
text: string
|
text: string
|
||||||
|
language?: string
|
||||||
toolbar?: ReactNode
|
toolbar?: ReactNode
|
||||||
hideToolbar?: boolean
|
hideToolbar?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
}) => {
|
}) => {
|
||||||
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
|
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
|
||||||
|
|
||||||
|
const customStyle = {
|
||||||
|
...atomOneDarkReasonable,
|
||||||
|
'code[class*="language-"]': {
|
||||||
|
...atomOneDarkReasonable['code[class*="language-"]'],
|
||||||
|
background: "none",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (copyButtonState !== CopyButtonState.Copy) {
|
if (copyButtonState !== CopyButtonState.Copy) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -71,7 +84,14 @@ const BlockCode = ({
|
||||||
<div
|
<div
|
||||||
className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg ${!hideToolbar ? "rounded-tl-none" : ""} ${className}`}
|
className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg ${!hideToolbar ? "rounded-tl-none" : ""} ${className}`}
|
||||||
>
|
>
|
||||||
<pre className="p-2">{text}</pre>
|
<SyntaxHighlighter
|
||||||
|
language={language}
|
||||||
|
style={customStyle}
|
||||||
|
className={"rounded-sm"}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
||||||
}
|
}
|
||||||
|
|
||||||
if (t.type === "code") {
|
if (t.type === "code") {
|
||||||
return <BlockCode text={t.text} />
|
return <BlockCode text={t.text} language={t.lang} />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (t.type === "heading") {
|
if (t.type === "heading") {
|
||||||
|
|
@ -165,4 +165,4 @@ const MarkdownRender = ({ string, nested = false }: { string: string, nested?: b
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MarkdownRender
|
export default MarkdownRender
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue