| author | |
| committer | |
| log | 9fe4832a3f87e48152f915cfac9f85ec78e14e24 |
| tree | 5e891ef0204e26eb05886903a8474b6df628e3f1 |
| parent | 376c149eac201fd1492c7b7512a1de96c3b345b7 |
| signature | Signed by PGP key B5690EEEBB952194 |
10 files changed, 118 insertions(+), 217 deletions(-)
apps/web/src/commandPaletteBus.ts created+30| ... | ... | @@ -0,0 +1,30 @@ |
| 1 | // Tiny event bus allowing components to programmatically open the command palette | |
| 2 | // without owning its React state. | |
| 3 | const COMMAND_PALETTE_OPEN_EVENT = "t3code:open-command-palette"; | |
| 4 | ||
| 5 | export interface CommandPaletteOpenDetail { | |
| 6 | readonly open?: "add-project" | "new-thread-in"; | |
| 7 | } | |
| 8 | ||
| 9 | export function openCommandPalette(detail?: CommandPaletteOpenDetail): void { | |
| 10 | window.dispatchEvent( | |
| 11 | new CustomEvent(COMMAND_PALETTE_OPEN_EVENT, detail ? { detail } : undefined), | |
| 12 | ); | |
| 13 | } | |
| 14 | ||
| 15 | export function onOpenCommandPalette( | |
| 16 | listener: (detail: CommandPaletteOpenDetail) => void, | |
| 17 | ): () => void { | |
| 18 | const handler = (event: Event) => { | |
| 19 | listener((event as CustomEvent<CommandPaletteOpenDetail>).detail ?? {}); | |
| 20 | }; | |
| 21 | window.addEventListener(COMMAND_PALETTE_OPEN_EVENT, handler); | |
| 22 | return () => window.removeEventListener(COMMAND_PALETTE_OPEN_EVENT, handler); | |
| 23 | } | |
| 24 | ||
| 25 | /** Read at event time so consumers do not subscribe to transient dialog state. */ | |
| 26 | export function isCommandPaletteOpen(): boolean { | |
| 27 | return ( | |
| 28 | typeof document !== "undefined" && document.querySelector("[data-command-palette]") !== null | |
| 29 | ); | |
| 30 | } |
apps/web/src/commandPaletteContext.tsx deleted-29| ... | ... | @@ -1,29 +0,0 @@ |
| 1 | import { createContext, use, type ReactNode } from "react"; | |
| 2 | ||
| 3 | const OpenAddProjectCommandPaletteContext = createContext<(() => void) | null>(null); | |
| 4 | ||
| 5 | export function OpenAddProjectCommandPaletteProvider(props: { | |
| 6 | readonly children: ReactNode; | |
| 7 | readonly openAddProject: () => void; | |
| 8 | }) { | |
| 9 | return ( | |
| 10 | <OpenAddProjectCommandPaletteContext value={props.openAddProject}> | |
| 11 | {props.children} | |
| 12 | </OpenAddProjectCommandPaletteContext> | |
| 13 | ); | |
| 14 | } | |
| 15 | ||
| 16 | export function useOpenAddProjectCommandPalette(): () => void { | |
| 17 | const openAddProject = use(OpenAddProjectCommandPaletteContext); | |
| 18 | if (!openAddProject) { | |
| 19 | throw new Error("Command palette actions must be used inside CommandPalette"); | |
| 20 | } | |
| 21 | return openAddProject; | |
| 22 | } | |
| 23 | ||
| 24 | /** Read at event time so the chat tree does not subscribe to transient dialog state. */ | |
| 25 | export function isCommandPaletteOpen(): boolean { | |
| 26 | return ( | |
| 27 | typeof document !== "undefined" && document.querySelector("[data-command-palette]") !== null | |
| 28 | ); | |
| 29 | } |
apps/web/src/components/ChatView.tsx+1-1| ... | ... | @@ -112,7 +112,7 @@ import { |
| 112 | 112 | } from "../types"; |
| 113 | 113 | import { useTheme } from "../hooks/useTheme"; |
| 114 | 114 | import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; |
| 115 | import { isCommandPaletteOpen } from "../commandPaletteContext"; | |
| 115 | import { isCommandPaletteOpen } from "../commandPaletteBus"; | |
| 116 | 116 | import { buildTemporaryWorktreeBranchName } from "@t3tools/shared/git"; |
| 117 | 117 | import { useMediaQuery } from "../hooks/useMediaQuery"; |
| 118 | 118 | import { RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY } from "../rightPanelLayout"; |
apps/web/src/components/CommandPalette.tsx+62-15| ... | ... | @@ -43,7 +43,7 @@ import { |
| 43 | 43 | type ReactNode, |
| 44 | 44 | } from "react"; |
| 45 | 45 | import { useAtomValue } from "@effect/atom-react"; |
| 46 | import { OpenAddProjectCommandPaletteProvider } from "../commandPaletteContext"; | |
| 46 | ||
| 47 | 47 | import { isDesktopLocalConnectionTarget } from "../connection/desktopLocal"; |
| 48 | 48 | import { useDesktopLocalBootstraps } from "../connection/useDesktopLocalBootstraps"; |
| 49 | 49 | import { useHandleNewThread } from "../hooks/useHandleNewThread"; |
| ... | ... | @@ -77,6 +77,7 @@ import { |
| 77 | 77 | isUnsupportedWindowsProjectPath, |
| 78 | 78 | resolveProjectPathForDispatch, |
| 79 | 79 | } from "../lib/projectPaths"; |
| 80 | import { onOpenCommandPalette } from "../commandPaletteBus"; | |
| 80 | 81 | import { isTerminalFocused } from "../lib/terminalFocus"; |
| 81 | 82 | import { getLatestThreadForProject } from "../lib/threadSort"; |
| 82 | 83 | import { cn, isMacPlatform, isWindowsPlatform, newProjectId } from "../lib/utils"; |
| ... | ... | @@ -334,7 +335,7 @@ function errorMessage(error: unknown): string { |
| 334 | 335 | } |
| 335 | 336 | |
| 336 | 337 | interface CommandPaletteOpenIntent { |
| 337 | readonly kind: "add-project"; | |
| 338 | readonly kind: "add-project" | "new-thread-in"; | |
| 338 | 339 | } |
| 339 | 340 | |
| 340 | 341 | interface CommandPaletteUiState { |
| ... | ... | @@ -346,6 +347,7 @@ type CommandPaletteUiAction = |
| 346 | 347 | | { readonly _tag: "SetOpen"; readonly open: boolean } |
| 347 | 348 | | { readonly _tag: "Toggle" } |
| 348 | 349 | | { readonly _tag: "OpenAddProject" } |
| 350 | | { readonly _tag: "OpenNewThreadIn" } | |
| 349 | 351 | | { readonly _tag: "ClearOpenIntent" }; |
| 350 | 352 | |
| 351 | 353 | function reduceCommandPaletteUiState( |
| ... | ... | @@ -362,6 +364,8 @@ function reduceCommandPaletteUiState( |
| 362 | 364 | return { open: !state.open, openIntent: null }; |
| 363 | 365 | case "OpenAddProject": |
| 364 | 366 | return { open: true, openIntent: { kind: "add-project" } }; |
| 367 | case "OpenNewThreadIn": | |
| 368 | return { open: true, openIntent: { kind: "new-thread-in" } }; | |
| 365 | 369 | case "ClearOpenIntent": |
| 366 | 370 | return state.openIntent ? { ...state, openIntent: null } : state; |
| 367 | 371 | } |
| ... | ... | @@ -375,6 +379,7 @@ export function CommandPalette({ children }: { children: ReactNode }) { |
| 375 | 379 | const setOpen = useCallback((open: boolean) => dispatch({ _tag: "SetOpen", open }), []); |
| 376 | 380 | const toggleOpen = useCallback(() => dispatch({ _tag: "Toggle" }), []); |
| 377 | 381 | const openAddProject = useCallback(() => dispatch({ _tag: "OpenAddProject" }), []); |
| 382 | const openNewThreadIn = useCallback(() => dispatch({ _tag: "OpenNewThreadIn" }), []); | |
| 378 | 383 | const clearOpenIntent = useCallback(() => dispatch({ _tag: "ClearOpenIntent" }), []); |
| 379 | 384 | const keybindings = useAtomValue(primaryServerKeybindingsAtom); |
| 380 | 385 | const composerHandleRef = useRef<ChatComposerHandle | null>(null); |
| ... | ... | @@ -409,20 +414,32 @@ export function CommandPalette({ children }: { children: ReactNode }) { |
| 409 | 414 | return () => window.removeEventListener("keydown", onKeyDown); |
| 410 | 415 | }, [keybindings, terminalOpen, toggleOpen]); |
| 411 | 416 | |
| 417 | useEffect( | |
| 418 | () => | |
| 419 | onOpenCommandPalette((detail) => { | |
| 420 | if (detail.open === "new-thread-in") { | |
| 421 | openNewThreadIn(); | |
| 422 | } else if (detail.open === "add-project") { | |
| 423 | openAddProject(); | |
| 424 | } else { | |
| 425 | setOpen(true); | |
| 426 | } | |
| 427 | }), | |
| 428 | [openAddProject, openNewThreadIn, setOpen], | |
| 429 | ); | |
| 430 | ||
| 412 | 431 | return ( |
| 413 | <OpenAddProjectCommandPaletteProvider openAddProject={openAddProject}> | |
| 414 | <ComposerHandleContext value={composerHandleRef}> | |
| 415 | <CommandDialog open={state.open} onOpenChange={setOpen}> | |
| 416 | {children} | |
| 417 | <CommandPaletteDialog | |
| 418 | open={state.open} | |
| 419 | openIntent={state.openIntent} | |
| 420 | setOpen={setOpen} | |
| 421 | clearOpenIntent={clearOpenIntent} | |
| 422 | /> | |
| 423 | </CommandDialog> | |
| 424 | </ComposerHandleContext> | |
| 425 | </OpenAddProjectCommandPaletteProvider> | |
| 432 | <ComposerHandleContext value={composerHandleRef}> | |
| 433 | <CommandDialog open={state.open} onOpenChange={setOpen}> | |
| 434 | {children} | |
| 435 | <CommandPaletteDialog | |
| 436 | open={state.open} | |
| 437 | openIntent={state.openIntent} | |
| 438 | setOpen={setOpen} | |
| 439 | clearOpenIntent={clearOpenIntent} | |
| 440 | /> | |
| 441 | </CommandDialog> | |
| 442 | </ComposerHandleContext> | |
| 426 | 443 | ); |
| 427 | 444 | } |
| 428 | 445 | |
| ... | ... | @@ -960,6 +977,36 @@ function OpenCommandPaletteDialog(props: { |
| 960 | 977 | openAddProjectFlow(); |
| 961 | 978 | }, [clearOpenIntent, openAddProjectFlow, openIntent]); |
| 962 | 979 | |
| 980 | useLayoutEffect(() => { | |
| 981 | if (openIntent?.kind !== "new-thread-in" || projectThreadItems.length === 0) { | |
| 982 | return; | |
| 983 | } | |
| 984 | clearOpenIntent(); | |
| 985 | setAddProjectCloneFlow(null); | |
| 986 | setViewStack([]); | |
| 987 | setQuery(""); | |
| 988 | const currentPrefix = | |
| 989 | currentProjectEnvironmentId && currentProjectId | |
| 990 | ? `new-thread-in:${currentProjectEnvironmentId}:${currentProjectId}` | |
| 991 | : null; | |
| 992 | const prioritized = currentPrefix | |
| 993 | ? [ | |
| 994 | ...projectThreadItems.filter((item) => item.value === currentPrefix), | |
| 995 | ...projectThreadItems.filter((item) => item.value !== currentPrefix), | |
| 996 | ] | |
| 997 | : projectThreadItems; | |
| 998 | pushPaletteView({ | |
| 999 | addonIcon: <SquarePenIcon className={ADDON_ICON_CLASS} />, | |
| 1000 | groups: [{ value: "projects", label: "Projects", items: prioritized }], | |
| 1001 | }); | |
| 1002 | }, [ | |
| 1003 | clearOpenIntent, | |
| 1004 | currentProjectEnvironmentId, | |
| 1005 | currentProjectId, | |
| 1006 | openIntent, | |
| 1007 | projectThreadItems, | |
| 1008 | ]); | |
| 1009 | ||
| 963 | 1010 | const actionItems: Array<CommandPaletteActionItem | CommandPaletteSubmenuItem> = []; |
| 964 | 1011 | |
| 965 | 1012 | if (projects.length > 0) { |
apps/web/src/components/Sidebar.tsx+5-2| ... | ... | @@ -177,7 +177,7 @@ import { |
| 177 | 177 | useSidebar, |
| 178 | 178 | } from "./ui/sidebar"; |
| 179 | 179 | import { useThreadSelectionStore } from "../threadSelectionStore"; |
| 180 | import { useOpenAddProjectCommandPalette } from "../commandPaletteContext"; | |
| 180 | import { openCommandPalette } from "../commandPaletteBus"; | |
| 181 | 181 | import { |
| 182 | 182 | archiveSelectedThreadEntries, |
| 183 | 183 | buildMultiSelectThreadContextMenuItems, |
| ... | ... | @@ -3086,7 +3086,10 @@ export default function Sidebar() { |
| 3086 | 3086 | : false, |
| 3087 | 3087 | ); |
| 3088 | 3088 | const keybindings = useAtomValue(primaryServerKeybindingsAtom); |
| 3089 | const openAddProjectCommandPalette = useOpenAddProjectCommandPalette(); | |
| 3089 | const openAddProjectCommandPalette = useCallback( | |
| 3090 | () => openCommandPalette({ open: "add-project" }), | |
| 3091 | [], | |
| 3092 | ); | |
| 3090 | 3093 | const [expandedThreadListsByProject, setExpandedThreadListsByProject] = useState< |
| 3091 | 3094 | ReadonlySet<string> |
| 3092 | 3095 | >(() => new Set()); |
apps/web/src/components/SidebarV2.tsx+10-146| ... | ... | @@ -57,14 +57,8 @@ import { useUiStateStore } from "../uiStateStore"; |
| 57 | 57 | import { useThreadSelectionStore } from "../threadSelectionStore"; |
| 58 | 58 | import { useThreadActions } from "../hooks/useThreadActions"; |
| 59 | 59 | import { useHandleNewThread } from "../hooks/useHandleNewThread"; |
| 60 | import { useOpenAddProjectCommandPalette } from "../commandPaletteContext"; | |
| 61 | import { onOpenNewThreadPicker } from "../newThreadPickerBus"; | |
| 62 | import { Dialog, DialogHeader, DialogPopup, DialogTitle } from "./ui/dialog"; | |
| 63 | import { | |
| 64 | resolveThreadActionProjectRef, | |
| 65 | startNewThreadFromContext, | |
| 66 | startNewThreadInProjectFromContext, | |
| 67 | } from "../lib/chatThreadActions"; | |
| 60 | import { openCommandPalette } from "../commandPaletteBus"; | |
| 61 | import { startNewThreadFromContext } from "../lib/chatThreadActions"; | |
| 68 | 62 | import { useClientSettings } from "../hooks/useSettings"; |
| 69 | 63 | import { useEnvironments, usePrimaryEnvironmentId } from "../state/environments"; |
| 70 | 64 | import { useProjects, useThreadShells } from "../state/entities"; |
| ... | ... | @@ -630,7 +624,10 @@ export default function SidebarV2() { |
| 630 | 624 | reportFailure: false, |
| 631 | 625 | }); |
| 632 | 626 | const newThreadContext = useHandleNewThread(); |
| 633 | const openAddProjectCommandPalette = useOpenAddProjectCommandPalette(); | |
| 627 | const openAddProjectCommandPalette = useCallback( | |
| 628 | () => openCommandPalette({ open: "add-project" }), | |
| 629 | [], | |
| 630 | ); | |
| 634 | 631 | const { environments } = useEnvironments(); |
| 635 | 632 | const primaryEnvironmentId = usePrimaryEnvironmentId(); |
| 636 | 633 | const clearSelection = useThreadSelectionStore((s) => s.clearSelection); |
| ... | ... | @@ -1301,12 +1298,8 @@ export default function SidebarV2() { |
| 1301 | 1298 | |
| 1302 | 1299 | // New thread defaults to the project you're in (active thread's project, |
| 1303 | 1300 | // falling back to the top project) — same resolution the command palette |
| 1304 | // uses. The chevron menu is the explicit project picker the flat list no | |
| 1305 | // longer gets from per-project headers. | |
| 1306 | const [newThreadPickerOpen, setNewThreadPickerOpen] = useState(false); | |
| 1307 | // chat.new (mod+shift+o / mod+n) is handled by the _chat route layout; in | |
| 1308 | // v2 with multiple projects it opens this picker via the event bus. | |
| 1309 | useEffect(() => onOpenNewThreadPicker(() => setNewThreadPickerOpen(true)), []); | |
| 1301 | // uses. The command palette already offers a "New thread in..." submenu | |
| 1302 | // for multi-project setups. | |
| 1310 | 1303 | const handleNewThreadClick = useCallback(() => { |
| 1311 | 1304 | // One project: nothing to pick, create immediately. |
| 1312 | 1305 | if (projects.length <= 1) { |
| ... | ... | @@ -1319,57 +1312,9 @@ export default function SidebarV2() { |
| 1319 | 1312 | }); |
| 1320 | 1313 | return; |
| 1321 | 1314 | } |
| 1322 | setNewThreadPickerOpen(true); | |
| 1315 | if (isMobile) setOpenMobile(false); | |
| 1316 | openCommandPalette({ open: "new-thread-in" }); | |
| 1323 | 1317 | }, [isMobile, newThreadContext, projects.length, setOpenMobile]); |
| 1324 | const createThreadInProject = useCallback( | |
| 1325 | (environmentId: (typeof projects)[number]["environmentId"], projectId: string) => { | |
| 1326 | setNewThreadPickerOpen(false); | |
| 1327 | if (isMobile) setOpenMobile(false); | |
| 1328 | const project = projects.find( | |
| 1329 | (candidate) => candidate.environmentId === environmentId && candidate.id === projectId, | |
| 1330 | ); | |
| 1331 | if (!project) return; | |
| 1332 | void startNewThreadInProjectFromContext( | |
| 1333 | { | |
| 1334 | activeDraftThread: newThreadContext.activeDraftThread, | |
| 1335 | activeThread: newThreadContext.activeThread ?? undefined, | |
| 1336 | defaultProjectRef: newThreadContext.defaultProjectRef, | |
| 1337 | handleNewThread: newThreadContext.handleNewThread, | |
| 1338 | }, | |
| 1339 | scopeProjectRef(project.environmentId, project.id), | |
| 1340 | ); | |
| 1341 | }, | |
| 1342 | [isMobile, newThreadContext, projects, setOpenMobile], | |
| 1343 | ); | |
| 1344 | const contextualProjectRef = resolveThreadActionProjectRef({ | |
| 1345 | activeDraftThread: newThreadContext.activeDraftThread, | |
| 1346 | activeThread: newThreadContext.activeThread ?? undefined, | |
| 1347 | defaultProjectRef: newThreadContext.defaultProjectRef, | |
| 1348 | handleNewThread: newThreadContext.handleNewThread, | |
| 1349 | }); | |
| 1350 | const newThreadTargetRef = scopedProject | |
| 1351 | ? scopeProjectRef(scopedProject.environmentId, scopedProject.id) | |
| 1352 | : contextualProjectRef; | |
| 1353 | const newThreadTargetProject = newThreadTargetRef | |
| 1354 | ? (projects.find( | |
| 1355 | (project) => | |
| 1356 | project.environmentId === newThreadTargetRef.environmentId && | |
| 1357 | project.id === newThreadTargetRef.projectId, | |
| 1358 | ) ?? null) | |
| 1359 | : null; | |
| 1360 | // Picker order: the contextual default first (preselected), everything else | |
| 1361 | // after — the common case is Enter/click on the top row. | |
| 1362 | const newThreadPickerProjects = useMemo(() => { | |
| 1363 | if (!newThreadTargetProject) return projects; | |
| 1364 | return [ | |
| 1365 | newThreadTargetProject, | |
| 1366 | ...projects.filter( | |
| 1367 | (project) => | |
| 1368 | project.environmentId !== newThreadTargetProject.environmentId || | |
| 1369 | project.id !== newThreadTargetProject.id, | |
| 1370 | ), | |
| 1371 | ]; | |
| 1372 | }, [newThreadTargetProject, projects]); | |
| 1373 | 1318 | |
| 1374 | 1319 | const commandPaletteShortcutLabel = shortcutLabelForCommand(keybindings, "commandPalette.toggle"); |
| 1375 | 1320 | // Same resolution as v1: prefer the local-thread binding, fall back to |
| ... | ... | @@ -1639,87 +1584,6 @@ export default function SidebarV2() { |
| 1639 | 1584 | </SidebarContent> |
| 1640 | 1585 | <SidebarSeparator /> |
| 1641 | 1586 | <SidebarChromeFooter /> |
| 1642 | <Dialog open={newThreadPickerOpen} onOpenChange={setNewThreadPickerOpen}> | |
| 1643 | <DialogPopup className="max-w-sm p-0"> | |
| 1644 | <DialogHeader className="px-4 pb-2 pt-4"> | |
| 1645 | <DialogTitle className="text-sm">New thread in…</DialogTitle> | |
| 1646 | </DialogHeader> | |
| 1647 | <div | |
| 1648 | className="flex flex-col gap-0.5 px-2 pb-3" | |
| 1649 | role="listbox" | |
| 1650 | aria-label="Choose a project for the new thread" | |
| 1651 | onKeyDown={(event) => { | |
| 1652 | if ( | |
| 1653 | event.key !== "ArrowDown" && | |
| 1654 | event.key !== "ArrowUp" && | |
| 1655 | event.key !== "Home" && | |
| 1656 | event.key !== "End" | |
| 1657 | ) { | |
| 1658 | return; | |
| 1659 | } | |
| 1660 | const container = event.currentTarget; | |
| 1661 | const options = [...container.querySelectorAll<HTMLButtonElement>("button")]; | |
| 1662 | if (options.length === 0) return; | |
| 1663 | const currentIndex = options.findIndex((option) => option === document.activeElement); | |
| 1664 | const nextIndex = | |
| 1665 | event.key === "Home" | |
| 1666 | ? 0 | |
| 1667 | : event.key === "End" | |
| 1668 | ? options.length - 1 | |
| 1669 | : event.key === "ArrowDown" | |
| 1670 | ? (currentIndex + 1) % options.length | |
| 1671 | : (currentIndex - 1 + options.length) % options.length; | |
| 1672 | event.preventDefault(); | |
| 1673 | options[nextIndex]?.focus(); | |
| 1674 | }} | |
| 1675 | > | |
| 1676 | {newThreadPickerProjects.map((project, index) => { | |
| 1677 | const isDefault = index === 0 && newThreadTargetProject !== null; | |
| 1678 | return ( | |
| 1679 | <button | |
| 1680 | key={`${project.environmentId}:${project.id}`} | |
| 1681 | type="button" | |
| 1682 | autoFocus={isDefault} | |
| 1683 | onClick={() => createThreadInProject(project.environmentId, project.id)} | |
| 1684 | className={cn( | |
| 1685 | "flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-left transition-colors", | |
| 1686 | "hover:bg-accent focus:bg-accent focus:outline-none", | |
| 1687 | isDefault && "bg-accent/50", | |
| 1688 | )} | |
| 1689 | > | |
| 1690 | <ProjectFavicon | |
| 1691 | environmentId={project.environmentId} | |
| 1692 | cwd={project.workspaceRoot} | |
| 1693 | className="size-4" | |
| 1694 | /> | |
| 1695 | <span className="min-w-0 flex-1"> | |
| 1696 | <span className="block truncate text-[13px] font-medium text-foreground"> | |
| 1697 | {project.title} | |
| 1698 | </span> | |
| 1699 | <span className="block truncate font-mono text-[10px] text-muted-foreground/60"> | |
| 1700 | {project.workspaceRoot} | |
| 1701 | </span> | |
| 1702 | </span> | |
| 1703 | {isDefault ? ( | |
| 1704 | <Kbd className="h-4 shrink-0 rounded-sm px-1.5 text-[10px]">↵</Kbd> | |
| 1705 | ) : null} | |
| 1706 | </button> | |
| 1707 | ); | |
| 1708 | })} | |
| 1709 | <button | |
| 1710 | type="button" | |
| 1711 | onClick={() => { | |
| 1712 | setNewThreadPickerOpen(false); | |
| 1713 | openAddProjectCommandPalette(); | |
| 1714 | }} | |
| 1715 | className="mt-1 flex items-center gap-2.5 rounded-lg border border-dashed border-border px-2.5 py-2 text-left text-[13px] text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:bg-accent focus:text-foreground focus:outline-none" | |
| 1716 | > | |
| 1717 | <PlusIcon className="size-4" /> | |
| 1718 | Add project | |
| 1719 | </button> | |
| 1720 | </div> | |
| 1721 | </DialogPopup> | |
| 1722 | </Dialog> | |
| 1723 | 1587 | </> |
| 1724 | 1588 | ); |
| 1725 | 1589 | } |
apps/web/src/components/chat/DraftHeroHeadline.tsx+3-3| ... | ... | @@ -1,9 +1,9 @@ |
| 1 | 1 | import type { ScopedProjectRef } from "@t3tools/contracts"; |
| 2 | 2 | import { scopedProjectKey, scopeProjectRef } from "@t3tools/client-runtime/environment"; |
| 3 | 3 | import { FolderPlusIcon } from "lucide-react"; |
| 4 | import { useMemo } from "react"; | |
| 4 | import { useCallback, useMemo } from "react"; | |
| 5 | 5 | |
| 6 | import { useOpenAddProjectCommandPalette } from "~/commandPaletteContext"; | |
| 6 | import { openCommandPalette } from "~/commandPaletteBus"; | |
| 7 | 7 | import { useNewThreadHandler } from "~/hooks/useHandleNewThread"; |
| 8 | 8 | import { useProjects, useThreadShells } from "~/state/entities"; |
| 9 | 9 | import { sortScopedProjectsForSidebar } from "../Sidebar.logic"; |
| ... | ... | @@ -29,7 +29,7 @@ export function DraftHeroHeadline({ |
| 29 | 29 | const projects = useProjects(); |
| 30 | 30 | const threads = useThreadShells(); |
| 31 | 31 | const handleNewThread = useNewThreadHandler(); |
| 32 | const openAddProject = useOpenAddProjectCommandPalette(); | |
| 32 | const openAddProject = useCallback(() => openCommandPalette({ open: "add-project" }), []); | |
| 33 | 33 | |
| 34 | 34 | const orderedProjects = useMemo( |
| 35 | 35 | () => sortScopedProjectsForSidebar(projects, threads, "updated_at"), |
apps/web/src/newThreadPickerBus.ts deleted-14| ... | ... | @@ -1,14 +0,0 @@ |
| 1 | // Tiny event bus connecting the global chat.new shortcut (handled in the | |
| 2 | // _chat route layout) to SidebarV2's project picker dialog. The route layer | |
| 3 | // can't render the picker itself — it lives with the sidebar — and the | |
| 4 | // sidebar can't own the window keydown handler without racing the layout's. | |
| 5 | const NEW_THREAD_PICKER_EVENT = "t3code:open-new-thread-picker"; | |
| 6 | ||
| 7 | export function openNewThreadPicker(): void { | |
| 8 | window.dispatchEvent(new CustomEvent(NEW_THREAD_PICKER_EVENT)); | |
| 9 | } | |
| 10 | ||
| 11 | export function onOpenNewThreadPicker(listener: () => void): () => void { | |
| 12 | window.addEventListener(NEW_THREAD_PICKER_EVENT, listener); | |
| 13 | return () => window.removeEventListener(NEW_THREAD_PICKER_EVENT, listener); | |
| 14 | } |
apps/web/src/routes/_chat.index.tsx+3-3| ... | ... | @@ -1,9 +1,9 @@ |
| 1 | 1 | import { scopeProjectRef } from "@t3tools/client-runtime/environment"; |
| 2 | 2 | import { createFileRoute, Link } from "@tanstack/react-router"; |
| 3 | 3 | import { LinkIcon, PlusIcon, RotateCcwIcon } from "lucide-react"; |
| 4 | import { useEffect, useMemo, useRef, useState } from "react"; | |
| 4 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | |
| 5 | 5 | |
| 6 | import { useOpenAddProjectCommandPalette } from "../commandPaletteContext"; | |
| 6 | import { openCommandPalette } from "../commandPaletteBus"; | |
| 7 | 7 | import { sortScopedProjectsForSidebar } from "../components/Sidebar.logic"; |
| 8 | 8 | import { Button } from "../components/ui/button"; |
| 9 | 9 | import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "../components/ui/empty"; |
| ... | ... | @@ -105,7 +105,7 @@ function DraftStartError({ onRetry }: { readonly onRetry: () => void }) { |
| 105 | 105 | } |
| 106 | 106 | |
| 107 | 107 | function NoProjectsHero() { |
| 108 | const openAddProject = useOpenAddProjectCommandPalette(); | |
| 108 | const openAddProject = useCallback(() => openCommandPalette({ open: "add-project" }), []); | |
| 109 | 109 | |
| 110 | 110 | return ( |
| 111 | 111 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground"> |
apps/web/src/routes/_chat.tsx+4-4| ... | ... | @@ -2,9 +2,9 @@ import { Outlet, createFileRoute, redirect } from "@tanstack/react-router"; |
| 2 | 2 | import { useAtomValue } from "@effect/atom-react"; |
| 3 | 3 | import { useEffect } from "react"; |
| 4 | 4 | |
| 5 | import { isCommandPaletteOpen } from "../commandPaletteContext"; | |
| 5 | import { isCommandPaletteOpen } from "../commandPaletteBus"; | |
| 6 | 6 | import { useClientSettings } from "../hooks/useSettings"; |
| 7 | import { openNewThreadPicker } from "../newThreadPickerBus"; | |
| 7 | import { openCommandPalette } from "../commandPaletteBus"; | |
| 8 | 8 | import { useProjects } from "../state/entities"; |
| 9 | 9 | import { dispatchPreviewAction } from "../components/preview/previewActionBus"; |
| 10 | 10 | import { useHandleNewThread } from "../hooks/useHandleNewThread"; |
| ... | ... | @@ -80,11 +80,11 @@ function ChatRouteGlobalShortcuts() { |
| 80 | 80 | if (command === "chat.new") { |
| 81 | 81 | event.preventDefault(); |
| 82 | 82 | event.stopPropagation(); |
| 83 | // Sidebar v2 routes creation through its project picker whenever | |
| 83 | // Sidebar v2 routes creation through the command palette whenever | |
| 84 | 84 | // there is a real choice to make; v1 (and single-project setups) |
| 85 | 85 | // keep the immediate contextual create. |
| 86 | 86 | if (sidebarV2Enabled && projectCount > 1) { |
| 87 | openNewThreadPicker(); | |
| 87 | openCommandPalette({ open: "new-thread-in" }); | |
| 88 | 88 | return; |
| 89 | 89 | } |
| 90 | 90 | void startNewThreadFromContext({ |