diff --git a/.cursor/rules/ipc.mdc b/.cursor/rules/ipc.mdc
index 2b88b96..d8d59e1 100644
--- a/.cursor/rules/ipc.mdc
+++ b/.cursor/rules/ipc.mdc
@@ -66,27 +66,7 @@ The pattern involves a client-side React hook interacting with main process IPC
* Contains the core business logic, interacting with databases (e.g., `db`), file system (`fs`), or other main-process services (e.g., `git`).
* **Error Handling (Crucial):**
* **Handlers MUST `throw new Error("Descriptive error message")` when an operation fails or an invalid state is encountered.** This is the preferred pattern over returning objects like `{ success: false, errorMessage: "..." }`.
- * Use `try...catch` blocks to handle errors from underlying operations (e.g., database queries, file system access, git commands).
- * Inside the `catch` block, log the original error for debugging purposes and then `throw` a new, often more user-friendly or context-specific, `Error`.
- * Example:
- ```typescript
- ipcMain.handle("list-entities", async (_, { parentId }) => {
- if (!parentId) {
- throw new Error("Parent ID is required to list entities.");
- }
- try {
- const entities = await db.query.entities.findMany({ where: eq(entities.parentId, parentId) });
- if (!entities) {
- // Or handle as empty list depending on requirements
- throw new Error(`No entities found for parent ID: ${parentId}`);
- }
- return entities;
- } catch (error: any) {
- logger.error(`Error listing entities for parent ${parentId}:`, error);
- throw new Error(`Failed to list entities: ${error.message}`);
- }
- });
- ```
+
* **Concurrency (If Applicable):**
* For operations that modify shared resources related to a specific entity (like an `appId`), use a locking mechanism (e.g., `withLock(appId, async () => { ... })`) to prevent race conditions.
diff --git a/drizzle/0005_left_thor.sql b/drizzle/0005_left_thor.sql
new file mode 100644
index 0000000..8942bb5
--- /dev/null
+++ b/drizzle/0005_left_thor.sql
@@ -0,0 +1,20 @@
+CREATE TABLE `language_model_providers` (
+ `id` text PRIMARY KEY NOT NULL,
+ `name` text NOT NULL,
+ `api_base_url` text NOT NULL,
+ `env_var_name` text,
+ `created_at` integer DEFAULT (unixepoch()) NOT NULL,
+ `updated_at` integer DEFAULT (unixepoch()) NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE `language_models` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `name` text NOT NULL,
+ `provider_id` text NOT NULL,
+ `description` text,
+ `max_output_tokens` integer,
+ `context_window` integer,
+ `created_at` integer DEFAULT (unixepoch()) NOT NULL,
+ `updated_at` integer DEFAULT (unixepoch()) NOT NULL,
+ FOREIGN KEY (`provider_id`) REFERENCES `language_model_providers`(`id`) ON UPDATE no action ON DELETE cascade
+);
diff --git a/drizzle/meta/0005_snapshot.json b/drizzle/meta/0005_snapshot.json
new file mode 100644
index 0000000..3fdb55f
--- /dev/null
+++ b/drizzle/meta/0005_snapshot.json
@@ -0,0 +1,356 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "29ca03c0-a5d6-4db2-a84a-03721206fdb4",
+ "prevId": "ceedb797-6aa3-4a50-b42f-bc85ee08b3df",
+ "tables": {
+ "apps": {
+ "name": "apps",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch())"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch())"
+ },
+ "github_org": {
+ "name": "github_org",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "github_repo": {
+ "name": "github_repo",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "supabase_project_id": {
+ "name": "supabase_project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "chats": {
+ "name": "chats",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "app_id": {
+ "name": "app_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "initial_commit_hash": {
+ "name": "initial_commit_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch())"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "chats_app_id_apps_id_fk": {
+ "name": "chats_app_id_apps_id_fk",
+ "tableFrom": "chats",
+ "tableTo": "apps",
+ "columnsFrom": [
+ "app_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "language_model_providers": {
+ "name": "language_model_providers",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "api_base_url": {
+ "name": "api_base_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "env_var_name": {
+ "name": "env_var_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch())"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch())"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "language_models": {
+ "name": "language_models",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "max_output_tokens": {
+ "name": "max_output_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "context_window": {
+ "name": "context_window",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch())"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch())"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "language_models_provider_id_language_model_providers_id_fk": {
+ "name": "language_models_provider_id_language_model_providers_id_fk",
+ "tableFrom": "language_models",
+ "tableTo": "language_model_providers",
+ "columnsFrom": [
+ "provider_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "messages": {
+ "name": "messages",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "approval_state": {
+ "name": "approval_state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "commit_hash": {
+ "name": "commit_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch())"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "messages_chat_id_chats_id_fk": {
+ "name": "messages_chat_id_chats_id_fk",
+ "tableFrom": "messages",
+ "tableTo": "chats",
+ "columnsFrom": [
+ "chat_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 1c3c5a2..9a45e80 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -36,6 +36,13 @@
"when": 1746556241557,
"tag": "0004_flawless_jigsaw",
"breakpoints": true
+ },
+ {
+ "idx": 5,
+ "version": "6",
+ "when": 1747083562867,
+ "tag": "0005_left_thor",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/src/components/ProviderSettings.tsx b/src/components/ProviderSettings.tsx
index 9a32e59..71e8d60 100644
--- a/src/components/ProviderSettings.tsx
+++ b/src/components/ProviderSettings.tsx
@@ -1,4 +1,3 @@
-import { PROVIDERS } from "@/constants/models";
import {
Card,
CardHeader,
@@ -7,42 +6,84 @@ import {
} from "@/components/ui/card";
import { useNavigate } from "@tanstack/react-router";
import { providerSettingsRoute } from "@/routes/settings/providers/$provider";
-import type { ModelProvider } from "@/lib/schemas";
-import { useSettings } from "@/hooks/useSettings";
+import type { LanguageModelProvider } from "@/ipc/ipc_types";
+
+import { useLanguageModelProviders } from "@/hooks/useLanguageModelProviders";
import { GiftIcon } from "lucide-react";
+import { Skeleton } from "./ui/skeleton";
+import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
+import { AlertTriangle } from "lucide-react";
export function ProviderSettingsGrid() {
const navigate = useNavigate();
- const handleProviderClick = (provider: ModelProvider) => {
+ const {
+ data: providers,
+ isLoading,
+ error,
+ isProviderSetup,
+ } = useLanguageModelProviders();
+
+ const handleProviderClick = (providerId: string) => {
navigate({
to: providerSettingsRoute.id,
- params: { provider },
+ params: { provider: providerId },
});
};
- const { isProviderSetup } = useSettings();
+ if (isLoading) {
+ return (
+
+
AI Providers
+
+ {[1, 2, 3, 4, 5].map((i) => (
+
+
+
+
+
+
+ ))}
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
AI Providers
+
+
+ Error
+
+ Failed to load AI providers: {error.message}
+
+
+
+ );
+ }
return (
AI Providers
- {Object.entries(PROVIDERS).map(([key, provider]) => {
+ {providers?.map((provider: LanguageModelProvider) => {
return (
handleProviderClick(key as ModelProvider)}
+ onClick={() => handleProviderClick(provider.id)}
>
- {provider.displayName}
- {isProviderSetup(key) ? (
+ {provider.name}
+ {isProviderSetup(provider.id) ? (
Ready
) : (
-
+
Needs Setup
)}
diff --git a/src/components/SetupBanner.tsx b/src/components/SetupBanner.tsx
index 1496d26..0d3d34e 100644
--- a/src/components/SetupBanner.tsx
+++ b/src/components/SetupBanner.tsx
@@ -11,7 +11,7 @@ import {
} from "lucide-react";
import { providerSettingsRoute } from "@/routes/settings/providers/$provider";
import { settingsRoute } from "@/routes/settings";
-import { useSettings } from "@/hooks/useSettings";
+
import { useState, useEffect, useCallback } from "react";
import { IpcClient } from "@/ipc/ipc_client";
import {
@@ -24,6 +24,7 @@ import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { NodeSystemInfo } from "@/ipc/ipc_types";
import { usePostHog } from "posthog-js/react";
+import { useLanguageModelProviders } from "@/hooks/useLanguageModelProviders";
type NodeInstallStep =
| "install"
| "waiting-for-continue"
@@ -33,7 +34,8 @@ type NodeInstallStep =
export function SetupBanner() {
const posthog = usePostHog();
const navigate = useNavigate();
- const { isAnyProviderSetup, loading } = useSettings();
+ const { isAnyProviderSetup, isLoading: loading } =
+ useLanguageModelProviders();
const [nodeSystemInfo, setNodeSystemInfo] = useState(
null,
);
diff --git a/src/components/chat/HomeChatInput.tsx b/src/components/chat/HomeChatInput.tsx
index d38129e..86626c5 100644
--- a/src/components/chat/HomeChatInput.tsx
+++ b/src/components/chat/HomeChatInput.tsx
@@ -20,7 +20,7 @@ export function HomeChatInput({
const posthog = usePostHog();
const [inputValue, setInputValue] = useAtom(homeChatInputValueAtom);
const textareaRef = useRef(null);
- const { settings, updateSettings, isAnyProviderSetup } = useSettings();
+ const { settings, updateSettings } = useSettings();
const { isStreaming } = useStreamChat({
hasChatId: false,
}); // eslint-disable-line @typescript-eslint/no-unused-vars
@@ -137,10 +137,7 @@ export function HomeChatInput({
) : (