diff --git a/apps/desktop/src/renderer/components/Welcome.module.css b/apps/desktop/src/renderer/components/Welcome.module.css
index acb5ca75..255c4698 100644
--- a/apps/desktop/src/renderer/components/Welcome.module.css
+++ b/apps/desktop/src/renderer/components/Welcome.module.css
@@ -73,6 +73,7 @@
.card:nth-child(1) { animation-delay: 100ms; }
.card:nth-child(2) { animation-delay: 200ms; }
.card:nth-child(3) { animation-delay: 300ms; }
+.card:nth-child(4) { animation-delay: 400ms; }
.cardTitle {
font-family: var(--font-sans);
@@ -96,7 +97,7 @@
align-items: center;
gap: var(--space-3);
animation: fade-in 400ms ease both;
- animation-delay: 400ms;
+ animation-delay: 500ms;
}
.skip {
@@ -113,3 +114,23 @@
.skip:hover {
color: var(--text-secondary);
}
+
+.hint {
+ font-family: var(--font-sans);
+ font-size: var(--text-sm);
+ color: var(--text-faint);
+ margin: 0;
+ animation: fade-in 400ms ease both;
+ animation-delay: 600ms;
+}
+
+.kbd {
+ display: inline-block;
+ padding: 1px 6px;
+ font-family: var(--font-mono, ui-monospace, monospace);
+ font-size: var(--text-xs);
+ color: var(--text-secondary);
+ background: var(--glass-bg);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-sm);
+}
diff --git a/apps/desktop/src/renderer/components/Welcome.tsx b/apps/desktop/src/renderer/components/Welcome.tsx
index f05d9989..f4de9b04 100644
--- a/apps/desktop/src/renderer/components/Welcome.tsx
+++ b/apps/desktop/src/renderer/components/Welcome.tsx
@@ -27,6 +27,10 @@ const features = [
title: 'Extensible',
desc: 'Plugins, themes, and AI built in.',
},
+ {
+ title: 'Import Your Notes',
+ desc: 'Import from Obsidian, Markdown folders, or other apps.',
+ },
] as const;
export function Welcome({ onComplete }: WelcomeProps) {
@@ -78,9 +82,13 @@ export function Welcome({ onComplete }: WelcomeProps) {
Create Your First Note
+
+
+ Pro tip: Press Cmd+K to open the command palette
+
);
diff --git a/apps/desktop/src/renderer/components/auth/MagicLinkFlow.module.css b/apps/desktop/src/renderer/components/auth/MagicLinkFlow.module.css
index 726c616d..718f29f5 100644
--- a/apps/desktop/src/renderer/components/auth/MagicLinkFlow.module.css
+++ b/apps/desktop/src/renderer/components/auth/MagicLinkFlow.module.css
@@ -193,3 +193,14 @@
flex-direction: column;
gap: 0.75rem;
}
+
+.resendContent {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+}
+
+.spinnerInline {
+ animation: spin 1s linear infinite;
+}
diff --git a/apps/desktop/src/renderer/components/auth/MagicLinkFlow.tsx b/apps/desktop/src/renderer/components/auth/MagicLinkFlow.tsx
index 267f6ea4..cd0d0854 100644
--- a/apps/desktop/src/renderer/components/auth/MagicLinkFlow.tsx
+++ b/apps/desktop/src/renderer/components/auth/MagicLinkFlow.tsx
@@ -4,8 +4,8 @@
* Multi-step dialog for passwordless authentication via email magic link.
*/
-import { useState, useCallback, useEffect, FormEvent } from 'react';
-import { Mail, CheckCircle, AlertCircle, X } from 'lucide-react';
+import { useState, useCallback, useEffect, useRef, FormEvent } from 'react';
+import { Mail, CheckCircle, AlertCircle, X, RefreshCw } from 'lucide-react';
import { useAuthStore } from '../../stores/authStore';
import styles from './MagicLinkFlow.module.css';
@@ -22,6 +22,9 @@ export function MagicLinkFlow({ onSuccess, onCancel }: MagicLinkFlowProps) {
const [email, setEmail] = useState('');
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
+ const [resendCooldown, setResendCooldown] = useState(0);
+ const [isResending, setIsResending] = useState(false);
+ const cooldownRef = useRef | null>(null);
// Watch for auth success (deep link verified in background)
useEffect(() => {
@@ -64,8 +67,55 @@ export function MagicLinkFlow({ onSuccess, onCancel }: MagicLinkFlowProps) {
const handleRetry = useCallback(() => {
setStep('email');
setError(null);
+ setResendCooldown(0);
+ if (cooldownRef.current) {
+ clearInterval(cooldownRef.current);
+ cooldownRef.current = null;
+ }
+ }, []);
+
+ const startCooldown = useCallback(() => {
+ setResendCooldown(60);
+ if (cooldownRef.current) clearInterval(cooldownRef.current);
+ cooldownRef.current = setInterval(() => {
+ setResendCooldown(prev => {
+ if (prev <= 1) {
+ if (cooldownRef.current) clearInterval(cooldownRef.current);
+ cooldownRef.current = null;
+ return 0;
+ }
+ return prev - 1;
+ });
+ }, 1000);
}, []);
+ // Start cooldown when entering "sent" step
+ useEffect(() => {
+ if (step === 'sent') {
+ startCooldown();
+ }
+ return () => {
+ if (cooldownRef.current) {
+ clearInterval(cooldownRef.current);
+ cooldownRef.current = null;
+ }
+ };
+ }, [step, startCooldown]);
+
+ const handleResend = useCallback(async () => {
+ if (resendCooldown > 0 || isResending) return;
+ setIsResending(true);
+ setError(null);
+ try {
+ await requestMagicLink(email);
+ startCooldown();
+ } catch {
+ setError('Failed to resend magic link. Please try again.');
+ } finally {
+ setIsResending(false);
+ }
+ }, [email, requestMagicLink, resendCooldown, isResending, startCooldown]);
+
return (
e.stopPropagation()}>
@@ -122,6 +172,21 @@ export function MagicLinkFlow({ onSuccess, onCancel }: MagicLinkFlowProps) {
{error &&
{error}
}
+
diff --git a/apps/desktop/src/renderer/pages/settings/sections/AiSection.tsx b/apps/desktop/src/renderer/pages/settings/sections/AiSection.tsx
index 48f5ed46..ad4b0527 100644
--- a/apps/desktop/src/renderer/pages/settings/sections/AiSection.tsx
+++ b/apps/desktop/src/renderer/pages/settings/sections/AiSection.tsx
@@ -334,31 +334,13 @@ export function AiSection() {
{isConnected ? (
/* Connected state */
-
-
-
-
+
+
+
+
-
- Connected to {providerInfo.name}
-
-
+
Connected to {providerInfo.name}
+
API key stored securely in your system keychain
@@ -375,32 +357,12 @@ export function AiSection() {
) : (
/* Not connected — show connect flow */
-
-
+
+
{currentProvider !== 'ollama' && (
<>
-
-
+
+
Connect your {providerInfo.name} account
-
+
{
if (e.key === 'Enter') void handleConnect();
}}
@@ -452,8 +405,8 @@ export function AiSection() {
)}
{currentProvider === 'ollama' && (
-
-
+
+
Ollama runs locally — no API key needed. Make sure Ollama is running on your
machine.
@@ -470,19 +423,7 @@ export function AiSection() {
)}
{connectError && (
-
+
{connectError}
@@ -548,7 +489,7 @@ export function AiSection() {
{presetMessage?.type === 'success' && (
-
+
{presetMessage.text}
@@ -557,7 +498,7 @@ export function AiSection() {
{presetMessage?.type === 'error' && (
-
+
{presetMessage.text}
@@ -565,34 +506,15 @@ export function AiSection() {
)}
{registeredAiCommands.length > 0 && (
-
-
+
+
Registered AI Commands ({registeredAiCommands.length})
-
+
{registeredAiCommands.map(cmd => (
-
-
{cmd.name}
-
- {cmd.pluginId}
-
+
+ {cmd.name}
+ {cmd.pluginId}
))}
diff --git a/apps/desktop/src/renderer/pages/settings/sections/Section.module.css b/apps/desktop/src/renderer/pages/settings/sections/Section.module.css
index 3a817652..89f78d1c 100644
--- a/apps/desktop/src/renderer/pages/settings/sections/Section.module.css
+++ b/apps/desktop/src/renderer/pages/settings/sections/Section.module.css
@@ -905,6 +905,147 @@
background: rgba(239, 68, 68, 0.1);
}
+/* ============================================================================
+ AI Section — Connection States
+ ============================================================================ */
+
+.aiConnectionWrapper {
+ padding: 1rem 1.25rem;
+}
+
+.aiConnectedBox {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem 1.25rem;
+ background: var(--success-muted);
+ border: 1px solid color-mix(in srgb, var(--success) 30%, transparent);
+ border-radius: 0.75rem;
+}
+
+.aiConnectedInfo {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.aiConnectedIcon {
+ color: var(--success);
+}
+
+.aiConnectedTitle {
+ font-weight: 600;
+ font-size: 0.875rem;
+ color: var(--text-primary);
+}
+
+.aiConnectedSubtitle {
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ margin-top: 2px;
+}
+
+.aiConnectBox {
+ padding: 1.25rem;
+ background: var(--bg-hover);
+ border: 1px solid var(--border-subtle);
+ border-radius: 0.75rem;
+}
+
+.aiConnectHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 0.75rem;
+}
+
+.aiConnectLabel {
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: var(--text-primary);
+}
+
+.aiKeyInputRow {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.aiKeyInput {
+ flex: 1;
+ padding: 0.625rem 0.875rem;
+ background: var(--bg-base);
+ border: 1px solid var(--border);
+ border-radius: 0.5rem;
+ color: var(--text-primary);
+ font-size: 0.875rem;
+ font-family: monospace;
+}
+
+.aiOllamaInfo {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.aiOllamaDescription {
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+}
+
+.aiErrorBox {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-top: 0.5rem;
+ padding: 0.5rem 0.75rem;
+ background: var(--danger-muted, rgba(239, 68, 68, 0.08));
+ border-radius: 0.5rem;
+ font-size: 0.8125rem;
+ color: var(--danger, #ef4444);
+}
+
+.aiCommandListWrapper {
+ padding: 0.75rem 1rem;
+}
+
+.aiCommandListTitle {
+ font-size: 0.8125rem;
+ color: var(--text-secondary);
+ margin-bottom: 0.5rem;
+}
+
+.aiCommandList {
+ display: flex;
+ flex-direction: column;
+ gap: 0.375rem;
+}
+
+.aiCommandItem {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.5rem 0.75rem;
+ background: var(--bg-hover);
+ border-radius: 0.5rem;
+ font-size: 0.8125rem;
+}
+
+.aiCommandName {
+ color: var(--text-primary);
+}
+
+.aiCommandPlugin {
+ color: var(--text-tertiary);
+ font-size: 0.75rem;
+}
+
+.aiMessageIcon {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
.deviceActions {
padding: 0.5rem 0.75rem 0;
}