diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml
index b9a129f2..63ef8902 100644
--- a/.github/workflows/electron.yml
+++ b/.github/workflows/electron.yml
@@ -69,6 +69,7 @@ jobs:
- name: Build Electron app
env:
+ GH_TOKEN: ${{ secrets.TOKEN }}
NODE_OPTIONS: "--max_old_space_size=4096"
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
@@ -115,7 +116,6 @@ jobs:
tag_name: ${{ github.ref_name }}
draft: false
name: Release ${{ github.ref_name }}
- fail_on_unmatched_files: false
files: |
artifacts/**/*.exe
artifacts/**/*.dmg
@@ -123,7 +123,6 @@ jobs:
artifacts/**/*.AppImage
artifacts/**/*.zip
artifacts/**/*.blockmap
- artifacts/**/latest.yml
- artifacts/**/latest-*.yml
+ artifacts/**/*.yml
env:
- GITHUB_TOKEN: ${{ secrets.TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.TOKEN }}
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index 9a48e763..77703be1 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2025 CodinIT.dev
+Copyright (c) 2026 CodinIT.dev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 351e8e4a..c3eb2cc0 100644
--- a/README.md
+++ b/README.md
@@ -48,12 +48,18 @@ Whether you are prototyping, scaling a SaaS product, or experimenting with local
## Quick Start
+### Run as a Desktop App
+
+Download the latest prebuilt release for macOS, Windows, and Linux.
+
+[Download Latest Release](https://github.com/codinit-dev/codinit-dev/releases/latest)
+
Get up and running in minutes.
### 1. Clone the Repository
```bash
-git clone [https://github.com/codinit-dev/codinit-dev.git](https://github.com/codinit-dev/codinit-dev.git)
+git clone https://github.com/codinit-dev/codinit-dev.git
cd codinit-dev
```
@@ -120,8 +126,4 @@ docker compose --profile development up
```
-### Run as a Desktop App
-Download the latest prebuilt release for macOS, Windows, and Linux.
-
-[Download Latest Release](https://github.com/codinit-dev/codinit-dev/releases/latest)
diff --git a/__tests__/unit/providers/manager.test.ts b/__tests__/unit/providers/manager.test.ts
index b54ca7ee..6aeb0f15 100644
--- a/__tests__/unit/providers/manager.test.ts
+++ b/__tests__/unit/providers/manager.test.ts
@@ -1,4 +1,4 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { LLMManager } from '~/lib/modules/llm/manager';
import { mockOpenAIModels, mockApiKeys } from '../../fixtures/api-responses';
@@ -9,6 +9,15 @@ describe('LLMManager', () => {
// Reset the singleton instance for each test
(LLMManager as any)._instance = null;
manager = LLMManager.getInstance();
+
+ // Silence console for cleaner test output
+ vi.spyOn(console, 'log').mockImplementation(() => { });
+ vi.spyOn(console, 'error').mockImplementation(() => { });
+ vi.spyOn(console, 'warn').mockImplementation(() => { });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
});
describe('getInstance', () => {
diff --git a/__tests__/unit/runtime/code-validator.test.ts b/__tests__/unit/runtime/code-validator.test.ts
index 07f0710d..01749903 100644
--- a/__tests__/unit/runtime/code-validator.test.ts
+++ b/__tests__/unit/runtime/code-validator.test.ts
@@ -1,15 +1,15 @@
-import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { validateCode } from '~/lib/runtime/code-validator';
describe('code-validator', () => {
- beforeAll(() => {
+ beforeEach(() => {
vi.spyOn(console, 'log').mockImplementation(() => undefined);
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
vi.spyOn(console, 'debug').mockImplementation(() => undefined);
vi.spyOn(console, 'error').mockImplementation(() => undefined);
});
- afterAll(() => {
+ afterEach(() => {
vi.restoreAllMocks();
});
describe('validateCode - General', () => {
diff --git a/__tests__/unit/runtime/message-parser.test.ts b/__tests__/unit/runtime/message-parser.test.ts
index d6caa858..50aca263 100644
--- a/__tests__/unit/runtime/message-parser.test.ts
+++ b/__tests__/unit/runtime/message-parser.test.ts
@@ -1,4 +1,4 @@
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
function cleanoutMarkdownSyntax(content: string) {
const codeBlockRegex = /^\s*```[\w-]*\s*\n?([\s\S]*?)\n?\s*```\s*$/;
@@ -18,6 +18,15 @@ function cleanoutMarkdownSyntax(content: string) {
}
describe('message-parser - cleanoutMarkdownSyntax', () => {
+ beforeEach(() => {
+ vi.spyOn(console, 'log').mockImplementation(() => { });
+ vi.spyOn(console, 'error').mockImplementation(() => { });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
describe('Basic markdown removal', () => {
it('should remove markdown code block with language', () => {
const input = '```javascript\nconst x = 1;\n```';
diff --git a/__tests__/unit/server/llm-utils.test.ts b/__tests__/unit/server/llm-utils.test.ts
index b80107ad..14f887d6 100644
--- a/__tests__/unit/server/llm-utils.test.ts
+++ b/__tests__/unit/server/llm-utils.test.ts
@@ -1,4 +1,4 @@
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
extractFileReferences,
createReferencedFilesContext,
@@ -12,6 +12,15 @@ import type { FileMap } from '~/lib/.server/llm/constants';
import type { Message } from 'ai';
describe('LLM Utils - File References', () => {
+ beforeEach(() => {
+ vi.spyOn(console, 'log').mockImplementation(() => { });
+ vi.spyOn(console, 'error').mockImplementation(() => { });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
describe('extractFileReferences', () => {
it('should extract single file reference', () => {
const text = 'Can you check @src/index.ts?';
diff --git a/__tests__/unit/storage.test.ts b/__tests__/unit/storage.test.ts
new file mode 100644
index 00000000..1d08e7c3
--- /dev/null
+++ b/__tests__/unit/storage.test.ts
@@ -0,0 +1,75 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { initializeProject, saveFileLocal, isElectron } from '~/utils/electron';
+
+describe('Storage System', () => {
+ beforeEach(() => {
+ // Reset global window object mocks
+ (global as any).window = {
+ electronAPI: {
+ initializeProject: vi.fn(),
+ saveFileLocal: vi.fn(),
+ },
+ };
+
+ // Silence console.error for expected failures
+ vi.spyOn(console, 'error').mockImplementation(() => { });
+ });
+
+ describe('isElectron', () => {
+ it('should return true when electronAPI is available', () => {
+ expect(isElectron()).toBe(true);
+ });
+
+ it('should return false when window is undefined', () => {
+ const originalWindow = global.window;
+ delete (global as any).window;
+ expect(isElectron()).toBe(false);
+ global.window = originalWindow;
+ });
+ });
+
+ describe('initializeProject', () => {
+ it('should call electronAPI.initializeProject with projectName', async () => {
+ const projectName = 'test-project';
+ const mockInit = vi.spyOn((window as any).electronAPI, 'initializeProject').mockResolvedValue(true);
+
+ const result = await initializeProject(projectName);
+
+ expect(result).toBe(true);
+ expect(mockInit).toHaveBeenCalledWith(projectName);
+ });
+
+ it('should return false if initializeProject fails', async () => {
+ const projectName = 'test-project';
+ vi.spyOn((window as any).electronAPI, 'initializeProject').mockRejectedValue(new Error('Failed'));
+
+ const result = await initializeProject(projectName);
+
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('saveFileLocal', () => {
+ it('should call electronAPI.saveFileLocal with correct arguments', async () => {
+ const projectName = 'test-project';
+ const filePath = 'src/test.ts';
+ const content = 'test content';
+ const mockSave = vi.spyOn((window as any).electronAPI, 'saveFileLocal').mockResolvedValue(true);
+
+ const result = await saveFileLocal(projectName, filePath, content);
+
+ expect(result).toBe(true);
+ expect(mockSave).toHaveBeenCalledWith(projectName, filePath, content);
+ });
+
+ it('should handle Uint8Array content', async () => {
+ const content = new Uint8Array([1, 2, 3]);
+ const mockSave = vi.spyOn((window as any).electronAPI, 'saveFileLocal').mockResolvedValue(true);
+
+ const result = await saveFileLocal('project', 'file', content);
+
+ expect(result).toBe(true);
+ expect(mockSave).toHaveBeenCalledWith('project', 'file', content);
+ });
+ });
+});
diff --git a/__tests__/unit/utils/diff.test.ts b/__tests__/unit/utils/diff.test.ts
index bec68ffe..71efc7b3 100644
--- a/__tests__/unit/utils/diff.test.ts
+++ b/__tests__/unit/utils/diff.test.ts
@@ -1,9 +1,18 @@
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { extractRelativePath, computeFileModifications, diffFiles, fileModificationsToHTML } from '~/utils/diff';
import { WORK_DIR } from '~/utils/constants';
import type { FileMap } from '~/lib/stores/files';
describe('extractRelativePath', () => {
+ beforeEach(() => {
+ vi.spyOn(console, 'log').mockImplementation(() => { });
+ vi.spyOn(console, 'error').mockImplementation(() => { });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
it('should strip out WORK_DIR from file paths', () => {
const filePath = `${WORK_DIR}/src/components/Button.tsx`;
const result = extractRelativePath(filePath);
diff --git a/__tests__/unit/utils/toolMentionParser.test.ts b/__tests__/unit/utils/toolMentionParser.test.ts
index f3b709b4..4022a0f3 100644
--- a/__tests__/unit/utils/toolMentionParser.test.ts
+++ b/__tests__/unit/utils/toolMentionParser.test.ts
@@ -1,4 +1,4 @@
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
shouldShowAutocomplete,
detectReferenceType,
@@ -7,6 +7,15 @@ import {
} from '~/utils/toolMentionParser';
describe('toolMentionParser', () => {
+ beforeEach(() => {
+ vi.spyOn(console, 'log').mockImplementation(() => { });
+ vi.spyOn(console, 'error').mockImplementation(() => { });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
describe('shouldShowAutocomplete', () => {
it('should return isOpen=true when @ is at start of text', () => {
const result = shouldShowAutocomplete('@', 1);
diff --git a/app/components/@settings/tabs/connections/ConnectionsTab.tsx b/app/components/@settings/tabs/connections/ConnectionsTab.tsx
index 1e44eb38..cc732060 100644
--- a/app/components/@settings/tabs/connections/ConnectionsTab.tsx
+++ b/app/components/@settings/tabs/connections/ConnectionsTab.tsx
@@ -1,6 +1,5 @@
import { motion } from 'framer-motion';
import React, { Suspense, useState } from 'react';
-import { classNames } from '~/utils/classNames';
import ConnectionDiagnostics from './ConnectionDiagnostics';
import { Button } from '~/components/ui/Button';
import VercelConnection from './VercelConnection';
@@ -21,7 +20,6 @@ const LoadingFallback = () => (
);
export default function ConnectionsTab() {
- const [isEnvVarsExpanded, setIsEnvVarsExpanded] = useState(false);
const [showDiagnostics, setShowDiagnostics] = useState(false);
return (
@@ -64,92 +62,6 @@ export default function ConnectionsTab() {
{/* Diagnostics Tool - Conditionally rendered */}
{showDiagnostics && }
- {/* Environment Variables Info - Collapsible */}
-
-
-
-
- {isEnvVarsExpanded && (
-
-
- You can configure connections using environment variables in your{' '}
-
- .env.local
- {' '}
- file:
-
-
-
- # GitHub Authentication
-
-
- VITE_GITHUB_ACCESS_TOKEN=your_token_here
-
-
- # Optional: Specify token type (defaults to 'classic' if not specified)
-
-
- VITE_GITHUB_TOKEN_TYPE=classic|fine-grained
-
-
- # Netlify Authentication
-
-
- VITE_NETLIFY_ACCESS_TOKEN=your_token_here
-
-
-
-
- Token types:
-
-
- -
- classic - Personal Access Token with{' '}
-
- repo, read:org, read:user
- {' '}
- scopes
-
- -
- fine-grained - Fine-grained token with Repository and
- Organization access
-
-
-
- When set, these variables will be used automatically without requiring manual connection.
-
-
-
- )}
-
-
-
}>
diff --git a/app/components/@settings/tabs/connections/components/ConnectionForm.tsx b/app/components/@settings/tabs/connections/components/ConnectionForm.tsx
deleted file mode 100644
index 01293b39..00000000
--- a/app/components/@settings/tabs/connections/components/ConnectionForm.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-import React, { useEffect } from 'react';
-import { classNames } from '~/utils/classNames';
-import type { GitHubAuthState } from '~/components/@settings/tabs/connections/types/GitHub';
-import Cookies from 'js-cookie';
-import { getLocalStorage } from '~/lib/persistence';
-
-const GITHUB_TOKEN_KEY = 'github_token';
-
-interface ConnectionFormProps {
- authState: GitHubAuthState;
- setAuthState: React.Dispatch
>;
- onSave: (e: React.FormEvent) => void;
- onDisconnect: () => void;
-}
-
-export function ConnectionForm({ authState, setAuthState, onSave, onDisconnect }: ConnectionFormProps) {
- // Check for saved token on mount
- useEffect(() => {
- const savedToken = Cookies.get(GITHUB_TOKEN_KEY) || Cookies.get('githubToken') || getLocalStorage(GITHUB_TOKEN_KEY);
-
- if (savedToken && !authState.tokenInfo?.token) {
- setAuthState((prev: GitHubAuthState) => ({
- ...prev,
- tokenInfo: {
- token: savedToken,
- scope: [],
- avatar_url: '',
- name: null,
- created_at: new Date().toISOString(),
- followers: 0,
- },
- }));
-
- // Ensure the token is also saved with the correct key for API requests
- Cookies.set('githubToken', savedToken);
- }
- }, []);
-
- return (
-
-
-
-
-
-
-
Connection Settings
-
Configure your GitHub connection
-
-
-
-
-
-
-
- );
-}
diff --git a/app/components/@settings/tabs/settings/SettingsTab.tsx b/app/components/@settings/tabs/settings/SettingsTab.tsx
index 26cf14b6..921a753e 100644
--- a/app/components/@settings/tabs/settings/SettingsTab.tsx
+++ b/app/components/@settings/tabs/settings/SettingsTab.tsx
@@ -1,7 +1,9 @@
import { useState } from 'react';
import { toast } from 'react-toastify';
+import { motion } from 'framer-motion';
import { classNames } from '~/utils/classNames';
-import { SettingsSection } from '~/components/@settings/shared/components/SettingsCard';
+import { SettingsSection, SettingsCard } from '~/components/@settings/shared/components/SettingsCard';
+import { Badge } from '~/components/ui/Badge';
type ProjectVisibility = 'private' | 'secret' | 'public';
type AgentType = 'claude' | 'codex' | 'v1';
@@ -25,190 +27,282 @@ export default function SettingsTab() {
};
return (
-
-
- {/* Project Name */}
-
-
-
- setProjectName(e.target.value)}
- placeholder="Enter project name"
- className={classNames(
- 'flex-1 px-4 py-2.5 rounded-lg text-sm',
- 'bg-codinit-elements-background-depth-1 border border-codinit-elements-borderColor',
- 'text-codinit-elements-textPrimary placeholder-gray-500',
- 'focus:outline-none focus:border-blue-500/50 focus:ring-1 focus:ring-blue-500/20',
- 'transition-all duration-200',
- )}
- />
-
-
-
-
- {/* Project Agent */}
-
-
-
-
-
-
+
+
+
+ {/* Project Name Card */}
+
+
-
Codex
-
- Coming Soon
-
+
+
+
+
+
+
setProjectName(e.target.value)}
+ placeholder="Enter project name..."
+ className={classNames(
+ 'w-full px-4 py-3 rounded-xl text-sm transition-all duration-300',
+ 'bg-white dark:bg-black/20 border border-codinit-elements-borderColor',
+ 'text-codinit-elements-textPrimary placeholder-gray-400 dark:placeholder-gray-600',
+ 'focus:outline-none focus:border-blue-500/50 focus:ring-4 focus:ring-blue-500/10',
+ 'group-hover:border-codinit-elements-borderColorActive',
+ )}
+ />
+
+
+
+
+ This name will be used across the platform to identify your work.
+
+
-
-
-
-
- {/* Context */}
-
-
-
-
-
-
- Free up context. This is useful when a part of your app is completed and you want to work on a new one.
-
-
+ {/* Agent Selection Section */}
+
+
+
+
+
+ AI Intelligence Agent
+
+
+
+ PRO FEATURE
+
+
- {/* Project Visibility */}
-
-
-
-
+
+
+ {agent.badge && (
+
+ {agent.badge}
+
+ )}
+ {agent.comingSoon && (
+
+ COMING SOON
+
+ )}
+
+
+ {agent.label}
+
+
{agent.desc}
-
setVisibility('secret')}
- className={classNames(
- 'p-4 rounded-xl text-left transition-all duration-200 border',
- visibility === 'secret'
- ? 'bg-blue-500/10 border-blue-500/50'
- : 'bg-codinit-elements-background-depth-1 border-codinit-elements-borderColor hover:border-codinit-elements-borderColorActive',
- )}
- >
-
+
+ {/* Visibility Section */}
+
+
+
+
+ Project Access & Visibility
+
+
+
+
+ {[
+ {
+ id: 'private',
+ label: 'Private',
+ desc: 'Secure vault for your eyes only.',
+ icon: 'i-ph:lock-key-duotone',
+ color: 'text-rose-500',
+ recommended: true,
+ },
+ {
+ id: 'secret',
+ label: 'Secret',
+ desc: 'Invisible except with a direct link.',
+ icon: 'i-ph:shield-check-duotone',
+ color: 'text-amber-500',
+ },
+ {
+ id: 'public',
+ label: 'Public',
+ desc: 'Open source for the entire world.',
+ icon: 'i-ph:globe-hemisphere-east-duotone',
+ color: 'text-emerald-500',
+ },
+ ].map((option) => (
+ setVisibility(option.id as ProjectVisibility)}
className={classNames(
- 'font-medium text-sm',
- visibility === 'secret' ? 'text-blue-400' : 'text-codinit-elements-textPrimary',
+ 'relative group p-5 rounded-2xl border transition-all duration-300 text-left overflow-hidden h-full',
+ visibility === option.id
+ ? 'bg-blue-500/5 border-blue-500/50 dark:bg-blue-500/10'
+ : 'bg-white dark:bg-black/20 border-codinit-elements-borderColor hover:border-blue-300 dark:hover:border-blue-800',
)}
>
- Secret
-
-
-
Accessible via shared URL
-
-
-
setVisibility('public')}
- className={classNames(
- 'p-4 rounded-xl text-left transition-all duration-200 border',
- visibility === 'public'
- ? 'bg-blue-500/10 border-blue-500/50'
- : 'bg-codinit-elements-background-depth-1 border-codinit-elements-borderColor hover:border-codinit-elements-borderColorActive',
- )}
- >
-
-
+
+
+
+
+ {option.label}
+
+ {option.recommended && (
+
+ TOP CHOICE
+
+ )}
+
+
{option.desc}
+
+
+ {visibility === option.id && (
+
)}
- />
-
+ ))}
+
+
+
+
+ Looking to share your site privately? Use the{' '}
+
+ Share
+ {' '}
+ control in the header for temporary access.
+
+
+
+
+ {/* Context Management Card */}
+
+
+
+
+
- Public
-
+ Clear Environment Context
+
-
Everyone can view
-
-
-
-
-
- Looking to share your site privately? Click{' '}
- Share in the top-right corner of your
- screen.
-
-
+
+
+
Purge Active Context
+
+ Instantly free up AI memory. Highly recommended when shifting focus to a new feature or after a
+ major component is finished.
+
+
+
+
+
+
+ Warning: This action cannot be undone
+
+
+
diff --git a/app/components/@settings/tabs/update/UpdateTab.tsx b/app/components/@settings/tabs/update/UpdateTab.tsx
index 9a1b7c08..9b9f35b4 100644
--- a/app/components/@settings/tabs/update/UpdateTab.tsx
+++ b/app/components/@settings/tabs/update/UpdateTab.tsx
@@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react';
-import { motion } from 'framer-motion';
+import { motion, AnimatePresence } from 'framer-motion';
import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck';
-import { toast } from 'react-toastify';
-import { Dialog, DialogRoot, DialogTitle, DialogDescription, DialogButton } from '~/components/ui/Dialog';
+import Cookies from 'js-cookie';
import { classNames } from '~/utils/classNames';
import { Markdown } from '~/components/chat/Markdown';
import { GITHUB_REPOSITORY } from '~/lib/version';
+import { SettingsSection, SettingsCard } from '~/components/@settings/shared/components/SettingsCard';
+import { Button } from '~/components/ui/Button';
interface UpdateSettings {
autoUpdate: boolean;
@@ -25,7 +26,6 @@ const UpdateTab = () => {
downloadProgress,
isReadyToInstall,
error,
- acknowledgeUpdate,
manualCheck,
downloadAndInstall,
quitAndInstall,
@@ -42,343 +42,244 @@ const UpdateTab = () => {
checkInterval: 24,
};
});
- const [showUpdateDialog, setShowUpdateDialog] = useState(false);
+
+ useEffect(() => {
+ /*
+ * if (hasUpdate && !isDownloading && !isReadyToInstall) {
+ * setShowUpdateDialog(true);
+ * }
+ */
+ }, [hasUpdate, isDownloading, isReadyToInstall]);
useEffect(() => {
localStorage.setItem('update_settings', JSON.stringify(updateSettings));
}, [updateSettings]);
- const handleUpdate = () => {
- if (isElectron) {
- downloadAndInstall();
-
- // Keep dialog open or show new status
- } else {
- if (releaseUrl) {
- window.open(releaseUrl, '_blank');
- }
- }
-
- acknowledgeUpdate();
- setShowUpdateDialog(false);
- toast.success('Update process started');
- };
-
return (
-
-
-
-
-
Updates
-
Check for and manage application updates
-
-
-
- {/* Update Settings Card */}
-
-
-
-
-
-
-
Automatic Updates
-
- Automatically check and apply updates when available
-
-
-
setUpdateSettings((prev) => ({ ...prev, autoUpdate: !prev.autoUpdate }))}
- className={classNames(
- 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
- updateSettings.autoUpdate ? 'bg-blue-500' : 'bg-gray-200 dark:bg-gray-700',
- )}
- >
-
-
-
+
+
+
+ {/* Main Status Dashboard */}
+
+
+
+
+
+
+
+
+
+ Connected
+
+
-
-
-
In-App Notifications
-
- Show notifications when updates are available
-
-
-
setUpdateSettings((prev) => ({ ...prev, notifyInApp: !prev.notifyInApp }))}
- className={classNames(
- 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
- updateSettings.notifyInApp ? 'bg-blue-500' : 'bg-gray-200 dark:bg-gray-700',
- )}
- >
-
-
-
+
+
-
-
-
Check Interval
-
How often to check for updates
-
-
-
-
-
+
+
+ Installed Version
+
+
+ v{currentVersion}
+
+
- {/* Update Status Card */}
-
-
-
-
- {isReadyToInstall ? (
-
-
- Restart & Install
-
- ) : isDownloading ? (
-
-
- Downloading... {Math.round(downloadProgress)}%
+
+
+ Latest Release
+
+
+ v{latestVersion || currentVersion}
+
+
- ) : (
- hasUpdate && (
-
(isElectron ? downloadAndInstall() : setShowUpdateDialog(true))}
- className={classNames(
- 'flex items-center gap-2 px-4 py-2 rounded-lg text-sm',
- 'bg-blue-500 text-white',
- 'hover:bg-blue-600',
- 'transition-colors duration-200',
- )}
- >
-
- {isElectron ? 'Download Update' : 'View Update'}
-
- )
- )}
- {!isDownloading && !isReadyToInstall && (
-
- {isLoading ? (
-
-
- Checking...
+
+
+
+
+
+
Updates tracked via {GITHUB_REPOSITORY}
+
GitHub
- ) : error?.includes('rate limit') ? (
- <>
-
- Rate Limited
- >
- ) : (
- <>
-
- Refresh Check
- >
- )}
-
- )}
-
-
- {/* Show current version and update status */}
-
-
-
-
- Current version: {currentVersion}
-
- {hasUpdate && latestVersion && (
-
- Latest version: {latestVersion}
-
- )}
-
- Updates are checked from: {GITHUB_REPOSITORY} (GitHub releases)
-
-
- {hasUpdate && releaseUrl && !isElectron && (
-
-
- View Release on GitHub
-
- )}
-
+
+ {isReadyToInstall ? (
+
+
+ Restart to Install
+
+ ) : isDownloading ? (
+
+
+
+
+
{Math.round(downloadProgress)}%
+
+ ) : (
+
+
+
+ {isLoading ? 'Checking...' : 'Refresh'}
+
- {/* Show release notes if available */}
- {hasUpdate && releaseNotes && (
-
-
-
-
-
{releaseNotes}
+ {(hasUpdate || Cookies.get('codinit_update_snooze') === latestVersion) && (
+
(isElectron ? downloadAndInstall() : window.open(releaseUrl, '_blank'))}
+ className="bg-blue-600 text-white hover:bg-blue-700 shadow-lg shadow-blue-500/20"
+ >
+
+ Update Now
+
+ )}
+
+ )}
+
-
- )}
-
+
- {error && (
-
-
-
-
Update Check Failed
-
-
{error}
- {error.includes('rate limit') && (
-
- 💡 Tip: GitHub limits unauthenticated API requests. The limit resets hourly.
-
- )}
+ {/* Release Notes Explorer - Integrated Card */}
+
+ {(hasUpdate || Cookies.get('codinit_update_snooze') === latestVersion) && releaseNotes && (
+
+
+
+
+
+
+
+ )}
+
- )}
-
- {/* Update dialog */}
-
-