mirror of
https://github.com/voideditor/void
synced 2026-05-23 09:28:23 +00:00
Merge branch 'main' into feat-mistral-new
This commit is contained in:
commit
fd989b2c13
60 changed files with 1813 additions and 1472 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -3,26 +3,29 @@
|
|||
|
||||
|
||||
|
||||
## Jan. 12, 2025 - Entering beta
|
||||
## Jan. 13, 2025 - Entering beta
|
||||
|
||||
- Added quick edits! Void handles FIM-prompting, output parsing, and history management for inline UI.
|
||||
|
||||
- Migrated away from VS Code extension API - Void now lives and interacts entirely within the VS Code codebase.
|
||||
|
||||
- Added quick edits! Void handles FIM-prompting and output parsing, inline UI, and history management.
|
||||
|
||||
- New settings page with model configuration, one-click switch, and user settings.
|
||||
|
||||
- Added auto-detection (via polling) of local models by default.
|
||||
|
||||
- LLM requests originate from `node/`, which fixes common CORS and CSP issues when running some models locally.
|
||||
|
||||
- Misc improvements like UI and history for Accept | Reject in the sidebar and editor, stream interruptions, and past chats history.
|
||||
- Misc improvements like UI and history for Accept | Reject in the sidebar and editor, streaming interruptions, and past chat history.
|
||||
|
||||
- Automatic file selection on tab switches.
|
||||
|
||||
- Lots of new UI, misc bug fixes, and performance improvements.
|
||||
|
||||
- VS Code's default Ctrl+L is now Ctrl+M in Void (on Mac Cmd+L becomes Cmd+M).
|
||||
|
||||
- Switched from the MIT License to the Apache 2.0 License. Apache's attribution clause provides a small amount of protection to our source initiative.
|
||||
|
||||
Many thanks to our contributors __, __, __
|
||||
A huge shoutout to our many contributors. If you'd like to help build Void,
|
||||
|
||||
## Sept/Oct. 2024 - Early launch
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ First, run `npm install -g node-gyp`. Then:
|
|||
To build Void, open `void/` inside VSCode. Then open your terminal and run:
|
||||
|
||||
1. `npm install` to install all dependencies.
|
||||
2. `npm run watchreact` to build Void's browser dependencies like React.
|
||||
2. `npm run watchreact` to build Void's browser dependencies like React. (If this doesn't work, try `npm run buildreact`).
|
||||
3. Build Void.
|
||||
- Press <kbd>Cmd+Shift+B</kbd> (Mac).
|
||||
- Press <kbd>Ctrl+Shift+B</kbd> (Windows/Linux).
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
// Void explanation - product-build-darwin-universal.yml runs this (create-universal-app.ts), then sign.ts
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const minimatch = require("minimatch");
|
||||
|
|
|
|||
|
|
@ -78,24 +78,24 @@ async function main(buildDir) {
|
|||
// universal will get its copy from the x64 build.
|
||||
if (arch !== 'universal') {
|
||||
await (0, cross_spawn_promise_1.spawn)('plutil', [
|
||||
'-insert',
|
||||
'-replace', // Void changed this to replace
|
||||
'NSAppleEventsUsageDescription',
|
||||
'-string',
|
||||
'An application in Visual Studio Code wants to use AppleScript.',
|
||||
'An application in Void wants to use AppleScript.',
|
||||
`${infoPlistPath}`
|
||||
]);
|
||||
await (0, cross_spawn_promise_1.spawn)('plutil', [
|
||||
'-replace',
|
||||
'NSMicrophoneUsageDescription',
|
||||
'-string',
|
||||
'An application in Visual Studio Code wants to use the Microphone.',
|
||||
'An application in Void wants to use the Microphone.',
|
||||
`${infoPlistPath}`
|
||||
]);
|
||||
await (0, cross_spawn_promise_1.spawn)('plutil', [
|
||||
'-replace',
|
||||
'NSCameraUsageDescription',
|
||||
'-string',
|
||||
'An application in Visual Studio Code wants to use the Camera.',
|
||||
'An application in Void wants to use the Camera.',
|
||||
`${infoPlistPath}`
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ async function main(buildDir?: string): Promise<void> {
|
|||
// universal will get its copy from the x64 build.
|
||||
if (arch !== 'universal') {
|
||||
await spawn('plutil', [
|
||||
'-insert',
|
||||
'-replace', // Void changed this to replace
|
||||
'NSAppleEventsUsageDescription',
|
||||
'-string',
|
||||
'An application in Void wants to use AppleScript.',
|
||||
|
|
|
|||
|
|
@ -1,333 +0,0 @@
|
|||
import * as vscode from 'vscode';
|
||||
import Parser from 'tree-sitter';
|
||||
import JavaScript from 'tree-sitter-javascript';
|
||||
|
||||
interface Definition {
|
||||
file: string;
|
||||
node: Parser.SyntaxNode;
|
||||
}
|
||||
|
||||
interface DefnUse {
|
||||
parent: Parser.SyntaxNode;
|
||||
file: string;
|
||||
}
|
||||
|
||||
interface ImportInfo {
|
||||
source: string;
|
||||
imported: string;
|
||||
}
|
||||
|
||||
class ProjectAnalyzer {
|
||||
private parser: Parser;
|
||||
private graph: Map<string, Set<string>>;
|
||||
private visited: Set<string>;
|
||||
private parsedFiles: Map<string, Parser.Tree>;
|
||||
private imports: Map<string, Map<string, ImportInfo>>;
|
||||
private definitions: Map<string, Definition>;
|
||||
private fileStack: Set<string>;
|
||||
|
||||
constructor() {
|
||||
this.parser = new Parser();
|
||||
this.parser.setLanguage(JavaScript);
|
||||
this.graph = new Map();
|
||||
this.visited = new Set();
|
||||
this.parsedFiles = new Map();
|
||||
this.imports = new Map();
|
||||
this.definitions = new Map();
|
||||
this.fileStack = new Set();
|
||||
}
|
||||
|
||||
async parseFile(filePath: string): Promise<Parser.Tree | null> {
|
||||
if (this.parsedFiles.has(filePath)) {
|
||||
return this.parsedFiles.get(filePath)!;
|
||||
}
|
||||
|
||||
if (this.fileStack.has(filePath)) {
|
||||
return null; // Circular import
|
||||
}
|
||||
|
||||
this.fileStack.add(filePath);
|
||||
|
||||
try {
|
||||
const uri = vscode.Uri.file(filePath);
|
||||
const document = await vscode.workspace.openTextDocument(uri);
|
||||
const code = document.getText();
|
||||
const tree = this.parser.parse(code);
|
||||
|
||||
this.parsedFiles.set(filePath, tree);
|
||||
this.collectImports(filePath, tree);
|
||||
this.collectDefinitions(filePath, tree);
|
||||
|
||||
return tree;
|
||||
} catch (error) {
|
||||
console.error(`Error parsing ${filePath}:`, error);
|
||||
return null;
|
||||
} finally {
|
||||
this.fileStack.delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private collectImports(filePath: string, tree: Parser.Tree): void {
|
||||
const fileImports = new Map<string, ImportInfo>();
|
||||
|
||||
const visit = (node: Parser.SyntaxNode): void => {
|
||||
if (node.type === 'import_declaration') {
|
||||
const source = node.childForFieldName('source')?.text.slice(1, -1) ?? '';
|
||||
const specifiers = node.childForFieldName('specifiers');
|
||||
|
||||
specifiers?.children.forEach(spec => {
|
||||
if (spec.type === 'import_specifier') {
|
||||
const local = spec.childForFieldName('local')?.text ?? '';
|
||||
const imported = spec.childForFieldName('imported')?.text ?? '';
|
||||
fileImports.set(local, { source, imported });
|
||||
}
|
||||
});
|
||||
}
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
|
||||
visit(tree.rootNode);
|
||||
this.imports.set(filePath, fileImports);
|
||||
}
|
||||
|
||||
private collectDefinitions(filePath: string, tree: Parser.Tree): void {
|
||||
const visit = (node: Parser.SyntaxNode): void => {
|
||||
if (node.type === 'function_declaration') {
|
||||
const name = node.childForFieldName('name')?.text ?? '';
|
||||
this.definitions.set(name, { file: filePath, node });
|
||||
}
|
||||
else if (node.type === 'variable_declarator') {
|
||||
const name = node.childForFieldName('name')?.text;
|
||||
const value = node.childForFieldName('value');
|
||||
if (name && (value?.type === 'arrow_function' || value?.type === 'function')) {
|
||||
this.definitions.set(name, { file: filePath, node: value });
|
||||
}
|
||||
}
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
|
||||
visit(tree.rootNode);
|
||||
}
|
||||
|
||||
private async getTypeFromPosition(uri: vscode.Uri, position: vscode.Position): Promise<string | null> {
|
||||
const hover = await vscode.commands.executeCommand<vscode.Hover[]>(
|
||||
'vscode.executeHoverProvider',
|
||||
uri,
|
||||
position
|
||||
);
|
||||
|
||||
if (hover?.[0]?.contents.length) {
|
||||
for (const content of hover[0].contents) {
|
||||
let hoverText = typeof content === 'string' ?
|
||||
content :
|
||||
('value' in content ? content.value : '');
|
||||
|
||||
// Remove typescript backticks if present
|
||||
hoverText = hoverText.replace(/```typescript\s*/, '').replace(/```\s*$/, '');
|
||||
console.log('Processing hover text:', hoverText);
|
||||
|
||||
// Extract the type information - look for the type after the colon
|
||||
const typeMatches = [
|
||||
/:\s*([\w<>]+)(?:\[\])?/, // matches "foo: Type" or "foo: Type[]"
|
||||
/var\s+\w+:\s*([\w<>]+)/, // matches "var foo: Type"
|
||||
/\(type\)\s+[\w<>]+:\s*([\w<>]+)/, // matches "(type) foo: Type"
|
||||
/\(method\)\s*([\w<>]+)\./ // matches "(method) Type.method"
|
||||
];
|
||||
|
||||
for (const pattern of typeMatches) {
|
||||
const match = pattern.exec(hoverText);
|
||||
if (match) {
|
||||
let type = match[1];
|
||||
// Handle array types
|
||||
if (hoverText.includes('[]')) {
|
||||
return 'Array';
|
||||
}
|
||||
// Extract base type from generics
|
||||
if (type.includes('<')) {
|
||||
type = type.split('<')[0];
|
||||
}
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getCallsInDefn(defnNode: Parser.SyntaxNode, currentFile: string): Promise<Set<string>> {
|
||||
const calls = new Set<string>();
|
||||
const fileImports = this.imports.get(currentFile) ?? new Map();
|
||||
const uri = vscode.Uri.file(currentFile);
|
||||
|
||||
const visit = async (node: Parser.SyntaxNode): Promise<void> => {
|
||||
if (node.type === 'call_expression') {
|
||||
const callee = node.childForFieldName('function');
|
||||
if (callee?.type === 'identifier') {
|
||||
const name = callee.text;
|
||||
const importInfo = fileImports.get(name);
|
||||
if (importInfo) {
|
||||
calls.add(`${importInfo.source}:${importInfo.imported}`);
|
||||
} else {
|
||||
calls.add(name);
|
||||
}
|
||||
}
|
||||
else if (callee?.type === 'member_expression') {
|
||||
const method = callee.childForFieldName('property')?.text;
|
||||
const object = callee.childForFieldName('object');
|
||||
|
||||
if (method && object) {
|
||||
const position = new vscode.Position(
|
||||
object.startPosition.row,
|
||||
object.startPosition.column
|
||||
);
|
||||
|
||||
const type = await this.getTypeFromPosition(uri, position);
|
||||
if (type) {
|
||||
calls.add(`${type}.${method}`);
|
||||
} else {
|
||||
calls.add(`method:${method}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of node.children) {
|
||||
await visit(child);
|
||||
}
|
||||
};
|
||||
|
||||
await visit(defnNode);
|
||||
return calls;
|
||||
}
|
||||
|
||||
private gotoDefn(name: string): Definition | null {
|
||||
if (name.includes(':')) {
|
||||
const [file, funcName] = name.split(':');
|
||||
const def = this.definitions.get(funcName);
|
||||
return def ?? null;
|
||||
}
|
||||
|
||||
return this.definitions.get(name) ?? null;
|
||||
}
|
||||
|
||||
private getUses(defnNode: Parser.SyntaxNode, currentFile: string): DefnUse[] {
|
||||
const uses: DefnUse[] = [];
|
||||
|
||||
let fnName: string | undefined;
|
||||
if (defnNode.type === 'function_declaration') {
|
||||
fnName = defnNode.childForFieldName('name')?.text;
|
||||
} else if (defnNode.type === 'arrow_function' || defnNode.type === 'function') {
|
||||
const parent = defnNode.parent;
|
||||
if (parent?.type === 'variable_declarator') {
|
||||
fnName = parent.childForFieldName('name')?.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fnName) return uses;
|
||||
|
||||
for (const [file, tree] of this.parsedFiles) {
|
||||
const visit = (node: Parser.SyntaxNode): void => {
|
||||
if (node.type === 'call_expression') {
|
||||
const callee = node.childForFieldName('function');
|
||||
if (callee?.type === 'identifier' && callee.text === fnName) {
|
||||
let current: Parser.SyntaxNode | null = node;
|
||||
while (current) {
|
||||
if (current.type === 'function_declaration' ||
|
||||
current.type === 'arrow_function' ||
|
||||
current.type === 'function') {
|
||||
uses.push({ parent: current, file });
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
|
||||
visit(tree.rootNode);
|
||||
}
|
||||
|
||||
return uses;
|
||||
}
|
||||
|
||||
private async visitAllNodesInGraphFromDefinition(defn: Parser.SyntaxNode, currentFile: string): Promise<void> {
|
||||
let defnName: string | undefined;
|
||||
if (defn.type === 'function_declaration') {
|
||||
defnName = defn.childForFieldName('name')?.text;
|
||||
} else if (defn.type === 'arrow_function' || defn.type === 'function') {
|
||||
const parent = defn.parent;
|
||||
if (parent?.type === 'variable_declarator') {
|
||||
defnName = parent.childForFieldName('name')?.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!defnName) return;
|
||||
|
||||
const fullName = `${currentFile}:${defnName}`;
|
||||
if (this.visited.has(fullName)) return;
|
||||
|
||||
const calls = await this.getCallsInDefn(defn, currentFile);
|
||||
this.graph.set(fullName, calls);
|
||||
this.visited.add(fullName);
|
||||
|
||||
const callDefns = Array.from(calls).map(call => this.gotoDefn(call));
|
||||
for (const callDefn of callDefns) {
|
||||
if (callDefn) {
|
||||
await this.visitAllNodesInGraphFromDefinition(callDefn.node, callDefn.file);
|
||||
}
|
||||
}
|
||||
|
||||
const defnUses = this.getUses(defn, currentFile);
|
||||
for (const defnUse of defnUses) {
|
||||
await this.visitAllNodesInGraphFromDefinition(defnUse.parent, defnUse.file);
|
||||
}
|
||||
}
|
||||
|
||||
async analyze(entryFile: string): Promise<Map<string, Set<string>>> {
|
||||
const tree = await this.parseFile(entryFile);
|
||||
if (!tree) return new Map();
|
||||
|
||||
const visit = async (node: Parser.SyntaxNode): Promise<void> => {
|
||||
if (node.type === 'function_declaration') {
|
||||
await this.visitAllNodesInGraphFromDefinition(node, entryFile);
|
||||
}
|
||||
else if (node.type === 'variable_declarator') {
|
||||
const value = node.childForFieldName('value');
|
||||
if (value?.type === 'arrow_function' || value?.type === 'function') {
|
||||
await this.visitAllNodesInGraphFromDefinition(value, entryFile);
|
||||
}
|
||||
}
|
||||
for (const child of node.children) {
|
||||
await visit(child);
|
||||
}
|
||||
};
|
||||
|
||||
await visit(tree.rootNode);
|
||||
return this.graph;
|
||||
}
|
||||
}
|
||||
|
||||
export async function runTreeSitter(filePath?: string): Promise<Map<string, Set<string>> | null> {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor && !filePath) {
|
||||
vscode.window.showWarningMessage('No active editor found');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const targetPath = filePath ?? editor!.document.uri.fsPath;
|
||||
const analyzer = new ProjectAnalyzer();
|
||||
const graph = await analyzer.analyze(targetPath);
|
||||
|
||||
for (const [defn, calls] of graph) {
|
||||
console.log(`${defn} calls: ${[...calls].join(', ')}`);
|
||||
}
|
||||
|
||||
return graph;
|
||||
} catch (error) {
|
||||
console.error('Error analyzing file:', error);
|
||||
vscode.window.showErrorMessage('Error analyzing file');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import * as vscode from 'vscode';
|
||||
|
||||
const legend = new vscode.SemanticTokensLegend([], []);
|
||||
|
||||
export async function findFunctions() {
|
||||
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
const document = editor.document;
|
||||
|
||||
const tokens = await vscode.commands.executeCommand<vscode.SemanticTokens>(
|
||||
'vscode.provideDocumentSemanticTokens',
|
||||
document.uri
|
||||
);
|
||||
|
||||
if (!tokens) {
|
||||
console.error('No tokens found');
|
||||
return [];
|
||||
}
|
||||
|
||||
const allTokens = decodeTokens(tokens, document);
|
||||
|
||||
|
||||
return allTokens;
|
||||
}
|
||||
|
||||
function decodeTokens(tokens: vscode.SemanticTokens, document: vscode.TextDocument) {
|
||||
const data = tokens.data;
|
||||
const decodedTokens = [];
|
||||
let line = 0;
|
||||
let character = 0;
|
||||
|
||||
for (let i = 0; i < data.length; i += 5) {
|
||||
const deltaLine = data[i];
|
||||
const deltaStartChar = data[i + 1];
|
||||
const length = data[i + 2];
|
||||
const tokenTypeIdx = data[i + 3];
|
||||
const tokenModifierIdx = data[i + 4];
|
||||
|
||||
line += deltaLine;
|
||||
character = deltaLine === 0 ? character + deltaStartChar : deltaStartChar;
|
||||
|
||||
const type = legend.tokenTypes[tokenTypeIdx] || `(${tokenTypeIdx})`;
|
||||
const modifier = legend.tokenModifiers[tokenModifierIdx] || `(${tokenModifierIdx})`;
|
||||
|
||||
const tokenRange = new vscode.Range(line, character, line, character + length);
|
||||
const tokenText = document.getText(tokenRange);
|
||||
|
||||
decodedTokens.push({
|
||||
line,
|
||||
startCharacter: character,
|
||||
length,
|
||||
type,
|
||||
modifier,
|
||||
text: tokenText,
|
||||
});
|
||||
|
||||
console.log(`Token: '${tokenText}' | Type: ${type} | Modifier: ${modifier} | Line: ${line}, Character: ${character}`);
|
||||
}
|
||||
|
||||
return decodedTokens;
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
<Application xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<VisualElements
|
||||
BackgroundColor="#2D2D30"
|
||||
BackgroundColor="#FFFFFF"
|
||||
ShowNameOnSquare150x150Logo="on"
|
||||
Square150x150Logo="resources\app\resources\win32\code_150x150.png"
|
||||
Square70x70Logo="resources\app\resources\win32\code_70x70.png"
|
||||
ForegroundText="light"
|
||||
ShortDisplayName="Code - OSS" />
|
||||
ForegroundText="light"
|
||||
ShortDisplayName="Void" />
|
||||
</Application>
|
||||
|
|
|
|||
BIN
resources/win32/logo_cube_noshadow.png
Normal file
BIN
resources/win32/logo_cube_noshadow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 795 KiB |
|
|
@ -124,7 +124,8 @@ import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationS
|
|||
import { LLMMessageChannel } from '../../platform/void/electron-main/llmMessageChannel.js';
|
||||
import { IMetricsService } from '../../platform/void/common/metricsService.js';
|
||||
import { MetricsMainService } from '../../platform/void/electron-main/metricsMainService.js';
|
||||
|
||||
import { VoidMainUpdateService } from '../../platform/void/electron-main/voidUpdateMainService.js';
|
||||
import { IVoidUpdateService } from '../../platform/void/common/voidUpdateService.js';
|
||||
/**
|
||||
* The main VS Code application. There will only ever be one instance,
|
||||
* even if the user starts many instances (e.g. from the command line).
|
||||
|
|
@ -1107,6 +1108,7 @@ export class CodeApplication extends Disposable {
|
|||
|
||||
// Void main process services (required for services with a channel for comm between browser and electron-main (node))
|
||||
services.set(IMetricsService, new SyncDescriptor(MetricsMainService, undefined, false));
|
||||
services.set(IVoidUpdateService, new SyncDescriptor(VoidMainUpdateService, undefined, false));
|
||||
|
||||
// Default Extensions Profile Init
|
||||
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
|
||||
|
|
@ -1245,6 +1247,10 @@ export class CodeApplication extends Disposable {
|
|||
// Void - use loggerChannel as reference
|
||||
const metricsChannel = ProxyChannel.fromService(accessor.get(IMetricsService), disposables);
|
||||
mainProcessElectronServer.registerChannel('void-channel-metrics', metricsChannel);
|
||||
|
||||
const voidUpdatesChannel = ProxyChannel.fromService(accessor.get(IVoidUpdateService), disposables);
|
||||
mainProcessElectronServer.registerChannel('void-channel-update', voidUpdatesChannel);
|
||||
|
||||
const llmMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
|
||||
mainProcessElectronServer.registerChannel('void-channel-llmMessageService', llmMessageChannel);
|
||||
|
||||
|
|
|
|||
|
|
@ -404,8 +404,8 @@ function isVersionValid(currentVersion: string, date: ProductDate, requestedVers
|
|||
|
||||
if (!isValidVersion(currentVersion, date, desiredVersion)) {
|
||||
// Void - ignore not compatible
|
||||
// notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion));
|
||||
// return false;
|
||||
notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -64,7 +64,8 @@ export const enum KeybindingWeight {
|
|||
EditorContrib = 100,
|
||||
WorkbenchContrib = 200,
|
||||
BuiltinExtension = 300,
|
||||
ExternalExtension = 400
|
||||
ExternalExtension = 400,
|
||||
VoidExtension = 605, // Void - must trump any external extension
|
||||
}
|
||||
|
||||
export interface ICommandAndKeybindingRule extends IKeybindingRule {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DisposableStore } from '../../../base/common/lifecycle.js';
|
||||
// import { mixin } from '../../../base/common/objects.js';
|
||||
import { isWeb } from '../../../base/common/platform.js';
|
||||
import { escapeRegExpCharacters } from '../../../base/common/strings.js';
|
||||
import { localize } from '../../../nls.js';
|
||||
|
|
@ -15,6 +16,7 @@ import { Registry } from '../../registry/common/platform.js';
|
|||
import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from './gdprTypings.js';
|
||||
import { ITelemetryData, ITelemetryService, TelemetryConfiguration, TelemetryLevel, TELEMETRY_CRASH_REPORTER_SETTING_ID, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SECTION_ID, TELEMETRY_SETTING_ID, ICommonProperties } from './telemetry.js';
|
||||
import { getTelemetryLevel, ITelemetryAppender } from './telemetryUtils.js';
|
||||
// import { cleanData } from './telemetryUtils.js';
|
||||
|
||||
export interface ITelemetryServiceConfig {
|
||||
appenders: ITelemetryAppender[];
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { timeout } from '../../../base/common/async.js';
|
||||
// import { timeout } from '../../../base/common/async.js';
|
||||
import { CancellationToken } from '../../../base/common/cancellation.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { IConfigurationService } from '../../configuration/common/configuration.js';
|
||||
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
|
||||
// import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
|
||||
import { ILifecycleMainService, LifecycleMainPhase } from '../../lifecycle/electron-main/lifecycleMainService.js';
|
||||
import { ILogService } from '../../log/common/log.js';
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
|
|
@ -60,7 +61,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
|||
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
|
||||
@IRequestService protected requestService: IRequestService,
|
||||
@ILogService protected logService: ILogService,
|
||||
@IProductService protected readonly productService: IProductService
|
||||
@IProductService protected readonly productService: IProductService,
|
||||
) {
|
||||
lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen)
|
||||
.finally(() => this.initialize());
|
||||
|
|
@ -79,72 +80,29 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
|||
}
|
||||
console.log('is built, continuing with update service')
|
||||
|
||||
// Void commented this
|
||||
// if (this.environmentMainService.disableUpdates) {
|
||||
// this.setState(State.Disabled(DisablementReason.DisabledByEnvironment));
|
||||
// this.logService.info('update#ctor - updates are disabled by the environment');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (!this.productService.updateUrl || !this.productService.commit) {
|
||||
// this.setState(State.Disabled(DisablementReason.MissingConfiguration));
|
||||
// this.logService.info('update#ctor - updates are disabled as there is no update URL');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Void - for now, always update
|
||||
|
||||
const updateMode = 'default' //this.configurationService.getValue<'none' | 'manual' | 'start' | 'default'>('update.mode');
|
||||
|
||||
const quality = this.getProductQuality(updateMode);
|
||||
if (!quality) {
|
||||
this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
|
||||
this.logService.info('update#ctor - updates are disabled by user preference');
|
||||
return;
|
||||
}
|
||||
|
||||
// const quality = 'stable'
|
||||
this.url = this.doBuildUpdateFeedUrl(quality);
|
||||
this.url = this.doBuildUpdateFeedUrl('stable');
|
||||
if (!this.url) {
|
||||
this.setState(State.Disabled(DisablementReason.InvalidConfiguration));
|
||||
this.logService.info('update#ctor - updates are disabled as the update URL is badly formed');
|
||||
return;
|
||||
}
|
||||
|
||||
// hidden setting
|
||||
if (this.configurationService.getValue<boolean>('_update.prss')) {
|
||||
const url = new URL(this.url);
|
||||
url.searchParams.set('prss', 'true');
|
||||
this.url = url.toString();
|
||||
}
|
||||
this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
|
||||
|
||||
this.setState(State.Idle(this.getUpdateType()));
|
||||
|
||||
// if (updateMode === 'manual') {
|
||||
// this.logService.info('update#ctor - manual checks only; automatic updates are disabled by user preference');
|
||||
// return;
|
||||
// }
|
||||
// Void - temporarily disabled while we figure out how to do this the right way
|
||||
|
||||
// if (updateMode === 'start') {
|
||||
// this.logService.info('update#ctor - startup checks only; automatic updates are disabled by user preference');
|
||||
// this.setState(State.Idle(this.getUpdateType()));
|
||||
|
||||
// // Check for updates only once after 30 seconds
|
||||
// setTimeout(() => this.checkForUpdates(false), 30 * 1000);
|
||||
// } else {
|
||||
// Start checking for updates after 30 seconds
|
||||
this.scheduleCheckForUpdates(30 * 1000).then(undefined, err => this.logService.error(err));
|
||||
// }
|
||||
// start checking for updates after 10 seconds
|
||||
// this.scheduleCheckForUpdates(10 * 1000).then(undefined, err => this.logService.error(err));
|
||||
}
|
||||
|
||||
private getProductQuality(updateMode: string): string | undefined {
|
||||
return updateMode === 'none' ? undefined : this.productService.quality;
|
||||
}
|
||||
|
||||
private async scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
|
||||
await timeout(delay);
|
||||
await this.checkForUpdates(false);
|
||||
return await this.scheduleCheckForUpdates(60 * 60 * 1000);
|
||||
}
|
||||
// private async scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
|
||||
// await timeout(delay);
|
||||
// await this.checkForUpdates(false);
|
||||
// return await this.scheduleCheckForUpdates(60 * 60 * 1000);
|
||||
// }
|
||||
|
||||
async checkForUpdates(explicit: boolean): Promise<void> {
|
||||
this.logService.trace('update#checkForUpdates, state = ', this.state.type);
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
|
|||
@IEnvironmentMainService environmentMainService: IEnvironmentMainService,
|
||||
@IRequestService requestService: IRequestService,
|
||||
@ILogService logService: ILogService,
|
||||
@IProductService productService: IProductService
|
||||
@IProductService productService: IProductService,
|
||||
) {
|
||||
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
|||
@ILogService logService: ILogService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
|
||||
@IProductService productService: IProductService
|
||||
@IProductService productService: IProductService,
|
||||
) {
|
||||
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
|
||||
|
||||
|
|
|
|||
|
|
@ -16,3 +16,6 @@ import '../common/refreshModelService.js'
|
|||
|
||||
// metrics
|
||||
import '../common/metricsService.js'
|
||||
|
||||
// updates
|
||||
import '../common/voidUpdateService.js'
|
||||
|
|
|
|||
|
|
@ -65,18 +65,18 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
|||
this._onRequestIdDone(e.requestId)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_llm') satisfies Event<EventLLMMessageOnErrorParams>)(e => {
|
||||
console.log('Error in LLMMessageService:', JSON.stringify(e))
|
||||
console.error('Error in LLMMessageService:', JSON.stringify(e))
|
||||
this.onErrorHooks_llm[e.requestId]?.(e)
|
||||
this._onRequestIdDone(e.requestId)
|
||||
}))
|
||||
// ollama
|
||||
// ollama .list()
|
||||
this._register((this.channel.listen('onSuccess_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => {
|
||||
this.onSuccess_ollama[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => {
|
||||
this.onError_ollama[e.requestId]?.(e)
|
||||
}))
|
||||
// openaiCompatible
|
||||
// openaiCompatible .list()
|
||||
this._register((this.channel.listen('onSuccess_openAICompatible') satisfies Event<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>)(e => {
|
||||
this.onSuccess_openAICompatible[e.requestId]?.(e)
|
||||
}))
|
||||
|
|
@ -88,7 +88,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
|||
|
||||
sendLLMMessage(params: ServiceSendLLMMessageParams) {
|
||||
const { onText, onFinalMessage, onError, ...proxyParams } = params;
|
||||
const { featureName } = proxyParams
|
||||
const { useProviderFor: featureName } = proxyParams
|
||||
|
||||
// end early if no provider
|
||||
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName]
|
||||
|
|
@ -98,6 +98,10 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
|||
}
|
||||
const { providerName, modelName } = modelSelection
|
||||
|
||||
const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions
|
||||
if (aiInstructions)
|
||||
proxyParams.messages.unshift({ role: 'system', content: aiInstructions })
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.onTextHooks_llm[requestId_] = onText
|
||||
|
|
|
|||
|
|
@ -31,12 +31,12 @@ export type LLMMessage = {
|
|||
}
|
||||
|
||||
export type ServiceSendLLMFeatureParams = {
|
||||
featureName: 'Ctrl+K';
|
||||
useProviderFor: 'Ctrl+K';
|
||||
range: IRange;
|
||||
} | {
|
||||
featureName: 'Ctrl+L';
|
||||
useProviderFor: 'Ctrl+L';
|
||||
} | {
|
||||
featureName: 'Autocomplete';
|
||||
useProviderFor: 'Autocomplete';
|
||||
range: IRange;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,15 @@ import { createDecorator } from '../../instantiation/common/instantiation.js';
|
|||
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { Action2, registerAction2 } from '../../actions/common/actions.js';
|
||||
import { localize2 } from '../../../nls.js';
|
||||
import { ServicesAccessor } from '../../../editor/browser/editorExtensions.js';
|
||||
import { INotificationService } from '../../notification/common/notification.js';
|
||||
|
||||
export interface IMetricsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
capture(event: string, params: Record<string, any>): void;
|
||||
getDebuggingProperties(): Promise<object>;
|
||||
}
|
||||
|
||||
export const IMetricsService = createDecorator<IMetricsService>('metricsService');
|
||||
|
|
@ -25,6 +30,7 @@ export class MetricsService implements IMetricsService {
|
|||
constructor(
|
||||
@IMainProcessService mainProcessService: IMainProcessService // (only usable on client side)
|
||||
) {
|
||||
// creates an IPC proxy to use metricsMainService.ts
|
||||
this.metricsService = ProxyChannel.toService<IMetricsService>(mainProcessService.getChannel('void-channel-metrics'));
|
||||
}
|
||||
|
||||
|
|
@ -33,7 +39,30 @@ export class MetricsService implements IMetricsService {
|
|||
this.metricsService.capture(...params);
|
||||
}
|
||||
|
||||
// anything transmitted over a channel must be async even if it looks like it doesn't have to be
|
||||
async getDebuggingProperties(): Promise<object> {
|
||||
return this.metricsService.getDebuggingProperties()
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager);
|
||||
|
||||
|
||||
// debugging action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'voidDebugInfo',
|
||||
f1: true,
|
||||
title: localize2('voidMetricsDebug', 'Void: Log Debug Info'),
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
const notifService = accessor.get(INotificationService)
|
||||
|
||||
const debugProperties = await metricsService.getDebuggingProperties()
|
||||
console.log('Metrics:', debugProperties)
|
||||
notifService.info(`Void Debug info:\n${JSON.stringify(debugProperties, null, 2)}`)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -25,11 +25,20 @@ type RefreshableState = ({
|
|||
state: 'finished',
|
||||
timeoutId: null,
|
||||
} | {
|
||||
state: 'finished_invisible',
|
||||
state: 'error',
|
||||
timeoutId: null,
|
||||
})
|
||||
|
||||
|
||||
/*
|
||||
|
||||
user click -> error -> fire(error)
|
||||
\> success -> fire(success)
|
||||
finally: keep polling
|
||||
|
||||
poll -> do not fire
|
||||
|
||||
*/
|
||||
export type RefreshModelStateOfProvider = Record<RefreshableProviderName, RefreshableState>
|
||||
|
||||
|
||||
|
|
@ -41,6 +50,8 @@ const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvide
|
|||
const REFRESH_INTERVAL = 5_000
|
||||
// const COOLDOWN_TIMEOUT = 300
|
||||
|
||||
const autoOptions = { enableProviderOnSuccess: true, doNotFire: true }
|
||||
|
||||
// element-wise equals
|
||||
function eq<T>(a: T[], b: T[]): boolean {
|
||||
if (a.length !== b.length) return false
|
||||
|
|
@ -51,7 +62,7 @@ function eq<T>(a: T[], b: T[]): boolean {
|
|||
}
|
||||
export interface IRefreshModelService {
|
||||
readonly _serviceBrand: undefined;
|
||||
refreshModels: (providerName: RefreshableProviderName) => Promise<void>;
|
||||
startRefreshingModels: (providerName: RefreshableProviderName, options: { enableProviderOnSuccess: boolean, doNotFire: boolean }) => void;
|
||||
onDidChangeState: Event<RefreshableProviderName>;
|
||||
state: RefreshModelStateOfProvider;
|
||||
}
|
||||
|
|
@ -75,23 +86,23 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
|
||||
const disposables: Set<IDisposable> = new Set()
|
||||
|
||||
const initializePollingAndOnChange = () => {
|
||||
const initializeAutoPollingAndOnChange = () => {
|
||||
this._clearAllTimeouts()
|
||||
disposables.forEach(d => d.dispose())
|
||||
disposables.clear()
|
||||
|
||||
if (!voidSettingsService.state.featureFlagSettings.autoRefreshModels) return
|
||||
if (!voidSettingsService.state.globalSettings.autoRefreshModels) return
|
||||
|
||||
for (const providerName of refreshableProviderNames) {
|
||||
|
||||
const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
|
||||
this.refreshModels(providerName, !enabled, { isPolling: true, isInvisible: true })
|
||||
// const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
|
||||
this.startRefreshingModels(providerName, autoOptions)
|
||||
|
||||
// every time providerName.enabled changes, refresh models too, like a useEffect
|
||||
let relevantVals = () => refreshBasedOn[providerName].map(settingName => this.voidSettingsService.state.settingsOfProvider[providerName][settingName])
|
||||
let relevantVals = () => refreshBasedOn[providerName].map(settingName => voidSettingsService.state.settingsOfProvider[providerName][settingName])
|
||||
let prevVals = relevantVals() // each iteration of a for loop has its own context and vars, so this is ok
|
||||
disposables.add(
|
||||
this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this
|
||||
voidSettingsService.onDidChangeState(() => { // we might want to debounce this
|
||||
const newVals = relevantVals()
|
||||
if (!eq(prevVals, newVals)) {
|
||||
|
||||
|
|
@ -101,7 +112,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
// if it was just enabled, or there was a change and it wasn't to the enabled state, refresh
|
||||
if ((enabled && !prevEnabled) || (!enabled && !prevEnabled)) {
|
||||
// if user just clicked enable, refresh
|
||||
this.refreshModels(providerName, !enabled, { isPolling: false, isInvisible: true })
|
||||
this.startRefreshingModels(providerName, autoOptions)
|
||||
}
|
||||
else {
|
||||
// else if user just clicked disable, don't refresh
|
||||
|
|
@ -117,11 +128,11 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
}
|
||||
}
|
||||
|
||||
// on mount (when get init settings state), and if a relevant feature flag changes (detected natively right now by refreshing if any flag changes), start refreshing models
|
||||
// on mount (when get init settings state), and if a relevant feature flag changes, start refreshing models
|
||||
voidSettingsService.waitForInitState.then(() => {
|
||||
initializePollingAndOnChange()
|
||||
initializeAutoPollingAndOnChange()
|
||||
this._register(
|
||||
voidSettingsService.onDidChangeState((type) => { if (type === 'featureFlagSettings') initializePollingAndOnChange() })
|
||||
voidSettingsService.onDidChangeState((type) => { if (typeof type === 'object' && type[1] === 'autoRefreshModels') initializeAutoPollingAndOnChange() })
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -129,22 +140,23 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
|
||||
state: RefreshModelStateOfProvider = {
|
||||
ollama: { state: 'init', timeoutId: null },
|
||||
// openAICompatible: { state: 'init', timeoutId: null },
|
||||
}
|
||||
|
||||
|
||||
// start listening for models (and don't stop until success)
|
||||
async refreshModels(providerName: RefreshableProviderName, enableProviderOnSuccess?: boolean, options?: { isPolling?: boolean, isInvisible?: boolean }) {
|
||||
|
||||
const { isPolling, isInvisible } = options ?? {}
|
||||
|
||||
console.log(`refreshModels, isInvisible ${isInvisible} isPolling ${isPolling}`)
|
||||
startRefreshingModels: IRefreshModelService['startRefreshingModels'] = (providerName, options) => {
|
||||
|
||||
this._clearProviderTimeout(providerName)
|
||||
|
||||
// start loading models
|
||||
if (!isInvisible) this._setRefreshState(providerName, 'refreshing')
|
||||
this._setRefreshState(providerName, 'refreshing', options)
|
||||
|
||||
const autoPoll = () => {
|
||||
if (this.voidSettingsService.state.globalSettings.autoRefreshModels) {
|
||||
// resume auto-polling
|
||||
const timeoutId = setTimeout(() => this.startRefreshingModels(providerName, autoOptions), REFRESH_INTERVAL)
|
||||
this._setTimeoutId(providerName, timeoutId)
|
||||
}
|
||||
}
|
||||
const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList
|
||||
: providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList
|
||||
: () => { }
|
||||
|
|
@ -160,34 +172,21 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id;
|
||||
else throw new Error('refreshMode fn: unknown provider', providerName);
|
||||
}),
|
||||
{ enableProviderOnSuccess, isPolling, isInvisible }
|
||||
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
|
||||
)
|
||||
|
||||
// update state
|
||||
if (enableProviderOnSuccess) {
|
||||
this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
|
||||
}
|
||||
|
||||
if (!isInvisible) {
|
||||
this._setRefreshState(providerName, 'finished')
|
||||
} else if (isInvisible) {
|
||||
this._setRefreshState(providerName, 'finished_invisible')
|
||||
}
|
||||
|
||||
if (options.enableProviderOnSuccess) this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
|
||||
|
||||
this._setRefreshState(providerName, 'finished', options)
|
||||
autoPoll()
|
||||
},
|
||||
onError: ({ error }) => {
|
||||
console.log('retrying list models:', providerName, error)
|
||||
this._setRefreshState(providerName, 'error', options)
|
||||
autoPoll()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// check if we should poll
|
||||
// if it was originally called as `isPolling` and if the `autoRefreshModels` flag is enabled
|
||||
if (isPolling && this.voidSettingsService.state.featureFlagSettings.autoRefreshModels) {
|
||||
const timeoutId = setTimeout(() => this.refreshModels(providerName, enableProviderOnSuccess, options), REFRESH_INTERVAL)
|
||||
this._setTimeoutId(providerName, timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
_clearAllTimeouts() {
|
||||
|
|
@ -208,7 +207,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
this.state[providerName].timeoutId = timeoutId
|
||||
}
|
||||
|
||||
private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state']) {
|
||||
private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state'], options?: { doNotFire: boolean }) {
|
||||
if (options?.doNotFire) return
|
||||
this.state[providerName].state = state
|
||||
this._onDidChangeState.fire(providerName)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ import { registerSingleton, InstantiationType } from '../../instantiation/common
|
|||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
|
||||
import { IMetricsService } from './metricsService.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, FeatureFlagSettings, FeatureFlagName, defaultFeatureFlagSettings } from './voidSettingsTypes.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
const STORAGE_KEY = 'void.voidSettingsStorage'
|
||||
const STORAGE_KEY = 'void.settingsServiceStorage'
|
||||
|
||||
type SetSettingOfProviderFn = <S extends SettingName>(
|
||||
providerName: ProviderName,
|
||||
|
|
@ -28,7 +28,7 @@ type SetModelSelectionOfFeatureFn = <K extends FeatureName>(
|
|||
options?: { doNotApplyEffects?: true }
|
||||
) => Promise<void>;
|
||||
|
||||
type SetFeatureFlagFn = (flagName: FeatureFlagName, newVal: boolean) => void;
|
||||
type SetGlobalSettingFn = <T extends GlobalSettingName, >(settingName: T, newVal: GlobalSettings[T]) => void;
|
||||
|
||||
export type ModelOption = { name: string, selection: ModelSelection }
|
||||
|
||||
|
|
@ -37,12 +37,13 @@ export type ModelOption = { name: string, selection: ModelSelection }
|
|||
export type VoidSettingsState = {
|
||||
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
|
||||
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
|
||||
readonly featureFlagSettings: FeatureFlagSettings;
|
||||
readonly globalSettings: GlobalSettings;
|
||||
|
||||
readonly _modelOptions: ModelOption[] // computed based on the two above items
|
||||
}
|
||||
|
||||
type EventProp = Exclude<keyof VoidSettingsState, '_modelOptions'> | 'all'
|
||||
type RealVoidSettings = Exclude<keyof VoidSettingsState, '_modelOptions'>
|
||||
type EventProp<T extends RealVoidSettings = RealVoidSettings> = T extends 'globalSettings' ? [T, keyof VoidSettingsState[T]] : T | 'all'
|
||||
|
||||
|
||||
export interface IVoidSettingsService {
|
||||
|
|
@ -54,9 +55,9 @@ export interface IVoidSettingsService {
|
|||
|
||||
setSettingOfProvider: SetSettingOfProviderFn;
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
|
||||
setFeatureFlag: SetFeatureFlagFn;
|
||||
setGlobalSetting: SetGlobalSettingFn;
|
||||
|
||||
setAutodetectedModels(providerName: ProviderName, modelNames: string[], logging: { enableProviderOnSuccess?: boolean, isPolling?: boolean, isInvisible?: boolean }): void;
|
||||
setAutodetectedModels(providerName: ProviderName, modelNames: string[], logging: object): void;
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string): void;
|
||||
addModel(providerName: ProviderName, modelName: string): void;
|
||||
deleteModel(providerName: ProviderName, modelName: string): boolean;
|
||||
|
|
@ -81,7 +82,7 @@ const defaultState = () => {
|
|||
const d: VoidSettingsState = {
|
||||
settingsOfProvider: deepClone(defaultSettingsOfProvider),
|
||||
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null },
|
||||
featureFlagSettings: deepClone(defaultFeatureFlagSettings),
|
||||
globalSettings: deepClone(defaultGlobalSettings),
|
||||
_modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed
|
||||
}
|
||||
return d
|
||||
|
|
@ -150,7 +151,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
}
|
||||
}
|
||||
|
||||
const newFeatureFlags = this.state.featureFlagSettings
|
||||
const newGlobalSettings = this.state.globalSettings
|
||||
|
||||
// if changed models or enabled a provider, recompute models list
|
||||
const modelsListChanged = settingName === 'models' || settingName === '_enabled'
|
||||
|
|
@ -159,7 +160,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
const newState: VoidSettingsState = {
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
featureFlagSettings: newFeatureFlags,
|
||||
globalSettings: newGlobalSettings,
|
||||
_modelOptions: newModelsList,
|
||||
}
|
||||
|
||||
|
|
@ -187,17 +188,17 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
}
|
||||
|
||||
|
||||
setFeatureFlag: SetFeatureFlagFn = async (flagName, newVal) => {
|
||||
const newState = {
|
||||
setGlobalSetting: SetGlobalSettingFn = async (settingName, newVal) => {
|
||||
const newState: VoidSettingsState = {
|
||||
...this.state,
|
||||
featureFlagSettings: {
|
||||
...this.state.featureFlagSettings,
|
||||
[flagName]: newVal
|
||||
globalSettings: {
|
||||
...this.state.globalSettings,
|
||||
[settingName]: newVal
|
||||
}
|
||||
}
|
||||
this.state = newState
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire('featureFlagSettings')
|
||||
this._onDidChangeState.fire(['globalSettings', settingName])
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -222,7 +223,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
|
||||
|
||||
|
||||
setAutodetectedModels(providerName: ProviderName, newDefaultModelNames: string[], logging: { enableProviderOnSuccess?: boolean, isPolling?: boolean, isInvisible?: boolean }) {
|
||||
setAutodetectedModels(providerName: ProviderName, newDefaultModelNames: string[], logging: object) {
|
||||
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
|
||||
|
|
|
|||
|
|
@ -429,26 +429,16 @@ export type RefreshableProviderName = typeof refreshableProviderNames[number]
|
|||
|
||||
|
||||
|
||||
export type FeatureFlagSettings = {
|
||||
export type GlobalSettings = {
|
||||
autoRefreshModels: boolean;
|
||||
aiInstructions: string;
|
||||
}
|
||||
export const defaultFeatureFlagSettings: FeatureFlagSettings = {
|
||||
export const defaultGlobalSettings: GlobalSettings = {
|
||||
autoRefreshModels: true,
|
||||
aiInstructions: '',
|
||||
}
|
||||
|
||||
export type FeatureFlagName = keyof FeatureFlagSettings
|
||||
export const featureFlagNames = Object.keys(defaultFeatureFlagSettings) as FeatureFlagName[]
|
||||
|
||||
type FeatureFlagDisplayInfo = {
|
||||
description: string,
|
||||
}
|
||||
export const displayInfoOfFeatureFlag = (featureFlag: FeatureFlagName): FeatureFlagDisplayInfo => {
|
||||
if (featureFlag === 'autoRefreshModels') {
|
||||
return {
|
||||
description: `Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`,
|
||||
}
|
||||
}
|
||||
throw new Error(`featureFlagInfo: Unknown feature flag: "${featureFlag}"`)
|
||||
}
|
||||
export type GlobalSettingName = keyof GlobalSettings
|
||||
export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSettingName[]
|
||||
|
||||
|
||||
|
|
|
|||
46
src/vs/platform/void/common/voidUpdateService.ts
Normal file
46
src/vs/platform/void/common/voidUpdateService.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
|
||||
|
||||
|
||||
export interface IVoidUpdateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
check: () => Promise<{ hasUpdate: true, message: string } | { hasUpdate: false } | null>;
|
||||
}
|
||||
|
||||
|
||||
export const IVoidUpdateService = createDecorator<IVoidUpdateService>('VoidUpdateService');
|
||||
|
||||
|
||||
// implemented by calling channel
|
||||
export class VoidUpdateService implements IVoidUpdateService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly voidUpdateService: IVoidUpdateService;
|
||||
|
||||
constructor(
|
||||
@IMainProcessService mainProcessService: IMainProcessService, // (only usable on client side)
|
||||
) {
|
||||
// creates an IPC proxy to use metricsMainService.ts
|
||||
this.voidUpdateService = ProxyChannel.toService<IVoidUpdateService>(mainProcessService.getChannel('void-channel-update'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// anything transmitted over a channel must be async even if it looks like it doesn't have to be
|
||||
check: IVoidUpdateService['check'] = async () => {
|
||||
const res = await this.voidUpdateService.check()
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IVoidUpdateService, VoidUpdateService, InstantiationType.Eager);
|
||||
|
||||
|
||||
|
|
@ -38,7 +38,6 @@ export const sendLLMMessage = ({
|
|||
modelName,
|
||||
numMessages: messages?.length,
|
||||
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
|
||||
version: '2024-11-14',
|
||||
...extras,
|
||||
})
|
||||
}
|
||||
|
|
@ -63,7 +62,6 @@ export const sendLLMMessage = ({
|
|||
|
||||
const onError: OnError = ({ message: error, fullError }) => {
|
||||
if (_didAbort) return
|
||||
console.log("ERROR!!!!!", error)
|
||||
console.error('sendLLMMessage onError:', error)
|
||||
captureChatEvent(`${loggingName} - Error`, { error })
|
||||
onError_({ message: error, fullError })
|
||||
|
|
|
|||
|
|
@ -4,43 +4,97 @@
|
|||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
|
||||
import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js';
|
||||
import { generateUuid } from '../../../base/common/uuid.js';
|
||||
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
|
||||
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
import { IStorageMainService } from '../../storage/electron-main/storageMainService.js';
|
||||
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { PostHog } from 'posthog-node'
|
||||
|
||||
|
||||
// posthog-js (old):
|
||||
// posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { api_host: 'https://us.i.posthog.com', })
|
||||
const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
|
||||
|
||||
// const buildEnv = 'development';
|
||||
// const buildNumber = '1.0.0';
|
||||
// const isMac = process.platform === 'darwin';
|
||||
const VOID_MACHINE_STORAGE_KEY = 'void.machineId'
|
||||
|
||||
export class MetricsMainService extends Disposable implements IMetricsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
readonly _distinctId: string
|
||||
readonly client: PostHog
|
||||
private readonly client: PostHog
|
||||
|
||||
private readonly _initProperties: object
|
||||
|
||||
|
||||
// TODO we should eventually identify people based on email
|
||||
private get machineId() {
|
||||
const currVal = this._storageService.applicationStorage.get(VOID_MACHINE_STORAGE_KEY)
|
||||
if (currVal !== undefined) return currVal
|
||||
const newVal = generateUuid()
|
||||
this._storageService.applicationStorage.set(VOID_MACHINE_STORAGE_KEY, newVal)
|
||||
return newVal
|
||||
}
|
||||
|
||||
|
||||
constructor(
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
@IStorageMainService private readonly _storageService: IStorageMainService,
|
||||
@IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService,
|
||||
) {
|
||||
super()
|
||||
this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { host: 'https://us.i.posthog.com', })
|
||||
this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', {
|
||||
host: 'https://us.i.posthog.com',
|
||||
})
|
||||
|
||||
const { devDeviceId, firstSessionDate, machineId } = this._telemetryService
|
||||
this._distinctId = devDeviceId
|
||||
this.client.identify({ distinctId: devDeviceId, properties: { firstSessionDate, machineId } })
|
||||
// we'd like to use devDeviceId on telemetryService, but that gets sanitized by the time it gets here as 'someValue.devDeviceId'
|
||||
|
||||
console.log('Void posthog metrics info:', JSON.stringify({ devDeviceId, firstSessionDate, machineId }))
|
||||
const { commit, version, quality } = this._productService
|
||||
|
||||
const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts
|
||||
|
||||
|
||||
// custom properties we identify
|
||||
this._initProperties = {
|
||||
commit,
|
||||
version,
|
||||
os,
|
||||
quality,
|
||||
distinctId: this.machineId,
|
||||
isDevMode,
|
||||
...this._getOSInfo(),
|
||||
}
|
||||
|
||||
const identifyMessage = {
|
||||
distinctId: this.machineId,
|
||||
properties: this._initProperties,
|
||||
}
|
||||
this.client.identify(identifyMessage)
|
||||
|
||||
console.log('Void posthog metrics info:', JSON.stringify(identifyMessage, null, 2))
|
||||
|
||||
}
|
||||
|
||||
_getOSInfo() {
|
||||
try {
|
||||
const { platform, arch } = process // see platform.ts
|
||||
return { platform, arch }
|
||||
}
|
||||
catch (e) {
|
||||
return { osInfo: { platform: '??', arch: '??' } }
|
||||
}
|
||||
}
|
||||
|
||||
capture: IMetricsService['capture'] = (event, params) => {
|
||||
const capture = { distinctId: this._distinctId, event, properties: params } as const
|
||||
const capture = { distinctId: this.machineId, event, properties: params } as const
|
||||
// console.log('full capture:', capture)
|
||||
this.client.capture(capture)
|
||||
}
|
||||
|
||||
|
||||
async getDebuggingProperties() {
|
||||
return this._initProperties
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
50
src/vs/platform/void/electron-main/voidUpdateMainService.ts
Normal file
50
src/vs/platform/void/electron-main/voidUpdateMainService.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
|
||||
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
|
||||
import { IVoidUpdateService } from '../common/voidUpdateService.js';
|
||||
|
||||
|
||||
|
||||
export class VoidMainUpdateService extends Disposable implements IVoidUpdateService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
@IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async check() {
|
||||
const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts
|
||||
|
||||
if (isDevMode) {
|
||||
return { hasUpdate: false } as const
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`https://updates.voideditor.dev/api/v0/${this._productService.commit}`)
|
||||
const resJSON = await res.json()
|
||||
|
||||
if (!resJSON) return null
|
||||
|
||||
const { hasUpdate, downloadMessage } = resJSON ?? {}
|
||||
if (hasUpdate === undefined)
|
||||
return null
|
||||
|
||||
const after = (downloadMessage || '') + ''
|
||||
return { hasUpdate: !!hasUpdate, message: after }
|
||||
}
|
||||
catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2534,7 +2534,7 @@ const LayoutStateKeys = {
|
|||
// Part Sizing
|
||||
GRID_SIZE: new InitializationStateKey('grid.size', StorageScope.PROFILE, StorageTarget.MACHINE, { width: 800, height: 600 }),
|
||||
SIDEBAR_SIZE: new InitializationStateKey<number>('sideBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200),
|
||||
AUXILIARYBAR_SIZE: new InitializationStateKey<number>('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200),
|
||||
AUXILIARYBAR_SIZE: new InitializationStateKey<number>('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 800), // Void changed this from 200 to 800
|
||||
PANEL_SIZE: new InitializationStateKey<number>('panel.size', StorageScope.PROFILE, StorageTarget.MACHINE, 300),
|
||||
|
||||
PANEL_LAST_NON_MAXIMIZED_HEIGHT: new RuntimeStateKey<number>('panel.lastNonMaximizedHeight', StorageScope.PROFILE, StorageTarget.MACHINE, 300),
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><style>.st0{fill:#f6f6f6;fill-opacity:0}.st1{fill:#fff}.st2{fill:#167abf}</style><path class="st0" d="M1024 1024H0V0h1024v1024z"/><path class="st1" d="M1024 85.333v853.333H0V85.333h1024z"/><path class="st2" d="M0 85.333h298.667v853.333H0V85.333zm1024 0v853.333H384V85.333h640zm-554.667 160h341.333v-64H469.333v64zm341.334 533.334H469.333v64h341.333l.001-64zm128-149.334H597.333v64h341.333l.001-64zm0-149.333H597.333v64h341.333l.001-64zm0-149.333H597.333v64h341.333l.001-64z"/></svg>
|
||||
|
Before Width: | Height: | Size: 559 B |
BIN
src/vs/workbench/browser/media/void-icon-sm.png
Normal file
BIN
src/vs/workbench/browser/media/void-icon-sm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 795 KiB |
|
|
@ -45,7 +45,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart {
|
|||
static readonly viewContainersWorkspaceStateKey = 'workbench.auxiliarybar.viewContainersWorkspaceState';
|
||||
|
||||
// Use the side bar dimensions
|
||||
override readonly minimumWidth: number = 170;
|
||||
override readonly minimumWidth: number = 230; // Void changed this (was 170)
|
||||
override readonly maximumWidth: number = Number.POSITIVE_INFINITY;
|
||||
override readonly minimumHeight: number = 0;
|
||||
override readonly maximumHeight: number = Number.POSITIVE_INFINITY;
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 16px;
|
||||
background-image: url('../../../../browser/media/code-icon.svg');
|
||||
background-image: url('../../../../browser/media/void-icon-sm.png');
|
||||
width: 16px;
|
||||
padding: 0;
|
||||
margin: 0 6px 0 10px;
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@
|
|||
}
|
||||
|
||||
.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-left > .window-appicon:not(.codicon) {
|
||||
background-image: url('../../../media/code-icon.svg');
|
||||
background-image: url('../../../media/void-icon-sm.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 16px;
|
||||
|
|
@ -275,7 +275,7 @@
|
|||
height: 8px;
|
||||
z-index: 1;
|
||||
/* on top of home indicator */
|
||||
background-image: url('../../../media/code-icon.svg');
|
||||
background-image: url('../../../media/void-icon-sm.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 8px;
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@
|
|||
|
||||
.file-icons-enabled .show-file-icons .webview-vs_code_release_notes-name-file-icon.file-icon::before {
|
||||
content: ' ';
|
||||
background-image: url('../../../../browser/media/code-icon.svg');
|
||||
background-image: url('../../../../browser/media/void-icon-sm.png');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -652,7 +652,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
|||
// newAutocompletion.abortRef = { current: () => { } }
|
||||
newAutocompletion.status = 'finished'
|
||||
// newAutocompletion.promise = undefined
|
||||
newAutocompletion.insertText = postprocessResult(extractCodeFromRegular(fullText))
|
||||
const [text, _] = extractCodeFromRegular({ text: fullText, recentlyAddedTextLen: 0 })
|
||||
newAutocompletion.insertText = postprocessResult(text)
|
||||
|
||||
resolve(newAutocompletion.insertText)
|
||||
|
||||
|
|
@ -662,7 +663,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
|||
newAutocompletion.status = 'error'
|
||||
reject(message)
|
||||
},
|
||||
featureName: 'Autocomplete',
|
||||
useProviderFor: 'Autocomplete',
|
||||
range: { startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column },
|
||||
})
|
||||
newAutocompletion.requestId = requestId
|
||||
|
|
|
|||
302
src/vs/workbench/contrib/void/browser/chatThreadService.ts
Normal file
302
src/vs/workbench/contrib/void/browser/chatThreadService.ts
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { VSReadFile } from './helpers/readFile.js';
|
||||
import { chat_prompt, chat_systemMessage } from './prompt/prompts.js';
|
||||
|
||||
export type CodeSelection = {
|
||||
fileURI: URI;
|
||||
selectionStr: string | null;
|
||||
content: string; // TODO remove this (replace `selectionStr` with `content`)
|
||||
range: IRange;
|
||||
}
|
||||
|
||||
// if selectionStr is null, it means to use the entire file at send time
|
||||
export type CodeStagingSelection = {
|
||||
type: 'Selection',
|
||||
fileURI: URI,
|
||||
selectionStr: string,
|
||||
range: IRange
|
||||
} | {
|
||||
type: 'File',
|
||||
fileURI: URI,
|
||||
selectionStr: null,
|
||||
range: null
|
||||
}
|
||||
|
||||
|
||||
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
|
||||
export type ChatMessage =
|
||||
| {
|
||||
role: 'user';
|
||||
content: string | null; // content sent to the llm - allowed to be '', will be replaced with (empty)
|
||||
displayContent: string | null; // content displayed to user - allowed to be '', will be ignored
|
||||
selections: CodeSelection[] | null; // the user's selection
|
||||
}
|
||||
| {
|
||||
role: 'assistant';
|
||||
content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
|
||||
displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored
|
||||
}
|
||||
| {
|
||||
role: 'system';
|
||||
content: string;
|
||||
displayContent?: undefined;
|
||||
}
|
||||
|
||||
// a 'thread' means a chat message history
|
||||
export type ChatThreads = {
|
||||
[id: string]: {
|
||||
id: string; // store the id here too
|
||||
createdAt: string; // ISO string
|
||||
lastModified: string; // ISO string
|
||||
messages: ChatMessage[];
|
||||
};
|
||||
}
|
||||
|
||||
export type ThreadsState = {
|
||||
allThreads: ChatThreads;
|
||||
currentThreadId: string; // intended for internal use only
|
||||
currentStagingSelections: CodeStagingSelection[] | null;
|
||||
}
|
||||
|
||||
export type ThreadStreamState = {
|
||||
[threadId: string]: undefined | {
|
||||
error?: { message: string, fullError: Error | null };
|
||||
messageSoFar?: string;
|
||||
streamingToken?: string;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const newThreadObject = () => {
|
||||
const now = new Date().toISOString()
|
||||
return {
|
||||
id: new Date().getTime().toString(),
|
||||
createdAt: now,
|
||||
lastModified: now,
|
||||
messages: [],
|
||||
} satisfies ChatThreads[string]
|
||||
}
|
||||
|
||||
const THREAD_STORAGE_KEY = 'void.chatThreadStorage'
|
||||
|
||||
export interface IChatThreadService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly state: ThreadsState;
|
||||
readonly streamState: ThreadStreamState;
|
||||
|
||||
onDidChangeCurrentThread: Event<void>;
|
||||
onDidChangeStreamState: Event<{ threadId: string }>
|
||||
|
||||
getCurrentThread(): ChatThreads[string];
|
||||
openNewThread(): void;
|
||||
switchToThread(threadId: string): void;
|
||||
|
||||
setStaging(stagingSelection: CodeStagingSelection[] | null): void;
|
||||
|
||||
addUserMessageAndStreamResponse(userMessage: string): Promise<void>;
|
||||
cancelStreaming(threadId: string): void;
|
||||
dismissStreamError(threadId: string): void;
|
||||
|
||||
}
|
||||
|
||||
export const IChatThreadService = createDecorator<IChatThreadService>('voidChatThreadService');
|
||||
class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
// this fires when the current thread changes at all (a switch of currentThread, or a message added to it, etc)
|
||||
private readonly _onDidChangeCurrentThread = new Emitter<void>();
|
||||
readonly onDidChangeCurrentThread: Event<void> = this._onDidChangeCurrentThread.event;
|
||||
|
||||
readonly streamState: ThreadStreamState = {}
|
||||
private readonly _onDidChangeStreamState = new Emitter<{ threadId: string }>();
|
||||
readonly onDidChangeStreamState: Event<{ threadId: string }> = this._onDidChangeStreamState.event;
|
||||
|
||||
state: ThreadsState // allThreads is persisted, currentThread is not
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
|
||||
) {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
allThreads: this._readAllThreads(),
|
||||
currentThreadId: null as unknown as string, // gets set in startNewThread()
|
||||
currentStagingSelections: null,
|
||||
}
|
||||
|
||||
// always be in a thread
|
||||
this.openNewThread()
|
||||
}
|
||||
|
||||
|
||||
private _readAllThreads(): ChatThreads {
|
||||
// PUT ANY VERSION CHANGE FORMAT CONVERSION CODE HERE
|
||||
const threads = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
|
||||
return threads ? JSON.parse(threads) : {}
|
||||
}
|
||||
|
||||
private _storeAllThreads(threads: ChatThreads) {
|
||||
this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads), StorageScope.APPLICATION, StorageTarget.USER)
|
||||
}
|
||||
|
||||
// this should be the only place this.state = ... appears besides constructor
|
||||
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
...state
|
||||
}
|
||||
if (affectsCurrent)
|
||||
this._onDidChangeCurrentThread.fire()
|
||||
}
|
||||
|
||||
private _setStreamState(threadId: string, state: Partial<NonNullable<ThreadStreamState[string]>>) {
|
||||
this.streamState[threadId] = {
|
||||
...this.streamState[threadId],
|
||||
...state
|
||||
}
|
||||
this._onDidChangeStreamState.fire({ threadId })
|
||||
}
|
||||
|
||||
|
||||
// ---------- streaming ----------
|
||||
|
||||
finishStreaming = (threadId: string, content: string, error?: { message: string, fullError: Error | null }) => {
|
||||
// add assistant's message to chat history, and clear selection
|
||||
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null }
|
||||
this._addMessageToThread(threadId, assistantHistoryElt)
|
||||
this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error })
|
||||
}
|
||||
|
||||
async addUserMessageAndStreamResponse(userMessage: string) {
|
||||
const threadId = this.getCurrentThread().id
|
||||
|
||||
const currSelns = this.state.currentStagingSelections ?? []
|
||||
const selections = !currSelns ? null : await Promise.all(
|
||||
currSelns.map(async (sel) => ({ ...sel, content: await VSReadFile(this._modelService, sel.fileURI) }))
|
||||
).then(
|
||||
(files) => files.filter(file => file.content !== null) as CodeSelection[]
|
||||
)
|
||||
|
||||
// add user's message to chat history
|
||||
const instructions = userMessage
|
||||
const userHistoryElt: ChatMessage = { role: 'user', content: chat_prompt(instructions, selections), displayContent: instructions, selections: selections }
|
||||
this._addMessageToThread(threadId, userHistoryElt)
|
||||
|
||||
|
||||
this._setStreamState(threadId, { error: undefined })
|
||||
|
||||
const llmCancelToken = this._llmMessageService.sendLLMMessage({
|
||||
logging: { loggingName: 'Chat' },
|
||||
messages: [
|
||||
{ role: 'system', content: chat_systemMessage },
|
||||
...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(null)' })),
|
||||
],
|
||||
onText: ({ newText, fullText }) => {
|
||||
this._setStreamState(threadId, { messageSoFar: fullText })
|
||||
},
|
||||
onFinalMessage: ({ fullText: content }) => {
|
||||
this.finishStreaming(threadId, content)
|
||||
},
|
||||
onError: (error) => {
|
||||
this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '', error)
|
||||
},
|
||||
useProviderFor: 'Ctrl+L',
|
||||
|
||||
})
|
||||
if (llmCancelToken === null) return
|
||||
this._setStreamState(threadId, { streamingToken: llmCancelToken })
|
||||
|
||||
}
|
||||
|
||||
cancelStreaming(threadId: string) {
|
||||
const llmCancelToken = this.streamState[threadId]?.streamingToken
|
||||
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
|
||||
this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '')
|
||||
}
|
||||
|
||||
dismissStreamError(threadId: string): void {
|
||||
this._setStreamState(threadId, { error: undefined })
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------- the rest ----------
|
||||
|
||||
getCurrentThread(): ChatThreads[string] {
|
||||
const state = this.state
|
||||
return state.allThreads[state.currentThreadId];
|
||||
}
|
||||
|
||||
switchToThread(threadId: string) {
|
||||
// console.log('threadId', threadId)
|
||||
// console.log('messages', this.state.allThreads[threadId].messages)
|
||||
this._setState({ currentThreadId: threadId }, true)
|
||||
}
|
||||
|
||||
|
||||
openNewThread() {
|
||||
// if a thread with 0 messages already exists, switch to it
|
||||
const { allThreads: currentThreads } = this.state
|
||||
for (const threadId in currentThreads) {
|
||||
if (currentThreads[threadId].messages.length === 0) {
|
||||
this.switchToThread(threadId)
|
||||
return
|
||||
}
|
||||
}
|
||||
// otherwise, start a new thread
|
||||
const newThread = newThreadObject()
|
||||
|
||||
// update state
|
||||
const newThreads: ChatThreads = {
|
||||
...currentThreads,
|
||||
[newThread.id]: newThread
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads, currentThreadId: newThread.id }, true)
|
||||
}
|
||||
|
||||
|
||||
_addMessageToThread(threadId: string, message: ChatMessage) {
|
||||
const { allThreads } = this.state
|
||||
|
||||
const oldThread = allThreads[threadId]
|
||||
|
||||
// update state and store it
|
||||
const newThreads = {
|
||||
...allThreads,
|
||||
[oldThread.id]: {
|
||||
...oldThread,
|
||||
lastModified: new Date().toISOString(),
|
||||
messages: [...oldThread.messages, message],
|
||||
}
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
|
||||
}
|
||||
|
||||
|
||||
setStaging(stagingSelection: CodeStagingSelection[] | null): void {
|
||||
this._setState({ currentStagingSelections: stagingSelection }, true) // this is a hack for now
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IChatThreadService, ChatThreadService, InstantiationType.Eager);
|
||||
|
||||
|
|
@ -80,6 +80,7 @@ export class ConsistentItemService extends Disposable {
|
|||
}
|
||||
|
||||
const initializeEditor = (editor: ICodeEditor) => {
|
||||
// if (editor.getModel()?.uri.scheme !== 'file') return // THIS BREAKS THINGS
|
||||
addTabSwitchListeners(editor)
|
||||
addDisposeListener(editor)
|
||||
putItemsOnEditor(editor, editor.getModel()?.uri ?? null)
|
||||
|
|
@ -126,6 +127,8 @@ export class ConsistentItemService extends Disposable {
|
|||
|
||||
const editorId = editor.getId()
|
||||
this.itemIdsOfEditorId[editorId]?.delete(itemId)
|
||||
if (this.itemIdsOfEditorId[editorId]?.size === 0)
|
||||
delete this.itemIdsOfEditorId[editorId]
|
||||
|
||||
this.disposeFnOfItemId[itemId]?.()
|
||||
delete this.disposeFnOfItemId[itemId]
|
||||
|
|
@ -157,7 +160,6 @@ export class ConsistentItemService extends Disposable {
|
|||
|
||||
|
||||
removeConsistentItemFromURI(consistentItemId: string) {
|
||||
|
||||
if (!(consistentItemId in this.infoOfConsistentItemId))
|
||||
return
|
||||
|
||||
|
|
@ -173,6 +175,9 @@ export class ConsistentItemService extends Disposable {
|
|||
|
||||
// clear
|
||||
this.consistentItemIdsOfURI[uri.fsPath]?.delete(consistentItemId)
|
||||
if (this.consistentItemIdsOfURI[uri.fsPath]?.size === 0)
|
||||
delete this.consistentItemIdsOfURI[uri.fsPath]
|
||||
|
||||
delete this.infoOfConsistentItemId[consistentItemId]
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class SurroundingsRemover {
|
|||
// returns whether it removed the whole prefix
|
||||
removePrefix = (prefix: string): boolean => {
|
||||
let offset = 0
|
||||
console.log('prefix', prefix, Math.min(this.j, prefix.length - 1))
|
||||
// console.log('prefix', prefix, Math.min(this.j, prefix.length - 1))
|
||||
while (this.i <= this.j && offset <= prefix.length - 1) {
|
||||
if (this.originalS.charAt(this.i) !== prefix.charAt(offset))
|
||||
break
|
||||
|
|
@ -64,7 +64,7 @@ class SurroundingsRemover {
|
|||
this.i = this.j + 1
|
||||
return false
|
||||
}
|
||||
console.log('index', index, until.length)
|
||||
// console.log('index', index, until.length)
|
||||
|
||||
if (alsoRemoveUntilStr)
|
||||
this.i = index + until.length
|
||||
|
|
@ -90,11 +90,21 @@ class SurroundingsRemover {
|
|||
}
|
||||
|
||||
|
||||
actualRecentlyAdded = (recentlyAddedTextLen: number) => {
|
||||
// aaaaaatextaaaaaa{recentlyAdded}
|
||||
// i ^ j
|
||||
// |
|
||||
// recentyAddedIdx
|
||||
const recentlyAddedIdx = this.j - recentlyAddedTextLen + 1
|
||||
return this.originalS.substring(Math.max(this.i, recentlyAddedIdx), this.j + 1)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const extractCodeFromRegular = (text: string): string => {
|
||||
export const extractCodeFromRegular = ({ text, recentlyAddedTextLen }: { text: string, recentlyAddedTextLen: number }): [string, string] => {
|
||||
// Match either:
|
||||
// 1. ```language\n<code>```
|
||||
// 2. ```<code>```
|
||||
|
|
@ -104,7 +114,9 @@ export const extractCodeFromRegular = (text: string): string => {
|
|||
pm.removeCodeBlock()
|
||||
|
||||
const s = pm.value()
|
||||
return s
|
||||
const actual = pm.actualRecentlyAdded(recentlyAddedTextLen)
|
||||
|
||||
return [s, actual]
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -112,7 +124,7 @@ export const extractCodeFromRegular = (text: string): string => {
|
|||
|
||||
|
||||
// Ollama has its own FIM, we should not use this if we use that
|
||||
export const extractCodeFromFIM = ({ text, midTag }: { text: string, midTag: string }): string => {
|
||||
export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { text: string, recentlyAddedTextLen: number, midTag: string }): [string, string] => {
|
||||
|
||||
/* ------------- summary of the regex -------------
|
||||
[optional ` | `` | ```]
|
||||
|
|
@ -126,23 +138,17 @@ export const extractCodeFromFIM = ({ text, midTag }: { text: string, midTag: str
|
|||
|
||||
const pm = new SurroundingsRemover(text)
|
||||
|
||||
console.log('ORIGIINAL CODE', text)
|
||||
|
||||
pm.removeCodeBlock()
|
||||
|
||||
console.log('D', pm.i, pm.j)
|
||||
|
||||
|
||||
const foundMid = pm.removePrefix(`<${midTag}>`)
|
||||
console.log('E', midTag, pm.i, pm.j)
|
||||
|
||||
if (foundMid) {
|
||||
pm.removeSuffix(`</${midTag}>`)
|
||||
console.log('F', pm.i, pm.j)
|
||||
|
||||
}
|
||||
const s = pm.value()
|
||||
return s
|
||||
const actual = pm.actualRecentlyAdded(recentlyAddedTextLen)
|
||||
|
||||
return [s, actual]
|
||||
|
||||
|
||||
// // const regex = /[\s\S]*?(?:`{1,3}\s*([a-zA-Z_]+[\w]*)?[\s\S]*?)?<MID>([\s\S]*?)(?:<\/MID>|`{1,3}|$)/;
|
||||
|
|
|
|||
10
src/vs/workbench/contrib/void/browser/helpers/readFile.ts
Normal file
10
src/vs/workbench/contrib/void/browser/helpers/readFile.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { URI } from '../../../../../base/common/uri'
|
||||
import { EndOfLinePreference } from '../../../../../editor/common/model'
|
||||
import { IModelService } from '../../../../../editor/common/services/model.js'
|
||||
|
||||
// read files from VSCode
|
||||
export const VSReadFile = async (modelService: IModelService, uri: URI): Promise<string | null> => {
|
||||
const model = modelService.getModel(uri)
|
||||
if (!model) return null
|
||||
return model.getValue(EndOfLinePreference.LF)
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js';
|
||||
import { CodeSelection } from '../threadHistoryService.js';
|
||||
import { CodeSelection } from '../chatThreadService.js';
|
||||
|
||||
export const chat_systemMessage = `\
|
||||
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
|
||||
|
|
@ -341,32 +341,18 @@ export const defaultFimTags: FimTagsType = {
|
|||
midTag: 'SELECTION',
|
||||
}
|
||||
|
||||
export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage, fimTags, ollamaStyleFIM, language }:
|
||||
{ selection: string, prefix: string, suffix: string, userMessage: string, ollamaStyleFIM: boolean, fimTags: FimTagsType, language: string }) => {
|
||||
export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage, fimTags, isOllamaFIM, language }:
|
||||
{
|
||||
selection: string, prefix: string, suffix: string, userMessage: string, fimTags: FimTagsType, language: string,
|
||||
isOllamaFIM: false, // we require this be false for clarity
|
||||
}) => {
|
||||
const { preTag, sufTag, midTag } = fimTags
|
||||
|
||||
|
||||
|
||||
if (ollamaStyleFIM) {
|
||||
// const preTag = 'PRE'
|
||||
// const sufTag = 'SUF'
|
||||
// const midTag = 'MID'
|
||||
return `\
|
||||
<${preTag}>
|
||||
/* Original Selection:
|
||||
${selection}*/
|
||||
/* Instructions:
|
||||
${userMessage}*/
|
||||
${prefix}</${preTag}>
|
||||
<${sufTag}>${suffix}</${sufTag}>
|
||||
<${midTag}>`
|
||||
}
|
||||
// prompt the model artifically on how to do FIM
|
||||
else {
|
||||
// const preTag = 'BEFORE'
|
||||
// const sufTag = 'AFTER'
|
||||
// const midTag = 'SELECTION'
|
||||
return `\
|
||||
// const preTag = 'BEFORE'
|
||||
// const sufTag = 'AFTER'
|
||||
// const midTag = 'SELECTION'
|
||||
return `\
|
||||
The user is selecting this code as their SELECTION:
|
||||
\`\`\` ${language}
|
||||
<${midTag}>${selection}</${midTag}>
|
||||
|
|
@ -377,12 +363,12 @@ ${userMessage}
|
|||
|
||||
Please edit the SELECTION following the user's INSTRUCTIONS, and return the edited selection.
|
||||
|
||||
Note that the SELECTION has code that comes before it. This code is indicated with <${preTag}>...before<${preTag}/>.
|
||||
Note also that the SELECTION has code that comes after it. This code is indicated with <${sufTag}>...after<${sufTag}/>.
|
||||
Note that the SELECTION has code that comes before it. This code is indicated with <${preTag}>...before</${preTag}>.
|
||||
Note also that the SELECTION has code that comes after it. This code is indicated with <${sufTag}>...after</${sufTag}>.
|
||||
|
||||
Instructions:
|
||||
1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_selection<${midTag}/>. Do not give any explanation before or after this. ONLY output this format, nothing more.
|
||||
2. You may ONLY CHANGE the original SELECTION, and NOT the content in the <${preTag}>...<${preTag}/> or <${sufTag}>...<${sufTag}/> tags.
|
||||
1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_selection</${midTag}>. Do NOT output any text or explanations before or after this.
|
||||
2. You may ONLY CHANGE the original SELECTION, and NOT the content in the <${preTag}>...</${preTag}> or <${sufTag}>...</${sufTag}> tags.
|
||||
3. Make sure all brackets in the new selection are balanced the same as in the original selection.
|
||||
4. Be careful not to duplicate or remove variables, comments, or other syntax by mistake.
|
||||
|
||||
|
|
@ -390,8 +376,7 @@ Given the code:
|
|||
<${preTag}>${prefix}</${preTag}>
|
||||
<${sufTag}>${suffix}</${sufTag}>
|
||||
|
||||
Return only the completion block of code (of the form \`\`\` ${language}\n <${midTag}>...new_selection<${midTag}/>\`\`\`):`
|
||||
}
|
||||
Return only the completion block of code (of the form \`\`\` ${language}\n <${midTag}>...new_selection</${midTag}>\`\`\`):`
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,10 +12,12 @@ import { ICodeEditorService } from '../../../../editor/browser/services/codeEdit
|
|||
import { IInlineDiffsService } from './inlineDiffsService.js';
|
||||
import { roundRangeToLines } from './sidebarActions.js';
|
||||
import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
|
||||
|
||||
export type QuickEditPropsType = {
|
||||
diffareaid: number,
|
||||
initStreamingDiffZoneId: number | null,
|
||||
textAreaRef: (ref: HTMLTextAreaElement | null) => void;
|
||||
onChangeHeight: (height: number) => void;
|
||||
onChangeText: (text: string) => void;
|
||||
|
|
@ -36,10 +38,11 @@ registerAction2(class extends Action2 {
|
|||
) {
|
||||
super({
|
||||
id: VOID_CTRL_K_ACTION_ID,
|
||||
title: 'Void: Quick Edit',
|
||||
f1: true,
|
||||
title: localize2('voidQuickEditAction', 'Void: Quick Edit'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyK,
|
||||
weight: KeybindingWeight.BuiltinExtension,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -60,9 +63,6 @@ registerAction2(class extends Action2 {
|
|||
|
||||
const { startLineNumber: startLine, endLineNumber: endLine } = selection
|
||||
|
||||
// deselect - clear selection
|
||||
editor.setSelection({ startLineNumber: startLine, endLineNumber: startLine, startColumn: 1, endColumn: 1 })
|
||||
|
||||
const inlineDiffsService = accessor.get(IInlineDiffsService)
|
||||
inlineDiffsService.addCtrlKZone({ startLine, endLine, editor })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,23 +9,21 @@ import { VoidCodeEditor, VoidCodeEditorProps } from '../util/inputs.js';
|
|||
|
||||
|
||||
export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHover?: React.ReactNode } & VoidCodeEditorProps) => {
|
||||
|
||||
const isSingleLine = !codeEditorProps.initValue.includes('\n')
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`relative group w-full overflow-hidden`}>
|
||||
|
||||
<div className="relative group w-full overflow-hidden">
|
||||
{buttonsOnHover === null ? null : (
|
||||
<div className="z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
|
||||
<div className={`flex space-x-2 ${isSingleLine ? '' : 'p-2'}`}>
|
||||
<div className={`z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200 ${isSingleLine ? 'h-full flex items-center' : ''
|
||||
}`}>
|
||||
<div className={`flex space-x-1 ${isSingleLine ? 'pr-2' : 'p-2'}`}>
|
||||
{buttonsOnHover}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<VoidCodeEditor {...codeEditorProps} />
|
||||
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,15 +4,18 @@
|
|||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSettingsState, useSidebarState, useThreadsState, useQuickEditState, useAccessor } from '../util/services.js';
|
||||
import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor } from '../util/services.js';
|
||||
import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
|
||||
import { QuickEditPropsType } from '../../../quickEditActions.js';
|
||||
import { ButtonStop, ButtonSubmit, IconX } from '../sidebar-tsx/SidebarChat.js';
|
||||
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js';
|
||||
import { useRefState } from '../util/helpers.js';
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
|
||||
|
||||
export const QuickEditChat = ({
|
||||
diffareaid,
|
||||
initStreamingDiffZoneId,
|
||||
onChangeHeight,
|
||||
onChangeText: onChangeText_,
|
||||
textAreaRef: textAreaRef_,
|
||||
|
|
@ -43,39 +46,41 @@ export const QuickEditChat = ({
|
|||
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions
|
||||
const isDisabled = instructionsAreEmpty
|
||||
|
||||
const currentlyStreamingIdRef = useRef<number | undefined>(undefined)
|
||||
const [isStreaming, setIsStreaming] = useState(false)
|
||||
const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState<number | null>(initStreamingDiffZoneId)
|
||||
const isStreaming = currStreamingDiffZoneRef.current !== null
|
||||
|
||||
const onSubmit = useCallback((e: FormEvent) => {
|
||||
if (isDisabled) return
|
||||
if (currentlyStreamingIdRef.current !== undefined) return
|
||||
if (currStreamingDiffZoneRef.current !== null) return
|
||||
textAreaFnsRef.current?.disable()
|
||||
|
||||
const instructions = textAreaRef.current?.value ?? ''
|
||||
currentlyStreamingIdRef.current = inlineDiffsService.startApplying({
|
||||
const id = inlineDiffsService.startApplying({
|
||||
featureName: 'Ctrl+K',
|
||||
diffareaid: diffareaid,
|
||||
userMessage: instructions,
|
||||
})
|
||||
setIsStreaming(true)
|
||||
}, [isDisabled, inlineDiffsService, diffareaid])
|
||||
setCurrentlyStreamingDiffZone(id ?? null)
|
||||
}, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, inlineDiffsService, diffareaid])
|
||||
|
||||
const onInterrupt = useCallback(() => {
|
||||
if (currentlyStreamingIdRef.current !== undefined)
|
||||
inlineDiffsService.interruptStreaming(currentlyStreamingIdRef.current)
|
||||
if (currStreamingDiffZoneRef.current === null) return
|
||||
inlineDiffsService.interruptStreaming(currStreamingDiffZoneRef.current)
|
||||
setCurrentlyStreamingDiffZone(null)
|
||||
textAreaFnsRef.current?.enable()
|
||||
setIsStreaming(false)
|
||||
}, [inlineDiffsService])
|
||||
}, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, inlineDiffsService])
|
||||
|
||||
|
||||
const onX = useCallback(() => {
|
||||
onInterrupt()
|
||||
inlineDiffsService.removeCtrlKZone({ diffareaid })
|
||||
}, [inlineDiffsService, diffareaid])
|
||||
|
||||
useScrollbarStyles(sizerRef)
|
||||
|
||||
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel()
|
||||
|
||||
return <div ref={sizerRef} className='py-2 w-full max-w-xl'>
|
||||
return <div ref={sizerRef} style={{ maxWidth: 500 }} className={`py-2 w-full`}>
|
||||
<form
|
||||
// copied from SidebarChat.tsx
|
||||
className={`
|
||||
|
|
@ -92,27 +97,17 @@ export const QuickEditChat = ({
|
|||
|
||||
{/* // this div is used to position the input box properly */}
|
||||
<div
|
||||
className={`w-full z-[999] relative
|
||||
@@[&_textarea]:!void-bg-transparent
|
||||
@@[&_textarea]:!void-outline-none
|
||||
@@[&_textarea]:!void-text-vscode-input-fg
|
||||
@@[&_div.monaco-inputbox]:!void-border-none
|
||||
@@[&_div.monaco-inputbox]:!void-outline-none`}
|
||||
className={`w-full z-[999] relative`}
|
||||
>
|
||||
<div className='flex flex-row items-center justify-between items-end gap-1'>
|
||||
|
||||
{/* input */}
|
||||
<div // copied from SidebarChat.tsx
|
||||
className={`w-full
|
||||
@@[&_textarea]:!void-bg-transparent
|
||||
@@[&_textarea]:!void-outline-none
|
||||
@@[&_textarea]:!void-text-vscode-input-fg
|
||||
@@[&_div.monaco-inputbox]:!void-outline-none
|
||||
`}
|
||||
className={`w-full`}
|
||||
>
|
||||
{/* text input */}
|
||||
<VoidInputBox2
|
||||
|
||||
className='px-1'
|
||||
initValue={initText}
|
||||
|
||||
ref={useCallback((r: HTMLTextAreaElement | null) => {
|
||||
|
|
@ -129,7 +124,8 @@ export const QuickEditChat = ({
|
|||
|
||||
fnsRef={textAreaFnsRef}
|
||||
|
||||
placeholder={`${keybindingString} to select`}
|
||||
placeholder={`Enter instructions...`}
|
||||
// ${keybindingString} to select.
|
||||
|
||||
onChangeText={useCallback((newStr: string) => {
|
||||
setInstructionsAreEmpty(!newStr)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { errorDetails } from '../../../../../../../platform/void/common/llmMessa
|
|||
|
||||
|
||||
export const ErrorDisplay = ({
|
||||
message,
|
||||
message:message_,
|
||||
fullError,
|
||||
onDismiss,
|
||||
showDismiss,
|
||||
|
|
@ -23,6 +23,8 @@ export const ErrorDisplay = ({
|
|||
|
||||
const details = errorDetails(fullError)
|
||||
|
||||
const message = message_ === 'TypeError: fetch failed' ? 'TypeError: fetch failed. This likely means you specified the wrong endpoint in Void Settings.' : message_
|
||||
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 overflow-auto`}>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export const Sidebar = ({ className }: { className: string }) => {
|
|||
<div
|
||||
// default background + text styles for sidebar
|
||||
className={`
|
||||
w-full h-full py-2
|
||||
w-full h-full
|
||||
bg-void-bg-2
|
||||
text-void-fg-1
|
||||
`}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
|
||||
import { useAccessor, useSidebarState, useThreadsState } from '../util/services.js';
|
||||
import { ChatMessage, CodeSelection, CodeStagingSelection, IThreadHistoryService } from '../../../threadHistoryService.js';
|
||||
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState } from '../util/services.js';
|
||||
import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../chatThreadService.js';
|
||||
|
||||
import { BlockCode } from '../markdown/BlockCode.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
|
|
@ -18,7 +18,7 @@ import { ErrorDisplay } from './ErrorDisplay.js';
|
|||
import { OnError, ServiceSendLLMMessageParams } from '../../../../../../../platform/void/common/llmMessageTypes.js';
|
||||
import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
import { TextAreaFns, VoidCodeEditorProps, VoidInputBox2 } from '../util/inputs.js';
|
||||
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { ModelDropdown, WarningBox } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { chat_systemMessage, chat_prompt } from '../../../prompt/prompts.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { ILLMMessageService } from '../../../../../../../platform/void/common/llmMessageService.js';
|
||||
|
|
@ -29,6 +29,7 @@ import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
|||
import { ArrowBigLeftDash, CopyX, Delete, FileX2, SquareX, X } from 'lucide-react';
|
||||
import { filenameToVscodeLanguage } from '../../../helpers/detectLanguage.js';
|
||||
import { Pencil } from 'lucide-react'
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
||||
|
||||
|
||||
export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
|
||||
|
|
@ -177,8 +178,8 @@ export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Re
|
|||
|
||||
return <button
|
||||
type='button'
|
||||
className={`rounded-full flex-shrink-0 flex-grow-0 cursor-pointer flex items-center justify-center
|
||||
${disabled ? 'bg-vscode-disabled-fg' : 'bg-white'}
|
||||
className={`rounded-full flex-shrink-0 flex-grow-0 flex items-center justify-center
|
||||
${disabled ? 'bg-vscode-disabled-fg cursor-default' : 'bg-white cursor-pointer'}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
|
|
@ -250,13 +251,6 @@ const ScrollToBottomContainer = ({ children, className, style, scrollContainerRe
|
|||
};
|
||||
|
||||
|
||||
// read files from VSCode
|
||||
const VSReadFile = async (modelService: IModelService, uri: URI): Promise<string | null> => {
|
||||
const model = modelService.getModel(uri)
|
||||
if (!model) return null
|
||||
return model.getValue(EndOfLinePreference.LF)
|
||||
}
|
||||
|
||||
|
||||
const getBasename = (pathStr: string) => {
|
||||
// 'unixify' path
|
||||
|
|
@ -266,9 +260,9 @@ const getBasename = (pathStr: string) => {
|
|||
}
|
||||
|
||||
export const SelectedFiles = (
|
||||
{ type, selections, setStaging }:
|
||||
| { type: 'past', selections: CodeSelection[] | null; setStaging?: undefined }
|
||||
| { type: 'staging', selections: CodeStagingSelection[] | null; setStaging: ((files: CodeStagingSelection[]) => void) }
|
||||
{ type, selections, setSelections, showProspectiveSelections }:
|
||||
| { type: 'past', selections: CodeSelection[]; setSelections?: undefined, showProspectiveSelections?: undefined }
|
||||
| { type: 'staging', selections: CodeStagingSelection[]; setSelections: ((newSelections: CodeStagingSelection[]) => void), showProspectiveSelections?: boolean }
|
||||
) => {
|
||||
|
||||
// index -> isOpened
|
||||
|
|
@ -280,85 +274,123 @@ export const SelectedFiles = (
|
|||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
// state for tracking prospective files
|
||||
const { currentUri } = useUriState()
|
||||
const [recentUris, setRecentUris] = useState<URI[]>([])
|
||||
const maxRecentUris = 10
|
||||
const maxProspectiveFiles = 3
|
||||
useEffect(() => { // handle recent files
|
||||
if (!currentUri) return
|
||||
setRecentUris(prev => {
|
||||
const withoutCurrent = prev.filter(uri => uri.fsPath !== currentUri.fsPath) // remove duplicates
|
||||
const withCurrent = [currentUri, ...withoutCurrent]
|
||||
return withCurrent.slice(0, maxRecentUris)
|
||||
})
|
||||
}, [currentUri])
|
||||
let prospectiveSelections: CodeStagingSelection[] = []
|
||||
if (type === 'staging' && showProspectiveSelections) { // handle prospective files
|
||||
// add a prospective file if type === 'staging' and if the user is in a file, and if the file is not selected yet
|
||||
prospectiveSelections = recentUris
|
||||
.filter(uri => !selections.find(s => s.range === null && s.fileURI.fsPath === uri.fsPath))
|
||||
.slice(0, maxProspectiveFiles)
|
||||
.map(uri => ({
|
||||
type: 'File',
|
||||
fileURI: uri,
|
||||
selectionStr: null,
|
||||
range: null,
|
||||
}))
|
||||
}
|
||||
|
||||
const allSelections = [...selections, ...prospectiveSelections]
|
||||
|
||||
if (allSelections.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
!!selections && selections.length !== 0 && (
|
||||
<div
|
||||
className='flex items-center flex-wrap gap-0.5 text-left relative'
|
||||
>
|
||||
{selections.map((selection, i) => {
|
||||
<div className='flex items-center flex-wrap text-left relative'>
|
||||
|
||||
const isThisSelectionOpened = !!(selection.selectionStr && selectionIsOpened[i])
|
||||
const isThisSelectionAFile = selection.selectionStr === null
|
||||
{allSelections.map((selection, i) => {
|
||||
|
||||
return <div key={i} // container for `selectionSummary` and `selectionText`
|
||||
className={`${isThisSelectionOpened ? 'w-full' : ''}`}
|
||||
const isThisSelectionOpened = !!(selection.selectionStr && selectionIsOpened[i])
|
||||
const isThisSelectionAFile = selection.selectionStr === null
|
||||
const isThisSelectionProspective = i > selections.length - 1
|
||||
|
||||
const thisKey = `${isThisSelectionProspective}-${i}-${selections.length}`
|
||||
|
||||
const selectionHTML = (<div key={thisKey} // container for `selectionSummary` and `selectionText`
|
||||
className={`
|
||||
${isThisSelectionOpened ? 'w-full' : ''}
|
||||
`}
|
||||
>
|
||||
{/* selection summary */}
|
||||
<div // container for item and its delete button (if it's last)
|
||||
className='flex items-center gap-1 mr-0.5 mb-0.5'
|
||||
>
|
||||
{/* selection summary */}
|
||||
<div // container for delete button
|
||||
className='flex items-center gap-0.5'
|
||||
>
|
||||
<div // styled summary box
|
||||
className={`flex items-center gap-0.5 relative
|
||||
rounded-md px-1
|
||||
<div // styled summary box
|
||||
className={`flex items-center gap-0.5 relative
|
||||
px-1
|
||||
w-fit h-fit
|
||||
select-none
|
||||
bg-void-bg-3 hover:brightness-95
|
||||
text-void-fg-1 text-xs text-nowrap
|
||||
border rounded-xs ${isClearHovered ? 'border-void-border-1' : 'border-void-border-2'}
|
||||
${isThisSelectionProspective ? 'bg-void-1 text-void-fg-3 opacity-80' : 'bg-void-bg-3 hover:brightness-95 text-void-fg-1'}
|
||||
text-xs text-nowrap
|
||||
border rounded-sm ${isClearHovered && !isThisSelectionProspective ? 'border-void-border-1' : 'border-void-border-2'} hover:border-void-border-1
|
||||
transition-all duration-150`}
|
||||
onClick={() => {
|
||||
// open the file if it is a file
|
||||
if (isThisSelectionAFile) {
|
||||
commandService.executeCommand('vscode.open', selection.fileURI, {
|
||||
preview: true,
|
||||
// preserveFocus: false,
|
||||
});
|
||||
} else {
|
||||
// open the selection if it is a text-selection
|
||||
setSelectionIsOpened(s => {
|
||||
const newS = [...s]
|
||||
newS[i] = !newS[i]
|
||||
return newS
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{/* file name */}
|
||||
{getBasename(selection.fileURI.fsPath)}
|
||||
{/* selection range */}
|
||||
{!isThisSelectionAFile ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
|
||||
</span>
|
||||
onClick={() => {
|
||||
if (isThisSelectionProspective) { // add prospective selection to selections
|
||||
if (type !== 'staging') return; // (never)
|
||||
setSelections([...selections, selection as CodeStagingSelection])
|
||||
|
||||
{/* X button */}
|
||||
{type === 'staging' &&
|
||||
<span
|
||||
className='cursor-pointer hover:brightness-95 rounded-md z-1'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // don't open/close selection
|
||||
if (type !== 'staging') return;
|
||||
setStaging([...selections.slice(0, i), ...selections.slice(i + 1)])
|
||||
setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
|
||||
}}
|
||||
>
|
||||
<IconX size={16} className="p-[2px] stroke-[3]" />
|
||||
</span>}
|
||||
} else if (isThisSelectionAFile) { // open files
|
||||
commandService.executeCommand('vscode.open', selection.fileURI, {
|
||||
preview: true,
|
||||
// preserveFocus: false,
|
||||
});
|
||||
} else { // show text
|
||||
setSelectionIsOpened(s => {
|
||||
const newS = [...s]
|
||||
newS[i] = !newS[i]
|
||||
return newS
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{/* file name */}
|
||||
{getBasename(selection.fileURI.fsPath)}
|
||||
{/* selection range */}
|
||||
{!isThisSelectionAFile ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
|
||||
</span>
|
||||
|
||||
{/* X button */}
|
||||
{type === 'staging' && !isThisSelectionProspective &&
|
||||
<span
|
||||
className='cursor-pointer z-1'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // don't open/close selection
|
||||
if (type !== 'staging') return;
|
||||
setSelections([...selections.slice(0, i), ...selections.slice(i + 1)])
|
||||
setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
|
||||
}}
|
||||
>
|
||||
<IconX size={10} className="stroke-[2]" />
|
||||
</span>}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* clear all selections button */}
|
||||
{type !== 'staging' || selections.length === 0 || i !== selections.length - 1
|
||||
? null
|
||||
: <div key={i} className={`flex items-center gap-0.5 ${isThisSelectionOpened ? 'w-full' : ''}`}>
|
||||
<div
|
||||
className='rounded-md'
|
||||
onMouseEnter={() => setIsClearHovered(true)}
|
||||
onMouseLeave={() => setIsClearHovered(false)}
|
||||
>
|
||||
<Delete
|
||||
size={16}
|
||||
className={`stroke-[1]
|
||||
{/* clear all selections button */}
|
||||
{type !== 'staging' || selections.length === 0 || i !== selections.length - 1
|
||||
? null
|
||||
: <div className={`flex items-center ${isThisSelectionOpened ? 'w-full' : ''}`}>
|
||||
<div
|
||||
className='rounded-md'
|
||||
onMouseEnter={() => setIsClearHovered(true)}
|
||||
onMouseLeave={() => setIsClearHovered(false)}
|
||||
>
|
||||
<Delete
|
||||
size={16}
|
||||
className={`stroke-[1]
|
||||
stroke-void-fg-1
|
||||
fill-void-bg-3
|
||||
opacity-40
|
||||
|
|
@ -366,58 +398,104 @@ export const SelectedFiles = (
|
|||
transition-all duration-150
|
||||
cursor-pointer
|
||||
`}
|
||||
onClick={() => { setStaging([]) }}
|
||||
/>
|
||||
</div>
|
||||
onClick={() => { setSelections([]) }}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{/* selection text */}
|
||||
{isThisSelectionOpened &&
|
||||
<div
|
||||
className='w-full px-1 rounded-sm border-vscode-editor-border'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // don't focus input box
|
||||
}}
|
||||
>
|
||||
<BlockCode
|
||||
initValue={selection.selectionStr!}
|
||||
language={filenameToVscodeLanguage(selection.fileURI.path)}
|
||||
maxHeight={100}
|
||||
showScrollbars={true}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{/* selection text */}
|
||||
{isThisSelectionOpened &&
|
||||
<div
|
||||
className='w-full px-1 rounded-sm border-vscode-editor-border'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // don't focus input box
|
||||
}}
|
||||
>
|
||||
<BlockCode
|
||||
initValue={selection.selectionStr!}
|
||||
language={filenameToVscodeLanguage(selection.fileURI.path)}
|
||||
maxHeight={200}
|
||||
showScrollbars={true}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>)
|
||||
|
||||
})}
|
||||
return <Fragment key={thisKey}>
|
||||
{selections.length > 0 && i === selections.length &&
|
||||
<div className='w-full'></div> // divider between `selections` and `prospectiveSelections`
|
||||
}
|
||||
{selectionHTML}
|
||||
</Fragment>
|
||||
|
||||
})}
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ChatBubble = ({ chatMessage, isLoading }: {
|
||||
chatMessage: ChatMessage,
|
||||
isLoading?: boolean,
|
||||
}) => {
|
||||
const ChatBubble_ = ({ isEditMode, isLoading, children, role }: { role: ChatMessage['role'], children: React.ReactNode, isLoading: boolean, isEditMode: boolean }) => {
|
||||
|
||||
return <div
|
||||
// align chatbubble accoridng to role
|
||||
className={`
|
||||
relative
|
||||
${isEditMode ? 'px-2 w-full max-w-full'
|
||||
: role === 'user' ? `px-2 self-end w-fit max-w-full`
|
||||
: role === 'assistant' ? `px-2 self-start w-full max-w-full` : ''
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
// style chatbubble according to role
|
||||
className={`
|
||||
text-left space-y-2 rounded-lg
|
||||
overflow-x-auto max-w-full
|
||||
${role === 'user' ? 'p-2 bg-void-bg-1 text-void-fg-1' : 'px-2'}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
{isLoading && <IconLoading className='opacity-50 text-sm' />}
|
||||
</div>
|
||||
|
||||
{/* edit button */}
|
||||
{/* {role === 'user' &&
|
||||
<Pencil
|
||||
size={16}
|
||||
className={`
|
||||
absolute top-0 right-2
|
||||
translate-x-0 -translate-y-0
|
||||
cursor-pointer z-1
|
||||
`}
|
||||
onClick={() => { setIsEditMode(v => !v); }}
|
||||
/>
|
||||
} */}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLoading?: boolean, }) => {
|
||||
|
||||
const role = chatMessage.role
|
||||
|
||||
// edit mode state
|
||||
const [isEditMode, setIsEditMode] = useState(false)
|
||||
|
||||
if (!chatMessage.displayContent)
|
||||
|
||||
if (!chatMessage.content) { // don't show if empty
|
||||
return null
|
||||
}
|
||||
|
||||
let chatbubbleContents: React.ReactNode
|
||||
|
||||
if (role === 'user') {
|
||||
chatbubbleContents = <>
|
||||
<SelectedFiles type='past' selections={chatMessage.selections} />
|
||||
<SelectedFiles type='past' selections={chatMessage.selections || []} />
|
||||
{chatMessage.displayContent}
|
||||
|
||||
{/* {!isEditMode ? chatMessage.displayContent : <></>} */}
|
||||
|
|
@ -437,44 +515,12 @@ const ChatBubble = ({ chatMessage, isLoading }: {
|
|||
</>
|
||||
}
|
||||
else if (role === 'assistant') {
|
||||
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent} />
|
||||
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent ?? ''} />
|
||||
}
|
||||
|
||||
return <div
|
||||
// align chatbubble accoridng to role
|
||||
className={`
|
||||
relative
|
||||
${isEditMode ? 'px-2 w-full max-w-full'
|
||||
: role === 'user' ? `px-2 self-end w-fit max-w-full`
|
||||
: role === 'assistant' ? `px-2 self-start w-full max-w-full` : ''
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
// style chatbubble according to role
|
||||
className={`
|
||||
p-2 text-left space-y-2 rounded-lg
|
||||
overflow-x-auto max-w-full
|
||||
${role === 'user' ? 'bg-void-bg-1 text-void-fg-1' : ''}
|
||||
`}
|
||||
>
|
||||
{chatbubbleContents}
|
||||
{isLoading && <IconLoading className='opacity-50 text-sm' />}
|
||||
</div>
|
||||
|
||||
{/* edit button */}
|
||||
{/* {role === 'user' &&
|
||||
<Pencil
|
||||
size={16}
|
||||
className={`
|
||||
absolute top-0 right-2
|
||||
translate-x-0 -translate-y-0
|
||||
cursor-pointer z-1
|
||||
`}
|
||||
onClick={() => { setIsEditMode(v => !v); }}
|
||||
/>
|
||||
} */}
|
||||
</div>
|
||||
return <ChatBubble_ role={role} isEditMode={isEditMode} isLoading={!!isLoading}>
|
||||
{chatbubbleContents}
|
||||
</ChatBubble_>
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -484,7 +530,8 @@ export const SidebarChat = () => {
|
|||
const textAreaFnsRef = useRef<TextAreaFns | null>(null)
|
||||
|
||||
const accessor = useAccessor()
|
||||
const modelService = accessor.get('IModelService')
|
||||
// const modelService = accessor.get('IModelService')
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
// ----- HIGHER STATE -----
|
||||
// sidebar state
|
||||
|
|
@ -498,24 +545,24 @@ export const SidebarChat = () => {
|
|||
return () => disposables.forEach(d => d.dispose())
|
||||
}, [sidebarStateService, textAreaRef])
|
||||
|
||||
const { currentTab, isHistoryOpen } = useSidebarState()
|
||||
const { isHistoryOpen } = useSidebarState()
|
||||
|
||||
// threads state
|
||||
const threadsState = useThreadsState()
|
||||
const threadsStateService = accessor.get('IThreadHistoryService')
|
||||
const chatThreadsState = useChatThreadsState()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
|
||||
const llmMessageService = accessor.get('ILLMMessageService')
|
||||
const currentThread = chatThreadsService.getCurrentThread()
|
||||
const previousMessages = currentThread?.messages ?? []
|
||||
const selections = chatThreadsState.currentStagingSelections
|
||||
|
||||
// stream state
|
||||
const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
|
||||
const isStreaming = !!currThreadStreamState?.streamingToken
|
||||
const latestError = currThreadStreamState?.error
|
||||
const messageSoFar = currThreadStreamState?.messageSoFar
|
||||
|
||||
// ----- SIDEBAR CHAT state (local) -----
|
||||
|
||||
// state of chat
|
||||
const [messageStream, setMessageStream] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const latestRequestIdRef = useRef<string | null>(null)
|
||||
|
||||
const [latestError, setLatestError] = useState<Parameters<OnError>[0] | null>(null)
|
||||
|
||||
|
||||
// state of current message
|
||||
const initVal = ''
|
||||
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!initVal)
|
||||
|
|
@ -527,110 +574,27 @@ export const SidebarChat = () => {
|
|||
|
||||
useScrollbarStyles(sidebarRef)
|
||||
|
||||
|
||||
const onSubmit = async () => {
|
||||
|
||||
if (isDisabled) return
|
||||
if (isLoading) return
|
||||
|
||||
const currSelns = threadsStateService.state._currentStagingSelections ?? []
|
||||
const selections = !currSelns ? null : await Promise.all(
|
||||
currSelns.map(async (sel) => ({ ...sel, content: await VSReadFile(modelService, sel.fileURI) }))
|
||||
).then(
|
||||
(files) => files.filter(file => file.content !== null) as CodeSelection[]
|
||||
)
|
||||
|
||||
|
||||
// // TODO don't save files to the thread history
|
||||
// const selectedSnippets = currSelns.filter(sel => sel.selectionStr !== null)
|
||||
// const selectedFiles = await Promise.all( // do not add these to the context history
|
||||
// currSelns.filter(sel => sel.selectionStr === null)
|
||||
// .map(async (sel) => ({ ...sel, content: await VSReadFile(modelService, sel.fileURI) }))
|
||||
// ).then(
|
||||
// (files) => files.filter(file => file.content !== null) as CodeSelection[]
|
||||
// )
|
||||
// const contextToSendToLLM = ''
|
||||
// const contextToAddToHistory = ''
|
||||
|
||||
|
||||
// add system message to chat history
|
||||
const systemPromptElt: ChatMessage = { role: 'system', content: chat_systemMessage }
|
||||
threadsStateService.addMessageToCurrentThread(systemPromptElt)
|
||||
|
||||
// add user's message to chat history
|
||||
const instructions = textAreaRef.current?.value ?? ''
|
||||
const userHistoryElt: ChatMessage = { role: 'user', content: chat_prompt(instructions, selections), displayContent: instructions, selections: selections }
|
||||
threadsStateService.addMessageToCurrentThread(userHistoryElt)
|
||||
|
||||
const currentThread = threadsStateService.getCurrentThread(threadsStateService.state) // the the instant state right now, don't wait for the React state
|
||||
if (isStreaming) return
|
||||
|
||||
// send message to LLM
|
||||
setIsLoading(true) // must come before message is sent so onError will work
|
||||
setLatestError(null)
|
||||
if (textAreaRef.current) {
|
||||
textAreaFnsRef.current?.setValue('') // triggers onChange
|
||||
textAreaRef.current.blur();
|
||||
}
|
||||
|
||||
const object: ServiceSendLLMMessageParams = {
|
||||
logging: { loggingName: 'Chat' },
|
||||
messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content || '(null)' })),],
|
||||
onText: ({ newText, fullText }) => setMessageStream(fullText),
|
||||
onFinalMessage: ({ fullText: content }) => {
|
||||
console.log('chat: running final message')
|
||||
|
||||
// add assistant's message to chat history, and clear selection
|
||||
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null }
|
||||
threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
|
||||
setMessageStream(null)
|
||||
setIsLoading(false)
|
||||
},
|
||||
onError: ({ message, fullError }) => {
|
||||
console.log('chat: running error', message, fullError)
|
||||
|
||||
// add assistant's message to chat history, and clear selection
|
||||
let content = messageStream ?? ''; // just use the current content
|
||||
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null, }
|
||||
threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
|
||||
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
|
||||
setLatestError({ message, fullError })
|
||||
},
|
||||
featureName: 'Ctrl+L',
|
||||
|
||||
}
|
||||
|
||||
const latestRequestId = llmMessageService.sendLLMMessage(object)
|
||||
latestRequestIdRef.current = latestRequestId
|
||||
|
||||
threadsStateService.setStaging([]) // clear staging
|
||||
const userMessage = textAreaRef.current?.value ?? ''
|
||||
await chatThreadsService.addUserMessageAndStreamResponse(userMessage)
|
||||
|
||||
chatThreadsService.setStaging([]) // clear staging
|
||||
textAreaFnsRef.current?.setValue('')
|
||||
textAreaRef.current?.focus() // focus input after submit
|
||||
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
// abort the LLM call
|
||||
if (latestRequestIdRef.current)
|
||||
llmMessageService.abort(latestRequestIdRef.current)
|
||||
|
||||
// if messageStream was not empty, add it to the history
|
||||
const llmContent = messageStream ?? ''
|
||||
const assistantHistoryElt: ChatMessage = { role: 'assistant', content: llmContent, displayContent: messageStream || null, }
|
||||
threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
|
||||
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
|
||||
const threadId = currentThread.id
|
||||
chatThreadsService.cancelStreaming(threadId)
|
||||
}
|
||||
|
||||
const currentThread = threadsStateService.getCurrentThread(threadsState)
|
||||
|
||||
const selections = threadsState._currentStagingSelections
|
||||
|
||||
const previousMessages = currentThread?.messages ?? []
|
||||
|
||||
// const [_test_messages, _set_test_messages] = useState<string[]>([])
|
||||
|
||||
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_L_ACTION_ID)?.getLabel()
|
||||
|
|
@ -640,7 +604,7 @@ export const SidebarChat = () => {
|
|||
useEffect(() => {
|
||||
if (isHistoryOpen)
|
||||
scrollContainerRef.current?.scrollTo({ top: 0, left: 0 })
|
||||
}, [isHistoryOpen, currentThread?.id])
|
||||
}, [isHistoryOpen, currentThread.id])
|
||||
|
||||
return <div
|
||||
ref={sidebarRef}
|
||||
|
|
@ -658,11 +622,11 @@ export const SidebarChat = () => {
|
|||
scrollContainerRef={scrollContainerRef}
|
||||
className={`
|
||||
w-full h-auto
|
||||
flex flex-col gap-0
|
||||
flex flex-col gap-1
|
||||
overflow-x-hidden
|
||||
overflow-y-auto
|
||||
`}
|
||||
style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - formDimensions.height - 30 }} // the height of the previousMessages is determined by all other heights
|
||||
style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - formDimensions.height - 36 }} // the height of the previousMessages is determined by all other heights
|
||||
>
|
||||
{/* previous messages */}
|
||||
{previousMessages.map((message, i) =>
|
||||
|
|
@ -670,7 +634,22 @@ export const SidebarChat = () => {
|
|||
)}
|
||||
|
||||
{/* message stream */}
|
||||
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream || null }} isLoading={isLoading} />
|
||||
<ChatBubble chatMessage={{ role: 'assistant', content: messageSoFar ?? '', displayContent: messageSoFar || null }} isLoading={isStreaming} />
|
||||
|
||||
|
||||
{/* error message */}
|
||||
{latestError === undefined ? null :
|
||||
<div className='px-2'>
|
||||
<ErrorDisplay
|
||||
message={latestError.message}
|
||||
fullError={latestError.fullError}
|
||||
onDismiss={() => { chatThreadsService.dismissStreamError(currentThread.id) }}
|
||||
showDismiss={true}
|
||||
/>
|
||||
|
||||
<WarningBox className='text-sm my-2 pl-4' onClick={() => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }} text='Open settings' />
|
||||
</div>
|
||||
}
|
||||
|
||||
</ScrollToBottomContainer>
|
||||
|
||||
|
|
@ -682,7 +661,7 @@ export const SidebarChat = () => {
|
|||
<div
|
||||
ref={formRef}
|
||||
className={`
|
||||
flex flex-col gap-2 p-2 relative input text-left shrink-0
|
||||
flex flex-col gap-1 p-2 relative input text-left shrink-0
|
||||
transition-all duration-200
|
||||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
|
|
@ -696,46 +675,16 @@ export const SidebarChat = () => {
|
|||
{/* top row */}
|
||||
<>
|
||||
{/* selections */}
|
||||
{(selections && selections.length !== 0) &&
|
||||
<SelectedFiles type='staging' selections={selections} setStaging={threadsStateService.setStaging.bind(threadsStateService)} />
|
||||
}
|
||||
|
||||
{/* error message */}
|
||||
{latestError === null ? null :
|
||||
<ErrorDisplay
|
||||
message={latestError.message}
|
||||
fullError={latestError.fullError}
|
||||
onDismiss={() => { setLatestError(null) }}
|
||||
showDismiss={true}
|
||||
/>
|
||||
}
|
||||
<SelectedFiles type='staging' selections={selections || []} setSelections={chatThreadsService.setStaging.bind(chatThreadsService)} showProspectiveSelections={previousMessages.length === 0}/>
|
||||
</>
|
||||
|
||||
{/* middle row */}
|
||||
<div
|
||||
className={
|
||||
// // hack to overwrite vscode styles (generated with this code):
|
||||
// `bg-transparent outline-none text-vscode-input-fg min-h-[81px] max-h-[500px]`
|
||||
// .split(' ')
|
||||
// .map(style => `@@[&_textarea]:!void-${style}`) // apply styles to ancestor textarea elements
|
||||
// .join(' ') +
|
||||
// ` outline-none border-none`
|
||||
// .split(' ')
|
||||
// .map(style => `@@[&_div.monaco-inputbox]:!void-${style}`)
|
||||
// .join(' ');
|
||||
`
|
||||
@@[&_textarea]:!void-outline-none
|
||||
@@[&_textarea]:!void-min-h-[81px]
|
||||
@@[&_textarea]:!void-max-h-[500px]
|
||||
@@[&_div.monaco-inputbox]:!void-border-none
|
||||
@@[&_div.monaco-inputbox]:!void-outline-none
|
||||
`
|
||||
}
|
||||
>
|
||||
<div>
|
||||
|
||||
{/* text input */}
|
||||
<VoidInputBox2
|
||||
placeholder={`${keybindingString} to select`}
|
||||
className='min-h-[81px] p-1'
|
||||
placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`}
|
||||
onChangeText={useCallback((newStr: string) => { setInstructionsAreEmpty(!newStr) }, [setInstructionsAreEmpty])}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
|
|
@ -763,7 +712,7 @@ export const SidebarChat = () => {
|
|||
</div>
|
||||
|
||||
{/* submit / stop button */}
|
||||
{isLoading ?
|
||||
{isStreaming ?
|
||||
// stop button
|
||||
<ButtonStop
|
||||
onClick={onAbort}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React from "react";
|
||||
import { useAccessor, useThreadsState } from '../util/services.js';
|
||||
import { IThreadHistoryService } from '../../../threadHistoryService.js';
|
||||
import { useAccessor, useChatThreadsState } from '../util/services.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { IconX } from './SidebarChat.js';
|
||||
|
||||
|
|
@ -20,19 +19,21 @@ const truncate = (s: string) => {
|
|||
|
||||
|
||||
export const SidebarThreadSelector = () => {
|
||||
const threadsState = useThreadsState()
|
||||
const threadsState = useChatThreadsState()
|
||||
|
||||
const accessor = useAccessor()
|
||||
const threadsStateService = accessor.get('IThreadHistoryService')
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
const { allThreads } = threadsState
|
||||
|
||||
// sorted by most recent to least recent
|
||||
const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? -1 : 1)
|
||||
const sortedThreadIds = Object.keys(allThreads ?? {})
|
||||
.sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? -1 : 1)
|
||||
.filter(threadId => allThreads![threadId].messages.length !== 0)
|
||||
|
||||
return (
|
||||
<div className="flex p-2 flex-col mb-2 gap-y-1 max-h-[400px] overflow-y-auto">
|
||||
<div className="flex p-2 flex-col gap-y-1 max-h-[400px] overflow-y-auto">
|
||||
|
||||
<div className="w-full relative flex justify-center items-center">
|
||||
{/* title */}
|
||||
|
|
@ -56,11 +57,11 @@ export const SidebarThreadSelector = () => {
|
|||
|
||||
{sortedThreadIds.length === 0
|
||||
|
||||
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-sm">{`No history found`}</div>
|
||||
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-sm">{`There are no chat threads yet.`}</div>
|
||||
|
||||
: sortedThreadIds.map((threadId) => {
|
||||
if (!allThreads) {
|
||||
return <li key="error" className="text-void-warning">{`No history found`}</li>;
|
||||
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
||||
}
|
||||
|
||||
const pastThread = allThreads[threadId];
|
||||
|
|
@ -93,16 +94,16 @@ export const SidebarThreadSelector = () => {
|
|||
return (
|
||||
<li key={pastThread.id}>
|
||||
<button
|
||||
type='button'
|
||||
type='button'
|
||||
className={`
|
||||
hover:bg-void-bg-1
|
||||
${threadsState._currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
|
||||
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
|
||||
rounded-sm px-2 py-1
|
||||
w-full
|
||||
text-left
|
||||
flex items-center
|
||||
`}
|
||||
onClick={() => threadsStateService.switchToThread(pastThread.id)}
|
||||
onClick={() => chatThreadsService.switchToThread(pastThread.id)}
|
||||
title={new Date(pastThread.createdAt).toLocaleString()}
|
||||
>
|
||||
<div className='truncate'>{`${firstMsg}`}</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import { useCallback, useRef, useState } from 'react'
|
||||
|
||||
|
||||
|
||||
type ReturnType<T> = [
|
||||
{ readonly current: T },
|
||||
(t: T) => void
|
||||
]
|
||||
|
||||
// use this if state might be too slow to catch
|
||||
export const useRefState = <T,>(initVal: T): ReturnType<T> => {
|
||||
const [_, _setState] = useState(false)
|
||||
const ref = useRef<T>(initVal)
|
||||
const setState = useCallback((newVal: T) => {
|
||||
_setState(n => !n) // call rerender
|
||||
ref.current = newVal
|
||||
}, [])
|
||||
return [ref, setState]
|
||||
}
|
||||
|
|
@ -54,11 +54,12 @@ type InputBox2Props = {
|
|||
placeholder: string;
|
||||
multiline: boolean;
|
||||
fnsRef?: { current: null | TextAreaFns };
|
||||
className?: string;
|
||||
onChangeText?: (value: string) => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
onChangeHeight?: (newHeight: number) => void;
|
||||
}
|
||||
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, onKeyDown, onChangeText }, ref) {
|
||||
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onChangeText }, ref) {
|
||||
|
||||
// mirrors whatever is in ref
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
|
|
@ -72,7 +73,7 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
|
||||
if (r.scrollHeight === 0) return requestAnimationFrame(adjustHeight)
|
||||
const h = r.scrollHeight
|
||||
const newHeight = Math.min(h, 500)
|
||||
const newHeight = Math.min(h + 1, 500) // plus one to avoid scrollbar appearing when it shouldn't
|
||||
r.style.height = `${newHeight}px`
|
||||
}, []);
|
||||
|
||||
|
|
@ -114,7 +115,7 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
|
||||
disabled={!isEnabled}
|
||||
|
||||
className="w-full resize-none max-h-[500px] overflow-y-auto"
|
||||
className={`w-full resize-none max-h-[500px] overflow-y-auto text-void-fg-1 placeholder:text-void-fg-3 ${className}`}
|
||||
style={{
|
||||
// defaultInputBoxStyles
|
||||
background: asCssVariable(inputBackground),
|
||||
|
|
@ -299,7 +300,8 @@ export const VoidCustomSelectBox = <T extends any>({
|
|||
options,
|
||||
selectedOption: selectedOption_,
|
||||
onChangeOption,
|
||||
getOptionName,
|
||||
getOptionDropdownName,
|
||||
getOptionDisplayName,
|
||||
getOptionsEqual,
|
||||
className,
|
||||
arrowTouchesText = true,
|
||||
|
|
@ -310,7 +312,8 @@ export const VoidCustomSelectBox = <T extends any>({
|
|||
options: T[];
|
||||
selectedOption?: T;
|
||||
onChangeOption: (newValue: T) => void;
|
||||
getOptionName: (option: T) => string;
|
||||
getOptionDropdownName: (option: T) => string;
|
||||
getOptionDisplayName: (option: T) => string;
|
||||
getOptionsEqual: (a: T, b: T) => boolean;
|
||||
className?: string;
|
||||
arrowTouchesText?: boolean;
|
||||
|
|
@ -420,9 +423,9 @@ export const VoidCustomSelectBox = <T extends any>({
|
|||
aria-hidden="true"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<div key={getOptionName(option)} className="flex items-center whitespace-nowrap">
|
||||
<div key={getOptionDropdownName(option)} className="flex items-center whitespace-nowrap">
|
||||
<div className="w-4" />
|
||||
<span className="px-2">{getOptionName(option)}</span>
|
||||
<span className="px-2">{getOptionDropdownName(option)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -437,7 +440,7 @@ export const VoidCustomSelectBox = <T extends any>({
|
|||
}}
|
||||
>
|
||||
<span className={`max-w-[120px] truncate ${arrowTouchesText ? 'mr-1' : ''}`}>
|
||||
{getOptionName(selectedOption)}
|
||||
{getOptionDisplayName(selectedOption)}
|
||||
</span>
|
||||
<svg
|
||||
className={`size-3 flex-shrink-0 ${arrowTouchesText ? '' : 'ml-auto'}`}
|
||||
|
|
@ -466,7 +469,7 @@ export const VoidCustomSelectBox = <T extends any>({
|
|||
>
|
||||
{options.map((option) => {
|
||||
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
|
||||
const optionName = getOptionName(option);
|
||||
const optionName = getOptionDropdownName(option);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -706,7 +709,7 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
|
|||
onCreateInstance={useCallback((editor: CodeEditorWidget) => {
|
||||
const model = modelOfEditorId[id] ?? modelService.createModel(
|
||||
initValueRef.current, {
|
||||
languageId: languageRef.current ? languageRef.current : '',
|
||||
languageId: languageRef.current ? languageRef.current : 'typescript',
|
||||
onDidChange: (e) => { return { dispose: () => { } } } // no idea why they'd require this
|
||||
})
|
||||
modelRef.current = model
|
||||
|
|
@ -719,6 +722,7 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
|
|||
if (parentNode) {
|
||||
// const height = Math.min(, MAX_HEIGHT);
|
||||
parentNode.style.height = `${height}px`;
|
||||
parentNode.style.maxHeight = `${MAX_HEIGHT}px`;
|
||||
editor.layout();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@
|
|||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { ThreadsState } from '../../../threadHistoryService.js'
|
||||
import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js'
|
||||
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
import { VoidSidebarState } from '../../../sidebarStateService.js'
|
||||
import { VoidSettingsState } from '../../../../../../../platform/void/common/voidSettingsService.js'
|
||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
|
||||
import { VoidUriState } from '../../../voidUriStateService.js';
|
||||
import { VoidQuickEditState } from '../../../quickEditStateService.js'
|
||||
import { RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js'
|
||||
|
||||
|
|
@ -28,9 +29,10 @@ import { ILLMMessageService } from '../../../../../../../platform/void/common/ll
|
|||
import { IRefreshModelService } from '../../../../../../../platform/void/common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../../../../../../../platform/void/common/voidSettingsService.js';
|
||||
import { IInlineDiffsService } from '../../../inlineDiffsService.js';
|
||||
import { IVoidUriStateService } from '../../../voidUriStateService.js';
|
||||
import { IQuickEditStateService } from '../../../quickEditStateService.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { IThreadHistoryService } from '../../../threadHistoryService.js';
|
||||
import { IChatThreadService } from '../../../chatThreadService.js';
|
||||
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
|
||||
import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js'
|
||||
import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'
|
||||
|
|
@ -47,18 +49,25 @@ import { IPathService } from '../../../../../../../workbench/services/path/commo
|
|||
import { IMetricsService } from '../../../../../../../platform/void/common/metricsService.js'
|
||||
|
||||
|
||||
|
||||
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
|
||||
|
||||
// even if React hasn't mounted yet, the variables are always updated to the latest state.
|
||||
// React listens by adding a setState function to these listeners.
|
||||
let uriState: VoidUriState
|
||||
const uriStateListeners: Set<(s: VoidUriState) => void> = new Set()
|
||||
|
||||
let quickEditState: VoidQuickEditState
|
||||
const quickEditStateListeners: Set<(s: VoidQuickEditState) => void> = new Set()
|
||||
|
||||
let sidebarState: VoidSidebarState
|
||||
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
|
||||
|
||||
let threadsState: ThreadsState
|
||||
const threadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
|
||||
let chatThreadsState: ThreadsState
|
||||
const chatThreadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
|
||||
|
||||
let chatThreadsStreamState: ThreadStreamState
|
||||
const chatThreadsStreamStateListeners: Set<(threadId: string) => void> = new Set()
|
||||
|
||||
let settingsState: VoidSettingsState
|
||||
const settingsStateListeners: Set<(s: VoidSettingsState) => void> = new Set()
|
||||
|
|
@ -87,15 +96,25 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
|||
_registerAccessor(accessor)
|
||||
|
||||
const stateServices = {
|
||||
uriStateService: accessor.get(IVoidUriStateService),
|
||||
quickEditStateService: accessor.get(IQuickEditStateService),
|
||||
sidebarStateService: accessor.get(ISidebarStateService),
|
||||
threadsStateService: accessor.get(IThreadHistoryService),
|
||||
chatThreadsStateService: accessor.get(IChatThreadService),
|
||||
settingsStateService: accessor.get(IVoidSettingsService),
|
||||
refreshModelService: accessor.get(IRefreshModelService),
|
||||
themeService: accessor.get(IThemeService),
|
||||
inlineDiffsService: accessor.get(IInlineDiffsService),
|
||||
}
|
||||
|
||||
const { sidebarStateService, quickEditStateService, settingsStateService, threadsStateService, refreshModelService, themeService, } = stateServices
|
||||
const { uriStateService, sidebarStateService, quickEditStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, inlineDiffsService } = stateServices
|
||||
|
||||
uriState = uriStateService.state
|
||||
disposables.push(
|
||||
uriStateService.onDidChangeState(() => {
|
||||
uriState = uriStateService.state
|
||||
uriStateListeners.forEach(l => l(uriState))
|
||||
})
|
||||
)
|
||||
|
||||
quickEditState = quickEditStateService.state
|
||||
disposables.push(
|
||||
|
|
@ -113,11 +132,20 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
|||
})
|
||||
)
|
||||
|
||||
threadsState = threadsStateService.state
|
||||
chatThreadsState = chatThreadsStateService.state
|
||||
disposables.push(
|
||||
threadsStateService.onDidChangeCurrentThread(() => {
|
||||
threadsState = threadsStateService.state
|
||||
threadsStateListeners.forEach(l => l(threadsState))
|
||||
chatThreadsStateService.onDidChangeCurrentThread(() => {
|
||||
chatThreadsState = chatThreadsStateService.state
|
||||
chatThreadsStateListeners.forEach(l => l(chatThreadsState))
|
||||
})
|
||||
)
|
||||
|
||||
// same service, different state
|
||||
chatThreadsStreamState = chatThreadsStateService.streamState
|
||||
disposables.push(
|
||||
chatThreadsStateService.onDidChangeStreamState(({ threadId }) => {
|
||||
chatThreadsStreamState = chatThreadsStateService.streamState
|
||||
chatThreadsStreamStateListeners.forEach(l => l(threadId))
|
||||
})
|
||||
)
|
||||
|
||||
|
|
@ -146,6 +174,7 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
|||
})
|
||||
)
|
||||
|
||||
|
||||
return disposables
|
||||
}
|
||||
|
||||
|
|
@ -164,9 +193,10 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
|||
IRefreshModelService: accessor.get(IRefreshModelService),
|
||||
IVoidSettingsService: accessor.get(IVoidSettingsService),
|
||||
IInlineDiffsService: accessor.get(IInlineDiffsService),
|
||||
IVoidUriStateService: accessor.get(IVoidUriStateService),
|
||||
IQuickEditStateService: accessor.get(IQuickEditStateService),
|
||||
ISidebarStateService: accessor.get(ISidebarStateService),
|
||||
IThreadHistoryService: accessor.get(IThreadHistoryService),
|
||||
IChatThreadService: accessor.get(IChatThreadService),
|
||||
|
||||
IInstantiationService: accessor.get(IInstantiationService),
|
||||
ICodeEditorService: accessor.get(ICodeEditorService),
|
||||
|
|
@ -210,6 +240,16 @@ export const useAccessor = () => {
|
|||
|
||||
// -- state of services --
|
||||
|
||||
export const useUriState = () => {
|
||||
const [s, ss] = useState(uriState)
|
||||
useEffect(() => {
|
||||
ss(uriState)
|
||||
uriStateListeners.add(ss)
|
||||
return () => { uriStateListeners.delete(ss) }
|
||||
}, [ss])
|
||||
return s
|
||||
}
|
||||
|
||||
export const useQuickEditState = () => {
|
||||
const [s, ss] = useState(quickEditState)
|
||||
useEffect(() => {
|
||||
|
|
@ -240,17 +280,36 @@ export const useSettingsState = () => {
|
|||
return s
|
||||
}
|
||||
|
||||
export const useThreadsState = () => {
|
||||
const [s, ss] = useState(threadsState)
|
||||
export const useChatThreadsState = () => {
|
||||
const [s, ss] = useState(chatThreadsState)
|
||||
useEffect(() => {
|
||||
ss(threadsState)
|
||||
threadsStateListeners.add(ss)
|
||||
return () => { threadsStateListeners.delete(ss) }
|
||||
ss(chatThreadsState)
|
||||
chatThreadsStateListeners.add(ss)
|
||||
return () => { chatThreadsStateListeners.delete(ss) }
|
||||
}, [ss])
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const useChatThreadsStreamState = (threadId: string) => {
|
||||
const [s, ss] = useState<ThreadStreamState[string] | undefined>(chatThreadsStreamState[threadId])
|
||||
useEffect(() => {
|
||||
ss(chatThreadsStreamState[threadId])
|
||||
const listener = (threadId_: string) => {
|
||||
if (threadId_ !== threadId) return
|
||||
ss(chatThreadsStreamState[threadId])
|
||||
}
|
||||
chatThreadsStreamStateListeners.add(listener)
|
||||
return () => { chatThreadsStreamStateListeners.delete(listener) }
|
||||
}, [ss, threadId])
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const useRefreshModelState = () => {
|
||||
const [s, ss] = useState(refreshModelState)
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
|
|||
options={options}
|
||||
selectedOption={selectedOption}
|
||||
onChangeOption={onChangeOption}
|
||||
getOptionName={(option) => option.name}
|
||||
getOptionDisplayName={(option) => option.selection.modelName}
|
||||
getOptionDropdownName={(option) => option.name}
|
||||
getOptionsEqual={(a, b) => optionsEqual([a], [b])}
|
||||
className={`text-xs text-void-fg-3 px-1`}
|
||||
matchInputWidth={false}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { VoidButton, VoidCheckBox, VoidCustomSelectBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
|
||||
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
|
|
@ -38,32 +38,39 @@ const RefreshModelButton = ({ providerName }: { providerName: RefreshableProvide
|
|||
const refreshModelService = accessor.get('IRefreshModelService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
const [justFinished, setJustFinished] = useState(false)
|
||||
const [justFinished, setJustFinished] = useState<null | 'finished' | 'error'>(null)
|
||||
|
||||
useRefreshModelListener(
|
||||
useCallback((providerName2, refreshModelState) => {
|
||||
if (providerName2 !== providerName) return
|
||||
const { state } = refreshModelState[providerName]
|
||||
if (state !== 'finished') return
|
||||
if (!(state === 'finished' || state === 'error')) return
|
||||
// now we know we just entered 'finished' state for this providerName
|
||||
setJustFinished(true)
|
||||
const tid = setTimeout(() => { setJustFinished(false) }, 2000)
|
||||
setJustFinished(state)
|
||||
const tid = setTimeout(() => { setJustFinished(null) }, 2000)
|
||||
return () => clearTimeout(tid)
|
||||
}, [providerName])
|
||||
)
|
||||
|
||||
const { state } = refreshModelState[providerName]
|
||||
const isRefreshing = state === 'refreshing'
|
||||
|
||||
const { title: providerTitle } = displayInfoOfProviderName(providerName)
|
||||
return <SubtleButton
|
||||
onClick={() => {
|
||||
refreshModelService.refreshModels(providerName)
|
||||
refreshModelService.startRefreshingModels(providerName, { enableProviderOnSuccess: false, doNotFire: false })
|
||||
metricsService.capture('Click', { providerName, action: 'Refresh Models' })
|
||||
}}
|
||||
text={justFinished ? `${providerTitle} Models are up-to-date!` : `Manually refresh models list for ${providerTitle}.`}
|
||||
icon={isRefreshing ? <Loader2 className='size-3 animate-spin' /> : (justFinished ? <Check className='stroke-green-500 size-3' /> : <RefreshCw className='size-3' />)}
|
||||
disabled={isRefreshing || justFinished}
|
||||
text={justFinished === 'finished' ? `${providerTitle} Models are up-to-date!`
|
||||
: justFinished === 'error' ? `${providerTitle} not found!`
|
||||
: `Manually refresh ${providerTitle} models.`
|
||||
}
|
||||
icon={justFinished === 'finished' ? <Check className='stroke-green-500 size-3' />
|
||||
: justFinished === 'error' ? <X className='stroke-red-500 size-3' />
|
||||
: state === 'refreshing' ? <Loader2 className='size-3 animate-spin' />
|
||||
: <RefreshCw className='size-3' />
|
||||
}
|
||||
|
||||
disabled={state === 'refreshing' || justFinished !== null}
|
||||
/>
|
||||
}
|
||||
|
||||
|
|
@ -109,7 +116,8 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
|
|||
options={providerNames}
|
||||
selectedOption={providerName}
|
||||
onChangeOption={(pn) => setProviderName(pn)}
|
||||
getOptionName={(pn) => pn ? displayInfoOfProviderName(pn).title : '(null)'}
|
||||
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : '(null)'}
|
||||
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : '(null)'}
|
||||
getOptionsEqual={(a, b) => a === b}
|
||||
className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root
|
||||
py-[4px] px-[6px]
|
||||
|
|
@ -126,6 +134,7 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
|
|||
<div className='max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root'>
|
||||
<VoidInputBox2
|
||||
placeholder='Model Name'
|
||||
className='mt-[2px] px-[6px] h-full w-full'
|
||||
ref={modelNameRef}
|
||||
multiline={false}
|
||||
/>
|
||||
|
|
@ -353,31 +362,10 @@ export const VoidProviderSettings = ({ providerNames }: { providerNames: Provide
|
|||
</>
|
||||
}
|
||||
|
||||
// export const VoidFeatureFlagSettings = () => {
|
||||
// const accessor = useAccessor()
|
||||
// const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
// const voidSettingsState = useSettingsState()
|
||||
|
||||
// return <>
|
||||
// {featureFlagNames.map((flagName) => {
|
||||
// const value = voidSettingsState.featureFlagSettings[flagName]
|
||||
// const { description } = displayInfoOfFeatureFlag(flagName)
|
||||
// return <div key={flagName} className='hover:bg-black/10 hover:dark:bg-gray-200/10 rounded-sm overflow-hidden py-1 px-3 my-1'>
|
||||
// <div className='flex items-center'>
|
||||
// <VoidCheckBox
|
||||
// label=''
|
||||
// value={value}
|
||||
// onClick={() => { voidSettingsService.setFeatureFlag(flagName, !value) }}
|
||||
// />
|
||||
// <h4 className='text-sm'>{description}</h4>
|
||||
// </div>
|
||||
// </div>
|
||||
// })}
|
||||
// </>
|
||||
// }
|
||||
type TabName = 'models' | 'general'
|
||||
export const VoidFeatureFlagSettings = () => {
|
||||
export const AutoRefreshToggle = () => {
|
||||
const settingName: GlobalSettingName = 'autoRefreshModels'
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
|
@ -385,22 +373,33 @@ export const VoidFeatureFlagSettings = () => {
|
|||
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
return featureFlagNames.map((flagName) => {
|
||||
// right now this is just `enabled_autoRefreshModels`
|
||||
const enabled = voidSettingsState.globalSettings[settingName]
|
||||
|
||||
// right now this is just `enabled_autoRefreshModels`
|
||||
const enabled = voidSettingsState.featureFlagSettings[flagName]
|
||||
const { description } = displayInfoOfFeatureFlag(flagName)
|
||||
return <SubtleButton
|
||||
onClick={() => {
|
||||
voidSettingsService.setGlobalSetting(settingName, !enabled)
|
||||
metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled })
|
||||
}}
|
||||
text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`}
|
||||
icon={enabled ? <Check className='stroke-green-500 size-3' /> : <X className='stroke-red-500 size-3' />}
|
||||
disabled={false}
|
||||
/>
|
||||
}
|
||||
|
||||
return <SubtleButton key={flagName}
|
||||
onClick={() => {
|
||||
voidSettingsService.setFeatureFlag(flagName, !enabled)
|
||||
metricsService.capture('Click', { action: 'Autorefresh Toggle', flagName, enabled: !enabled })
|
||||
}}
|
||||
text={description}
|
||||
icon={enabled ? <Check className='stroke-green-500 size-3' /> : <X className='stroke-red-500 size-3' />}
|
||||
disabled={false}
|
||||
/>
|
||||
})
|
||||
export const AIInstructionsBox = () => {
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidSettingsState = useSettingsState()
|
||||
return <VoidInputBox2
|
||||
className='min-h-[81px] p-3 rounded-sm'
|
||||
initValue={voidSettingsState.globalSettings.aiInstructions}
|
||||
placeholder={`Do not change my indentation or delete my comments. When writing TS or JS, do not add ;'s. Respond to all queries in French. `}
|
||||
multiline
|
||||
onChangeText={(newText) => {
|
||||
voidSettingsService.setGlobalSetting('aiInstructions', newText)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
export const FeaturesTab = () => {
|
||||
|
|
@ -423,16 +422,16 @@ export const FeaturesTab = () => {
|
|||
<VoidProviderSettings providerNames={localProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-16`}>Providers</h2>
|
||||
<h2 className={`text-3xl mb-2 mt-12`}>Providers</h2>
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}</h3>
|
||||
{/* <h3 className={`opacity-50 mb-2`}>{`Access models like ChatGPT and Claude. We recommend using Anthropic or OpenAI as providers, or Groq as a faster alternative.`}</h3> */}
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings providerNames={nonlocalProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-16`}>Models</h2>
|
||||
<h2 className={`text-3xl mb-2 mt-12`}>Models</h2>
|
||||
<ErrorBoundary>
|
||||
<VoidFeatureFlagSettings />
|
||||
<AutoRefreshToggle />
|
||||
<RefreshableModels />
|
||||
<ModelDump />
|
||||
<AddModelMenuFull />
|
||||
|
|
@ -569,15 +568,8 @@ const GeneralTab = () => {
|
|||
</div>
|
||||
|
||||
|
||||
{/* <div className='my-4'>
|
||||
<h3 className={`text-xl mb-2 mt-4`}>Rules for AI</h3>
|
||||
{`placeholder: "Do not add ;'s. Do not change or delete spacing, formatting, or comments. Respond to queries in French when applicable. "`}
|
||||
</div> */}
|
||||
|
||||
|
||||
|
||||
|
||||
<div className='mt-16'>
|
||||
<div className='mt-12'>
|
||||
<h2 className={`text-3xl mb-2`}>Built-in Settings</h2>
|
||||
<h4 className={`text-void-fg-3 mb-2`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
|
||||
|
||||
|
|
@ -598,7 +590,13 @@ const GeneralTab = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* <VoidFeatureFlagSettings /> */}
|
||||
|
||||
<div className='mt-12'>
|
||||
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
|
||||
<h4 className={`text-void-fg-3 mb-2`}>{`Instructions to include on all AI requests.`}</h4>
|
||||
<AIInstructionsBox />
|
||||
</div>
|
||||
|
||||
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js
|
|||
|
||||
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { CodeStagingSelection, IThreadHistoryService } from './threadHistoryService.js';
|
||||
import { CodeStagingSelection, IChatThreadService } from './chatThreadService.js';
|
||||
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
|
|
@ -26,10 +26,9 @@ import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase
|
|||
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { IViewsService } from '../../../services/views/common/viewsService.js';
|
||||
|
||||
import { IVoidUriStateService } from './voidUriStateService.js';
|
||||
|
||||
// ---------- Register commands and keybindings ----------
|
||||
|
||||
|
|
@ -126,8 +125,8 @@ registerAction2(class extends Action2 {
|
|||
}
|
||||
|
||||
// add selection to staging
|
||||
const threadHistoryService = accessor.get(IThreadHistoryService)
|
||||
const currentStaging = threadHistoryService.state._currentStagingSelections
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
const currentStaging = chatThreadService.state.currentStagingSelections
|
||||
const currentStagingEltIdx = currentStaging?.findIndex(s =>
|
||||
s.fileURI.fsPath === model.uri.fsPath
|
||||
&& s.range?.startLineNumber === selection.range?.startLineNumber
|
||||
|
|
@ -136,7 +135,7 @@ registerAction2(class extends Action2 {
|
|||
|
||||
// if matches with existing selection, overwrite
|
||||
if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) {
|
||||
threadHistoryService.setStaging([
|
||||
chatThreadService.setStaging([
|
||||
...currentStaging!.slice(0, currentStagingEltIdx),
|
||||
selection,
|
||||
...currentStaging!.slice(currentStagingEltIdx + 1, Infinity)
|
||||
|
|
@ -144,7 +143,7 @@ registerAction2(class extends Action2 {
|
|||
}
|
||||
// if no match, add
|
||||
else {
|
||||
threadHistoryService.setStaging([...(currentStaging ?? []), selection])
|
||||
chatThreadService.setStaging([...(currentStaging ?? []), selection])
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -153,7 +152,15 @@ registerAction2(class extends Action2 {
|
|||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({ id: VOID_CTRL_L_ACTION_ID, title: 'Void: Press Ctrl+L', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.BuiltinExtension } });
|
||||
super({
|
||||
id: VOID_CTRL_L_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidCtrlL', 'Void: Add Select to Chat'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyL,
|
||||
weight: KeybindingWeight.VoidExtension
|
||||
}
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandService = accessor.get(ICommandService)
|
||||
|
|
@ -184,8 +191,8 @@ registerAction2(class extends Action2 {
|
|||
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
stateService.fireFocusChat()
|
||||
const historyService = accessor.get(IThreadHistoryService)
|
||||
historyService.startNewThread()
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
chatThreadService.openNewThread()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -233,7 +240,7 @@ registerAction2(class extends Action2 {
|
|||
export class TabSwitchListener extends Disposable {
|
||||
|
||||
constructor(
|
||||
onSwitchTab: (uri: URI) => void,
|
||||
onSwitchTab: () => void,
|
||||
@ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
) {
|
||||
super()
|
||||
|
|
@ -241,9 +248,8 @@ export class TabSwitchListener extends Disposable {
|
|||
// when editor switches tabs (models)
|
||||
const addTabSwitchListeners = (editor: ICodeEditor) => {
|
||||
this._register(editor.onDidChangeModel(e => {
|
||||
if (e.newModelUrl && e.newModelUrl.scheme === 'file') {
|
||||
onSwitchTab(e.newModelUrl)
|
||||
}
|
||||
if (e.newModelUrl?.scheme !== 'file') return
|
||||
onSwitchTab()
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -263,8 +269,10 @@ class TabSwitchContribution extends Disposable implements IWorkbenchContribution
|
|||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@IViewsService private readonly viewsService: IViewsService,
|
||||
@IVoidUriStateService private readonly uriStateService: IVoidUriStateService,
|
||||
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
|
||||
// @ICommandService private readonly commandService: ICommandService,
|
||||
) {
|
||||
super()
|
||||
|
||||
|
|
@ -274,18 +282,22 @@ class TabSwitchContribution extends Disposable implements IWorkbenchContribution
|
|||
sidebarIsVisible = e.visible
|
||||
}))
|
||||
|
||||
const addCurrentFileIfVisible = () => {
|
||||
if (sidebarIsVisible)
|
||||
this.commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
|
||||
const onSwitchTab = () => { // update state
|
||||
if (sidebarIsVisible) {
|
||||
const currentUri = this.codeEditorService.getActiveCodeEditor()?.getModel()?.uri
|
||||
if (!currentUri) return;
|
||||
this.uriStateService.setState({ currentUri })
|
||||
// this.commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
|
||||
}
|
||||
}
|
||||
|
||||
// when sidebar becomes visible, add current file
|
||||
this._register(this.viewsService.onDidChangeViewVisibility(e => { sidebarIsVisible = e.visible }))
|
||||
|
||||
// run on current tab if it exists, and listen for tab switches and visibility changes
|
||||
addCurrentFileIfVisible()
|
||||
this._register(this.viewsService.onDidChangeViewVisibility(() => { addCurrentFileIfVisible() }))
|
||||
this._register(this.instantiationService.createInstance(TabSwitchListener, () => { addCurrentFileIfVisible() }))
|
||||
onSwitchTab()
|
||||
this._register(this.viewsService.onDidChangeViewVisibility(() => { onSwitchTab() }))
|
||||
this._register(this.instantiationService.createInstance(TabSwitchListener, () => { onSwitchTab() }))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,219 +0,0 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { IAutocompleteService } from './autocompleteService.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
|
||||
export type CodeSelection = {
|
||||
fileURI: URI;
|
||||
selectionStr: string | null;
|
||||
content: string; // TODO remove this (replace `selectionStr` with `content`)
|
||||
range: IRange;
|
||||
}
|
||||
|
||||
// if selectionStr is null, it means to use the entire file at send time
|
||||
export type CodeStagingSelection = {
|
||||
type: 'Selection',
|
||||
fileURI: URI,
|
||||
selectionStr: string,
|
||||
range: IRange
|
||||
} | {
|
||||
type: 'File',
|
||||
fileURI: URI,
|
||||
selectionStr: null,
|
||||
range: null
|
||||
}
|
||||
|
||||
|
||||
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
|
||||
export type ChatMessage =
|
||||
| {
|
||||
role: 'user';
|
||||
content: string | null; // content sent to the llm - allowed to be '', will be replaced with (empty)
|
||||
displayContent: string | null; // content displayed to user - allowed to be '', will be ignored
|
||||
selections: CodeSelection[] | null; // the user's selection
|
||||
}
|
||||
| {
|
||||
role: 'assistant';
|
||||
content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
|
||||
displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored
|
||||
}
|
||||
| {
|
||||
role: 'system';
|
||||
content: string;
|
||||
displayContent?: undefined;
|
||||
}
|
||||
|
||||
// a 'thread' means a chat message history
|
||||
export type ChatThreads = {
|
||||
[id: string]: {
|
||||
id: string; // store the id here too
|
||||
createdAt: string; // ISO string
|
||||
lastModified: string; // ISO string
|
||||
messages: ChatMessage[];
|
||||
|
||||
// editing state
|
||||
isBeingEdited: boolean;
|
||||
_currentStagingSelections: CodeStagingSelection[] | null;
|
||||
};
|
||||
}
|
||||
|
||||
export type ThreadsState = {
|
||||
allThreads: ChatThreads;
|
||||
_currentThreadId: string | null; // intended for internal use only
|
||||
_currentStagingSelections: CodeStagingSelection[] | null;
|
||||
}
|
||||
|
||||
|
||||
const newThreadObject = () => {
|
||||
const now = new Date().toISOString()
|
||||
return {
|
||||
id: new Date().getTime().toString(),
|
||||
createdAt: now,
|
||||
lastModified: now,
|
||||
messages: [],
|
||||
isBeingEdited: false,
|
||||
_currentStagingSelections: null,
|
||||
}
|
||||
}
|
||||
|
||||
const THREAD_STORAGE_KEY = 'void.threadHistory'
|
||||
|
||||
export interface IThreadHistoryService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly state: ThreadsState;
|
||||
onDidChangeCurrentThread: Event<void>;
|
||||
|
||||
getCurrentThread(state: ThreadsState): ChatThreads[string] | null;
|
||||
startNewThread(): void;
|
||||
switchToThread(threadId: string): void;
|
||||
addMessageToCurrentThread(message: ChatMessage): void;
|
||||
|
||||
setStaging(stagingSelection: CodeStagingSelection[] | null): void;
|
||||
|
||||
}
|
||||
|
||||
export const IThreadHistoryService = createDecorator<IThreadHistoryService>('voidThreadHistoryService');
|
||||
class ThreadHistoryService extends Disposable implements IThreadHistoryService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
// this fires when the current thread changes at all (a switch of currentThread, or a message added to it, etc)
|
||||
private readonly _onDidChangeCurrentThread = new Emitter<void>();
|
||||
readonly onDidChangeCurrentThread: Event<void> = this._onDidChangeCurrentThread.event;
|
||||
|
||||
state: ThreadsState // allThreads is persisted, currentThread is not
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IAutocompleteService private readonly _autocomplete: IAutocompleteService,
|
||||
) {
|
||||
super()
|
||||
this._autocomplete
|
||||
|
||||
this.state = {
|
||||
allThreads: this._readAllThreads(),
|
||||
_currentThreadId: null,
|
||||
_currentStagingSelections: null,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private _readAllThreads(): ChatThreads {
|
||||
const threads = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
|
||||
return threads ? JSON.parse(threads) : {}
|
||||
}
|
||||
|
||||
private _storeAllThreads(threads: ChatThreads) {
|
||||
this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads), StorageScope.APPLICATION, StorageTarget.USER)
|
||||
}
|
||||
|
||||
// this should be the only place this.state = ... appears besides constructor
|
||||
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
...state
|
||||
}
|
||||
if (affectsCurrent)
|
||||
this._onDidChangeCurrentThread.fire()
|
||||
}
|
||||
|
||||
// must "prove" that you have access to the current state by providing it
|
||||
getCurrentThread(state: ThreadsState): ChatThreads[string] | null {
|
||||
return state._currentThreadId ? state.allThreads[state._currentThreadId] ?? null : null;
|
||||
}
|
||||
|
||||
switchToThread(threadId: string) {
|
||||
console.log('threadId', threadId)
|
||||
console.log('messages', this.state.allThreads[threadId].messages)
|
||||
this._setState({ _currentThreadId: threadId }, true)
|
||||
}
|
||||
|
||||
|
||||
startNewThread() {
|
||||
// if a thread with 0 messages already exists, switch to it
|
||||
const { allThreads: currentThreads } = this.state
|
||||
for (const threadId in currentThreads) {
|
||||
if (currentThreads[threadId].messages.length === 0) {
|
||||
this.switchToThread(threadId)
|
||||
return
|
||||
}
|
||||
}
|
||||
// otherwise, start a new thread
|
||||
const newThread = newThreadObject()
|
||||
|
||||
// update state
|
||||
const newThreads = {
|
||||
...currentThreads,
|
||||
[newThread.id]: newThread
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads, _currentThreadId: newThread.id }, true)
|
||||
}
|
||||
|
||||
|
||||
addMessageToCurrentThread(message: ChatMessage) {
|
||||
console.log('adding ', message.role, 'to chat')
|
||||
const { allThreads, _currentThreadId } = this.state
|
||||
|
||||
// get the current thread, or create one
|
||||
let currentThread: ChatThreads[string]
|
||||
if (_currentThreadId && (_currentThreadId in allThreads)) {
|
||||
currentThread = allThreads[_currentThreadId]
|
||||
}
|
||||
else {
|
||||
currentThread = newThreadObject()
|
||||
this.state._currentThreadId = currentThread.id
|
||||
}
|
||||
|
||||
// update state and store it
|
||||
const newThreads = {
|
||||
...allThreads,
|
||||
[currentThread.id]: {
|
||||
...currentThread,
|
||||
lastModified: new Date().toISOString(),
|
||||
messages: [...currentThread.messages, message],
|
||||
}
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
|
||||
}
|
||||
|
||||
|
||||
setStaging(stagingSelection: CodeStagingSelection[] | null): void {
|
||||
this._setState({ _currentStagingSelections: stagingSelection }, true) // this is a hack for now
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IThreadHistoryService, ThreadHistoryService, InstantiationType.Eager);
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ import './sidebarStateService.js'
|
|||
import './quickEditActions.js'
|
||||
|
||||
// register Thread History
|
||||
import './threadHistoryService.js'
|
||||
import './chatThreadService.js'
|
||||
|
||||
// register Autocomplete
|
||||
import './autocompleteService.js'
|
||||
|
|
@ -26,3 +26,6 @@ import './voidSettingsPane.js'
|
|||
|
||||
// register css
|
||||
import './media/void.css'
|
||||
|
||||
// update (frontend part, also see platform/)
|
||||
import './voidUpdateActions.js'
|
||||
|
|
|
|||
87
src/vs/workbench/contrib/void/browser/voidUpdateActions.ts
Normal file
87
src/vs/workbench/contrib/void/browser/voidUpdateActions.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import Severity from '../../../../base/common/severity.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { INotificationService } from '../../../../platform/notification/common/notification.js';
|
||||
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
|
||||
import { IVoidUpdateService } from '../../../../platform/void/common/voidUpdateService.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
|
||||
|
||||
|
||||
|
||||
const notifyYesUpdate = (notifService: INotificationService, msg?: string) => {
|
||||
const message = msg || 'This is a very old version of void, please download the latest version! [Void Editor](https://voideditor.com/download-beta)!'
|
||||
notifService.notify({
|
||||
severity: Severity.Info,
|
||||
message: message,
|
||||
})
|
||||
}
|
||||
const notifyNoUpdate = (notifService: INotificationService) => {
|
||||
notifService.notify({
|
||||
severity: Severity.Info,
|
||||
message: 'Void is up-to-date!',
|
||||
})
|
||||
}
|
||||
const notifyErrChecking = (notifService: INotificationService) => {
|
||||
const message = `Void Error: There was an error checking for updates. If this persists, please get in touch or reinstall Void [here](https://voideditor.com/download-beta)!`
|
||||
notifService.notify({
|
||||
severity: Severity.Info,
|
||||
message: message,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
f1: true,
|
||||
id: 'void.voidCheckUpdate',
|
||||
title: localize2('voidCheckUpdate', 'Void: Check for Updates'),
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const voidUpdateService = accessor.get(IVoidUpdateService)
|
||||
const notifService = accessor.get(INotificationService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
|
||||
const res = await voidUpdateService.check()
|
||||
if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update: Error', {}) }
|
||||
else if (res.hasUpdate) { notifyYesUpdate(notifService, res.message); metricsService.capture('Void Update: Yes', {}) }
|
||||
else if (!res.hasUpdate) { notifyNoUpdate(notifService); metricsService.capture('Void Update: No', {}) }
|
||||
}
|
||||
})
|
||||
|
||||
// on mount
|
||||
class VoidUpdateWorkbenchContribution extends Disposable implements IWorkbenchContribution {
|
||||
static readonly ID = 'workbench.contrib.void.voidUpdate'
|
||||
constructor(
|
||||
@IVoidUpdateService private readonly voidUpdateService: IVoidUpdateService,
|
||||
@INotificationService private readonly notifService: INotificationService,
|
||||
@IMetricsService private readonly metricsService: IMetricsService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// on mount
|
||||
setTimeout(async () => {
|
||||
const res = await this.voidUpdateService.check()
|
||||
|
||||
const notifService = this.notifService
|
||||
const metricsService = this.metricsService
|
||||
|
||||
if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update Startup: Error', {}) }
|
||||
else if (res.hasUpdate) { notifyYesUpdate(this.notifService, res.message); metricsService.capture('Void Update Startup: Yes', {}) }
|
||||
else if (!res.hasUpdate) { metricsService.capture('Void Update Startup: No', {}) } // display nothing if up to date
|
||||
|
||||
}, 5 * 1000)
|
||||
}
|
||||
}
|
||||
registerWorkbenchContribution2(VoidUpdateWorkbenchContribution.ID, VoidUpdateWorkbenchContribution, WorkbenchPhase.BlockRestore);
|
||||
57
src/vs/workbench/contrib/void/browser/voidUriStateService.ts
Normal file
57
src/vs/workbench/contrib/void/browser/voidUriStateService.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
|
||||
|
||||
|
||||
// service that manages state
|
||||
export type VoidUriState = {
|
||||
currentUri?: URI
|
||||
}
|
||||
|
||||
export interface IVoidUriStateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly state: VoidUriState; // readonly to the user
|
||||
setState(newState: Partial<VoidUriState>): void;
|
||||
onDidChangeState: Event<void>;
|
||||
}
|
||||
|
||||
export const IVoidUriStateService = createDecorator<IVoidUriStateService>('voidUriStateService');
|
||||
class VoidUriStateService extends Disposable implements IVoidUriStateService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
static readonly ID = 'voidUriStateService';
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
|
||||
// state
|
||||
state: VoidUriState
|
||||
|
||||
constructor(
|
||||
) {
|
||||
super()
|
||||
|
||||
// initial state
|
||||
this.state = { currentUri: undefined }
|
||||
}
|
||||
|
||||
setState(newState: Partial<VoidUriState>) {
|
||||
|
||||
this.state = { ...this.state, ...newState }
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IVoidUriStateService, VoidUriStateService, InstantiationType.Eager);
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
.file-icons-enabled .show-file-icons .vscode_getting_started_page-name-file-icon.file-icon::before {
|
||||
content: ' ';
|
||||
background-image: url('../../../../browser/media/code-icon.svg');
|
||||
background-image: url('../../../../browser/media/void-icon-sm.png');
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer {
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@
|
|||
|
||||
.file-icons-enabled .show-file-icons .vs_code_editor_walkthrough\.md-name-file-icon.md-ext-file-icon.ext-file-icon.markdown-lang-file-icon.file-icon::before {
|
||||
content: ' ';
|
||||
background-image: url('../../../../browser/media/code-icon.svg');
|
||||
background-image: url('../../../../browser/media/void-icon-sm.png');
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .walkThroughContent .mac-only,
|
||||
|
|
|
|||
Loading…
Reference in a new issue