Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -154,31 +154,32 @@ const GitExplorerComponent: React.FC = () => {
const refreshGit = useCallback(async () => {
if (!rootPath) return;
try {
const status = await invoke("get_git_status", { path: rootPath });
if (status) {
setIsGitRepo(true);
const lines = status.split("\n").filter((l: string) => l.trim());
// Porcelain v1: XY filename
// X = staged status, Y = unstaged status
const staged: GitFileChange[] = [];
const unstaged: GitFileChange[] = [];
for (const l of lines) {
const x = l[0]; // staged
const y = l[1]; // unstaged
const file = l.substring(3).trim();
if (x !== " " && x !== "?") staged.push({ status: x, file });
if (y !== " " && y !== "?") unstaged.push({ status: y, file });
// Untracked files (??) go to unstaged
if (x === "?" && y === "?") unstaged.push({ status: "??", file });
}
setStagedChanges(staged);
setGitChanges(unstaged);
const status = await invoke("get_git_status", { path: rootPath }, { silent: true });
setIsGitRepo(true);
const lines = status.split("\n").filter((l: string) => l.trim());
// Porcelain v1: XY filename
// X = staged status, Y = unstaged status
const staged: GitFileChange[] = [];
const unstaged: GitFileChange[] = [];
for (const l of lines) {
const x = l[0]; // staged
const y = l[1]; // unstaged
const file = l.substring(3).trim();
if (x !== " " && x !== "?") staged.push({ status: x, file });
if (y !== " " && y !== "?") unstaged.push({ status: y, file });
// Untracked files (??) go to unstaged
if (x === "?" && y === "?") unstaged.push({ status: "??", file });
}
setStagedChanges(staged);
setGitChanges(unstaged);
const bl = await invoke("get_git_branches", { path: rootPath });
setBranches(bl);
if (bl.length > 0) setCurrentBranch(bl[0]);
} catch (err) {
const errStr = String(err);
const errStr = String(err).toLowerCase();
const isNotGitRepoError =
errStr.includes("not a git repository") ||
errStr.includes("must be run in a work tree");
if (errStr.includes("dubious ownership")) {
const shouldFix = window.confirm(
`${t('git.explorer.safe_dir_title')}\n\n${t('git.explorer.safe_dir_desc', { path: rootPath })}\n\n(This runs: git config --global --add safe.directory)`
Expand All @@ -193,10 +194,14 @@ const GitExplorerComponent: React.FC = () => {
console.error("[Git safe dir fix error]", fixErr);
}
}
} else {
} else if (!isNotGitRepoError) {
console.error("[Git refresh error]", err);
}
setIsGitRepo(false);
setStagedChanges([]);
setGitChanges([]);
setBranches([]);
setCurrentBranch("");
}
}, [rootPath]);

Expand Down
57 changes: 51 additions & 6 deletions apps/desktop/src/api/store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { load, Store } from "@tauri-apps/plugin-store";
import { isTauri } from "@/api/tauri";

/**
* Global Store instance managed by tauri-plugin-store.
Expand All @@ -9,10 +10,19 @@ const STORE_NAME = "settings.json";
class TrixtyStore {
private store: Store | null = null;
private initPromise: Promise<void> | null = null;
private hasLoggedBrowserFallback = false;

async init() {
if (this.initPromise) return this.initPromise;


if (!isTauri()) {
if (!this.hasLoggedBrowserFallback) {
console.warn("[Store] Tauri runtime not detected. Falling back to localStorage.");
this.hasLoggedBrowserFallback = true;
}
return;
}

this.initPromise = (async () => {
console.log(`[Store] Initializing ${STORE_NAME}...`);
try {
Expand Down Expand Up @@ -80,22 +90,57 @@ class TrixtyStore {
}
}

private getFromLocalStorage<T>(key: string, defaultValue: T): T {
if (typeof window === "undefined") return defaultValue;

const raw = localStorage.getItem(key);
if (raw === null) return defaultValue;
return this.safeParse(raw) as T;
}

private setToLocalStorage(key: string, value: unknown) {
if (typeof window === "undefined") return;
const serialized = JSON.stringify(value);
if (serialized === undefined) {
localStorage.removeItem(key);
return;
}
localStorage.setItem(key, serialized);
}
Comment thread
matiaspalmac marked this conversation as resolved.

private deleteFromLocalStorage(key: string) {
if (typeof window === "undefined") return;
localStorage.removeItem(key);
}

async get<T>(key: string, defaultValue: T): Promise<T> {
if (!isTauri()) return this.getFromLocalStorage(key, defaultValue);

await this.init();
const val = await this.store!.get(key);
const val = await this.store?.get(key);
return (val as T) ?? defaultValue;
}

async set(key: string, value: unknown) {
if (!isTauri()) {
this.setToLocalStorage(key, value);
return;
}

await this.init();
await this.store!.set(key, value);
await this.store!.save();
await this.store?.set(key, value);
await this.store?.save();
}

async delete(key: string) {
if (!isTauri()) {
this.deleteFromLocalStorage(key);
return;
}

await this.init();
await this.store!.delete(key);
await this.store!.save();
await this.store?.delete(key);
await this.store?.save();
}
}

Expand Down
70 changes: 50 additions & 20 deletions apps/desktop/src/components/OnboardingWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,8 @@ import {
Sparkles,
Languages,
Brain,
Palette,
ChevronRight,
ChevronLeft,
Rocket,
Download,
AlertCircle,
Minus,
Square,
X,
Expand All @@ -19,11 +15,10 @@ import {
Loader2,
Settings2
} from "lucide-react";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { motion, AnimatePresence } from "framer-motion";
import { useApp } from "@/context/AppContext";
import { useL10n } from "@/hooks/useL10n";
import { safeInvoke as invoke } from "@/api/tauri";
import { isTauri, safeInvoke as invoke } from "@/api/tauri";

const steps = [
{ id: "welcome", title: "onboarding.welcome", icon: Sparkles, color: "text-blue-400" },
Expand All @@ -49,22 +44,55 @@ const OnboardingWizard: React.FC = () => {
const [isVerifyingOllama, setIsVerifyingOllama] = useState(false);
const [ollamaStatus, setOllamaStatus] = useState<"idle" | "success" | "error">("idle");
const [isMaximized, setIsMaximized] = useState(false);
const [isNativeWindow, setIsNativeWindow] = useState(false);

const handleMinimize = () => getCurrentWindow().minimize();
const handleMinimize = async () => {
if (!isNativeWindow) return;
const { getCurrentWindow } = await import("@tauri-apps/api/window");
await getCurrentWindow().minimize();
};
const handleMaximize = async () => {
if (!isNativeWindow) return;
const { getCurrentWindow } = await import("@tauri-apps/api/window");
await getCurrentWindow().toggleMaximize();
const maximized = await getCurrentWindow().isMaximized();
setIsMaximized(maximized);
};
const handleClose = () => getCurrentWindow().close();
const handleClose = async () => {
if (!isNativeWindow) return;
const { getCurrentWindow } = await import("@tauri-apps/api/window");
await getCurrentWindow().close();
};

useEffect(() => {
const unlisten = getCurrentWindow().onResized(async () => {
const maximized = await getCurrentWindow().isMaximized();
setIsMaximized(maximized);
});
let mounted = true;
let cleanup: (() => void) | undefined;

if (!isTauri()) return;

(async () => {
try {
const { getCurrentWindow } = await import("@tauri-apps/api/window");
const win = getCurrentWindow();
const maximized = await win.isMaximized();
if (!mounted) return;
setIsNativeWindow(true);
setIsMaximized(maximized);
const unlisten = await win.onResized(async () => {
const m = await win.isMaximized();
setIsMaximized(m);
});
if (!mounted) {
unlisten();
return;
}
cleanup = unlisten;
} catch {}
Comment thread
matiaspalmac marked this conversation as resolved.
})();

return () => {
unlisten.then(u => u());
mounted = false;
if (cleanup) cleanup();
};
}, []);

Expand All @@ -81,7 +109,7 @@ const OnboardingWizard: React.FC = () => {
} else {
setOllamaStatus("error");
}
} catch (e) {
} catch {
setOllamaStatus("error");
} finally {
setIsVerifyingOllama(false);
Expand Down Expand Up @@ -148,13 +176,15 @@ const OnboardingWizard: React.FC = () => {
return (
<div className="fixed inset-0 z-[100] bg-black flex items-center justify-center p-6 overflow-hidden text-white">
{/* Absolute Window Header (Top-level) */}
<div className="absolute top-0 left-0 right-0 h-10 flex items-center justify-end px-4 z-[120]" data-tauri-drag-region>
<div data-tauri-no-drag className="flex items-center gap-1">
<button onClick={handleMinimize} className="h-8 w-8 flex items-center justify-center hover:bg-white/5 rounded-lg transition-colors text-white/20 hover:text-white"><Minus size={14} /></button>
<button onClick={handleMaximize} className="h-8 w-8 flex items-center justify-center hover:bg-white/5 rounded-lg transition-colors text-white/20 hover:text-white">{isMaximized ? <Copy size={12} /> : <Square size={12} />}</button>
<button onClick={handleClose} className="h-8 w-8 flex items-center justify-center hover:bg-red-500/80 hover:text-white rounded-lg transition-colors text-white/20 hover:text-white"><X size={14} /></button>
{isNativeWindow && (
<div className="absolute top-0 left-0 right-0 h-10 flex items-center justify-end px-4 z-[120]" data-tauri-drag-region>
<div data-tauri-no-drag className="flex items-center gap-1">
<button onClick={handleMinimize} className="h-8 w-8 flex items-center justify-center hover:bg-white/5 rounded-lg transition-colors text-white/20 hover:text-white"><Minus size={14} /></button>
<button onClick={handleMaximize} className="h-8 w-8 flex items-center justify-center hover:bg-white/5 rounded-lg transition-colors text-white/20 hover:text-white">{isMaximized ? <Copy size={12} /> : <Square size={12} />}</button>
<button onClick={handleClose} className="h-8 w-8 flex items-center justify-center hover:bg-red-500/80 hover:text-white rounded-lg transition-colors text-white/20 hover:text-white"><X size={14} /></button>
</div>
</div>
</div>
)}

{/* Main Studio Container */}
<div className="relative w-full max-w-4xl h-[640px] bg-[#0c0c0c] border border-white/5 rounded-2xl overflow-hidden flex shadow-2xl">
Expand Down