Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
cfaff89
fix: track entry count manually in unpack_vsix (zip crate len() bug)
airdropia Aug 14, 2026
759eb2c
chore: bump version to 0.2.1
airdropia Aug 14, 2026
892ac4b
docs: add state.json with full repo context for agents
airdropia Aug 14, 2026
724cccc
fix: register sidex-asset file system provider for extension installs
airdropia Aug 15, 2026
7ed5197
chore: bump version to 0.2.2
airdropia Aug 15, 2026
bb17f57
fix: properly handle sidex-asset URIs with raw Windows paths for exte…
airdropia Aug 15, 2026
18641de
fix: resolve extension install platform and icon loading issues
airdropia Aug 15, 2026
c75cbe4
chore: bump version to 0.2.4
airdropia Aug 15, 2026
14abf13
fix: enable VSIX install and local extension management in SideX
airdropia Aug 15, 2026
adae5e6
chore: bump version to 0.2.5
airdropia Aug 15, 2026
e222fc9
fix: implement TauriExtensionManagementService for VSIX support
airdropia Aug 15, 2026
621fddd
chore: bump version to 0.2.6
airdropia Aug 15, 2026
c440703
fix: use Tauri command for VSIX installation
airdropia Aug 15, 2026
fea9024
chore: bump version to 0.2.7
airdropia Aug 15, 2026
92900e2
fix: VSIX install accessor timing + suppress CMD flashes on startup
airdropia Aug 16, 2026
f93b958
chore: bump version to 0.2.8
airdropia Aug 16, 2026
49bbd3c
fix: add CommandExt import for creation_flags in extension_platform.rs
airdropia Aug 16, 2026
3892b64
fix: show Tauri-installed user extensions in the workbench UI
airdropia Aug 16, 2026
bc90fee
chore: remove debug file ci-steps.json
airdropia Aug 16, 2026
eb8350f
fix: show installed user extensions in UI, serve icons over asset pro…
airdropia Aug 16, 2026
16cbae3
fix: allow dots in view container IDs for extension compatibility
airdropia Aug 17, 2026
ee2bb61
fix: register Node-only user extensions in the workbench registry so …
airdropia Aug 17, 2026
93f114a
fix(extension-host): add missing visibleNotebookEditors/activeNoteboo…
airdropia Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ members = [
exclude = ["extensions-rust"]

[workspace.package]
version = "0.2.0"
version = "0.2.1"
edition = "2021"
rust-version = "1.91.0"
license = "MIT"
Expand Down
13 changes: 12 additions & 1 deletion crates/sidex-extensions/src/marketplace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -545,8 +545,10 @@ impl MarketplaceClient {
/// immediately because retrying a bad URL or auth error cannot recover.
pub async fn download_from_url(&self, url: &str) -> Result<Vec<u8>> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(900))
.timeout(std::time::Duration::from_secs(300))
.connect_timeout(std::time::Duration::from_secs(15))
.gzip(true)
.brotli(true)
.user_agent(concat!(
"SideX/",
env!("CARGO_PKG_VERSION"),
Expand All @@ -556,6 +558,7 @@ impl MarketplaceClient {
.map_err(|e| anyhow::anyhow!("failed to build download client: {e}"))?;

const MAX_ATTEMPTS: u32 = 3;
const MAX_DOWNLOAD_BYTES: u64 = 1024 * 1024 * 1024; // 1 GiB safety cap
let mut last_error = String::new();

for attempt in 1..=MAX_ATTEMPTS {
Expand Down Expand Up @@ -592,6 +595,14 @@ impl MarketplaceClient {
));
}

if let Some(content_length) = response.content_length() {
if content_length > MAX_DOWNLOAD_BYTES {
return Err(anyhow::anyhow!(
"vsix download too large: {content_length} bytes (limit {MAX_DOWNLOAD_BYTES})"
));
}
}

match response.bytes().await {
Ok(bytes) => return Ok(bytes.to_vec()),
Err(err) => {
Expand Down
8 changes: 8 additions & 0 deletions crates/sidex-extensions/src/vsix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ pub fn unpack_vsix(vsix_path: &Path) -> Result<VsixPackage> {
}

let mut total_bytes: u64 = 0;
let mut entry_count: usize = 0;

let mut manifest_json: Option<String> = None;
let mut vsix_manifest_xml: Option<String> = None;
Expand All @@ -92,6 +93,13 @@ pub fn unpack_vsix(vsix_path: &Path) -> Result<VsixPackage> {
let mut modes: HashMap<String, u32> = HashMap::new();

for i in 0..archive.len() {
entry_count += 1;
if entry_count > MAX_ARCHIVE_ENTRIES {
anyhow::bail!(
"VSIX contains too many entries ({} > {MAX_ARCHIVE_ENTRIES})",
entry_count
);
}
let mut entry = archive.by_index(i)?;
let Some(name) = entry.enclosed_name() else {
log::warn!("skipping unsafe VSIX entry: {}", entry.name());
Expand Down
17 changes: 16 additions & 1 deletion infrastructure/marketplace-proxy/src/gallery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ interface GalleryQueryBody {
const FILTER_SEARCH_TEXT = 10;
const FILTER_EXTENSION_IDS = 4;
const FILTER_EXTENSION_NAMES = 7;
const FILTER_TARGET_PLATFORM = 9; // VS Code filter type for target platform

// Target platform string to Open VSX API path segment mapping
const TARGET_PLATFORM_MAP: Record<string, string> = {
'win32-x64': 'win32-x64',
'win32-arm64': 'win32-arm64',
'linux-x64': 'linux-x64',
'linux-arm64': 'linux-arm64',
'darwin-x64': 'darwin-x64',
'darwin-arm64': 'darwin-arm64'
};

// ---------------------------------------------------------------------------
// Upstream fetch helpers (reuse the existing raw fetchers from ms.ts / openvsx.ts)
Expand Down Expand Up @@ -326,13 +337,17 @@ export async function handleGalleryQuery(request: Request, origin: string): Prom
return { body: JSON.stringify(raw), total: pageSize };
}

// Extract target platform from query filters (filterType 9)
const targetPlatformFilter = filter.criteria.find(c => c.filterType === FILTER_TARGET_PLATFORM);
const targetPlatform = targetPlatformFilter?.value ?? null;

// Standard search: fan out to both backends.
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), UPSTREAM_TIMEOUT_MS);

const [msResult, ovsxResult] = await Promise.allSettled([
fetchMsGallery(queryBody, controller.signal),
fetchOpenVsx(query, pageSize, controller.signal)
fetchOpenVsx(query, pageSize, targetPlatform, controller.signal)
]);
clearTimeout(timeout);

Expand Down
31 changes: 30 additions & 1 deletion infrastructure/marketplace-proxy/src/openvsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export function normalizeOpenVsxItem(item: OpenVsxSearchItem): NormalizedExtensi
export async function searchOpenVsx(
query: string,
pageSize: number,
targetPlatform: string | null,
signal: AbortSignal
): Promise<{ items: NormalizedExtension[]; total: number }> {
const url = new URL(`${OPEN_VSX_BASE}/api/-/search`);
Expand All @@ -74,6 +75,34 @@ export async function searchOpenVsx(
throw new Error(`open-vsx ${res.status}`);
}
const json = (await res.json()) as OpenVsxSearchResponse;
const items = (json.extensions ?? []).map(normalizeOpenVsxItem).filter((e): e is NormalizedExtension => !!e);
let items = (json.extensions ?? []).map(normalizeOpenVsxItem).filter((e): e is NormalizedExtension => !!e);

// Filter by target platform if specified
if (targetPlatform) {
items = filterByTargetPlatform(items, targetPlatform);
}

return { items, total: json.totalSize ?? items.length };
}

/**
* Filters Open VSX extensions by target platform.
* Open VSX URLs contain the platform in the path:
* https://open-vsx.org/api/{ns}/{name}/{platform}/{version}/file/...
*/
function filterByTargetPlatform(items: NormalizedExtension[], targetPlatform: string): NormalizedExtension[] {
// Try to find an exact match first
const exactMatch = items.filter(item => item.downloadUrl.includes(`/${targetPlatform}/`));
if (exactMatch.length > 0) {
return exactMatch;
}

// If no exact match, try without platform (universal extensions)
const universal = items.filter(item => !item.downloadUrl.match(/\/(win32|x64|arm64|linux|darwin|alpine)[^/]*\//));
if (universal.length > 0) {
return universal;
}

// Fallback: return first item (allows install to proceed even without perfect match)
return items.slice(0, 1);
}
2 changes: 1 addition & 1 deletion infrastructure/marketplace-proxy/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ async function runSearch(
const timeout = setTimeout(() => controller.abort(), UPSTREAM_TIMEOUT_MS);
const [msResult, ovsxResult] = await Promise.allSettled([
searchMicrosoftMarketplace(query, pageSize, controller.signal),
searchOpenVsx(query, pageSize, controller.signal)
searchOpenVsx(query, pageSize, null, controller.signal)
]);
clearTimeout(timeout);

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sidex",
"version": "0.2.0",
"version": "0.2.13",
"type": "module",
"scripts": {
"setup": "node scripts/generate-extension-meta.js",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sidex"
version = "0.2.0"
version = "0.2.1"
description = "SideX - A fast, open-source code editor"
authors = ["Siden Technologies Inc"]
license = "MIT"
Expand Down
6 changes: 6 additions & 0 deletions src-tauri/extension-host/host.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3314,6 +3314,12 @@ function createVscodeShim() {
get visibleTextEditors() {
return [...host._editorValues.values()];
},
get visibleNotebookEditors() {
return [];
},
get activeNotebookEditor() {
return undefined;
},
onDidChangeActiveTextEditor: (listener, thisArg, disposables) =>
host._onActiveEditorChangeEvent.event(listener, thisArg, disposables),
onDidChangeVisibleTextEditors: (listener, thisArg, disposables) =>
Expand Down
27 changes: 18 additions & 9 deletions src-tauri/src/commands/extension_platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,24 +304,33 @@ pub fn resolve_builtin_extensions_dir(app: &AppHandle) -> PathBuf {
}

fn read_node_version(binary: &str) -> Option<String> {
Command::new(binary)
.arg("--version")
let mut cmd = Command::new(binary);
cmd.arg("--version")
.stdout(Stdio::piped())
.stderr(Stdio::null())
.output()
.stderr(Stdio::null());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
}
cmd.output()
.ok()
.and_then(|out| String::from_utf8(out.stdout).ok())
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
}

fn is_usable_node(binary: &str) -> bool {
Command::new(binary)
.arg("--version")
let mut cmd = Command::new(binary);
cmd.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok()
.stderr(Stdio::null());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
}
cmd.status().is_ok()
}

pub fn bundled_node_candidates(app: &AppHandle) -> Vec<PathBuf> {
Expand Down
5 changes: 5 additions & 0 deletions src-tauri/src/commands/extension_wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ impl LspServerProcess {
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
command.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
}

let mut child = command
.spawn()
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "SideX",
"version": "0.2.0",
"version": "0.2.13",
"identifier": "com.siden.sidex",
"build": {
"frontendDist": "../dist",
Expand Down
7 changes: 6 additions & 1 deletion src/vs/base/common/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,13 @@ class FileAccessImpl {
}

if (uri.scheme === Schemas.file && (globalThis as any).__SIDEX_TAURI__) {
// The WebView runs over HTTPS (useHttpsScheme: true). `sidex-asset://`
// (non-secure custom scheme) would be blocked as mixed content, and
// `vscode-file://` produces ERR_UNKNOWN_URL_SCHEME in `<img>`/fetch.
// Tauri's built-in asset protocol serves files over HTTPS at
// https://asset.localhost/<encoded> with scope $HOME/**.
const encoded = encodeURIComponent(uri.fsPath);
return URI.parse(`sidex-asset://localhost/${encoded}`);
return URI.parse(`https://asset.localhost/${encoded}`);
}

// Convert to `vscode-file` resource..
Expand Down
17 changes: 17 additions & 0 deletions src/vs/platform/files/browser/tauriFileSystemProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,23 @@ export class TauriFileSystemProvider extends Disposable implements IFileSystemPr
if (resource.scheme === 'vscode-file') {
return decodeURIComponent(resource.path);
}
if (resource.scheme === 'sidex-asset') {
let path = resource.path;
// If the path starts with a raw Windows drive letter (e.g. /c:/ or c:/),
// decodeURIComponent may not work properly. Normalize by stripping
// leading slash and then using the raw path.
if (path.startsWith('/') && path.length > 2 && path[2] === ':') {
// It's a raw Windows path like /c:/Users/... -> return as-is after removing leading slash
return path.substring(1);
}
// If it's already percent-encoded, decode it
try {
return decodeURIComponent(path);
} catch {
// If decoding fails, return the path as-is (might be raw)
return path;
}
}
return resource.fsPath;
}

Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/api/browser/viewsExtensionPoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,11 +487,11 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
);
return false;
}
if (!/^[a-z0-9_-]+$/i.test(descriptor.id)) {
if (!/^[a-z0-9_.-]+$/i.test(descriptor.id)) {
collector.error(
localize(
'requireidstring',
"property `{0}` is mandatory and must be of type `string` with non-empty value. Only alphanumeric characters, '_', and '-' are allowed.",
"property `{0}` is mandatory and must be of type `string` with non-empty value. Only alphanumeric characters, '_', '-', and '.' are allowed.",
'id'
)
);
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/api/common/extHostExtensionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
return extensionDescription.extensionLocation;
},
get extensionPath() {
return extensionDescription.extensionLocation.fsPath;
return extensionDescription.extensionLocation.toString();
},
asAbsolutePath(relativePath: string) {
return path.join(extensionDescription.extensionLocation.fsPath, relativePath);
Expand Down
4 changes: 4 additions & 0 deletions src/vs/workbench/browser/web.main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,10 @@ export class BrowserMain extends Disposable {
const vscodeFileProvider = new TauriFileSystemProvider();
fileService.registerProvider(Schemas.vscodeFileResource, vscodeFileProvider);
logService.info('[SideX] Registered TauriFileSystemProvider for vscode-file:// scheme');

const sidexAssetProvider = new TauriFileSystemProvider();
fileService.registerProvider('sidex-asset', sidexAssetProvider);
logService.info('[SideX] Registered TauriFileSystemProvider for sidex-asset:// scheme');
} else {
const userDataProvider = new InMemoryFileSystemProvider();
fileService.registerProvider(Schemas.vscodeUserData, userDataProvider);
Expand Down
Loading
Loading