Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c779d52
feat: add Sentry error tracking (#287)
jmaxdev Apr 29, 2026
1bb0749
Revert "feat: add Sentry error tracking (#287)" (#291)
jmaxdev Apr 29, 2026
c3cb5a8
feat: cloud AI streaming for OpenAI / Anthropic / Gemini / OpenRouter…
matiaspalmac Apr 29, 2026
e6e3b36
feat: store cloud AI provider keys in OS keychain (#294)
matiaspalmac Apr 29, 2026
9207f5e
feat: cloud AI tool calling and agent mode for OpenAI / Anthropic / G…
matiaspalmac Apr 29, 2026
f7d6531
feat: detach bottom panel into a floating window (#296)
matiaspalmac Apr 29, 2026
97c4c43
feat: cross-window chat history sync between main and floating window…
matiaspalmac Apr 29, 2026
86a6067
feat: JSON Crack-style graph view for .json files (#298)
matiaspalmac Apr 29, 2026
285a668
feat: drag-to-reorder rows in .env and package.json visual editors (#…
matiaspalmac Apr 29, 2026
61b47a9
feat: open another workspace in a new window via Ctrl+Shift+N (#300)
matiaspalmac Apr 29, 2026
3c3f936
Sentry telemetry (#292)
jmaxdev Apr 29, 2026
bf9f325
fix(desktop): avoid Tauri state TypeId collision between Ollama and C…
matiaspalmac Apr 29, 2026
c75ec25
fix(desktop): use crypto.getRandomValues fallback for WINDOW_SESSION_…
matiaspalmac Apr 30, 2026
feb4c00
feat: implement core UI components, localization hooks, and agent con…
jmaxdev May 1, 2026
8b9d495
Improvement: Add Discord RPC
jmaxdev May 1, 2026
7be4bd4
add: Included colavorative features using discord (beta)
jmaxdev May 1, 2026
896b1e8
feat: implement agent architecture with context providers, workspace …
jmaxdev May 1, 2026
94d0ea1
feat: implement StatusBar and TitleBar components with integrated col…
jmaxdev May 1, 2026
6298be1
feat: implement CollaborationContext for Yjs-based real-time sessions…
jmaxdev May 1, 2026
3e71a46
feat: implement CollaborationContext for Yjs-based real-time syncing …
jmaxdev May 1, 2026
105c40d
feat: implement Status Bar and Tab Bar components with collaboration …
jmaxdev May 1, 2026
da7d3d9
feat: add real-time collaboration support via Yjs and WebRTC with sta…
jmaxdev May 1, 2026
70b97eb
chore: increment version
jmaxdev May 1, 2026
9f45ed2
fix: fixed wrong version
jmaxdev May 1, 2026
1714577
Merge branch 'main' into dev
jmaxdev May 1, 2026
14624f9
feat: implement backend support for workspace synchronization, Discor…
jmaxdev May 1, 2026
1fa3a6a
fixed lint errors in quality check
jmaxdev May 1, 2026
15bd194
Merge branch 'main' into dev
jmaxdev May 1, 2026
59af568
test: add unit tests for awareness block generation and project stack…
jmaxdev May 1, 2026
63b18bd
feat: implement Discord Rich Presence IPC service for cross-platform …
jmaxdev May 1, 2026
b5cb5bc
feat: implement CLI argument support and Discord RPC integration in d…
jmaxdev May 1, 2026
8418a05
feat: implement CLI argument parsing for workspace paths and Discord …
jmaxdev May 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions apps/desktop/src-tauri/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ pub fn parse_args(argv: &[String], cwd: Option<PathBuf>) -> CliResult {
seen_double_dash = true;
continue;
}

// Handle Discord RPC join flag
if arg == "--discord-rpc-join-secret" {
if let Some(val) = iter.next() {
Expand Down Expand Up @@ -101,12 +101,12 @@ pub fn parse_args(argv: &[String], cwd: Option<PathBuf>) -> CliResult {
workspace_candidate = Some(value.to_string());
continue;
}

if arg.starts_with("--") {
continue;
}
}

if workspace_candidate.is_none() {
workspace_candidate = Some(arg.clone());
}
Expand Down Expand Up @@ -190,14 +190,14 @@ mod tests {
#[test]
fn no_args_is_empty() {
let out = parse_args(&argv(&[]), None);
assert_eq!(out, CliWorkspace::Empty);
assert_eq!(out.workspace, CliWorkspace::Empty);
}

#[test]
fn positional_existing_directory_is_accepted() {
let dir = TempDir::new().unwrap();
let out = parse_args(&argv(&[dir.path().to_str().unwrap()]), None);
match out {
match out.workspace {
CliWorkspace::Path(p) => assert!(p.is_dir()),
other => panic!("expected Path, got {:?}", other),
}
Expand All @@ -207,7 +207,7 @@ mod tests {
fn dash_path_flag_existing_directory_is_accepted() {
let dir = TempDir::new().unwrap();
let out = parse_args(&argv(&["--path", dir.path().to_str().unwrap()]), None);
match out {
match out.workspace {
CliWorkspace::Path(p) => assert!(p.is_dir()),
other => panic!("expected Path, got {:?}", other),
}
Expand All @@ -218,7 +218,7 @@ mod tests {
let dir = TempDir::new().unwrap();
let arg = format!("--path={}", dir.path().to_str().unwrap());
let out = parse_args(&argv(&[&arg]), None);
match out {
match out.workspace {
CliWorkspace::Path(p) => assert!(p.is_dir()),
other => panic!("expected Path, got {:?}", other),
}
Expand All @@ -229,7 +229,7 @@ mod tests {
let dir = TempDir::new().unwrap();
let canonical_cwd = dir.path().canonicalize().unwrap();
let out = parse_args(&argv(&["."]), Some(canonical_cwd.clone()));
match out {
match out.workspace {
CliWorkspace::Path(p) => assert_eq!(p, canonical_cwd),
other => panic!("expected Path, got {:?}", other),
}
Expand All @@ -242,7 +242,7 @@ mod tests {
fs::create_dir(&child).unwrap();
let cwd = Some(parent.path().canonicalize().unwrap());
let out = parse_args(&argv(&["sub"]), cwd);
match out {
match out.workspace {
CliWorkspace::Path(p) => {
assert!(p.is_dir());
assert!(p.ends_with("sub"));
Expand All @@ -257,7 +257,7 @@ mod tests {
&argv(&["C:/definitely/not/a/real/path/for/this/test"]),
None,
);
assert!(matches!(out, CliWorkspace::Invalid { .. }));
assert!(matches!(out.workspace, CliWorkspace::Invalid { .. }));
}

#[test]
Expand All @@ -266,7 +266,7 @@ mod tests {
let file = dir.path().join("a.txt");
fs::write(&file, "hi").unwrap();
let out = parse_args(&argv(&[file.to_str().unwrap()]), None);
match out {
match out.workspace {
CliWorkspace::Invalid { reason, .. } => {
assert!(reason.contains("not a directory"), "got: {}", reason);
}
Expand All @@ -277,7 +277,7 @@ mod tests {
#[test]
fn dash_path_without_value_is_invalid() {
let out = parse_args(&argv(&["--path"]), None);
match out {
match out.workspace {
CliWorkspace::Invalid { reason, .. } => {
assert!(reason.contains("requires a value"), "got: {}", reason);
}
Expand All @@ -288,7 +288,7 @@ mod tests {
#[test]
fn empty_path_is_invalid() {
let out = parse_args(&argv(&["--path", " "]), None);
match out {
match out.workspace {
CliWorkspace::Invalid { reason, .. } => {
assert!(reason.contains("empty"), "got: {}", reason);
}
Expand All @@ -309,7 +309,7 @@ mod tests {
]),
None,
);
match out {
match out.workspace {
CliWorkspace::Path(p) => assert!(p.is_dir()),
other => panic!("expected Path, got {:?}", other),
}
Expand All @@ -321,6 +321,6 @@ mod tests {
// positional. It will fail validation (no such dir), but we're
// checking that flag parsing stopped at `--`.
let out = parse_args(&argv(&["--", "--path"]), None);
assert!(matches!(out, CliWorkspace::Invalid { .. }));
assert!(matches!(out.workspace, CliWorkspace::Invalid { .. }));
}
}
51 changes: 29 additions & 22 deletions apps/desktop/src-tauri/src/discord_rpc.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

use log::{error, info, warn};
use tauri::{AppHandle, Emitter};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[cfg(unix)]
use tokio::net::UnixStream;
use uuid::Uuid;
use log::{info, warn, error};
use tauri::{AppHandle, Emitter};
use tokio::sync::mpsc;
use uuid::Uuid;

#[cfg(windows)]
use tokio::net::windows::named_pipe::ClientOptions;
Expand All @@ -22,7 +22,7 @@ pub enum OpCode {
}

pub enum RpcMessage {
UpdateActivity(Option<Activity>),
UpdateActivity(Box<Option<Activity>>),
AcceptJoin(String),
RejectJoin(String),
}
Expand All @@ -49,12 +49,16 @@ impl IpcStream {
#[cfg(windows)]
IpcStream::Windows(s) => {
s.write_all(&header).await.map_err(|e| e.to_string())?;
s.write_all(payload.as_bytes()).await.map_err(|e| e.to_string())?;
s.write_all(payload.as_bytes())
.await
.map_err(|e| e.to_string())?;
}
#[cfg(unix)]
IpcStream::Unix(s) => {
s.write_all(&header).await.map_err(|e| e.to_string())?;
s.write_all(payload.as_bytes()).await.map_err(|e| e.to_string())?;
s.write_all(payload.as_bytes())
.await
.map_err(|e| e.to_string())?;
}
}
Ok(())
Expand All @@ -80,11 +84,15 @@ impl IpcStream {
match self {
#[cfg(windows)]
IpcStream::Windows(s) => {
s.read_exact(&mut payload).await.map_err(|e| e.to_string())?;
s.read_exact(&mut payload)
.await
.map_err(|e| e.to_string())?;
}
#[cfg(unix)]
IpcStream::Unix(s) => {
s.read_exact(&mut payload).await.map_err(|e| e.to_string())?;
s.read_exact(&mut payload)
.await
.map_err(|e| e.to_string())?;
}
}

Expand Down Expand Up @@ -114,7 +122,7 @@ impl DiscordRpc {
match Self::try_connect(&client_id).await {
Ok(mut stream) => {
info!("[Discord] Connected and handshaked.");

let _ = Self::do_subscribe(&mut stream, "ACTIVITY_JOIN").await;
let _ = Self::do_subscribe(&mut stream, "ACTIVITY_SPECTATE").await;
let _ = Self::do_subscribe(&mut stream, "ACTIVITY_JOIN_REQUEST").await;
Expand All @@ -125,7 +133,7 @@ impl DiscordRpc {
let payload = match msg {
RpcMessage::UpdateActivity(activity) => json!({
"cmd": "SET_ACTIVITY",
"args": { "pid": std::process::id(), "activity": activity },
"args": { "pid": std::process::id(), "activity": *activity },
"nonce": Uuid::new_v4().to_string()
}).to_string(),
RpcMessage::AcceptJoin(user_id) => json!({
Expand All @@ -139,7 +147,7 @@ impl DiscordRpc {
"nonce": Uuid::new_v4().to_string()
}).to_string(),
};

if let Err(e) = stream.send(OpCode::Frame, &payload).await {
error!("[Discord] Send failed: {}", e);
break;
Expand All @@ -149,13 +157,8 @@ impl DiscordRpc {
match res {
Ok((_opcode, payload)) => {
if let Ok(v) = serde_json::from_str::<Value>(&payload) {
if let Some(evt) = v["evt"].as_str() {
match evt {
"ACTIVITY_JOIN" | "ACTIVITY_SPECTATE" | "ACTIVITY_JOIN_REQUEST" => {
let _ = app_handle.emit("discord-rpc-event", v);
}
_ => {}
}
if let Some("ACTIVITY_JOIN" | "ACTIVITY_SPECTATE" | "ACTIVITY_JOIN_REQUEST") = v["evt"].as_str() {
let _ = app_handle.emit("discord-rpc-event", v);
}
}
}
Expand Down Expand Up @@ -230,14 +233,16 @@ impl DiscordRpc {
"cmd": "SUBSCRIBE",
"evt": evt,
"nonce": Uuid::new_v4().to_string()
}).to_string();
})
.to_string();
stream.send(OpCode::Frame, &payload).await?;
Ok(())
}

pub fn set_activity(&self, activity: Option<Activity>) -> Result<(), String> {
if let Some(tx) = &self.tx {
tx.send(RpcMessage::UpdateActivity(activity)).map_err(|e| e.to_string())?;
tx.send(RpcMessage::UpdateActivity(Box::new(activity)))
.map_err(|e| e.to_string())?;
Ok(())
} else {
Err("RPC not started".to_string())
Expand All @@ -246,7 +251,8 @@ impl DiscordRpc {

pub fn accept_join_request(&self, user_id: String) -> Result<(), String> {
if let Some(tx) = &self.tx {
tx.send(RpcMessage::AcceptJoin(user_id)).map_err(|e| e.to_string())?;
tx.send(RpcMessage::AcceptJoin(user_id))
.map_err(|e| e.to_string())?;
Ok(())
} else {
Err("RPC not started".to_string())
Expand All @@ -255,7 +261,8 @@ impl DiscordRpc {

pub fn reject_join_request(&self, user_id: String) -> Result<(), String> {
if let Some(tx) = &self.tx {
tx.send(RpcMessage::RejectJoin(user_id)).map_err(|e| e.to_string())?;
tx.send(RpcMessage::RejectJoin(user_id))
.map_err(|e| e.to_string())?;
Ok(())
} else {
Err("RPC not started".to_string())
Expand Down
3 changes: 1 addition & 2 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
mod about;
mod cli;
mod discord_rpc;
mod error;
mod fs_atomic;
mod fs_guard;
mod fs_watcher;
mod http;
mod pty;
mod discord_rpc;


use error::redact_user_paths;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ const GitExplorerComponent: React.FC = () => {
});
}
} catch (e) { logger.error(e); } finally { setLoading(false); }
}, [rootPath, systemSettings.filesExclude]);
}, [rootPath, systemSettings.filesExclude, isCollaborating, role, ydoc]);

const handleOpenFolder = async () => {
try {
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/components/EditorArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ const EditorArea: React.FC = () => {
ytext.insert(0, currentFile.content);
}

console.log(`[Collaboration] Binding Monaco to Y.Text for: ${currentFile.path}`);


const binding = new MonacoBinding(
ytext,
Expand All @@ -216,7 +216,7 @@ const EditorArea: React.FC = () => {
binding.destroy();
bindingRef.current = null;
};
}, [isCollaborating, ydoc, provider, currentFile?.path]);
}, [isCollaborating, ydoc, provider, currentFile]);

// Performance: Efficient Layout handling
React.useEffect(() => {
Expand Down Expand Up @@ -268,7 +268,7 @@ const EditorArea: React.FC = () => {
path: currentFile.path
});
}
}, [currentFile?.path, isLargeFile]);
}, [currentFile, isLargeFile]);

const debounceTimer = useRef<NodeJS.Timeout | null>(null);

Expand Down
15 changes: 5 additions & 10 deletions apps/desktop/src/components/StatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import React, { useEffect, useState } from "react";
import { GitBranch, Users } from "lucide-react";
import { useFiles } from "@/context/FilesContext";
import { useUI } from "@/context/UIContext";
import { useWorkspace } from "@/context/WorkspaceContext";
import { useL10n } from "@/hooks/useL10n";
import { isTauri, safeInvoke } from "@/api/tauri";
Expand All @@ -19,11 +18,7 @@ import { useCollaboration } from "@/context/CollaborationContext";
// converted to a proper `<button>` at that point.
const StatusBar: React.FC = () => {
const { currentFile } = useFiles();
const {
setSidebarOpen,
setRightPanelOpen,
setBottomPanelOpen,
} = useUI();

const { rootPath } = useWorkspace();
const { t } = useL10n();
const { isCollaborating, activeUsers, role, provider } = useCollaboration();
Expand All @@ -33,12 +28,12 @@ const StatusBar: React.FC = () => {
// command is best-effort — if the folder is not a git repo we surface
// nothing (the GitBranch chip hides itself).
useEffect(() => {
if (!rootPath || !isTauri()) {
setBranch(null);
return;
}
let cancelled = false;
void (async () => {
if (!rootPath || !isTauri()) {
if (!cancelled) setBranch(null);
return;
}
try {
const result = await safeInvoke(
"get_git_branches",
Expand Down
9 changes: 6 additions & 3 deletions apps/desktop/src/components/Terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { logger } from "@/lib/logger";
import { useCollaboration } from "@/context/CollaborationContext";
import * as Sentry from "@sentry/nextjs";
import "@xterm/xterm/css/xterm.css";
import * as Y from "yjs";

interface TerminalProps {
/**
Expand Down Expand Up @@ -182,9 +183,11 @@ const Terminal: React.FC<TerminalProps> = ({ sessionId, cwd, isActive }) => {
term.write(sharedText.toString());

// Listen for new data
const observer = (event: any) => {
event.changes.delta.forEach((item: any) => {
if (item.insert) term.write(item.insert);
const observer = (event: Y.YTextEvent) => {
event.changes.delta.forEach((item) => {
if (item.insert && typeof item.insert === "string") {
term.write(item.insert);
}
});
};
sharedText.observe(observer);
Expand Down
Loading
Loading