mirror of
https://github.com/voideditor/void
synced 2026-05-23 01:18:25 +00:00
Lint, window.setInterval, + remove ignored files from git tracking
This commit is contained in:
parent
a2b2fd6ad5
commit
abfd426d3f
36 changed files with 1502 additions and 97382 deletions
|
|
@ -41,7 +41,7 @@ export default tseslint.config(
|
|||
'curly': 'off', // <-- Void
|
||||
'eqeqeq': 'warn',
|
||||
'prefer-const': [
|
||||
'warn',
|
||||
'off', // <-- Void
|
||||
{
|
||||
'destructuring': 'all'
|
||||
}
|
||||
|
|
@ -140,7 +140,7 @@ export default tseslint.config(
|
|||
},
|
||||
rules: {
|
||||
'@stylistic/ts/semi': 'off', // <-- Void
|
||||
'@stylistic/ts/member-delimiter-style': 'warn',
|
||||
'@stylistic/ts/member-delimiter-style': 'off', // <-- Void
|
||||
'local/code-no-unused-expressions': [
|
||||
'warn',
|
||||
{
|
||||
|
|
@ -148,7 +148,7 @@ export default tseslint.config(
|
|||
}
|
||||
],
|
||||
'jsdoc/no-types': 'warn',
|
||||
'local/code-no-static-self-ref': 'warn'
|
||||
'local/code-no-static-self-ref': 'off' // <-- Void
|
||||
}
|
||||
},
|
||||
// vscode TS
|
||||
|
|
|
|||
1662
package-lock.json
generated
1662
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -104,6 +104,7 @@
|
|||
"@xterm/xterm": "^5.6.0-beta.98",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"diff": "^7.0.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"groq-sdk": "^0.15.0",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { Range } from '../../../../editor/common/core/range.js';
|
|||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { CodeActionContext, CodeActionTriggerType } from '../../../../editor/common/languages.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import * as dom from '../../../../base/browser/dom.js';
|
||||
|
||||
export interface IMarkerCheckService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
|
@ -29,8 +30,7 @@ class MarkerCheckService extends Disposable implements IMarkerCheckService {
|
|||
@ITextModelService private readonly _textModelService: ITextModelService,
|
||||
) {
|
||||
super();
|
||||
|
||||
setInterval(async () => {
|
||||
const check = async () => {
|
||||
const allMarkers = this._markerService.read();
|
||||
const errors = allMarkers.filter(marker => marker.severity === MarkerSeverity.Error);
|
||||
|
||||
|
|
@ -76,8 +76,8 @@ class MarkerCheckService extends Disposable implements IMarkerCheckService {
|
|||
if (actions?.actions?.length) {
|
||||
|
||||
const quickFixes = actions.actions.filter(action => action.isPreferred); // ! all quickFixes for the error
|
||||
const quickFixesForImports = actions.actions.filter(action => action.isPreferred && action.title.includes('import')); // ! all possible imports
|
||||
quickFixesForImports
|
||||
// const quickFixesForImports = actions.actions.filter(action => action.isPreferred && action.title.includes('import')); // ! all possible imports
|
||||
// quickFixesForImports
|
||||
|
||||
if (quickFixes.length > 0) {
|
||||
console.log('Available Quick Fixes:');
|
||||
|
|
@ -96,7 +96,9 @@ class MarkerCheckService extends Disposable implements IMarkerCheckService {
|
|||
}
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
const { window } = dom.getActiveWindow()
|
||||
window.setInterval(check, 5000);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -327,7 +327,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
const thread = this.getCurrentThread()
|
||||
|
||||
if (thread.messages?.[messageIdx]?.role !== 'user') {
|
||||
throw new Error("Error: editing a message with role !=='user'")
|
||||
throw new Error(`Error: editing a message with role !=='user'`)
|
||||
}
|
||||
|
||||
// get prev and curr selections before clearing the message
|
||||
|
|
|
|||
|
|
@ -50,12 +50,12 @@ class ContextGatheringService extends Disposable implements IContextGatheringSer
|
|||
}
|
||||
|
||||
private _subscribeToModel(model: ITextModel): void {
|
||||
console.log("Subscribing to model:", model.uri.toString());
|
||||
console.log('Subscribing to model:', model.uri.toString());
|
||||
this._register(model.onDidChangeContent(() => {
|
||||
const editor = this._codeEditorService.getFocusedCodeEditor();
|
||||
if (editor && editor.getModel() === model) {
|
||||
const pos = editor.getPosition();
|
||||
console.log("updateCache called at position:", pos);
|
||||
console.log('updateCache called at position:', pos);
|
||||
if (pos) {
|
||||
this.updateCache(model, pos);
|
||||
}
|
||||
|
|
@ -72,7 +72,7 @@ class ContextGatheringService extends Disposable implements IContextGatheringSer
|
|||
|
||||
// Convert to array and filter overlapping snippets
|
||||
this._cache = Array.from(snippets);
|
||||
console.log("Cache updated:", this._cache);
|
||||
console.log('Cache updated:', this._cache);
|
||||
}
|
||||
|
||||
public getCachedSnippets(): string[] {
|
||||
|
|
@ -232,7 +232,7 @@ class ContextGatheringService extends Disposable implements IContextGatheringSer
|
|||
symbols.push(...intersecting);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Symbol provider error:", e);
|
||||
console.warn('Symbol provider error:', e);
|
||||
}
|
||||
}
|
||||
// Also check reference providers.
|
||||
|
|
@ -262,7 +262,7 @@ class ContextGatheringService extends Disposable implements IContextGatheringSer
|
|||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Reference provider error:", e);
|
||||
console.warn('Reference provider error:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -319,7 +319,7 @@ class ContextGatheringService extends Disposable implements IContextGatheringSer
|
|||
})));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Definition provider error:", e);
|
||||
console.warn('Definition provider error:', e);
|
||||
}
|
||||
}
|
||||
return defs;
|
||||
|
|
|
|||
|
|
@ -732,7 +732,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
offsetLines = 1
|
||||
}
|
||||
}
|
||||
else { throw 1 }
|
||||
else { throw new Error('Void 1') }
|
||||
|
||||
const buttonsWidget = new AcceptRejectWidget({
|
||||
editor,
|
||||
|
|
@ -1394,7 +1394,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
else if (from === 'ClickApply') {
|
||||
return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen })
|
||||
}
|
||||
throw 1
|
||||
throw new Error('Void 1')
|
||||
}
|
||||
|
||||
const latestStreamInfoMutable: StreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
|
||||
|
|
@ -1833,7 +1833,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
const diffArea = this.diffAreaOfId[diffareaid]
|
||||
if (!diffArea) continue
|
||||
|
||||
if (diffArea.type == 'DiffZone') {
|
||||
if (diffArea.type === 'DiffZone') {
|
||||
if (behavior === 'reject') this._revertAndDeleteDiffZone(diffArea)
|
||||
else if (behavior === 'accept') this._deleteDiffZone(diffArea)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,553 +0,0 @@
|
|||
// ../../../../../../../node_modules/diff/lib/index.mjs
|
||||
function Diff() {
|
||||
}
|
||||
Diff.prototype = {
|
||||
diff: function diff(oldString, newString) {
|
||||
var _options$timeout;
|
||||
var options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
|
||||
var callback = options.callback;
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
var self = this;
|
||||
function done(value) {
|
||||
value = self.postProcess(value, options);
|
||||
if (callback) {
|
||||
setTimeout(function() {
|
||||
callback(value);
|
||||
}, 0);
|
||||
return true;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
oldString = this.castInput(oldString, options);
|
||||
newString = this.castInput(newString, options);
|
||||
oldString = this.removeEmpty(this.tokenize(oldString, options));
|
||||
newString = this.removeEmpty(this.tokenize(newString, options));
|
||||
var newLen = newString.length, oldLen = oldString.length;
|
||||
var editLength = 1;
|
||||
var maxEditLength = newLen + oldLen;
|
||||
if (options.maxEditLength != null) {
|
||||
maxEditLength = Math.min(maxEditLength, options.maxEditLength);
|
||||
}
|
||||
var maxExecutionTime = (_options$timeout = options.timeout) !== null && _options$timeout !== void 0 ? _options$timeout : Infinity;
|
||||
var abortAfterTimestamp = Date.now() + maxExecutionTime;
|
||||
var bestPath = [{
|
||||
oldPos: -1,
|
||||
lastComponent: void 0
|
||||
}];
|
||||
var newPos = this.extractCommon(bestPath[0], newString, oldString, 0, options);
|
||||
if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
|
||||
return done(buildValues(self, bestPath[0].lastComponent, newString, oldString, self.useLongestToken));
|
||||
}
|
||||
var minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
|
||||
function execEditLength() {
|
||||
for (var diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
|
||||
var basePath = void 0;
|
||||
var removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
|
||||
if (removePath) {
|
||||
bestPath[diagonalPath - 1] = void 0;
|
||||
}
|
||||
var canAdd = false;
|
||||
if (addPath) {
|
||||
var addPathNewPos = addPath.oldPos - diagonalPath;
|
||||
canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
|
||||
}
|
||||
var canRemove = removePath && removePath.oldPos + 1 < oldLen;
|
||||
if (!canAdd && !canRemove) {
|
||||
bestPath[diagonalPath] = void 0;
|
||||
continue;
|
||||
}
|
||||
if (!canRemove || canAdd && removePath.oldPos < addPath.oldPos) {
|
||||
basePath = self.addToPath(addPath, true, false, 0, options);
|
||||
} else {
|
||||
basePath = self.addToPath(removePath, false, true, 1, options);
|
||||
}
|
||||
newPos = self.extractCommon(basePath, newString, oldString, diagonalPath, options);
|
||||
if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
|
||||
return done(buildValues(self, basePath.lastComponent, newString, oldString, self.useLongestToken));
|
||||
} else {
|
||||
bestPath[diagonalPath] = basePath;
|
||||
if (basePath.oldPos + 1 >= oldLen) {
|
||||
maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
|
||||
}
|
||||
if (newPos + 1 >= newLen) {
|
||||
minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
editLength++;
|
||||
}
|
||||
if (callback) {
|
||||
(function exec() {
|
||||
setTimeout(function() {
|
||||
if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
|
||||
return callback();
|
||||
}
|
||||
if (!execEditLength()) {
|
||||
exec();
|
||||
}
|
||||
}, 0);
|
||||
})();
|
||||
} else {
|
||||
while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
|
||||
var ret = execEditLength();
|
||||
if (ret) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
addToPath: function addToPath(path, added, removed, oldPosInc, options) {
|
||||
var last = path.lastComponent;
|
||||
if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
|
||||
return {
|
||||
oldPos: path.oldPos + oldPosInc,
|
||||
lastComponent: {
|
||||
count: last.count + 1,
|
||||
added,
|
||||
removed,
|
||||
previousComponent: last.previousComponent
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
oldPos: path.oldPos + oldPosInc,
|
||||
lastComponent: {
|
||||
count: 1,
|
||||
added,
|
||||
removed,
|
||||
previousComponent: last
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
extractCommon: function extractCommon(basePath, newString, oldString, diagonalPath, options) {
|
||||
var newLen = newString.length, oldLen = oldString.length, oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
|
||||
while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(oldString[oldPos + 1], newString[newPos + 1], options)) {
|
||||
newPos++;
|
||||
oldPos++;
|
||||
commonCount++;
|
||||
if (options.oneChangePerToken) {
|
||||
basePath.lastComponent = {
|
||||
count: 1,
|
||||
previousComponent: basePath.lastComponent,
|
||||
added: false,
|
||||
removed: false
|
||||
};
|
||||
}
|
||||
}
|
||||
if (commonCount && !options.oneChangePerToken) {
|
||||
basePath.lastComponent = {
|
||||
count: commonCount,
|
||||
previousComponent: basePath.lastComponent,
|
||||
added: false,
|
||||
removed: false
|
||||
};
|
||||
}
|
||||
basePath.oldPos = oldPos;
|
||||
return newPos;
|
||||
},
|
||||
equals: function equals(left, right, options) {
|
||||
if (options.comparator) {
|
||||
return options.comparator(left, right);
|
||||
} else {
|
||||
return left === right || options.ignoreCase && left.toLowerCase() === right.toLowerCase();
|
||||
}
|
||||
},
|
||||
removeEmpty: function removeEmpty(array) {
|
||||
var ret = [];
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
if (array[i]) {
|
||||
ret.push(array[i]);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
castInput: function castInput(value) {
|
||||
return value;
|
||||
},
|
||||
tokenize: function tokenize(value) {
|
||||
return Array.from(value);
|
||||
},
|
||||
join: function join(chars) {
|
||||
return chars.join("");
|
||||
},
|
||||
postProcess: function postProcess(changeObjects) {
|
||||
return changeObjects;
|
||||
}
|
||||
};
|
||||
function buildValues(diff2, lastComponent, newString, oldString, useLongestToken) {
|
||||
var components = [];
|
||||
var nextComponent;
|
||||
while (lastComponent) {
|
||||
components.push(lastComponent);
|
||||
nextComponent = lastComponent.previousComponent;
|
||||
delete lastComponent.previousComponent;
|
||||
lastComponent = nextComponent;
|
||||
}
|
||||
components.reverse();
|
||||
var componentPos = 0, componentLen = components.length, newPos = 0, oldPos = 0;
|
||||
for (; componentPos < componentLen; componentPos++) {
|
||||
var component = components[componentPos];
|
||||
if (!component.removed) {
|
||||
if (!component.added && useLongestToken) {
|
||||
var value = newString.slice(newPos, newPos + component.count);
|
||||
value = value.map(function(value2, i) {
|
||||
var oldValue = oldString[oldPos + i];
|
||||
return oldValue.length > value2.length ? oldValue : value2;
|
||||
});
|
||||
component.value = diff2.join(value);
|
||||
} else {
|
||||
component.value = diff2.join(newString.slice(newPos, newPos + component.count));
|
||||
}
|
||||
newPos += component.count;
|
||||
if (!component.added) {
|
||||
oldPos += component.count;
|
||||
}
|
||||
} else {
|
||||
component.value = diff2.join(oldString.slice(oldPos, oldPos + component.count));
|
||||
oldPos += component.count;
|
||||
}
|
||||
}
|
||||
return components;
|
||||
}
|
||||
function longestCommonPrefix(str1, str2) {
|
||||
var i;
|
||||
for (i = 0; i < str1.length && i < str2.length; i++) {
|
||||
if (str1[i] != str2[i]) {
|
||||
return str1.slice(0, i);
|
||||
}
|
||||
}
|
||||
return str1.slice(0, i);
|
||||
}
|
||||
function longestCommonSuffix(str1, str2) {
|
||||
var i;
|
||||
if (!str1 || !str2 || str1[str1.length - 1] != str2[str2.length - 1]) {
|
||||
return "";
|
||||
}
|
||||
for (i = 0; i < str1.length && i < str2.length; i++) {
|
||||
if (str1[str1.length - (i + 1)] != str2[str2.length - (i + 1)]) {
|
||||
return str1.slice(-i);
|
||||
}
|
||||
}
|
||||
return str1.slice(-i);
|
||||
}
|
||||
function replacePrefix(string, oldPrefix, newPrefix) {
|
||||
if (string.slice(0, oldPrefix.length) != oldPrefix) {
|
||||
throw Error("string ".concat(JSON.stringify(string), " doesn't start with prefix ").concat(JSON.stringify(oldPrefix), "; this is a bug"));
|
||||
}
|
||||
return newPrefix + string.slice(oldPrefix.length);
|
||||
}
|
||||
function replaceSuffix(string, oldSuffix, newSuffix) {
|
||||
if (!oldSuffix) {
|
||||
return string + newSuffix;
|
||||
}
|
||||
if (string.slice(-oldSuffix.length) != oldSuffix) {
|
||||
throw Error("string ".concat(JSON.stringify(string), " doesn't end with suffix ").concat(JSON.stringify(oldSuffix), "; this is a bug"));
|
||||
}
|
||||
return string.slice(0, -oldSuffix.length) + newSuffix;
|
||||
}
|
||||
function removePrefix(string, oldPrefix) {
|
||||
return replacePrefix(string, oldPrefix, "");
|
||||
}
|
||||
function removeSuffix(string, oldSuffix) {
|
||||
return replaceSuffix(string, oldSuffix, "");
|
||||
}
|
||||
function maximumOverlap(string1, string2) {
|
||||
return string2.slice(0, overlapCount(string1, string2));
|
||||
}
|
||||
function overlapCount(a, b) {
|
||||
var startA = 0;
|
||||
if (a.length > b.length) {
|
||||
startA = a.length - b.length;
|
||||
}
|
||||
var endB = b.length;
|
||||
if (a.length < b.length) {
|
||||
endB = a.length;
|
||||
}
|
||||
var map = Array(endB);
|
||||
var k = 0;
|
||||
map[0] = 0;
|
||||
for (var j = 1; j < endB; j++) {
|
||||
if (b[j] == b[k]) {
|
||||
map[j] = map[k];
|
||||
} else {
|
||||
map[j] = k;
|
||||
}
|
||||
while (k > 0 && b[j] != b[k]) {
|
||||
k = map[k];
|
||||
}
|
||||
if (b[j] == b[k]) {
|
||||
k++;
|
||||
}
|
||||
}
|
||||
k = 0;
|
||||
for (var i = startA; i < a.length; i++) {
|
||||
while (k > 0 && a[i] != b[k]) {
|
||||
k = map[k];
|
||||
}
|
||||
if (a[i] == b[k]) {
|
||||
k++;
|
||||
}
|
||||
}
|
||||
return k;
|
||||
}
|
||||
var extendedWordChars = "a-zA-Z0-9_\\u{C0}-\\u{FF}\\u{D8}-\\u{F6}\\u{F8}-\\u{2C6}\\u{2C8}-\\u{2D7}\\u{2DE}-\\u{2FF}\\u{1E00}-\\u{1EFF}";
|
||||
var tokenizeIncludingWhitespace = new RegExp("[".concat(extendedWordChars, "]+|\\s+|[^").concat(extendedWordChars, "]"), "ug");
|
||||
var wordDiff = new Diff();
|
||||
wordDiff.equals = function(left, right, options) {
|
||||
if (options.ignoreCase) {
|
||||
left = left.toLowerCase();
|
||||
right = right.toLowerCase();
|
||||
}
|
||||
return left.trim() === right.trim();
|
||||
};
|
||||
wordDiff.tokenize = function(value) {
|
||||
var options = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {};
|
||||
var parts;
|
||||
if (options.intlSegmenter) {
|
||||
if (options.intlSegmenter.resolvedOptions().granularity != "word") {
|
||||
throw new Error('The segmenter passed must have a granularity of "word"');
|
||||
}
|
||||
parts = Array.from(options.intlSegmenter.segment(value), function(segment) {
|
||||
return segment.segment;
|
||||
});
|
||||
} else {
|
||||
parts = value.match(tokenizeIncludingWhitespace) || [];
|
||||
}
|
||||
var tokens = [];
|
||||
var prevPart = null;
|
||||
parts.forEach(function(part) {
|
||||
if (/\s/.test(part)) {
|
||||
if (prevPart == null) {
|
||||
tokens.push(part);
|
||||
} else {
|
||||
tokens.push(tokens.pop() + part);
|
||||
}
|
||||
} else if (/\s/.test(prevPart)) {
|
||||
if (tokens[tokens.length - 1] == prevPart) {
|
||||
tokens.push(tokens.pop() + part);
|
||||
} else {
|
||||
tokens.push(prevPart + part);
|
||||
}
|
||||
} else {
|
||||
tokens.push(part);
|
||||
}
|
||||
prevPart = part;
|
||||
});
|
||||
return tokens;
|
||||
};
|
||||
wordDiff.join = function(tokens) {
|
||||
return tokens.map(function(token, i) {
|
||||
if (i == 0) {
|
||||
return token;
|
||||
} else {
|
||||
return token.replace(/^\s+/, "");
|
||||
}
|
||||
}).join("");
|
||||
};
|
||||
wordDiff.postProcess = function(changes, options) {
|
||||
if (!changes || options.oneChangePerToken) {
|
||||
return changes;
|
||||
}
|
||||
var lastKeep = null;
|
||||
var insertion = null;
|
||||
var deletion = null;
|
||||
changes.forEach(function(change) {
|
||||
if (change.added) {
|
||||
insertion = change;
|
||||
} else if (change.removed) {
|
||||
deletion = change;
|
||||
} else {
|
||||
if (insertion || deletion) {
|
||||
dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, change);
|
||||
}
|
||||
lastKeep = change;
|
||||
insertion = null;
|
||||
deletion = null;
|
||||
}
|
||||
});
|
||||
if (insertion || deletion) {
|
||||
dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, null);
|
||||
}
|
||||
return changes;
|
||||
};
|
||||
function dedupeWhitespaceInChangeObjects(startKeep, deletion, insertion, endKeep) {
|
||||
if (deletion && insertion) {
|
||||
var oldWsPrefix = deletion.value.match(/^\s*/)[0];
|
||||
var oldWsSuffix = deletion.value.match(/\s*$/)[0];
|
||||
var newWsPrefix = insertion.value.match(/^\s*/)[0];
|
||||
var newWsSuffix = insertion.value.match(/\s*$/)[0];
|
||||
if (startKeep) {
|
||||
var commonWsPrefix = longestCommonPrefix(oldWsPrefix, newWsPrefix);
|
||||
startKeep.value = replaceSuffix(startKeep.value, newWsPrefix, commonWsPrefix);
|
||||
deletion.value = removePrefix(deletion.value, commonWsPrefix);
|
||||
insertion.value = removePrefix(insertion.value, commonWsPrefix);
|
||||
}
|
||||
if (endKeep) {
|
||||
var commonWsSuffix = longestCommonSuffix(oldWsSuffix, newWsSuffix);
|
||||
endKeep.value = replacePrefix(endKeep.value, newWsSuffix, commonWsSuffix);
|
||||
deletion.value = removeSuffix(deletion.value, commonWsSuffix);
|
||||
insertion.value = removeSuffix(insertion.value, commonWsSuffix);
|
||||
}
|
||||
} else if (insertion) {
|
||||
if (startKeep) {
|
||||
insertion.value = insertion.value.replace(/^\s*/, "");
|
||||
}
|
||||
if (endKeep) {
|
||||
endKeep.value = endKeep.value.replace(/^\s*/, "");
|
||||
}
|
||||
} else if (startKeep && endKeep) {
|
||||
var newWsFull = endKeep.value.match(/^\s*/)[0], delWsStart = deletion.value.match(/^\s*/)[0], delWsEnd = deletion.value.match(/\s*$/)[0];
|
||||
var newWsStart = longestCommonPrefix(newWsFull, delWsStart);
|
||||
deletion.value = removePrefix(deletion.value, newWsStart);
|
||||
var newWsEnd = longestCommonSuffix(removePrefix(newWsFull, newWsStart), delWsEnd);
|
||||
deletion.value = removeSuffix(deletion.value, newWsEnd);
|
||||
endKeep.value = replacePrefix(endKeep.value, newWsFull, newWsEnd);
|
||||
startKeep.value = replaceSuffix(startKeep.value, newWsFull, newWsFull.slice(0, newWsFull.length - newWsEnd.length));
|
||||
} else if (endKeep) {
|
||||
var endKeepWsPrefix = endKeep.value.match(/^\s*/)[0];
|
||||
var deletionWsSuffix = deletion.value.match(/\s*$/)[0];
|
||||
var overlap = maximumOverlap(deletionWsSuffix, endKeepWsPrefix);
|
||||
deletion.value = removeSuffix(deletion.value, overlap);
|
||||
} else if (startKeep) {
|
||||
var startKeepWsSuffix = startKeep.value.match(/\s*$/)[0];
|
||||
var deletionWsPrefix = deletion.value.match(/^\s*/)[0];
|
||||
var _overlap = maximumOverlap(startKeepWsSuffix, deletionWsPrefix);
|
||||
deletion.value = removePrefix(deletion.value, _overlap);
|
||||
}
|
||||
}
|
||||
var wordWithSpaceDiff = new Diff();
|
||||
wordWithSpaceDiff.tokenize = function(value) {
|
||||
var regex = new RegExp("(\\r?\\n)|[".concat(extendedWordChars, "]+|[^\\S\\n\\r]+|[^").concat(extendedWordChars, "]"), "ug");
|
||||
return value.match(regex) || [];
|
||||
};
|
||||
var lineDiff = new Diff();
|
||||
lineDiff.tokenize = function(value, options) {
|
||||
if (options.stripTrailingCr) {
|
||||
value = value.replace(/\r\n/g, "\n");
|
||||
}
|
||||
var retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
|
||||
if (!linesAndNewlines[linesAndNewlines.length - 1]) {
|
||||
linesAndNewlines.pop();
|
||||
}
|
||||
for (var i = 0; i < linesAndNewlines.length; i++) {
|
||||
var line = linesAndNewlines[i];
|
||||
if (i % 2 && !options.newlineIsToken) {
|
||||
retLines[retLines.length - 1] += line;
|
||||
} else {
|
||||
retLines.push(line);
|
||||
}
|
||||
}
|
||||
return retLines;
|
||||
};
|
||||
lineDiff.equals = function(left, right, options) {
|
||||
if (options.ignoreWhitespace) {
|
||||
if (!options.newlineIsToken || !left.includes("\n")) {
|
||||
left = left.trim();
|
||||
}
|
||||
if (!options.newlineIsToken || !right.includes("\n")) {
|
||||
right = right.trim();
|
||||
}
|
||||
} else if (options.ignoreNewlineAtEof && !options.newlineIsToken) {
|
||||
if (left.endsWith("\n")) {
|
||||
left = left.slice(0, -1);
|
||||
}
|
||||
if (right.endsWith("\n")) {
|
||||
right = right.slice(0, -1);
|
||||
}
|
||||
}
|
||||
return Diff.prototype.equals.call(this, left, right, options);
|
||||
};
|
||||
function diffLines(oldStr, newStr, callback) {
|
||||
return lineDiff.diff(oldStr, newStr, callback);
|
||||
}
|
||||
var sentenceDiff = new Diff();
|
||||
sentenceDiff.tokenize = function(value) {
|
||||
return value.split(/(\S.+?[.!?])(?=\s+|$)/);
|
||||
};
|
||||
var cssDiff = new Diff();
|
||||
cssDiff.tokenize = function(value) {
|
||||
return value.split(/([{}:;,]|\s+)/);
|
||||
};
|
||||
function _typeof(o) {
|
||||
"@babel/helpers - typeof";
|
||||
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o2) {
|
||||
return typeof o2;
|
||||
} : function(o2) {
|
||||
return o2 && "function" == typeof Symbol && o2.constructor === Symbol && o2 !== Symbol.prototype ? "symbol" : typeof o2;
|
||||
}, _typeof(o);
|
||||
}
|
||||
var jsonDiff = new Diff();
|
||||
jsonDiff.useLongestToken = true;
|
||||
jsonDiff.tokenize = lineDiff.tokenize;
|
||||
jsonDiff.castInput = function(value, options) {
|
||||
var undefinedReplacement = options.undefinedReplacement, _options$stringifyRep = options.stringifyReplacer, stringifyReplacer = _options$stringifyRep === void 0 ? function(k, v) {
|
||||
return typeof v === "undefined" ? undefinedReplacement : v;
|
||||
} : _options$stringifyRep;
|
||||
return typeof value === "string" ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), stringifyReplacer, " ");
|
||||
};
|
||||
jsonDiff.equals = function(left, right, options) {
|
||||
return Diff.prototype.equals.call(jsonDiff, left.replace(/,([\r\n])/g, "$1"), right.replace(/,([\r\n])/g, "$1"), options);
|
||||
};
|
||||
function canonicalize(obj, stack, replacementStack, replacer, key) {
|
||||
stack = stack || [];
|
||||
replacementStack = replacementStack || [];
|
||||
if (replacer) {
|
||||
obj = replacer(key, obj);
|
||||
}
|
||||
var i;
|
||||
for (i = 0; i < stack.length; i += 1) {
|
||||
if (stack[i] === obj) {
|
||||
return replacementStack[i];
|
||||
}
|
||||
}
|
||||
var canonicalizedObj;
|
||||
if ("[object Array]" === Object.prototype.toString.call(obj)) {
|
||||
stack.push(obj);
|
||||
canonicalizedObj = new Array(obj.length);
|
||||
replacementStack.push(canonicalizedObj);
|
||||
for (i = 0; i < obj.length; i += 1) {
|
||||
canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, key);
|
||||
}
|
||||
stack.pop();
|
||||
replacementStack.pop();
|
||||
return canonicalizedObj;
|
||||
}
|
||||
if (obj && obj.toJSON) {
|
||||
obj = obj.toJSON();
|
||||
}
|
||||
if (_typeof(obj) === "object" && obj !== null) {
|
||||
stack.push(obj);
|
||||
canonicalizedObj = {};
|
||||
replacementStack.push(canonicalizedObj);
|
||||
var sortedKeys = [], _key;
|
||||
for (_key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, _key)) {
|
||||
sortedKeys.push(_key);
|
||||
}
|
||||
}
|
||||
sortedKeys.sort();
|
||||
for (i = 0; i < sortedKeys.length; i += 1) {
|
||||
_key = sortedKeys[i];
|
||||
canonicalizedObj[_key] = canonicalize(obj[_key], stack, replacementStack, replacer, _key);
|
||||
}
|
||||
stack.pop();
|
||||
replacementStack.pop();
|
||||
} else {
|
||||
canonicalizedObj = obj;
|
||||
}
|
||||
return canonicalizedObj;
|
||||
}
|
||||
var arrayDiff = new Diff();
|
||||
arrayDiff.tokenize = function(value) {
|
||||
return value.slice();
|
||||
};
|
||||
arrayDiff.join = arrayDiff.removeEmpty = function(value) {
|
||||
return value;
|
||||
};
|
||||
|
||||
export { diffLines };
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
|
@ -248,7 +248,7 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
|
|||
};
|
||||
|
||||
const useResizeObserver = () => {
|
||||
const ref = useRef(null);
|
||||
const ref = useRef<any>(null);
|
||||
const [dimensions, setDimensions] = useState({ height: 0, width: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -295,7 +295,6 @@ export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Re
|
|||
}
|
||||
|
||||
export const ButtonStop = ({ className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
|
||||
return <button
|
||||
className={`rounded-full flex-shrink-0 flex-grow-0 cursor-pointer flex items-center justify-center
|
||||
bg-white
|
||||
|
|
@ -565,10 +564,10 @@ const ToolResult = ({
|
|||
<div className="mx-4 select-none">
|
||||
<div className="border border-void-border-3 rounded px-1 py-0.5 bg-void-bg-tool">
|
||||
<div
|
||||
className={`flex items-center min-h-[24px] ${isDropdown ? 'cursor-pointer hover:brightness-125 transition-all duration-150' : 'mx-1'}`}
|
||||
className={`flex items-center min-h-[24px] ${isDropdown ? 'cursor-pointer hover:brightness-125 transition-all duration-150' : 'mx-1'}`}
|
||||
onClick={() => children && setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isDropdown && (
|
||||
{isDropdown && (
|
||||
<ChevronRight
|
||||
className={`text-void-fg-3 mr-0.5 h-5 w-5 flex-shrink-0 transition-transform duration-100 ease-[cubic-bezier(0.4,0,0.2,1)] ${isExpanded ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,8 +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 { diffLines, Change } from 'diff';
|
||||
|
||||
export { diffLines, Change };
|
||||
|
|
@ -1,29 +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 React from 'react';
|
||||
|
||||
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="void-relative void-group void-w-full void-overflow-hidden void-my-4">
|
||||
{buttonsOnHover === null ? null :
|
||||
<div className={`void-z-[1] void-absolute void-top-0 void-right-0 void-opacity-0 group-hover:void-opacity-100 void-duration-200 ${isSingleLine ? "void-h-full void-flex void-items-center" : ""}`}>
|
||||
<div className={`void-flex void-space-x-1 ${isSingleLine ? "void-pr-2" : "void-p-2"}`}>
|
||||
{buttonsOnHover}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<VoidCodeEditor {...codeEditorProps} />
|
||||
</div>
|
||||
</>);
|
||||
|
||||
};
|
||||
|
|
@ -1,301 +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 React, { JSX, useCallback, useEffect, useState } from 'react';
|
||||
import { marked, MarkedToken, Token } from 'marked';
|
||||
import { BlockCode } from './BlockCode.js';
|
||||
import { useAccessor, useChatThreadsState, useChatThreadsStreamState } from '../util/services.js';
|
||||
import { ChatMessageLocation } from '../../../searchAndReplaceService.js';
|
||||
import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js';
|
||||
|
||||
|
||||
enum CopyButtonState {
|
||||
Copy = 'Copy',
|
||||
Copied = 'Copied!',
|
||||
Error = 'Could not copy',
|
||||
}
|
||||
|
||||
const COPY_FEEDBACK_TIMEOUT = 1000; // amount of time to say 'Copied!'
|
||||
|
||||
|
||||
|
||||
type ApplyBoxLocation = ChatMessageLocation & {tokenIdx: string;};
|
||||
|
||||
const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => {
|
||||
return `${threadId}-${messageIdx}-${tokenIdx}`;
|
||||
};
|
||||
|
||||
|
||||
|
||||
const ApplyButtonsOnHover = ({ applyStr, applyBoxId }: {applyStr: string;applyBoxId: string;}) => {
|
||||
const accessor = useAccessor();
|
||||
|
||||
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy);
|
||||
const inlineDiffService = accessor.get('IInlineDiffsService');
|
||||
const clipboardService = accessor.get('IClipboardService');
|
||||
const metricsService = accessor.get('IMetricsService');
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (copyButtonState !== CopyButtonState.Copy) {
|
||||
setTimeout(() => {
|
||||
setCopyButtonState(CopyButtonState.Copy);
|
||||
}, COPY_FEEDBACK_TIMEOUT);
|
||||
}
|
||||
}, [copyButtonState]);
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
clipboardService.writeText(applyStr).
|
||||
then(() => {setCopyButtonState(CopyButtonState.Copied);}).
|
||||
catch(() => {setCopyButtonState(CopyButtonState.Error);});
|
||||
metricsService.capture('Copy Code', { length: applyStr.length }); // capture the length only
|
||||
|
||||
}, [metricsService, clipboardService, applyStr]);
|
||||
|
||||
const onApply = useCallback(() => {
|
||||
|
||||
inlineDiffService.startApplying({
|
||||
from: 'ClickApply',
|
||||
type: 'searchReplace',
|
||||
applyStr
|
||||
});
|
||||
metricsService.capture('Apply Code', { length: applyStr.length }); // capture the length only
|
||||
}, [metricsService, inlineDiffService, applyStr]);
|
||||
|
||||
const isSingleLine = !applyStr.includes('\n');
|
||||
|
||||
return <>
|
||||
<button
|
||||
className={`${isSingleLine ? "" : "void-px-1 void-py-0.5"} void-text-sm void-bg-void-bg-1 void-text-void-fg-1 hover:void-brightness-110 void-border void-border-vscode-input-border void-rounded`}
|
||||
onClick={onCopy}>
|
||||
|
||||
{copyButtonState}
|
||||
</button>
|
||||
<button
|
||||
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? "" : "void-px-1 void-py-0.5"} void-text-sm void-bg-void-bg-1 void-text-void-fg-1 hover:void-brightness-110 void-border void-border-vscode-input-border void-rounded`}
|
||||
onClick={onApply}>
|
||||
|
||||
Apply
|
||||
</button>
|
||||
</>;
|
||||
};
|
||||
|
||||
export const CodeSpan = ({ children, className }: {children: React.ReactNode;className?: string;}) => {
|
||||
return <code className={` void-bg-void-bg-1 void-px-1 void-rounded-sm void-font-mono void-font-medium void-break-all ${
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
className} `}>
|
||||
|
||||
|
||||
{children}
|
||||
</code>;
|
||||
};
|
||||
|
||||
const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocation: chatLocation, tokenIdx }: {token: Token | string;nested?: boolean;noSpace?: boolean;chatMessageLocation?: ChatMessageLocation;tokenIdx: string;}): JSX.Element => {
|
||||
|
||||
|
||||
// deal with built-in tokens first (assume marked token)
|
||||
const t = token as MarkedToken;
|
||||
// console.log('render:', t.raw)
|
||||
|
||||
if (t.type === "space") {
|
||||
return <span>{t.raw}</span>;
|
||||
}
|
||||
|
||||
if (t.type === "code") {
|
||||
const isCodeblockClosed = t.raw?.startsWith('```') && t.raw?.endsWith('```');
|
||||
|
||||
const applyBoxId = getApplyBoxId({
|
||||
threadId: chatLocation!.threadId,
|
||||
messageIdx: chatLocation!.messageIdx,
|
||||
tokenIdx: tokenIdx
|
||||
});
|
||||
|
||||
return <BlockCode
|
||||
initValue={t.text}
|
||||
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]}
|
||||
buttonsOnHover={<ApplyButtonsOnHover applyStr={t.text} applyBoxId={applyBoxId} />} />;
|
||||
|
||||
}
|
||||
|
||||
if (t.type === "heading") {
|
||||
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements;
|
||||
const headingClasses: {[h: string]: string;} = {
|
||||
h1: "text-4xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2",
|
||||
h2: "text-3xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2",
|
||||
h3: "text-2xl font-semibold mt-6 mb-4",
|
||||
h4: "text-xl font-semibold mt-6 mb-4",
|
||||
h5: "text-lg font-semibold mt-6 mb-4",
|
||||
h6: "text-base font-semibold mt-6 mb-4 text-gray-600"
|
||||
};
|
||||
return <HeadingTag className={headingClasses[HeadingTag]}>{t.text}</HeadingTag>;
|
||||
}
|
||||
|
||||
if (t.type === "table") {
|
||||
return (
|
||||
<div className={`${noSpace ? "" : "void-my-4"} void-overflow-x-auto`}>
|
||||
<table className="void-min-w-full void-border void-border-void-bg-2">
|
||||
<thead>
|
||||
<tr className="void-bg-void-bg-1">
|
||||
{t.header.map((cell: any, index: number) =>
|
||||
<th
|
||||
key={index}
|
||||
className="void-px-4 void-py-2 void-border void-border-void-bg-2 void-font-semibold"
|
||||
style={{ textAlign: t.align[index] || "left" }}>
|
||||
|
||||
{cell.raw}
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{t.rows.map((row: any[], rowIndex: number) =>
|
||||
<tr key={rowIndex} className={rowIndex % 2 === 0 ? "void-bg-white" : "void-bg-void-bg-1"}>
|
||||
{row.map((cell: any, cellIndex: number) =>
|
||||
<td
|
||||
key={cellIndex}
|
||||
className="void-px-4 void-py-2 void-border void-border-void-bg-2"
|
||||
style={{ textAlign: t.align[cellIndex] || "left" }}>
|
||||
|
||||
{cell.raw}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>);
|
||||
|
||||
}
|
||||
|
||||
if (t.type === "hr") {
|
||||
return <hr className="void-my-6 void-border-t void-border-void-bg-2" />;
|
||||
}
|
||||
|
||||
if (t.type === "blockquote") {
|
||||
return <blockquote className={`void-pl-4 void-border-l-4 void-border-void-bg-2 void-italic ${noSpace ? "" : "void-my-4"}`}>{t.text}</blockquote>;
|
||||
}
|
||||
|
||||
if (t.type === "list") {
|
||||
const ListTag = t.ordered ? "ol" : "ul";
|
||||
return (
|
||||
<ListTag
|
||||
start={t.start ? t.start : undefined}
|
||||
className={`void-list-inside void-pl-2 ${noSpace ? "" : "void-my-4"} ${t.ordered ? "void-list-decimal" : "void-list-disc"}`}>
|
||||
|
||||
{t.items.map((item, index) =>
|
||||
<li key={index} className={`${noSpace ? "" : "void-mb-4"}`}>
|
||||
{item.task &&
|
||||
<input type="checkbox" checked={item.checked} readOnly className="void-mr-2 void-form-checkbox" />
|
||||
}
|
||||
<span className="void-ml-1">
|
||||
<ChatMarkdownRender string={item.text} nested={true} />
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
</ListTag>);
|
||||
|
||||
}
|
||||
|
||||
if (t.type === "paragraph") {
|
||||
const contents = <>
|
||||
{t.tokens.map((token, index) =>
|
||||
<RenderToken key={index} token={token} tokenIdx={`${tokenIdx ? `${tokenIdx}-` : ''}${index}`} /> // assign a unique tokenId to nested components
|
||||
)}
|
||||
</>;
|
||||
if (nested) return contents;
|
||||
|
||||
return <p className={`${noSpace ? "" : "void-my-4"}`}>
|
||||
{contents}
|
||||
</p>;
|
||||
}
|
||||
|
||||
if (t.type === "html") {
|
||||
return (
|
||||
<p className={`${noSpace ? "" : "void-my-4"}`}>
|
||||
{t.raw}
|
||||
</p>);
|
||||
|
||||
}
|
||||
|
||||
if (t.type === "text" || t.type === "escape") {
|
||||
return <span>{t.raw}</span>;
|
||||
}
|
||||
|
||||
if (t.type === "def") {
|
||||
return <></>; // Definitions are typically not rendered
|
||||
}
|
||||
|
||||
if (t.type === "link") {
|
||||
return (
|
||||
<a
|
||||
className="void-underline"
|
||||
onClick={() => {window.open(t.href);}}
|
||||
href={t.href}
|
||||
title={t.title ?? undefined}>
|
||||
|
||||
{t.text}
|
||||
</a>);
|
||||
|
||||
}
|
||||
|
||||
if (t.type === "image") {
|
||||
return <img
|
||||
src={t.href}
|
||||
alt={t.text}
|
||||
title={t.title ?? undefined}
|
||||
className={`void-max4w-full void-h-auto void-rounded ${noSpace ? "" : "void-my-4"}`} />;
|
||||
|
||||
}
|
||||
|
||||
if (t.type === "strong") {
|
||||
return <strong className="void-font-semibold">{t.text}</strong>;
|
||||
}
|
||||
|
||||
if (t.type === "em") {
|
||||
return <em className="void-italic">{t.text}</em>;
|
||||
}
|
||||
|
||||
// inline code
|
||||
if (t.type === "codespan") {
|
||||
return (
|
||||
<CodeSpan>
|
||||
{t.text}
|
||||
</CodeSpan>);
|
||||
|
||||
}
|
||||
|
||||
if (t.type === "br") {
|
||||
return <br />;
|
||||
}
|
||||
|
||||
// strikethrough
|
||||
if (t.type === "del") {
|
||||
return <del className="void-line-through">{t.text}</del>;
|
||||
}
|
||||
|
||||
// default
|
||||
return (
|
||||
<div className="void-bg-orange-50 void-rounded-sm void-overflow-hidden void-p-2">
|
||||
<span className="void-text-sm void-text-orange-500">Unknown type:</span>
|
||||
{t.raw}
|
||||
</div>);
|
||||
|
||||
};
|
||||
|
||||
export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatMessageLocation }: {string: string;nested?: boolean;noSpace?: boolean;chatMessageLocation?: ChatMessageLocation;}) => {
|
||||
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
|
||||
return (
|
||||
<>
|
||||
{tokens.map((token, index) =>
|
||||
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} chatMessageLocation={chatMessageLocation} tokenIdx={index + ''} />
|
||||
)}
|
||||
</>);
|
||||
|
||||
};
|
||||
|
|
@ -1,23 +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 React, { useEffect, useState } from 'react';
|
||||
import { useIsDark, useSidebarState } from '../util/services.js';
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js';
|
||||
import { QuickEditChat } from './QuickEditChat.js';
|
||||
import { QuickEditPropsType } from '../../../quickEditActions.js';
|
||||
|
||||
export const QuickEdit = (props: QuickEditPropsType) => {
|
||||
|
||||
const isDark = useIsDark();
|
||||
|
||||
return <div className={`void-scope ${isDark ? "void-dark" : ""}`}>
|
||||
<ErrorBoundary>
|
||||
<QuickEditChat {...props} />
|
||||
</ErrorBoundary>
|
||||
</div>;
|
||||
|
||||
|
||||
};
|
||||
|
|
@ -1,127 +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 React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
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, VoidChatArea } 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';
|
||||
import { isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
|
||||
|
||||
export const QuickEditChat = ({
|
||||
diffareaid,
|
||||
initStreamingDiffZoneId,
|
||||
onChangeHeight,
|
||||
onChangeText: onChangeText_,
|
||||
textAreaRef: textAreaRef_,
|
||||
initText
|
||||
}: QuickEditPropsType) => {
|
||||
|
||||
const accessor = useAccessor();
|
||||
const inlineDiffsService = accessor.get('IInlineDiffsService');
|
||||
const sizerRef = useRef<HTMLDivElement | null>(null);
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const textAreaFnsRef = useRef<TextAreaFns | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const inputContainer = sizerRef.current;
|
||||
if (!inputContainer) return;
|
||||
// only observing 1 element
|
||||
let resizeObserver: ResizeObserver | undefined;
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
const height = entries[0].borderBoxSize[0].blockSize;
|
||||
onChangeHeight(height);
|
||||
});
|
||||
resizeObserver.observe(inputContainer);
|
||||
return () => {resizeObserver?.disconnect();};
|
||||
}, [onChangeHeight]);
|
||||
|
||||
|
||||
const settingsState = useSettingsState();
|
||||
|
||||
// state of current message
|
||||
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')); // the user's instructions
|
||||
const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+K', settingsState);
|
||||
|
||||
const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState<number | null>(initStreamingDiffZoneId);
|
||||
const isStreaming = currStreamingDiffZoneRef.current !== null;
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
if (isDisabled) return;
|
||||
if (currStreamingDiffZoneRef.current !== null) return;
|
||||
textAreaFnsRef.current?.disable();
|
||||
|
||||
const id = inlineDiffsService.startApplying({
|
||||
from: 'QuickEdit',
|
||||
type: 'rewrite',
|
||||
diffareaid: diffareaid
|
||||
});
|
||||
setCurrentlyStreamingDiffZone(id ?? null);
|
||||
}, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, inlineDiffsService, diffareaid]);
|
||||
|
||||
const onInterrupt = useCallback(() => {
|
||||
if (currStreamingDiffZoneRef.current === null) return;
|
||||
inlineDiffsService.interruptStreaming(currStreamingDiffZoneRef.current);
|
||||
setCurrentlyStreamingDiffZone(null);
|
||||
textAreaFnsRef.current?.enable();
|
||||
}, [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();
|
||||
|
||||
const chatAreaRef = useRef<HTMLDivElement | null>(null);
|
||||
return <div ref={sizerRef} style={{ maxWidth: 450 }} className={`void-py-2 void-w-full`}>
|
||||
<VoidChatArea
|
||||
divRef={chatAreaRef}
|
||||
onSubmit={onSubmit}
|
||||
onAbort={onInterrupt}
|
||||
onClose={onX}
|
||||
isStreaming={isStreaming}
|
||||
isDisabled={isDisabled}
|
||||
featureName="Ctrl+K"
|
||||
className="void-py-2 void-w-full"
|
||||
onClickAnywhere={() => {textAreaRef.current?.focus();}}>
|
||||
|
||||
<VoidInputBox2
|
||||
className="void-px-1"
|
||||
initValue={initText}
|
||||
ref={useCallback((r: HTMLTextAreaElement | null) => {
|
||||
textAreaRef.current = r;
|
||||
textAreaRef_(r);
|
||||
r?.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape')
|
||||
onX();
|
||||
});
|
||||
}, [textAreaRef_, onX])}
|
||||
fnsRef={textAreaFnsRef}
|
||||
placeholder="Enter instructions..."
|
||||
onChangeText={useCallback((newStr: string) => {
|
||||
setInstructionsAreEmpty(!newStr);
|
||||
onChangeText_(newStr);
|
||||
}, [onChangeText_])}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit();
|
||||
return;
|
||||
}
|
||||
}}
|
||||
multiline={true} />
|
||||
|
||||
</VoidChatArea>
|
||||
</div>;
|
||||
|
||||
|
||||
};
|
||||
|
|
@ -1,10 +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 { mountFnGenerator } from '../util/mountFnGenerator.js';
|
||||
import { QuickEdit } from './QuickEdit.js';
|
||||
|
||||
|
||||
export const mountCtrlK = mountFnGenerator(QuickEdit);
|
||||
|
|
@ -1,66 +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 React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
errorInfo: ErrorInfo | null;
|
||||
}
|
||||
|
||||
class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): Partial<State> {
|
||||
return {
|
||||
hasError: true,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||
this.setState({
|
||||
error,
|
||||
errorInfo
|
||||
});
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
if (this.state.hasError && this.state.error) {
|
||||
// If a custom fallback is provided, use it
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
// Use ErrorDisplay component as the default error UI
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={this.state.error + ''}
|
||||
fullError={this.state.error}
|
||||
onDismiss={this.props.onDismiss || null} />);
|
||||
|
||||
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
|
|
@ -1,81 +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 React, { useEffect, useState } from 'react';
|
||||
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
|
||||
import { errorDetails } from '../../../../../../../workbench/contrib/void/common/llmMessageTypes.js';
|
||||
import { useSettingsState } from '../util/services.js';
|
||||
|
||||
|
||||
export const ErrorDisplay = ({
|
||||
message: message_,
|
||||
fullError,
|
||||
onDismiss,
|
||||
showDismiss
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}: {message: string;fullError: Error | null;onDismiss: (() => void) | null;showDismiss?: boolean;}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const details = errorDetails(fullError);
|
||||
const isExpandable = !!details;
|
||||
|
||||
const message = message_ + '';
|
||||
|
||||
return (
|
||||
<div className={`void-rounded-lg void-border void-border-red-200 void-bg-red-50 void-p-4 void-overflow-auto`}>
|
||||
{/* Header */}
|
||||
<div className="void-flex void-items-start void-justify-between">
|
||||
<div className="void-flex void-gap-3">
|
||||
<AlertCircle className="void-h-5 void-w-5 void-text-red-600 void-mt-0.5" />
|
||||
<div className="void-flex-1">
|
||||
<h3 className="void-font-semibold void-text-red-800">
|
||||
{/* eg Error */}
|
||||
Error
|
||||
</h3>
|
||||
<p className="void-text-red-700 void-mt-1">
|
||||
{/* eg Something went wrong */}
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="void-flex void-gap-2">
|
||||
{isExpandable &&
|
||||
<button className="void-text-red-600 hover:void-text-red-800 void-p-1 void-rounded"
|
||||
onClick={() => setIsExpanded(!isExpanded)}>
|
||||
|
||||
{isExpanded ?
|
||||
<ChevronUp className="void-h-5 void-w-5" /> :
|
||||
|
||||
<ChevronDown className="void-h-5 void-w-5" />
|
||||
}
|
||||
</button>
|
||||
}
|
||||
{showDismiss && onDismiss &&
|
||||
<button className="void-text-red-600 hover:void-text-red-800 void-p-1 void-rounded"
|
||||
onClick={onDismiss}>
|
||||
|
||||
<X className="void-h-5 void-w-5" />
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expandable Details */}
|
||||
{isExpanded && details &&
|
||||
<div className="void-mt-4 void-space-y-3 void-border-t void-border-red-200 void-pt-3 void-overflow-auto">
|
||||
<div>
|
||||
<span className="void-font-semibold void-text-red-800">Full Error: </span>
|
||||
<pre className="void-text-red-700">{details}</pre>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>);
|
||||
|
||||
};
|
||||
|
|
@ -1,69 +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 React, { useEffect, useState } from 'react';
|
||||
import { mountFnGenerator } from '../util/mountFnGenerator.js';
|
||||
|
||||
// import { SidebarSettings } from './SidebarSettings.js';
|
||||
|
||||
|
||||
import { useIsDark, useSidebarState } from '../util/services.js';
|
||||
// import { SidebarThreadSelector } from './SidebarThreadSelector.js';
|
||||
// import { SidebarChat } from './SidebarChat.js';
|
||||
|
||||
import '../styles.css';
|
||||
import { SidebarChat } from './SidebarChat.js';
|
||||
import ErrorBoundary from './ErrorBoundary.js';
|
||||
|
||||
export const Sidebar = ({ className }: {className: string;}) => {
|
||||
const sidebarState = useSidebarState();
|
||||
const { currentTab: tab } = sidebarState;
|
||||
|
||||
// const isDark = useIsDark()
|
||||
return <div
|
||||
className={`void-scope`} // ${isDark ? 'dark' : ''}
|
||||
style={{ width: '100%', height: '100%' }}>
|
||||
|
||||
<div
|
||||
// default background + text styles for sidebar
|
||||
className={` void-w-full void-h-full void-bg-void-bg-2 void-text-void-fg-1 `}>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{/* <span onClick={() => {
|
||||
const tabs = ['chat', 'settings', 'threadSelector']
|
||||
const index = tabs.indexOf(tab)
|
||||
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
|
||||
}}>clickme {tab}</span> */}
|
||||
|
||||
{/* <div className={`w-full h-auto mb-2 ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow z-10`}>
|
||||
<ErrorBoundary>
|
||||
<SidebarThreadSelector />
|
||||
</ErrorBoundary>
|
||||
</div> */}
|
||||
|
||||
<div className={`void-w-full void-h-full ${tab === 'chat' ? "" : "void-hidden"}`}>
|
||||
<ErrorBoundary>
|
||||
<SidebarChat />
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* <ErrorBoundary>
|
||||
<ModelSelectionSettings />
|
||||
</ErrorBoundary> */}
|
||||
</div>
|
||||
|
||||
{/* <div className={`w-full h-full ${tab === 'settings' ? '' : 'hidden'}`}>
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings />
|
||||
</ErrorBoundary>
|
||||
</div> */}
|
||||
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
|
||||
};
|
||||
|
|
@ -1,916 +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 React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
|
||||
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js';
|
||||
import { ChatMessage, StagingInfo, StagingSelectionItem } from '../../../chatThreadService.js';
|
||||
|
||||
import { BlockCode } from '../markdown/BlockCode.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
|
||||
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
|
||||
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
||||
import { filenameToVscodeLanguage } from '../../../helpers/detectLanguage.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
||||
import { Pencil, X } from 'lucide-react';
|
||||
import { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
|
||||
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
|
||||
import { ChatMessageLocation } from '../../../searchAndReplaceService.js';
|
||||
|
||||
|
||||
|
||||
export const IconX = ({ size, className = '', ...props }: {size: number;className?: string;} & React.SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
className={className}
|
||||
{...props}>
|
||||
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
d='M6 18 18 6M6 6l12 12' />
|
||||
|
||||
</svg>);
|
||||
|
||||
};
|
||||
|
||||
const IconArrowUp = ({ size, className = '' }: {size: number;className?: string;}) => {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<path
|
||||
fill="black"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z">
|
||||
</path>
|
||||
</svg>);
|
||||
|
||||
};
|
||||
|
||||
|
||||
const IconSquare = ({ size, className = '' }: {size: number;className?: string;}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
stroke="black"
|
||||
fill="black"
|
||||
strokeWidth="0"
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
height={size}
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<rect x="2" y="2" width="20" height="20" rx="4" ry="4" />
|
||||
</svg>);
|
||||
|
||||
};
|
||||
|
||||
|
||||
export const IconWarning = ({ size, className = '' }: {size: number;className?: string;}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
strokeWidth="0"
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
height={size}
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.56 1h.88l6.54 12.26-.44.74H1.44L1 13.26 7.56 1zM8 2.28L2.28 13H13.7L8 2.28zM8.625 12v-1h-1.25v1h1.25zm-1.25-2V6h1.25v4h-1.25z" />
|
||||
|
||||
</svg>);
|
||||
|
||||
};
|
||||
|
||||
|
||||
export const IconLoading = ({ className = '' }: {className?: string;}) => {
|
||||
|
||||
const [loadingText, setLoadingText] = useState('.');
|
||||
|
||||
useEffect(() => {
|
||||
let intervalId;
|
||||
|
||||
// Function to handle the animation
|
||||
const toggleLoadingText = () => {
|
||||
if (loadingText === '...') {
|
||||
setLoadingText('.');
|
||||
} else {
|
||||
setLoadingText(loadingText + '.');
|
||||
}
|
||||
};
|
||||
|
||||
// Start the animation loop
|
||||
intervalId = setInterval(toggleLoadingText, 300);
|
||||
|
||||
// Cleanup function to clear the interval when component unmounts
|
||||
return () => clearInterval(intervalId);
|
||||
}, [loadingText, setLoadingText]);
|
||||
|
||||
return <div className={`${className}`}>{loadingText}</div>;
|
||||
|
||||
};
|
||||
|
||||
|
||||
interface VoidChatAreaProps {
|
||||
// Required
|
||||
children: React.ReactNode; // This will be the input component
|
||||
|
||||
// Form controls
|
||||
onSubmit: () => void;
|
||||
onAbort: () => void;
|
||||
isStreaming: boolean;
|
||||
isDisabled?: boolean;
|
||||
divRef?: React.RefObject<HTMLDivElement>;
|
||||
|
||||
// UI customization
|
||||
featureName: FeatureName;
|
||||
className?: string;
|
||||
showModelDropdown?: boolean;
|
||||
showSelections?: boolean;
|
||||
showProspectiveSelections?: boolean;
|
||||
|
||||
staging?: StagingInfo;
|
||||
setStaging?: (s: StagingInfo) => void;
|
||||
// selections?: any[];
|
||||
// onSelectionsChange?: (selections: any[]) => void;
|
||||
|
||||
onClickAnywhere?: () => void;
|
||||
// Optional close button
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
|
||||
children,
|
||||
onSubmit,
|
||||
onAbort,
|
||||
onClose,
|
||||
onClickAnywhere,
|
||||
divRef,
|
||||
isStreaming = false,
|
||||
isDisabled = false,
|
||||
className = '',
|
||||
showModelDropdown = true,
|
||||
featureName,
|
||||
showSelections = false,
|
||||
showProspectiveSelections = true,
|
||||
staging,
|
||||
setStaging
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
ref={divRef}
|
||||
className={` void-flex void-flex-col void-gap-1 void-p-2 void-relative void-input void-text-left void-shrink-0 void-transition-all void-duration-200 void-rounded-md void-bg-vscode-input-bg void-border void-border-void-border-3 focus-within:void-border-void-border-1 hover:void-border-void-border-1 ${
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
className} `}
|
||||
|
||||
onClick={(e) => {
|
||||
onClickAnywhere?.();
|
||||
}}>
|
||||
|
||||
{/* Selections section */}
|
||||
{showSelections && staging && setStaging &&
|
||||
<SelectedFiles
|
||||
type='staging'
|
||||
selections={staging.selections || []}
|
||||
setSelections={(selections) => setStaging({ ...staging, selections })}
|
||||
showProspectiveSelections={showProspectiveSelections} />
|
||||
|
||||
}
|
||||
|
||||
{/* Input section */}
|
||||
<div className="void-relative void-w-full">
|
||||
{children}
|
||||
|
||||
{/* Close button (X) if onClose is provided */}
|
||||
{onClose &&
|
||||
<div className="void-absolute -void-top-1 -void-right-1 void-cursor-pointer void-z-1">
|
||||
<IconX
|
||||
size={12}
|
||||
className="void-stroke-[2] void-opacity-80 void-text-void-fg-3 hover:void-brightness-95"
|
||||
onClick={onClose} />
|
||||
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Bottom row */}
|
||||
<div className="void-flex void-flex-row void-justify-between void-items-end void-gap-1">
|
||||
{showModelDropdown &&
|
||||
<div className="void-max-w-[150px] [&_select]:!void-border-none [&_select]:!void-outline-none void-flex-grow"
|
||||
onClick={(e) => {e.preventDefault();e.stopPropagation();}}>
|
||||
<ModelDropdown featureName={featureName} />
|
||||
</div>
|
||||
}
|
||||
|
||||
{isStreaming ?
|
||||
<ButtonStop onClick={onAbort} /> :
|
||||
|
||||
<ButtonSubmit
|
||||
onClick={onSubmit}
|
||||
disabled={isDisabled} />
|
||||
|
||||
}
|
||||
</div>
|
||||
</div>);
|
||||
|
||||
};
|
||||
|
||||
const useResizeObserver = () => {
|
||||
const ref = useRef(null);
|
||||
const [dimensions, setDimensions] = useState({ height: 0, width: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
if (entries.length > 0) {
|
||||
const entry = entries[0];
|
||||
setDimensions({
|
||||
height: entry.contentRect.height,
|
||||
width: entry.contentRect.width
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(ref.current);
|
||||
|
||||
return () => {
|
||||
if (ref.current)
|
||||
resizeObserver.unobserve(ref.current);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return [ref, dimensions] as const;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
const DEFAULT_BUTTON_SIZE = 22;
|
||||
export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Required<Pick<ButtonProps, 'disabled'>>) => {
|
||||
|
||||
return <button
|
||||
type='button'
|
||||
className={`void-rounded-full void-flex-shrink-0 void-flex-grow-0 void-flex void-items-center void-justify-center ${
|
||||
disabled ? "void-bg-vscode-disabled-fg void-cursor-default" : "void-bg-white void-cursor-pointer"} ${
|
||||
className} `}
|
||||
|
||||
{...props}>
|
||||
|
||||
<IconArrowUp size={DEFAULT_BUTTON_SIZE} className="void-stroke-[2] void-p-[2px]" />
|
||||
</button>;
|
||||
};
|
||||
|
||||
export const ButtonStop = ({ className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
|
||||
return <button
|
||||
className={`void-rounded-full void-flex-shrink-0 void-flex-grow-0 void-cursor-pointer void-flex void-items-center void-justify-center void-bg-white ${
|
||||
|
||||
className} `}
|
||||
|
||||
type='button'
|
||||
{...props}>
|
||||
|
||||
<IconSquare size={DEFAULT_BUTTON_SIZE} className="void-stroke-[3] void-p-[7px]" />
|
||||
</button>;
|
||||
};
|
||||
|
||||
|
||||
const ScrollToBottomContainer = ({ children, className, style, scrollContainerRef }: {children: React.ReactNode;className?: string;style?: React.CSSProperties;scrollContainerRef: React.MutableRefObject<HTMLDivElement | null>;}) => {
|
||||
const [isAtBottom, setIsAtBottom] = useState(true); // Start at bottom
|
||||
|
||||
const divRef = scrollContainerRef;
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (divRef.current) {
|
||||
divRef.current.scrollTop = divRef.current.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
const onScroll = () => {
|
||||
const div = divRef.current;
|
||||
if (!div) return;
|
||||
|
||||
const isBottom = Math.abs(
|
||||
div.scrollHeight - div.clientHeight - div.scrollTop
|
||||
) < 4;
|
||||
|
||||
setIsAtBottom(isBottom);
|
||||
};
|
||||
|
||||
// When children change (new messages added)
|
||||
useEffect(() => {
|
||||
if (isAtBottom) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}, [children, isAtBottom]); // Dependency on children to detect new messages
|
||||
|
||||
// Initial scroll to bottom
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
// options={{ vertical: ScrollbarVisibility.Auto, horizontal: ScrollbarVisibility.Auto }}
|
||||
ref={divRef}
|
||||
onScroll={onScroll}
|
||||
className={className}
|
||||
style={style}>
|
||||
|
||||
{children}
|
||||
</div>);
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
const getBasename = (pathStr: string) => {
|
||||
// 'unixify' path
|
||||
pathStr = pathStr.replace(/[/\\]+/g, '/'); // replace any / or \ or \\ with /
|
||||
const parts = pathStr.split('/'); // split on /
|
||||
return parts[parts.length - 1];
|
||||
};
|
||||
|
||||
export const SelectedFiles = (
|
||||
{ type, selections, setSelections, showProspectiveSelections
|
||||
|
||||
}: {type: 'past';selections: StagingSelectionItem[];setSelections?: undefined;showProspectiveSelections?: undefined;} | {type: 'staging';selections: StagingSelectionItem[];setSelections: ((newSelections: StagingSelectionItem[]) => void);showProspectiveSelections?: boolean;}) =>
|
||||
{
|
||||
|
||||
// index -> isOpened
|
||||
const [selectionIsOpened, setSelectionIsOpened] = useState<(boolean)[]>(selections?.map(() => false) ?? []);
|
||||
|
||||
// state for tracking hover on clear all button
|
||||
const [isClearHovered, setIsClearHovered] = useState(false);
|
||||
|
||||
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: StagingSelectionItem[] = [];
|
||||
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.type === 'File' && 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 (
|
||||
<div className="void-flex void-items-center void-flex-wrap void-text-left void-relative">
|
||||
|
||||
{allSelections.map((selection, i) => {
|
||||
|
||||
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 ? "void-w-full" : ""} `}>
|
||||
|
||||
|
||||
{/* selection summary */}
|
||||
<div // container for item and its delete button (if it's last)
|
||||
className="void-flex void-items-center void-gap-1 void-mr-0.5 void-my-0.5">
|
||||
|
||||
<div // styled summary box
|
||||
className={`void-flex void-items-center void-gap-0.5 void-relative void-px-1 void-w-fit void-h-fit void-select-none ${
|
||||
|
||||
|
||||
|
||||
isThisSelectionProspective ? "void-bg-void-1 void-text-void-fg-3 void-opacity-80" : "void-bg-void-bg-3 hover:void-brightness-95 void-text-void-fg-1"} void-text-xs void-text-nowrap void-border void-rounded-sm ${
|
||||
|
||||
isClearHovered && !isThisSelectionProspective ? "void-border-void-border-1" : "void-border-void-border-2"} hover:void-border-void-border-1 void-transition-all void-duration-150`}
|
||||
|
||||
onClick={() => {
|
||||
if (isThisSelectionProspective) {// add prospective selection to selections
|
||||
if (type !== 'staging') return; // (never)
|
||||
setSelections([...selections, selection]);
|
||||
|
||||
} 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="void-cursor-pointer void-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="void-stroke-[2]" />
|
||||
</span>}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
hover:opacity-60
|
||||
transition-all duration-150
|
||||
cursor-pointer
|
||||
`}
|
||||
onClick={() => { setSelections([]) }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
} */}
|
||||
</div>
|
||||
{/* selection text */}
|
||||
{isThisSelectionOpened &&
|
||||
<div
|
||||
className="void-w-full void-px-1 void-rounded-sm void-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}>
|
||||
{/* divider between `selections` and `prospectiveSelections` */}
|
||||
{/* {selections.length > 0 && i === selections.length && <div className='w-full'></div>} */}
|
||||
{selectionHTML}
|
||||
</Fragment>;
|
||||
|
||||
})}
|
||||
|
||||
|
||||
</div>);
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
type ChatBubbleMode = 'display' | 'edit';
|
||||
const ChatBubble = ({ chatMessage, isLoading, messageIdx }: {chatMessage: ChatMessage;messageIdx?: number;isLoading?: boolean;}) => {
|
||||
|
||||
const role = chatMessage.role;
|
||||
|
||||
const accessor = useAccessor();
|
||||
const chatThreadsService = accessor.get('IChatThreadService');
|
||||
|
||||
// edit mode state
|
||||
const [staging, setStaging] = chatThreadsService._useFocusedStagingState(messageIdx);
|
||||
const mode: ChatBubbleMode = staging.isBeingEdited ? 'edit' : 'display';
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [isDisabled, setIsDisabled] = useState(false);
|
||||
const [textAreaRefState, setTextAreaRef] = useState<HTMLTextAreaElement | null>(null);
|
||||
const textAreaFnsRef = useRef<TextAreaFns | null>(null);
|
||||
// initialize on first render, and when edit was just enabled
|
||||
const _mustInitialize = useRef(true);
|
||||
const _justEnabledEdit = useRef(false);
|
||||
useEffect(() => {
|
||||
const canInitialize = role === 'user' && mode === 'edit' && textAreaRefState;
|
||||
const shouldInitialize = _justEnabledEdit.current || _mustInitialize.current;
|
||||
if (canInitialize && shouldInitialize) {
|
||||
setStaging({
|
||||
...staging,
|
||||
selections: chatMessage.selections || []
|
||||
});
|
||||
if (textAreaFnsRef.current)
|
||||
textAreaFnsRef.current.setValue(chatMessage.displayContent || '');
|
||||
|
||||
textAreaRefState.focus();
|
||||
|
||||
_justEnabledEdit.current = false;
|
||||
_mustInitialize.current = false;
|
||||
}
|
||||
|
||||
}, [role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current]);
|
||||
const EditSymbol = mode === 'display' ? Pencil : X;
|
||||
const onOpenEdit = () => {
|
||||
setStaging({ ...staging, isBeingEdited: true });
|
||||
chatThreadsService.setFocusedMessageIdx(messageIdx);
|
||||
_justEnabledEdit.current = true;
|
||||
};
|
||||
const onCloseEdit = () => {
|
||||
setIsFocused(false);
|
||||
setIsHovered(false);
|
||||
setStaging({ ...staging, isBeingEdited: false });
|
||||
chatThreadsService.setFocusedMessageIdx(undefined);
|
||||
|
||||
};
|
||||
// set chat bubble contents
|
||||
let chatbubbleContents: React.ReactNode;
|
||||
if (role === 'user') {
|
||||
if (mode === 'display') {
|
||||
chatbubbleContents = <>
|
||||
<SelectedFiles type='past' selections={chatMessage.selections || []} />
|
||||
{chatMessage.displayContent}
|
||||
</>;
|
||||
} else
|
||||
if (mode === 'edit') {
|
||||
|
||||
const onSubmit = async () => {
|
||||
|
||||
if (isDisabled) return;
|
||||
if (!textAreaRefState) return;
|
||||
if (messageIdx === undefined) return;
|
||||
|
||||
// cancel any streams on this thread
|
||||
const thread = chatThreadsService.getCurrentThread();
|
||||
chatThreadsService.cancelStreaming(thread.id);
|
||||
|
||||
// reset state
|
||||
setStaging({ ...staging, isBeingEdited: false });
|
||||
chatThreadsService.setFocusedMessageIdx(undefined);
|
||||
|
||||
// stream the edit
|
||||
const userMessage = textAreaRefState.value;
|
||||
await chatThreadsService.editUserMessageAndStreamResponse(userMessage, messageIdx);
|
||||
};
|
||||
|
||||
const onAbort = () => {
|
||||
const threadId = chatThreadsService.state.currentThreadId;
|
||||
chatThreadsService.cancelStreaming(threadId);
|
||||
};
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Escape') {
|
||||
onCloseEdit();
|
||||
}
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
if (!chatMessage.content && !isLoading) {// don't show if empty and not loading (if loading, want to show)
|
||||
return null;
|
||||
}
|
||||
|
||||
chatbubbleContents = <>
|
||||
<VoidChatArea
|
||||
onSubmit={onSubmit}
|
||||
onAbort={onAbort}
|
||||
isStreaming={false}
|
||||
isDisabled={isDisabled}
|
||||
showSelections={true}
|
||||
showProspectiveSelections={false}
|
||||
featureName="Ctrl+L"
|
||||
staging={staging}
|
||||
setStaging={setStaging}>
|
||||
|
||||
<VoidInputBox2
|
||||
ref={setTextAreaRef}
|
||||
className="void-min-h-[81px] void-max-h-[500px] void-p-1"
|
||||
placeholder="Edit your message..."
|
||||
onChangeText={(text) => setIsDisabled(!text)}
|
||||
onFocus={() => {
|
||||
setIsFocused(true);
|
||||
chatThreadsService.setFocusedMessageIdx(messageIdx);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsFocused(false);
|
||||
}}
|
||||
onKeyDown={onKeyDown}
|
||||
fnsRef={textAreaFnsRef}
|
||||
multiline={true} />
|
||||
|
||||
</VoidChatArea>
|
||||
</>;
|
||||
}
|
||||
} else
|
||||
if (role === 'assistant') {
|
||||
const thread = chatThreadsService.getCurrentThread();
|
||||
|
||||
const chatMessageLocation: ChatMessageLocation = {
|
||||
threadId: thread.id,
|
||||
messageIdx: messageIdx!
|
||||
};
|
||||
|
||||
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent ?? ''} chatMessageLocation={chatMessageLocation} />;
|
||||
}
|
||||
|
||||
return <div
|
||||
// align chatbubble accoridng to role
|
||||
className={` void-relative ${
|
||||
|
||||
mode === 'edit' ? "void-px-2 void-w-full void-max-w-full" :
|
||||
role === 'user' ? `void-px-2 void-self-end void-w-fit void-max-w-full void-whitespace-pre-wrap` :
|
||||
role === 'assistant' ? `void-px-2 void-self-start void-w-full void-max-w-full` : ""} ${
|
||||
|
||||
role !== 'assistant' ? "void-my-2" : ""} `}
|
||||
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}>
|
||||
|
||||
<div
|
||||
// style chatbubble according to role
|
||||
className={` void-text-left void-rounded-lg void-max-w-full ${
|
||||
|
||||
|
||||
mode === 'edit' ? "" :
|
||||
role === 'user' ? "void-p-2 void-bg-void-bg-1 void-text-void-fg-1 void-overflow-x-auto" :
|
||||
role === 'assistant' ? "void-px-2 void-overflow-x-auto" : ""} `}>
|
||||
|
||||
|
||||
|
||||
{chatbubbleContents}
|
||||
{isLoading && <IconLoading className="void-opacity-50 void-text-sm void-px-2" />}
|
||||
</div>
|
||||
|
||||
{/* edit button */}
|
||||
{role === 'user' && <EditSymbol
|
||||
size={18}
|
||||
className={` void-absolute -void-top-1 void-right-1 void-translate-x-0 -void-translate-y-0 void-cursor-pointer void-z-1 void-p-[2px] void-bg-void-bg-1 void-border void-border-void-border-1 void-rounded-md void-transition-opacity void-duration-200 void-ease-in-out ${
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
isHovered || isFocused && mode === 'edit' ? "void-opacity-100" : "void-opacity-0"} `}
|
||||
|
||||
onClick={() => {
|
||||
if (mode === 'display') {
|
||||
onOpenEdit();
|
||||
} else if (mode === 'edit') {
|
||||
onCloseEdit();
|
||||
}
|
||||
}} />
|
||||
}
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
||||
export const SidebarChat = () => {
|
||||
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const textAreaFnsRef = useRef<TextAreaFns | null>(null);
|
||||
|
||||
const accessor = useAccessor();
|
||||
// const modelService = accessor.get('IModelService')
|
||||
const commandService = accessor.get('ICommandService');
|
||||
const chatThreadsService = accessor.get('IChatThreadService');
|
||||
|
||||
const settingsState = useSettingsState();
|
||||
// ----- HIGHER STATE -----
|
||||
// sidebar state
|
||||
const sidebarStateService = accessor.get('ISidebarStateService');
|
||||
useEffect(() => {
|
||||
const disposables: IDisposable[] = [];
|
||||
disposables.push(
|
||||
sidebarStateService.onDidFocusChat(() => {!chatThreadsService.isFocusingMessage() && textAreaRef.current?.focus();}),
|
||||
sidebarStateService.onDidBlurChat(() => {!chatThreadsService.isFocusingMessage() && textAreaRef.current?.blur();})
|
||||
);
|
||||
return () => disposables.forEach((d) => d.dispose());
|
||||
}, [sidebarStateService, textAreaRef]);
|
||||
|
||||
const { isHistoryOpen } = useSidebarState();
|
||||
|
||||
// threads state
|
||||
const chatThreadsState = useChatThreadsState();
|
||||
|
||||
const currentThread = chatThreadsService.getCurrentThread();
|
||||
const previousMessages = currentThread?.messages ?? [];
|
||||
const [staging, setStaging] = chatThreadsService._useFocusedStagingState();
|
||||
|
||||
// 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 current message
|
||||
const initVal = '';
|
||||
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!initVal);
|
||||
|
||||
const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+L', settingsState);
|
||||
|
||||
const [sidebarRef, sidebarDimensions] = useResizeObserver();
|
||||
const [chatAreaRef, chatAreaDimensions] = useResizeObserver();
|
||||
const [historyRef, historyDimensions] = useResizeObserver();
|
||||
|
||||
useScrollbarStyles(sidebarRef);
|
||||
|
||||
|
||||
const onSubmit = useCallback(async () => {
|
||||
|
||||
if (isDisabled) return;
|
||||
if (isStreaming) return;
|
||||
|
||||
// send message to LLM
|
||||
const userMessage = textAreaRef.current?.value ?? '';
|
||||
await chatThreadsService.addUserMessageAndStreamResponse(userMessage);
|
||||
|
||||
setStaging({ ...staging, selections: [] }); // clear staging
|
||||
textAreaFnsRef.current?.setValue('');
|
||||
textAreaRef.current?.focus(); // focus input after submit
|
||||
|
||||
}, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, staging, setStaging]);
|
||||
|
||||
const onAbort = () => {
|
||||
const threadId = currentThread.id;
|
||||
chatThreadsService.cancelStreaming(threadId);
|
||||
};
|
||||
|
||||
// const [_test_messages, _set_test_messages] = useState<string[]>([])
|
||||
|
||||
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_L_ACTION_ID)?.getLabel();
|
||||
|
||||
// scroll to top on thread switch
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
if (isHistoryOpen)
|
||||
scrollContainerRef.current?.scrollTo({ top: 0, left: 0 });
|
||||
}, [isHistoryOpen, currentThread.id]);
|
||||
|
||||
|
||||
const prevMessagesHTML = useMemo(() => {
|
||||
return previousMessages.map((message, i) =>
|
||||
<ChatBubble key={`${message.displayContent}-${i}`} chatMessage={message} messageIdx={i} />
|
||||
);
|
||||
}, [previousMessages]);
|
||||
|
||||
|
||||
const threadSelector = <div ref={historyRef}
|
||||
className={`void-w-full void-h-auto ${isHistoryOpen ? "" : "void-hidden"} void-ring-2 void-ring-widget-shadow void-ring-inset void-z-10`}>
|
||||
|
||||
<SidebarThreadSelector />
|
||||
</div>;
|
||||
|
||||
|
||||
|
||||
const messagesHTML = <ScrollToBottomContainer
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
className={` void-w-full void-h-auto void-flex void-flex-col void-overflow-x-hidden void-overflow-y-auto void-py-4 ${
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
prevMessagesHTML.length === 0 && !messageSoFar ? "void-hidden" : ""} `}
|
||||
|
||||
style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - chatAreaDimensions.height - 36 }} // the height of the previousMessages is determined by all other heights
|
||||
>
|
||||
{/* previous messages */}
|
||||
{prevMessagesHTML}
|
||||
|
||||
{/* message stream */}
|
||||
<ChatBubble chatMessage={{ role: 'assistant', content: messageSoFar ?? '', displayContent: messageSoFar || null }} isLoading={isStreaming} />
|
||||
|
||||
|
||||
{/* error message */}
|
||||
{latestError === undefined ? null :
|
||||
<div className="void-px-2">
|
||||
<ErrorDisplay
|
||||
message={latestError.message}
|
||||
fullError={latestError.fullError}
|
||||
onDismiss={() => {chatThreadsService.dismissStreamError(currentThread.id);}}
|
||||
showDismiss={true} />
|
||||
|
||||
|
||||
<WarningBox className="void-text-sm void-my-2 void-mx-4" onClick={() => {commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID);}} text='Open settings' />
|
||||
</div>
|
||||
}
|
||||
</ScrollToBottomContainer>;
|
||||
|
||||
|
||||
const onChangeText = useCallback((newStr: string) => {
|
||||
setInstructionsAreEmpty(!newStr);
|
||||
}, [setInstructionsAreEmpty]);
|
||||
const onKeyDown = useCallback((e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit();
|
||||
}
|
||||
}, [onSubmit]);
|
||||
const inputForm = <div className={`void-right-0 void-left-0 void-m-2 void-z-[999] void-overflow-hidden ${previousMessages.length > 0 ? "void-absolute void-bottom-0" : ""}`}>
|
||||
<VoidChatArea
|
||||
divRef={chatAreaRef}
|
||||
onSubmit={onSubmit}
|
||||
onAbort={onAbort}
|
||||
isStreaming={isStreaming}
|
||||
isDisabled={isDisabled}
|
||||
showSelections={true}
|
||||
showProspectiveSelections={prevMessagesHTML.length === 0}
|
||||
staging={staging}
|
||||
setStaging={setStaging}
|
||||
onClickAnywhere={() => {textAreaRef.current?.focus();}}
|
||||
featureName="Ctrl+L">
|
||||
|
||||
<VoidInputBox2
|
||||
className="void-min-h-[81px] void-p-1"
|
||||
placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`}
|
||||
onChangeText={onChangeText}
|
||||
onKeyDown={onKeyDown}
|
||||
onFocus={() => {chatThreadsService.setFocusedMessageIdx(undefined);}}
|
||||
ref={textAreaRef}
|
||||
fnsRef={textAreaFnsRef}
|
||||
multiline={true} />
|
||||
|
||||
</VoidChatArea>
|
||||
</div>;
|
||||
|
||||
return <div ref={sidebarRef} className={`void-w-full void-h-full`}>
|
||||
{threadSelector}
|
||||
|
||||
{messagesHTML}
|
||||
|
||||
{inputForm}
|
||||
|
||||
</div>;
|
||||
};
|
||||
|
|
@ -1,122 +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 React from "react";
|
||||
import { useAccessor, useChatThreadsState } from '../util/services.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { IconX } from './SidebarChat.js';
|
||||
|
||||
|
||||
const truncate = (s: string) => {
|
||||
let len = s.length;
|
||||
const TRUNC_AFTER = 16;
|
||||
if (len >= TRUNC_AFTER)
|
||||
s = s.substring(0, TRUNC_AFTER) + '...';
|
||||
return s;
|
||||
};
|
||||
|
||||
|
||||
export const SidebarThreadSelector = () => {
|
||||
const threadsState = useChatThreadsState();
|
||||
|
||||
const accessor = useAccessor();
|
||||
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).
|
||||
filter((threadId) => allThreads![threadId].messages.length !== 0);
|
||||
|
||||
return (
|
||||
<div className="void-flex void-p-2 void-flex-col void-gap-y-1 void-max-h-[400px] void-overflow-y-auto">
|
||||
|
||||
<div className="void-w-full void-relative void-flex void-justify-center void-items-center">
|
||||
{/* title */}
|
||||
<h2 className="void-font-bold void-text-lg">{`History`}</h2>
|
||||
{/* X button at top right */}
|
||||
<button
|
||||
type='button'
|
||||
className="void-absolute void-top-0 void-right-0"
|
||||
onClick={() => sidebarStateService.setState({ isHistoryOpen: false })}>
|
||||
|
||||
<IconX
|
||||
size={16}
|
||||
className="void-p-[1px] void-stroke-[2] void-opacity-80 void-text-void-fg-3 hover:void-brightness-95" />
|
||||
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* a list of all the past threads */}
|
||||
<div className="void-px-1">
|
||||
<ul className="void-flex void-flex-col void-gap-y-0.5 void-overflow-y-auto void-list-disc">
|
||||
|
||||
{sortedThreadIds.length === 0 ?
|
||||
|
||||
<div key="nothreads" className="void-text-center void-text-void-fg-3 void-brightness-90 void-text-sm">{`There are no chat threads yet.`}</div> :
|
||||
|
||||
sortedThreadIds.map((threadId) => {
|
||||
if (!allThreads) {
|
||||
return <li key="error" className="void-text-void-warning">{`Error accessing chat history.`}</li>;
|
||||
}
|
||||
|
||||
const pastThread = allThreads[threadId];
|
||||
let firstMsg = null;
|
||||
// let secondMsg = null;
|
||||
|
||||
const firstMsgIdx = pastThread.messages.findIndex(
|
||||
(msg) => msg.role !== 'system' && !!msg.displayContent
|
||||
);
|
||||
|
||||
if (firstMsgIdx !== -1) {
|
||||
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
|
||||
firstMsg = pastThread.messages[firstMsgIdx].displayContent ?? '';
|
||||
} else {
|
||||
firstMsg = '""';
|
||||
}
|
||||
|
||||
// const secondMsgIdx = pastThread.messages.findIndex(
|
||||
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
|
||||
// );
|
||||
|
||||
// if (secondMsgIdx !== -1) {
|
||||
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
|
||||
// }
|
||||
|
||||
const numMessages = pastThread.messages.filter(
|
||||
(msg) => msg.role !== 'system'
|
||||
).length;
|
||||
|
||||
return (
|
||||
<li key={pastThread.id}>
|
||||
<button
|
||||
type='button'
|
||||
className={` hover:void-bg-void-bg-1 ${
|
||||
|
||||
threadsState.currentThreadId === pastThread.id ? "void-bg-void-bg-1" : ""} void-rounded-sm void-px-2 void-py-1 void-w-full void-text-left void-flex void-items-center `}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
onClick={() => chatThreadsService.switchToThread(pastThread.id)}
|
||||
onDoubleClick={() => sidebarStateService.setState({ isHistoryOpen: false })}
|
||||
title={new Date(pastThread.createdAt).toLocaleString()}>
|
||||
|
||||
<div className="void-truncate">{`${firstMsg}`}</div>
|
||||
<div>{`\u00A0(${numMessages})`}</div>
|
||||
</button>
|
||||
</li>);
|
||||
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>);
|
||||
|
||||
};
|
||||
|
|
@ -1,9 +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 { mountFnGenerator } from '../util/mountFnGenerator.js';
|
||||
import { Sidebar } from './Sidebar.js';
|
||||
|
||||
export const mountSidebar = mountFnGenerator(Sidebar);
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,19 +0,0 @@
|
|||
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];
|
||||
};
|
||||
|
|
@ -1,876 +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 React, { forwardRef, MutableRefObject, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||
import { IInputBoxStyles, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
import { defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { Checkbox } from '../../../../../../../base/browser/ui/toggle/toggle.js';
|
||||
|
||||
import { CodeEditorWidget } from '../../../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';
|
||||
import { useAccessor } from './services.js';
|
||||
import { ITextModel } from '../../../../../../../editor/common/model.js';
|
||||
import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js';
|
||||
import { inputBackground, inputForeground } from '../../../../../../../platform/theme/common/colorRegistry.js';
|
||||
import { useFloating, autoUpdate, offset, flip, shift, size, autoPlacement } from '@floating-ui/react';
|
||||
|
||||
|
||||
// type guard
|
||||
const isConstructor = (f: any)
|
||||
: f is {new (...params: any[]): any;} => {
|
||||
return !!f.prototype && f.prototype.constructor === f;
|
||||
};
|
||||
|
||||
export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, propsFn, dispose, onCreateInstance, children, className
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}: {ctor: {new (...params: CtorParams): Instance;} | ((container: HTMLDivElement) => Instance);propsFn: (container: HTMLDivElement) => CtorParams; // unused if fn
|
||||
onCreateInstance: (instance: Instance) => IDisposable[];dispose: (instance: Instance) => void;children?: React.ReactNode;className?: string;}) => {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const instance = isConstructor(ctor) ? new ctor(...propsFn(containerRef.current!)) : ctor(containerRef.current!);
|
||||
const disposables = onCreateInstance(instance);
|
||||
return () => {
|
||||
disposables.forEach((d) => d.dispose());
|
||||
dispose(instance);
|
||||
};
|
||||
}, [ctor, propsFn, dispose, onCreateInstance, containerRef]);
|
||||
|
||||
return <div ref={containerRef} className={className === undefined ? `void-w-full` : className}>{children}</div>;
|
||||
};
|
||||
|
||||
|
||||
export type TextAreaFns = {setValue: (v: string) => void;enable: () => void;disable: () => void;};
|
||||
type InputBox2Props = {
|
||||
initValue?: string | null;
|
||||
placeholder: string;
|
||||
multiline: boolean;
|
||||
fnsRef?: {current: null | TextAreaFns;};
|
||||
className?: string;
|
||||
onChangeText?: (value: string) => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
onFocus?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
||||
onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
||||
onChangeHeight?: (newHeight: number) => void;
|
||||
};
|
||||
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onFocus, onBlur, onChangeText }, ref) {
|
||||
|
||||
// mirrors whatever is in ref
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const [isEnabled, setEnabled] = useState(true);
|
||||
|
||||
const adjustHeight = useCallback(() => {
|
||||
const r = textAreaRef.current;
|
||||
if (!r) return;
|
||||
|
||||
r.style.height = 'auto'; // set to auto to reset height, then set to new height
|
||||
|
||||
if (r.scrollHeight === 0) return requestAnimationFrame(adjustHeight);
|
||||
const h = r.scrollHeight;
|
||||
const newHeight = Math.min(h + 1, 500); // plus one to avoid scrollbar appearing when it shouldn't
|
||||
r.style.height = `${newHeight}px`;
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const fns: TextAreaFns = useMemo(() => ({
|
||||
setValue: (val) => {
|
||||
const r = textAreaRef.current;
|
||||
if (!r) return;
|
||||
r.value = val;
|
||||
onChangeText?.(r.value);
|
||||
adjustHeight();
|
||||
},
|
||||
enable: () => {setEnabled(true);},
|
||||
disable: () => {setEnabled(false);}
|
||||
}), [onChangeText, adjustHeight]);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (initValue)
|
||||
fns.setValue(initValue);
|
||||
}, [initValue]);
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<textarea
|
||||
ref={useCallback((r: HTMLTextAreaElement | null) => {
|
||||
if (fnsRef)
|
||||
fnsRef.current = fns;
|
||||
|
||||
textAreaRef.current = r;
|
||||
if (typeof ref === 'function') ref(r);else
|
||||
if (ref) ref.current = r;
|
||||
adjustHeight();
|
||||
}, [fnsRef, fns, setEnabled, adjustHeight, ref])}
|
||||
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
|
||||
disabled={!isEnabled}
|
||||
|
||||
className={`void-w-full void-resize-none void-max-h-[500px] void-overflow-y-auto void-text-void-fg-1 placeholder:void-text-void-fg-3 ${className}`}
|
||||
style={{
|
||||
// defaultInputBoxStyles
|
||||
background: asCssVariable(inputBackground),
|
||||
color: asCssVariable(inputForeground)
|
||||
// inputBorder: asCssVariable(inputBorder),
|
||||
}}
|
||||
|
||||
onChange={useCallback(() => {
|
||||
const r = textAreaRef.current;
|
||||
if (!r) return;
|
||||
onChangeText?.(r.value);
|
||||
adjustHeight();
|
||||
}, [onChangeText, adjustHeight])}
|
||||
|
||||
onKeyDown={useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
// Shift + Enter when multiline = newline
|
||||
const shouldAddNewline = e.shiftKey && multiline;
|
||||
if (!shouldAddNewline) e.preventDefault(); // prevent newline from being created
|
||||
}
|
||||
onKeyDown?.(e);
|
||||
}, [onKeyDown, multiline])}
|
||||
|
||||
rows={1}
|
||||
placeholder={placeholder} />);
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}: {onChangeText: (value: string) => void;styles?: Partial<IInputBoxStyles>;onCreateInstance?: (instance: InputBox) => void | IDisposable[];inputBoxRef?: {current: InputBox | null;};placeholder: string;multiline: boolean;}) => {
|
||||
|
||||
const accessor = useAccessor();
|
||||
|
||||
const contextViewProvider = accessor.get('IContextViewService');
|
||||
return <WidgetComponent
|
||||
ctor={InputBox}
|
||||
className=" void-bg-void-bg-1 [&_::placeholder]:!void-text-void-fg-3 "
|
||||
|
||||
|
||||
|
||||
propsFn={useCallback((container) => [
|
||||
container,
|
||||
contextViewProvider,
|
||||
{
|
||||
inputBoxStyles: {
|
||||
...defaultInputBoxStyles,
|
||||
inputForeground: "var(--vscode-foreground)"
|
||||
// inputBackground: 'transparent',
|
||||
// inputBorder: 'none',
|
||||
},
|
||||
placeholder,
|
||||
tooltip: '',
|
||||
flexibleHeight: multiline,
|
||||
flexibleMaxHeight: 500,
|
||||
flexibleWidth: false
|
||||
}] as
|
||||
const, [contextViewProvider, placeholder, multiline])}
|
||||
dispose={useCallback((instance: InputBox) => {
|
||||
instance.dispose();
|
||||
instance.element.remove();
|
||||
}, [])}
|
||||
onCreateInstance={useCallback((instance: InputBox) => {
|
||||
const disposables: IDisposable[] = [];
|
||||
disposables.push(
|
||||
instance.onDidChange((newText) => onChangeText(newText))
|
||||
);
|
||||
if (onCreateInstance) {
|
||||
const ds = onCreateInstance(instance) ?? [];
|
||||
disposables.push(...ds);
|
||||
}
|
||||
if (inputBoxRef)
|
||||
inputBoxRef.current = instance;
|
||||
|
||||
return disposables;
|
||||
}, [onChangeText, onCreateInstance, inputBoxRef])
|
||||
} />;
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
export const VoidSwitch = ({
|
||||
value,
|
||||
onChange,
|
||||
size = 'md',
|
||||
label,
|
||||
disabled = false
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}: {value: boolean;onChange: (value: boolean) => void;label?: string;disabled?: boolean;size?: 'xs' | 'sm' | 'sm+' | 'md';}) => {
|
||||
return (
|
||||
<label className="void-inline-flex void-items-center void-cursor-pointer">
|
||||
<div
|
||||
onClick={() => !disabled && onChange(!value)}
|
||||
className={` void-relative void-inline-flex void-items-center void-rounded-full void-transition-colors void-duration-200 void-ease-in-out ${
|
||||
|
||||
value ? "void-bg-gray-900 dark:void-bg-white" : "void-bg-gray-200 dark:void-bg-gray-700"} ${
|
||||
disabled ? "void-opacity-25" : ""} ${
|
||||
size === 'xs' ? "void-h-4 void-w-7" : ""} ${
|
||||
size === 'sm' ? "void-h-5 void-w-9" : ""} ${
|
||||
size === 'sm+' ? "void-h-5 void-w-10" : ""} ${
|
||||
size === 'md' ? "void-h-6 void-w-11" : ""} `}>
|
||||
|
||||
|
||||
<span
|
||||
className={` void-inline-block void-transform void-rounded-full void-bg-white dark:void-bg-gray-900 void-shadow void-transition-transform void-duration-200 void-ease-in-out ${
|
||||
|
||||
size === 'xs' ? "void-h-2.5 void-w-2.5" : ""} ${
|
||||
size === 'sm' ? "void-h-3 void-w-3" : ""} ${
|
||||
size === 'sm+' ? "void-h-3.5 void-w-3.5" : ""} ${
|
||||
size === 'md' ? "void-h-4 void-w-4" : ""} ${
|
||||
size === 'xs' ? value ? "void-translate-x-3.5" : "void-translate-x-0.5" : ""} ${
|
||||
size === 'sm' ? value ? "void-translate-x-5" : "void-translate-x-1" : ""} ${
|
||||
size === 'sm+' ? value ? "void-translate-x-6" : "void-translate-x-1" : ""} ${
|
||||
size === 'md' ? value ? "void-translate-x-6" : "void-translate-x-1" : ""} `} />
|
||||
|
||||
|
||||
</div>
|
||||
{label &&
|
||||
<span className={` void-ml-3 void-font-medium void-text-gray-900 dark:void-text-gray-100 ${
|
||||
|
||||
size === 'xs' ? "void-text-xs" : "void-text-sm"} `}>
|
||||
|
||||
{label}
|
||||
</span>
|
||||
}
|
||||
</label>);
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const VoidCheckBox = ({ label, value, onClick, className }: {label: string;value: boolean;onClick: (checked: boolean) => void;className?: string;}) => {
|
||||
const divRef = useRef<HTMLDivElement | null>(null);
|
||||
const instanceRef = useRef<Checkbox | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!instanceRef.current) return;
|
||||
instanceRef.current.checked = value;
|
||||
}, [value]);
|
||||
|
||||
|
||||
return <WidgetComponent
|
||||
className={className ?? ''}
|
||||
ctor={Checkbox}
|
||||
propsFn={useCallback((container: HTMLDivElement) => {
|
||||
divRef.current = container;
|
||||
return [label, value, defaultCheckboxStyles] as const;
|
||||
}, [label, value])}
|
||||
onCreateInstance={useCallback((instance: Checkbox) => {
|
||||
instanceRef.current = instance;
|
||||
divRef.current?.append(instance.domNode);
|
||||
const d = instance.onChange(() => onClick(instance.checked));
|
||||
return [d];
|
||||
}, [onClick])}
|
||||
dispose={useCallback((instance: Checkbox) => {
|
||||
instance.dispose();
|
||||
instance.domNode.remove();
|
||||
}, [])} />;
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const VoidCustomDropdownBox = <T extends any,>({
|
||||
options,
|
||||
selectedOption,
|
||||
onChangeOption,
|
||||
getOptionDropdownName,
|
||||
getOptionDisplayName,
|
||||
getOptionsEqual,
|
||||
className,
|
||||
arrowTouchesText = true,
|
||||
matchInputWidth = false,
|
||||
gap = 0
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}: {options: T[];selectedOption: T | undefined;onChangeOption: (newValue: T) => void;getOptionDropdownName: (option: T) => string;getOptionDisplayName: (option: T) => string;getOptionsEqual: (a: T, b: T) => boolean;className?: string;arrowTouchesText?: boolean;matchInputWidth?: boolean;gap?: number;}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const measureRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Replace manual positioning with floating-ui
|
||||
const {
|
||||
x,
|
||||
y,
|
||||
strategy,
|
||||
refs,
|
||||
middlewareData,
|
||||
update
|
||||
} = useFloating({
|
||||
open: isOpen,
|
||||
onOpenChange: setIsOpen,
|
||||
placement: 'bottom-start',
|
||||
|
||||
middleware: [
|
||||
offset(gap),
|
||||
flip({
|
||||
boundary: document.body,
|
||||
padding: 8
|
||||
}),
|
||||
shift({
|
||||
boundary: document.body,
|
||||
padding: 8
|
||||
}),
|
||||
size({
|
||||
apply({ availableHeight, elements, rects }) {
|
||||
const maxHeight = Math.min(availableHeight);
|
||||
|
||||
Object.assign(elements.floating.style, {
|
||||
maxHeight: `${maxHeight}px`,
|
||||
overflowY: 'auto',
|
||||
// Ensure the width isn't constrained by the parent
|
||||
width: `${Math.max(
|
||||
rects.reference.width,
|
||||
measureRef.current?.offsetWidth ?? 0
|
||||
)}px`
|
||||
});
|
||||
},
|
||||
padding: 8,
|
||||
// Use viewport as boundary instead of any parent element
|
||||
boundary: document.body
|
||||
})],
|
||||
|
||||
whileElementsMounted: autoUpdate,
|
||||
strategy: 'fixed'
|
||||
});
|
||||
|
||||
// if the selected option is null, set the selection to the 0th option
|
||||
useEffect(() => {
|
||||
if (options.length === 0) return;
|
||||
if (selectedOption) return;
|
||||
onChangeOption(options[0]);
|
||||
}, [selectedOption, onChangeOption, options]);
|
||||
|
||||
// Handle clicks outside
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as Node;
|
||||
const floating = refs.floating.current;
|
||||
const reference = refs.reference.current;
|
||||
|
||||
// Check if reference is an HTML element before using contains
|
||||
const isReferenceHTMLElement = reference && 'contains' in reference;
|
||||
|
||||
if (
|
||||
floating && (
|
||||
!isReferenceHTMLElement || !reference.contains(target)) &&
|
||||
!floating.contains(target))
|
||||
{
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [isOpen, refs.floating, refs.reference]);
|
||||
|
||||
if (!selectedOption)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div className={`void-inline-block void-relative ${className}`}>
|
||||
{/* Hidden measurement div */}
|
||||
<div
|
||||
ref={measureRef}
|
||||
className="void-opacity-0 void-pointer-events-none void-absolute -void-left-[999999px] -void-top-[999999px] void-flex void-flex-col"
|
||||
aria-hidden="true">
|
||||
|
||||
{options.map((option) =>
|
||||
<div key={getOptionDropdownName(option)} className="void-flex void-items-center void-whitespace-nowrap">
|
||||
<div className="void-w-4" />
|
||||
<span className="void-px-2">{getOptionDropdownName(option)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Select Button */}
|
||||
<button
|
||||
type='button'
|
||||
ref={refs.setReference}
|
||||
className="void-flex void-items-center void-h-4 void-bg-transparent void-whitespace-nowrap hover:void-brightness-90 void-w-full"
|
||||
onClick={() => setIsOpen(!isOpen)}>
|
||||
|
||||
<span className={`void-max-w-[120px] void-truncate ${arrowTouchesText ? "void-mr-1" : ""}`}>
|
||||
{getOptionDisplayName(selectedOption)}
|
||||
</span>
|
||||
<svg
|
||||
className={`void-size-3 void-flex-shrink-0 ${arrowTouchesText ? "" : "void-ml-auto"}`}
|
||||
viewBox="0 0 12 12"
|
||||
fill="none">
|
||||
|
||||
<path
|
||||
d="M2.5 4.5L6 8L9.5 4.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round" />
|
||||
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{isOpen &&
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
className="void-z-10 void-bg-void-bg-1 void-border-void-border-1 void-border void-overflow-hidden void-rounded void-shadow-lg"
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
left: x ?? 0,
|
||||
width: matchInputWidth ?
|
||||
refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0 :
|
||||
Math.max(
|
||||
refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0,
|
||||
measureRef.current instanceof HTMLElement ? measureRef.current.offsetWidth : 0
|
||||
)
|
||||
}}>
|
||||
|
||||
{options.map((option) => {
|
||||
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
|
||||
const optionName = getOptionDropdownName(option);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={optionName}
|
||||
className={`void-flex void-items-center void-px-2 void-py-1 void-cursor-pointer void-whitespace-nowrap void-transition-all void-duration-100 void-bg-void-bg-1 ${
|
||||
|
||||
|
||||
thisOptionIsSelected ? "void-bg-void-bg-2" : "hover:void-bg-void-bg-2"} `}
|
||||
|
||||
onClick={() => {
|
||||
onChangeOption(option);
|
||||
setIsOpen(false);
|
||||
}}>
|
||||
|
||||
<div className="void-w-4 void-flex void-justify-center void-flex-shrink-0">
|
||||
{thisOptionIsSelected &&
|
||||
<svg className="void-size-3" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M10 3L4.5 8.5L2 6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round" />
|
||||
|
||||
</svg>
|
||||
}
|
||||
</div>
|
||||
<span>{optionName}</span>
|
||||
</div>);
|
||||
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
</div>);
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const _VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectBoxRef, options, className
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}: {onChangeSelection: (value: T) => void;onCreateInstance?: ((instance: SelectBox) => void | IDisposable[]);selectBoxRef?: React.MutableRefObject<SelectBox | null>;options: readonly {text: string;value: T;}[];className?: string;}) => {
|
||||
const accessor = useAccessor();
|
||||
const contextViewProvider = accessor.get('IContextViewService');
|
||||
|
||||
let containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
return <WidgetComponent
|
||||
className={` select-child-restyle [&_select]:!void-text-void-fg-3 [&_select]:!void-text-xs void-!text-void-fg-3 ${
|
||||
|
||||
|
||||
|
||||
|
||||
className ?? ''} `}
|
||||
|
||||
ctor={SelectBox}
|
||||
propsFn={useCallback((container) => {
|
||||
containerRef.current = container;
|
||||
const defaultIndex = 0;
|
||||
return [
|
||||
options.map((opt) => ({ text: opt.text })),
|
||||
defaultIndex,
|
||||
contextViewProvider,
|
||||
defaultSelectBoxStyles] as
|
||||
const;
|
||||
}, [containerRef, options])}
|
||||
|
||||
dispose={useCallback((instance: SelectBox) => {
|
||||
instance.dispose();
|
||||
containerRef.current?.childNodes.forEach((child) => {
|
||||
containerRef.current?.removeChild(child);
|
||||
});
|
||||
}, [containerRef])}
|
||||
|
||||
onCreateInstance={useCallback((instance: SelectBox) => {
|
||||
const disposables: IDisposable[] = [];
|
||||
|
||||
if (containerRef.current)
|
||||
instance.render(containerRef.current);
|
||||
|
||||
disposables.push(
|
||||
instance.onDidSelect((e) => {onChangeSelection(options[e.index].value);})
|
||||
);
|
||||
|
||||
if (onCreateInstance) {
|
||||
const ds = onCreateInstance(instance) ?? [];
|
||||
disposables.push(...ds);
|
||||
}
|
||||
if (selectBoxRef)
|
||||
selectBoxRef.current = instance;
|
||||
|
||||
return disposables;
|
||||
}, [containerRef, onChangeSelection, options, onCreateInstance, selectBoxRef])} />;
|
||||
|
||||
|
||||
};
|
||||
|
||||
// makes it so that code in the sidebar isnt too tabbed out
|
||||
const normalizeIndentation = (code: string): string => {
|
||||
const lines = code.split('\n');
|
||||
|
||||
let minLeadingSpaces = Infinity;
|
||||
|
||||
// find the minimum number of leading spaces
|
||||
for (const line of lines) {
|
||||
if (line.trim() === '') continue;
|
||||
let leadingSpaces = 0;
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const char = line[i];
|
||||
if (char === '\t' || char === ' ') {
|
||||
leadingSpaces += 1;
|
||||
} else {break;}
|
||||
}
|
||||
minLeadingSpaces = Math.min(minLeadingSpaces, leadingSpaces);
|
||||
}
|
||||
|
||||
// remove the leading spaces
|
||||
return lines.map((line) => {
|
||||
if (line.trim() === '') return line;
|
||||
|
||||
let spacesToRemove = minLeadingSpaces;
|
||||
let i = 0;
|
||||
while (spacesToRemove > 0 && i < line.length) {
|
||||
const char = line[i];
|
||||
if (char === '\t' || char === ' ') {
|
||||
spacesToRemove -= 1;
|
||||
i++;
|
||||
} else {break;}
|
||||
}
|
||||
|
||||
return line.slice(i);
|
||||
|
||||
}).join('\n');
|
||||
|
||||
};
|
||||
|
||||
|
||||
const modelOfEditorId: {[id: string]: ITextModel | undefined;} = {};
|
||||
export type VoidCodeEditorProps = {initValue: string;language?: string;maxHeight?: number;showScrollbars?: boolean;};
|
||||
export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars }: VoidCodeEditorProps) => {
|
||||
|
||||
initValue = normalizeIndentation(initValue);
|
||||
|
||||
// default settings
|
||||
const MAX_HEIGHT = maxHeight ?? Infinity;
|
||||
const SHOW_SCROLLBARS = showScrollbars ?? false;
|
||||
|
||||
const divRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const accessor = useAccessor();
|
||||
const instantiationService = accessor.get('IInstantiationService');
|
||||
// const languageDetectionService = accessor.get('ILanguageDetectionService')
|
||||
const modelService = accessor.get('IModelService');
|
||||
|
||||
|
||||
const id = useId();
|
||||
|
||||
// these are used to pass to the model creation of modelRef
|
||||
const initValueRef = useRef(initValue);
|
||||
const languageRef = useRef(language);
|
||||
|
||||
const modelRef = useRef<ITextModel | null>(null);
|
||||
|
||||
// if we change the initial value, don't re-render the whole thing, just set it here. same for language
|
||||
useEffect(() => {
|
||||
initValueRef.current = initValue;
|
||||
modelRef.current?.setValue(initValue);
|
||||
}, [initValue]);
|
||||
useEffect(() => {
|
||||
languageRef.current = language;
|
||||
if (language) modelRef.current?.setLanguage(language);
|
||||
}, [language]);
|
||||
|
||||
return <div ref={divRef} className="void-relative void-z-0 void-px-2 void-py-1 void-bg-void-bg-3">
|
||||
<WidgetComponent
|
||||
className="bg-editor-style-override" // text-sm
|
||||
ctor={useCallback((container) => {
|
||||
return instantiationService.createInstance(
|
||||
CodeEditorWidget,
|
||||
container,
|
||||
{
|
||||
automaticLayout: true,
|
||||
wordWrap: 'off',
|
||||
|
||||
scrollbar: {
|
||||
alwaysConsumeMouseWheel: false,
|
||||
...(SHOW_SCROLLBARS ? {
|
||||
vertical: 'auto',
|
||||
verticalScrollbarSize: 8,
|
||||
horizontal: 'auto',
|
||||
horizontalScrollbarSize: 8
|
||||
} : {
|
||||
vertical: 'hidden',
|
||||
verticalScrollbarSize: 0,
|
||||
horizontal: 'auto',
|
||||
horizontalScrollbarSize: 8,
|
||||
ignoreHorizontalScrollbarInContentHeight: true
|
||||
|
||||
})
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
|
||||
lineNumbers: 'off',
|
||||
|
||||
readOnly: true,
|
||||
domReadOnly: true,
|
||||
readOnlyMessage: { value: '' },
|
||||
|
||||
minimap: {
|
||||
enabled: false
|
||||
// maxColumn: 0,
|
||||
},
|
||||
|
||||
hover: { enabled: false },
|
||||
|
||||
selectionHighlight: false, // highlights whole words
|
||||
renderLineHighlight: 'none',
|
||||
|
||||
folding: false,
|
||||
lineDecorationsWidth: 0,
|
||||
overviewRulerLanes: 0,
|
||||
hideCursorInOverviewRuler: true,
|
||||
overviewRulerBorder: false,
|
||||
glyphMargin: false,
|
||||
|
||||
stickyScroll: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
{
|
||||
isSimpleWidget: true
|
||||
});
|
||||
}, [instantiationService])}
|
||||
|
||||
onCreateInstance={useCallback((editor: CodeEditorWidget) => {
|
||||
const model = modelOfEditorId[id] ?? modelService.createModel(
|
||||
initValueRef.current, {
|
||||
languageId: languageRef.current ? languageRef.current : 'typescript',
|
||||
onDidChange: (e) => {return { dispose: () => {} };} // no idea why they'd require this
|
||||
});
|
||||
modelRef.current = model;
|
||||
editor.setModel(model);
|
||||
|
||||
const container = editor.getDomNode();
|
||||
const parentNode = container?.parentElement;
|
||||
const resize = () => {
|
||||
const height = editor.getScrollHeight() + 1;
|
||||
if (parentNode) {
|
||||
// const height = Math.min(, MAX_HEIGHT);
|
||||
parentNode.style.height = `${height}px`;
|
||||
parentNode.style.maxHeight = `${MAX_HEIGHT}px`;
|
||||
editor.layout();
|
||||
}
|
||||
};
|
||||
|
||||
resize();
|
||||
const disposable = editor.onDidContentSizeChange(() => {resize();});
|
||||
|
||||
return [disposable, model];
|
||||
}, [modelService])}
|
||||
|
||||
dispose={useCallback((editor: CodeEditorWidget) => {
|
||||
editor.dispose();
|
||||
}, [modelService])}
|
||||
|
||||
propsFn={useCallback(() => {return [];}, [])} />
|
||||
|
||||
</div>;
|
||||
|
||||
};
|
||||
|
||||
|
||||
export const VoidButton = ({ children, disabled, onClick }: {children: React.ReactNode;disabled?: boolean;onClick: () => void;}) => {
|
||||
return <button disabled={disabled}
|
||||
className="void-px-3 void-py-1 void-bg-black/10 dark:void-bg-gray-200/10 void-rounded-sm void-overflow-hidden"
|
||||
onClick={onClick}>
|
||||
{children}</button>;
|
||||
};
|
||||
|
||||
// export const VoidScrollableElt = ({ options, children }: { options: ScrollableElementCreationOptions, children: React.ReactNode }) => {
|
||||
// const instanceRef = useRef<DomScrollableElement | null>(null);
|
||||
// const [childrenPortal, setChildrenPortal] = useState<React.ReactNode | null>(null)
|
||||
|
||||
// return <>
|
||||
// <WidgetComponent
|
||||
// ctor={DomScrollableElement}
|
||||
// propsFn={useCallback((container) => {
|
||||
// return [container, options] as const;
|
||||
// }, [options])}
|
||||
// onCreateInstance={useCallback((instance: DomScrollableElement) => {
|
||||
// instanceRef.current = instance;
|
||||
// setChildrenPortal(createPortal(children, instance.getDomNode()))
|
||||
// return []
|
||||
// }, [setChildrenPortal, children])}
|
||||
// dispose={useCallback((instance: DomScrollableElement) => {
|
||||
// console.log('calling dispose!!!!')
|
||||
// // instance.dispose();
|
||||
// // instance.getDomNode().remove()
|
||||
// }, [])}
|
||||
// >{children}</WidgetComponent>
|
||||
|
||||
// {childrenPortal}
|
||||
|
||||
// </>
|
||||
// }
|
||||
|
||||
// export const VoidSelectBox = <T,>({ onChangeSelection, initVal, selectBoxRef, options }: {
|
||||
// initVal: T;
|
||||
// selectBoxRef: React.MutableRefObject<SelectBox | null>;
|
||||
// options: readonly { text: string, value: T }[];
|
||||
// onChangeSelection: (value: T) => void;
|
||||
// }) => {
|
||||
|
||||
|
||||
// return <WidgetComponent
|
||||
// ctor={DropdownMenu}
|
||||
// propsFn={useCallback((container) => {
|
||||
// return [
|
||||
// container, {
|
||||
// contextMenuProvider,
|
||||
// actions: options.map(({ text, value }, i) => ({
|
||||
// id: i + '',
|
||||
// label: text,
|
||||
// tooltip: text,
|
||||
// class: undefined,
|
||||
// enabled: true,
|
||||
// run: () => {
|
||||
// onChangeSelection(value);
|
||||
// },
|
||||
// }))
|
||||
|
||||
// }] as const;
|
||||
// }, [options, initVal, contextViewProvider])}
|
||||
|
||||
// dispose={useCallback((instance: DropdownMenu) => {
|
||||
// instance.dispose();
|
||||
// // instance.element.remove()
|
||||
// }, [])}
|
||||
|
||||
// onCreateInstance={useCallback((instance: DropdownMenu) => {
|
||||
// return []
|
||||
// }, [])}
|
||||
|
||||
// />;
|
||||
// };
|
||||
|
||||
|
||||
|
||||
|
||||
// export const VoidCheckBox = ({ onChangeChecked, initVal, label, checkboxRef, }: {
|
||||
// onChangeChecked: (checked: boolean) => void;
|
||||
// initVal: boolean;
|
||||
// checkboxRef: React.MutableRefObject<ObjectSettingCheckboxWidget | null>;
|
||||
// label: string;
|
||||
// }) => {
|
||||
// const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!containerRef.current) return;
|
||||
|
||||
// // Create and mount the Checkbox using VSCode's implementation
|
||||
|
||||
// checkboxRef.current = new ObjectSettingCheckboxWidget(
|
||||
// containerRef.current,
|
||||
// themeService,
|
||||
// contextViewService,
|
||||
// hoverService,
|
||||
// );
|
||||
|
||||
|
||||
// checkboxRef.current.setValue([{
|
||||
// key: { type: 'string', data: label },
|
||||
// value: { type: 'boolean', data: initVal },
|
||||
// removable: false,
|
||||
// resetable: true,
|
||||
// }])
|
||||
|
||||
// checkboxRef.current.onDidChangeList((list) => {
|
||||
// onChangeChecked(!!list);
|
||||
// })
|
||||
|
||||
|
||||
// // cleanup
|
||||
// return () => {
|
||||
// if (checkboxRef.current) {
|
||||
// checkboxRef.current.dispose();
|
||||
// if (containerRef.current) {
|
||||
// while (containerRef.current.firstChild) {
|
||||
// containerRef.current.removeChild(containerRef.current.firstChild);
|
||||
// }
|
||||
// }
|
||||
// checkboxRef.current = null;
|
||||
// }
|
||||
// };
|
||||
// }, [checkboxRef, label, initVal, onChangeChecked]);
|
||||
|
||||
// return <div ref={containerRef} className="w-full" />;
|
||||
// };
|
||||
|
|
@ -1,26 +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 React, { useEffect, useState } from 'react';
|
||||
import * as ReactDOM from 'react-dom/client';
|
||||
import { _registerServices } from './services.js';
|
||||
|
||||
|
||||
import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js';
|
||||
|
||||
export const mountFnGenerator = (Component: (params: any) => React.ReactNode) => (rootElement: HTMLElement, accessor: ServicesAccessor, props?: any) => {
|
||||
if (typeof document === 'undefined') {
|
||||
console.error('index.tsx error: document was undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
const disposables = _registerServices(accessor);
|
||||
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(<Component {...props} />); // tailwind dark theme indicator
|
||||
|
||||
return disposables;
|
||||
};
|
||||
|
|
@ -1,355 +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 React, { useState, useEffect } from 'react';
|
||||
import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js';
|
||||
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { VoidSidebarState } from '../../../sidebarStateService.js';
|
||||
import { VoidSettingsState } from '../../../../../../../workbench/contrib/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 '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js';
|
||||
import { IModelService } from '../../../../../../../editor/common/services/model.js';
|
||||
import { IClipboardService } from '../../../../../../../platform/clipboard/common/clipboardService.js';
|
||||
import { IContextViewService, IContextMenuService } from '../../../../../../../platform/contextview/browser/contextView.js';
|
||||
import { IFileService } from '../../../../../../../platform/files/common/files.js';
|
||||
import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js';
|
||||
import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js';
|
||||
import { ILLMMessageService } from '../../../../../../../workbench/contrib/void/common/llmMessageService.js';
|
||||
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/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 { 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';
|
||||
import { IContextKeyService } from '../../../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { INotificationService } from '../../../../../../../platform/notification/common/notification.js';
|
||||
import { IAccessibilityService } from '../../../../../../../platform/accessibility/common/accessibility.js';
|
||||
import { ILanguageConfigurationService } from '../../../../../../../editor/common/languages/languageConfigurationRegistry.js';
|
||||
import { ILanguageFeaturesService } from '../../../../../../../editor/common/services/languageFeatures.js';
|
||||
import { ILanguageDetectionService } from '../../../../../../services/languageDetection/common/languageDetectionWorkerService.js';
|
||||
import { IKeybindingService } from '../../../../../../../platform/keybinding/common/keybinding.js';
|
||||
import { IEnvironmentService } from '../../../../../../../platform/environment/common/environment.js';
|
||||
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js';
|
||||
import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js';
|
||||
import { IMetricsService } from '../../../../../../../workbench/contrib/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 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();
|
||||
|
||||
let refreshModelState: RefreshModelStateOfProvider;
|
||||
const refreshModelStateListeners: Set<(s: RefreshModelStateOfProvider) => void> = new Set();
|
||||
const refreshModelProviderListeners: Set<(p: RefreshableProviderName, s: RefreshModelStateOfProvider) => void> = new Set();
|
||||
|
||||
let colorThemeState: ColorScheme;
|
||||
const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set();
|
||||
|
||||
// must call this before you can use any of the hooks below
|
||||
// this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it!
|
||||
let wasCalled = false;
|
||||
export const _registerServices = (accessor: ServicesAccessor) => {
|
||||
|
||||
const disposables: IDisposable[] = [];
|
||||
|
||||
// don't register services twice
|
||||
if (wasCalled) {
|
||||
return;
|
||||
// console.error(`⚠️ Void _registerServices was called again! It should only be called once.`)
|
||||
}
|
||||
wasCalled = true;
|
||||
|
||||
_registerAccessor(accessor);
|
||||
|
||||
const stateServices = {
|
||||
uriStateService: accessor.get(IVoidUriStateService),
|
||||
quickEditStateService: accessor.get(IQuickEditStateService),
|
||||
sidebarStateService: accessor.get(ISidebarStateService),
|
||||
chatThreadsStateService: accessor.get(IChatThreadService),
|
||||
settingsStateService: accessor.get(IVoidSettingsService),
|
||||
refreshModelService: accessor.get(IRefreshModelService),
|
||||
themeService: accessor.get(IThemeService),
|
||||
inlineDiffsService: accessor.get(IInlineDiffsService)
|
||||
};
|
||||
|
||||
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(
|
||||
quickEditStateService.onDidChangeState(() => {
|
||||
quickEditState = quickEditStateService.state;
|
||||
quickEditStateListeners.forEach((l) => l(quickEditState));
|
||||
})
|
||||
);
|
||||
|
||||
sidebarState = sidebarStateService.state;
|
||||
disposables.push(
|
||||
sidebarStateService.onDidChangeState(() => {
|
||||
sidebarState = sidebarStateService.state;
|
||||
sidebarStateListeners.forEach((l) => l(sidebarState));
|
||||
})
|
||||
);
|
||||
|
||||
chatThreadsState = chatThreadsStateService.state;
|
||||
disposables.push(
|
||||
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));
|
||||
})
|
||||
);
|
||||
|
||||
settingsState = settingsStateService.state;
|
||||
disposables.push(
|
||||
settingsStateService.onDidChangeState(() => {
|
||||
settingsState = settingsStateService.state;
|
||||
settingsStateListeners.forEach((l) => l(settingsState));
|
||||
})
|
||||
);
|
||||
|
||||
refreshModelState = refreshModelService.state;
|
||||
disposables.push(
|
||||
refreshModelService.onDidChangeState((providerName) => {
|
||||
refreshModelState = refreshModelService.state;
|
||||
refreshModelStateListeners.forEach((l) => l(refreshModelState));
|
||||
refreshModelProviderListeners.forEach((l) => l(providerName, refreshModelState));
|
||||
})
|
||||
);
|
||||
|
||||
colorThemeState = themeService.getColorTheme().type;
|
||||
disposables.push(
|
||||
themeService.onDidColorThemeChange((theme) => {
|
||||
colorThemeState = theme.type;
|
||||
colorThemeStateListeners.forEach((l) => l(colorThemeState));
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
return disposables;
|
||||
};
|
||||
|
||||
|
||||
|
||||
const getReactAccessor = (accessor: ServicesAccessor) => {
|
||||
const reactAccessor = {
|
||||
IModelService: accessor.get(IModelService),
|
||||
IClipboardService: accessor.get(IClipboardService),
|
||||
IContextViewService: accessor.get(IContextViewService),
|
||||
IContextMenuService: accessor.get(IContextMenuService),
|
||||
IFileService: accessor.get(IFileService),
|
||||
IHoverService: accessor.get(IHoverService),
|
||||
IThemeService: accessor.get(IThemeService),
|
||||
ILLMMessageService: accessor.get(ILLMMessageService),
|
||||
IRefreshModelService: accessor.get(IRefreshModelService),
|
||||
IVoidSettingsService: accessor.get(IVoidSettingsService),
|
||||
IInlineDiffsService: accessor.get(IInlineDiffsService),
|
||||
IVoidUriStateService: accessor.get(IVoidUriStateService),
|
||||
IQuickEditStateService: accessor.get(IQuickEditStateService),
|
||||
ISidebarStateService: accessor.get(ISidebarStateService),
|
||||
IChatThreadService: accessor.get(IChatThreadService),
|
||||
|
||||
IInstantiationService: accessor.get(IInstantiationService),
|
||||
ICodeEditorService: accessor.get(ICodeEditorService),
|
||||
ICommandService: accessor.get(ICommandService),
|
||||
IContextKeyService: accessor.get(IContextKeyService),
|
||||
INotificationService: accessor.get(INotificationService),
|
||||
IAccessibilityService: accessor.get(IAccessibilityService),
|
||||
ILanguageConfigurationService: accessor.get(ILanguageConfigurationService),
|
||||
ILanguageDetectionService: accessor.get(ILanguageDetectionService),
|
||||
ILanguageFeaturesService: accessor.get(ILanguageFeaturesService),
|
||||
IKeybindingService: accessor.get(IKeybindingService),
|
||||
|
||||
IEnvironmentService: accessor.get(IEnvironmentService),
|
||||
IConfigurationService: accessor.get(IConfigurationService),
|
||||
IPathService: accessor.get(IPathService),
|
||||
IMetricsService: accessor.get(IMetricsService)
|
||||
|
||||
} as const;
|
||||
return reactAccessor;
|
||||
};
|
||||
|
||||
type ReactAccessor = ReturnType<typeof getReactAccessor>;
|
||||
|
||||
|
||||
let reactAccessor_: ReactAccessor | null = null;
|
||||
const _registerAccessor = (accessor: ServicesAccessor) => {
|
||||
const reactAccessor = getReactAccessor(accessor);
|
||||
reactAccessor_ = reactAccessor;
|
||||
};
|
||||
|
||||
// -- services --
|
||||
export const useAccessor = () => {
|
||||
if (!reactAccessor_) {
|
||||
throw new Error(`⚠️ Void useAccessor was called before _registerServices!`);
|
||||
}
|
||||
|
||||
return { get: <S extends keyof ReactAccessor,>(service: S): ReactAccessor[S] => reactAccessor_![service] };
|
||||
};
|
||||
|
||||
|
||||
|
||||
// -- 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(() => {
|
||||
ss(quickEditState);
|
||||
quickEditStateListeners.add(ss);
|
||||
return () => {quickEditStateListeners.delete(ss);};
|
||||
}, [ss]);
|
||||
return s;
|
||||
};
|
||||
|
||||
export const useSidebarState = () => {
|
||||
const [s, ss] = useState(sidebarState);
|
||||
useEffect(() => {
|
||||
ss(sidebarState);
|
||||
sidebarStateListeners.add(ss);
|
||||
return () => {sidebarStateListeners.delete(ss);};
|
||||
}, [ss]);
|
||||
return s;
|
||||
};
|
||||
|
||||
export const useSettingsState = () => {
|
||||
const [s, ss] = useState(settingsState);
|
||||
useEffect(() => {
|
||||
ss(settingsState);
|
||||
settingsStateListeners.add(ss);
|
||||
return () => {settingsStateListeners.delete(ss);};
|
||||
}, [ss]);
|
||||
return s;
|
||||
};
|
||||
|
||||
export const useChatThreadsState = () => {
|
||||
const [s, ss] = useState(chatThreadsState);
|
||||
useEffect(() => {
|
||||
ss(chatThreadsState);
|
||||
chatThreadsStateListeners.add(ss);
|
||||
return () => {chatThreadsStateListeners.delete(ss);};
|
||||
}, [ss]);
|
||||
return s;
|
||||
// allow user to set state natively in react
|
||||
// const ss: React.Dispatch<React.SetStateAction<ThreadsState>> = (action)=>{
|
||||
// _ss(action)
|
||||
// if (typeof action === 'function') {
|
||||
// const newState = action(chatThreadsState)
|
||||
// chatThreadsState = newState
|
||||
// } else {
|
||||
// chatThreadsState = action
|
||||
// }
|
||||
// }
|
||||
// return [s, ss] as const
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
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(() => {
|
||||
ss(refreshModelState);
|
||||
refreshModelStateListeners.add(ss);
|
||||
return () => {refreshModelStateListeners.delete(ss);};
|
||||
}, [ss]);
|
||||
return s;
|
||||
};
|
||||
|
||||
|
||||
export const useRefreshModelListener = (listener: (providerName: RefreshableProviderName, s: RefreshModelStateOfProvider) => void) => {
|
||||
useEffect(() => {
|
||||
refreshModelProviderListeners.add(listener);
|
||||
return () => {refreshModelProviderListeners.delete(listener);};
|
||||
}, [listener]);
|
||||
};
|
||||
|
||||
|
||||
export const useIsDark = () => {
|
||||
const [s, ss] = useState(colorThemeState);
|
||||
useEffect(() => {
|
||||
ss(colorThemeState);
|
||||
colorThemeStateListeners.add(ss);
|
||||
return () => {colorThemeStateListeners.delete(ss);};
|
||||
}, [ss]);
|
||||
|
||||
// s is the theme, return isDark instead of s
|
||||
const isDark = s === ColorScheme.DARK || s === ColorScheme.HIGH_CONTRAST_DARK;
|
||||
return isDark;
|
||||
|
||||
};
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
export const useScrollbarStyles = (containerRef: React.MutableRefObject<HTMLDivElement | null>) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
// Create selector for specific overflow classes
|
||||
const overflowSelector = [
|
||||
'[class*="overflow-auto"]',
|
||||
'[class*="overflow-x-auto"]',
|
||||
'[class*="overflow-y-auto"]'].
|
||||
join(',');
|
||||
|
||||
// Get all matching elements within the container, including the container itself
|
||||
const scrollElements = [
|
||||
...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []),
|
||||
...Array.from(containerRef.current.querySelectorAll(overflowSelector))];
|
||||
|
||||
|
||||
// Apply styles and listeners to each scroll element
|
||||
scrollElements.forEach((element) => {
|
||||
// Add the scrollable class directly to the overflow element
|
||||
element.classList.add('void-scrollable-element');
|
||||
|
||||
let fadeTimeout: NodeJS.Timeout | null = null;
|
||||
let fadeInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
const fadeIn = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
|
||||
let step = 0;
|
||||
fadeInterval = setInterval(() => {
|
||||
if (step <= 10) {
|
||||
element.classList.remove(`show-scrollbar-${step - 1}`);
|
||||
element.classList.add(`show-scrollbar-${step}`);
|
||||
step++;
|
||||
} else {
|
||||
clearInterval(fadeInterval!);
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
|
||||
const fadeOut = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
|
||||
let step = 10;
|
||||
fadeInterval = setInterval(() => {
|
||||
if (step >= 0) {
|
||||
element.classList.remove(`show-scrollbar-${step + 1}`);
|
||||
element.classList.add(`show-scrollbar-${step}`);
|
||||
step--;
|
||||
} else {
|
||||
clearInterval(fadeInterval!);
|
||||
}
|
||||
}, 60);
|
||||
};
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
fadeIn();
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
fadeTimeout = setTimeout(() => {
|
||||
fadeOut();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
element.addEventListener('mouseenter', onMouseEnter);
|
||||
element.addEventListener('mouseleave', onMouseLeave);
|
||||
|
||||
// Store cleanup function
|
||||
const cleanup = () => {
|
||||
element.removeEventListener('mouseenter', onMouseEnter);
|
||||
element.removeEventListener('mouseleave', onMouseLeave);
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
element.classList.remove('void-scrollable-element');
|
||||
// Remove any remaining show-scrollbar classes
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
element.classList.remove(`show-scrollbar-${i}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Store the cleanup function on the element for later use
|
||||
(element as any).__scrollbarCleanup = cleanup;
|
||||
});
|
||||
|
||||
return () => {
|
||||
// Clean up all scroll elements
|
||||
scrollElements.forEach((element) => {
|
||||
if ((element as any).__scrollbarCleanup) {
|
||||
(element as any).__scrollbarCleanup();
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [containerRef]);
|
||||
};
|
||||
|
|
@ -1,115 +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 { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { FeatureName, featureNames, isFeatureNameDisabled, ModelSelection, modelSelectionsEqual, ProviderName, providerNames, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
|
||||
import { useSettingsState, useRefreshModelState, useAccessor } from '../util/services.js';
|
||||
import { _VoidSelectBox, VoidCustomDropdownBox } from '../util/inputs.js';
|
||||
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
|
||||
import { IconWarning } from '../sidebar-tsx/SidebarChat.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID, VOID_TOGGLE_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
||||
import { ModelOption } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
|
||||
import { WarningBox } from './WarningBox.js';
|
||||
|
||||
const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => {
|
||||
if (m1.length !== m2.length) return false;
|
||||
for (let i = 0; i < m1.length; i++) {
|
||||
if (!modelSelectionsEqual(m1[i].selection, m2[i].selection)) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const ModelSelectBox = ({ options, featureName }: {options: ModelOption[];featureName: FeatureName;}) => {
|
||||
const accessor = useAccessor();
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService');
|
||||
|
||||
const selection = voidSettingsService.state.modelSelectionOfFeature[featureName];
|
||||
const selectedOption = selection ? voidSettingsService.state._modelOptions.find((v) => modelSelectionsEqual(v.selection, selection))! : options[0];
|
||||
|
||||
const onChangeOption = useCallback((newOption: ModelOption) => {
|
||||
voidSettingsService.setModelSelectionOfFeature(featureName, newOption.selection);
|
||||
}, [voidSettingsService, featureName]);
|
||||
|
||||
return <VoidCustomDropdownBox
|
||||
options={options}
|
||||
selectedOption={selectedOption}
|
||||
onChangeOption={onChangeOption}
|
||||
getOptionDisplayName={(option) => option.selection.modelName}
|
||||
getOptionDropdownName={(option) => option.name}
|
||||
getOptionsEqual={(a, b) => optionsEqual([a], [b])}
|
||||
className="void-text-xs void-text-void-fg-3 void-px-1"
|
||||
matchInputWidth={false} />;
|
||||
|
||||
};
|
||||
// const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
|
||||
// const accessor = useAccessor()
|
||||
|
||||
// const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
// let weChangedText = false
|
||||
|
||||
// return <VoidSelectBox
|
||||
// className='@@[&_select]:!void-text-xs text-void-fg-3'
|
||||
// options={options}
|
||||
// onChangeSelection={useCallback((newVal: ModelSelection) => {
|
||||
// if (weChangedText) return
|
||||
// voidSettingsService.setModelSelectionOfFeature(featureName, newVal)
|
||||
// }, [voidSettingsService, featureName])}
|
||||
// // we are responsible for setting the initial state here. always sync instance when state changes.
|
||||
// onCreateInstance={useCallback((instance: SelectBox) => {
|
||||
// const syncInstance = () => {
|
||||
// const modelsListRef = voidSettingsService.state._modelOptions // as a ref
|
||||
// const settingsAtProvider = voidSettingsService.state.modelSelectionOfFeature[featureName]
|
||||
// const selectionIdx = settingsAtProvider === null ? -1 : modelsListRef.findIndex(v => modelSelectionsEqual(v.value, settingsAtProvider))
|
||||
// weChangedText = true
|
||||
// instance.select(selectionIdx === -1 ? 0 : selectionIdx)
|
||||
// weChangedText = false
|
||||
// }
|
||||
// syncInstance()
|
||||
// const disposable = voidSettingsService.onDidChangeState(syncInstance)
|
||||
// return [disposable]
|
||||
// }, [voidSettingsService, featureName])}
|
||||
// />
|
||||
// }
|
||||
|
||||
|
||||
|
||||
const MemoizedModelDropdown = ({ featureName }: {featureName: FeatureName;}) => {
|
||||
const settingsState = useSettingsState();
|
||||
const oldOptionsRef = useRef<ModelOption[]>([]);
|
||||
const [memoizedOptions, setMemoizedOptions] = useState(oldOptionsRef.current);
|
||||
|
||||
useEffect(() => {
|
||||
const oldOptions = oldOptionsRef.current;
|
||||
const newOptions = settingsState._modelOptions;
|
||||
if (!optionsEqual(oldOptions, newOptions)) {
|
||||
setMemoizedOptions(newOptions);
|
||||
}
|
||||
oldOptionsRef.current = newOptions;
|
||||
}, [settingsState._modelOptions]);
|
||||
|
||||
return <ModelSelectBox featureName={featureName} options={memoizedOptions} />;
|
||||
|
||||
};
|
||||
|
||||
export const ModelDropdown = ({ featureName }: {featureName: FeatureName;}) => {
|
||||
const settingsState = useSettingsState();
|
||||
|
||||
const accessor = useAccessor();
|
||||
const commandService = accessor.get('ICommandService');
|
||||
|
||||
const openSettings = () => {commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID);};
|
||||
|
||||
const isDisabled = isFeatureNameDisabled(featureName, settingsState);
|
||||
if (isDisabled)
|
||||
return <WarningBox onClick={openSettings} text={
|
||||
isDisabled === 'needToEnableModel' ? 'Enable a model' :
|
||||
isDisabled === 'addModel' ? 'Add a model' :
|
||||
isDisabled === 'addProvider' || isDisabled === 'notFilledIn' || isDisabled === 'providerNotAutoDetected' ? 'Provider required' :
|
||||
'Provider required'
|
||||
} />;
|
||||
|
||||
return <MemoizedModelDropdown featureName={featureName} />;
|
||||
};
|
||||
|
|
@ -1,660 +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 React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled } from '../../../../common/voidSettingsTypes.js';
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js';
|
||||
import { VoidButton, VoidCheckBox, VoidCustomDropdownBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js';
|
||||
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js';
|
||||
import { X, RefreshCw, Loader2, Check, MoveRight } from 'lucide-react';
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
|
||||
import { isWindows, isLinux, isMacintosh } from '../../../../../../../base/common/platform.js';
|
||||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { env } from '../../../../../../../base/common/process.js';
|
||||
import { ModelDropdown } from './ModelDropdown.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
import { WarningBox } from './WarningBox.js';
|
||||
|
||||
const SubtleButton = ({ onClick, text, icon, disabled }: {onClick: () => void;text: string;icon: React.ReactNode;disabled: boolean;}) => {
|
||||
|
||||
return <div className="void-flex void-items-center void-text-void-fg-3 void-mb-1 void-px-3 void-rounded-sm void-overflow-hidden void-gap-2 hover:void-bg-black/10 dark:hover:void-bg-gray-300/10">
|
||||
<button className="void-flex void-items-center" disabled={disabled} onClick={onClick}>
|
||||
{icon}
|
||||
</button>
|
||||
<span>
|
||||
{text}
|
||||
</span>
|
||||
</div>;
|
||||
};
|
||||
|
||||
// models
|
||||
const RefreshModelButton = ({ providerName }: {providerName: RefreshableProviderName;}) => {
|
||||
|
||||
const refreshModelState = useRefreshModelState();
|
||||
|
||||
const accessor = useAccessor();
|
||||
const refreshModelService = accessor.get('IRefreshModelService');
|
||||
const metricsService = accessor.get('IMetricsService');
|
||||
|
||||
const [justFinished, setJustFinished] = useState<null | 'finished' | 'error'>(null);
|
||||
|
||||
useRefreshModelListener(
|
||||
useCallback((providerName2, refreshModelState) => {
|
||||
if (providerName2 !== providerName) return;
|
||||
const { state } = refreshModelState[providerName];
|
||||
if (!(state === 'finished' || state === 'error')) return;
|
||||
// now we know we just entered 'finished' state for this providerName
|
||||
setJustFinished(state);
|
||||
const tid = setTimeout(() => {setJustFinished(null);}, 2000);
|
||||
return () => clearTimeout(tid);
|
||||
}, [providerName])
|
||||
);
|
||||
|
||||
const { state } = refreshModelState[providerName];
|
||||
|
||||
const { title: providerTitle } = displayInfoOfProviderName(providerName);
|
||||
return <SubtleButton
|
||||
onClick={() => {
|
||||
refreshModelService.startRefreshingModels(providerName, { enableProviderOnSuccess: false, doNotFire: false });
|
||||
metricsService.capture('Click', { providerName, action: 'Refresh Models' });
|
||||
}}
|
||||
text={justFinished === 'finished' ? `${providerTitle} Models are up-to-date!` :
|
||||
justFinished === 'error' ? `${providerTitle} not found!` :
|
||||
`Manually refresh ${providerTitle} models.`
|
||||
}
|
||||
icon={justFinished === 'finished' ? <Check className="void-stroke-green-500 void-size-3" /> :
|
||||
justFinished === 'error' ? <X className="void-stroke-red-500 void-size-3" /> :
|
||||
state === 'refreshing' ? <Loader2 className="void-size-3 void-animate-spin" /> :
|
||||
<RefreshCw className="void-size-3" />
|
||||
}
|
||||
|
||||
disabled={state === 'refreshing' || justFinished !== null} />;
|
||||
|
||||
};
|
||||
|
||||
const RefreshableModels = () => {
|
||||
const settingsState = useSettingsState();
|
||||
|
||||
|
||||
const buttons = refreshableProviderNames.map((providerName) => {
|
||||
if (!settingsState.settingsOfProvider[providerName]._didFillInProviderSettings) return null;
|
||||
return <div key={providerName} className="void-pb-4">
|
||||
<RefreshModelButton providerName={providerName} />
|
||||
</div>;
|
||||
});
|
||||
|
||||
return <>
|
||||
{buttons}
|
||||
</>;
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
const AddModelMenu = ({ onSubmit }: {onSubmit: () => void;}) => {
|
||||
|
||||
const accessor = useAccessor();
|
||||
const settingsStateService = accessor.get('IVoidSettingsService');
|
||||
|
||||
const settingsState = useSettingsState();
|
||||
|
||||
// const providerNameRef = useRef<ProviderName | null>(null)
|
||||
const [providerName, setProviderName] = useState<ProviderName | null>(null);
|
||||
|
||||
const modelNameRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const [errorString, setErrorString] = useState('');
|
||||
|
||||
|
||||
return <>
|
||||
<div className="void-flex void-items-center void-gap-4">
|
||||
|
||||
{/* provider */}
|
||||
<VoidCustomDropdownBox
|
||||
options={providerNames}
|
||||
selectedOption={providerName}
|
||||
onChangeOption={(pn) => setProviderName(pn)}
|
||||
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : '(null)'}
|
||||
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : '(null)'}
|
||||
getOptionsEqual={(a, b) => a === b}
|
||||
className={`void-max-w-44 void-w-full void-border void-border-void-border-2 void-bg-void-bg-1 void-text-void-fg-3 void-text-root void-py-[4px] void-px-[6px] `}
|
||||
|
||||
|
||||
arrowTouchesText={false} />
|
||||
|
||||
{/* <_VoidSelectBox
|
||||
onCreateInstance={useCallback(() => { providerNameRef.current = providerOptions[0].value }, [providerOptions])} // initialize state
|
||||
onChangeSelection={useCallback((providerName: ProviderName) => { providerNameRef.current = providerName }, [])}
|
||||
options={providerOptions}
|
||||
/> */}
|
||||
|
||||
{/* model */}
|
||||
<div className="void-max-w-44 void-w-full void-border void-border-void-border-2 void-bg-void-bg-1 void-text-void-fg-3 void-text-root">
|
||||
<VoidInputBox2
|
||||
placeholder='Model Name'
|
||||
className="void-mt-[2px] void-px-[6px] void-h-full void-w-full"
|
||||
ref={modelNameRef}
|
||||
multiline={false} />
|
||||
|
||||
</div>
|
||||
|
||||
{/* button */}
|
||||
<div className="void-max-w-40">
|
||||
<VoidButton onClick={() => {
|
||||
const modelName = modelNameRef.current?.value;
|
||||
|
||||
if (providerName === null) {
|
||||
setErrorString('Please select a provider.');
|
||||
return;
|
||||
}
|
||||
if (!modelName) {
|
||||
setErrorString('Please enter a model name.');
|
||||
return;
|
||||
}
|
||||
// if model already exists here
|
||||
if (settingsState.settingsOfProvider[providerName].models.find((m) => m.modelName === modelName)) {
|
||||
setErrorString(`This model already exists under ${providerName}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
settingsStateService.addModel(providerName, modelName);
|
||||
onSubmit();
|
||||
|
||||
}}>
|
||||
Add model</VoidButton>
|
||||
</div>
|
||||
|
||||
{!errorString ? null : <div className="void-text-red-500 void-truncate void-whitespace-nowrap">
|
||||
{errorString}
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
</>;
|
||||
|
||||
};
|
||||
|
||||
const AddModelMenuFull = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return <div className="hover:void-bg-black/10 dark:hover:void-bg-gray-300/10 void-py-1 void-my-4 void-pb-1 void-px-3 void-rounded-sm void-overflow-hidden ">
|
||||
{open ?
|
||||
<AddModelMenu onSubmit={() => {setOpen(false);}} /> :
|
||||
<VoidButton onClick={() => setOpen(true)}>Add Model</VoidButton>
|
||||
}
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
||||
export const ModelDump = () => {
|
||||
|
||||
const accessor = useAccessor();
|
||||
const settingsStateService = accessor.get('IVoidSettingsService');
|
||||
|
||||
const settingsState = useSettingsState();
|
||||
|
||||
// a dump of all the enabled providers' models
|
||||
const modelDump: (VoidModelInfo & {providerName: ProviderName;providerEnabled: boolean;})[] = [];
|
||||
for (let providerName of providerNames) {
|
||||
const providerSettings = settingsState.settingsOfProvider[providerName];
|
||||
// if (!providerSettings.enabled) continue
|
||||
modelDump.push(...providerSettings.models.map((model) => ({ ...model, providerName, providerEnabled: !!providerSettings._didFillInProviderSettings })));
|
||||
}
|
||||
|
||||
// sort by hidden
|
||||
modelDump.sort((a, b) => {
|
||||
return Number(b.providerEnabled) - Number(a.providerEnabled);
|
||||
});
|
||||
|
||||
return <div className="">
|
||||
{modelDump.map((m, i) => {
|
||||
const { isHidden, isDefault, isAutodetected, modelName, providerName, providerEnabled } = m;
|
||||
|
||||
const isNewProviderName = (i > 0 ? modelDump[i - 1] : undefined)?.providerName !== providerName;
|
||||
|
||||
const disabled = !providerEnabled;
|
||||
|
||||
return <div key={`${modelName}${providerName}`}
|
||||
className={`void-flex void-items-center void-justify-between void-gap-4 hover:void-bg-black/10 dark:hover:void-bg-gray-300/10 void-py-1 void-px-3 void-rounded-sm void-overflow-hidden void-cursor-default void-truncate `}>
|
||||
|
||||
|
||||
{/* left part is width:full */}
|
||||
<div className={`void-flex-grow void-flex void-items-center void-gap-4`}>
|
||||
<span className="void-w-full void-max-w-32">{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
|
||||
<span className="void-w-fit void-truncate">{modelName}</span>
|
||||
</div>
|
||||
{/* right part is anything that fits */}
|
||||
<div className="void-flex void-items-center void-gap-4">
|
||||
<span className="void-opacity-50 void-truncate">{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
|
||||
|
||||
<VoidSwitch
|
||||
value={disabled ? false : !isHidden}
|
||||
onChange={() => {
|
||||
settingsStateService.toggleModelHidden(providerName, modelName);
|
||||
}}
|
||||
disabled={disabled}
|
||||
size='sm' />
|
||||
|
||||
|
||||
<div className={`void-w-5 void-flex void-items-center void-justify-center`}>
|
||||
{isDefault ? null : <button onClick={() => {settingsStateService.deleteModel(providerName, modelName);}}><X className="void-size-4" /></button>}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
})}
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
||||
|
||||
// providers
|
||||
|
||||
const ProviderSetting = ({ providerName, settingName }: {providerName: ProviderName;settingName: SettingName;}) => {
|
||||
|
||||
|
||||
// const { title: providerTitle, } = displayInfoOfProviderName(providerName)
|
||||
|
||||
const { title: settingTitle, placeholder, subTextMd } = displayInfoOfSettingName(providerName, settingName);
|
||||
|
||||
const accessor = useAccessor();
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService');
|
||||
|
||||
let weChangedTextRef = false;
|
||||
|
||||
return <ErrorBoundary>
|
||||
<div className="void-my-1">
|
||||
<VoidInputBox
|
||||
// placeholder={`${providerTitle} ${settingTitle} (${placeholder})`}
|
||||
placeholder={`${settingTitle} (${placeholder})`}
|
||||
onChangeText={useCallback((newVal) => {
|
||||
if (weChangedTextRef) return;
|
||||
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal);
|
||||
}, [voidSettingsService, providerName, settingName])}
|
||||
|
||||
// we are responsible for setting the initial value. always sync the instance whenever there's a change to state.
|
||||
onCreateInstance={useCallback((instance: InputBox) => {
|
||||
const syncInstance = () => {
|
||||
const settingsAtProvider = voidSettingsService.state.settingsOfProvider[providerName];
|
||||
const stateVal = settingsAtProvider[settingName as SettingName];
|
||||
|
||||
// console.log('SYNCING TO', providerName, settingName, stateVal)
|
||||
weChangedTextRef = true;
|
||||
instance.value = stateVal as string;
|
||||
weChangedTextRef = false;
|
||||
};
|
||||
|
||||
syncInstance();
|
||||
const disposable = voidSettingsService.onDidChangeState(syncInstance);
|
||||
return [disposable];
|
||||
}, [voidSettingsService, providerName, settingName])}
|
||||
multiline={false} />
|
||||
|
||||
{subTextMd === undefined ? null : <div className="void-py-1 void-px-3 void-opacity-50 void-text-sm">
|
||||
<ChatMarkdownRender noSpace string={subTextMd} />
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>;
|
||||
};
|
||||
|
||||
const SettingsForProvider = ({ providerName }: {providerName: ProviderName;}) => {
|
||||
const voidSettingsState = useSettingsState();
|
||||
|
||||
const needsModel = isProviderNameDisabled(providerName, voidSettingsState) === 'addModel';
|
||||
|
||||
// const accessor = useAccessor()
|
||||
// const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
// const { enabled } = voidSettingsState.settingsOfProvider[providerName]
|
||||
const settingNames = customSettingNamesOfProvider(providerName);
|
||||
|
||||
const { title: providerTitle } = displayInfoOfProviderName(providerName);
|
||||
|
||||
return <div className="void-my-4">
|
||||
|
||||
<div className="void-flex void-items-center void-w-full void-gap-4">
|
||||
<h3 className="void-text-xl void-truncate">{providerTitle}</h3>
|
||||
|
||||
{/* enable provider switch */}
|
||||
{/* <VoidSwitch
|
||||
value={!!enabled}
|
||||
onChange={
|
||||
useCallback(() => {
|
||||
const enabledRef = voidSettingsService.state.settingsOfProvider[providerName].enabled
|
||||
voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabledRef)
|
||||
}, [voidSettingsService, providerName])}
|
||||
size='sm+'
|
||||
/> */}
|
||||
</div>
|
||||
|
||||
<div className="void-px-0">
|
||||
{/* settings besides models (e.g. api key) */}
|
||||
{settingNames.map((settingName, i) => {
|
||||
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />;
|
||||
})}
|
||||
|
||||
{needsModel ?
|
||||
providerName === 'ollama' ?
|
||||
<WarningBox text={`Please install an Ollama model. We'll auto-detect it.`} /> :
|
||||
<WarningBox text={`Please add a model for ${providerTitle} below (Models).`} /> :
|
||||
null}
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
||||
export const VoidProviderSettings = ({ providerNames }: {providerNames: ProviderName[];}) => {
|
||||
return <>
|
||||
{providerNames.map((providerName) =>
|
||||
<SettingsForProvider key={providerName} providerName={providerName} />
|
||||
)}
|
||||
</>;
|
||||
};
|
||||
|
||||
|
||||
type TabName = 'models' | 'general';
|
||||
export const AutoRefreshToggle = () => {
|
||||
const settingName: GlobalSettingName = 'autoRefreshModels';
|
||||
|
||||
const accessor = useAccessor();
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService');
|
||||
const metricsService = accessor.get('IMetricsService');
|
||||
|
||||
const voidSettingsState = useSettingsState();
|
||||
|
||||
// right now this is just `enabled_autoRefreshModels`
|
||||
const enabled = voidSettingsState.globalSettings[settingName];
|
||||
|
||||
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="void-stroke-green-500 void-size-3" /> : <X className="void-stroke-red-500 void-size-3" />}
|
||||
disabled={false} />;
|
||||
|
||||
};
|
||||
|
||||
export const AIInstructionsBox = () => {
|
||||
const accessor = useAccessor();
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService');
|
||||
const voidSettingsState = useSettingsState();
|
||||
return <VoidInputBox2
|
||||
className="void-min-h-[81px] void-p-3 void-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 = () => {
|
||||
return <>
|
||||
<h2 className={`void-text-3xl void-mb-2`}>Local Providers</h2>
|
||||
{/* <h3 className={`opacity-50 mb-2`}>{`Keep your data private by hosting AI locally on your computer.`}</h3> */}
|
||||
{/* <h3 className={`opacity-50 mb-2`}>{`Instructions:`}</h3> */}
|
||||
{/* <h3 className={`mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3> */}
|
||||
<h3 className={`void-text-void-fg-3 void-mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
|
||||
<div className="void-pl-4 void-opacity-50">
|
||||
<span className={`void-text-sm void-mb-2`}><ChatMarkdownRender noSpace string={`1. Download [Ollama](https://ollama.com/download).`} /></span>
|
||||
<span className={`void-text-sm void-mb-2`}><ChatMarkdownRender noSpace string={`2. Open your terminal.`} /></span>
|
||||
<span className={`void-text-sm void-mb-2 void-select-text`}><ChatMarkdownRender noSpace string={`3. Run \`ollama run llama3.1\`. This installs Meta's llama3.1 model which is best for chat and inline edits. Requires 5GB of memory.`} /></span>
|
||||
<span className={`void-text-sm void-mb-2 void-select-text`}><ChatMarkdownRender noSpace string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This installs a faster autocomplete model. Requires 1GB of memory.`} /></span>
|
||||
<span className={`void-text-sm void-mb-2`}><ChatMarkdownRender noSpace string={`Void automatically detects locally running models and enables them.`} /></span>
|
||||
{/* TODO we should create UI for downloading models without user going into terminal */}
|
||||
</div>
|
||||
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings providerNames={localProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
||||
<h2 className={`void-text-3xl void-mb-2 void-mt-12`}>Providers</h2>
|
||||
<h3 className={`void-text-void-fg-3 void-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={`void-text-3xl void-mb-2 void-mt-12`}>Models</h2>
|
||||
<ErrorBoundary>
|
||||
<AutoRefreshToggle />
|
||||
<RefreshableModels />
|
||||
<ModelDump />
|
||||
<AddModelMenuFull />
|
||||
</ErrorBoundary>
|
||||
</>;
|
||||
};
|
||||
|
||||
|
||||
|
||||
// https://github.com/VSCodium/vscodium/blob/master/docs/index.md#migrating-from-visual-studio-code-to-vscodium
|
||||
// https://code.visualstudio.com/docs/editor/extension-marketplace#_where-are-extensions-installed
|
||||
type TransferFilesInfo = {from: URI;to: URI;}[];
|
||||
const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null): TransferFilesInfo => {
|
||||
if (os === null)
|
||||
throw new Error(`One-click switch is not possible in this environment.`);
|
||||
if (os === 'mac') {
|
||||
const homeDir = env['HOME'];
|
||||
if (!homeDir) throw new Error(`$HOME not found`);
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json')
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json')
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions')
|
||||
}];
|
||||
}
|
||||
|
||||
if (os === 'linux') {
|
||||
const homeDir = env['HOME'];
|
||||
if (!homeDir) throw new Error(`variable for $HOME location not found`);
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json')
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json')
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions')
|
||||
}];
|
||||
}
|
||||
|
||||
if (os === 'windows') {
|
||||
const appdata = env['APPDATA'];
|
||||
if (!appdata) throw new Error(`variable for %APPDATA% location not found`);
|
||||
const userprofile = env['USERPROFILE'];
|
||||
if (!userprofile) throw new Error(`variable for %USERPROFILE% location not found`);
|
||||
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json')
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json')
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions')
|
||||
}];
|
||||
}
|
||||
|
||||
throw new Error(`os '${os}' not recognized`);
|
||||
};
|
||||
|
||||
const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null;
|
||||
let transferTheseFiles: TransferFilesInfo = [];
|
||||
let transferError: string | null = null;
|
||||
|
||||
try {transferTheseFiles = transferTheseFilesOfOS(os);}
|
||||
catch (e) {transferError = e + '';}
|
||||
|
||||
const OneClickSwitchButton = () => {
|
||||
const accessor = useAccessor();
|
||||
const fileService = accessor.get('IFileService');
|
||||
|
||||
const [state, setState] = useState<{type: 'done';error?: string;} | {type: 'loading' | 'justfinished';}>({ type: 'done' });
|
||||
|
||||
if (transferTheseFiles.length === 0)
|
||||
return <>
|
||||
<WarningBox text={transferError ?? `One-click switch not available.`} />
|
||||
</>;
|
||||
|
||||
|
||||
|
||||
const onClick = async () => {
|
||||
|
||||
if (state.type !== 'done') return;
|
||||
|
||||
setState({ type: 'loading' });
|
||||
|
||||
let errAcc = '';
|
||||
for (let { from, to } of transferTheseFiles) {
|
||||
console.log('transferring', from, to);
|
||||
// not sure if this can fail, just wrapping it with try/catch for now
|
||||
try {await fileService.copy(from, to, true);}
|
||||
catch (e) {errAcc += e + '\n';}
|
||||
}
|
||||
const hadError = !!errAcc;
|
||||
if (hadError) {
|
||||
setState({ type: 'done', error: errAcc });
|
||||
} else
|
||||
{
|
||||
setState({ type: 'justfinished' });
|
||||
setTimeout(() => {setState({ type: 'done' });}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
return <>
|
||||
<VoidButton disabled={state.type !== 'done'} onClick={onClick}>
|
||||
{state.type === 'done' ? 'Transfer my Settings' :
|
||||
state.type === 'loading' ? 'Transferring...' :
|
||||
state.type === 'justfinished' ? 'Success!' :
|
||||
null
|
||||
}
|
||||
</VoidButton>
|
||||
{state.type === 'done' && state.error ? <WarningBox text={state.error} /> : null}
|
||||
</>;
|
||||
};
|
||||
|
||||
|
||||
const GeneralTab = () => {
|
||||
const accessor = useAccessor();
|
||||
const commandService = accessor.get('ICommandService');
|
||||
|
||||
return <>
|
||||
|
||||
|
||||
<div className="">
|
||||
<h2 className={`void-text-3xl void-mb-2`}>One-Click Switch</h2>
|
||||
<h4 className={`void-text-void-fg-3 void-mb-2`}>{`Transfer your settings from VS Code to Void in one click.`}</h4>
|
||||
<OneClickSwitchButton />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="void-mt-12">
|
||||
<h2 className={`void-text-3xl void-mb-2`}>Built-in Settings</h2>
|
||||
<h4 className={`void-text-void-fg-3 void-mb-2`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
|
||||
|
||||
<div className="void-my-4">
|
||||
<VoidButton onClick={() => {commandService.executeCommand('workbench.action.openSettings');}}>
|
||||
General Settings
|
||||
</VoidButton>
|
||||
</div>
|
||||
<div className="void-my-4">
|
||||
<VoidButton onClick={() => {commandService.executeCommand('workbench.action.openGlobalKeybindings');}}>
|
||||
Keyboard Settings
|
||||
</VoidButton>
|
||||
</div>
|
||||
<div className="void-my-4">
|
||||
<VoidButton onClick={() => {commandService.executeCommand('workbench.action.selectTheme');}}>
|
||||
Theme Settings
|
||||
</VoidButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="void-mt-12">
|
||||
<h2 className={`void-text-3xl void-mb-2`}>AI Instructions</h2>
|
||||
<h4 className={`void-text-void-fg-3 void-mb-2`}>{`Instructions to include on all AI requests.`}</h4>
|
||||
<AIInstructionsBox />
|
||||
</div>
|
||||
|
||||
<div className="void-mt-12">
|
||||
<h2 className={`void-text-3xl void-mb-2`}>Model Selection</h2>
|
||||
{featureNames.map((featureName) =>
|
||||
<div key={featureName}
|
||||
className="void-mb-2">
|
||||
|
||||
<h4 className={`void-text-void-fg-3`}>{displayInfoOfFeatureName(featureName)}</h4>
|
||||
<ModelDropdown featureName={featureName} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</>;
|
||||
};
|
||||
|
||||
// full settings
|
||||
|
||||
export const Settings = () => {
|
||||
const isDark = useIsDark();
|
||||
|
||||
const [tab, setTab] = useState<TabName>('models');
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
useScrollbarStyles(containerRef);
|
||||
|
||||
return <div className={`void-scope ${isDark ? "void-dark" : ""}`} style={{ height: '100%', width: '100%' }}>
|
||||
<div ref={containerRef} className="void-overflow-y-auto void-w-full void-h-full void-px-10 void-py-10 void-select-none">
|
||||
|
||||
<div className="void-max-w-5xl void-mx-auto">
|
||||
|
||||
<h1 className="void-text-2xl void-w-full">Void Settings</h1>
|
||||
|
||||
{/* separator */}
|
||||
<div className="void-w-full void-h-[1px] void-my-4" />
|
||||
|
||||
<div className="void-flex void-items-stretch">
|
||||
|
||||
{/* tabs */}
|
||||
<div className="void-flex void-flex-col void-w-full void-max-w-32">
|
||||
<button className={`void-text-left void-p-1 void-px-3 void-my-0.5 void-rounded-sm void-overflow-hidden ${tab === 'models' ? "void-bg-black/10 dark:void-bg-gray-200/10" : ""} hover:void-bg-black/10 hover:dark:void-bg-gray-200/10 active:void-bg-black/10 active:dark:void-bg-gray-200/10 `}
|
||||
onClick={() => {setTab('models');}}>
|
||||
Models</button>
|
||||
<button className={`void-text-left void-p-1 void-px-3 void-my-0.5 void-rounded-sm void-overflow-hidden ${tab === 'general' ? "void-bg-black/10 dark:void-bg-gray-200/10" : ""} hover:void-bg-black/10 hover:dark:void-bg-gray-200/10 active:void-bg-black/10 active:dark:void-bg-gray-200/10 `}
|
||||
onClick={() => {setTab('general');}}>
|
||||
General</button>
|
||||
</div>
|
||||
|
||||
{/* separator */}
|
||||
<div className="void-w-[1px] void-mx-4" />
|
||||
|
||||
|
||||
{/* content */}
|
||||
<div className="void-w-full void-min-w-[600px] void-overflow-auto">
|
||||
|
||||
<div className={`${tab !== 'models' ? "void-hidden" : ""}`}>
|
||||
<FeaturesTab />
|
||||
</div>
|
||||
|
||||
<div className={`${tab !== 'general' ? "void-hidden" : ""}`}>
|
||||
<GeneralTab />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>;
|
||||
};
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { IconWarning } from '../sidebar-tsx/SidebarChat.js';
|
||||
|
||||
|
||||
export const WarningBox = ({ text, onClick, className }: {text: string;onClick?: () => void;className?: string;}) => {
|
||||
|
||||
return <div
|
||||
className={` void-text-void-warning void-brightness-90 void-opacity-90 void-w-fit void-text-xs void-text-ellipsis ${
|
||||
|
||||
|
||||
onClick ? `hover:void-brightness-75 void-transition-all void-duration-200 void-cursor-pointer` : ""} void-flex void-items-center void-flex-nowrap ${
|
||||
|
||||
className} `}
|
||||
|
||||
onClick={onClick}>
|
||||
|
||||
<IconWarning
|
||||
size={14}
|
||||
className="void-mr-1" />
|
||||
|
||||
<span>{text}</span>
|
||||
</div>;
|
||||
// return <VoidSelectBox
|
||||
// options={[{ text: 'Please add a model!', value: null }]}
|
||||
// onChangeSelection={() => { }}
|
||||
// />
|
||||
};
|
||||
|
|
@ -1,9 +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 { mountFnGenerator } from '../util/mountFnGenerator.js';
|
||||
import { Settings } from './Settings.js';
|
||||
|
||||
export const mountVoidSettings = mountFnGenerator(Settings);
|
||||
|
|
@ -172,7 +172,7 @@ registerAction2(class extends Action2 {
|
|||
constructor() {
|
||||
super({
|
||||
id: VOID_OPEN_SETTINGS_ACTION_ID,
|
||||
title: nls.localize2('voidSettings', "Void: Open Settings"),
|
||||
title: nls.localize2('voidSettingsAction2', "Void: Open Settings"),
|
||||
f1: true,
|
||||
icon: Codicon.settingsGear,
|
||||
});
|
||||
|
|
@ -202,7 +202,7 @@ MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
|
|||
group: '0_command',
|
||||
command: {
|
||||
id: VOID_TOGGLE_SETTINGS_ACTION_ID,
|
||||
title: nls.localize('voidSettings', "Void\'s Settings")
|
||||
title: nls.localize('voidSettingsActionGear', "Void\'s Settings")
|
||||
},
|
||||
order: 1
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { INotificationService } from '../../../../platform/notification/common/n
|
|||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { IVoidUpdateService } from '../common/voidUpdateService.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
import * as dom from '../../../../base/browser/dom.js';
|
||||
|
||||
|
||||
|
||||
|
|
@ -84,8 +85,10 @@ class VoidUpdateWorkbenchContribution extends Disposable implements IWorkbenchCo
|
|||
this._register({ dispose: () => clearTimeout(initId) })
|
||||
|
||||
// check every 3 hours
|
||||
const intervalId = setInterval(() => autoCheck(), 3 * 60 * 60 * 1000)
|
||||
this._register({ dispose: () => clearInterval(intervalId) })
|
||||
const { window } = dom.getActiveWindow()
|
||||
|
||||
const intervalId = window.setInterval(() => autoCheck(), 3 * 60 * 60 * 1000)
|
||||
this._register({ dispose: () => window.clearInterval(intervalId) })
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue