Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions apps/desktop/src/components/main/body/ai.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
X,
} from "lucide-react";
import { useCallback, useMemo, useRef, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";

import type { ChatShortcut } from "@hypr/store";
import { Button } from "@hypr/ui/components/ui/button";
Expand All @@ -18,6 +19,7 @@ import {
} from "@hypr/ui/components/ui/scroll-fade";
import { cn } from "@hypr/utils";

import { useSettingsNavigation } from "../../../hooks/useSettingsNavigation";
import * as main from "../../../store/tinybase/store/main";
import { type Tab, useTabs } from "../../../store/zustand/tabs";
import { LLM } from "../../settings/ai/llm";
Expand Down Expand Up @@ -97,6 +99,46 @@ function AIView({ tab }: { tab: Extract<Tab, { type: "ai" }> }) {
[updateAiTabState, tab],
);

const enabledMenuKeys: AITabKey[] = [
"transcription",
"intelligence",
"templates",
"shortcuts",
];
const currentIndex = enabledMenuKeys.indexOf(activeTab);

useHotkeys(
"ctrl+alt+left",
() => {
if (currentIndex > 0) {
setActiveTab(enabledMenuKeys[currentIndex - 1]);
}
},
{
preventDefault: true,
enableOnFormTags: true,
enableOnContentEditable: true,
},
[currentIndex, setActiveTab],
);

useHotkeys(
"ctrl+alt+right",
() => {
if (currentIndex >= 0 && currentIndex < enabledMenuKeys.length - 1) {
setActiveTab(enabledMenuKeys[currentIndex + 1]);
}
},
{
preventDefault: true,
enableOnFormTags: true,
enableOnContentEditable: true,
},
[currentIndex, setActiveTab],
);
Comment on lines +110 to +138
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 ctrl+alt+left/right hotkey collision between AI/Settings sub-tabs and note-input tabs

The new ctrl+alt+left/right hotkeys added here and in settings.tsx:104-131 use the same key combination as the pre-existing hotkeys in apps/desktop/src/components/main/body/sessions/note-input/index.tsx:352-394. In react-hotkeys-hook, all mounted handlers for the same key fire. If both the AI/Settings view and the note-input view are mounted simultaneously (e.g., multiple tabs rendered in the DOM), pressing ctrl+alt+left would trigger both handlers. However, based on the tab rendering pattern (conditional rendering with {activeTab === ...}), only the active tab's content component should be mounted, which likely prevents the conflict. This warrants investigation to confirm that inactive tab content is truly unmounted rather than hidden.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


useSettingsNavigation(ref, activeTab);

const menuItems: Array<{
key: AITabKey;
label: string;
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/components/main/body/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -931,7 +931,7 @@ function useTabsShortcuts() {
);

useHotkeys(
"mod+alt+left",
"meta+alt+left",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Changing mod to meta breaks top-level tab navigation on Windows/Linux

The hotkey for switching between top-level tabs was changed from mod+alt+left/right to meta+alt+left/right. In react-hotkeys-hook, mod is a cross-platform alias that maps to Meta (Cmd) on macOS and Control on Windows/Linux, whereas meta always maps to the Meta key specifically (Cmd on macOS, Windows key on Windows/Linux).

Root Cause and Impact

On macOS this change has no effect (both mod and meta resolve to Cmd). However, on Windows/Linux the shortcut changes from Ctrl+Alt+Left/Right to Win+Alt+Left/Right. The Win+Alt+Arrow combination is typically intercepted by the OS for window management, making the shortcut effectively unusable.

Every other hotkey in this file (index.tsx:851, index.tsx:868, index.tsx:879, index.tsx:915, etc.) consistently uses mod for cross-platform compatibility. The new sub-tab navigation shortcuts in ai.tsx:110 and settings.tsx:103 use ctrl+alt+left/right, which would have conflicted with the old mod+alt+left/right on Windows/Linux (since mod = Ctrl there). It appears the intent was to separate the two shortcuts on all platforms, but meta is the wrong choice for Windows/Linux — the top-level shortcut should likely remain mod+alt+left/right and the sub-tab shortcut should use a different key combination entirely.

Impact: Top-level tab navigation via keyboard is completely broken on Windows/Linux.

Prompt for agents
The root problem is that on Windows/Linux, `mod` resolves to `Ctrl`, so both the top-level tab navigation (`mod+alt+left/right`) and the new sub-tab navigation (`ctrl+alt+left/right`) would use the same physical keys. Simply reverting `meta` back to `mod` at lines 934 and 945 in index.tsx would restore Windows/Linux support but re-introduce the conflict with the sub-tab shortcuts in ai.tsx and settings.tsx. You need to pick a different key combination for one of the two levels. For example, keep `mod+alt+left/right` for top-level tabs (lines 934 and 945 in index.tsx) and change the sub-tab navigation in ai.tsx (lines 110, 125) and settings.tsx (lines 103, 118) to a non-conflicting shortcut such as `ctrl+shift+left/right` or `alt+left/right`.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

() => selectPrev(),
{
preventDefault: true,
Expand All @@ -942,7 +942,7 @@ function useTabsShortcuts() {
);

useHotkeys(
"mod+alt+right",
"meta+alt+right",
() => selectNext(),
{
preventDefault: true,
Expand Down
36 changes: 36 additions & 0 deletions apps/desktop/src/components/main/body/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
UserIcon,
} from "lucide-react";
import { useCallback, useRef } from "react";
import { useHotkeys } from "react-hotkeys-hook";

import { Button } from "@hypr/ui/components/ui/button";
import {
Expand All @@ -15,6 +16,7 @@ import {
} from "@hypr/ui/components/ui/scroll-fade";
import { cn } from "@hypr/utils";

import { useSettingsNavigation } from "../../../hooks/useSettingsNavigation";
import {
type SettingsTab,
type Tab,
Expand Down Expand Up @@ -96,6 +98,40 @@ function SettingsView({ tab }: { tab: Extract<Tab, { type: "settings" }> }) {
[updateSettingsTabState, tab],
);

const currentIndex = SECTIONS.findIndex((s) => s.id === activeTab);

useHotkeys(
"ctrl+alt+left",
() => {
if (currentIndex > 0) {
setActiveTab(SECTIONS[currentIndex - 1].id);
}
},
{
preventDefault: true,
enableOnFormTags: true,
enableOnContentEditable: true,
},
[currentIndex, setActiveTab],
);

useHotkeys(
"ctrl+alt+right",
() => {
if (currentIndex < SECTIONS.length - 1) {
setActiveTab(SECTIONS[currentIndex + 1].id);
}
},
{
preventDefault: true,
enableOnFormTags: true,
enableOnContentEditable: true,
},
[currentIndex, setActiveTab],
);

useSettingsNavigation(ref, activeTab);

const renderContent = () => {
switch (activeTab) {
case "account":
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src/components/settings/general/account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,10 @@ function Container({
children?: ReactNode;
}) {
return (
<section className="bg-neutral-50 p-4 rounded-lg flex flex-col gap-4">
<section
data-settings-item
className="bg-neutral-50 p-4 rounded-lg flex flex-col gap-4"
>
<div className="flex flex-col gap-2">
<h1 className="text-md font-semibold font-serif">{title}</h1>
{description && (
Expand Down
8 changes: 6 additions & 2 deletions apps/desktop/src/components/settings/general/app-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,16 @@ function SettingRow({
onChange: (checked: boolean) => void;
}) {
return (
<div className="flex items-center justify-between gap-4">
<div data-settings-item className="flex items-center justify-between gap-4">
<div className="flex-1">
<h3 className="text-sm font-medium mb-1">{title}</h3>
<p className="text-xs text-neutral-600">{description}</p>
</div>
<Switch checked={checked} onCheckedChange={onChange} />
<Switch
data-settings-activate
checked={checked}
onCheckedChange={onChange}
/>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ export function MainLanguageView({
);

return (
<div className="flex flex-row items-center justify-between">
<div
data-settings-item
className="flex flex-row items-center justify-between"
>
<div>
<h3 className="text-sm font-medium mb-1">Main language</h3>
<p className="text-xs text-neutral-600">
Expand Down
18 changes: 15 additions & 3 deletions apps/desktop/src/components/settings/general/notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,14 +220,18 @@ export function NotificationSettingsView() {
<div className="flex flex-col gap-6">
<form.Field name="notification_event">
{(field) => (
<div className="flex items-start justify-between gap-4">
<div
data-settings-item
className="flex items-start justify-between gap-4"
>
<div className="flex-1">
<h3 className="mb-1 text-sm font-medium">Event notifications</h3>
<p className="text-xs text-neutral-600">
Get notified 5 minutes before calendar events start
</p>
</div>
<Switch
data-settings-activate
checked={field.state.value}
onCheckedChange={field.handleChange}
/>
Expand All @@ -238,7 +242,10 @@ export function NotificationSettingsView() {
<form.Field name="notification_detect">
{(field) => (
<div className="flex flex-col gap-4">
<div className="flex items-start justify-between gap-4">
<div
data-settings-item
className="flex items-start justify-between gap-4"
>
<div className="flex-1">
<h3 className="mb-1 text-sm font-medium">
Microphone detection
Expand All @@ -249,6 +256,7 @@ export function NotificationSettingsView() {
</p>
</div>
<Switch
data-settings-activate
checked={field.state.value}
onCheckedChange={field.handleChange}
/>
Expand Down Expand Up @@ -367,7 +375,10 @@ export function NotificationSettingsView() {

<form.Field name="respect_dnd">
{(field) => (
<div className="flex items-start justify-between gap-4">
<div
data-settings-item
className="flex items-start justify-between gap-4"
>
<div className="flex-1">
<h3 className="mb-1 text-sm font-medium">
Respect Do-Not-Disturb mode
Expand All @@ -378,6 +389,7 @@ export function NotificationSettingsView() {
</p>
</div>
<Switch
data-settings-activate
checked={field.state.value}
onCheckedChange={field.handleChange}
disabled={!anyNotificationEnabled}
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/components/settings/general/permissions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ function PermissionRow({
};

return (
<div className="flex items-center justify-between gap-4">
<div data-settings-item className="flex items-center justify-between gap-4">
<div className="flex-1">
<div
className={cn([
Expand Down Expand Up @@ -103,6 +103,7 @@ function PermissionRow({
</div>
</div>
<Button
data-settings-activate
variant={isAuthorized ? "outline" : "default"}
size="icon"
onClick={handleButtonClick}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export function SearchableSelect({
onSelect={() => handleSelect(option.value)}
className={cn([
"cursor-pointer",
"focus:bg-neutral-200! hover:bg-neutral-200! aria-selected:bg-transparent",
"hover:bg-neutral-200! data-[selected=true]:bg-neutral-200!",
])}
>
<span className="truncate flex-1">{option.label}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function SpokenLanguagesView({
};

return (
<div>
<div data-settings-item>
<h3 className="text-sm font-medium mb-1">Spoken languages</h3>
<p className="text-xs text-neutral-600 mb-3">
Add other languages you use other than the main language
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/components/settings/general/storage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ function StoragePathRow({
};

return (
<div className="flex items-center gap-3">
<div data-settings-item className="flex items-center gap-3">
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<div className="flex items-center gap-2 w-24 shrink-0 cursor-default">
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src/components/settings/general/timezone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ export function TimezoneSelector() {
};

return (
<div className="flex flex-row items-center justify-between">
<div
data-settings-item
className="flex flex-row items-center justify-between"
>
<div>
<h3 className="text-sm font-medium mb-1">Timezone</h3>
<p className="text-xs text-neutral-600">
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src/components/settings/general/week-start.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ export function WeekStartSelector() {
};

return (
<div className="flex flex-row items-center justify-between">
<div
data-settings-item
className="flex flex-row items-center justify-between"
>
<div>
<h3 className="text-sm font-medium mb-1">Week starts on</h3>
<p className="text-xs text-neutral-600">
Expand Down
16 changes: 13 additions & 3 deletions apps/desktop/src/components/settings/lab/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,22 @@ export function SettingsLab() {

return (
<div className="flex flex-col gap-4 pt-3">
<div className="flex items-center justify-between gap-4">
<div
data-settings-item
className="flex items-center justify-between gap-4"
>
<div className="flex-1">
<h3 className="text-sm font-medium mb-1">Control Overlay</h3>
<p className="text-xs text-neutral-600">
Floating window for quick access to recording controls.
</p>
</div>
<Button variant="outline" size="sm" onClick={handleOpenControlWindow}>
<Button
data-settings-activate
variant="outline"
size="sm"
onClick={handleOpenControlWindow}
>
Open
</Button>
</div>
Expand All @@ -47,7 +55,7 @@ function MeetingReminderToggle() {
);

return (
<div className="flex items-center justify-between gap-4">
<div data-settings-item className="flex items-center justify-between gap-4">
<div className="flex-1">
<h3 className="text-sm font-medium mb-1">In-Meeting Reminder</h3>
<p className="text-xs text-neutral-600">
Expand All @@ -56,6 +64,7 @@ function MeetingReminderToggle() {
</p>
</div>
<Switch
data-settings-activate
checked={value}
onCheckedChange={(checked) => setValue(checked)}
/>
Expand Down Expand Up @@ -112,6 +121,7 @@ function DownloadButtons() {
return (
<div
key={channel}
data-settings-item
className="flex items-center justify-between gap-4"
>
<div className="flex-1">
Expand Down
Loading
Loading