+
void;
+ shortcutScopeRef: RefObject;
keyboardShortcutsEnabled?: boolean;
};
@@ -169,6 +172,7 @@ function useResolveCallback(
type CarouselShortcutArgs = {
enabled: boolean;
+ scopeRef: RefObject;
meta: SingleQuestionMeta;
activeIndex: number;
total: number;
@@ -211,13 +215,14 @@ function tryHandleMetaEnter(e: KeyboardEvent, canSubmit: boolean, onSubmit: () =
}
function CarouselKeyboardShortcuts(args: CarouselShortcutArgs) {
- const { enabled } = args;
+ const { enabled, scopeRef } = args;
const optionsCount = args.meta.question.options.length;
const isLast = args.activeIndex === args.total - 1;
const { canSubmit, onPick, onPrev, onNext, onSkip, onSubmit } = args;
useEffect(() => {
if (!enabled) return;
const onKey = (e: KeyboardEvent) => {
+ if (!(e.target instanceof Node) || !scopeRef.current?.contains(e.target)) return;
if (tryHandleMetaEnter(e, canSubmit, onSubmit)) return;
if (shouldIgnoreShortcut(e)) return;
if (e.key === "Escape") {
@@ -244,7 +249,18 @@ function CarouselKeyboardShortcuts(args: CarouselShortcutArgs) {
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
- }, [enabled, optionsCount, isLast, canSubmit, onPick, onPrev, onNext, onSkip, onSubmit]);
+ }, [
+ enabled,
+ scopeRef,
+ optionsCount,
+ isLast,
+ canSubmit,
+ onPick,
+ onPrev,
+ onNext,
+ onSkip,
+ onSubmit,
+ ]);
return null;
}
@@ -257,6 +273,7 @@ type CarouselBodyProps = {
setCustomDrafts: React.Dispatch>>;
allAnswered: boolean;
isSubmitting: boolean;
+ shortcutScopeRef: RefObject;
keyboardShortcutsEnabled: boolean;
onSubmit: () => void;
};
@@ -362,6 +379,7 @@ function ClarificationCarouselBody({
setCustomDrafts,
allAnswered,
isSubmitting,
+ shortcutScopeRef,
keyboardShortcutsEnabled,
onSubmit,
}: CarouselBodyProps) {
@@ -418,7 +436,8 @@ function ClarificationCarouselBody({
/>
)}
void;
+ onSkip: () => void;
+}) {
+ return (
+
+ {total > 1 && (
+
+
+
+
+
+ )}
+
+
+
+
+
+
+ );
+}
+
export function ClarificationInputOverlay({
messages,
onResolved,
+ shortcutScopeRef,
keyboardShortcutsEnabled = true,
}: ClarificationInputOverlayProps) {
const sortedMessages = useMemo(
@@ -492,35 +578,13 @@ export function ClarificationInputOverlay({
)}
-
- {total > 1 && (
-
- )}
-
-
+
void group.skipAll("User skipped")}
+ />
diff --git a/apps/web/components/task/chat/clarification-overlay-parts.tsx b/apps/web/components/task/chat/clarification-overlay-parts.tsx
index 105e0a74bb..2e1ec3877e 100644
--- a/apps/web/components/task/chat/clarification-overlay-parts.tsx
+++ b/apps/web/components/task/chat/clarification-overlay-parts.tsx
@@ -6,6 +6,8 @@ import { Textarea } from "@kandev/ui/textarea";
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
import { cn } from "@/lib/utils";
import type { ClarificationOption } from "@/lib/types/http";
+import { KeyboardShortcutTooltip } from "@/components/keyboard-shortcut-tooltip";
+import { KEYS } from "@/lib/keyboard/constants";
// Grow the custom-answer box up to ~6 lines, then scroll internally so the
// clarification overlay stays compact.
@@ -328,36 +330,52 @@ export function ClarificationCarouselNav({
const isLast = activeIndex === total - 1;
return (
-
-
+
+
+ Next
+
+
+
+
);
}
diff --git a/apps/web/components/task/chat/route-panel-mouse-down.ts b/apps/web/components/task/chat/route-panel-mouse-down.ts
new file mode 100644
index 0000000000..ec2c29209a
--- /dev/null
+++ b/apps/web/components/task/chat/route-panel-mouse-down.ts
@@ -0,0 +1,13 @@
+import type { MouseEvent, RefObject } from "react";
+
+const interactiveSelector =
+ "input, textarea, select, button, a, [contenteditable], [tabindex]:not([tabindex='-1'])";
+
+export function routePanelMouseDown(
+ event: MouseEvent
,
+ ref: RefObject,
+): void {
+ const target = event.target as HTMLElement | null;
+ if (!target || target.closest(interactiveSelector)) return;
+ ref.current?.focus({ preventScroll: true });
+}
diff --git a/apps/web/components/task/task-chat-panel.tsx b/apps/web/components/task/task-chat-panel.tsx
index 5016c83f99..caab03007a 100644
--- a/apps/web/components/task/task-chat-panel.tsx
+++ b/apps/web/components/task/task-chat-panel.tsx
@@ -18,6 +18,7 @@ import { useSessionSearch } from "@/hooks/domains/session/use-session-search";
import { useLazyLoadMessages } from "@/hooks/use-lazy-load-messages";
import { useAppStore } from "@/components/state-provider";
import type { Message } from "@/lib/types/http";
+import { routePanelMouseDown } from "./chat/route-panel-mouse-down";
function useClarificationKey(agentMessageCount: number) {
const lastCountRef = useRef(agentMessageCount);
@@ -236,6 +237,7 @@ export const TaskChatPanel = memo(function TaskChatPanel({
height={clarificationHeight}
messages={pendingClarificationGroup}
onResolved={handleClarificationResolved}
+ shortcutScopeRef={panelRef}
/>
void;
+ shortcutScopeRef: RefObject;
};
function ClarificationSection({
@@ -272,6 +275,7 @@ function ClarificationSection({
height,
messages,
onResolved,
+ shortcutScopeRef,
}: ClarificationSectionProps) {
if (!pendingClarification || isArchived) return null;
return (
@@ -283,7 +287,11 @@ function ClarificationSection({
className="px-1 overflow-y-scroll overscroll-contain max-h-[50vh]"
style={height === null ? undefined : { height }}
>
-
+
);
@@ -340,21 +348,3 @@ function ChatFooter({
/>
);
}
-
-// routePanelMouseDown routes non-interactive clicks to the panel root so that
-// Ctrl+F can detect focus within the session panel. Extracted to keep
-// TaskChatPanel's cyclomatic complexity within limits.
-const interactiveSelector =
- "input, textarea, select, button, a, [contenteditable], [tabindex]:not([tabindex='-1'])";
-
-function routePanelMouseDown(
- e: React.MouseEvent