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
14 changes: 8 additions & 6 deletions apps/desktop/src/session/components/floating/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ListenButton } from "./listen";
import { MeetingOverCTA } from "./meeting-over-cta";

import {
useCurrentNoteTab,
Expand All @@ -15,20 +16,21 @@ export function FloatingActionButton({
tab: Extract<Tab, { type: "sessions" }>;
chatOpenMode?: "floating" | "right-panel";
}) {
const micStoppedPending = useListener((state) => state.micStoppedPending);
const shouldShowListen = useShouldShowListeningFab(tab);
const shouldShowChat = useShouldShowChatFab(tab);

if (!shouldShowListen && !shouldShowChat) {
if (!micStoppedPending && !shouldShowListen && !shouldShowChat) {
return null;
}

return (
<div className="absolute bottom-4 left-1/2 z-20 -translate-x-1/2">
{shouldShowListen ? (
<ListenButton tab={tab} />
) : (
<ChatCTA openMode={chatOpenMode} />
)}
{micStoppedPending
? <MeetingOverCTA />
: shouldShowListen
? <ListenButton tab={tab} />
: <ChatCTA openMode={chatOpenMode} />}
</div>
);
}
Expand Down
31 changes: 31 additions & 0 deletions apps/desktop/src/session/components/floating/meeting-over-cta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useListener } from "~/stt/contexts";

export function MeetingOverCTA() {
const stop = useListener((state) => state.stop);
const setMicStoppedPending = useListener(
(state) => state.setMicStoppedPending,
);

return (
<div className="flex items-center gap-2 rounded-full border-2 border-stone-600 bg-stone-800 px-4 py-2 text-sm text-white shadow-[0_4px_14px_rgba(87,83,78,0.4)]">
<span>Is your meeting over?</span>
<button
type="button"
onClick={() => {
stop();
setMicStoppedPending(false);
}}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale micStoppedPending flag can stop a subsequent session

High Severity

micStoppedPending is only cleared by the MeetingOverCTA buttons ("Yes"/"No"). If the session stops through any other path — the header "Stop" button, a sleepStateChanged event, or an error — the flag stays true. When the user later starts a new session, the stale CTA appears, and clicking "Yes" calls stop() on the new session, terminating it unexpectedly. The flag needs to be cleared whenever the live session transitions to inactive (e.g., in markLiveInactive or equivalent).

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a7ae905. Configure here.

className="rounded-full bg-white px-3 py-1 text-xs font-medium text-stone-800 transition-colors hover:bg-stone-200"
>
Yes
</button>
<button
type="button"
onClick={() => setMicStoppedPending(false)}
className="rounded-full border border-stone-500 px-3 py-1 text-xs font-medium text-stone-300 transition-colors hover:bg-stone-700"
>
No
</button>
</div>
);
}
2 changes: 2 additions & 0 deletions apps/desktop/src/store/zustand/listener/general-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type LoadingPhase =
export type LiveIntervalId = ReturnType<typeof setInterval>;

export type GeneralState = {
micStoppedPending: boolean;
live: {
eventUnlistenersBySession: Record<string, (() => void)[]>;
loading: boolean;
Expand Down Expand Up @@ -59,6 +60,7 @@ const initialLiveState: LiveState = {
};

export const initialGeneralState: GeneralState = {
micStoppedPending: false,
live: initialLiveState,
};

Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/src/store/zustand/listener/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type GeneralActions = {
options?: { handlePersist?: BatchPersistCallback },
) => Promise<void>;
stopTranscription: (sessionId: string) => Promise<void>;
setMicStoppedPending: (value: boolean) => void;
getSessionMode: (sessionId: string) => SessionMode;
};

Expand Down Expand Up @@ -150,6 +151,13 @@ export const createGeneralSlice = <

await listenerCommands.stopTranscription(sessionId).catch(console.error);
},
setMicStoppedPending: (value) => {
set((state) =>
mutate(state, (draft) => {
draft.micStoppedPending = value;
}),
);
},
getSessionMode: (sessionId) => {
if (!sessionId) {
return "inactive";
Expand Down
7 changes: 5 additions & 2 deletions apps/desktop/src/stt/contexts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useShallow } from "zustand/shallow";

import { events as detectEvents } from "@hypr/plugin-detect";
import { commands as notificationCommands } from "@hypr/plugin-notification";
import { commands as windowsCommands } from "@hypr/plugin-windows";

import * as main from "~/store/tinybase/store/main";
import {
Expand Down Expand Up @@ -87,6 +88,7 @@ function getNearbyEvents(
const useHandleDetectEvents = (store: ListenerStore) => {
const stop = useStore(store, (state) => state.stop);
const setMuted = useStore(store, (state) => state.setMuted);
const setMicStoppedPending = useStore(store, (state) => state.setMicStoppedPending);
const tinybaseStore = main.UI.useStore(main.STORE_ID);

const tinybaseStoreRef = useRef(tinybaseStore);
Expand Down Expand Up @@ -144,7 +146,8 @@ const useHandleDetectEvents = (store: ListenerStore) => {
icon: null,
});
} else if (payload.type === "micStopped") {
stop();
setMicStoppedPending(true);
windowsCommands.windowShow({ type: "main" });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing active session guard for micStopped event handler

Medium Severity

The micStopped event handler sets micStoppedPending to true and shows the main window without checking whether Hypr has an active recording session. The detect plugin fires micStopped at the system level whenever any meeting app releases the mic — independent of Hypr's state. Previously, calling stop() was a harmless no-op when no session was active. Now it results in a confusing "Is your meeting over?" prompt even when the user never started recording. The micDetected handler already has a live.status === "active" guard for exactly this reason, but the micStopped handler lacks an equivalent check.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a7ae905. Configure here.

} else if (payload.type === "sleepStateChanged") {
if (payload.value) {
stop();
Expand All @@ -168,5 +171,5 @@ const useHandleDetectEvents = (store: ListenerStore) => {
cancelled = true;
unlisten?.();
};
}, [stop, setMuted]);
}, [stop, setMuted, setMicStoppedPending]);
};
Loading