From fc6ae97200997e88199dfb3908fdb36b3d49890e Mon Sep 17 00:00:00 2001 From: jmaxdev <251231531+jmaxdev@users.noreply.github.com> Date: Sat, 25 Apr 2026 20:40:55 -0300 Subject: [PATCH 01/13] Feat: Deep Link support --- apps/desktop/src-tauri/Cargo.toml | 2 ++ apps/desktop/src-tauri/src/cli.rs | 5 +++++ apps/desktop/src-tauri/src/lib.rs | 9 +++++++++ apps/desktop/src-tauri/tauri.conf.json | 5 +++++ 4 files changed, 21 insertions(+) diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 282ed3d0..859afaae 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -62,6 +62,8 @@ sysinfo = { version = "0.36.1", default-features = false, features = ["system"] scraper = "0.19" html2text = "0.12" tauri-plugin-http = "2" +tauri-plugin-deep-link = "2.0.0" +tauri-plugin-single-instance = "2.0.0" notify = "6" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] diff --git a/apps/desktop/src-tauri/src/cli.rs b/apps/desktop/src-tauri/src/cli.rs index e10f6f6a..ea62dafa 100644 --- a/apps/desktop/src-tauri/src/cli.rs +++ b/apps/desktop/src-tauri/src/cli.rs @@ -108,6 +108,11 @@ pub fn parse_args(argv: &[String], cwd: Option) -> CliWorkspace { return CliWorkspace::Empty; }; + // Ignore deep link URLs + if raw.starts_with("tide://") { + return CliWorkspace::Empty; + } + resolve_workspace_path(&raw, cwd.as_deref()) } diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 8c8093a9..34345916 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -1969,6 +1969,15 @@ pub fn run() { .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_store::Builder::default().build()) .plugin(tauri_plugin_positioner::init()) + .plugin(tauri_plugin_deep_link::init()) + .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { + if let Some(window) = app.get_webview_window("main") { + let _ = window.set_focus(); + // If it's a deep link, we might want to trigger a refresh + // The deep-link plugin handles the initial link, and subsequent ones + // trigger an event we'll listen for in the frontend. + } + })) .plugin( tauri_plugin_window_state::Builder::default() .with_state_flags( diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 8328abd6..5a04bb90 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -52,6 +52,11 @@ "https://github.com/TrixtyAI/ide/releases/latest/download/latest.json" ], "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEZENjJGRDY0ODk4MjA5MEQKUldRTkNZS0paUDFpL1RTS3hzZm5RWCt0aUR5ckhYSjU4NUZ1VFUvVzBMZXlVMnVNVkxVZHFBc3IK" + }, + "deep-link": { + "schemes": [ + "tide" + ] } }, "bundle": { From 3bfbccc0b2f1596f94b32530e8a04a97becacb0f Mon Sep 17 00:00:00 2001 From: jmaxdev <251231531+jmaxdev@users.noreply.github.com> Date: Sat, 25 Apr 2026 20:59:46 -0300 Subject: [PATCH 02/13] chore: release v1.1.0 --- apps/desktop/package.json | 2 +- apps/desktop/src-tauri/Cargo.toml | 2 +- apps/desktop/src-tauri/tauri.conf.json | 4 +- package.json | 3 +- scripts/publish.mjs | 94 ++++++++++++++++++++++++++ 5 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 scripts/publish.mjs diff --git a/apps/desktop/package.json b/apps/desktop/package.json index b571f876..22178f70 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@trixty/desktop", - "version": "1.0.11", + "version": "1.1.0", "private": true, "scripts": { "dev": "next dev", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 859afaae..b488c88d 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "TrixtyIDE" -version = "0.0.0" +version = "1.1.0" description = "A Tauri App" authors = ["jmaxdev"] license = "UPL-1.0" diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 5a04bb90..48d700bd 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "TrixtyIDE", - "version": "0.0.0", + "version": "1.1.0", "identifier": "trixty.ide", "build": { "frontendDist": "../out", @@ -71,4 +71,4 @@ "icons/icon.ico" ] } -} \ No newline at end of file +} diff --git a/package.json b/package.json index cb2a0b82..dfef932a 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "scripts": { "build": "turbo run build", "desktop": "pnpm --filter @trixty/desktop tauri", - "version:sync": "node scripts/sync-version.mjs" + "version:sync": "node scripts/sync-version.mjs", + "version:publish": "node scripts/publish.mjs" }, "devDependencies": { "turbo": "^2" diff --git a/scripts/publish.mjs b/scripts/publish.mjs new file mode 100644 index 00000000..676b85af --- /dev/null +++ b/scripts/publish.mjs @@ -0,0 +1,94 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { execSync } from 'node:child_process'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.join(__dirname, '..'); +const PKG_PATH = path.join(ROOT, 'apps/desktop/package.json'); + +function getVersion() { + const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf-8')); + return pkg.version; +} + +function parseVersion(v) { + return v.split('.').map(Number); +} + +function isGreater(v1, v2) { + const [major1, minor1, patch1] = parseVersion(v1); + const [major2, minor2, patch2] = parseVersion(v2); + + if (major1 > major2) return true; + if (major1 < major2) return false; + if (minor1 > minor2) return true; + if (minor1 < minor2) return false; + return patch1 > patch2; +} + +function incrementVersion(version, type) { + let [major, minor, patch] = parseVersion(version); + + switch (type) { + case 'major': + major++; + minor = 0; + patch = 0; + break; + case 'minor': + minor++; + patch = 0; + break; + case 'patch': + patch++; + break; + default: + if (type.includes('.')) { + if (!isGreater(type, version)) { + throw new Error(`Nueva versión ${type} no puede ser menor o igual a la actual ${version}`); + } + return type; + } + throw new Error('Tipo de incremento inválido. Usar: major, minor, patch o versión específica (e.g. 1.2.3)'); + } + + return `${major}.${minor}.${patch}`; +} + +const type = process.argv[2]; +if (!type) { + console.error('Uso: node scripts/publish.mjs '); + process.exit(1); +} + +try { + const currentVersion = getVersion(); + const nextVersion = incrementVersion(currentVersion, type); + + console.log(`🚀 Incrementando versión: ${currentVersion} -> ${nextVersion}`); + + // 1. Update apps/desktop/package.json + const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf-8')); + pkg.version = nextVersion; + fs.writeFileSync(PKG_PATH, JSON.stringify(pkg, null, 2) + '\n'); + + // 2. Sync other files + console.log('🔄 Sincronizando archivos...'); + execSync('node scripts/sync-version.mjs', { stdio: 'inherit' }); + + // 3. Git operations + console.log('📦 Commiteando y tagueando...'); + execSync('git add .', { stdio: 'inherit' }); + execSync(`git commit -m "chore: release v${nextVersion}"`, { stdio: 'inherit' }); + execSync(`git tag v${nextVersion}`, { stdio: 'inherit' }); + + console.log('📤 Pusheando a GitHub...'); + execSync('git push origin main', { stdio: 'inherit' }); + execSync(`git push origin v${nextVersion}`, { stdio: 'inherit' }); + + console.log(`✅ Publicado con éxito: v${nextVersion}`); +} catch (err) { + console.error(`❌ Error: ${err.message}`); + process.exit(1); +} From 0f2a86327e80ec27bb52c6bfcf1a902247d9eb44 Mon Sep 17 00:00:00 2001 From: jmaxdev <251231531+jmaxdev@users.noreply.github.com> Date: Sat, 25 Apr 2026 21:31:26 -0300 Subject: [PATCH 03/13] Ai chat fix --- apps/desktop/src-tauri/Cargo.lock | 499 +++++++++++++++++- .../builtin.ai-assistant/AiChatComponent.tsx | 51 +- apps/desktop/src/context/SettingsContext.tsx | 13 +- 3 files changed, 541 insertions(+), 22 deletions(-) diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 5ab55eb1..bb2e9269 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -4,7 +4,7 @@ version = 3 [[package]] name = "TrixtyIDE" -version = "0.0.0" +version = "1.1.0" dependencies = [ "dirs 5.0.1", "grep-regex", @@ -22,6 +22,7 @@ dependencies = [ "sysinfo", "tauri", "tauri-build", + "tauri-plugin-deep-link", "tauri-plugin-dialog", "tauri-plugin-fs", "tauri-plugin-http", @@ -29,6 +30,7 @@ dependencies = [ "tauri-plugin-positioner", "tauri-plugin-process", "tauri-plugin-shell", + "tauri-plugin-single-instance", "tauri-plugin-store", "tauri-plugin-updater", "tauri-plugin-window-state", @@ -141,6 +143,30 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-compression" version = "0.4.41" @@ -153,6 +179,113 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "atk" version = "0.18.2" @@ -260,6 +393,19 @@ dependencies = [ "objc2", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "borsh" version = "1.6.1" @@ -539,6 +685,35 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -676,6 +851,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -943,6 +1124,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "document-features" version = "0.2.12" @@ -1053,6 +1243,33 @@ dependencies = [ "encoding_rs", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "env_filter" version = "0.1.4" @@ -1090,6 +1307,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -1273,6 +1511,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -1716,6 +1967,12 @@ dependencies = [ "ahash 0.7.8", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -1743,6 +2000,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1897,7 +2160,7 @@ dependencies = [ "tokio", "tower-service", "tracing", - "windows-registry", + "windows-registry 0.6.1", ] [[package]] @@ -2881,6 +3144,26 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_pipe" version = "1.2.3" @@ -2930,6 +3213,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -3168,6 +3457,17 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -3206,6 +3506,20 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "portable-pty" version = "0.9.0" @@ -3826,6 +4140,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rust_decimal" version = "1.41.0" @@ -4878,6 +5202,27 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-deep-link" +version = "2.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94deb2e2e4641514ac496db2cddcfc850d6fc9d51ea17b82292a0490bd20ba5b" +dependencies = [ + "dunce", + "plist", + "rust-ini", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "tracing", + "url", + "windows-registry 0.5.3", + "windows-result 0.3.4", +] + [[package]] name = "tauri-plugin-dialog" version = "2.7.0" @@ -5012,6 +5357,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33a5b7d78f0dec4406b003ea87c40bf928d801b6fd9323a556172c91d8712c1" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.18", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + [[package]] name = "tauri-plugin-store" version = "2.4.2" @@ -5292,6 +5652,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -5623,6 +5992,17 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + [[package]] name = "unic-char-property" version = "0.9.0" @@ -6222,6 +6602,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-registry" version = "0.6.1" @@ -6598,6 +6989,9 @@ name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] [[package]] name = "winnow" @@ -6828,6 +7222,67 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.15", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow 0.7.15", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.48" @@ -6925,3 +7380,43 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.15", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow 0.7.15", +] diff --git a/apps/desktop/src/addons/builtin.ai-assistant/AiChatComponent.tsx b/apps/desktop/src/addons/builtin.ai-assistant/AiChatComponent.tsx index c0e5ae77..14fee2d1 100644 --- a/apps/desktop/src/addons/builtin.ai-assistant/AiChatComponent.tsx +++ b/apps/desktop/src/addons/builtin.ai-assistant/AiChatComponent.tsx @@ -238,22 +238,28 @@ const AiChatComponent: React.FC = () => { let cancelled = false; // Provider mode: models are manually configured - if (aiSettings.allowProviderKeys && aiSettings.activeProvider) { + if (aiSettings.allowProviderKeys) { const pId = aiSettings.activeProvider; - const providerModels = aiSettings.providerModels[pId] || []; - const syntheticModels: OllamaModel[] = providerModels.map(m => ({ - name: m, - size: 0, - details: { family: pId, parameter_size: '---', quantization_level: '---' } - })); - - setModels(syntheticModels); - setOllamaStatus('connected'); - - if (syntheticModels.length > 0) { - setSelectedModel((prev) => - prev && syntheticModels.some(m => m.name === prev) ? prev : syntheticModels[0].name - ); + if (pId) { + const providerModels = aiSettings.providerModels[pId] || []; + const syntheticModels: OllamaModel[] = providerModels.map(m => ({ + name: m, + size: 0, + details: { family: pId, parameter_size: '---', quantization_level: '---' } + })); + + setModels(syntheticModels); + setOllamaStatus('connected'); + + if (syntheticModels.length > 0) { + setSelectedModel((prev) => + prev && syntheticModels.some(m => m.name === prev) ? prev : syntheticModels[0].name + ); + } + } else { + // No active provider yet, but we are in provider mode so don't fetch from Ollama + setModels([]); + setOllamaStatus('connected'); // status 'connected' prevents showing the "Ollama Required" screen } return; } @@ -693,7 +699,7 @@ const AiChatComponent: React.FC = () => { // one. Otherwise push a fresh "interacting" bubble as before. if (placeholderPushed) { finalizeLastAiMessage(activeSessionId, { - text: t('ai.status.interacting'), + text: message.content || undefined, thinking: message.thinking, }); // The finalizer does not know how to attach `tool_calls`, so we @@ -812,13 +818,13 @@ const AiChatComponent: React.FC = () => { // still renders something. if (placeholderPushed) { finalizeLastAiMessage(activeSessionId, { - text: message.content ?? "", + text: message.content || undefined, thinking: message.thinking, }); } else { addMessageToSession(activeSessionId, { role: "ai", - text: message.content, + text: message.content || "", thinking: message.thinking }); } @@ -855,9 +861,16 @@ const AiChatComponent: React.FC = () => { const baseUrl = aiSettings.useCloudModel ? cloudEndpoint : aiSettings.endpoint; const isLikelyOOM = err instanceof Error && (err.message?.includes("Failed to fetch") || err.message?.includes("NetworkError")); const requestFailed = err instanceof Error && err.message?.includes("Ollama Request Failed"); + const isProviderMode = aiSettings.allowProviderKeys; + + let errorText = t('ai.error_connect', { endpoint: baseUrl }); + if (isLikelyOOM) errorText = t('ai.error_oom'); + else if (requestFailed) errorText = t('ai.error.request_failed'); + else if (isProviderMode) errorText = err instanceof Error ? err.message : String(err); + addMessageToSession(activeSessionId, { role: "ai", - text: isLikelyOOM ? t('ai.error_oom') : (requestFailed ? t('ai.error.request_failed') : t('ai.error_connect', { endpoint: baseUrl })) + text: errorText }); } } finally { diff --git a/apps/desktop/src/context/SettingsContext.tsx b/apps/desktop/src/context/SettingsContext.tsx index 3b3b4346..74452f1c 100644 --- a/apps/desktop/src/context/SettingsContext.tsx +++ b/apps/desktop/src/context/SettingsContext.tsx @@ -270,7 +270,18 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil }, [systemSettings, isInitialLoadComplete]); const updateAISettings = useCallback((newSettings: Partial) => { - setAiSettings((prev) => ({ ...prev, ...newSettings })); + setAiSettings((prev) => { + const updated = { ...prev, ...newSettings }; + + // Mutual exclusivity: Allow Provider Keys vs Cloud Mode + if (newSettings.allowProviderKeys === true) { + updated.useCloudModel = false; + } else if (newSettings.useCloudModel === true) { + updated.allowProviderKeys = false; + } + + return updated; + }); }, []); const updateEditorSettings = useCallback((newSettings: Partial) => { From 64c723a9298e87671ca5c69c06aa9fc1e5972e7c Mon Sep 17 00:00:00 2001 From: jmaxdev <251231531+jmaxdev@users.noreply.github.com> Date: Sat, 25 Apr 2026 21:51:22 -0300 Subject: [PATCH 04/13] removed OLD CLOUD URL --- apps/desktop/src-tauri/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 34345916..da439441 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -106,7 +106,7 @@ fn get_cloud_config() -> String { // Use option_env! to get the variable at compile time. // If not present (e.g. local dev), it falls back to the default. option_env!("CLOUD_CONFIG_URL") - .unwrap_or("https://ollama.unsetsoft.com") + .unwrap_or("https://[IP_ADDRESS]") .to_string() } @@ -1974,7 +1974,7 @@ pub fn run() { if let Some(window) = app.get_webview_window("main") { let _ = window.set_focus(); // If it's a deep link, we might want to trigger a refresh - // The deep-link plugin handles the initial link, and subsequent ones + // The deep-link plugin handles the initial link, and subsequent ones // trigger an event we'll listen for in the frontend. } })) From 7c15472119131a7861bc8f5d36d6fd19b8e554f1 Mon Sep 17 00:00:00 2001 From: jmaxdev <251231531+jmaxdev@users.noreply.github.com> Date: Sat, 25 Apr 2026 22:19:48 -0300 Subject: [PATCH 05/13] feat: implement Gemini and OpenRouter provider adapters with proxy support and add core localization module --- apps/desktop/src-tauri/build.rs | 17 ++++++++ apps/desktop/src-tauri/src/lib.rs | 2 +- .../builtin.ai-assistant/AiChatComponent.tsx | 21 +++++----- .../builtin.ai-assistant/providerAdapter.ts | 40 ++++++++++--------- apps/desktop/src/api/builtin.l10n.ts | 6 ++- 5 files changed, 54 insertions(+), 32 deletions(-) diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs index c4465922..66a2abb5 100644 --- a/apps/desktop/src-tauri/build.rs +++ b/apps/desktop/src-tauri/build.rs @@ -1,4 +1,21 @@ fn main() { println!("cargo:rerun-if-env-changed=CLOUD_CONFIG_URL"); + println!("cargo:rerun-if-changed=../.env"); + + // Manually parse .env to support CLOUD_CONFIG_URL at compile time via option_env! + if let Ok(content) = std::fs::read_to_string("../.env") { + for line in content.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + if let Some((key, value)) = line.split_once('=') { + if key.trim() == "CLOUD_CONFIG_URL" { + println!("cargo:rustc-env=CLOUD_CONFIG_URL={}", value.trim()); + } + } + } + } + tauri_build::build() } diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index da439441..94aefa65 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -106,7 +106,7 @@ fn get_cloud_config() -> String { // Use option_env! to get the variable at compile time. // If not present (e.g. local dev), it falls back to the default. option_env!("CLOUD_CONFIG_URL") - .unwrap_or("https://[IP_ADDRESS]") + .unwrap_or("") .to_string() } diff --git a/apps/desktop/src/addons/builtin.ai-assistant/AiChatComponent.tsx b/apps/desktop/src/addons/builtin.ai-assistant/AiChatComponent.tsx index 14fee2d1..b234f273 100644 --- a/apps/desktop/src/addons/builtin.ai-assistant/AiChatComponent.tsx +++ b/apps/desktop/src/addons/builtin.ai-assistant/AiChatComponent.tsx @@ -247,12 +247,12 @@ const AiChatComponent: React.FC = () => { size: 0, details: { family: pId, parameter_size: '---', quantization_level: '---' } })); - + setModels(syntheticModels); setOllamaStatus('connected'); - + if (syntheticModels.length > 0) { - setSelectedModel((prev) => + setSelectedModel((prev) => prev && syntheticModels.some(m => m.name === prev) ? prev : syntheticModels[0].name ); } @@ -650,7 +650,7 @@ const AiChatComponent: React.FC = () => { // Wipe the partially-streamed placeholder so the retry starts from // a clean bubble instead of appending to the prior failed attempt. placeholderPushed = false; - + if (isProviderMode && aiSettings.activeProvider === 'gemini') { streamResult = await streamGeminiChat(providerKey, selectedModel, history, { ...options, think: false, tools: options.tools as unknown[] }, (delta) => { pushPlaceholderOnce(); @@ -674,7 +674,7 @@ const AiChatComponent: React.FC = () => { } if (!streamResult.ok || !streamResult.message) { - throw new Error(streamResult.errorText || "Ollama Request Failed"); + throw new Error(streamResult.errorText || "AI Request Failed"); } const message: OllamaStreamFinalMessage = streamResult.message; @@ -858,12 +858,11 @@ const AiChatComponent: React.FC = () => { // Do nothing, user stopped intentionally } else { // Check for OOM / Connection lost - const baseUrl = aiSettings.useCloudModel ? cloudEndpoint : aiSettings.endpoint; const isLikelyOOM = err instanceof Error && (err.message?.includes("Failed to fetch") || err.message?.includes("NetworkError")); const requestFailed = err instanceof Error && err.message?.includes("Ollama Request Failed"); const isProviderMode = aiSettings.allowProviderKeys; - - let errorText = t('ai.error_connect', { endpoint: baseUrl }); + + let errorText = aiSettings.useCloudModel ? t('ai.error_connect_cloud') : t('ai.error_connect_local'); if (isLikelyOOM) errorText = t('ai.error_oom'); else if (requestFailed) errorText = t('ai.error.request_failed'); else if (isProviderMode) errorText = err instanceof Error ? err.message : String(err); @@ -999,7 +998,7 @@ const AiChatComponent: React.FC = () => {
- {aiSettings.allowProviderKeys && aiSettings.activeProvider + {aiSettings.allowProviderKeys && aiSettings.activeProvider ? t('ai.models.provider_title', { provider: PROVIDERS[aiSettings.activeProvider].name }) : (aiSettings.useCloudModel ? t('ai.models.cloud_title') : t('ai.models.local_title'))} @@ -1149,7 +1148,7 @@ const AiChatComponent: React.FC = () => { aria-relevant="additions text" aria-busy={isTyping} aria-label={t('ai.chat_log_label')} - className="flex-1 overflow-y-auto p-4 space-y-4 scrollbar-thin bg-[#0e0e0e]" + className="flex-1 overflow-y-auto overflow-x-hidden p-4 space-y-4 scrollbar-thin bg-[#0e0e0e]" > {activeSession?.messages.map((msg, i) => (
@@ -1216,7 +1215,7 @@ const AiChatComponent: React.FC = () => { {msg.text}
) : ( -
{msg.text}
+
{msg.text}
)}
diff --git a/apps/desktop/src/addons/builtin.ai-assistant/providerAdapter.ts b/apps/desktop/src/addons/builtin.ai-assistant/providerAdapter.ts index dac5cd54..87f27480 100644 --- a/apps/desktop/src/addons/builtin.ai-assistant/providerAdapter.ts +++ b/apps/desktop/src/addons/builtin.ai-assistant/providerAdapter.ts @@ -28,10 +28,15 @@ function createProxyFetch() { if (typeof init.body === 'string') { body = JSON.parse(init.body); } else { - // If it's a non-string BodyInit, we don't try to parse it for proxying - // as our proxy command expects a JSON object. body = null; } + + // --- TRUCO DE COMPATIBILIDAD --- + // Si el SDK envolvió todo en 'chatRequest', lo aplanamos para el servidor + if (body && typeof body === 'object' && 'chatRequest' in body) { + const wrapper = body as { chatRequest: Record }; + body = wrapper.chatRequest; + } } catch { body = null; } @@ -51,9 +56,6 @@ function createProxyFetch() { if (payload.kind === "delta" && payload.content) { controller.enqueue(new TextEncoder().encode(payload.content)); } else if (payload.kind === "done") { - // OpenRouter/OpenAI usually don't send a final 'done' string in the body like Ollama, - // but our Rust proxy might be wrapping it. - // Actually, for generic proxying, we should just pass the raw chunks. controller.close(); unlisten(); } else if (payload.kind === "error") { @@ -166,7 +168,7 @@ export async function streamGeminiChat( return { ok: true, status: 200, message: finalMessage }; } catch (err: unknown) { const error = err as Error; - logger.error("[Gemini Adapter] Stream error:", error); + logger.error("Gemini Stream Error:", error); return { ok: false, status: 500, errorText: error.message }; } } @@ -183,23 +185,25 @@ export async function streamOpenRouterChat( const openRouter = new OpenRouter({ apiKey, }); - // Custom fetch for proxy support + + // Inyectamos el proxy fetch para que Tauri maneje la petición (openRouter as unknown as { fetch: unknown }).fetch = createProxyFetch(); - const stream = await (openRouter as unknown as { - chat: { completions: { create: (p: unknown) => Promise> } } - }).chat.completions.create({ - model, - messages: toOpenAIMessages(messages) as never, - temperature: options.temperature, - max_tokens: options.maxTokens, - stream: true, + const stream = await openRouter.chat.send({ + chatRequest: { + model, + messages: toOpenAIMessages(messages) as never, + temperature: options.temperature, + maxTokens: options.maxTokens, + stream: true, + } }); let fullText = ""; - for await (const chunk of stream) { + // El SDK de OpenRouter devuelve un iterador asíncrono directamente + for await (const chunk of stream as AsyncIterable<{ choices: { delta: { content?: string } }[] }>) { if (abortSignal.aborted) break; - const content = chunk.choices[0]?.delta?.content || ""; + const content = (chunk.choices?.[0]?.delta as { content?: string })?.content || ""; if (content) { fullText += content; onDelta(content); @@ -214,7 +218,7 @@ export async function streamOpenRouterChat( return { ok: true, status: 200, message: finalMessage }; } catch (err: unknown) { const error = err as Error; - logger.error("[OpenRouter Adapter] Stream error:", error); + logger.error("OpenRouter SDK Error:", error); return { ok: false, status: 500, errorText: error.message }; } } diff --git a/apps/desktop/src/api/builtin.l10n.ts b/apps/desktop/src/api/builtin.l10n.ts index a205cfe9..58d6357c 100644 --- a/apps/desktop/src/api/builtin.l10n.ts +++ b/apps/desktop/src/api/builtin.l10n.ts @@ -233,7 +233,8 @@ export function registerBuiltinTranslations() { 'ai.history_tooltip': 'Chat History', 'ai.settings_tooltip': 'AI Settings', 'ai.new_session_tooltip': 'New Session', - 'ai.error_connect': 'Error: Could not connect to Ollama at {endpoint}. Check your settings.', + 'ai.error_connect_local': 'Error: No se pudo conectar con Ollama. Revisa tus ajustes.', + 'ai.error_connect_cloud': 'Error: No se pudo conectar con el servidor de Trixty.', 'ai.ollama_error.title': 'Ollama Required', 'ai.ollama_error.desc': 'To use Trixty\'s AI features, you need to have Ollama installed and running on your machine.', 'ai.ollama_error.download': 'Download Ollama', @@ -688,7 +689,8 @@ export function registerBuiltinTranslations() { 'ai.history_tooltip': 'Historial de Chat', 'ai.settings_tooltip': 'Ajustes de IA', 'ai.new_session_tooltip': 'Nueva Sesión', - 'ai.error_connect': 'Error: No se pudo conectar con Ollama en {endpoint}. Revisa tus ajustes.', + 'ai.error_connect_local': 'Error: No se pudo conectar con Ollama. Revisa tus ajustes.', + 'ai.error_connect_cloud': 'Error: No se pudo conectar con el servidor de Trixty.', 'ai.ollama_error.title': 'Se requiere Ollama', 'ai.ollama_error.desc': 'Para usar las funciones de IA de Trixty, necesitas tener Ollama instalado y ejecutándose en tu equipo.', 'ai.ollama_error.download': 'Descargar Ollama', From 448e62f2ba74088b9b8d731c422afdd623852f33 Mon Sep 17 00:00:00 2001 From: jmaxdev <251231531+jmaxdev@users.noreply.github.com> Date: Sat, 25 Apr 2026 22:46:09 -0300 Subject: [PATCH 06/13] feat: implement AiChatComponent for interactive AI assistant sessions --- .../desktop/src/addons/builtin.ai-assistant/AiChatComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/addons/builtin.ai-assistant/AiChatComponent.tsx b/apps/desktop/src/addons/builtin.ai-assistant/AiChatComponent.tsx index b234f273..e054bece 100644 --- a/apps/desktop/src/addons/builtin.ai-assistant/AiChatComponent.tsx +++ b/apps/desktop/src/addons/builtin.ai-assistant/AiChatComponent.tsx @@ -1262,7 +1262,7 @@ const AiChatComponent: React.FC = () => { /> )} - {isTyping && ( + {isTyping && !(activeSession?.messages[activeSession.messages.length - 1]?.role === "ai" && activeSession.messages[activeSession.messages.length - 1].text) && (
From 5b0a76235bb35488d4d2fcb552ae9d997b7dd33d Mon Sep 17 00:00:00 2001 From: jmaxdev <251231531+jmaxdev@users.noreply.github.com> Date: Sat, 25 Apr 2026 22:56:33 -0300 Subject: [PATCH 07/13] Agent Settings improvement --- .../builtin.agent-support/ProvidersPanel.tsx | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/apps/desktop/src/addons/builtin.agent-support/ProvidersPanel.tsx b/apps/desktop/src/addons/builtin.agent-support/ProvidersPanel.tsx index dab81e7d..4c45e71b 100644 --- a/apps/desktop/src/addons/builtin.agent-support/ProvidersPanel.tsx +++ b/apps/desktop/src/addons/builtin.agent-support/ProvidersPanel.tsx @@ -56,11 +56,6 @@ export const ProvidersPanel: React.FC = () => { return (
-
-

{t('agent.providers.title')}

-

{t('agent.providers.desc')}

-
-
{/* Provider Selector */}
@@ -112,10 +107,30 @@ export const ProvidersPanel: React.FC = () => { )}
- {/* Models List */} + {/* Models Section */}
+ {/* Add Model Input (Now at the top) */} +
+ setNewModel(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && addModel()} + placeholder={t('agent.providers.add_model_placeholder', { example: provider.placeholder })} + className="flex-1 bg-[#0a0a0a] border border-[#1a1a1a] rounded-xl p-2.5 text-[12px] text-[#ccc] focus:border-blue-500/50 outline-none" + /> + +
+
{models.map((m) => (
{
)}
- -
- setNewModel(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && addModel()} - placeholder={t('agent.providers.add_model_placeholder', { example: provider.placeholder })} - className="flex-1 bg-[#0a0a0a] border border-[#1a1a1a] rounded-xl p-2.5 text-[12px] text-[#ccc] focus:border-blue-500/50 outline-none" - /> - -
From 158b6983935c6a7798404b47537656fd57757ff0 Mon Sep 17 00:00:00 2001 From: jmaxdev <251231531+jmaxdev@users.noreply.github.com> Date: Sat, 25 Apr 2026 23:01:37 -0300 Subject: [PATCH 08/13] chore: release v1.1.1 --- apps/desktop/package.json | 2 +- apps/desktop/src-tauri/Cargo.toml | 2 +- apps/desktop/src-tauri/tauri.conf.json | 2 +- package.json | 1 + pnpm-lock.yaml | 23 ++++++ scripts/publish.mjs | 108 +++++++++++++++++-------- 6 files changed, 103 insertions(+), 35 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 22178f70..9a0a035c 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@trixty/desktop", - "version": "1.1.0", + "version": "1.1.1", "private": true, "scripts": { "dev": "next dev", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index b488c88d..a82aefd1 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "TrixtyIDE" -version = "1.1.0" +version = "1.1.1" description = "A Tauri App" authors = ["jmaxdev"] license = "UPL-1.0" diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 48d700bd..1402fdb1 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "TrixtyIDE", - "version": "1.1.0", + "version": "1.1.1", "identifier": "trixty.ide", "build": { "frontendDist": "../out", diff --git a/package.json b/package.json index dfef932a..31d65a49 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "version:publish": "node scripts/publish.mjs" }, "devDependencies": { + "prompts": "^2.4.2", "turbo": "^2" }, "packageManager": "pnpm@9.15.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6de1724..bec55194 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + prompts: + specifier: ^2.4.2 + version: 2.4.2 turbo: specifier: ^2 version: 2.9.6 @@ -2311,6 +2314,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -2748,6 +2755,10 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -2918,6 +2929,9 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -5397,6 +5411,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kleur@3.0.3: {} + language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -6028,6 +6044,11 @@ snapshots: prelude-ls@1.2.1: {} + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -6319,6 +6340,8 @@ snapshots: siginfo@2.0.0: {} + sisteransi@1.0.5: {} + source-map-js@1.2.1: {} space-separated-tokens@2.0.2: {} diff --git a/scripts/publish.mjs b/scripts/publish.mjs index 676b85af..877ef5af 100644 --- a/scripts/publish.mjs +++ b/scripts/publish.mjs @@ -2,6 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execSync } from 'node:child_process'; +import prompts from 'prompts'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.join(__dirname, '..'); @@ -56,39 +57,82 @@ function incrementVersion(version, type) { return `${major}.${minor}.${patch}`; } -const type = process.argv[2]; -if (!type) { - console.error('Uso: node scripts/publish.mjs '); - process.exit(1); -} +async function run() { + let type = process.argv[2]; + + if (!type) { + const currentVersion = getVersion(); + const nextPatch = incrementVersion(currentVersion, 'patch'); + const nextMinor = incrementVersion(currentVersion, 'minor'); + const nextMajor = incrementVersion(currentVersion, 'major'); + + const response = await prompts({ + type: 'select', + name: 'value', + message: `Selecciona el tipo de incremento (actual: v${currentVersion})`, + choices: [ + { title: `Patch (v${nextPatch})`, value: 'patch' }, + { title: `Minor (v${nextMinor})`, value: 'minor' }, + { title: `Major (v${nextMajor})`, value: 'major' }, + { title: 'Versión personalizada', value: 'custom' }, + { title: 'Cancelar', value: 'cancel' } + ], + initial: 0 + }); + + if (!response.value || response.value === 'cancel') { + console.log('Operación cancelada.'); + process.exit(0); + } + + if (response.value === 'custom') { + const customResponse = await prompts({ + type: 'text', + name: 'value', + message: 'Ingresa la nueva versión:', + validate: value => /^\d+\.\d+\.\d+$/.test(value) ? true : 'Formato inválido (ej: 1.2.3)' + }); + + if (!customResponse.value) { + console.log('Operación cancelada.'); + process.exit(0); + } + type = customResponse.value; + } else { + type = response.value; + } + } -try { - const currentVersion = getVersion(); - const nextVersion = incrementVersion(currentVersion, type); + try { + const currentVersion = getVersion(); + const nextVersion = incrementVersion(currentVersion, type); - console.log(`🚀 Incrementando versión: ${currentVersion} -> ${nextVersion}`); + console.log(`🚀 Incrementando versión: ${currentVersion} -> ${nextVersion}`); - // 1. Update apps/desktop/package.json - const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf-8')); - pkg.version = nextVersion; - fs.writeFileSync(PKG_PATH, JSON.stringify(pkg, null, 2) + '\n'); - - // 2. Sync other files - console.log('🔄 Sincronizando archivos...'); - execSync('node scripts/sync-version.mjs', { stdio: 'inherit' }); - - // 3. Git operations - console.log('📦 Commiteando y tagueando...'); - execSync('git add .', { stdio: 'inherit' }); - execSync(`git commit -m "chore: release v${nextVersion}"`, { stdio: 'inherit' }); - execSync(`git tag v${nextVersion}`, { stdio: 'inherit' }); - - console.log('📤 Pusheando a GitHub...'); - execSync('git push origin main', { stdio: 'inherit' }); - execSync(`git push origin v${nextVersion}`, { stdio: 'inherit' }); - - console.log(`✅ Publicado con éxito: v${nextVersion}`); -} catch (err) { - console.error(`❌ Error: ${err.message}`); - process.exit(1); + // 1. Update apps/desktop/package.json + const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf-8')); + pkg.version = nextVersion; + fs.writeFileSync(PKG_PATH, JSON.stringify(pkg, null, 2) + '\n'); + + // 2. Sync other files + console.log('🔄 Sincronizando archivos...'); + execSync('node scripts/sync-version.mjs', { stdio: 'inherit' }); + + // 3. Git operations + console.log('📦 Commiteando y tagueando...'); + execSync('git add .', { stdio: 'inherit' }); + execSync(`git commit -m "chore: release v${nextVersion}"`, { stdio: 'inherit' }); + execSync(`git tag v${nextVersion}`, { stdio: 'inherit' }); + + console.log('📤 Pusheando a GitHub...'); + execSync('git push origin main', { stdio: 'inherit' }); + execSync(`git push origin v${nextVersion}`, { stdio: 'inherit' }); + + console.log(`✅ Publicado con éxito: v${nextVersion}`); + } catch (err) { + console.error(`❌ Error: ${err.message}`); + process.exit(1); + } } + +run(); From 4df4ec0fe814f91122ae61b23378c5e325c57470 Mon Sep 17 00:00:00 2001 From: jmaxdev <251231531+jmaxdev@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:06:02 -0300 Subject: [PATCH 09/13] fixed provider bugs --- apps/desktop/src-tauri/Cargo.lock | 2 +- apps/desktop/src-tauri/src/lib.rs | 135 +++++++-- .../builtin.ai-assistant/providerAdapter.ts | 268 ++++++++++++++++-- 3 files changed, 352 insertions(+), 53 deletions(-) diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index bb2e9269..82ca54fb 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -4,7 +4,7 @@ version = 3 [[package]] name = "TrixtyIDE" -version = "1.1.0" +version = "1.1.1" dependencies = [ "dirs 5.0.1", "grep-regex", diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 94aefa65..44b98eaf 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -1662,6 +1662,15 @@ async fn ollama_proxy_stream( let mut sent_done = false; let is_raw = raw_mode.unwrap_or(false); + // Accumulators for building the final message. Ollama sends + // `tool_calls` in non-done chunks (`done: false`) while the final + // `done: true` chunk only carries stats — no `tool_calls`. We + // accumulate content, tool_calls and thinking across all chunks + // and synthesize a complete message for the frontend's done event. + let mut accumulated_content = String::new(); + let mut accumulated_tool_calls: Vec = Vec::new(); + let mut accumulated_thinking = String::new(); + // Main chunk pump. `response.chunk().await` yields `Ok(Some(bytes))` // for each new body fragment, `Ok(None)` at EOF, or `Err(..)` on // transport failure. Cancellation is observed via `try_recv` on the @@ -1723,40 +1732,97 @@ async fn ollama_proxy_stream( Ok(value) => { let done = value.get("done").and_then(|v| v.as_bool()).unwrap_or(false); if done { - let message = value.get("message").cloned(); + // Build the final message by merging what the + // done chunk carries (usually just role + + // empty content) with our accumulated data. + let mut final_msg = value + .get("message") + .cloned() + .unwrap_or_else(|| serde_json::json!({ + "role": "assistant", + "content": "" + })); + + if let serde_json::Value::Object(ref mut map) = final_msg { + // Overwrite content with full accumulated text + if !accumulated_content.is_empty() { + map.insert( + "content".to_string(), + serde_json::Value::String(accumulated_content.clone()), + ); + } + // Attach accumulated tool_calls if the done + // chunk didn't already carry them + if !accumulated_tool_calls.is_empty() + && !map.contains_key("tool_calls") + { + map.insert( + "tool_calls".to_string(), + serde_json::Value::Array(accumulated_tool_calls.clone()), + ); + } + // Attach accumulated thinking if present + if !accumulated_thinking.is_empty() + && !map.contains_key("thinking") + { + map.insert( + "thinking".to_string(), + serde_json::Value::String(accumulated_thinking.clone()), + ); + } + } + let _ = app_for_task.emit( "ollama-stream", OllamaStreamPayload { stream_id: stream_id_for_task.clone(), kind: "done", content: None, - message, + message: Some(final_msg), error: None, }, ); sent_done = true; break; } - // Non-terminal chunk. Extract the delta content - // from the `message.content` slot that Ollama - // populates for chat streams. - let delta = value - .get("message") - .and_then(|m| m.get("content")) - .and_then(|c| c.as_str()) - .map(|s| s.to_string()); - if let Some(content) = delta { - if !content.is_empty() { - let _ = app_for_task.emit( - "ollama-stream", - OllamaStreamPayload { - stream_id: stream_id_for_task.clone(), - kind: "delta", - content: Some(content), - message: None, - error: None, - }, - ); + + // Non-terminal chunk. Extract and accumulate + // content, tool_calls and thinking from the + // `message` object. + if let Some(msg) = value.get("message") { + // Delta content → stream to frontend + accumulate + let delta = msg + .get("content") + .and_then(|c| c.as_str()) + .map(|s| s.to_string()); + if let Some(ref content) = delta { + if !content.is_empty() { + accumulated_content.push_str(content); + let _ = app_for_task.emit( + "ollama-stream", + OllamaStreamPayload { + stream_id: stream_id_for_task.clone(), + kind: "delta", + content: Some(content.clone()), + message: None, + error: None, + }, + ); + } + } + + // Accumulate tool_calls from this chunk + if let Some(serde_json::Value::Array(calls)) = msg.get("tool_calls") { + for call in calls { + accumulated_tool_calls.push(call.clone()); + } + } + + // Accumulate thinking content + if let Some(thinking) = msg.get("thinking").and_then(|t| t.as_str()) { + if !thinking.is_empty() { + accumulated_thinking.push_str(thinking); + } } } } @@ -1776,17 +1842,34 @@ async fn ollama_proxy_stream( // If we fell out of the loop without seeing `done: true` (EOF // mid-stream, or a cancellation), still emit a done event so the - // frontend's awaiter can resolve. Using an empty message here - // signals "nothing to append" — the stream callers already saw - // every delta along the way. + // frontend's awaiter can resolve. Include any accumulated data so + // the frontend has a complete picture of what streamed so far. if !sent_done { + let mut fallback_msg = serde_json::json!({ + "role": "assistant", + "content": accumulated_content + }); + if let serde_json::Value::Object(ref mut map) = fallback_msg { + if !accumulated_tool_calls.is_empty() { + map.insert( + "tool_calls".to_string(), + serde_json::Value::Array(accumulated_tool_calls), + ); + } + if !accumulated_thinking.is_empty() { + map.insert( + "thinking".to_string(), + serde_json::Value::String(accumulated_thinking), + ); + } + } let _ = app_for_task.emit( "ollama-stream", OllamaStreamPayload { stream_id: stream_id_for_task.clone(), kind: "done", content: None, - message: Some(serde_json::json!({ "role": "assistant", "content": "" })), + message: Some(fallback_msg), error: None, }, ); diff --git a/apps/desktop/src/addons/builtin.ai-assistant/providerAdapter.ts b/apps/desktop/src/addons/builtin.ai-assistant/providerAdapter.ts index 87f27480..ff16474f 100644 --- a/apps/desktop/src/addons/builtin.ai-assistant/providerAdapter.ts +++ b/apps/desktop/src/addons/builtin.ai-assistant/providerAdapter.ts @@ -9,6 +9,8 @@ interface ProviderMessage { role: string; content?: string; text?: string; + tool_calls?: { function: { name: string; arguments: Record }; id?: string; type?: string }[]; + tool_call_id?: string; } /** @@ -101,27 +103,120 @@ function createProxyFetch() { }; } -// --- TRANSFORMERS --- +// --- TOOL FORMAT CONVERTERS --- + +/** + * Converts the OpenAI-format IDE_TOOLS array to Gemini's functionDeclarations + * format. Gemini expects: `[{ functionDeclarations: [{ name, description, parameters }] }]` + */ +function toGeminiTools(tools: unknown[]): unknown[] | undefined { + if (!tools || tools.length === 0) return undefined; + const declarations = tools + .filter((t: unknown) => (t as { type: string }).type === 'function') + .map((t: unknown) => { + const fn = (t as { function: { name: string; description: string; parameters: unknown } }).function; + return { + name: fn.name, + description: fn.description, + parameters: fn.parameters, + }; + }); + if (declarations.length === 0) return undefined; + return [{ functionDeclarations: declarations }]; +} + +// --- MESSAGE TRANSFORMERS --- function toGeminiContents(messages: ProviderMessage[]) { // Filter out system messages as they go into systemInstruction return messages .filter(msg => msg.role !== 'system') - .map(msg => ({ - role: msg.role === 'user' ? 'user' : 'model', - parts: [{ text: msg.content || msg.text || '' }] - })); + .map(msg => { + // Tool result messages → functionResponse part + if (msg.role === 'tool' && msg.tool_call_id) { + return { + role: 'user', + parts: [{ + functionResponse: { + name: msg.tool_call_id, + response: { result: msg.content || msg.text || '' } + } + }] + }; + } + // Assistant messages with tool_calls → functionCall parts + if ((msg.role === 'assistant' || msg.role === 'ai') && msg.tool_calls && msg.tool_calls.length > 0) { + const parts = msg.tool_calls.map(tc => ({ + functionCall: { + name: tc.function.name, + args: tc.function.arguments + } + })); + // If there's also text content, prepend it + if (msg.content || msg.text) { + parts.unshift({ text: msg.content || msg.text || '' } as unknown as typeof parts[0]); + } + return { role: 'model', parts }; + } + // Regular user/assistant messages + return { + role: msg.role === 'user' ? 'user' : 'model', + parts: [{ text: msg.content || msg.text || '' }] + }; + }); } function toOpenAIMessages(messages: ProviderMessage[]) { - return messages.map(msg => ({ - role: msg.role === 'ai' ? 'assistant' : msg.role, - content: msg.content || msg.text || '' - })); + return messages.map(msg => { + // Tool result messages + if (msg.role === 'tool') { + return { + role: 'tool' as const, + content: msg.content || msg.text || '', + tool_call_id: msg.tool_call_id || '', + }; + } + // Assistant messages with tool_calls + if ((msg.role === 'assistant' || msg.role === 'ai') && msg.tool_calls && msg.tool_calls.length > 0) { + return { + role: 'assistant' as const, + content: msg.content || msg.text || null, + tool_calls: msg.tool_calls.map((tc, idx) => ({ + id: tc.id || `call_${idx}`, + type: 'function' as const, + function: { + name: tc.function.name, + arguments: typeof tc.function.arguments === 'string' + ? tc.function.arguments + : JSON.stringify(tc.function.arguments), + } + })), + }; + } + // Regular messages + return { + role: msg.role === 'ai' ? 'assistant' as const : msg.role as 'user' | 'system' | 'assistant', + content: msg.content || msg.text || '' + }; + }); } // --- ADAPTERS --- +// Gemini SDK chunk shape — loosened so we don't need to import the full SDK +// type surface. Only the fields we inspect are typed here. +interface GeminiChunk { + text: () => string; + candidates?: { + content?: { + parts?: { + text?: string; + functionCall?: { name: string; args: Record }; + }[]; + }; + }[]; +} + export async function streamGeminiChat( apiKey: string, model: string, @@ -132,13 +227,14 @@ export async function streamGeminiChat( ) { try { const ai = new (GoogleGenAI as unknown as new (opt: { apiKey: string }) => { - models: { generateContentStream: (p: unknown) => Promise<{ stream: AsyncIterable<{ text: () => string }> }> } + models: { generateContentStream: (p: unknown) => Promise<{ stream: AsyncIterable }> } })({ apiKey }); // Custom fetch for proxy support (ai as unknown as { fetch: unknown }).fetch = createProxyFetch(); const contents = toGeminiContents(messages); const systemMsg = messages.find(m => m.role === 'system'); + const geminiTools = options.tools ? toGeminiTools(options.tools) : undefined; const result = await ai.models.generateContentStream({ model, @@ -147,22 +243,57 @@ export async function streamGeminiChat( config: { temperature: options.temperature, maxOutputTokens: options.maxTokens, + ...(geminiTools ? { tools: geminiTools } : {}), } }); let fullText = ""; + const toolCalls: OllamaStreamFinalMessage['tool_calls'] = []; + for await (const chunk of result.stream) { if (abortSignal.aborted) break; - const text = chunk.text(); - if (text) { - fullText += text; - onDelta(text); + + // Check for function calls in the response parts + const candidates = chunk.candidates; + if (candidates) { + for (const candidate of candidates) { + const parts = candidate.content?.parts; + if (parts) { + for (const part of parts) { + if (part.functionCall) { + toolCalls.push({ + function: { + name: part.functionCall.name, + arguments: part.functionCall.args as Record, + }, + id: `gemini_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + type: 'function', + }); + } else if (part.text) { + fullText += part.text; + onDelta(part.text); + } + } + } + } + } else { + // Fallback: use the text() helper if candidates aren't exposed + try { + const text = chunk.text(); + if (text) { + fullText += text; + onDelta(text); + } + } catch { + // text() throws if the chunk is a function call; already handled above + } } } const finalMessage: OllamaStreamFinalMessage = { role: "assistant", - content: fullText + content: fullText, + ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}), }; return { ok: true, status: 200, message: finalMessage }; @@ -173,6 +304,25 @@ export async function streamGeminiChat( } } +// OpenRouter streaming chunk shape (OpenAI-compatible) +interface OpenRouterStreamChunk { + choices: { + delta: { + content?: string; + tool_calls?: { + index: number; + id?: string; + type?: string; + function?: { + name?: string; + arguments?: string; + }; + }[]; + }; + finish_reason?: string; + }[]; +} + export async function streamOpenRouterChat( apiKey: string, model: string, @@ -189,30 +339,96 @@ export async function streamOpenRouterChat( // Inyectamos el proxy fetch para que Tauri maneje la petición (openRouter as unknown as { fetch: unknown }).fetch = createProxyFetch(); + const chatRequest: Record = { + model, + messages: toOpenAIMessages(messages) as never, + temperature: options.temperature, + maxTokens: options.maxTokens, + stream: true, + }; + + // Pass tools to the API if provided + if (options.tools && options.tools.length > 0) { + chatRequest.tools = options.tools; + chatRequest.tool_choice = 'auto'; + } + const stream = await openRouter.chat.send({ - chatRequest: { - model, - messages: toOpenAIMessages(messages) as never, - temperature: options.temperature, - maxTokens: options.maxTokens, - stream: true, - } + chatRequest: chatRequest as never, }); let fullText = ""; - // El SDK de OpenRouter devuelve un iterador asíncrono directamente - for await (const chunk of stream as AsyncIterable<{ choices: { delta: { content?: string } }[] }>) { + // Accumulate streamed tool_calls. OpenAI streaming splits tool calls + // across multiple chunks: the first chunk for a given index carries + // `id` + `function.name`, subsequent ones append to `function.arguments`. + const toolCallAccumulator: Map = new Map(); + + for await (const chunk of stream as AsyncIterable) { if (abortSignal.aborted) break; - const content = (chunk.choices?.[0]?.delta as { content?: string })?.content || ""; + + const choice = chunk.choices?.[0]; + if (!choice) continue; + + const delta = choice.delta; + + // Handle text content + const content = delta?.content || ""; if (content) { fullText += content; onDelta(content); } + + // Handle streamed tool calls + if (delta?.tool_calls) { + for (const tc of delta.tool_calls) { + const idx = tc.index; + const existing = toolCallAccumulator.get(idx); + if (existing) { + // Append to existing tool call (arguments come in fragments) + if (tc.function?.arguments) { + existing.arguments += tc.function.arguments; + } + } else { + // New tool call entry + toolCallAccumulator.set(idx, { + id: tc.id || `or_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + type: tc.type || 'function', + name: tc.function?.name || '', + arguments: tc.function?.arguments || '', + }); + } + } + } + } + + // Convert accumulated tool calls to the final message format + const toolCalls: OllamaStreamFinalMessage['tool_calls'] = []; + for (const [, tc] of toolCallAccumulator) { + let parsedArgs: Record = {}; + try { + parsedArgs = JSON.parse(tc.arguments); + } catch { + logger.warn("[OpenRouter] Failed to parse tool call arguments:", tc.arguments); + } + toolCalls.push({ + function: { + name: tc.name, + arguments: parsedArgs, + }, + id: tc.id, + type: tc.type, + }); } const finalMessage: OllamaStreamFinalMessage = { role: "assistant", - content: fullText + content: fullText, + ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}), }; return { ok: true, status: 200, message: finalMessage }; From 42cbd2ba56dac7f696cc2117bf3e8a522c316eb3 Mon Sep 17 00:00:00 2001 From: jmaxdev <251231531+jmaxdev@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:06:44 -0300 Subject: [PATCH 10/13] chore: release v1.1.2 --- apps/desktop/package.json | 2 +- apps/desktop/src-tauri/Cargo.toml | 2 +- apps/desktop/src-tauri/tauri.conf.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 9a0a035c..226a402c 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@trixty/desktop", - "version": "1.1.1", + "version": "1.1.2", "private": true, "scripts": { "dev": "next dev", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index a82aefd1..1c0e1849 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "TrixtyIDE" -version = "1.1.1" +version = "1.1.2" description = "A Tauri App" authors = ["jmaxdev"] license = "UPL-1.0" diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 1402fdb1..0ac92a16 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "TrixtyIDE", - "version": "1.1.1", + "version": "1.1.2", "identifier": "trixty.ide", "build": { "frontendDist": "../out", From 1a661571416140b768092b9c8f198a22f669b77a Mon Sep 17 00:00:00 2001 From: jmaxdev <251231531+jmaxdev@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:17:15 -0300 Subject: [PATCH 11/13] feat: implement provider adapter with Tauri proxy and message transformers for Gemini and OpenRouter --- apps/desktop/src/addons/builtin.ai-assistant/providerAdapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/addons/builtin.ai-assistant/providerAdapter.ts b/apps/desktop/src/addons/builtin.ai-assistant/providerAdapter.ts index ff16474f..29042f9a 100644 --- a/apps/desktop/src/addons/builtin.ai-assistant/providerAdapter.ts +++ b/apps/desktop/src/addons/builtin.ai-assistant/providerAdapter.ts @@ -368,7 +368,7 @@ export async function streamOpenRouterChat( arguments: string; }> = new Map(); - for await (const chunk of stream as AsyncIterable) { + for await (const chunk of stream as unknown as AsyncIterable) { if (abortSignal.aborted) break; const choice = chunk.choices?.[0]; From d02c88af79645fdfe0bf0d360f6c70996105d58c Mon Sep 17 00:00:00 2001 From: jmaxdev <251231531+jmaxdev@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:17:51 -0300 Subject: [PATCH 12/13] chore: release v1.1.3 --- apps/desktop/package.json | 2 +- apps/desktop/src-tauri/Cargo.toml | 2 +- apps/desktop/src-tauri/tauri.conf.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 226a402c..a818316f 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@trixty/desktop", - "version": "1.1.2", + "version": "1.1.3", "private": true, "scripts": { "dev": "next dev", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 1c0e1849..39d5b81c 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "TrixtyIDE" -version = "1.1.2" +version = "1.1.3" description = "A Tauri App" authors = ["jmaxdev"] license = "UPL-1.0" diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 0ac92a16..f5c62111 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "TrixtyIDE", - "version": "1.1.2", + "version": "1.1.3", "identifier": "trixty.ide", "build": { "frontendDist": "../out", From 800b0d45ed5b52a0b2a6eeb59e5f127454caa156 Mon Sep 17 00:00:00 2001 From: jmaxdev <251231531+jmaxdev@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:24:44 -0300 Subject: [PATCH 13/13] chore: generate Cargo.lock file for desktop application dependencies --- apps/desktop/src-tauri/Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 82ca54fb..ddc506da 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -4,7 +4,7 @@ version = 3 [[package]] name = "TrixtyIDE" -version = "1.1.1" +version = "1.1.3" dependencies = [ "dirs 5.0.1", "grep-regex",