Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,29 @@ sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file libssl-dev
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

**Linux** (Fedora/RPM):
```bash
sudo dnf install -y \
webkit2gtk4.1-devel \
openssl-devel \
gtk3-devel \
libayatana-appindicator-gtk3-devel \
librsvg2-devel \
curl wget file gcc make

# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env

# Enable cron scheduling support
sudo dnf install -y cronie
sudo systemctl enable --now crond
```

> **Fedora note:** If `libayatana-appindicator-gtk3-devel` is not found, try `libappindicator-gtk3-devel`. If `webkit2gtk4.1-devel` is missing, also install `libsoup3-devel`.

> **Wayland:** ATM supports Wayland natively via gnome-terminal or konsole. If you are running a pure Wayland session (no XWayland), ensure one of these is installed.

</details>

**Build for production**:
Expand Down Expand Up @@ -221,7 +244,8 @@ pnpm tauri build
|----------|--------|---------|
| Windows | Full support | PowerShell |
| macOS | Full support | bash (Terminal.app) |
| Linux | Supported | bash |
| Linux (Debian/Ubuntu) | Supported | gnome-terminal, xterm, or any installed emulator |
| Linux (Fedora/RPM) | Supported | konsole, gnome-terminal, alacritty, kitty, xterm |

---

Expand Down
67 changes: 52 additions & 15 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,42 @@ fn delete_scheduled_task(task_name: String) -> Result<String, String> {
}
}

/// Finds an available terminal emulator, preferring Wayland-native ones when running
/// under a pure Wayland session (WAYLAND_DISPLAY set, DISPLAY unset).
#[cfg(target_os = "linux")]
fn find_terminal_emulator() -> Option<&'static str> {
let wayland = std::env::var("WAYLAND_DISPLAY").is_ok();
let x11 = std::env::var("DISPLAY").is_ok();
let pure_wayland = wayland && !x11;

// X11-only terminals that won't work without XWayland
const X11_ONLY: &[&str] = &["xterm", "urxvt"];

// Ordered by preference: Debian compat first, then common DEs, then fallbacks
const CANDIDATES: &[&str] = &[
"x-terminal-emulator", // Debian/Ubuntu alternatives system
"gnome-terminal", // GNOME (native Wayland)
"konsole", // KDE (native Wayland, default on Fedora KDE)
"xfce4-terminal", // XFCE
"alacritty", // Modern, widespread
"kitty", // Modern, widespread
"tilix", // GNOME alternative
"xterm", // X11 fallback
"urxvt", // X11 fallback
];

CANDIDATES.iter().find(|&&term| {
if pure_wayland && X11_ONLY.contains(&term) {
return false;
}
StdCommand::new("which")
.arg(term)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}).copied()
}

/// Opens a visible terminal window running the given script.
#[tauri::command]
fn open_terminal(script_path: String) -> Result<(), String> {
Expand Down Expand Up @@ -287,21 +323,22 @@ fn open_terminal(script_path: String) -> Result<(), String> {

#[cfg(target_os = "linux")]
{
let terminals = [
("x-terminal-emulator", vec!["-e", &script_path]),
("gnome-terminal", vec!["--", &script_path]),
("xterm", vec!["-e", &script_path]),
];
let mut launched = false;
for (term, args) in &terminals {
if StdCommand::new(term).args(args).spawn().is_ok() {
launched = true;
break;
}
}
if !launched {
return Err("No terminal emulator found".into());
}
let term = find_terminal_emulator().ok_or_else(|| {
"No terminal emulator found. Install gnome-terminal, konsole, xterm, or another terminal emulator.".to_string()
})?;

// Terminals that use "--" as argument separator instead of "-e"
const DASH_DASH_TERMS: &[&str] = &["gnome-terminal", "konsole", "kitty"];
let args: Vec<&str> = if DASH_DASH_TERMS.contains(&term) {
vec!["--", &script_path]
} else {
vec!["-e", &script_path]
};

StdCommand::new(term)
.args(&args)
.spawn()
.map_err(|e| format!("Failed to open terminal '{}': {}", term, e))?;
}

Ok(())
Expand Down
72 changes: 72 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,84 @@
--radius-xl: 12px;
}

/* Tinted background tokens — used for node cards and inline panels */
:root {
--tint-blue: rgba(74, 158, 255, 0.06);
--tint-blue-hover: rgba(74, 158, 255, 0.14);
--tint-green: rgba(63, 185, 80, 0.06);
--tint-orange: rgba(240, 136, 62, 0.07);
--tint-purple: rgba(217, 70, 239, 0.06);
--tint-gold: rgba(210, 153, 34, 0.08);
--tint-subagent: rgba(165, 214, 255, 0.07);
--bg-toast: #1e1e3a;
--shadow-panel: rgba(0, 0, 0, 0.4);
--shadow-dropdown: rgba(0, 0, 0, 0.35);
/* White-alpha tokens used in inputs, pills, subtle dividers */
--bg-input: rgba(255, 255, 255, 0.05);
--border-input: rgba(255, 255, 255, 0.10);
--border-input-hover:rgba(255, 255, 255, 0.25);
--border-subtle: rgba(255, 255, 255, 0.06);
--bg-hover-subtle: rgba(255, 255, 255, 0.06);
}

/* Light theme overrides — applied when <html data-theme="light"> */
[data-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: #f6f8fa;
--bg-surface: #ffffff;
--bg-elevated: #eaeef2;
--text-primary: #1f2328;
--text-secondary: #636c76;
--text-tertiary: #9198a1;
--border-color: #d1d9e0;
--border-hover: #8c959f;
/* Tint tokens need more opacity on light backgrounds to remain visible */
--tint-blue: rgba(74, 158, 255, 0.13);
--tint-blue-hover: rgba(74, 158, 255, 0.24);
--tint-green: rgba(63, 185, 80, 0.13);
--tint-orange: rgba(240, 136, 62, 0.13);
--tint-purple: rgba(217, 70, 239, 0.11);
--tint-gold: rgba(210, 153, 34, 0.13);
--tint-subagent: rgba(74, 140, 210, 0.13);
--bg-toast: #eaeef2;
--shadow-panel: rgba(0, 0, 0, 0.12);
--shadow-dropdown: rgba(0, 0, 0, 0.10);
/* White-alpha tokens → solid equivalents on light background */
--bg-input: var(--bg-primary);
--border-input: var(--border-color);
--border-input-hover:var(--border-hover);
--border-subtle: var(--border-color);
--bg-hover-subtle: var(--bg-elevated);
}

* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

/* Native form elements — override OS/browser defaults so they follow the active theme */
select,
input[type="text"],
input[type="password"],
input[type="number"],
input[type="email"],
textarea {
background-color: var(--bg-input);
color: var(--text-primary);
border-color: var(--border-input);
color-scheme: dark;
}

[data-theme="light"] select,
[data-theme="light"] input[type="text"],
[data-theme="light"] input[type="password"],
[data-theme="light"] input[type="number"],
[data-theme="light"] input[type="email"],
[data-theme="light"] textarea {
color-scheme: light;
}

html, body, #root {
height: 100%;
width: 100%;
Expand Down
10 changes: 10 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { useUiStore } from "./store/ui-store";
import { startWatching } from "./services/file-watcher";

function App() {
const theme = useUiStore((s) => s.theme);
const inspectorOpen = useUiStore((s) => s.inspectorOpen);
const createDialogOpen = useUiStore((s) => s.createDialogOpen);
const closeCreateDialog = useUiStore((s) => s.closeCreateDialog);
Expand Down Expand Up @@ -132,7 +133,13 @@ function App() {
return () => document.removeEventListener("keydown", handleKeyDown);
}, []);

// Apply theme to DOM whenever it changes
useEffect(() => {
document.documentElement.setAttribute("data-theme", theme);
}, [theme]);

// Load project on mount — use saved projectPath if available, else home directory
// Also restore saved theme preference.
useEffect(() => {
(async () => {
const home = await homeDir();
Expand All @@ -148,6 +155,9 @@ function App() {
targetPath = parsed.projectPath;
}
}
if (parsed.theme === "light" || parsed.theme === "dark") {
useUiStore.getState().setTheme(parsed.theme);
}
}
} catch {
// Fall back to home directory
Expand Down
6 changes: 4 additions & 2 deletions src/components/common/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,15 @@ function ToastItem({ entry }: { entry: ToastEntry }) {
return (
<div
style={{
background: "#1e1e3a",
background: "var(--bg-toast)",
borderLeft: `4px solid ${borderColors[entry.type]}`,
border: `1px solid var(--border-color)`,
borderLeftWidth: 4,
borderRadius: 6,
padding: "10px 16px",
color: "var(--text-primary)",
fontSize: 13,
boxShadow: "0 2px 12px rgba(0,0,0,0.35)",
boxShadow: "0 2px 12px var(--shadow-dropdown)",
animation: entry.leaving
? "aui-toast-out 0.3s forwards"
: "aui-toast-in 0.25s ease-out",
Expand Down
40 changes: 19 additions & 21 deletions src/components/context-hub/ContextHub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ function SectionHeader({ label, count }: { label: string; count: number }) {
letterSpacing: "0.06em",
color: "var(--text-secondary)",
padding: "12px 0 6px",
borderBottom: "1px solid rgba(255,255,255,0.06)",
borderBottom: "1px solid var(--bg-hover-subtle)",
marginBottom: 8,
}}
>
Expand Down Expand Up @@ -502,7 +502,7 @@ function ImportDropdown({
background: "var(--bg-surface, #1c2333)",
border: "1px solid var(--border-color)",
borderRadius: 8,
boxShadow: "0 8px 24px rgba(0,0,0,0.5)",
boxShadow: "0 8px 24px var(--shadow-panel)",
zIndex: 200,
padding: 6,
}}
Expand All @@ -513,7 +513,7 @@ function ImportDropdown({
style={menuItemStyle}
onClick={handleFileImport}
disabled={importing}
onMouseEnter={(e) => { e.currentTarget.style.background = "rgba(255,255,255,0.06)"; }}
onMouseEnter={(e) => { e.currentTarget.style.background = "var(--bg-hover-subtle)"; }}
onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; }}
>
From File...
Expand All @@ -522,7 +522,7 @@ function ImportDropdown({
style={menuItemStyle}
onClick={() => setMode("url")}
disabled={importing}
onMouseEnter={(e) => { e.currentTarget.style.background = "rgba(255,255,255,0.06)"; }}
onMouseEnter={(e) => { e.currentTarget.style.background = "var(--bg-hover-subtle)"; }}
onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; }}
>
From GitHub URL
Expand All @@ -545,8 +545,8 @@ function ImportDropdown({
boxSizing: "border-box",
padding: "7px 10px",
fontSize: 12,
background: "rgba(255,255,255,0.05)",
border: "1px solid rgba(255,255,255,0.12)",
background: "var(--bg-input)",
border: "1px solid var(--border-subtle)",
borderRadius: 6,
color: "var(--text-primary)",
outline: "none",
Expand Down Expand Up @@ -591,12 +591,12 @@ function UtilityButton({ label, onClick }: { label: string; onClick: () => void
onClick={onClick}
style={{
padding: "4px 12px", fontSize: 11, fontWeight: 600,
background: "rgba(255,255,255,0.04)", border: "1px solid rgba(255,255,255,0.1)",
background: "var(--bg-input)", border: "1px solid var(--border-input)",
color: "var(--text-secondary)", borderRadius: 6, cursor: "pointer",
transition: "all 0.15s",
}}
onMouseEnter={(e) => { e.currentTarget.style.borderColor = "rgba(255,255,255,0.25)"; e.currentTarget.style.color = "var(--text-primary)"; }}
onMouseLeave={(e) => { e.currentTarget.style.borderColor = "rgba(255,255,255,0.1)"; e.currentTarget.style.color = "var(--text-secondary)"; }}
onMouseEnter={(e) => { e.currentTarget.style.borderColor = "var(--border-input-hover)"; e.currentTarget.style.color = "var(--text-primary)"; }}
onMouseLeave={(e) => { e.currentTarget.style.borderColor = "var(--border-input)"; e.currentTarget.style.color = "var(--text-secondary)"; }}
>
{label}
</button>
Expand Down Expand Up @@ -750,7 +750,7 @@ export function ContextHub() {
position: "fixed", top: "var(--toolbar-height)", right: 0, bottom: 0,
width: 480, background: "var(--bg-secondary)",
borderLeft: "1px solid var(--border-color)",
boxShadow: "-4px 0 24px rgba(0,0,0,0.4)",
boxShadow: "-4px 0 24px var(--shadow-panel)",
display: "flex", flexDirection: "column", zIndex: 100,
animation: "slideInRight 0.2s ease",
}}
Expand All @@ -759,10 +759,8 @@ export function ContextHub() {
<div
style={{
flexShrink: 0,
background: "linear-gradient(180deg, rgba(21,27,35,0.95) 0%, rgba(21,27,35,0.85) 100%)",
backdropFilter: "blur(16px)",
WebkitBackdropFilter: "blur(16px)",
borderBottom: "1px solid rgba(255,255,255,0.06)",
background: "var(--bg-secondary)",
borderBottom: "1px solid var(--border-subtle)",
padding: "14px 16px 12px",
}}
>
Expand All @@ -779,7 +777,7 @@ export function ContextHub() {
borderRadius: 8, cursor: "pointer",
transition: "border-color 0.15s",
}}
onMouseEnter={(e) => { e.currentTarget.style.borderColor = "rgba(255,255,255,0.25)"; }}
onMouseEnter={(e) => { e.currentTarget.style.borderColor = "var(--border-input-hover)"; }}
onMouseLeave={(e) => { e.currentTarget.style.borderColor = "var(--border-color)"; }}
>
Import
Expand Down Expand Up @@ -842,14 +840,14 @@ export function ContextHub() {
style={{
width: "100%", boxSizing: "border-box",
padding: "8px 30px 8px 12px",
background: "rgba(255,255,255,0.05)",
border: "1px solid rgba(255,255,255,0.1)",
background: "var(--bg-input)",
border: "1px solid var(--border-input)",
borderRadius: 8, color: "var(--text-primary)",
fontSize: 13, outline: "none",
transition: "border-color 0.2s",
}}
onFocus={(e) => { e.currentTarget.style.borderColor = "rgba(74,158,255,0.4)"; }}
onBlur={(e) => { e.currentTarget.style.borderColor = "rgba(255,255,255,0.1)"; }}
onBlur={(e) => { e.currentTarget.style.borderColor = "var(--border-input)"; }}
/>
{search && (
<button
Expand Down Expand Up @@ -878,18 +876,18 @@ export function ContextHub() {
fontSize: 12,
fontWeight: 600,
borderRadius: 20,
border: isActive ? "1px solid transparent" : "1px solid rgba(255,255,255,0.12)",
border: isActive ? "1px solid transparent" : "1px solid var(--border-subtle)",
background: isActive ? "var(--accent-blue)" : "transparent",
color: isActive ? "#fff" : "var(--text-secondary)",
cursor: "pointer",
transition: "all 0.15s ease",
letterSpacing: "0.02em",
}}
onMouseEnter={(e) => {
if (!isActive) e.currentTarget.style.borderColor = "rgba(255,255,255,0.25)";
if (!isActive) e.currentTarget.style.borderColor = "var(--border-input-hover)";
}}
onMouseLeave={(e) => {
if (!isActive) e.currentTarget.style.borderColor = "rgba(255,255,255,0.12)";
if (!isActive) e.currentTarget.style.borderColor = "var(--border-subtle)";
}}
>
{chip}
Expand Down
Loading