Skip to content
16 changes: 6 additions & 10 deletions apps/code/src/renderer/components/GlobalEventHandlers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,13 @@ export function GlobalEventHandlers({

const handleSwitchTask = useCallback(
(index: number) => {
if (index === 0) {
navigateToTaskInput();
} else {
const taskData = visualTaskOrder[index - 1];
const task = taskData ? taskById.get(taskData.id) : undefined;
if (task) {
navigateToTask(task);
}
const taskData = visualTaskOrder[index - 1];
const task = taskData ? taskById.get(taskData.id) : undefined;
if (task) {
navigateToTask(task);
}
},
[visualTaskOrder, taskById, navigateToTask, navigateToTaskInput],
[visualTaskOrder, taskById, navigateToTask],
);

const handlePrevTask = useCallback(() => {
Expand Down Expand Up @@ -195,7 +191,7 @@ export function GlobalEventHandlers({
[handleToggleFocus],
);

// Task switching with mod+0-9
// Task switching with mod+1-9
useHotkeys(
SHORTCUTS.SWITCH_TASK,
(event, handler) => {
Expand Down
1 change: 1 addition & 0 deletions apps/code/src/renderer/components/ui/combobox/Combobox.css
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@
box-sizing: border-box;
display: flex;
align-items: center;
gap: 6px;
width: 100%;
height: var(--combobox-item-height);
padding-left: var(--combobox-content-padding);
Expand Down
6 changes: 3 additions & 3 deletions apps/code/src/renderer/constants/keyboard-shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const SHORTCUTS = {
NEXT_TASK: "mod+shift+],ctrl+tab",
CLOSE_TAB: "mod+w",
SWITCH_TAB: "ctrl+1,ctrl+2,ctrl+3,ctrl+4,ctrl+5,ctrl+6,ctrl+7,ctrl+8,ctrl+9",
SWITCH_TASK: "mod+0,mod+1,mod+2,mod+3,mod+4,mod+5,mod+6,mod+7,mod+8,mod+9",
SWITCH_TASK: "mod+1,mod+2,mod+3,mod+4,mod+5,mod+6,mod+7,mod+8,mod+9",
OPEN_IN_EDITOR: "mod+o",
COPY_PATH: "mod+shift+c",
TOGGLE_FOCUS: "mod+r",
Expand Down Expand Up @@ -68,8 +68,8 @@ export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [
},
{
id: "switch-task",
keys: "mod+0-9",
description: "Switch to task 1-9 (0 = home)",
keys: "mod+1-9",
description: "Switch to task 1-9",
category: "navigation",
},
{
Expand Down
6 changes: 6 additions & 0 deletions apps/code/src/renderer/features/auth/hooks/authQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,15 @@ export function useAuthState() {
return useQuery({
...getAuthStateQueryOptions(),
placeholderData: ANONYMOUS_AUTH_STATE,
refetchOnMount: true,
});
}

export function useAuthStateFetched(): boolean {
const { isFetched } = useAuthState();
return isFetched;
}

export function useAuthStateValue<T>(selector: (state: AuthState) => T): T {
const { data } = useAuthState();
return selector(data ?? ANONYMOUS_AUTH_STATE);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { useDraftStore } from "@features/message-editor/stores/draftStore";
import { TaskInput } from "@features/task-detail/components/TaskInput";
import { ArrowsOut, Plus, X } from "@phosphor-icons/react";
import type { WorkspaceMode } from "@main/services/workspace/schemas";
import {
ArrowsOut,
Cloud,
Desktop,
GitFork,
Plus,
X,
} from "@phosphor-icons/react";
import { Flex, Text } from "@radix-ui/themes";
import type { Task } from "@shared/types";
import { useNavigationStore } from "@stores/navigationStore";
Expand All @@ -19,6 +27,27 @@ interface CommandCenterPanelProps {
isActiveSession: boolean;
}

const environmentConfig: Record<
WorkspaceMode,
{ label: string; icon: typeof Desktop }
> = {
local: { label: "Local", icon: Desktop },
worktree: { label: "Worktree", icon: GitFork },
cloud: { label: "Cloud", icon: Cloud },
};

function EnvironmentBadge({ mode }: { mode: WorkspaceMode | null }) {
if (!mode) return null;
const config = environmentConfig[mode];
const Icon = config.icon;
return (
<span className="inline-flex items-center gap-0.5 rounded bg-gray-3 px-1 py-0.5 text-[9px] text-gray-10">
<Icon size={10} />
{config.label}
</span>
);
}

function EmptyCell({ cellIndex }: { cellIndex: number }) {
const [selectorOpen, setSelectorOpen] = useState(false);
const isCreating = useCommandCenterStore((s) =>
Expand Down Expand Up @@ -148,6 +177,7 @@ function PopulatedCell({
</Text>
<Flex align="center" gap="1" className="shrink-0">
<StatusBadge status={cell.status} />
<EnvironmentBadge mode={cell.workspaceMode} />
{cell.repoName && (
<span className="rounded bg-gray-3 px-1 py-0.5 text-[9px] text-gray-10">
{cell.repoName}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useSessions } from "@features/sessions/hooks/useSession";
import type { AgentSession } from "@features/sessions/stores/sessionStore";
import { useTasks } from "@features/tasks/hooks/useTasks";
import { useWorkspaces } from "@features/workspace/hooks/useWorkspace";
import type { WorkspaceMode } from "@main/services/workspace/schemas";
import type { Task } from "@shared/types";
import { getTaskRepository, parseRepository } from "@utils/repository";
import { useMemo } from "react";
Expand All @@ -15,6 +17,7 @@ export interface CommandCenterCellData {
session: AgentSession | undefined;
status: CellStatus;
repoName: string | null;
workspaceMode: WorkspaceMode | null;
}

export interface StatusSummary {
Expand Down Expand Up @@ -56,6 +59,7 @@ export function useCommandCenterData(): {
const storeCells = useCommandCenterStore((s) => s.cells);
const { data: tasks = [] } = useTasks();
const sessions = useSessions();
const { data: workspaces } = useWorkspaces();

const taskMap = useMemo(() => {
const map = new Map<string, Task>();
Expand All @@ -81,10 +85,20 @@ export function useCommandCenterData(): {
const session = taskId ? sessionByTaskId.get(taskId) : undefined;
const status = taskId ? deriveStatus(session) : "idle";
const repoName = task ? getRepoName(task) : null;

return { cellIndex, taskId, task, session, status, repoName };
const workspaceMode =
(taskId ? workspaces?.[taskId]?.mode : null) ?? null;

return {
cellIndex,
taskId,
task,
session,
status,
repoName,
workspaceMode,
};
});
}, [storeCells, taskMap, sessionByTaskId]);
}, [storeCells, taskMap, sessionByTaskId, workspaces]);

const summary = useMemo(() => {
const populated = cells.filter((c) => c.taskId && c.task);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient"
import { useSelectProjectMutation } from "@features/auth/hooks/authMutations";
import {
authKeys,
useAuthStateFetched,
useAuthStateValue,
useCurrentUser,
} from "@features/auth/hooks/authQueries";
Expand Down Expand Up @@ -34,6 +35,7 @@ interface ProjectSelectStepProps {
}

export function ProjectSelectStep({ onNext, onBack }: ProjectSelectStepProps) {
const authFetched = useAuthStateFetched();
const isAuthenticated =
useAuthStateValue((state) => state.status) === "authenticated";
const selectProjectMutation = useSelectProjectMutation();
Expand Down Expand Up @@ -134,7 +136,7 @@ export function ProjectSelectStep({ onNext, onBack }: ProjectSelectStepProps) {
</Text>
</Flex>
</motion.div>
) : (
) : authFetched ? (
<motion.div
key="oauth"
initial={{ opacity: 1 }}
Expand All @@ -147,7 +149,7 @@ export function ProjectSelectStep({ onNext, onBack }: ProjectSelectStepProps) {
subtitle="Connect your account to get started."
/>
</motion.div>
)}
) : null}
</AnimatePresence>
</Flex>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ export function useProjects() {
);
const currentProjectId = useAuthStateValue((state) => state.projectId);
const client = useOptionalAuthenticatedClient();
const { data: currentUser, isLoading, error } = useCurrentUser({ client });
const {
data: currentUser,
isLoading: isQueryLoading,
error,
} = useCurrentUser({ client });
const isInitialLoading = isQueryLoading && !currentUser;

const projects = useMemo(() => {
if (!currentUser?.organization) return [];
Expand Down Expand Up @@ -119,7 +124,7 @@ export function useProjects() {
currentProject,
currentProjectId,
currentUser: currentUser ?? null,
isLoading,
isLoading: isInitialLoading,
error,
};
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient";
import { useLogoutMutation } from "@features/auth/hooks/authMutations";
import {
useAuthStateValue,
useCurrentUser,
} from "@features/auth/hooks/authQueries";
import { useAuthStore } from "@features/auth/stores/authStore";
import {
type SettingsCategory,
useSettingsDialogStore,
Expand Down Expand Up @@ -129,6 +129,7 @@ export function SettingsDialog() {
const { data: user } = useCurrentUser({ client });
const { seat, planLabel } = useSeat();
const billingEnabled = useFeatureFlag("posthog-code-billing");
const logoutMutation = useLogoutMutation();

const sidebarItems = useMemo(
() =>
Expand Down Expand Up @@ -231,8 +232,12 @@ export function SettingsDialog() {
{isAuthenticated && (
<button
type="button"
className="flex cursor-pointer items-center gap-2 border-0 border-gray-5 border-t bg-transparent px-3 py-2.5 text-left font-mono text-[12px] text-gray-9 transition-colors hover:bg-gray-3 hover:text-gray-11"
onClick={() => useAuthStore.getState().logout()}
disabled={logoutMutation.isPending}
className="flex cursor-pointer items-center gap-2 border-0 border-gray-5 border-t bg-transparent px-3 py-2.5 text-left font-mono text-[12px] text-gray-9 transition-colors hover:bg-gray-3 hover:text-gray-11 disabled:pointer-events-none disabled:opacity-50"
onClick={() => {
close();
logoutMutation.mutate();
}}
>
<SignOut size={14} />
<span>Sign out</span>
Expand Down
48 changes: 38 additions & 10 deletions apps/code/src/renderer/features/sidebar/components/TaskListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ import {
MenuLabel,
} from "@posthog/quill";
import { Flex, Text } from "@radix-ui/themes";
import builderHog from "@renderer/assets/images/hedgehogs/builder-hog-03.png";
import { useWorkspace } from "@renderer/features/workspace/hooks/useWorkspace";
import { normalizeRepoKey } from "@shared/utils/repo";
import { useNavigationStore } from "@stores/navigationStore";
import { motion } from "framer-motion";
import { useCallback, useEffect } from "react";
import type { TaskData, TaskGroup } from "../hooks/useSidebarData";
import { useSidebarStore } from "../stores/sidebarStore";
Expand Down Expand Up @@ -145,7 +147,7 @@ function TaskFilterMenu() {
align="start"
side="bottom"
sideOffset={6}
className="min-w-xs"
className="min-w-fit"
>
<MenuLabel>Organize</MenuLabel>
<DropdownMenuRadioGroup
Expand Down Expand Up @@ -223,6 +225,9 @@ export function TaskListView({
const navigateToTaskInput = useNavigationStore(
(state) => state.navigateToTaskInput,
);
const isOnTaskInput = useNavigationStore(
(state) => state.view.type === "task-input",
);

// biome-ignore lint/correctness/useExhaustiveDependencies: reset pagination when filters change
useEffect(() => {
Expand Down Expand Up @@ -277,17 +282,40 @@ export function TaskListView({
{pinnedTasks.length === 0 &&
flatTasks.length === 0 &&
groupedTasks.length === 0 ? (
<div className="flex flex-col items-center gap-3 px-4 py-8 text-center">
<Text size="2" className="text-gray-11">
<div className="flex flex-col items-center gap-1 px-4 pt-6 pb-4 text-center">
<motion.img
src={builderHog}
alt=""
className="pointer-events-none w-[72px]"
initial={{ opacity: 0, y: 8 }}
animate={{
opacity: 1,
y: [0, -4, 0],
}}
transition={{
opacity: { duration: 0.4 },
y: {
duration: 3,
repeat: Infinity,
ease: "easeInOut",
delay: 0.4,
},
}}
/>
<Text size="1" className="text-gray-10">
No tasks yet
</Text>
<button
type="button"
className="rounded-md bg-gray-3 px-3 py-1.5 text-[13px] text-gray-12 transition-colors hover:bg-gray-4"
onClick={() => navigateToTaskInput()}
>
New task
</button>
{!isOnTaskInput && (
<motion.button
type="button"
className="mt-1 rounded-md bg-gray-3 px-3 py-1.5 text-[13px] text-gray-12"
onClick={() => navigateToTaskInput()}
whileHover={{ scale: 1.05, backgroundColor: "var(--gray-4)" }}
whileTap={{ scale: 0.97 }}
>
Start building
</motion.button>
)}
</div>
) : organizeMode === "by-project" ? (
<DragDropProvider
Expand Down
Loading
Loading