Skip to content
Merged
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
16 changes: 14 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@
"@opencode-ai/sdk": "^1.2.1",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/electric-db-collection": "^0.2.33",
"@tanstack/hotkeys": "^0.4.1",
"@tanstack/react-db": "0.1.70",
"@tanstack/react-hotkeys": "^0.4.1",
"@tanstack/react-query": "^5.90.5",
"@tanstack/react-router": "^1.162.8",
"@tanstack/react-start": "^1.162.8",
Expand Down
44 changes: 36 additions & 8 deletions src/components/new-task-button.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { useState, type ComponentProps } from "react";
import { useNavigate } from "@tanstack/react-router";
import { useLiveQuery } from "@tanstack/react-db";
import { useHotkey } from "@tanstack/react-hotkeys";
import { Loader2, Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Kbd } from "@/components/ui/kbd";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { projectsCollection, tasksCollection } from "@/lib/collections";
import { createDesktopRunnerSession } from "@/lib/desktop-runner";
import { hotkeys } from "@/lib/hotkeys";

type ButtonProps = ComponentProps<typeof Button>;

type NewTaskButtonProps = Omit<ButtonProps, "children" | "disabled" | "onClick"> & {
iconOnly?: boolean;
};

export function NewTaskButton({ iconOnly = false, title, ...props }: NewTaskButtonProps) {
export function NewTaskButton({ iconOnly = false, ...props }: NewTaskButtonProps) {
const navigate = useNavigate();
const [creating, setCreating] = useState(false);
const { data: projects } = useLiveQuery((query) =>
Expand All @@ -21,6 +25,8 @@ export function NewTaskButton({ iconOnly = false, title, ...props }: NewTaskButt

const [defaultProject] = projects;

useHotkey(hotkeys.newTask.keys, () => handleNewTask());

function handleNewTask() {
const repoUrl = defaultProject?.repo_url;
if (creating || !defaultProject || !repoUrl) {
Expand Down Expand Up @@ -66,20 +72,42 @@ export function NewTaskButton({ iconOnly = false, title, ...props }: NewTaskButt
});
}

return (
const icon = creating ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : (
<Plus className="w-3.5 h-3.5" />
);

const button = (
<Button
type="button"
title={title ?? "New task"}
disabled={creating || !defaultProject}
onClick={() => handleNewTask()}
{...props}
>
{creating ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : (
<Plus className="w-3.5 h-3.5" />
{icon}
{iconOnly ? null : (
<>
<span>New task</span>
<Kbd keys={hotkeys.newTask.keys} />
</>
)}
{iconOnly ? null : <span>New task</span>}
</Button>
);

if (iconOnly) {
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent>
<span className="flex items-center gap-2">
{hotkeys.newTask.label}
<Kbd keys={hotkeys.newTask.keys} />
</span>
</TooltipContent>
</Tooltip>
);
}

return button;
}
7 changes: 7 additions & 0 deletions src/components/task-page-header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { useHotkey } from "@tanstack/react-hotkeys";
import { ExternalLink } from "lucide-react";
import { OpenEditorDropdown } from "@/components/open-editor-dropdown";
import { Button } from "@/components/ui/button";
import { Kbd } from "@/components/ui/kbd";
import { hotkeys } from "@/lib/hotkeys";
import {
formatChecksProgress,
getChecksStatusClasses,
Expand Down Expand Up @@ -46,6 +49,9 @@ export function TaskPageHeader({
onError,
onCreatePr,
}: TaskPageHeaderProps) {
const canCreatePr = !!branchName && !pullRequest && !sending && !isRunning;
useHotkey(hotkeys.createPr.keys, () => onCreatePr(), { enabled: canCreatePr });

return (
<div className="shrink-0 border-b border-border bg-card px-4 py-3 md:px-6">
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
Expand Down Expand Up @@ -124,6 +130,7 @@ export function TaskPageHeader({
onClick={onCreatePr}
>
Create a PR
<Kbd keys={hotkeys.createPr.keys} />
</Button>
</div>
) : null}
Expand Down
18 changes: 18 additions & 0 deletions src/components/ui/kbd.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { formatKeys } from "@/lib/hotkeys";
import { cn } from "@/lib/utils";

export function Kbd({ keys, className }: { keys: string; className?: string }) {
const parts = formatKeys(keys);
return (
<span className={cn("inline-flex items-center gap-0.5", className)}>
{parts.map((part) => (
<kbd
key={part}
className="inline-flex h-5 min-w-5 items-center justify-center rounded border border-border bg-muted px-1 font-mono text-[10px] font-medium text-muted-foreground"
>
{part}
</kbd>
))}
</span>
);
}
42 changes: 42 additions & 0 deletions src/components/ui/tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { cn } from "@/lib/utils";
import { Tooltip as TooltipPrimitive } from "radix-ui";

function TooltipProvider({
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider delayDuration={400} {...props}>
{children}
</TooltipPrimitive.Provider>
);
}

function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return <TooltipPrimitive.Root {...props} />;
}

function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger {...props} />;
}

function TooltipContent({
className,
sideOffset = 6,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
sideOffset={sideOffset}
className={cn(
"z-50 rounded-[var(--radius-sm)] border border-border bg-popover px-2.5 py-1.5 text-xs font-medium text-popover-foreground shadow-[2px_2px_0_0_var(--color-border)] animate-in fade-in-0 zoom-in-95",
className,
)}
{...props}
/>
</TooltipPrimitive.Portal>
);
}

export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
22 changes: 22 additions & 0 deletions src/lib/hotkeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { detectPlatform } from "@tanstack/hotkeys";

interface HotkeyDef {
keys: string;
label: string;
}

export const hotkeys = {
newTask: { keys: "Mod+N", label: "New task" },
createPr: { keys: "Mod+Shift+P", label: "Create PR" },
} as const satisfies Record<string, HotkeyDef>;

const isMac = detectPlatform() === "mac";

export function formatKeys(keys: string): string[] {
return keys.split("+").map((part) => {
if (part === "Mod") return isMac ? "⌘" : "Ctrl";
if (part === "Shift") return isMac ? "⇧" : "Shift";
if (part === "Alt") return isMac ? "⌥" : "Alt";
return part.toUpperCase();
});
}
6 changes: 5 additions & 1 deletion src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/// <reference types="vite/client" />
import { HeadContent, Outlet, Scripts, createRootRoute } from "@tanstack/react-router";
import { HotkeysProvider } from "@tanstack/react-hotkeys";
import type { ReactNode } from "react";
import { ThemeProvider } from "@/components/theme-provider";
import { TooltipProvider } from "@/components/ui/tooltip";
import { themeInitializationScript } from "@/lib/theme";
import appCss from "@/index.css?url";

Expand Down Expand Up @@ -31,7 +33,9 @@ function RootDocument({ children }: { children: ReactNode }) {
</head>
<body>
<ThemeProvider>
{children}
<HotkeysProvider>
<TooltipProvider>{children}</TooltipProvider>
</HotkeysProvider>
<Scripts />
</ThemeProvider>
</body>
Expand Down