From 66668d85d603c5841d755a6023aa1925559fc6d4 Mon Sep 17 00:00:00 2001
From: 苏向夜 <fu050409@163.com>
Date: Wed, 25 Feb 2026 01:32:51 +0800
Subject: chore(workspace): replace legacy codes

---
 .../ui/src/components/instance-creation-modal.tsx  | 552 +++++++++++++++++++++
 1 file changed, 552 insertions(+)
 create mode 100644 packages/ui/src/components/instance-creation-modal.tsx

(limited to 'packages/ui/src/components/instance-creation-modal.tsx')

diff --git a/packages/ui/src/components/instance-creation-modal.tsx b/packages/ui/src/components/instance-creation-modal.tsx
new file mode 100644
index 0000000..8a2b1b4
--- /dev/null
+++ b/packages/ui/src/components/instance-creation-modal.tsx
@@ -0,0 +1,552 @@
+import { invoke } from "@tauri-apps/api/core";
+import { Loader2, Search } from "lucide-react";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { toast } from "sonner";
+import { Button } from "@/components/ui/button";
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { useInstancesStore } from "@/models/instances";
+import { useGameStore } from "@/stores/game-store";
+import type { Version } from "@/types/bindings/manifest";
+import type { FabricLoaderEntry } from "../types/bindings/fabric";
+import type { ForgeVersion as ForgeVersionEntry } from "../types/bindings/forge";
+import type { Instance } from "../types/bindings/instance";
+
+interface Props {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+}
+
+export function InstanceCreationModal({ open, onOpenChange }: Props) {
+  const gameStore = useGameStore();
+  const instancesStore = useInstancesStore();
+
+  // Steps: 1 = name, 2 = version, 3 = mod loader
+  const [step, setStep] = useState<number>(1);
+
+  // Step 1
+  const [instanceName, setInstanceName] = useState<string>("");
+
+  // Step 2
+  const [versionSearch, setVersionSearch] = useState<string>("");
+  const [versionFilter, setVersionFilter] = useState<
+    "all" | "release" | "snapshot"
+  >("release");
+  const [selectedVersionUI, setSelectedVersionUI] = useState<Version | null>(
+    null,
+  );
+
+  // Step 3
+  const [modLoaderType, setModLoaderType] = useState<
+    "vanilla" | "fabric" | "forge"
+  >("vanilla");
+  const [fabricLoaders, setFabricLoaders] = useState<FabricLoaderEntry[]>([]);
+  const [forgeVersions, setForgeVersions] = useState<ForgeVersionEntry[]>([]);
+  const [selectedFabricLoader, setSelectedFabricLoader] = useState<string>("");
+  const [selectedForgeLoader, setSelectedForgeLoader] = useState<string>("");
+  const [loadingLoaders, setLoadingLoaders] = useState(false);
+
+  const loadModLoaders = useCallback(async () => {
+    if (!selectedVersionUI) return;
+    setLoadingLoaders(true);
+    setFabricLoaders([]);
+    setForgeVersions([]);
+    try {
+      if (modLoaderType === "fabric") {
+        const loaders = await invoke<FabricLoaderEntry[]>(
+          "get_fabric_loaders_for_version",
+          {
+            gameVersion: selectedVersionUI.id,
+          },
+        );
+        setFabricLoaders(loaders || []);
+        if (loaders && loaders.length > 0) {
+          setSelectedFabricLoader(loaders[0].loader.version);
+        } else {
+          setSelectedFabricLoader("");
+        }
+      } else if (modLoaderType === "forge") {
+        const versions = await invoke<ForgeVersionEntry[]>(
+          "get_forge_versions_for_game",
+          {
+            gameVersion: selectedVersionUI.id,
+          },
+        );
+        setForgeVersions(versions || []);
+        if (versions && versions.length > 0) {
+          // Binding `ForgeVersion` uses `version` (not `id`) — use `.version` here.
+          setSelectedForgeLoader(versions[0].version);
+        } else {
+          setSelectedForgeLoader("");
+        }
+      }
+    } catch (e) {
+      console.error("Failed to load mod loaders:", e);
+      toast.error("Failed to fetch mod loader versions");
+    } finally {
+      setLoadingLoaders(false);
+    }
+  }, [modLoaderType, selectedVersionUI]);
+
+  // When entering step 3 and a base version exists, fetch loaders if needed
+  useEffect(() => {
+    if (step === 3 && modLoaderType !== "vanilla" && selectedVersionUI) {
+      loadModLoaders();
+    }
+  }, [step, modLoaderType, selectedVersionUI, loadModLoaders]);
+
+  // Creating state
+  const [creating, setCreating] = useState(false);
+  const [errorMessage, setErrorMessage] = useState<string>("");
+
+  // Derived filtered versions
+  const filteredVersions = useMemo(() => {
+    const all = gameStore.versions || [];
+    let list = all.slice();
+    if (versionFilter !== "all") {
+      list = list.filter((v) => v.type === versionFilter);
+    }
+    if (versionSearch.trim()) {
+      const q = versionSearch.trim().toLowerCase().replace(/。/g, ".");
+      list = list.filter((v) => v.id.toLowerCase().includes(q));
+    }
+    return list;
+  }, [gameStore.versions, versionFilter, versionSearch]);
+
+  // Reset when opened/closed
+  useEffect(() => {
+    if (open) {
+      // ensure versions are loaded
+      gameStore.loadVersions();
+      setStep(1);
+      setInstanceName("");
+      setVersionSearch("");
+      setVersionFilter("release");
+      setSelectedVersionUI(null);
+      setModLoaderType("vanilla");
+      setFabricLoaders([]);
+      setForgeVersions([]);
+      setSelectedFabricLoader("");
+      setSelectedForgeLoader("");
+      setErrorMessage("");
+      setCreating(false);
+    }
+  }, [open, gameStore.loadVersions]);
+
+  function validateStep1(): boolean {
+    if (!instanceName.trim()) {
+      setErrorMessage("Please enter an instance name");
+      return false;
+    }
+    setErrorMessage("");
+    return true;
+  }
+
+  function validateStep2(): boolean {
+    if (!selectedVersionUI) {
+      setErrorMessage("Please select a Minecraft version");
+      return false;
+    }
+    setErrorMessage("");
+    return true;
+  }
+
+  async function handleNext() {
+    setErrorMessage("");
+    if (step === 1) {
+      if (!validateStep1()) return;
+      setStep(2);
+    } else if (step === 2) {
+      if (!validateStep2()) return;
+      setStep(3);
+    }
+  }
+
+  function handleBack() {
+    setErrorMessage("");
+    setStep((s) => Math.max(1, s - 1));
+  }
+
+  async function handleCreate() {
+    if (!validateStep1() || !validateStep2()) return;
+    setCreating(true);
+    setErrorMessage("");
+
+    try {
+      // Step 1: create instance
+      const instance = await invoke<Instance>("create_instance", {
+        name: instanceName.trim(),
+      });
+
+      // If selectedVersion provided, install it
+      if (selectedVersionUI) {
+        try {
+          await invoke("install_version", {
+            instanceId: instance.id,
+            versionId: selectedVersionUI.id,
+          });
+        } catch (err) {
+          console.error("Failed to install base version:", err);
+          // continue - instance created but version install failed
+          toast.error(
+            `Failed to install version ${selectedVersionUI.id}: ${String(err)}`,
+          );
+        }
+      }
+
+      // If mod loader selected, install it
+      if (modLoaderType === "fabric" && selectedFabricLoader) {
+        try {
+          await invoke("install_fabric", {
+            instanceId: instance.id,
+            gameVersion: selectedVersionUI?.id ?? "",
+            loaderVersion: selectedFabricLoader,
+          });
+        } catch (err) {
+          console.error("Failed to install Fabric:", err);
+          toast.error(`Failed to install Fabric: ${String(err)}`);
+        }
+      } else if (modLoaderType === "forge" && selectedForgeLoader) {
+        try {
+          await invoke("install_forge", {
+            instanceId: instance.id,
+            gameVersion: selectedVersionUI?.id ?? "",
+            installerVersion: selectedForgeLoader,
+          });
+        } catch (err) {
+          console.error("Failed to install Forge:", err);
+          toast.error(`Failed to install Forge: ${String(err)}`);
+        }
+      }
+
+      // Refresh instances list
+      await instancesStore.refresh();
+
+      toast.success("Instance created successfully");
+      onOpenChange(false);
+    } catch (e) {
+      console.error("Failed to create instance:", e);
+      setErrorMessage(String(e));
+      toast.error(`Failed to create instance: ${e}`);
+    } finally {
+      setCreating(false);
+    }
+  }
+
+  // UI pieces
+  const StepIndicator = () => (
+    <div className="flex gap-2 w-full">
+      <div
+        className={`flex-1 h-1 rounded ${step >= 1 ? "bg-indigo-500" : "bg-zinc-700"}`}
+      />
+      <div
+        className={`flex-1 h-1 rounded ${step >= 2 ? "bg-indigo-500" : "bg-zinc-700"}`}
+      />
+      <div
+        className={`flex-1 h-1 rounded ${step >= 3 ? "bg-indigo-500" : "bg-zinc-700"}`}
+      />
+    </div>
+  );
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent className="w-full max-w-3xl max-h-[90vh] overflow-hidden">
+        <DialogHeader>
+          <DialogTitle>Create New Instance</DialogTitle>
+          <DialogDescription>
+            Multi-step wizard — create an instance and optionally install a
+            version or mod loader.
+          </DialogDescription>
+        </DialogHeader>
+
+        <div className="px-6">
+          <div className="pt-4 pb-6">
+            <StepIndicator />
+          </div>
+
+          {/* Step 1 - Name */}
+          {step === 1 && (
+            <div className="space-y-4">
+              <div>
+                <label
+                  htmlFor="instance-name"
+                  className="block text-sm font-medium mb-2"
+                >
+                  Instance Name
+                </label>
+                <Input
+                  id="instance-name"
+                  placeholder="My Minecraft Instance"
+                  value={instanceName}
+                  onChange={(e) => setInstanceName(e.target.value)}
+                  disabled={creating}
+                />
+              </div>
+              <p className="text-xs text-muted-foreground">
+                Give your instance a memorable name.
+              </p>
+            </div>
+          )}
+
+          {/* Step 2 - Version selection */}
+          {step === 2 && (
+            <div className="space-y-4">
+              <div className="flex gap-3">
+                <div className="relative flex-1">
+                  <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
+                  <Input
+                    value={versionSearch}
+                    onChange={(e) => setVersionSearch(e.target.value)}
+                    placeholder="Search versions..."
+                    className="pl-9"
+                  />
+                </div>
+
+                <div className="flex gap-2">
+                  <Button
+                    type="button"
+                    variant={versionFilter === "all" ? "default" : "outline"}
+                    onClick={() => setVersionFilter("all")}
+                  >
+                    All
+                  </Button>
+                  <Button
+                    type="button"
+                    variant={
+                      versionFilter === "release" ? "default" : "outline"
+                    }
+                    onClick={() => setVersionFilter("release")}
+                  >
+                    Release
+                  </Button>
+                  <Button
+                    type="button"
+                    variant={
+                      versionFilter === "snapshot" ? "default" : "outline"
+                    }
+                    onClick={() => setVersionFilter("snapshot")}
+                  >
+                    Snapshot
+                  </Button>
+                </div>
+              </div>
+
+              <ScrollArea className="max-h-[36vh]">
+                <div className="space-y-2 py-2">
+                  {gameStore.versions.length === 0 ? (
+                    <div className="flex items-center justify-center py-8 text-muted-foreground">
+                      <Loader2 className="animate-spin mr-2" />
+                      Loading versions...
+                    </div>
+                  ) : filteredVersions.length === 0 ? (
+                    <div className="text-center py-8 text-muted-foreground">
+                      No matching versions found
+                    </div>
+                  ) : (
+                    filteredVersions.map((v) => {
+                      const isSelected = selectedVersionUI?.id === v.id;
+                      return (
+                        <button
+                          key={v.id}
+                          type="button"
+                          onClick={() => setSelectedVersionUI(v)}
+                          className={`w-full text-left p-3 rounded-lg border transition-colors ${
+                            isSelected
+                              ? "bg-indigo-50 dark:bg-indigo-600/20 border-indigo-200"
+                              : "bg-white/40 dark:bg-white/5 border-black/5 dark:border-white/5 hover:bg-white/60"
+                          }`}
+                        >
+                          <div className="flex items-center justify-between">
+                            <div>
+                              <div className="font-mono font-bold">{v.id}</div>
+                              <div className="text-xs text-muted-foreground mt-1">
+                                {v.type}{" "}
+                                {v.releaseTime
+                                  ? ` • ${new Date(v.releaseTime).toLocaleDateString()}`
+                                  : ""}
+                              </div>
+                            </div>
+                            {v.javaVersion && (
+                              <div className="text-sm">
+                                Java {v.javaVersion}
+                              </div>
+                            )}
+                          </div>
+                        </button>
+                      );
+                    })
+                  )}
+                </div>
+              </ScrollArea>
+            </div>
+          )}
+
+          {/* Step 3 - Mod loader */}
+          {step === 3 && (
+            <div className="space-y-4">
+              <div>
+                <div className="text-sm font-medium mb-2">Mod Loader Type</div>
+                <div className="flex gap-3">
+                  <Button
+                    type="button"
+                    variant={
+                      modLoaderType === "vanilla" ? "default" : "outline"
+                    }
+                    onClick={() => setModLoaderType("vanilla")}
+                  >
+                    Vanilla
+                  </Button>
+                  <Button
+                    type="button"
+                    variant={modLoaderType === "fabric" ? "default" : "outline"}
+                    onClick={() => setModLoaderType("fabric")}
+                  >
+                    Fabric
+                  </Button>
+                  <Button
+                    type="button"
+                    variant={modLoaderType === "forge" ? "default" : "outline"}
+                    onClick={() => setModLoaderType("forge")}
+                  >
+                    Forge
+                  </Button>
+                </div>
+              </div>
+
+              {modLoaderType === "fabric" && (
+                <div>
+                  {loadingLoaders ? (
+                    <div className="flex items-center gap-2">
+                      <Loader2 className="animate-spin" />
+                      Loading Fabric versions...
+                    </div>
+                  ) : fabricLoaders.length > 0 ? (
+                    <div className="space-y-2">
+                      <select
+                        value={selectedFabricLoader}
+                        onChange={(e) =>
+                          setSelectedFabricLoader(e.target.value)
+                        }
+                        className="w-full px-3 py-2 rounded border bg-transparent"
+                      >
+                        {fabricLoaders.map((f) => (
+                          <option
+                            key={f.loader.version}
+                            value={f.loader.version}
+                          >
+                            {f.loader.version}{" "}
+                            {f.loader.stable ? "(Stable)" : "(Beta)"}
+                          </option>
+                        ))}
+                      </select>
+                    </div>
+                  ) : (
+                    <p className="text-sm text-muted-foreground">
+                      No Fabric loaders available for this version
+                    </p>
+                  )}
+                </div>
+              )}
+
+              {modLoaderType === "forge" && (
+                <div>
+                  {loadingLoaders ? (
+                    <div className="flex items-center gap-2">
+                      <Loader2 className="animate-spin" />
+                      Loading Forge versions...
+                    </div>
+                  ) : forgeVersions.length > 0 ? (
+                    <div className="space-y-2">
+                      <select
+                        value={selectedForgeLoader}
+                        onChange={(e) => setSelectedForgeLoader(e.target.value)}
+                        className="w-full px-3 py-2 rounded border bg-transparent"
+                      >
+                        {forgeVersions.map((f) => (
+                          // binding ForgeVersion uses `version` as the identifier
+                          <option key={f.version} value={f.version}>
+                            {f.version}
+                          </option>
+                        ))}
+                      </select>
+                    </div>
+                  ) : (
+                    <p className="text-sm text-muted-foreground">
+                      No Forge versions available for this version
+                    </p>
+                  )}
+                </div>
+              )}
+            </div>
+          )}
+
+          {errorMessage && (
+            <div className="text-sm text-red-400 mt-3">{errorMessage}</div>
+          )}
+        </div>
+
+        <DialogFooter>
+          <div className="w-full flex justify-between items-center">
+            <div>
+              <Button
+                type="button"
+                variant="ghost"
+                onClick={() => {
+                  // cancel
+                  onOpenChange(false);
+                }}
+                disabled={creating}
+              >
+                Cancel
+              </Button>
+            </div>
+
+            <div className="flex gap-2">
+              {step > 1 && (
+                <Button
+                  type="button"
+                  variant="outline"
+                  onClick={handleBack}
+                  disabled={creating}
+                >
+                  Back
+                </Button>
+              )}
+
+              {step < 3 ? (
+                <Button type="button" onClick={handleNext} disabled={creating}>
+                  Next
+                </Button>
+              ) : (
+                <Button
+                  type="button"
+                  onClick={handleCreate}
+                  disabled={creating}
+                >
+                  {creating ? (
+                    <>
+                      <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                      Creating...
+                    </>
+                  ) : (
+                    "Create"
+                  )}
+                </Button>
+              )}
+            </div>
+          </div>
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
+  );
+}
+
+export default InstanceCreationModal;
-- 
cgit v1.2.3-70-g09d2


From d95ca2801c19a89a2a845f43b6e0133bf4e9be50 Mon Sep 17 00:00:00 2001
From: 苏向夜 <fu050409@163.com>
Date: Thu, 26 Feb 2026 18:30:57 +0800
Subject: refactor: migrate some invokes

---
 .changes/migrate-apis.md                           |   5 +
 .../ui/src/components/instance-creation-modal.tsx  |  70 +++++------
 .../ui/src/components/instance-editor-modal.tsx    |   4 +-
 packages/ui/src/models/instance.ts                 | 131 ++++++++++++++++++++
 packages/ui/src/models/instances.ts                | 135 ---------------------
 packages/ui/src/pages/index.tsx                    |   5 +-
 packages/ui/src/pages/instances-view.tsx           |   4 +-
 7 files changed, 175 insertions(+), 179 deletions(-)
 create mode 100644 .changes/migrate-apis.md
 create mode 100644 packages/ui/src/models/instance.ts
 delete mode 100644 packages/ui/src/models/instances.ts

(limited to 'packages/ui/src/components/instance-creation-modal.tsx')

diff --git a/.changes/migrate-apis.md b/.changes/migrate-apis.md
new file mode 100644
index 0000000..54f1566
--- /dev/null
+++ b/.changes/migrate-apis.md
@@ -0,0 +1,5 @@
+---
+"@dropout/ui": "patch:refactor"
+---
+
+Migrate tauri invokes of instance creation modal to generated client.
diff --git a/packages/ui/src/components/instance-creation-modal.tsx b/packages/ui/src/components/instance-creation-modal.tsx
index 8a2b1b4..7c46d0f 100644
--- a/packages/ui/src/components/instance-creation-modal.tsx
+++ b/packages/ui/src/components/instance-creation-modal.tsx
@@ -1,7 +1,13 @@
-import { invoke } from "@tauri-apps/api/core";
 import { Loader2, Search } from "lucide-react";
 import { useCallback, useEffect, useMemo, useState } from "react";
 import { toast } from "sonner";
+import {
+  getFabricLoadersForVersion,
+  getForgeVersionsForGame,
+  installFabric,
+  installForge,
+  installVersion,
+} from "@/client";
 import { Button } from "@/components/ui/button";
 import {
   Dialog,
@@ -13,12 +19,13 @@ import {
 } from "@/components/ui/dialog";
 import { Input } from "@/components/ui/input";
 import { ScrollArea } from "@/components/ui/scroll-area";
-import { useInstancesStore } from "@/models/instances";
+import { useInstanceStore } from "@/models/instance";
 import { useGameStore } from "@/stores/game-store";
-import type { Version } from "@/types/bindings/manifest";
-import type { FabricLoaderEntry } from "../types/bindings/fabric";
-import type { ForgeVersion as ForgeVersionEntry } from "../types/bindings/forge";
-import type { Instance } from "../types/bindings/instance";
+import type {
+  FabricLoaderEntry,
+  ForgeVersion as ForgeVersionEntry,
+  Version,
+} from "@/types";
 
 interface Props {
   open: boolean;
@@ -27,7 +34,7 @@ interface Props {
 
 export function InstanceCreationModal({ open, onOpenChange }: Props) {
   const gameStore = useGameStore();
-  const instancesStore = useInstancesStore();
+  const instancesStore = useInstanceStore();
 
   // Steps: 1 = name, 2 = version, 3 = mod loader
   const [step, setStep] = useState<number>(1);
@@ -61,12 +68,7 @@ export function InstanceCreationModal({ open, onOpenChange }: Props) {
     setForgeVersions([]);
     try {
       if (modLoaderType === "fabric") {
-        const loaders = await invoke<FabricLoaderEntry[]>(
-          "get_fabric_loaders_for_version",
-          {
-            gameVersion: selectedVersionUI.id,
-          },
-        );
+        const loaders = await getFabricLoadersForVersion(selectedVersionUI.id);
         setFabricLoaders(loaders || []);
         if (loaders && loaders.length > 0) {
           setSelectedFabricLoader(loaders[0].loader.version);
@@ -74,12 +76,7 @@ export function InstanceCreationModal({ open, onOpenChange }: Props) {
           setSelectedFabricLoader("");
         }
       } else if (modLoaderType === "forge") {
-        const versions = await invoke<ForgeVersionEntry[]>(
-          "get_forge_versions_for_game",
-          {
-            gameVersion: selectedVersionUI.id,
-          },
-        );
+        const versions = await getForgeVersionsForGame(selectedVersionUI.id);
         setForgeVersions(versions || []);
         if (versions && versions.length > 0) {
           // Binding `ForgeVersion` uses `version` (not `id`) — use `.version` here.
@@ -182,17 +179,12 @@ export function InstanceCreationModal({ open, onOpenChange }: Props) {
 
     try {
       // Step 1: create instance
-      const instance = await invoke<Instance>("create_instance", {
-        name: instanceName.trim(),
-      });
+      const instance = await instancesStore.create(instanceName.trim());
 
       // If selectedVersion provided, install it
-      if (selectedVersionUI) {
+      if (selectedVersionUI && instance) {
         try {
-          await invoke("install_version", {
-            instanceId: instance.id,
-            versionId: selectedVersionUI.id,
-          });
+          await installVersion(instance?.id, selectedVersionUI.id);
         } catch (err) {
           console.error("Failed to install base version:", err);
           // continue - instance created but version install failed
@@ -203,24 +195,24 @@ export function InstanceCreationModal({ open, onOpenChange }: Props) {
       }
 
       // If mod loader selected, install it
-      if (modLoaderType === "fabric" && selectedFabricLoader) {
+      if (modLoaderType === "fabric" && selectedFabricLoader && instance) {
         try {
-          await invoke("install_fabric", {
-            instanceId: instance.id,
-            gameVersion: selectedVersionUI?.id ?? "",
-            loaderVersion: selectedFabricLoader,
-          });
+          await installFabric(
+            instance?.id,
+            selectedVersionUI?.id ?? "",
+            selectedFabricLoader,
+          );
         } catch (err) {
           console.error("Failed to install Fabric:", err);
           toast.error(`Failed to install Fabric: ${String(err)}`);
         }
-      } else if (modLoaderType === "forge" && selectedForgeLoader) {
+      } else if (modLoaderType === "forge" && selectedForgeLoader && instance) {
         try {
-          await invoke("install_forge", {
-            instanceId: instance.id,
-            gameVersion: selectedVersionUI?.id ?? "",
-            installerVersion: selectedForgeLoader,
-          });
+          await installForge(
+            instance?.id,
+            selectedVersionUI?.id ?? "",
+            selectedForgeLoader,
+          );
         } catch (err) {
           console.error("Failed to install Forge:", err);
           toast.error(`Failed to install Forge: ${String(err)}`);
diff --git a/packages/ui/src/components/instance-editor-modal.tsx b/packages/ui/src/components/instance-editor-modal.tsx
index f880c20..d964185 100644
--- a/packages/ui/src/components/instance-editor-modal.tsx
+++ b/packages/ui/src/components/instance-editor-modal.tsx
@@ -16,7 +16,7 @@ import { Input } from "@/components/ui/input";
 import { Textarea } from "@/components/ui/textarea";
 
 import { toNumber } from "@/lib/tsrs-utils";
-import { useInstancesStore } from "@/models/instances";
+import { useInstanceStore } from "@/models/instance";
 import { useSettingsStore } from "@/models/settings";
 import type { FileInfo } from "../types/bindings/core";
 import type { Instance } from "../types/bindings/instance";
@@ -28,7 +28,7 @@ type Props = {
 };
 
 export function InstanceEditorModal({ open, instance, onOpenChange }: Props) {
-  const instancesStore = useInstancesStore();
+  const instancesStore = useInstanceStore();
   const { config } = useSettingsStore();
 
   const [activeTab, setActiveTab] = useState<
diff --git a/packages/ui/src/models/instance.ts b/packages/ui/src/models/instance.ts
new file mode 100644
index 0000000..a3fda3d
--- /dev/null
+++ b/packages/ui/src/models/instance.ts
@@ -0,0 +1,131 @@
+import { toast } from "sonner";
+import { create } from "zustand";
+import {
+  createInstance,
+  deleteInstance,
+  duplicateInstance,
+  getActiveInstance,
+  getInstance,
+  listInstances,
+  setActiveInstance,
+  updateInstance,
+} from "@/client";
+import type { Instance } from "@/types";
+
+interface InstanceState {
+  instances: Instance[];
+  activeInstance: Instance | null;
+
+  refresh: () => Promise<void>;
+  create: (name: string) => Promise<Instance | null>;
+  delete: (id: string) => Promise<void>;
+  update: (instance: Instance) => Promise<void>;
+  setActiveInstance: (instance: Instance) => Promise<void>;
+  duplicate: (id: string, newName: string) => Promise<Instance | null>;
+  get: (id: string) => Promise<Instance | null>;
+}
+
+export const useInstanceStore = create<InstanceState>((set, get) => ({
+  instances: [],
+  activeInstance: null,
+
+  refresh: async () => {
+    const { setActiveInstance } = get();
+    try {
+      const instances = await listInstances();
+      const activeInstance = await getActiveInstance();
+
+      if (!activeInstance && instances.length > 0) {
+        // If no active instance but instances exist, set the first one as active
+        await setActiveInstance(instances[0]);
+      }
+
+      set({ instances, activeInstance });
+    } catch (e) {
+      console.error("Failed to load instances:", e);
+      toast.error("Error loading instances");
+    }
+  },
+
+  create: async (name) => {
+    const { refresh } = get();
+    try {
+      const instance = await createInstance(name);
+      await refresh();
+      toast.success(`Instance "${name}" created successfully`);
+      return instance;
+    } catch (e) {
+      console.error("Failed to create instance:", e);
+      toast.error("Error creating instance");
+      return null;
+    }
+  },
+
+  delete: async (id) => {
+    const { refresh, instances, activeInstance, setActiveInstance } = get();
+    try {
+      await deleteInstance(id);
+      await refresh();
+
+      // If deleted instance was active, set another as active
+      if (activeInstance?.id === id) {
+        if (instances.length > 0) {
+          await setActiveInstance(instances[0]);
+        } else {
+          set({ activeInstance: null });
+        }
+      }
+
+      toast.success("Instance deleted successfully");
+    } catch (e) {
+      console.error("Failed to delete instance:", e);
+      toast.error("Error deleting instance");
+    }
+  },
+
+  update: async (instance) => {
+    const { refresh } = get();
+    try {
+      await updateInstance(instance);
+      await refresh();
+      toast.success("Instance updated successfully");
+    } catch (e) {
+      console.error("Failed to update instance:", e);
+      toast.error("Error updating instance");
+    }
+  },
+
+  setActiveInstance: async (instance) => {
+    try {
+      await setActiveInstance(instance.id);
+      set({ activeInstance: instance });
+      toast.success("Active instance changed");
+    } catch (e) {
+      console.error("Failed to set active instance:", e);
+      toast.error("Error setting active instance");
+    }
+  },
+
+  duplicate: async (id, newName) => {
+    const { refresh } = get();
+    try {
+      const instance = await duplicateInstance(id, newName);
+      await refresh();
+      toast.success(`Instance duplicated as "${newName}"`);
+      return instance;
+    } catch (e) {
+      console.error("Failed to duplicate instance:", e);
+      toast.error("Error duplicating instance");
+      return null;
+    }
+  },
+
+  get: async (id) => {
+    try {
+      return await getInstance(id);
+    } catch (e) {
+      console.error("Failed to get instance:", e);
+      return null;
+    }
+  },
+}));
diff --git a/packages/ui/src/models/instances.ts b/packages/ui/src/models/instances.ts
deleted file mode 100644
index f434c7c..0000000
--- a/packages/ui/src/models/instances.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-import { toast } from "sonner";
-import { create } from "zustand";
-import {
-  createInstance,
-  deleteInstance,
-  duplicateInstance,
-  getActiveInstance,
-  getInstance,
-  listInstances,
-  setActiveInstance,
-  updateInstance,
-} from "@/client";
-import type { Instance } from "@/types";
-
-interface InstancesState {
-  // State
-  instances: Instance[];
-  activeInstance: Instance | null;
-
-  // Actions
-  refresh: () => Promise<void>;
-  create: (name: string) => Promise<Instance | null>;
-  delete: (id: string) => Promise<void>;
-  update: (instance: Instance) => Promise<void>;
-  setActiveInstance: (instance: Instance) => Promise<void>;
-  duplicate: (id: string, newName: string) => Promise<Instance | null>;
-  getInstance: (id: string) => Promise<Instance | null>;
-}
-
-export const useInstancesStore = create<InstancesState>((set, get) => ({
-  // Initial state
-  instances: [],
-  activeInstance: null,
-
-  // Actions
-  refresh: async () => {
-    const { setActiveInstance } = get();
-    try {
-      const instances = await listInstances();
-      const active = await getActiveInstance();
-
-      if (!active && instances.length > 0) {
-        // If no active instance but instances exist, set the first one as active
-        await setActiveInstance(instances[0]);
-      }
-
-      set({ instances });
-    } catch (e) {
-      console.error("Failed to load instances:", e);
-      toast.error("Error loading instances");
-    }
-  },
-
-  create: async (name) => {
-    const { refresh } = get();
-    try {
-      const instance = await createInstance(name);
-      await refresh();
-      toast.success(`Instance "${name}" created successfully`);
-      return instance;
-    } catch (e) {
-      console.error("Failed to create instance:", e);
-      toast.error("Error creating instance");
-      return null;
-    }
-  },
-
-  delete: async (id) => {
-    const { refresh, instances, activeInstance, setActiveInstance } = get();
-    try {
-      await deleteInstance(id);
-      await refresh();
-
-      // If deleted instance was active, set another as active
-      if (activeInstance?.id === id) {
-        if (instances.length > 0) {
-          await setActiveInstance(instances[0]);
-        } else {
-          set({ activeInstance: null });
-        }
-      }
-
-      toast.success("Instance deleted successfully");
-    } catch (e) {
-      console.error("Failed to delete instance:", e);
-      toast.error("Error deleting instance");
-    }
-  },
-
-  update: async (instance) => {
-    const { refresh } = get();
-    try {
-      await updateInstance(instance);
-      await refresh();
-      toast.success("Instance updated successfully");
-    } catch (e) {
-      console.error("Failed to update instance:", e);
-      toast.error("Error updating instance");
-    }
-  },
-
-  setActiveInstance: async (instance) => {
-    try {
-      await setActiveInstance(instance.id);
-      set({ activeInstance: instance });
-      toast.success("Active instance changed");
-    } catch (e) {
-      console.error("Failed to set active instance:", e);
-      toast.error("Error setting active instance");
-    }
-  },
-
-  duplicate: async (id, newName) => {
-    const { refresh } = get();
-    try {
-      const instance = await duplicateInstance(id, newName);
-      await refresh();
-      toast.success(`Instance duplicated as "${newName}"`);
-      return instance;
-    } catch (e) {
-      console.error("Failed to duplicate instance:", e);
-      toast.error("Error duplicating instance");
-      return null;
-    }
-  },
-
-  getInstance: async (id) => {
-    try {
-      return await getInstance(id);
-    } catch (e) {
-      console.error("Failed to get instance:", e);
-      return null;
-    }
-  },
-}));
diff --git a/packages/ui/src/pages/index.tsx b/packages/ui/src/pages/index.tsx
index 54cfc1e..093ccb2 100644
--- a/packages/ui/src/pages/index.tsx
+++ b/packages/ui/src/pages/index.tsx
@@ -3,18 +3,21 @@ import { Outlet, useLocation } from "react-router";
 import { ParticleBackground } from "@/components/particle-background";
 import { Sidebar } from "@/components/sidebar";
 import { useAuthStore } from "@/models/auth";
+import { useInstanceStore } from "@/models/instance";
 import { useSettingsStore } from "@/models/settings";
 
 export function IndexPage() {
   const authStore = useAuthStore();
   const settingsStore = useSettingsStore();
+  const instanceStore = useInstanceStore();
 
   const location = useLocation();
 
   useEffect(() => {
     authStore.init();
     settingsStore.refresh();
-  }, [authStore.init, settingsStore.refresh]);
+    instanceStore.refresh();
+  }, [authStore.init, settingsStore.refresh, instanceStore.refresh]);
 
   return (
     <div className="relative h-screen w-full overflow-hidden bg-background font-sans">
diff --git a/packages/ui/src/pages/instances-view.tsx b/packages/ui/src/pages/instances-view.tsx
index ad6bd38..1634905 100644
--- a/packages/ui/src/pages/instances-view.tsx
+++ b/packages/ui/src/pages/instances-view.tsx
@@ -13,11 +13,11 @@ import {
 } from "@/components/ui/dialog";
 import { Input } from "@/components/ui/input";
 import { toNumber } from "@/lib/tsrs-utils";
-import { useInstancesStore } from "@/models/instances";
+import { useInstanceStore } from "@/models/instance";
 import type { Instance } from "../types/bindings/instance";
 
 export function InstancesView() {
-  const instancesStore = useInstancesStore();
+  const instancesStore = useInstanceStore();
 
   // Modal / UI state
   const [showCreateModal, setShowCreateModal] = useState(false);
-- 
cgit v1.2.3-70-g09d2

