From 695a41a3dd65dc45e3e8daa5c37091c44f0933a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B9=B2=E5=98=9B=E7=9A=B1=E7=9C=89=E5=91=A2?= <78773982+2379278408@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:09:10 +0000 Subject: [PATCH 1/4] fix(frontend): reuse dialog keyboard navigation --- .../components/console/task/chat-inputbox.tsx | 24 ++---- frontend/src/components/ui/alert-dialog.tsx | 17 ++++ .../pages/console/user/task/task-detail.tsx | 35 +++----- .../task-restart-dialog-keyboard.test.mjs | 81 +++++++++++++------ 4 files changed, 90 insertions(+), 67 deletions(-) diff --git a/frontend/src/components/console/task/chat-inputbox.tsx b/frontend/src/components/console/task/chat-inputbox.tsx index 764cfd705..bc803d0f8 100644 --- a/frontend/src/components/console/task/chat-inputbox.tsx +++ b/frontend/src/components/console/task/chat-inputbox.tsx @@ -6,7 +6,7 @@ import { VoiceInputButton } from "./voice-input-button" import type { TaskMessageHandlerStatus } from "@/components/console/task/task-message-handler" import type { AvailableCommand, AvailableCommands, TaskStreamStatus, TaskUserInput, TaskUserInputPayload } from "./task-shared" import { Button } from "@/components/ui/button" -import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog" +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, useAlertDialogActionNavigation } from "@/components/ui/alert-dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" import { cn } from "@/lib/utils" @@ -140,8 +140,7 @@ export const TaskChatInputBox = React.forwardRef(null) const quickInputContainerRef = useRef(null) const quickInputMeasureRef = useRef(null) - const slashCommandCancelRef = useRef(null) - const slashCommandConfirmRef = useRef(null) + const slashCommandDialogNavigation = useAlertDialogActionNavigation() const dragDepthRef = useRef(0) const nextAttachmentFileIndexRef = useRef(1) const autoSendingQueuedInputRef = useRef(false) @@ -472,19 +471,6 @@ export const TaskChatInputBox = React.forwardRef) => { - if (event.key === "ArrowLeft") { - event.preventDefault() - slashCommandCancelRef.current?.focus() - return - } - - if (event.key === "ArrowRight") { - event.preventDefault() - slashCommandConfirmRef.current?.focus() - } - } - const handleContentChange = (nextContent: string) => { if ( selectedQuickInputRef.current @@ -1330,7 +1316,7 @@ export const TaskChatInputBox = React.forwardRef - + {t("taskDetail.chat.slashCommand.title")} @@ -1338,8 +1324,8 @@ export const TaskChatInputBox = React.forwardRef - {t("taskDetail.common.cancel")} - + {t("taskDetail.common.cancel")} + {t("taskDetail.chat.slashCommand.confirm")} diff --git a/frontend/src/components/ui/alert-dialog.tsx b/frontend/src/components/ui/alert-dialog.tsx index b2c769b2e..ba2040067 100644 --- a/frontend/src/components/ui/alert-dialog.tsx +++ b/frontend/src/components/ui/alert-dialog.tsx @@ -10,6 +10,22 @@ function AlertDialog({ return } +function useAlertDialogActionNavigation() { + const cancelRef = React.useRef(null) + const confirmRef = React.useRef(null) + const onKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "ArrowLeft") { + event.preventDefault() + cancelRef.current?.focus() + } else if (event.key === "ArrowRight") { + event.preventDefault() + confirmRef.current?.focus() + } + } + + return { cancelRef, confirmRef, onKeyDown } +} + function AlertDialogTrigger({ ...props }: React.ComponentProps) { @@ -194,4 +210,5 @@ export { AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, + useAlertDialogActionNavigation, } diff --git a/frontend/src/pages/console/user/task/task-detail.tsx b/frontend/src/pages/console/user/task/task-detail.tsx index b2a9e87f7..bfb610635 100644 --- a/frontend/src/pages/console/user/task/task-detail.tsx +++ b/frontend/src/pages/console/user/task/task-detail.tsx @@ -25,6 +25,7 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, + useAlertDialogActionNavigation, } from "@/components/ui/alert-dialog" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" @@ -131,8 +132,9 @@ export default function TaskDetailPage() { const historyLoadedRef = React.useRef(false) const chatScrollRef = React.useRef(null) const chatInputRef = React.useRef(null) - const restartAgentCancelRef = React.useRef(null) - const restartAgentConfirmRef = React.useRef(null) + const modelSwitchDialogNavigation = useAlertDialogActionNavigation() + const resetContextDialogNavigation = useAlertDialogActionNavigation() + const restartAgentDialogNavigation = useAlertDialogActionNavigation() const chatContentRef = React.useRef(null) const taskMessageListRef = React.useRef(null) const taskFileExplorerRef = React.useRef(null) @@ -1019,19 +1021,6 @@ export default function TaskDetailPage() { setRestartAgentDialogOpen(true) }, [canInput]) - const handleRestartAgentDialogKeyDown = (event: React.KeyboardEvent) => { - if (event.key === "ArrowLeft") { - event.preventDefault() - restartAgentCancelRef.current?.focus() - return - } - - if (event.key === "ArrowRight") { - event.preventDefault() - restartAgentConfirmRef.current?.focus() - } - } - const handleConfirmRestartAgent = React.useCallback(async () => { if (restartAgentSubmitting) return @@ -1578,7 +1567,7 @@ export default function TaskDetailPage() { } }} > - + {t("taskDetail.page.dialogs.switchModel.title")} @@ -1588,8 +1577,9 @@ export default function TaskDetailPage() { - {t("taskDetail.common.cancel")} + {t("taskDetail.common.cancel")} - diff --git a/frontend/src/pages/console/user/tasks.tsx b/frontend/src/pages/console/user/tasks.tsx index 971b70ac3..e2870f5fd 100644 --- a/frontend/src/pages/console/user/tasks.tsx +++ b/frontend/src/pages/console/user/tasks.tsx @@ -11,6 +11,7 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; +import { useDialogActionNavigation } from "@/components/ui/dialog-action-navigation"; import { HoverCard, HoverCardTrigger } from "@/components/ui/hover-card"; import { Item, ItemContent, ItemFooter, ItemHeader, ItemTitle } from "@/components/ui/item"; import { Separator } from "@/components/ui/separator"; @@ -44,6 +45,8 @@ export default function TasksPage() { const [deleting, setDeleting] = useState(false) const [taskToStop, setTaskToStop] = useState(null) const [stopping, setStopping] = useState(false) + const deleteTaskDialogNavigation = useDialogActionNavigation() + const stopTaskDialogNavigation = useDialogActionNavigation() const loadMoreRef = useRef(null) const loadingRef = useRef(false) // Open Design handoff: od-web carries the prompt in the #od-task= fragment. @@ -275,7 +278,7 @@ export default function TasksPage() { {loading && } !open && setTaskToDelete(null)}> - + {t("consoleTasks.dialog.delete.title")} @@ -283,8 +286,9 @@ export default function TasksPage() { - {t("consoleTasks.dialog.common.cancel")} + {t("consoleTasks.dialog.common.cancel")} { e.preventDefault() handleConfirmDeleteTask() @@ -298,7 +302,7 @@ export default function TasksPage() { !open && setTaskToStop(null)}> - + {t("consoleTasks.dialog.stop.title")} @@ -306,8 +310,9 @@ export default function TasksPage() { - {t("consoleTasks.dialog.common.cancel")} + {t("consoleTasks.dialog.common.cancel")} { e.preventDefault() handleConfirmStopTask() diff --git a/frontend/test/task-restart-dialog-keyboard.test.mjs b/frontend/test/task-restart-dialog-keyboard.test.mjs index 8a5205197..2f5b2574c 100644 --- a/frontend/test/task-restart-dialog-keyboard.test.mjs +++ b/frontend/test/task-restart-dialog-keyboard.test.mjs @@ -10,33 +10,65 @@ const chatInputSource = readFileSync( new URL("../src/components/console/task/chat-inputbox.tsx", import.meta.url), "utf8", ); -const alertDialogSource = readFileSync( - new URL("../src/components/ui/alert-dialog.tsx", import.meta.url), +const tasksPageSource = readFileSync( + new URL("../src/pages/console/user/tasks.tsx", import.meta.url), + "utf8", +); +const projectTasksSource = readFileSync( + new URL("../src/pages/console/user/project/overview/tasks-tab.tsx", import.meta.url), + "utf8", +); +const navProjectSource = readFileSync( + new URL("../src/components/console/nav/nav-project.tsx", import.meta.url), + "utf8", +); +const longContentSource = readFileSync( + new URL("../src/components/console/task/task-long-content-dialog.tsx", import.meta.url), + "utf8", +); +const fileActionsSource = readFileSync( + new URL("../src/components/console/task/file-actions-dropdown.tsx", import.meta.url), + "utf8", +); +const terminalPanelSource = readFileSync( + new URL("../src/components/console/task/task-terminal-panel.tsx", import.meta.url), + "utf8", +); +const whiteboardSource = readFileSync( + new URL("../src/components/console/task/task-whiteboard-dialog.tsx", import.meta.url), + "utf8", +); +const dialogNavigationSource = readFileSync( + new URL("../src/components/ui/dialog-action-navigation.ts", import.meta.url), "utf8", ); -const getDialogSource = (source, openState) => { - const match = source.match(new RegExp(``)); - assert.ok(match, `${openState} dialog should be present`); +const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +const getAlertDialogSource = (source, openExpression) => { + const match = source.match(new RegExp(``)); + assert.ok(match, `${openExpression} alert dialog should be present`); return match[0]; }; -test("AlertDialog 双操作按钮共享左右方向键导航", () => { - const handlerStart = alertDialogSource.indexOf("function useAlertDialogActionNavigation"); - const handlerEnd = alertDialogSource.indexOf("function AlertDialogTrigger", handlerStart); - assert.notEqual(handlerStart, -1, "alert dialog should export shared action navigation"); - assert.notEqual(handlerEnd, -1, "shared navigation should precede dialog components"); - const handlerSource = alertDialogSource.slice(handlerStart, handlerEnd); - const leftBranch = handlerSource.match(/if \(event\.key === "ArrowLeft"\) \{([\s\S]*?)\n \}/); - const rightBranch = handlerSource.match(/else if \(event\.key === "ArrowRight"\) \{([\s\S]*?)\n \}/); +const assertDialogNavigation = (source, dialogSource, navigationName, contentName = "AlertDialogContent") => { + assert.match(source, new RegExp(`const ${navigationName} = useDialogActionNavigation\\(\\)`)); + assert.match(dialogSource, new RegExp(`<${contentName}\\b[\\s\\S]*?onKeyDown=\\{${navigationName}\\.onKeyDown\\}`)); + assert.match(dialogSource, new RegExp(`<(?:AlertDialogCancel|Button)[^>]*ref=\\{${navigationName}\\.cancelRef\\}`)); + assert.match(dialogSource, new RegExp(`<(?:AlertDialogAction|Button)[^>]*ref=\\{${navigationName}\\.confirmRef\\}`)); +}; + +test("双操作确认弹窗共享左右方向键导航", () => { + const leftBranch = dialogNavigationSource.match(/if \(event\.key === "ArrowLeft"\) \{([\s\S]*?)\n \}/); + const rightBranch = dialogNavigationSource.match(/else if \(event\.key === "ArrowRight"\) \{([\s\S]*?)\n \}/); assert.ok(leftBranch, "shared navigation should handle ArrowLeft"); assert.ok(rightBranch, "shared navigation should handle ArrowRight"); assert.match(leftBranch[1], /event\.preventDefault\(\)/); assert.match(leftBranch[1], /cancelRef\.current\?\.focus\(\)/); assert.match(rightBranch[1], /event\.preventDefault\(\)/); assert.match(rightBranch[1], /confirmRef\.current\?\.focus\(\)/); - assert.doesNotMatch(handlerSource, /event\.key === "Enter"/); - assert.match(alertDialogSource, /useAlertDialogActionNavigation,/); + assert.doesNotMatch(dialogNavigationSource, /event\.key === "Enter"/); + assert.match(dialogNavigationSource, /export \{ useDialogActionNavigation \}/); }); test("任务确认弹窗复用共享键盘导航", () => { @@ -47,10 +79,9 @@ test("任务确认弹窗复用共享键盘导航", () => { ]; for (const [dialogName, openState, submittingState, confirmHandler] of dialogs) { - const dialogSource = getDialogSource(pageSource, openState); - assert.match(pageSource, new RegExp(`const ${dialogName}DialogNavigation = useAlertDialogActionNavigation\\(\\)`)); - assert.match(dialogSource, new RegExp(``)); - assert.match(dialogSource, new RegExp(``)); + const dialogSource = getAlertDialogSource(pageSource, openState); + assertDialogNavigation(pageSource, dialogSource, `${dialogName}DialogNavigation`); + assert.match(dialogSource, new RegExp(`]*disabled=\\{${submittingState}\\}>`)); assert.match(dialogSource, new RegExp(`ref=\\{${dialogName}DialogNavigation\\.confirmRef\\}[\\s\\S]*?type="button"`)); assert.match(dialogSource, new RegExp(`void ${confirmHandler}\\(\\)`)); assert.match(dialogSource, new RegExp(`disabled=\\{${submittingState}\\}`)); @@ -62,10 +93,40 @@ test("任务确认弹窗复用共享键盘导航", () => { }); test("Slash 命令确认弹窗复用共享键盘导航", () => { - const dialogSource = getDialogSource(chatInputSource, "slashCommandConfirmOpen"); - assert.match(chatInputSource, /const slashCommandDialogNavigation = useAlertDialogActionNavigation\(\)/); - assert.match(dialogSource, //); - assert.match(dialogSource, //); - assert.match(dialogSource, / { + const dialogs = [ + [tasksPageSource, "!!taskToDelete", "deleteTaskDialogNavigation"], + [tasksPageSource, "!!taskToStop", "stopTaskDialogNavigation"], + [projectTasksSource, "!!taskToDelete", "deleteTaskDialogNavigation"], + [navProjectSource, "!!taskToDelete", "deleteTaskDialogNavigation"], + [navProjectSource, "!!taskToStop", "stopTaskDialogNavigation"], + ]; + + for (const [source, openExpression, navigationName] of dialogs) { + assertDialogNavigation(source, getAlertDialogSource(source, openExpression), navigationName); + } +}); + +test("任务辅助操作确认弹窗复用共享键盘导航", () => { + const dialogs = [ + [longContentSource, "open", "dialogNavigation"], + [fileActionsSource, "deleteDialogOpen", "deleteDialogNavigation"], + [terminalPanelSource, "closeDialogOpen", "closeDialogNavigation"], + [whiteboardSource, "resetDialogOpen", "resetDialogNavigation"], + ]; + + for (const [source, openExpression, navigationName] of dialogs) { + assertDialogNavigation(source, getAlertDialogSource(source, openExpression), navigationName); + } +}); + +test("发布网站确认弹窗复用共享键盘导航", () => { + const dialogMatch = pageSource.match(//); + assert.ok(dialogMatch, "publish website dialog should be present"); + assertDialogNavigation(pageSource, dialogMatch[0], "publishWebsiteDialogNavigation", "DialogContent"); +}); From e105e348b8810ffa3dfb5f331b4838f4c30f71ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B9=B2=E5=98=9B=E7=9A=B1=E7=9C=89=E5=91=A2?= <78773982+2379278408@users.noreply.github.com> Date: Fri, 7 Aug 2026 04:13:51 +0000 Subject: [PATCH 3/4] fix(frontend): narrow dialog keyboard navigation --- .../components/console/nav/nav-project.tsx | 13 +++------- .../user/project/overview/tasks-tab.tsx | 7 ++--- frontend/src/pages/console/user/tasks.tsx | 13 +++------- .../console-nav-project-dialog-layout.test.ts | 2 +- .../task-restart-dialog-keyboard.test.mjs | 26 ------------------- 5 files changed, 11 insertions(+), 50 deletions(-) diff --git a/frontend/src/components/console/nav/nav-project.tsx b/frontend/src/components/console/nav/nav-project.tsx index fb1f72a46..8251b3528 100644 --- a/frontend/src/components/console/nav/nav-project.tsx +++ b/frontend/src/components/console/nav/nav-project.tsx @@ -21,7 +21,6 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" -import { useDialogActionNavigation } from "@/components/ui/dialog-action-navigation" import { useCommonData } from "../data-provider" import { IconChevronDown, IconChevronRight, IconDots, IconFolder, IconFolderOpen, IconFolderPlus, IconLoader, IconPlus, IconPointFilled } from "@tabler/icons-react" import { Button } from "@/components/ui/button" @@ -105,8 +104,6 @@ export default function NavProject() { const [deleting, setDeleting] = useState(false) const [taskToStop, setTaskToStop] = useState(null) const [stopping, setStopping] = useState(false) - const deleteTaskDialogNavigation = useDialogActionNavigation() - const stopTaskDialogNavigation = useDialogActionNavigation() const [historyExpanded, setHistoryExpanded] = useState(false) const { projects, reloadProjects, unlinkedTasks, reloadUnlinkedTasks, historicalTasks, reloadHistoricalTasks } = useCommonData() @@ -410,7 +407,7 @@ export default function NavProject() { )} !open && setTaskToDelete(null)}> - + - {t("navProject.common.cancel")} + {t("navProject.common.cancel")} { e.preventDefault() handleConfirmDeleteTask() @@ -439,7 +435,7 @@ export default function NavProject() { !open && setTaskToStop(null)}> - + - {t("navProject.common.cancel")} + {t("navProject.common.cancel")} { e.preventDefault() handleConfirmStopTask() diff --git a/frontend/src/pages/console/user/project/overview/tasks-tab.tsx b/frontend/src/pages/console/user/project/overview/tasks-tab.tsx index 9aff0b41a..34f40d16c 100644 --- a/frontend/src/pages/console/user/project/overview/tasks-tab.tsx +++ b/frontend/src/pages/console/user/project/overview/tasks-tab.tsx @@ -14,7 +14,6 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog" import { Badge } from "@/components/ui/badge" -import { useDialogActionNavigation } from "@/components/ui/dialog-action-navigation" import { HoverCard, HoverCardTrigger } from "@/components/ui/hover-card" import { Item, ItemContent, ItemDescription, ItemFooter, ItemHeader, ItemTitle } from "@/components/ui/item" import { Spinner } from "@/components/ui/spinner" @@ -53,7 +52,6 @@ export default function ProjectOverviewTasksTab({ projectId, refreshKey }: Proje const [tasksInitialLoading, setTasksInitialLoading] = useState(true) const [taskToDelete, setTaskToDelete] = useState(null) const [deleting, setDeleting] = useState(false) - const deleteTaskDialogNavigation = useDialogActionNavigation() const loadMoreRef = useRef(null) const tasksLoadingRef = useRef(false) @@ -288,7 +286,7 @@ export default function ProjectOverviewTasksTab({ projectId, refreshKey }: Proje {tasksLoading && } !open && setTaskToDelete(null)}> - + {t("projectOverview.tasks.delete.title")} @@ -296,9 +294,8 @@ export default function ProjectOverviewTasksTab({ projectId, refreshKey }: Proje - {t("projectOverview.common.cancel")} + {t("projectOverview.common.cancel")} { e.preventDefault() handleConfirmDeleteTask() diff --git a/frontend/src/pages/console/user/tasks.tsx b/frontend/src/pages/console/user/tasks.tsx index e2870f5fd..971b70ac3 100644 --- a/frontend/src/pages/console/user/tasks.tsx +++ b/frontend/src/pages/console/user/tasks.tsx @@ -11,7 +11,6 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; -import { useDialogActionNavigation } from "@/components/ui/dialog-action-navigation"; import { HoverCard, HoverCardTrigger } from "@/components/ui/hover-card"; import { Item, ItemContent, ItemFooter, ItemHeader, ItemTitle } from "@/components/ui/item"; import { Separator } from "@/components/ui/separator"; @@ -45,8 +44,6 @@ export default function TasksPage() { const [deleting, setDeleting] = useState(false) const [taskToStop, setTaskToStop] = useState(null) const [stopping, setStopping] = useState(false) - const deleteTaskDialogNavigation = useDialogActionNavigation() - const stopTaskDialogNavigation = useDialogActionNavigation() const loadMoreRef = useRef(null) const loadingRef = useRef(false) // Open Design handoff: od-web carries the prompt in the #od-task= fragment. @@ -278,7 +275,7 @@ export default function TasksPage() { {loading && } !open && setTaskToDelete(null)}> - + {t("consoleTasks.dialog.delete.title")} @@ -286,9 +283,8 @@ export default function TasksPage() { - {t("consoleTasks.dialog.common.cancel")} + {t("consoleTasks.dialog.common.cancel")} { e.preventDefault() handleConfirmDeleteTask() @@ -302,7 +298,7 @@ export default function TasksPage() { !open && setTaskToStop(null)}> - + {t("consoleTasks.dialog.stop.title")} @@ -310,9 +306,8 @@ export default function TasksPage() { - {t("consoleTasks.dialog.common.cancel")} + {t("consoleTasks.dialog.common.cancel")} { e.preventDefault() handleConfirmStopTask() diff --git a/frontend/test/console-nav-project-dialog-layout.test.ts b/frontend/test/console-nav-project-dialog-layout.test.ts index be7beb427..071882c06 100644 --- a/frontend/test/console-nav-project-dialog-layout.test.ts +++ b/frontend/test/console-nav-project-dialog-layout.test.ts @@ -22,7 +22,7 @@ function assertDialogLayout(stateName: string, titleKey: string) { assert.match( dialogSource, - /]*className="max-h-\[calc\(100dvh-2rem\)\] grid-rows-\[minmax\(0,1fr\)_auto\] overflow-hidden"[^>]*>/, + //, ) assert.ok( dialogSource.includes( diff --git a/frontend/test/task-restart-dialog-keyboard.test.mjs b/frontend/test/task-restart-dialog-keyboard.test.mjs index 2f5b2574c..1ab0b151c 100644 --- a/frontend/test/task-restart-dialog-keyboard.test.mjs +++ b/frontend/test/task-restart-dialog-keyboard.test.mjs @@ -10,18 +10,6 @@ const chatInputSource = readFileSync( new URL("../src/components/console/task/chat-inputbox.tsx", import.meta.url), "utf8", ); -const tasksPageSource = readFileSync( - new URL("../src/pages/console/user/tasks.tsx", import.meta.url), - "utf8", -); -const projectTasksSource = readFileSync( - new URL("../src/pages/console/user/project/overview/tasks-tab.tsx", import.meta.url), - "utf8", -); -const navProjectSource = readFileSync( - new URL("../src/components/console/nav/nav-project.tsx", import.meta.url), - "utf8", -); const longContentSource = readFileSync( new URL("../src/components/console/task/task-long-content-dialog.tsx", import.meta.url), "utf8", @@ -98,20 +86,6 @@ test("Slash 命令确认弹窗复用共享键盘导航", () => { assert.doesNotMatch(chatInputSource, /handleSlashCommandDialogKeyDown/); }); -test("任务列表和侧边栏的删除终止弹窗复用共享键盘导航", () => { - const dialogs = [ - [tasksPageSource, "!!taskToDelete", "deleteTaskDialogNavigation"], - [tasksPageSource, "!!taskToStop", "stopTaskDialogNavigation"], - [projectTasksSource, "!!taskToDelete", "deleteTaskDialogNavigation"], - [navProjectSource, "!!taskToDelete", "deleteTaskDialogNavigation"], - [navProjectSource, "!!taskToStop", "stopTaskDialogNavigation"], - ]; - - for (const [source, openExpression, navigationName] of dialogs) { - assertDialogNavigation(source, getAlertDialogSource(source, openExpression), navigationName); - } -}); - test("任务辅助操作确认弹窗复用共享键盘导航", () => { const dialogs = [ [longContentSource, "open", "dialogNavigation"], From d9b6ce7352f868a4510c6f3b8555e573388ca586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B9=B2=E5=98=9B=E7=9A=B1=E7=9C=89=E5=91=A2?= <78773982+2379278408@users.noreply.github.com> Date: Fri, 7 Aug 2026 04:18:08 +0000 Subject: [PATCH 4/4] Revert "fix(frontend): narrow dialog keyboard navigation" This reverts commit e105e348b8810ffa3dfb5f331b4838f4c30f71ca. --- .../components/console/nav/nav-project.tsx | 13 +++++++--- .../user/project/overview/tasks-tab.tsx | 7 +++-- frontend/src/pages/console/user/tasks.tsx | 13 +++++++--- .../console-nav-project-dialog-layout.test.ts | 2 +- .../task-restart-dialog-keyboard.test.mjs | 26 +++++++++++++++++++ 5 files changed, 50 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/console/nav/nav-project.tsx b/frontend/src/components/console/nav/nav-project.tsx index 8251b3528..fb1f72a46 100644 --- a/frontend/src/components/console/nav/nav-project.tsx +++ b/frontend/src/components/console/nav/nav-project.tsx @@ -21,6 +21,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" +import { useDialogActionNavigation } from "@/components/ui/dialog-action-navigation" import { useCommonData } from "../data-provider" import { IconChevronDown, IconChevronRight, IconDots, IconFolder, IconFolderOpen, IconFolderPlus, IconLoader, IconPlus, IconPointFilled } from "@tabler/icons-react" import { Button } from "@/components/ui/button" @@ -104,6 +105,8 @@ export default function NavProject() { const [deleting, setDeleting] = useState(false) const [taskToStop, setTaskToStop] = useState(null) const [stopping, setStopping] = useState(false) + const deleteTaskDialogNavigation = useDialogActionNavigation() + const stopTaskDialogNavigation = useDialogActionNavigation() const [historyExpanded, setHistoryExpanded] = useState(false) const { projects, reloadProjects, unlinkedTasks, reloadUnlinkedTasks, historicalTasks, reloadHistoricalTasks } = useCommonData() @@ -407,7 +410,7 @@ export default function NavProject() { )} !open && setTaskToDelete(null)}> - + - {t("navProject.common.cancel")} + {t("navProject.common.cancel")} { e.preventDefault() handleConfirmDeleteTask() @@ -435,7 +439,7 @@ export default function NavProject() { !open && setTaskToStop(null)}> - + - {t("navProject.common.cancel")} + {t("navProject.common.cancel")} { e.preventDefault() handleConfirmStopTask() diff --git a/frontend/src/pages/console/user/project/overview/tasks-tab.tsx b/frontend/src/pages/console/user/project/overview/tasks-tab.tsx index 34f40d16c..9aff0b41a 100644 --- a/frontend/src/pages/console/user/project/overview/tasks-tab.tsx +++ b/frontend/src/pages/console/user/project/overview/tasks-tab.tsx @@ -14,6 +14,7 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog" import { Badge } from "@/components/ui/badge" +import { useDialogActionNavigation } from "@/components/ui/dialog-action-navigation" import { HoverCard, HoverCardTrigger } from "@/components/ui/hover-card" import { Item, ItemContent, ItemDescription, ItemFooter, ItemHeader, ItemTitle } from "@/components/ui/item" import { Spinner } from "@/components/ui/spinner" @@ -52,6 +53,7 @@ export default function ProjectOverviewTasksTab({ projectId, refreshKey }: Proje const [tasksInitialLoading, setTasksInitialLoading] = useState(true) const [taskToDelete, setTaskToDelete] = useState(null) const [deleting, setDeleting] = useState(false) + const deleteTaskDialogNavigation = useDialogActionNavigation() const loadMoreRef = useRef(null) const tasksLoadingRef = useRef(false) @@ -286,7 +288,7 @@ export default function ProjectOverviewTasksTab({ projectId, refreshKey }: Proje {tasksLoading && } !open && setTaskToDelete(null)}> - + {t("projectOverview.tasks.delete.title")} @@ -294,8 +296,9 @@ export default function ProjectOverviewTasksTab({ projectId, refreshKey }: Proje - {t("projectOverview.common.cancel")} + {t("projectOverview.common.cancel")} { e.preventDefault() handleConfirmDeleteTask() diff --git a/frontend/src/pages/console/user/tasks.tsx b/frontend/src/pages/console/user/tasks.tsx index 971b70ac3..e2870f5fd 100644 --- a/frontend/src/pages/console/user/tasks.tsx +++ b/frontend/src/pages/console/user/tasks.tsx @@ -11,6 +11,7 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; +import { useDialogActionNavigation } from "@/components/ui/dialog-action-navigation"; import { HoverCard, HoverCardTrigger } from "@/components/ui/hover-card"; import { Item, ItemContent, ItemFooter, ItemHeader, ItemTitle } from "@/components/ui/item"; import { Separator } from "@/components/ui/separator"; @@ -44,6 +45,8 @@ export default function TasksPage() { const [deleting, setDeleting] = useState(false) const [taskToStop, setTaskToStop] = useState(null) const [stopping, setStopping] = useState(false) + const deleteTaskDialogNavigation = useDialogActionNavigation() + const stopTaskDialogNavigation = useDialogActionNavigation() const loadMoreRef = useRef(null) const loadingRef = useRef(false) // Open Design handoff: od-web carries the prompt in the #od-task= fragment. @@ -275,7 +278,7 @@ export default function TasksPage() { {loading && } !open && setTaskToDelete(null)}> - + {t("consoleTasks.dialog.delete.title")} @@ -283,8 +286,9 @@ export default function TasksPage() { - {t("consoleTasks.dialog.common.cancel")} + {t("consoleTasks.dialog.common.cancel")} { e.preventDefault() handleConfirmDeleteTask() @@ -298,7 +302,7 @@ export default function TasksPage() { !open && setTaskToStop(null)}> - + {t("consoleTasks.dialog.stop.title")} @@ -306,8 +310,9 @@ export default function TasksPage() { - {t("consoleTasks.dialog.common.cancel")} + {t("consoleTasks.dialog.common.cancel")} { e.preventDefault() handleConfirmStopTask() diff --git a/frontend/test/console-nav-project-dialog-layout.test.ts b/frontend/test/console-nav-project-dialog-layout.test.ts index 071882c06..be7beb427 100644 --- a/frontend/test/console-nav-project-dialog-layout.test.ts +++ b/frontend/test/console-nav-project-dialog-layout.test.ts @@ -22,7 +22,7 @@ function assertDialogLayout(stateName: string, titleKey: string) { assert.match( dialogSource, - //, + /]*className="max-h-\[calc\(100dvh-2rem\)\] grid-rows-\[minmax\(0,1fr\)_auto\] overflow-hidden"[^>]*>/, ) assert.ok( dialogSource.includes( diff --git a/frontend/test/task-restart-dialog-keyboard.test.mjs b/frontend/test/task-restart-dialog-keyboard.test.mjs index 1ab0b151c..2f5b2574c 100644 --- a/frontend/test/task-restart-dialog-keyboard.test.mjs +++ b/frontend/test/task-restart-dialog-keyboard.test.mjs @@ -10,6 +10,18 @@ const chatInputSource = readFileSync( new URL("../src/components/console/task/chat-inputbox.tsx", import.meta.url), "utf8", ); +const tasksPageSource = readFileSync( + new URL("../src/pages/console/user/tasks.tsx", import.meta.url), + "utf8", +); +const projectTasksSource = readFileSync( + new URL("../src/pages/console/user/project/overview/tasks-tab.tsx", import.meta.url), + "utf8", +); +const navProjectSource = readFileSync( + new URL("../src/components/console/nav/nav-project.tsx", import.meta.url), + "utf8", +); const longContentSource = readFileSync( new URL("../src/components/console/task/task-long-content-dialog.tsx", import.meta.url), "utf8", @@ -86,6 +98,20 @@ test("Slash 命令确认弹窗复用共享键盘导航", () => { assert.doesNotMatch(chatInputSource, /handleSlashCommandDialogKeyDown/); }); +test("任务列表和侧边栏的删除终止弹窗复用共享键盘导航", () => { + const dialogs = [ + [tasksPageSource, "!!taskToDelete", "deleteTaskDialogNavigation"], + [tasksPageSource, "!!taskToStop", "stopTaskDialogNavigation"], + [projectTasksSource, "!!taskToDelete", "deleteTaskDialogNavigation"], + [navProjectSource, "!!taskToDelete", "deleteTaskDialogNavigation"], + [navProjectSource, "!!taskToStop", "stopTaskDialogNavigation"], + ]; + + for (const [source, openExpression, navigationName] of dialogs) { + assertDialogNavigation(source, getAlertDialogSource(source, openExpression), navigationName); + } +}); + test("任务辅助操作确认弹窗复用共享键盘导航", () => { const dialogs = [ [longContentSource, "open", "dialogNavigation"],