| author | |
| committer | |
| log | fcbf3f3c225ab41c44af3759ae27d8520ced61a4 |
| tree | d85a771d4b2672fd10632a0c20e3ca2403a4af27 |
| parent | ab0002f99d6fb5555c57bc91f696e6a9f8f4dfd7 |
- move chat-wide key handling into `_chat` route-level shortcut handler
- extract reusable `useHandleNewThread` hook and `isTerminalFocused` helper
- update browser WS fixture to support `terminalOpen` RPC shape6 files changed, 221 insertions(+), 133 deletions(-)
apps/web/src/components/ChatView.browser.tsx+16-2| ... | ... | @@ -353,7 +353,8 @@ function createSnapshotWithLongProposedPlan(): OrchestrationReadModel { |
| 353 | 353 | }; |
| 354 | 354 | } |
| 355 | 355 | |
| 356 | function resolveWsRpc(tag: string): unknown { | |
| 356 | function resolveWsRpc(body: WsRequestEnvelope["body"]): unknown { | |
| 357 | const tag = body._tag; | |
| 357 | 358 | if (tag === ORCHESTRATION_WS_METHODS.getSnapshot) { |
| 358 | 359 | return fixture.snapshot; |
| 359 | 360 | } |
| ... | ... | @@ -395,6 +396,19 @@ function resolveWsRpc(tag: string): unknown { |
| 395 | 396 | truncated: false, |
| 396 | 397 | }; |
| 397 | 398 | } |
| 399 | if (tag === WS_METHODS.terminalOpen) { | |
| 400 | return { | |
| 401 | threadId: typeof body.threadId === "string" ? body.threadId : THREAD_ID, | |
| 402 | terminalId: typeof body.terminalId === "string" ? body.terminalId : "default", | |
| 403 | cwd: typeof body.cwd === "string" ? body.cwd : "/repo/project", | |
| 404 | status: "running", | |
| 405 | pid: 123, | |
| 406 | history: "", | |
| 407 | exitCode: null, | |
| 408 | exitSignal: null, | |
| 409 | updatedAt: NOW_ISO, | |
| 410 | }; | |
| 411 | } | |
| 398 | 412 | return {}; |
| 399 | 413 | } |
| 400 | 414 | |
| ... | ... | @@ -423,7 +437,7 @@ const worker = setupWorker( |
| 423 | 437 | client.send( |
| 424 | 438 | JSON.stringify({ |
| 425 | 439 | id: request.id, |
| 426 | result: resolveWsRpc(method), | |
| 440 | result: resolveWsRpc(request.body), | |
| 427 | 441 | }), |
| 428 | 442 | ); |
| 429 | 443 | }); |
apps/web/src/components/ChatView.tsx+1-7| ... | ... | @@ -117,6 +117,7 @@ import { SidebarTrigger } from "./ui/sidebar"; |
| 117 | 117 | import { newCommandId, newMessageId, newThreadId } from "~/lib/utils"; |
| 118 | 118 | import { readNativeApi } from "~/nativeApi"; |
| 119 | 119 | import { resolveAppModelSelection, useAppSettings } from "../appSettings"; |
| 120 | import { isTerminalFocused } from "../lib/terminalFocus"; | |
| 120 | 121 | import { |
| 121 | 122 | type ComposerImageAttachment, |
| 122 | 123 | type DraftThreadEnvMode, |
| ... | ... | @@ -1937,13 +1938,6 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1937 | 1938 | }, [activeThreadId, focusComposer, terminalState.terminalOpen]); |
| 1938 | 1939 | |
| 1939 | 1940 | useEffect(() => { |
| 1940 | const isTerminalFocused = (): boolean => { | |
| 1941 | const activeElement = document.activeElement; | |
| 1942 | if (!(activeElement instanceof HTMLElement)) return false; | |
| 1943 | if (activeElement.classList.contains("xterm-helper-textarea")) return true; | |
| 1944 | return activeElement.closest(".thread-terminal-drawer .xterm") !== null; | |
| 1945 | }; | |
| 1946 | ||
| 1947 | 1941 | const handler = (event: globalThis.KeyboardEvent) => { |
| 1948 | 1942 | if (!activeThreadId || event.defaultPrevented) return; |
| 1949 | 1943 | const shortcutContext = { |
apps/web/src/components/Sidebar.tsx+6-124| ... | ... | @@ -27,7 +27,6 @@ import { SortableContext, useSortable, verticalListSortingStrategy } from "@dnd- |
| 27 | 27 | import { restrictToParentElement, restrictToVerticalAxis } from "@dnd-kit/modifiers"; |
| 28 | 28 | import { CSS } from "@dnd-kit/utilities"; |
| 29 | 29 | import { |
| 30 | DEFAULT_RUNTIME_MODE, | |
| 31 | 30 | DEFAULT_MODEL_BY_PROVIDER, |
| 32 | 31 | type DesktopUpdateState, |
| 33 | 32 | ProjectId, |
| ... | ... | @@ -40,14 +39,15 @@ import { useLocation, useNavigate, useParams } from "@tanstack/react-router"; |
| 40 | 39 | import { useAppSettings } from "../appSettings"; |
| 41 | 40 | import { isElectron } from "../env"; |
| 42 | 41 | import { APP_STAGE_LABEL, APP_VERSION } from "../branding"; |
| 43 | import { isMacPlatform, newCommandId, newProjectId, newThreadId } from "../lib/utils"; | |
| 42 | import { isMacPlatform, newCommandId, newProjectId } from "../lib/utils"; | |
| 44 | 43 | import { useStore } from "../store"; |
| 45 | import { isChatNewLocalShortcut, isChatNewShortcut, shortcutLabelForCommand } from "../keybindings"; | |
| 44 | import { shortcutLabelForCommand } from "../keybindings"; | |
| 46 | 45 | import { derivePendingApprovals, derivePendingUserInputs } from "../session-logic"; |
| 47 | 46 | import { gitRemoveWorktreeMutationOptions, gitStatusQueryOptions } from "../lib/gitReactQuery"; |
| 48 | 47 | import { serverConfigQueryOptions } from "../lib/serverReactQuery"; |
| 49 | 48 | import { readNativeApi } from "../nativeApi"; |
| 50 | import { type DraftThreadEnvMode, useComposerDraftStore } from "../composerDraftStore"; | |
| 49 | import { useComposerDraftStore } from "../composerDraftStore"; | |
| 50 | import { useHandleNewThread } from "../hooks/useHandleNewThread"; | |
| 51 | 51 | import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; |
| 52 | 52 | import { toastManager } from "./ui/toast"; |
| 53 | 53 | import { |
| ... | ... | @@ -262,11 +262,8 @@ export default function Sidebar() { |
| 262 | 262 | const getDraftThreadByProjectId = useComposerDraftStore( |
| 263 | 263 | (store) => store.getDraftThreadByProjectId, |
| 264 | 264 | ); |
| 265 | const getDraftThread = useComposerDraftStore((store) => store.getDraftThread); | |
| 266 | 265 | const terminalStateByThreadId = useTerminalStateStore((state) => state.terminalStateByThreadId); |
| 267 | 266 | const clearTerminalState = useTerminalStateStore((state) => state.clearTerminalState); |
| 268 | const setProjectDraftThreadId = useComposerDraftStore((store) => store.setProjectDraftThreadId); | |
| 269 | const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); | |
| 270 | 267 | const clearProjectDraftThreadId = useComposerDraftStore( |
| 271 | 268 | (store) => store.clearProjectDraftThreadId, |
| 272 | 269 | ); |
| ... | ... | @@ -276,6 +273,7 @@ export default function Sidebar() { |
| 276 | 273 | const navigate = useNavigate(); |
| 277 | 274 | const isOnSettings = useLocation({ select: (loc) => loc.pathname === "/settings" }); |
| 278 | 275 | const { settings: appSettings } = useAppSettings(); |
| 276 | const { handleNewThread } = useHandleNewThread(); | |
| 279 | 277 | const routeThreadId = useParams({ |
| 280 | 278 | strict: false, |
| 281 | 279 | select: (params) => (params.threadId ? ThreadId.makeUnsafe(params.threadId) : null), |
| ... | ... | @@ -384,80 +382,6 @@ export default function Sidebar() { |
| 384 | 382 | }); |
| 385 | 383 | }, []); |
| 386 | 384 | |
| 387 | const handleNewThread = useCallback( | |
| 388 | ( | |
| 389 | projectId: ProjectId, | |
| 390 | options?: { | |
| 391 | branch?: string | null; | |
| 392 | worktreePath?: string | null; | |
| 393 | envMode?: DraftThreadEnvMode; | |
| 394 | }, | |
| 395 | ): Promise<void> => { | |
| 396 | const hasBranchOption = options?.branch !== undefined; | |
| 397 | const hasWorktreePathOption = options?.worktreePath !== undefined; | |
| 398 | const hasEnvModeOption = options?.envMode !== undefined; | |
| 399 | const storedDraftThread = getDraftThreadByProjectId(projectId); | |
| 400 | if (storedDraftThread) { | |
| 401 | return (async () => { | |
| 402 | if (hasBranchOption || hasWorktreePathOption || hasEnvModeOption) { | |
| 403 | setDraftThreadContext(storedDraftThread.threadId, { | |
| 404 | ...(hasBranchOption ? { branch: options?.branch ?? null } : {}), | |
| 405 | ...(hasWorktreePathOption ? { worktreePath: options?.worktreePath ?? null } : {}), | |
| 406 | ...(hasEnvModeOption ? { envMode: options?.envMode } : {}), | |
| 407 | }); | |
| 408 | } | |
| 409 | setProjectDraftThreadId(projectId, storedDraftThread.threadId); | |
| 410 | if (routeThreadId === storedDraftThread.threadId) { | |
| 411 | return; | |
| 412 | } | |
| 413 | await navigate({ | |
| 414 | to: "/$threadId", | |
| 415 | params: { threadId: storedDraftThread.threadId }, | |
| 416 | }); | |
| 417 | })(); | |
| 418 | } | |
| 419 | clearProjectDraftThreadId(projectId); | |
| 420 | ||
| 421 | const activeDraftThread = routeThreadId ? getDraftThread(routeThreadId) : null; | |
| 422 | if (activeDraftThread && routeThreadId && activeDraftThread.projectId === projectId) { | |
| 423 | if (hasBranchOption || hasWorktreePathOption || hasEnvModeOption) { | |
| 424 | setDraftThreadContext(routeThreadId, { | |
| 425 | ...(hasBranchOption ? { branch: options?.branch ?? null } : {}), | |
| 426 | ...(hasWorktreePathOption ? { worktreePath: options?.worktreePath ?? null } : {}), | |
| 427 | ...(hasEnvModeOption ? { envMode: options?.envMode } : {}), | |
| 428 | }); | |
| 429 | } | |
| 430 | setProjectDraftThreadId(projectId, routeThreadId); | |
| 431 | return Promise.resolve(); | |
| 432 | } | |
| 433 | const threadId = newThreadId(); | |
| 434 | const createdAt = new Date().toISOString(); | |
| 435 | return (async () => { | |
| 436 | setProjectDraftThreadId(projectId, threadId, { | |
| 437 | createdAt, | |
| 438 | branch: options?.branch ?? null, | |
| 439 | worktreePath: options?.worktreePath ?? null, | |
| 440 | envMode: options?.envMode ?? "local", | |
| 441 | runtimeMode: DEFAULT_RUNTIME_MODE, | |
| 442 | }); | |
| 443 | ||
| 444 | await navigate({ | |
| 445 | to: "/$threadId", | |
| 446 | params: { threadId }, | |
| 447 | }); | |
| 448 | })(); | |
| 449 | }, | |
| 450 | [ | |
| 451 | clearProjectDraftThreadId, | |
| 452 | getDraftThreadByProjectId, | |
| 453 | navigate, | |
| 454 | getDraftThread, | |
| 455 | routeThreadId, | |
| 456 | setDraftThreadContext, | |
| 457 | setProjectDraftThreadId, | |
| 458 | ], | |
| 459 | ); | |
| 460 | ||
| 461 | 385 | const focusMostRecentThreadForProject = useCallback( |
| 462 | 386 | (projectId: ProjectId) => { |
| 463 | 387 | const latestThread = threads |
| ... | ... | @@ -1021,37 +945,6 @@ export default function Sidebar() { |
| 1021 | 945 | ); |
| 1022 | 946 | |
| 1023 | 947 | useEffect(() => { |
| 1024 | const onWindowKeyDown = (event: KeyboardEvent) => { | |
| 1025 | if (event.key === "Escape" && selectedThreadIds.size > 0) { | |
| 1026 | event.preventDefault(); | |
| 1027 | clearSelection(); | |
| 1028 | return; | |
| 1029 | } | |
| 1030 | ||
| 1031 | const activeThread = routeThreadId | |
| 1032 | ? threads.find((thread) => thread.id === routeThreadId) | |
| 1033 | : undefined; | |
| 1034 | const activeDraftThread = routeThreadId ? getDraftThread(routeThreadId) : null; | |
| 1035 | if (isChatNewLocalShortcut(event, keybindings)) { | |
| 1036 | const projectId = | |
| 1037 | activeThread?.projectId ?? activeDraftThread?.projectId ?? projects[0]?.id; | |
| 1038 | if (!projectId) return; | |
| 1039 | event.preventDefault(); | |
| 1040 | void handleNewThread(projectId); | |
| 1041 | return; | |
| 1042 | } | |
| 1043 | ||
| 1044 | if (!isChatNewShortcut(event, keybindings)) return; | |
| 1045 | const projectId = activeThread?.projectId ?? activeDraftThread?.projectId ?? projects[0]?.id; | |
| 1046 | if (!projectId) return; | |
| 1047 | event.preventDefault(); | |
| 1048 | void handleNewThread(projectId, { | |
| 1049 | branch: activeThread?.branch ?? activeDraftThread?.branch ?? null, | |
| 1050 | worktreePath: activeThread?.worktreePath ?? activeDraftThread?.worktreePath ?? null, | |
| 1051 | envMode: activeDraftThread?.envMode ?? (activeThread?.worktreePath ? "worktree" : "local"), | |
| 1052 | }); | |
| 1053 | }; | |
| 1054 | ||
| 1055 | 948 | const onMouseDown = (event: globalThis.MouseEvent) => { |
| 1056 | 949 | if (selectedThreadIds.size === 0) return; |
| 1057 | 950 | const target = event.target instanceof HTMLElement ? event.target : null; |
| ... | ... | @@ -1059,22 +952,11 @@ export default function Sidebar() { |
| 1059 | 952 | clearSelection(); |
| 1060 | 953 | }; |
| 1061 | 954 | |
| 1062 | window.addEventListener("keydown", onWindowKeyDown); | |
| 1063 | 955 | window.addEventListener("mousedown", onMouseDown); |
| 1064 | 956 | return () => { |
| 1065 | window.removeEventListener("keydown", onWindowKeyDown); | |
| 1066 | 957 | window.removeEventListener("mousedown", onMouseDown); |
| 1067 | 958 | }; |
| 1068 | }, [ | |
| 1069 | clearSelection, | |
| 1070 | getDraftThread, | |
| 1071 | handleNewThread, | |
| 1072 | keybindings, | |
| 1073 | projects, | |
| 1074 | routeThreadId, | |
| 1075 | selectedThreadIds.size, | |
| 1076 | threads, | |
| 1077 | ]); | |
| 959 | }, [clearSelection, selectedThreadIds.size]); | |
| 1078 | 960 | |
| 1079 | 961 | useEffect(() => { |
| 1080 | 962 | if (!isElectron) return; |
apps/web/src/hooks/useHandleNewThread.ts created+113| ... | ... | @@ -0,0 +1,113 @@ |
| 1 | import { DEFAULT_RUNTIME_MODE, type ProjectId, ThreadId } from "@t3tools/contracts"; | |
| 2 | import { useNavigate, useParams } from "@tanstack/react-router"; | |
| 3 | import { useCallback } from "react"; | |
| 4 | import { type DraftThreadEnvMode, useComposerDraftStore } from "../composerDraftStore"; | |
| 5 | import { newThreadId } from "../lib/utils"; | |
| 6 | import { useStore } from "../store"; | |
| 7 | ||
| 8 | export function useHandleNewThread() { | |
| 9 | const projects = useStore((store) => store.projects); | |
| 10 | const threads = useStore((store) => store.threads); | |
| 11 | const getDraftThreadByProjectId = useComposerDraftStore( | |
| 12 | (store) => store.getDraftThreadByProjectId, | |
| 13 | ); | |
| 14 | const getDraftThread = useComposerDraftStore((store) => store.getDraftThread); | |
| 15 | const setProjectDraftThreadId = useComposerDraftStore((store) => store.setProjectDraftThreadId); | |
| 16 | const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); | |
| 17 | const clearProjectDraftThreadId = useComposerDraftStore( | |
| 18 | (store) => store.clearProjectDraftThreadId, | |
| 19 | ); | |
| 20 | const navigate = useNavigate(); | |
| 21 | const routeThreadId = useParams({ | |
| 22 | strict: false, | |
| 23 | select: (params) => (params.threadId ? ThreadId.makeUnsafe(params.threadId) : null), | |
| 24 | }); | |
| 25 | ||
| 26 | const activeThread = routeThreadId | |
| 27 | ? threads.find((thread) => thread.id === routeThreadId) | |
| 28 | : undefined; | |
| 29 | const activeDraftThread = routeThreadId ? getDraftThread(routeThreadId) : null; | |
| 30 | ||
| 31 | const handleNewThread = useCallback( | |
| 32 | ( | |
| 33 | projectId: ProjectId, | |
| 34 | options?: { | |
| 35 | branch?: string | null; | |
| 36 | worktreePath?: string | null; | |
| 37 | envMode?: DraftThreadEnvMode; | |
| 38 | }, | |
| 39 | ): Promise<void> => { | |
| 40 | const hasBranchOption = options?.branch !== undefined; | |
| 41 | const hasWorktreePathOption = options?.worktreePath !== undefined; | |
| 42 | const hasEnvModeOption = options?.envMode !== undefined; | |
| 43 | const storedDraftThread = getDraftThreadByProjectId(projectId); | |
| 44 | if (storedDraftThread) { | |
| 45 | return (async () => { | |
| 46 | if (hasBranchOption || hasWorktreePathOption || hasEnvModeOption) { | |
| 47 | setDraftThreadContext(storedDraftThread.threadId, { | |
| 48 | ...(hasBranchOption ? { branch: options?.branch ?? null } : {}), | |
| 49 | ...(hasWorktreePathOption ? { worktreePath: options?.worktreePath ?? null } : {}), | |
| 50 | ...(hasEnvModeOption ? { envMode: options?.envMode } : {}), | |
| 51 | }); | |
| 52 | } | |
| 53 | setProjectDraftThreadId(projectId, storedDraftThread.threadId); | |
| 54 | if (routeThreadId === storedDraftThread.threadId) { | |
| 55 | return; | |
| 56 | } | |
| 57 | await navigate({ | |
| 58 | to: "/$threadId", | |
| 59 | params: { threadId: storedDraftThread.threadId }, | |
| 60 | }); | |
| 61 | })(); | |
| 62 | } | |
| 63 | ||
| 64 | clearProjectDraftThreadId(projectId); | |
| 65 | ||
| 66 | if (activeDraftThread && routeThreadId && activeDraftThread.projectId === projectId) { | |
| 67 | if (hasBranchOption || hasWorktreePathOption || hasEnvModeOption) { | |
| 68 | setDraftThreadContext(routeThreadId, { | |
| 69 | ...(hasBranchOption ? { branch: options?.branch ?? null } : {}), | |
| 70 | ...(hasWorktreePathOption ? { worktreePath: options?.worktreePath ?? null } : {}), | |
| 71 | ...(hasEnvModeOption ? { envMode: options?.envMode } : {}), | |
| 72 | }); | |
| 73 | } | |
| 74 | setProjectDraftThreadId(projectId, routeThreadId); | |
| 75 | return Promise.resolve(); | |
| 76 | } | |
| 77 | ||
| 78 | const threadId = newThreadId(); | |
| 79 | const createdAt = new Date().toISOString(); | |
| 80 | return (async () => { | |
| 81 | setProjectDraftThreadId(projectId, threadId, { | |
| 82 | createdAt, | |
| 83 | branch: options?.branch ?? null, | |
| 84 | worktreePath: options?.worktreePath ?? null, | |
| 85 | envMode: options?.envMode ?? "local", | |
| 86 | runtimeMode: DEFAULT_RUNTIME_MODE, | |
| 87 | }); | |
| 88 | ||
| 89 | await navigate({ | |
| 90 | to: "/$threadId", | |
| 91 | params: { threadId }, | |
| 92 | }); | |
| 93 | })(); | |
| 94 | }, | |
| 95 | [ | |
| 96 | activeDraftThread, | |
| 97 | clearProjectDraftThreadId, | |
| 98 | getDraftThreadByProjectId, | |
| 99 | navigate, | |
| 100 | routeThreadId, | |
| 101 | setDraftThreadContext, | |
| 102 | setProjectDraftThreadId, | |
| 103 | ], | |
| 104 | ); | |
| 105 | ||
| 106 | return { | |
| 107 | activeDraftThread, | |
| 108 | activeThread, | |
| 109 | handleNewThread, | |
| 110 | projects, | |
| 111 | routeThreadId, | |
| 112 | }; | |
| 113 | } |
apps/web/src/lib/terminalFocus.ts created+6| ... | ... | @@ -0,0 +1,6 @@ |
| 1 | export function isTerminalFocused(): boolean { | |
| 2 | const activeElement = document.activeElement; | |
| 3 | if (!(activeElement instanceof HTMLElement)) return false; | |
| 4 | if (activeElement.classList.contains("xterm-helper-textarea")) return true; | |
| 5 | return activeElement.closest(".thread-terminal-drawer .xterm") !== null; | |
| 6 | } |
apps/web/src/routes/_chat.tsx+79| ... | ... | @@ -1,10 +1,88 @@ |
| 1 | import { type ResolvedKeybindingsConfig } from "@t3tools/contracts"; | |
| 2 | import { useQuery } from "@tanstack/react-query"; | |
| 1 | 3 | import { Outlet, createFileRoute, useNavigate } from "@tanstack/react-router"; |
| 2 | 4 | import { useEffect } from "react"; |
| 3 | 5 | |
| 4 | 6 | import { DiffWorkerPoolProvider } from "../components/DiffWorkerPoolProvider"; |
| 5 | 7 | import ThreadSidebar from "../components/Sidebar"; |
| 8 | import { useHandleNewThread } from "../hooks/useHandleNewThread"; | |
| 9 | import { isTerminalFocused } from "../lib/terminalFocus"; | |
| 10 | import { serverConfigQueryOptions } from "../lib/serverReactQuery"; | |
| 11 | import { resolveShortcutCommand } from "../keybindings"; | |
| 12 | import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; | |
| 13 | import { useThreadSelectionStore } from "../threadSelectionStore"; | |
| 6 | 14 | import { Sidebar, SidebarProvider } from "~/components/ui/sidebar"; |
| 7 | 15 | |
| 16 | const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = []; | |
| 17 | ||
| 18 | function ChatRouteGlobalShortcuts() { | |
| 19 | const clearSelection = useThreadSelectionStore((state) => state.clearSelection); | |
| 20 | const selectedThreadIdsSize = useThreadSelectionStore((state) => state.selectedThreadIds.size); | |
| 21 | const { activeDraftThread, activeThread, handleNewThread, projects, routeThreadId } = | |
| 22 | useHandleNewThread(); | |
| 23 | const serverConfigQuery = useQuery(serverConfigQueryOptions()); | |
| 24 | const keybindings = serverConfigQuery.data?.keybindings ?? EMPTY_KEYBINDINGS; | |
| 25 | const terminalOpen = useTerminalStateStore((state) => | |
| 26 | routeThreadId | |
| 27 | ? selectThreadTerminalState(state.terminalStateByThreadId, routeThreadId).terminalOpen | |
| 28 | : false, | |
| 29 | ); | |
| 30 | ||
| 31 | useEffect(() => { | |
| 32 | const onWindowKeyDown = (event: KeyboardEvent) => { | |
| 33 | if (event.defaultPrevented) return; | |
| 34 | ||
| 35 | if (event.key === "Escape" && selectedThreadIdsSize > 0) { | |
| 36 | event.preventDefault(); | |
| 37 | clearSelection(); | |
| 38 | return; | |
| 39 | } | |
| 40 | ||
| 41 | const projectId = activeThread?.projectId ?? activeDraftThread?.projectId ?? projects[0]?.id; | |
| 42 | if (!projectId) return; | |
| 43 | ||
| 44 | const command = resolveShortcutCommand(event, keybindings, { | |
| 45 | context: { | |
| 46 | terminalFocus: isTerminalFocused(), | |
| 47 | terminalOpen, | |
| 48 | }, | |
| 49 | }); | |
| 50 | ||
| 51 | if (command === "chat.newLocal") { | |
| 52 | event.preventDefault(); | |
| 53 | event.stopPropagation(); | |
| 54 | void handleNewThread(projectId); | |
| 55 | return; | |
| 56 | } | |
| 57 | ||
| 58 | if (command !== "chat.new") return; | |
| 59 | event.preventDefault(); | |
| 60 | event.stopPropagation(); | |
| 61 | void handleNewThread(projectId, { | |
| 62 | branch: activeThread?.branch ?? activeDraftThread?.branch ?? null, | |
| 63 | worktreePath: activeThread?.worktreePath ?? activeDraftThread?.worktreePath ?? null, | |
| 64 | envMode: activeDraftThread?.envMode ?? (activeThread?.worktreePath ? "worktree" : "local"), | |
| 65 | }); | |
| 66 | }; | |
| 67 | ||
| 68 | window.addEventListener("keydown", onWindowKeyDown); | |
| 69 | return () => { | |
| 70 | window.removeEventListener("keydown", onWindowKeyDown); | |
| 71 | }; | |
| 72 | }, [ | |
| 73 | activeDraftThread, | |
| 74 | activeThread, | |
| 75 | clearSelection, | |
| 76 | handleNewThread, | |
| 77 | keybindings, | |
| 78 | projects, | |
| 79 | selectedThreadIdsSize, | |
| 80 | terminalOpen, | |
| 81 | ]); | |
| 82 | ||
| 83 | return null; | |
| 84 | } | |
| 85 | ||
| 8 | 86 | function ChatRouteLayout() { |
| 9 | 87 | const navigate = useNavigate(); |
| 10 | 88 | |
| ... | ... | @@ -26,6 +104,7 @@ function ChatRouteLayout() { |
| 26 | 104 | |
| 27 | 105 | return ( |
| 28 | 106 | <SidebarProvider defaultOpen> |
| 107 | <ChatRouteGlobalShortcuts /> | |
| 29 | 108 | <Sidebar |
| 30 | 109 | side="left" |
| 31 | 110 | collapsible="offcanvas" |