Lint, window.setInterval, + remove ignored files from git tracking

This commit is contained in:
Andrew Pareles 2025-03-01 17:27:24 -08:00
parent a2b2fd6ad5
commit abfd426d3f
36 changed files with 1502 additions and 97382 deletions

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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);
}

View file

@ -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

View file

@ -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;

View file

@ -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)
}

View file

@ -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 one or more lines are too long

View file

@ -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' : ''}`}
/>

View file

@ -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 };

View file

@ -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>
</>);
};

View file

@ -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 + ''} />
)}
</>);
};

View file

@ -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>;
};

View file

@ -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>;
};

View file

@ -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);

View file

@ -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;

View file

@ -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>);
};

View file

@ -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>;
};

View file

@ -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>;
};

View file

@ -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>);
};

View file

@ -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

View file

@ -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];
};

View file

@ -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" />;
// };

View file

@ -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;
};

View file

@ -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;
};

View file

@ -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]);
};

View file

@ -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} />;
};

View file

@ -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>;
};

View file

@ -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={() => { }}
// />
};

View file

@ -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);

View file

@ -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
});

View file

@ -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) })
}
}