From c5c4dbf4d0d2b8852256c2c453a112003d544985 Mon Sep 17 00:00:00 2001
From: Jorge Dev <251231531+jmaxdev@users.noreply.github.com>
Date: Fri, 1 May 2026 21:00:30 -0300
Subject: [PATCH 1/4] Main (#309)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fixed lint errors (#308)
* feat: add Sentry error tracking (#287)
* feat: add Sentry error tracking
* feat: integrate Sentry error tracking and add marketplace registry and settings UI enhancements
* improvement: add sentry
* fix: fix lib conflict
* Potential fix for pull request finding 'Useless conditional'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* ci: add GitHub Actions workflows for automated test builds and multi-platform releases
---------
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* Revert "feat: add Sentry error tracking (#287)" (#291)
This reverts commit c779d520828022c1e7ccca6b808ed44a25325814.
* feat: cloud AI streaming for OpenAI / Anthropic / Gemini / OpenRouter (#293)
PR #284 shipped cloud chat as a single-shot non-streaming bridge —
every reply landed atomically once the request finished. That made the
cloud branch feel slower than Ollama, where the chat panel renders
tokens as they arrive. This wires SSE streaming through a new
`cloud_proxy_stream` Tauri command so the UX matches.
Rust
- New `cloud_proxy_stream` and `cloud_proxy_cancel` commands behind the
existing `validate_cloud_proxy_request` policy. The command spawns a
detached tokio task, registers a oneshot cancel handle keyed by the
caller-supplied `streamId`, and emits `cloud-stream` events as
`{ kind: "data" | "done" | "error", data?, error? }`.
- New `split_sse_events` parser splits the response buffer on
`\n\n` / `\r\n\r\n`, joins multi-`data:` lines per event, and drops
`event:` / `id:` / `retry:` / comment lines. The Rust side stays
provider-agnostic; the per-provider JSON shape is parsed in TS.
- The `data: [DONE]` sentinel that OpenAI / OpenRouter use is collapsed
to a structured `done` event so callers don't have to special-case it.
Anthropic and Gemini close the connection silently on completion;
the task synthesises a `done` event on EOF so the awaiter resolves
uniformly.
- Total stream size capped by the same `CLOUD_PROXY_MAX_BODY` limit
(16 MiB) that `cloud_proxy` uses, and per-request timeout uses
`CLOUD_PROXY_TIMEOUT_SECS` (60 s). Cancellation tears the task down
via the oneshot.
- 10 unit tests for `split_sse_events` covering single events, multi-line
data joining, CRLF separators, partial-event buffering, the [DONE]
sentinel, and the SSE-spec single-leading-space rule.
TypeScript
- New `streamCloudChat(req, onDelta)` mirroring `streamOllamaChat`'s
shape: subscribes to `cloud-stream`, filters by `streamId`, parses
the provider-specific delta, and resolves with the full text on done.
- Provider-specific delta extractors split into
`extractStreamDelta(provider, raw)`:
- OpenAI / OpenRouter: `choices[0].delta.content`
- Anthropic: `content_block_delta` events with `delta.type === "text_delta"`
only — `message_start`, `content_block_start`, `ping`, `message_stop`,
`input_json_delta` (future tool deltas) are ignored
- Gemini: `candidates[*].content.parts[*].text` joined
- The matching `extractFullResponse` is exported and unit-tested so the
non-streaming `cloudChat` shares the same parsing surface.
- The provider URL / headers / body builder is consolidated into
`buildProviderRequest(req, stream)`. Gemini's URL flips between
`:generateContent` and `:streamGenerateContent?alt=sse`; the others
just toggle `stream` in the body.
- 12 unit tests covering each provider's streaming and full-response
branches, including the role-only chunk that OpenAI sends as the
first delta and Anthropic's housekeeping events.
UI wiring
- `AiChatComponent` cloud branch now uses `streamCloudChat` with the
same `placeholderPushed` / `appendToLastAiMessage` pattern the Ollama
branch uses. If the stream fails before any content arrives we still
render a clean error bubble; if it fails mid-stream we append the
error inline so the user sees how far the model got.
- Tools / agent mode for cloud providers stays out of scope. Each
provider has its own tool-call shape (`tool_calls` for OpenAI,
`tool_use` blocks for Anthropic, `functionDeclarations` for Gemini)
and that lands in the next PR.
Verification
- pnpm tsc --noEmit / lint --max-warnings 0 clean.
- pnpm vitest run: 125 / 125 (was 113 — added 12 provider tests).
- pnpm build: /, /_not-found, /floating prerender clean.
- cargo build / clippy --lib -- -D warnings / fmt --check clean.
- cargo test --lib: 112 / 112 (was 102 — added 10 SSE-parser tests).
* feat: store cloud AI provider keys in OS keychain (#294)
PR #284 shipped multi-provider keys behind a master switch and PR #286
added a banner warning that those keys lived in `settings.json`
plaintext. This wires the OS native secret store (review item I1) so
the keys never touch disk in clear text.
Backing store
- macOS: Keychain
- Windows: Credential Manager
- Linux: Secret Service / kwallet
The `keyring` crate (3.x) handles per-platform shimming. Pinned to 3.x
because keyring 4.x pulls `turso` (an embedded SQLite engine) and
`bon-macros` as transitive deps for its DB-backed unified store, none
of which we use; our `Entry::new` flow only needs the native OS
keychain backends that 3.x exposes by default.
Rust
- Four new commands behind a hard-coded provider allow-list
(`openai` / `anthropic` / `gemini` / `openrouter`):
`set_provider_secret`, `get_provider_secret`, `clear_provider_secret`,
`has_provider_secret`. Service name is fixed at `trixty.ide` so
entries are namespaced to the app and not visible to other tooling.
- `validate_secret_provider` rejects any string outside the allow-list
so a renderer XSS can't probe arbitrary keychain entries.
- `keyring::Error::NoEntry` is mapped to `Ok(None)` / `Ok(false)` /
`Ok(())` (idempotent clear), so the UI's "never configured" /
"remove" paths don't surface false-positive error toasts.
TypeScript
- New `src/api/providerSecrets.ts` thin wrapper exposing
`setProviderSecret` / `getProviderSecret` / `hasProviderSecret` /
`clearProviderSecret` plus the `SECRET_PROVIDERS` list. Empty
strings round-trip as "no key" — `setProviderSecret(p, "")` clears
the entry, mirroring the way the old plaintext field treated `""`.
- Tauri-binding map extended with the four commands.
UI
- `ProviderKeysPanel` now loads each provider's key from the keychain
on mount, holds it in component state for the reveal toggle and
edit flow, and persists changes via a 500 ms debounced
`setProviderSecret` (also flushed on blur). The "Configured" pill
reflects the keychain state instead of the settings field.
- Warning banner about plaintext storage replaced with a green
`ShieldCheck` reassurance pointing at the OS-native secret store.
- `AiChatComponent` cloud branch fetches the active provider's key
via `getProviderSecret(activeProvider)` per send rather than
reading `aiSettings.providerKeys`. Keys revoked or rotated mid-
session take effect on the next message instead of the next
reload.
- `keyForProvider` and the `ProviderKeys` import are removed from
`client.ts`; nothing on the renderer side touches the legacy field
for reads anymore.
Migration
- One-shot lazy migration in `SettingsContext`: any non-empty value
still living in `aiSettings.providerKeys` (the pre-keychain field)
gets moved to the keychain on the first load that detects it, then
the settings field is cleared so the next persist doesn't write
the secret back to disk. Idempotent — `providerKeys` empty short-
circuits on every subsequent boot. Failure on any single provider
bails out without clearing, so a transient keychain error doesn't
destroy the user's keys.
Out of scope (deliberate)
- Settings schema bump — `providerKeys` field stays in the type for
back-compat. New writes leave it empty; downgrading to a pre-
keychain build sees an empty field and prompts the user to re-enter
their keys (which still exist in the keychain, just unreachable
from the older code).
- macOS Touch ID prompt suppression. First read after install may
prompt; subsequent reads in the same session use the unlocked
keychain.
Verification
- pnpm tsc --noEmit / lint --max-warnings 0 clean.
- pnpm vitest run: 125 / 125 passing.
- pnpm build (Next 16 export) clean.
- cargo build / clippy --lib -- -D warnings / fmt --check clean.
- cargo test --lib: 112 / 112 passing.
- Manual: set keys via Settings → Provider Keys, restart app — keys
persist; check `settings.json` — `providerKeys` field is empty;
send chat — cloud branch reads from keychain transparently.
* feat: cloud AI tool calling and agent mode for OpenAI / Anthropic / Gemini / OpenRouter (#295)
PR #284 shipped multi-provider cloud chat as text-only. PR #293 added
streaming. This wires tool-calling and agent mode for the four cloud
providers so the workspace can drive them the same way it drives
Ollama: list_directory / read_file / write_file / execute_command /
get_workspace_structure / web_search / remember.
Renderer canonical history
- New `CanonicalHistoryEntry` shape lives in `providers/cloudTools.ts`
alongside `ToolDefinition` and `UnifiedToolCall`. The renderer
maintains a single canonical timeline (system / user / assistant /
assistant_with_tools / tool_result); per-provider translation
happens at the request boundary.
- `translateHistoryForProvider` emits the OpenAI message ladder,
Anthropic's split system + tool_use / tool_result content blocks,
or Gemini's contents + functionCall / functionResponse parts.
- `translateToolsForProvider` flattens the OpenAI envelope into
Anthropic's `{ name, description, input_schema }` and Gemini's
`[{ functionDeclarations: [...] }]`. OpenAI / OpenRouter pass
through unchanged.
- `extractToolCallsFromBody` parses the response back into a unified
`UnifiedToolCall` list (OpenAI's `tool_calls`, Anthropic's
`tool_use` blocks, Gemini's `functionCall` parts), JSON-encoding
arguments so the renderer can keep one canonical shape end-to-end.
Per-provider request
- New `cloudAgentChat(req)` in `providers/client.ts` — single-shot
per turn. Calls `cloud_proxy` (already on the host allow-list with
per-host method + path + header policy from PR #286), translates
history + tools per provider, and returns
`{ ok, text, toolCalls, error? }`.
- Streaming with tool-call deltas is intentionally out of scope —
each provider streams partial-arguments differently and getting
them right needs four bespoke parsers. One-shot per turn is the
predictable baseline; streaming can layer on later without
changing the renderer's loop shape.
UI wiring
- `AiChatComponent` cloud branch now has a sub-path for
`chatMode === 'agent' && rootPath`. Builds canonical history from
the chat session (preserving assistant_with_tools and tool_result
entries across turns), runs the same agent loop as the Ollama
path — repeat-failure detection, manual approval via
`requestToolApproval`, `aiSettings.alwaysAllowTools`,
`MAX_ITERATIONS = aiSettings.deepMode ? 15 : 5`, planner-mode
gating left intact for the Ollama path. Tool execution reuses the
existing `executeToolInternal` so file / shell / search /
workspace probes behave identically across providers.
- The non-agent cloud path keeps streaming text via
`streamCloudChat` from PR #293.
Tests
- 17 unit tests for `cloudTools.ts` covering each provider's tool
translation, response extraction, and history translation
(including the Anthropic tool_result grouping rule and the Gemini
`functionResponse.response` object-wrapping fallback).
Verification
- pnpm tsc --noEmit / lint --max-warnings 0 clean.
- pnpm vitest run: 137 / 137 (was 125 — added 17 cloudTools tests).
- pnpm build (Next 16 export) clean.
- cargo build / clippy / fmt --check clean. cargo test --lib: 112 / 112.
* feat: detach bottom panel into a floating window (#296)
PR #282 added detach for right-panel views. Bottom panel was the next
deferred item. Same drag-from-header / explicit-button affordance, same
re-dock flow through `floatingWindowRegistry`, but the panel itself
isn't a registered `WebviewView` — it's a hardcoded shell component —
so we wire a reserved viewId and render it directly inside the
floating page.
Registry
- `DetachablePanel` now accepts `"bottom"` alongside `"right"` / `"left"`.
- New `BOTTOM_PANEL_VIEW_ID` constant (`trixty.builtin.bottom-panel`)
exported from the registry. The shell consumes it to gate
inline vs placeholder rendering, the floating page consumes it to
bypass the regular view-registry lookup, and BottomPanel itself
consumes it as its own viewId for `useDetachableHeader`.
Bottom panel
- Header is now a drag handle wired through `useDetachableHeader` —
same threshold + cursor-outside-slot semantics as the right-panel
views. An explicit `ExternalLink` button next to the close X gives
a click affordance for users who don't discover the drag.
- New `isFloating` prop (default `false`). When true, the close X
fires a `floating-window:redock-request` event instead of toggling
the main shell's bottom strip — same handler the registry already
wires for right-panel re-dock — and the pop-out trigger is hidden.
- The dead `eslint-disable react-hooks/set-state-in-effect` block
around the `terminalPath` effect goes away — the rule no longer
fires there post-PR #285's surrounding refactor.
Floating page
- `viewId === BOTTOM_PANEL_VIEW_ID` short-circuits the regular
`useRegisteredView` path and renders ``
inside the same `FloatingTitleBar` shell the other views use.
Title resolves from `panel.bottom.terminal_tabs`; icon stays
consistent with the inline header.
Main shell
- Subscribes to `floatingWindowRegistry` via `useSyncExternalStore`
to detect when the bottom panel is detached. The
`` slot stays mounted (so the layout
preset / resize history doesn't change shape mid-detach) and we
swap its body to a `BottomPanelDetachedPlaceholder`. The
placeholder mirrors the right-panel one: "in floating window"
copy plus "Bring to front" / "Dock back" buttons that drive the
registry directly.
Limitations (deliberate, follow-up)
- Terminal sessions don't survive the detach hop. The Terminal
component unmounts in the main window when the panel detaches and
re-mounts in the floating window with fresh PTY ids; the orphaned
PTYs get reaped by the existing `aliveRef` guard in `Terminal.tsx`.
Cross-window state sync (#5 in the deferred queue) lifts the
terminal-tabs state above the panel so detach / redock preserves
the open shells.
Verification
- pnpm tsc --noEmit / lint --max-warnings 0 clean.
- pnpm vitest run: 137 / 137 passing.
- pnpm build (Next 16 export): /, /_not-found, /floating prerender clean.
- Manual on Windows 11: clicked pop-out on the bottom panel, the
floating window opens with the same UI; closed via X → main shell
re-shows the inline panel; restarted the app — the floating window
spawns at its prior bounds (registry's existing persistence path
works for the bottom panel too).
* feat: cross-window chat history sync between main and floating windows (#297)
Each Tauri WebviewWindow runs its own JS realm, so detaching the AI
chat panel into a floating window today gave you two independent
\`ChatContext\` instances — sending a message in the float left the
main shell stuck on the prior conversation, and switching sessions
in either window had no effect on the other. This wires a generic
event-bus so state slices can be mirrored across windows without
reaching for shared memory.
Sync layer
- New \`src/api/crossWindowSync.ts\` exposes a tiny pub-sub layer over
Tauri events. \`WINDOW_SESSION_ID\` is minted once per JS realm so
receivers can drop their own loopbacks. \`broadcastState(key, data)\`
emits a tagged payload; \`subscribeToBroadcasts(key, handler)\`
resolves an \`unlisten\` for useEffect cleanup. Outside Tauri (next
dev / vitest) both calls are noops so callers don't have to gate
on \`isTauri()\`.
Chat sync
- \`ChatContext\` now subscribes to \`trixty:state-sync:chat\` on mount
and replaces local sessions wholesale on incoming broadcasts. The
same persistence effect that writes to \`trixty-chats\` now also
emits a broadcast on every debounced flush — so the streaming-delta
bursts coalesce to one IPC round-trip per 300 ms instead of firing
once per token.
- \`remoteApplyRef\` short-circuits the broadcast emit when the
current state came from a remote sync, so we don't echo a sibling
window's update back to it. The persist still fires either way so
a fresh restart picks up the latest state regardless of which
window painted it last.
Other slices (deferred)
- The \`crossWindowSync\` utility is intentionally generic. Settings,
workspace selection, and terminal-tabs state can adopt the same
pattern in follow-ups; for now only \`ChatContext\` opts in because
it's the most user-visible inconsistency when the AI panel is
popped out.
Verification
- pnpm tsc --noEmit / lint --max-warnings 0 clean.
- pnpm vitest run: 137 / 137.
- pnpm build: /, /_not-found, /floating prerender clean.
- Manual on Windows 11: detached the AI chat panel, sent a message in
the float — the main shell's chat list updated within one debounce
tick. Switched sessions in main → float caught up on next tick.
Re-docked → no duplicate or stale messages remain.
* feat: JSON Crack-style graph view for .json files (#298)
PR #285 shipped Tree and Form visual surfaces for JSON / package.json
files. This adds a graph view powered by react-flow (now @xyflow/react),
JSON Crack-style: each object / array becomes a parent node, each
primitive becomes a leaf row, edges connect parent to child, and the
whole thing pans / zooms / has a minimap out of the box.
Visual surface
- New `JsonGraphEditor.tsx` walks the parsed JSON recursively to build
react-flow nodes + edges. Primitive nodes render `key` + the value
with type-specific colour (string = green, number = blue, bool =
yellow, null = grey). Container nodes render `key` + a `[ N items ]`
/ `{ N keys }` summary. The layout is a simple left-to-right tree
with each subtree's children stacked vertically and centered around
the parent.
- 512 KB safety cap mirrors the JsonTreeEditor — files past the cap
show a notice and stay on the source view rather than blocking the
UI thread.
- Read-only by design. The Tree surface (PR #285) stays the right
place for mutation; the graph is for structural insight on big
documents where the tree's vertical scroll loses you the shape.
Surface registry
- `getVisualEditor` now returns an array of registry entries so a
single file kind can offer multiple visual tabs. Generic `.json`
registers Tree + Graph; `package.json` registers Form + Graph;
`.env` keeps Table only. Each entry has a stable `id` so the
per-path mode-memory survives switching files of the same kind.
- `FileViewSurface` renders one sub-tab per visual entry alongside
the existing Source tab. Default mode stays "source" so behaviour
is unchanged for users who don't switch.
Dependencies
- `@xyflow/react ^12.10.2` added to `apps/desktop/package.json`.
Same package family that powers JSON Crack, n8n, Reactflow.dev's
own demos. About 75 KB minified gzipped — only paid once when the
user opens the graph tab thanks to the existing `React.lazy`
registration in `getVisualEditor`.
Verification
- pnpm tsc --noEmit / lint --max-warnings 0 clean.
- pnpm vitest run: 137 / 137.
- pnpm build: /, /_not-found, /floating prerender clean — the lazy
import keeps `@xyflow/react` off the boot graph for users who never
open a JSON file.
* feat: drag-to-reorder rows in .env and package.json visual editors (#299)
Issue #264 shipped tabular surfaces for .env and package.json. The
order of rows is meaningful in both — env vars cascade through dotenv
loaders in file order, and package.json scripts / deps are usually
ordered to make the file readable. Until now the only way to reorder
was to swap the source view, edit text, swap back. This wires native
HTML5 drag-and-drop on the row level so the order can be edited
visually.
Hook
- New `src/hooks/useDragReorder.ts` exposes a generic
`useDragReorder({ items, getId, onReorder, groupKey? })` that
returns a `getRowProps` factory each row spreads onto its outer
element. The hook tracks the in-flight drag id and the drop
indicator position internally; consumers only style the
`data-dragging` / `data-drag-target="top|bottom"` markers.
- Drop semantics: drop above the row's vertical midpoint inserts
before, below inserts after — same convention macOS / VSCode
lists use.
- Optional `groupKey` constrains drop targets to peers — useful if
a future surface mixes sections in one rendering pass.
EnvEditor
- Variable rows render with a `GripVertical` handle and accept
drag drops. When the user drops, the hook fires `onReorder`
with the new variable-row order; the editor rebuilds the full
rows array preserving comment-only rows in their original
array slots so reordering doesn't migrate comments to the tail
of the file.
PackageJsonEditor
- Each `DepSection` (`dependencies` / `devDependencies` /
`peerDependencies`) is now reorderable. The new
`reorderMapKeys` helper rebuilds the map preserving the
caller-provided key order. JS keeps insertion order for string
keys, so `JSON.stringify` writes the new order to disk
exactly. Defensive fallback re-appends any keys missing from
the visible row order so a stale input from a remount can't
drop entries.
Verification
- pnpm tsc --noEmit / lint --max-warnings 0 clean.
- pnpm vitest run: 137 / 137 passing.
- pnpm build: /, /_not-found, /floating prerender clean.
- Manual on Windows 11: opened `apps/desktop/.env`, dragged a row
past comment-only lines — comments stayed in place; opened
`apps/desktop/package.json`, reordered devDependencies — the
source view reflects the new order on next save tick.
* feat: open another workspace in a new window via Ctrl+Shift+N (#300)
Two repos side-by-side without context-switching. Each new window is
a fresh TrixtyIDE process with its own Rust state, terminals, AI
sessions, and settings store — so we don't have to invent a way to
share state between window instances. The existing `--path` CLI flag
that the `tide` launcher already uses is the contract.
Rust
- New `spawn_workspace_instance(path)` Tauri command resolves
`current_exe()`, canonicalises the user-supplied folder, and
spawns a detached ` --path ` process. Validates
that `path` is an absolute, existing directory before exec-ing
so a crafted argument can't trick the launcher into running a
sibling binary.
- On Windows, `CREATE_NO_WINDOW` keeps the spawn from flashing a
console — the new TrixtyIDE process is GUI-only, same as the
current one.
UI
- New `Ctrl+Shift+N` shortcut in the main shell. Opens the same
folder picker `Ctrl+O` uses (`@tauri-apps/plugin-dialog`), then
invokes `spawn_workspace_instance`. Failures are logged at warn
but don't bubble — the current window stays usable if the spawn
is denied or times out.
Verification
- pnpm tsc --noEmit / lint --max-warnings 0 clean.
- pnpm vitest run: 137 / 137.
- pnpm build: /, /_not-found, /floating prerender clean.
- cargo build / clippy --lib -- -D warnings / fmt --check / test --lib
(112) clean.
- Manual on Windows 11: Ctrl+Shift+N opens the picker; selecting a
folder spawns a second TrixtyIDE process pointed at that folder.
Two windows side-by-side run independently — terminals, AI chats,
and modified files in one don't leak into the other.
* Sentry telemetry (#292)
* feat: add Sentry error tracking
* feat: integrate Sentry error tracking and add marketplace registry and settings UI enhancements
* improvement: add sentry
* fix: fix lib conflict
* Potential fix for pull request finding 'Useless conditional'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* ci: add GitHub Actions workflows for automated test builds and multi-platform releases
* ci: add GitHub Actions workflow to automate Tauri build testing on Windows
---------
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: matiaspalmac
* fix(desktop): avoid Tauri state TypeId collision between Ollama and Cloud streams (#301)
`OllamaStreams` and `CloudStreams` were both type aliases of
`Arc>>>`. Tauri keys managed
state by `TypeId`, and aliases share the same `TypeId` as their
underlying type, so the second `.manage()` call panicked at startup
with "state for type ... is already being managed".
Convert `CloudStreams` to a newtype struct so it has a distinct
`TypeId`, and update its inner `.lock()` call sites accordingly.
* fix(desktop): use crypto.getRandomValues fallback for WINDOW_SESSION_ID (#302)
The runtime fallback used Math.random, which CodeQL flags as
js/insecure-randomness (CWE-338). The id is currently used only to
suppress cross-window event echos, but the fallback now relies on
WebCrypto so any future security-sensitive use of WINDOW_SESSION_ID
stays safe.
Order of preference:
1. crypto.randomUUID — modern Tauri webview, Node 19+, browsers
2. crypto.getRandomValues — older jsdom / runtimes without randomUUID
3. Date.now + performance.now — last-resort, only echo-suppression
* feat: implement core UI components, localization hooks, and agent configuration settings for the Trixty IDE desktop application.
* Improvement: Add Discord RPC
* add: Included colavorative features using discord (beta)
* feat: implement agent architecture with context providers, workspace synchronization, and Discord RPC integration
* feat: implement StatusBar and TitleBar components with integrated collaboration state and layout controls
* feat: implement CollaborationContext for Yjs-based real-time sessions with Discord RPC integration
* feat: implement CollaborationContext for Yjs-based real-time syncing and Discord RPC join requests
* feat: implement Status Bar and Tab Bar components with collaboration status, file metadata, and keyboard-navigable tab management
* feat: add real-time collaboration support via Yjs and WebRTC with status bar integration
* chore: increment version
* fix: fixed wrong version
* feat: implement backend support for workspace synchronization, Discord RPC, and dynamic update channels
* fixed lint errors in quality check
---------
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Matias Palma <83047050+matiaspalmac@users.noreply.github.com>
Co-authored-by: matiaspalmac
* feat: implement cross-platform Discord Rich Presence IPC integration
---------
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Matias Palma <83047050+matiaspalmac@users.noreply.github.com>
Co-authored-by: matiaspalmac
---
apps/desktop/src-tauri/src/discord_rpc.rs | 4 ++++
apps/desktop/src-tauri/src/lib.rs | 4 ++++
2 files changed, 8 insertions(+)
diff --git a/apps/desktop/src-tauri/src/discord_rpc.rs b/apps/desktop/src-tauri/src/discord_rpc.rs
index e88a6c9b..66be6e57 100644
--- a/apps/desktop/src-tauri/src/discord_rpc.rs
+++ b/apps/desktop/src-tauri/src/discord_rpc.rs
@@ -3,6 +3,10 @@ use serde_json::{json, Value};
use log::{error, info, warn};
use tauri::{AppHandle, Emitter};
+#[cfg(unix)]
+use std::env;
+#[cfg(unix)]
+use std::path::PathBuf;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[cfg(unix)]
use tokio::net::UnixStream;
diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs
index 36e45ea5..60888565 100644
--- a/apps/desktop/src-tauri/src/lib.rs
+++ b/apps/desktop/src-tauri/src/lib.rs
@@ -41,7 +41,11 @@ use tracing_subscriber::prelude::*;
/// supervisor lazily the first time you spawn.
#[inline]
fn silent_command(program: &str) -> Command {
+ #[cfg(target_os = "windows")]
let mut cmd = Command::new(program);
+ #[cfg(not(target_os = "windows"))]
+ let cmd = Command::new(program);
+
#[cfg(target_os = "windows")]
{
// `creation_flags` and `raw_arg` are inherent methods on
From 546821ca6022323989df477dee5c8552b09f98d8 Mon Sep 17 00:00:00 2001
From: Jorge Dev <251231531+jmaxdev@users.noreply.github.com>
Date: Fri, 1 May 2026 23:24:52 -0300
Subject: [PATCH 2/4] feat: integrate a internal browser for localhost (#310)
* feat: integrate a internal browser for localhost
* Potential fix for pull request finding 'CodeQL / DOM text reinterpreted as HTML'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
* Potential fix for pull request finding 'CodeQL / DOM text reinterpreted as HTML'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
* feat: initialize Tauri desktop application configuration and dependency manifest
* feat: implement BrowserView component with configurable port connectivity and status monitoring
---------
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
---
apps/desktop/src-tauri/Cargo.lock | 59 ++++-
apps/desktop/src-tauri/Cargo.toml | 2 +-
.../src-tauri/capabilities/default.json | 10 +
apps/desktop/src-tauri/src/discord_rpc.rs | 2 +-
apps/desktop/src-tauri/src/lib.rs | 35 ++-
apps/desktop/src-tauri/tauri.conf.json | 2 +-
apps/desktop/src/api/builtin.l10n.ts | 20 ++
apps/desktop/src/components/ActivityBar.tsx | 19 +-
apps/desktop/src/components/BrowserView.tsx | 220 ++++++++++++++++++
apps/desktop/src/components/EditorArea.tsx | 7 +
10 files changed, 366 insertions(+), 10 deletions(-)
create mode 100644 apps/desktop/src/components/BrowserView.tsx
diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock
index 8a2010ae..34cbb131 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.5"
dependencies = [
"dirs 5.0.1",
"grep-regex",
@@ -397,6 +397,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
[[package]]
name = "bytes"
version = "1.11.1"
@@ -2037,7 +2043,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
dependencies = [
"byteorder",
- "png",
+ "png 0.17.16",
]
[[package]]
@@ -2170,6 +2176,19 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "image"
+version = "0.25.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
+dependencies = [
+ "bytemuck",
+ "byteorder-lite",
+ "moxcms",
+ "num-traits",
+ "png 0.18.1",
+]
+
[[package]]
name = "indexmap"
version = "1.9.3"
@@ -2663,6 +2682,16 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "moxcms"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
+dependencies = [
+ "num-traits",
+ "pxfm",
+]
+
[[package]]
name = "muda"
version = "0.17.2"
@@ -2678,7 +2707,7 @@ dependencies = [
"objc2-core-foundation",
"objc2-foundation",
"once_cell",
- "png",
+ "png 0.17.16",
"serde",
"thiserror 2.0.18",
"windows-sys 0.60.2",
@@ -3486,6 +3515,19 @@ dependencies = [
"miniz_oxide",
]
+[[package]]
+name = "png"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
+dependencies = [
+ "bitflags 2.11.1",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
[[package]]
name = "portable-pty"
version = "0.9.0"
@@ -3651,6 +3693,12 @@ dependencies = [
"psl-types",
]
+[[package]]
+name = "pxfm"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
+
[[package]]
name = "quick-xml"
version = "0.38.4"
@@ -5163,6 +5211,7 @@ dependencies = [
"gtk",
"heck 0.5.0",
"http",
+ "image",
"jni",
"libc",
"log",
@@ -5230,7 +5279,7 @@ dependencies = [
"ico",
"json-patch",
"plist",
- "png",
+ "png 0.17.16",
"proc-macro2",
"quote",
"semver",
@@ -6008,7 +6057,7 @@ dependencies = [
"objc2-core-graphics",
"objc2-foundation",
"once_cell",
- "png",
+ "png 0.17.16",
"serde",
"thiserror 2.0.18",
"windows-sys 0.60.2",
diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml
index 13bde4fd..8a50d07f 100644
--- a/apps/desktop/src-tauri/Cargo.toml
+++ b/apps/desktop/src-tauri/Cargo.toml
@@ -37,7 +37,7 @@ tauri-build = { version = "2.5.6", features = [] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
-tauri = { version = "2.10.3", features = [] }
+tauri = { version = "2.10.3", features = [ "image-ico", "image-png", "tray-icon"] }
tauri-plugin-log = "2"
tauri-plugin-shell = "2.0.0"
tauri-plugin-store = "2.0.0"
diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json
index bb43535a..4afa90ee 100644
--- a/apps/desktop/src-tauri/capabilities/default.json
+++ b/apps/desktop/src-tauri/capabilities/default.json
@@ -23,6 +23,16 @@
"core:window:allow-start-resize-dragging",
"core:window:allow-show",
"core:window:allow-hide",
+ "core:window:allow-create",
+ "core:window:allow-set-size",
+ "core:window:allow-set-position",
+ "core:webview:default",
+ "core:webview:allow-create-webview",
+ "core:webview:allow-webview-close",
+ "core:webview:allow-set-webview-position",
+ "core:webview:allow-set-webview-size",
+ "core:webview:allow-webview-show",
+ "core:webview:allow-webview-hide",
"positioner:default",
"window-state:default",
diff --git a/apps/desktop/src-tauri/src/discord_rpc.rs b/apps/desktop/src-tauri/src/discord_rpc.rs
index 66be6e57..73e3c820 100644
--- a/apps/desktop/src-tauri/src/discord_rpc.rs
+++ b/apps/desktop/src-tauri/src/discord_rpc.rs
@@ -2,11 +2,11 @@ use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use log::{error, info, warn};
-use tauri::{AppHandle, Emitter};
#[cfg(unix)]
use std::env;
#[cfg(unix)]
use std::path::PathBuf;
+use tauri::{AppHandle, Emitter};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[cfg(unix)]
use tokio::net::UnixStream;
diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs
index 60888565..fbc08608 100644
--- a/apps/desktop/src-tauri/src/lib.rs
+++ b/apps/desktop/src-tauri/src/lib.rs
@@ -21,8 +21,10 @@ use scraper::{Html, Selector};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
+use std::net::TcpStream;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
+use std::time::Duration;
use sysinfo::System;
use tauri::Manager;
use tauri_plugin_store::StoreExt;
@@ -631,6 +633,15 @@ async fn get_system_health(
})
}
+#[tauri::command]
+async fn check_port(port: u16) -> bool {
+ TcpStream::connect_timeout(
+ &format!("127.0.0.1:{}", port).parse().unwrap(),
+ Duration::from_millis(150),
+ )
+ .is_ok()
+}
+
#[tauri::command]
async fn git_init(path: String) -> Result {
let output = silent_command("git")
@@ -2689,6 +2700,23 @@ fn delete_path(path: String, workspace: tauri::State<'_, WorkspaceState>) -> Res
}
}
+#[tauri::command]
+async fn open_browser_window(app: tauri::AppHandle, url: String) -> Result<(), String> {
+ // Generate a unique label so we can open multiple browser windows
+ let label = format!("browser-{}", uuid::Uuid::new_v4());
+ let _ = tauri::WebviewWindowBuilder::new(
+ &app,
+ &label,
+ tauri::WebviewUrl::External(url.parse::().map_err(|e| e.to_string())?),
+ )
+ .title("Trixty Browser")
+ .inner_size(1200.0, 800.0)
+ .resizable(true)
+ .build()
+ .map_err(|e| e.to_string())?;
+ Ok(())
+}
+
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
// Initialize Sentry
@@ -2779,6 +2807,7 @@ pub fn run() {
.manage::(new_initial_join_secret(initial_join_secret))
.manage(DiscordState(Arc::new(tokio::sync::Mutex::new(discord_rpc::DiscordRpc::new()))))
.invoke_handler(tauri::generate_handler![
+ check_port,
read_directory,
read_file,
write_file,
@@ -2852,7 +2881,8 @@ pub fn run() {
set_discord_activity,
accept_discord_join_request,
reject_discord_join_request,
- get_initial_join_secret
+ get_initial_join_secret,
+ open_browser_window
])
.setup(|app| {
// Main window is required — failing fast with a structured error beats
@@ -2996,6 +3026,9 @@ pub fn run() {
let _ = main_window.set_focus();
});
+
+
+
// Start Discord RPC background task
let discord_state = app.handle().state::();
let mut discord_rpc = discord_state.0.blocking_lock();
diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json
index 8d2aa739..52902ec4 100644
--- a/apps/desktop/src-tauri/tauri.conf.json
+++ b/apps/desktop/src-tauri/tauri.conf.json
@@ -43,7 +43,7 @@
],
"withGlobalTauri": false,
"security": {
- "csp": "default-src 'self'; img-src 'self' data: asset: https://asset.localhost https://raw.githubusercontent.com; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ipc: http://ipc.localhost https://ipc.localhost; font-src 'self' data:;"
+ "csp": "default-src 'self'; img-src 'self' data: asset: https://asset.localhost https://raw.githubusercontent.com; script-src 'self' blob:; style-src 'self' 'unsafe-inline'; connect-src 'self' ipc: http://ipc.localhost https://ipc.localhost; font-src 'self' data:; frame-src *; worker-src 'self' blob:;"
}
},
"plugins": {
diff --git a/apps/desktop/src/api/builtin.l10n.ts b/apps/desktop/src/api/builtin.l10n.ts
index 012f5d22..171d0461 100644
--- a/apps/desktop/src/api/builtin.l10n.ts
+++ b/apps/desktop/src/api/builtin.l10n.ts
@@ -94,6 +94,16 @@ export function registerBuiltinTranslations() {
'marketplace.view_install': 'View & Install',
'marketplace.installed_badge': 'Installed',
+ 'browser.title': 'Browser',
+ 'browser.back': 'Back',
+ 'browser.forward': 'Forward',
+ 'browser.reload': 'Reload',
+ 'browser.address_placeholder': 'Enter URL...',
+ 'browser.open_external': 'Open in External Browser',
+ 'browser.open_native': 'Open in Native Window',
+ 'browser.native_mode_active': 'Native Engine Active (No Restrictions)',
+ 'browser.security_note': 'Some sites may block embedding for security.',
+
'git.no_repo': 'No Repository',
'git.no_repo_desc': 'Initialize a Git repository to begin',
'git.init_button': 'git init',
@@ -613,6 +623,16 @@ export function registerBuiltinTranslations() {
'marketplace.view_install': 'Ver e Instalar',
'marketplace.installed_badge': 'Instalada',
+ 'browser.title': 'Navegador',
+ 'browser.back': 'Atrás',
+ 'browser.forward': 'Adelante',
+ 'browser.reload': 'Recargar',
+ 'browser.address_placeholder': 'Ingrese URL...',
+ 'browser.open_external': 'Abrir en navegador externo',
+ 'browser.open_native': 'Abrir en ventana nativa',
+ 'browser.native_mode_active': 'Motor nativo activo (sin restricciones)',
+ 'browser.security_note': 'Algunos sitios pueden bloquear su inserción por seguridad.',
+
'git.no_repo': 'Sin Repositorio',
'git.no_repo_desc': 'Inicializa un repositorio Git para empezar',
'git.init_button': 'git init',
diff --git a/apps/desktop/src/components/ActivityBar.tsx b/apps/desktop/src/components/ActivityBar.tsx
index a9aa6470..36ad21c0 100644
--- a/apps/desktop/src/components/ActivityBar.tsx
+++ b/apps/desktop/src/components/ActivityBar.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
-import { Settings, Package } from "lucide-react";
+import { Settings, Package, Globe } from "lucide-react";
import { useUI } from "@/context/UIContext";
import { useFiles } from "@/context/FilesContext";
import { trixty, WebviewView } from "@/api/trixty";
@@ -39,6 +39,11 @@ const ActivityBar: React.FC = () => {
return;
}
+ if (id === "browser") {
+ openFile("virtual://browser", t('browser.title'), "", "virtual");
+ return;
+ }
+
if (id === "settings") {
setSettingsOpen(true);
return;
@@ -87,6 +92,18 @@ const ActivityBar: React.FC = () => {
);
})}