diff --git a/apps/mail/components/keyboard-layout-indicator.tsx b/apps/mail/components/keyboard-layout-indicator.tsx
new file mode 100644
index 000000000..0cc7ea11b
--- /dev/null
+++ b/apps/mail/components/keyboard-layout-indicator.tsx
@@ -0,0 +1,60 @@
+/**
+ * Keyboard Layout Indicator Component
+ * Shows the current detected keyboard layout and confidence
+ */
+
+import { keyboardLayoutMapper, type LayoutDetectionResult } from '@/utils/keyboard-layout-map';
+import { KeyboardIcon } from 'lucide-react';
+import { useEffect, useState } from 'react';
+
+export function KeyboardLayoutIndicator() {
+ const layoutInfo = useKeyboardLayout();
+
+ if (!layoutInfo || layoutInfo.layout === 'unknown') {
+ return null;
+ }
+
+ const getLayoutDisplayName = (layout: string) => {
+ const names = {
+ qwerty: 'QWERTY',
+ dvorak: 'Dvorak',
+ colemak: 'Colemak',
+ azerty: 'AZERTY',
+ qwertz: 'QWERTZ',
+ };
+ return names[layout as keyof typeof names] || layout.toUpperCase();
+ };
+
+ return (
+
+
+ {getLayoutDisplayName(layoutInfo.layout)}
+
+ );
+}
+
+export function useKeyboardLayout() {
+ const [layoutInfo, setLayoutInfo] = useState(null);
+
+ useEffect(() => {
+ const updateLayoutInfo = () => {
+ const info = keyboardLayoutMapper.getDetectedLayout();
+ setLayoutInfo(info);
+ console.log('Detected keyboard layout:', info);
+ };
+
+ updateLayoutInfo();
+
+ const handleFocus = () => {
+ setTimeout(() => {
+ updateLayoutInfo();
+ console.log('Window focused, updated keyboard layout');
+ }, 100);
+ };
+
+ window.addEventListener('focus', handleFocus);
+ return () => window.removeEventListener('focus', handleFocus);
+ }, []);
+
+ return layoutInfo;
+}
diff --git a/apps/mail/components/mail/mail.tsx b/apps/mail/components/mail/mail.tsx
index 082ec0914..bf333cdd4 100644
--- a/apps/mail/components/mail/mail.tsx
+++ b/apps/mail/components/mail/mail.tsx
@@ -13,8 +13,8 @@ import {
DropdownMenuContent,
DropdownMenuTrigger,
} from '../ui/dropdown-menu';
-import { Bell, Lightning, Mail, ScanEye, Tag, Trash, User, X, Search } from '../icons/icons';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
+import { Bell, Lightning, Mail, ScanEye, Tag, Trash, User, X, Search } from '../icons/icons';
import { useCategorySettings, useDefaultCategoryId } from '@/hooks/use-categories';
import { ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';
import React, { useCallback, useEffect, useRef, useState } from 'react';
diff --git a/apps/mail/components/party.tsx b/apps/mail/components/party.tsx
index d17f2af2e..bd8e0c92a 100644
--- a/apps/mail/components/party.tsx
+++ b/apps/mail/components/party.tsx
@@ -5,8 +5,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { useTRPC } from '@/providers/query-provider';
import { usePartySocket } from 'partysocket/react';
-
- // 10 seconds is appropriate for real-time notifications
+// 10 seconds is appropriate for real-time notifications
export enum IncomingMessageType {
UseChatRequest = 'cf_agent_use_chat_request',
@@ -32,9 +31,6 @@ export const NotificationProvider = () => {
const [searchValue] = useSearchValue();
const { labels } = useSearchLabels();
-
-
-
usePartySocket({
party: 'zero-agent',
room: activeConnection?.id ? String(activeConnection.id) : 'general',
diff --git a/apps/mail/components/voice-button.tsx b/apps/mail/components/voice-button.tsx
index 9e10fdbb7..f559c29f4 100644
--- a/apps/mail/components/voice-button.tsx
+++ b/apps/mail/components/voice-button.tsx
@@ -1,5 +1,3 @@
-'use client';
-
import { Mic, MicOff, Loader2, WavesIcon } from 'lucide-react';
import { useVoice } from '@/providers/voice-provider';
import { motion } from 'motion/react';
diff --git a/apps/mail/config/shortcuts.ts b/apps/mail/config/shortcuts.ts
index 99dd4dd5e..4f7ebdd45 100644
--- a/apps/mail/config/shortcuts.ts
+++ b/apps/mail/config/shortcuts.ts
@@ -1,4 +1,6 @@
import { z } from 'zod';
+import { keyboardLayoutMapper } from '../utils/keyboard-layout-map';
+import { getKeyCodeFromKey } from '../utils/keyboard-utils';
export const shortcutSchema = z.object({
keys: z.array(z.string()),
@@ -13,6 +15,70 @@ export const shortcutSchema = z.object({
export type Shortcut = z.infer;
export type ShortcutType = Shortcut['type'];
+/**
+ * Enhanced shortcut type with keyboard layout mapping support
+ */
+export interface EnhancedShortcut extends Shortcut {
+ mappedKeys?: string[];
+ displayKeys?: string[];
+}
+
+/**
+ * Convert key codes to user-friendly display keys using keyboard layout mapping
+ */
+export function getDisplayKeysForShortcut(shortcut: Shortcut): string[] {
+ const detectedLayout = keyboardLayoutMapper.getDetectedLayout();
+
+ return shortcut.keys.map(key => {
+ // Handle special modifiers first
+ switch (key.toLowerCase()) {
+ case 'mod':
+ return navigator.platform.includes('Mac') ? '⌘' : 'Ctrl';
+ case 'meta':
+ return '⌘';
+ case 'ctrl':
+ case 'control':
+ return 'Ctrl';
+ case 'alt':
+ return navigator.platform.includes('Mac') ? '⌥' : 'Alt';
+ case 'shift':
+ return '⇧';
+ case 'escape':
+ return 'Esc';
+ case 'backspace':
+ return '⌫';
+ case 'enter':
+ return '↵';
+ case 'space':
+ return 'Space';
+ default:
+ // Use enhanced keyboard layout mapping
+ if (detectedLayout?.layout && detectedLayout.layout !== 'qwerty') {
+ const keyCode = getKeyCodeFromKey(key);
+ const mappedKey = keyboardLayoutMapper.getKeyForCode(keyCode);
+ return mappedKey.length === 1 ? mappedKey.toUpperCase() : mappedKey;
+ }
+ return key.length === 1 ? key.toUpperCase() : key;
+ }
+ });
+}
+
+/**
+ * Convert a key string to its corresponding KeyCode
+ */
+
+
+/**
+ * Enhance shortcuts with keyboard layout mapping
+ */
+export function enhanceShortcutsWithMapping(shortcuts: Shortcut[]): EnhancedShortcut[] {
+ return shortcuts.map(shortcut => ({
+ ...shortcut,
+ displayKeys: getDisplayKeysForShortcut(shortcut),
+ mappedKeys: keyboardLayoutMapper.mapKeys(shortcut.keys.map(getKeyCodeFromKey)),
+ }));
+}
+
const threadDisplayShortcuts: Shortcut[] = [
// {
// keys: ['i'],
@@ -363,3 +429,8 @@ export const keyboardShortcuts: Shortcut[] = [
...mailListShortcuts,
...composeShortcuts,
];
+
+/**
+ * Enhanced keyboard shortcuts with layout mapping
+ */
+export const enhancedKeyboardShortcuts: EnhancedShortcut[] = enhanceShortcutsWithMapping(keyboardShortcuts);
diff --git a/apps/mail/hooks/use-optimistic-actions.ts b/apps/mail/hooks/use-optimistic-actions.ts
index f61806627..93b5a072c 100644
--- a/apps/mail/hooks/use-optimistic-actions.ts
+++ b/apps/mail/hooks/use-optimistic-actions.ts
@@ -8,8 +8,8 @@ import { useTRPC } from '@/providers/query-provider';
import { useMail } from '@/components/mail/use-mail';
import { moveThreadsTo } from '@/lib/thread-actions';
import { m } from '@/paraglide/messages';
-import { useCallback } from 'react';
import { useQueryState } from 'nuqs';
+import { useCallback } from 'react';
import posthog from 'posthog-js';
import { useAtom } from 'jotai';
import { toast } from 'sonner';
@@ -52,25 +52,12 @@ export function useOptimisticActions() {
const generatePendingActionId = () =>
`pending_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
- const refreshData = useCallback(
- async (threadIds: string[], folders?: string[]) => {
- return await Promise.all([
- queryClient.refetchQueries({ queryKey: trpc.mail.count.queryKey() }),
- ...(folders?.map((folder) =>
- queryClient.refetchQueries({
- queryKey: trpc.mail.listThreads.infiniteQueryKey({ folder }),
- }),
- ) ?? []),
- ...threadIds.map((id) =>
- queryClient.refetchQueries({
- queryKey: trpc.mail.get.queryKey({ id }),
- }),
- ),
- queryClient.refetchQueries({ queryKey: trpc.labels.list.queryKey() }),
- ]);
- },
- [queryClient, trpc.mail.get, trpc.labels.list],
- );
+ const refreshData = useCallback(async () => {
+ return await Promise.all([
+ queryClient.refetchQueries({ queryKey: trpc.mail.count.queryKey() }),
+ queryClient.refetchQueries({ queryKey: trpc.labels.list.queryKey() }),
+ ]);
+ }, [queryClient]);
function createPendingAction({
type,
@@ -80,7 +67,6 @@ export function useOptimisticActions() {
execute,
undo,
toastMessage,
- folders,
}: {
type: keyof typeof ActionType;
threadIds: string[];
@@ -141,7 +127,7 @@ export function useOptimisticActions() {
optimisticActionsManager.pendingActions.delete(pendingActionId);
optimisticActionsManager.pendingActionsByType.get(type)?.delete(pendingActionId);
if (typeActions?.size === 1) {
- await refreshData(threadIds, folders);
+ await refreshData();
removeOptimisticAction(optimisticId);
}
} catch (error) {
@@ -149,12 +135,10 @@ export function useOptimisticActions() {
removeOptimisticAction(optimisticId);
optimisticActionsManager.pendingActions.delete(pendingActionId);
optimisticActionsManager.pendingActionsByType.get(type)?.delete(pendingActionId);
- showToast.error('Action failed');
+ toast.error('Action failed');
}
}
- const showToast = toast;
-
if (toastMessage.trim().length) {
toast(bulkActionMessage, {
onAutoClose: () => {
diff --git a/apps/mail/lib/hotkeys/compose-hotkeys.tsx b/apps/mail/lib/hotkeys/compose-hotkeys.tsx
index dc0e0b780..a348fb35e 100644
--- a/apps/mail/lib/hotkeys/compose-hotkeys.tsx
+++ b/apps/mail/lib/hotkeys/compose-hotkeys.tsx
@@ -1,4 +1,4 @@
-import { keyboardShortcuts } from '@/config/shortcuts';
+import { enhancedKeyboardShortcuts } from '@/config/shortcuts';
import { useShortcuts } from './use-hotkey-utils';
import { useQueryState } from 'nuqs';
@@ -14,7 +14,7 @@ export function ComposeHotkeys() {
},
};
- const composeShortcuts = keyboardShortcuts.filter((shortcut) => shortcut.scope === scope);
+ const composeShortcuts = enhancedKeyboardShortcuts.filter((shortcut) => shortcut.scope === scope);
useShortcuts(composeShortcuts, handlers, { scope });
diff --git a/apps/mail/lib/hotkeys/global-hotkeys.tsx b/apps/mail/lib/hotkeys/global-hotkeys.tsx
index b3cb6b394..ae99f779d 100644
--- a/apps/mail/lib/hotkeys/global-hotkeys.tsx
+++ b/apps/mail/lib/hotkeys/global-hotkeys.tsx
@@ -1,6 +1,6 @@
import { useCommandPalette } from '@/components/context/command-palette-context';
import { useOptimisticActions } from '@/hooks/use-optimistic-actions';
-import { keyboardShortcuts } from '@/config/shortcuts';
+import { enhancedKeyboardShortcuts } from '@/config/shortcuts';
import { useShortcuts } from './use-hotkey-utils';
import { useQueryState } from 'nuqs';
@@ -20,7 +20,7 @@ export function GlobalHotkeys() {
},
};
- const globalShortcuts = keyboardShortcuts.filter((shortcut) => shortcut.scope === scope);
+ const globalShortcuts = enhancedKeyboardShortcuts.filter((shortcut) => shortcut.scope === scope);
useShortcuts(globalShortcuts, handlers, { scope });
diff --git a/apps/mail/lib/hotkeys/mail-list-hotkeys.tsx b/apps/mail/lib/hotkeys/mail-list-hotkeys.tsx
index 0b6f36ad8..bdcf6e153 100644
--- a/apps/mail/lib/hotkeys/mail-list-hotkeys.tsx
+++ b/apps/mail/lib/hotkeys/mail-list-hotkeys.tsx
@@ -1,7 +1,7 @@
import { useOptimisticActions } from '@/hooks/use-optimistic-actions';
import { useCallback, useEffect, useMemo, useRef } from 'react';
+import { enhancedKeyboardShortcuts } from '@/config/shortcuts';
import { useSearchValue } from '@/hooks/use-search-value';
-import { keyboardShortcuts } from '@/config/shortcuts';
import { useLocation, useParams } from 'react-router';
import { useMail } from '@/components/mail/use-mail';
import { Categories } from '@/components/mail/mail';
@@ -239,7 +239,9 @@ export function MailListHotkeys() {
],
);
- const mailListShortcuts = keyboardShortcuts.filter((shortcut) => shortcut.scope === scope);
+ const mailListShortcuts = enhancedKeyboardShortcuts.filter(
+ (shortcut) => shortcut.scope === scope,
+ );
useShortcuts(mailListShortcuts, handlers, { scope });
diff --git a/apps/mail/lib/hotkeys/navigation-hotkeys.tsx b/apps/mail/lib/hotkeys/navigation-hotkeys.tsx
index b0eec0f1d..2099600a6 100644
--- a/apps/mail/lib/hotkeys/navigation-hotkeys.tsx
+++ b/apps/mail/lib/hotkeys/navigation-hotkeys.tsx
@@ -1,4 +1,4 @@
-import { keyboardShortcuts } from '@/config/shortcuts';
+import { enhancedKeyboardShortcuts } from '@/config/shortcuts';
import { useShortcuts } from './use-hotkey-utils';
import { useNavigate } from 'react-router';
@@ -16,7 +16,7 @@ export function NavigationHotkeys() {
helpWithShortcuts: () => navigate('/settings/shortcuts'),
};
- const globalShortcuts = keyboardShortcuts.filter((shortcut) => shortcut.scope === scope);
+ const globalShortcuts = enhancedKeyboardShortcuts.filter((shortcut) => shortcut.scope === scope);
useShortcuts(globalShortcuts, handlers, { scope });
diff --git a/apps/mail/lib/hotkeys/thread-display-hotkeys.tsx b/apps/mail/lib/hotkeys/thread-display-hotkeys.tsx
index 746bcdf8d..789711eb3 100644
--- a/apps/mail/lib/hotkeys/thread-display-hotkeys.tsx
+++ b/apps/mail/lib/hotkeys/thread-display-hotkeys.tsx
@@ -1,9 +1,9 @@
import { mailNavigationCommandAtom } from '@/hooks/use-mail-navigation';
-import { useThread, } from '@/hooks/use-threads';
-import { keyboardShortcuts } from '@/config/shortcuts';
+import { enhancedKeyboardShortcuts } from '@/config/shortcuts';
import useMoveTo from '@/hooks/driver/use-move-to';
import useDelete from '@/hooks/driver/use-delete';
import { useShortcuts } from './use-hotkey-utils';
+import { useThread } from '@/hooks/use-threads';
import { useParams } from 'react-router';
import { useQueryState } from 'nuqs';
import { useSetAtom } from 'jotai';
@@ -55,7 +55,9 @@ export function ThreadDisplayHotkeys() {
},
};
- const threadDisplayShortcuts = keyboardShortcuts.filter((shortcut) => shortcut.scope === scope);
+ const threadDisplayShortcuts = enhancedKeyboardShortcuts.filter(
+ (shortcut) => shortcut.scope === scope,
+ );
useShortcuts(threadDisplayShortcuts, handlers, { scope });
diff --git a/apps/mail/lib/hotkeys/use-hotkey-utils.ts b/apps/mail/lib/hotkeys/use-hotkey-utils.ts
index e3d059b9f..ee369abdb 100644
--- a/apps/mail/lib/hotkeys/use-hotkey-utils.ts
+++ b/apps/mail/lib/hotkeys/use-hotkey-utils.ts
@@ -1,5 +1,7 @@
// TODO: Implement shortcuts syncing and caching
-import { type Shortcut, keyboardShortcuts } from '@/config/shortcuts';
+import { type Shortcut, keyboardShortcuts, enhancedKeyboardShortcuts } from '@/config/shortcuts';
+import { keyboardLayoutMapper, type KeyboardLayout } from '@/utils/keyboard-layout-map';
+import { getKeyCodeFromKey } from '@/utils/keyboard-utils';
import { useHotkeys } from 'react-hotkeys-hook';
import { useCallback, useMemo } from 'react';
@@ -52,12 +54,6 @@ export const isMac =
(/macintosh|mac os x/i.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1));
-const isDvorak =
- typeof window !== 'undefined' &&
- (navigator.language === 'en-DV' ||
- navigator.languages?.includes('en-DV') ||
- document.documentElement.lang === 'en-DV');
-
const dvorakToQwerty: Record = {
a: 'a',
b: 'x',
@@ -109,7 +105,19 @@ export const formatKeys = (keys: string[] | undefined): string => {
const mapKey = (key: string) => {
const lowerKey = key.toLowerCase();
- const mappedKey = isDvorak ? qwertyToDvorak[lowerKey] || key : key;
+
+ // Use enhanced keyboard layout mapping
+ const detectedLayout = keyboardLayoutMapper.getDetectedLayout();
+ let mappedKey = key;
+
+ if (detectedLayout?.layout === 'dvorak') {
+ // Use the existing Dvorak mapping for backward compatibility
+ mappedKey = qwertyToDvorak[lowerKey] || key;
+ } else if (detectedLayout?.layout && detectedLayout.layout !== 'qwerty') {
+ // Use the KeyboardLayoutMap API for other layouts
+ const keyCode = getKeyCodeFromKey(key);
+ mappedKey = keyboardLayoutMapper.getKeyForCode(keyCode);
+ }
switch (mappedKey) {
case 'mod':
@@ -134,10 +142,26 @@ export const formatKeys = (keys: string[] | undefined): string => {
return mapKey(firstKey);
};
+/**
+ * Convert a key string to its corresponding KeyCode for the keyboard layout mapper
+ */
+
export const formatDisplayKeys = (keys: string[]): string[] => {
return keys.map((key) => {
const lowerKey = key.toLowerCase();
- const mappedKey = isDvorak ? qwertyToDvorak[lowerKey] || key : key;
+
+ // Use enhanced keyboard layout mapping
+ const detectedLayout = keyboardLayoutMapper.getDetectedLayout();
+ let mappedKey = key;
+
+ if (detectedLayout?.layout === 'dvorak') {
+ // Use the existing Dvorak mapping for backward compatibility
+ mappedKey = qwertyToDvorak[lowerKey] || key;
+ } else if (detectedLayout?.layout && detectedLayout.layout !== 'qwerty') {
+ // Use the KeyboardLayoutMap API for other layouts
+ const keyCode = getKeyCodeFromKey(key);
+ mappedKey = keyboardLayoutMapper.getKeyForCode(keyCode);
+ }
switch (mappedKey) {
case 'mod':
@@ -168,6 +192,36 @@ export const formatDisplayKeys = (keys: string[]): string[] => {
});
};
+/**
+ * Enhanced shortcut utilities with layout mapping support, here incase needed
+ */
+export const useEnhancedShortcuts = () => {
+ const layoutInfo = keyboardLayoutMapper.getDetectedLayout();
+
+ const getShortcutsForLayout = useCallback((layout: KeyboardLayout) => {
+ return enhancedKeyboardShortcuts.map((shortcut) => ({
+ ...shortcut,
+ mappedKeys: shortcut.keys.map((key) =>
+ keyboardLayoutMapper.convertKey(key, 'qwerty', layout),
+ ),
+ }));
+ }, []);
+
+ return {
+ layoutInfo,
+ formatKeysWithLayout: (keys: string[], targetLayout?: KeyboardLayout) => {
+ if (!targetLayout || !layoutInfo) return formatKeys(keys);
+
+ return keys
+ .map((key) => {
+ return keyboardLayoutMapper.convertKey(key, layoutInfo.layout, targetLayout);
+ })
+ .join('+');
+ },
+ getShortcutsForLayout,
+ };
+};
+
export type HotkeyOptions = {
scope: string;
preventDefault?: boolean;
diff --git a/apps/mail/providers/client-providers.tsx b/apps/mail/providers/client-providers.tsx
index c2692908b..453ee3fb0 100644
--- a/apps/mail/providers/client-providers.tsx
+++ b/apps/mail/providers/client-providers.tsx
@@ -1,7 +1,8 @@
+import { useKeyboardLayout } from '@/components/keyboard-layout-indicator';
+import { LoadingProvider } from '@/components/context/loading-context';
import { NuqsAdapter } from 'nuqs/adapters/react-router/v7';
import { SidebarProvider } from '@/components/ui/sidebar';
import { PostHogProvider } from '@/lib/posthog-provider';
-import { LoadingProvider } from '@/components/context/loading-context';
import { useSettings } from '@/hooks/use-settings';
import { Provider as JotaiProvider } from 'jotai';
import type { PropsWithChildren } from 'react';
@@ -10,6 +11,7 @@ import { ThemeProvider } from 'next-themes';
export function ClientProviders({ children }: PropsWithChildren) {
const { data } = useSettings();
+ useKeyboardLayout();
const theme = data?.settings.colorTheme || 'system';
diff --git a/apps/mail/utils/keyboard-layout-map.ts b/apps/mail/utils/keyboard-layout-map.ts
new file mode 100644
index 000000000..582cedb86
--- /dev/null
+++ b/apps/mail/utils/keyboard-layout-map.ts
@@ -0,0 +1,596 @@
+/**
+ * Keyboard Layout Map utility for mapping physical key codes to layout-specific key values
+ * Uses the KeyboardLayoutMap API: https://wicg.github.io/keyboard-map/
+ * Enhanced with comprehensive layout detection and Dvorak support
+ */
+
+export interface KeyboardLayoutMapAPI {
+ get(keyCode: string): string | undefined;
+ has(keyCode: string): boolean;
+ keys(): IterableIterator;
+ values(): IterableIterator;
+ entries(): IterableIterator<[string, string]>;
+ forEach(callback: (value: string, key: string) => void): void;
+ readonly size: number;
+}
+
+declare global {
+ interface Navigator {
+ keyboard?: {
+ getLayoutMap(): Promise;
+ };
+ }
+}
+
+/**
+ * Keyboard layout types and detection
+ */
+export type KeyboardLayout = 'qwerty' | 'dvorak' | 'colemak' | 'azerty' | 'qwertz' | 'unknown';
+
+export interface LayoutDetectionResult {
+ layout: KeyboardLayout;
+ confidence: number;
+ method: 'api' | 'language' | 'fallback';
+}
+
+/**
+ * Comprehensive layout mapping tables
+ */
+export const layoutMappings = {
+ dvorak: {
+ // Dvorak to QWERTY mapping
+ toQwerty: {
+ a: 'a',
+ b: 'x',
+ c: 'j',
+ d: 'e',
+ e: '.',
+ f: 'u',
+ g: 'i',
+ h: 'd',
+ i: 'c',
+ j: 'h',
+ k: 't',
+ l: 'n',
+ m: 'm',
+ n: 'b',
+ o: 'r',
+ p: 'l',
+ q: "'",
+ r: 'p',
+ s: 'o',
+ t: 'k',
+ u: 'g',
+ v: 'q',
+ w: ',',
+ x: 'z',
+ y: 'f',
+ z: ';',
+ ';': 's',
+ "'": '-',
+ ',': 'w',
+ '.': 'v',
+ '/': 'z',
+ '-': '[',
+ '[': '/',
+ ']': '=',
+ '=': ']',
+ },
+ // QWERTY to Dvorak mapping (reversed)
+ fromQwerty: {} as Record,
+ // Physical key codes to Dvorak keys
+ physicalMap: {
+ KeyA: 'a',
+ KeyB: 'x',
+ KeyC: 'j',
+ KeyD: 'e',
+ KeyE: '.',
+ KeyF: 'u',
+ KeyG: 'i',
+ KeyH: 'd',
+ KeyI: 'c',
+ KeyJ: 'h',
+ KeyK: 't',
+ KeyL: 'n',
+ KeyM: 'm',
+ KeyN: 'b',
+ KeyO: 'r',
+ KeyP: 'l',
+ KeyQ: "'",
+ KeyR: 'p',
+ KeyS: 'o',
+ KeyT: 'k',
+ KeyU: 'g',
+ KeyV: 'q',
+ KeyW: ',',
+ KeyX: 'z',
+ KeyY: 'f',
+ KeyZ: ';',
+ Semicolon: 's',
+ Quote: '-',
+ Comma: 'w',
+ Period: 'v',
+ Slash: 'z',
+ Minus: '[',
+ BracketLeft: '/',
+ BracketRight: '=',
+ Equal: ']',
+ },
+ },
+ colemak: {
+ physicalMap: {
+ KeyD: 'g',
+ KeyE: 'f',
+ KeyF: 'e',
+ KeyG: 'd',
+ KeyI: 'l',
+ KeyJ: 'u',
+ KeyK: 'y',
+ KeyL: ';',
+ KeyN: 'k',
+ KeyO: ';',
+ KeyP: 'r',
+ KeyR: 's',
+ KeyS: 'r',
+ KeyT: 'g',
+ KeyU: 'l',
+ KeyY: 'j',
+ Semicolon: 'o',
+ },
+ },
+ azerty: {
+ physicalMap: {
+ KeyA: 'q',
+ KeyQ: 'a',
+ KeyW: 'z',
+ KeyZ: 'w',
+ KeyM: ',',
+ Comma: 'm',
+ Period: ';',
+ Semicolon: '.',
+ Digit1: '&',
+ Digit2: 'é',
+ Digit3: '"',
+ Digit4: "'",
+ Digit5: '(',
+ Digit6: '-',
+ Digit7: 'è',
+ Digit8: '_',
+ Digit9: 'ç',
+ Digit0: 'à',
+ },
+ },
+ qwertz: {
+ physicalMap: {
+ KeyY: 'z',
+ KeyZ: 'y',
+ Semicolon: 'ö',
+ Quote: 'ä',
+ BracketLeft: 'ü',
+ BracketRight: '+',
+ Backslash: '#',
+ Minus: 'ß',
+ Equal: '´',
+ },
+ },
+};
+
+// Initialize reverse mappings
+layoutMappings.dvorak.fromQwerty = Object.entries(layoutMappings.dvorak.toQwerty).reduce(
+ (acc, [dvorak, qwerty]) => {
+ acc[qwerty] = dvorak;
+ return acc;
+ },
+ {} as Record,
+);
+
+class KeyboardLayoutMapper {
+ private layoutMap: KeyboardLayoutMapAPI | null = null;
+ private isInitialized = false;
+ private detectedLayout: LayoutDetectionResult | null = null;
+
+ /**
+ * Initialize the keyboard layout map
+ */
+ async init(): Promise {
+ if (this.isInitialized) return;
+
+ try {
+ if ('keyboard' in navigator && navigator.keyboard?.getLayoutMap) {
+ this.layoutMap = await navigator.keyboard.getLayoutMap();
+ this.detectedLayout = await this.detectLayout();
+ this.isInitialized = true;
+ } else {
+ console.warn('KeyboardLayoutMap API is not supported in this browser');
+ this.detectedLayout = this.detectLayoutFallback();
+ }
+ } catch (error) {
+ console.error('Failed to initialize KeyboardLayoutMap:', error);
+ this.detectedLayout = this.detectLayoutFallback();
+ }
+ }
+
+ /**
+ * Detect keyboard layout using multiple methods
+ */
+ private async detectLayout(): Promise {
+ // Method 1: Use KeyboardLayoutMap API
+ if (this.layoutMap) {
+ const layoutResult = this.analyzeLayoutFromAPI();
+ if (layoutResult.confidence > 0.8) {
+ return layoutResult;
+ }
+ }
+
+ // Method 2: Language-based detection
+ const languageResult = this.detectLayoutFromLanguage();
+ if (languageResult.confidence > 0.6) {
+ return languageResult;
+ }
+
+ // Method 3: Fallback detection
+ return this.detectLayoutFallback();
+ }
+
+ /**
+ * Analyze layout from KeyboardLayoutMap API
+ */
+ private analyzeLayoutFromAPI(): LayoutDetectionResult {
+ if (!this.layoutMap) {
+ return { layout: 'unknown', confidence: 0, method: 'api' };
+ }
+
+ // Check key signatures for different layouts
+ const keySignatures = {
+ dvorak: new Set([
+ { code: 'KeyQ', expected: "'" },
+ { code: 'KeyW', expected: ',' },
+ { code: 'KeyE', expected: '.' },
+ { code: 'KeyR', expected: 'p' },
+ { code: 'KeyT', expected: 'y' },
+ ]),
+ colemak: new Set([
+ { code: 'KeyE', expected: 'f' },
+ { code: 'KeyR', expected: 'p' },
+ { code: 'KeyT', expected: 'g' },
+ { code: 'KeyY', expected: 'j' },
+ ]),
+ azerty: new Set([
+ { code: 'KeyQ', expected: 'a' },
+ { code: 'KeyA', expected: 'q' },
+ { code: 'KeyW', expected: 'z' },
+ { code: 'KeyZ', expected: 'w' },
+ ]),
+ qwertz: new Set([
+ { code: 'KeyY', expected: 'z' },
+ { code: 'KeyZ', expected: 'y' },
+ ]),
+ };
+
+ for (const [layout, signatures] of Object.entries(keySignatures)) {
+ let matches = 0;
+ for (const { code, expected } of signatures) {
+ if (this.layoutMap!.get(code) === expected) {
+ matches++;
+ }
+ }
+
+ const confidence = matches / signatures.size;
+ if (confidence > 0.8) {
+ return { layout: layout as KeyboardLayout, confidence, method: 'api' };
+ }
+ }
+
+ // Default to QWERTY if no specific layout detected
+ return { layout: 'qwerty', confidence: 0.5, method: 'api' };
+ }
+
+ /**
+ * Detect layout from language/locale settings
+ */
+ private detectLayoutFromLanguage(): LayoutDetectionResult {
+ const language = navigator.language || navigator.languages?.[0] || '';
+ const languages = navigator.languages || [language];
+
+ // Check for explicit Dvorak indicators
+ const dvorakIndicators = new Set(['DV', 'dvorak']);
+ if (
+ languages.some(
+ (lang) => dvorakIndicators.has(lang) || lang.includes('DV') || lang.includes('dvorak'),
+ )
+ ) {
+ return { layout: 'dvorak', confidence: 0.9, method: 'language' };
+ }
+
+ // Check document language
+ const docLang = document.documentElement.lang;
+ if (
+ (docLang && dvorakIndicators.has(docLang)) ||
+ docLang?.includes('DV') ||
+ docLang?.includes('dvorak')
+ ) {
+ return { layout: 'dvorak', confidence: 0.8, method: 'language' };
+ }
+
+ // Regional layout detection using Set for faster lookups
+ const layoutByRegion = new Map([
+ ['fr', 'azerty'],
+ ['de', 'qwertz'],
+ ['at', 'qwertz'],
+ ['ch', 'qwertz'],
+ ]);
+
+ const region = language.split('-')[0];
+ if (layoutByRegion.has(region)) {
+ return { layout: layoutByRegion.get(region)!, confidence: 0.7, method: 'language' };
+ }
+
+ return { layout: 'qwerty', confidence: 0.3, method: 'language' };
+ }
+
+ /**
+ * Fallback layout detection
+ */
+ private detectLayoutFallback(): LayoutDetectionResult {
+ // Check for common Dvorak indicators in existing code
+ if (typeof window !== 'undefined') {
+ const dvorakLocales = new Set(['en-DV']);
+ const languageSet = new Set(navigator.languages || []);
+
+ const isDvorakFromExisting =
+ dvorakLocales.has(navigator.language) ||
+ languageSet.has('en-DV') ||
+ dvorakLocales.has(document.documentElement.lang);
+
+ if (isDvorakFromExisting) {
+ return { layout: 'dvorak', confidence: 0.6, method: 'fallback' };
+ }
+ }
+
+ return { layout: 'qwerty', confidence: 0.4, method: 'fallback' };
+ }
+
+ /**
+ * Get detected layout information
+ */
+ getDetectedLayout(): LayoutDetectionResult | null {
+ return this.detectedLayout;
+ }
+
+ /**
+ * Get the mapped key value for a given key code
+ * @param keyCode - The physical key code (e.g., 'KeyA', 'Space', 'Enter')
+ * @returns The mapped key value or the original key code if mapping fails
+ */
+ getKeyForCode(keyCode: string): string {
+ // Try KeyboardLayoutMap API first
+ if (this.layoutMap) {
+ const mappedKey = this.layoutMap.get(keyCode);
+ if (mappedKey) return mappedKey;
+ }
+
+ // Fallback to detected layout mapping
+ if (this.detectedLayout) {
+ const layoutMapping = this.getLayoutMapping(this.detectedLayout.layout);
+ if (layoutMapping && layoutMapping[keyCode]) {
+ return layoutMapping[keyCode];
+ }
+ }
+
+ return keyCode;
+ }
+
+ /**
+ * Get layout-specific mapping table
+ */
+ private getLayoutMapping(layout: KeyboardLayout): Record | null {
+ switch (layout) {
+ case 'dvorak':
+ return layoutMappings.dvorak.physicalMap;
+ case 'colemak':
+ return layoutMappings.colemak.physicalMap;
+ case 'azerty':
+ return layoutMappings.azerty.physicalMap;
+ case 'qwertz':
+ return layoutMappings.qwertz.physicalMap;
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Convert a key from one layout to another
+ */
+ convertKey(key: string, fromLayout: KeyboardLayout, toLayout: KeyboardLayout): string {
+ if (fromLayout === toLayout) return key;
+
+ // Special case for Dvorak <-> QWERTY conversion
+ if (fromLayout === 'dvorak' && toLayout === 'qwerty') {
+ return (
+ layoutMappings.dvorak.toQwerty[
+ key.toLowerCase() as keyof typeof layoutMappings.dvorak.toQwerty
+ ] || key
+ );
+ }
+ if (fromLayout === 'qwerty' && toLayout === 'dvorak') {
+ return (
+ layoutMappings.dvorak.fromQwerty[
+ key.toLowerCase() as keyof typeof layoutMappings.dvorak.fromQwerty
+ ] || key
+ );
+ }
+
+ // For other layouts, use physical mapping
+ const fromMapping = this.getLayoutMapping(fromLayout);
+ const toMapping = this.getLayoutMapping(toLayout);
+
+ if (fromMapping && toMapping) {
+ // Find the physical key code for the source key
+ const physicalKey = Object.entries(fromMapping).find(
+ ([_, mappedKey]) => mappedKey.toLowerCase() === key.toLowerCase(),
+ )?.[0];
+
+ if (physicalKey && toMapping[physicalKey]) {
+ return toMapping[physicalKey];
+ }
+ }
+
+ return key;
+ }
+
+ /**
+ * Map an array of key codes to their layout-specific values
+ * @param keyCodes - Array of key codes to map
+ * @returns Array of mapped key values
+ */
+ mapKeys(keyCodes: string[]): string[] {
+ return keyCodes.map((keyCode) => this.getKeyForCode(keyCode));
+ }
+
+ /**
+ * Check if the keyboard layout map is available and initialized
+ */
+ isAvailable(): boolean {
+ return this.layoutMap !== null;
+ }
+
+ /**
+ * Get all available key mappings
+ */
+ getAllMappings(): Record {
+ if (!this.layoutMap) return {};
+
+ const mappings: Record = {};
+ this.layoutMap.forEach((value, key) => {
+ mappings[key] = value;
+ });
+ return mappings;
+ }
+
+ /**
+ * Get all key codes as a Set for efficient lookups
+ */
+ getKeyCodesSet(): Set {
+ if (!this.layoutMap) return new Set();
+ return new Set(this.layoutMap.keys());
+ }
+
+ /**
+ * Get all key values as a Set for efficient lookups
+ */
+ getKeyValuesSet(): Set {
+ if (!this.layoutMap) return new Set();
+ return new Set(this.layoutMap.values());
+ }
+
+ /**
+ * Check if a key code exists in the current layout
+ */
+ hasKeyCode(keyCode: string): boolean {
+ return this.layoutMap?.has(keyCode) ?? false;
+ }
+
+ /**
+ * Check if a key value exists in the current layout
+ */
+ hasKeyValue(keyValue: string): boolean {
+ if (!this.layoutMap) return false;
+ for (const value of this.layoutMap.values()) {
+ if (value === keyValue) return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get intersection of two key sets
+ */
+ getKeySetIntersection(setA: Set, setB: Set): Set {
+ return new Set([...setA].filter((key) => setB.has(key)));
+ }
+
+ /**
+ * Get union of two key sets
+ */
+ getKeySetUnion(setA: Set, setB: Set): Set {
+ return new Set([...setA, ...setB]);
+ }
+
+ /**
+ * Get difference between two key sets
+ */
+ getKeySetDifference(setA: Set, setB: Set): Set {
+ return new Set([...setA].filter((key) => !setB.has(key)));
+ }
+}
+
+// Export a singleton instance
+export const keyboardLayoutMapper = new KeyboardLayoutMapper();
+
+/**
+ * Common key code mappings for reference
+ */
+export const commonKeyCodes = {
+ // Letters
+ KeyA: 'KeyA',
+ KeyB: 'KeyB',
+ KeyC: 'KeyC',
+ KeyD: 'KeyD',
+ KeyE: 'KeyE',
+ KeyF: 'KeyF',
+ KeyG: 'KeyG',
+ KeyH: 'KeyH',
+ KeyI: 'KeyI',
+ KeyJ: 'KeyJ',
+ KeyK: 'KeyK',
+ KeyL: 'KeyL',
+ KeyM: 'KeyM',
+ KeyN: 'KeyN',
+ KeyO: 'KeyO',
+ KeyP: 'KeyP',
+ KeyQ: 'KeyQ',
+ KeyR: 'KeyR',
+ KeyS: 'KeyS',
+ KeyT: 'KeyT',
+ KeyU: 'KeyU',
+ KeyV: 'KeyV',
+ KeyW: 'KeyW',
+ KeyX: 'KeyX',
+ KeyY: 'KeyY',
+ KeyZ: 'KeyZ',
+
+ // Numbers
+ Digit1: 'Digit1',
+ Digit2: 'Digit2',
+ Digit3: 'Digit3',
+ Digit4: 'Digit4',
+ Digit5: 'Digit5',
+ Digit6: 'Digit6',
+ Digit7: 'Digit7',
+ Digit8: 'Digit8',
+ Digit9: 'Digit9',
+ Digit0: 'Digit0',
+
+ // Special keys
+ Space: 'Space',
+ Enter: 'Enter',
+ Escape: 'Escape',
+ Backspace: 'Backspace',
+ Tab: 'Tab',
+
+ // Modifiers
+ ShiftLeft: 'ShiftLeft',
+ ShiftRight: 'ShiftRight',
+ ControlLeft: 'ControlLeft',
+ ControlRight: 'ControlRight',
+ AltLeft: 'AltLeft',
+ AltRight: 'AltRight',
+ MetaLeft: 'MetaLeft',
+ MetaRight: 'MetaRight',
+} as const;
+
+/**
+ * Initialize the keyboard layout mapper on module load
+ */
+if (typeof window !== 'undefined') {
+ keyboardLayoutMapper.init().catch(console.error);
+}
diff --git a/apps/mail/utils/keyboard-utils.ts b/apps/mail/utils/keyboard-utils.ts
new file mode 100644
index 000000000..6a1dfe319
--- /dev/null
+++ b/apps/mail/utils/keyboard-utils.ts
@@ -0,0 +1,33 @@
+/**
+ * Converts a key string to its corresponding keyboard event code
+ * @param key - The key string to convert
+ * @returns The keyboard event code
+ */
+export function getKeyCodeFromKey(key: string): string {
+ // Handle single characters
+ if (key.length === 1) {
+ const upperKey = key.toUpperCase();
+ if (upperKey >= 'A' && upperKey <= 'Z') {
+ return `Key${upperKey}`;
+ }
+ if (key >= '0' && key <= '9') {
+ return `Digit${key}`;
+ }
+ }
+
+ // Handle special keys
+ const specialKeys: Record = {
+ space: 'Space',
+ enter: 'Enter',
+ escape: 'Escape',
+ backspace: 'Backspace',
+ tab: 'Tab',
+ shift: 'ShiftLeft',
+ ctrl: 'ControlLeft',
+ control: 'ControlLeft',
+ alt: 'AltLeft',
+ meta: 'MetaLeft',
+ };
+
+ return specialKeys[key.toLowerCase()] || key;
+}
diff --git a/apps/server/src/lib/driver/google.ts b/apps/server/src/lib/driver/google.ts
index 4092d180b..e36ac2a85 100644
--- a/apps/server/src/lib/driver/google.ts
+++ b/apps/server/src/lib/driver/google.ts
@@ -208,22 +208,24 @@ export class GoogleMailManager implements MailManager {
type LabelCount = { label: string; count: number };
- const mapped: LabelCount[] = (await Promise.all(
- results.map(async (res) => {
- if ('_tag' in res && res._tag === 'LabelFetchFailed') {
- return null;
- }
- let labelName = (res.data.name ?? res.data.id ?? '').toLowerCase();
- if (labelName === 'draft') {
- labelName = 'drafts';
- }
- const isTotalLabel = labelName === 'drafts' || labelName === 'sent';
- return {
- label: labelName,
- count: Number(isTotalLabel ? res.data.threadsTotal : res.data.threadsUnread),
- };
- }),
- )).filter((item): item is LabelCount => item !== null);
+ const mapped: LabelCount[] = (
+ await Promise.all(
+ results.map(async (res) => {
+ if ('_tag' in res && res._tag === 'LabelFetchFailed') {
+ return null;
+ }
+ let labelName = (res.data.name ?? res.data.id ?? '').toLowerCase();
+ if (labelName === 'draft') {
+ labelName = 'drafts';
+ }
+ const isTotalLabel = labelName === 'drafts' || labelName === 'sent';
+ return {
+ label: labelName,
+ count: Number(isTotalLabel ? res.data.threadsTotal : res.data.threadsUnread),
+ };
+ }),
+ )
+ ).filter((item): item is LabelCount => item !== null);
// Get archive count
try {
@@ -847,27 +849,36 @@ export class GoogleMailManager implements MailManager {
const chunkSize = 15;
const delayBetweenChunks = 100;
- const allResults = [];
+ const allResults: Array<{
+ threadId: string;
+ status: 'fulfilled' | 'rejected';
+ value?: unknown;
+ reason?: unknown;
+ }> = [];
for (let i = 0; i < threadIds.length; i += chunkSize) {
const chunk = threadIds.slice(i, i + chunkSize);
- const promises = chunk.map(async (threadId) => {
- try {
- const response = await this.gmail.users.threads.modify({
- userId: 'me',
- id: threadId,
- requestBody: requestBody,
- });
- return { threadId, status: 'fulfilled' as const, value: response.data };
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- } catch (error: any) {
- const errorMessage = error?.errors?.[0]?.message || error.message || error;
- return { threadId, status: 'rejected' as const, reason: { error: errorMessage } };
- }
- });
+ const effects = chunk.map((threadId) =>
+ Effect.tryPromise({
+ try: async () => {
+ const response = await this.gmail.users.threads.modify({
+ userId: 'me',
+ id: threadId,
+ requestBody,
+ });
+ return { threadId, status: 'fulfilled' as const, value: response.data };
+ },
+ catch: (error: any) => {
+ const errorMessage = error?.errors?.[0]?.message || error.message || error;
+ return { threadId, status: 'rejected' as const, reason: { error: errorMessage } };
+ },
+ }),
+ );
- const chunkResults = await Promise.all(promises);
+ const chunkResults = await Effect.runPromise(
+ Effect.all(effects, { concurrency: 'unbounded' }),
+ );
allResults.push(...chunkResults);
if (i + chunkSize < threadIds.length) {
diff --git a/apps/server/src/lib/gmail-rate-limit.ts b/apps/server/src/lib/gmail-rate-limit.ts
index 5ca55e411..113862b47 100644
--- a/apps/server/src/lib/gmail-rate-limit.ts
+++ b/apps/server/src/lib/gmail-rate-limit.ts
@@ -5,18 +5,22 @@ import { Effect, Duration, Schedule } from 'effect';
* – HTTP 429 Too Many Requests
* – HTTP 403 with reason == userRateLimitExceeded or quotaExceeded
*/
-export function isGmailRateLimit(err: unknown): boolean {
+export function isRateLimit(err: unknown): boolean {
const e: any = err || {};
const status = e.code ?? e.status ?? e.response?.status;
-
+
if (status === 429) return true;
if (status === 403) {
- const errors = e.errors ??
- e.response?.data?.error?.errors ??
- [];
+ const errors = e.errors ?? e.response?.data?.error?.errors ?? [];
return errors.some((x: any) =>
- ['userRateLimitExceeded', 'rateLimitExceeded', 'quotaExceeded',
- 'dailyLimitExceeded', 'backendError', 'limitExceeded'].includes(x.reason)
+ [
+ 'userRateLimitExceeded',
+ 'rateLimitExceeded',
+ 'quotaExceeded',
+ 'dailyLimitExceeded',
+ 'backendError',
+ 'limitExceeded',
+ ].includes(x.reason),
);
}
return false;
@@ -28,16 +32,15 @@ export function isGmailRateLimit(err: unknown): boolean {
* – waits 60 seconds between retries (conservative for Gmail user quotas)
* – stops immediately for any other error
*/
-export const gmailRateLimitSchedule = Schedule
- .recurWhile(isGmailRateLimit)
- .pipe(Schedule.intersect(Schedule.recurs(10))) // max 10 attempts
- .pipe(Schedule.addDelay(() => Duration.seconds(60))); // 60s delay between retries
+export const rateLimitSchedule = Schedule.recurWhile(isRateLimit)
+ .pipe(Schedule.intersect(Schedule.recurs(10))) // max 10 attempts
+ .pipe(Schedule.addDelay(() => Duration.seconds(60))); // 60s delay between retries
/**
* Generic wrapper that applies the schedule
*/
-export function withGmailRetry(
+export function withRetry(
eff: Effect.Effect,
): Effect.Effect {
- return eff.pipe(Effect.retry(gmailRateLimitSchedule));
+ return eff.pipe(Effect.retry(rateLimitSchedule));
}
diff --git a/apps/server/src/routes/chat.ts b/apps/server/src/routes/chat.ts
index f3c03d229..66457b77b 100644
--- a/apps/server/src/routes/chat.ts
+++ b/apps/server/src/routes/chat.ts
@@ -45,7 +45,7 @@ import { FOLDERS } from '../lib/utils';
import { and, eq } from 'drizzle-orm';
import { McpAgent } from 'agents/mcp';
-import { withGmailRetry } from '../lib/gmail-rate-limit';
+import { withRetry } from '../lib/gmail-rate-limit';
import { createDb } from '../db';
import { Effect } from 'effect';
import { z } from 'zod';
@@ -895,6 +895,7 @@ export class ZeroAgent extends AIChatAgent {
}
this.syncThreadsInProgress.set(threadId, true);
+ console.log('Server: syncThread called for thread', threadId);
try {
const threadData = await this.getWithRetry(threadId);
const latest = threadData.latest;
@@ -937,6 +938,10 @@ export class ZeroAgent extends AIChatAgent {
});
}
this.syncThreadsInProgress.delete(threadId);
+ console.log('Server: syncThread result', {
+ threadId,
+ labels: threadData.labels,
+ });
return { success: true, threadId, threadData };
} else {
this.syncThreadsInProgress.delete(threadId);
@@ -957,13 +962,13 @@ export class ZeroAgent extends AIChatAgent {
private async listWithRetry(params: Parameters[0]) {
if (!this.driver) throw new Error('No driver available');
- return Effect.runPromise(withGmailRetry(Effect.tryPromise(() => this.driver!.list(params))));
+ return Effect.runPromise(withRetry(Effect.tryPromise(() => this.driver!.list(params))));
}
private async getWithRetry(threadId: string): Promise {
if (!this.driver) throw new Error('No driver available');
- return Effect.runPromise(withGmailRetry(Effect.tryPromise(() => this.driver!.get(threadId))));
+ return Effect.runPromise(withRetry(Effect.tryPromise(() => this.driver!.get(threadId))));
}
async syncThreads(folder: string) {
diff --git a/apps/server/src/trpc/routes/mail.ts b/apps/server/src/trpc/routes/mail.ts
index 7d563b19a..9a0a53862 100644
--- a/apps/server/src/trpc/routes/mail.ts
+++ b/apps/server/src/trpc/routes/mail.ts
@@ -1,5 +1,5 @@
-import { activeDriverProcedure, router, privateProcedure } from '../trpc';
import { updateWritingStyleMatrix } from '../../services/writing-style-service';
+import { activeDriverProcedure, router, privateProcedure } from '../trpc';
import { IGetThreadResponseSchema } from '../../lib/driver/types';
import { processEmailHtml } from '../../lib/email-processor';
import { defaultPageSize, FOLDERS } from '../../lib/utils';
@@ -153,7 +153,6 @@ export const mailRouter = router({
if (threadIds.length) {
await agent.modifyLabels(threadIds, addLabels, removeLabels);
- console.log('Server: Successfully updated thread labels');
return { success: true };
}