Skip to content
Open
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
7 changes: 6 additions & 1 deletion apps/code/src/renderer/components/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { useVisualTaskOrder } from "@features/sidebar/hooks/useVisualTaskOrder";
import { SkillsView } from "@features/skills/components/SkillsView";
import { TaskDetail } from "@features/task-detail/components/TaskDetail";
import { TaskInput } from "@features/task-detail/components/TaskInput";
import { TaskPendingView } from "@features/task-detail/components/TaskPendingView";
import { useTasks } from "@features/tasks/hooks/useTasks";
import { TourOverlay } from "@features/tour/components/TourOverlay";
import {
Expand Down Expand Up @@ -158,6 +159,10 @@ export function MainLayout() {
<TaskDetail key={view.data.id} task={view.data} />
)}

{view.type === "task-pending" && view.pendingTaskKey && (
<TaskPendingView pendingTaskKey={view.pendingTaskKey} />
)}

{view.type === "folder-settings" && <FolderSettingsView />}

{view.type === "inbox" && <InboxView />}
Expand All @@ -176,7 +181,7 @@ export function MainLayout() {
tasks={visualTaskOrder}
activeTaskId={activeTaskId}
allTasks={tasks ?? []}
isOnNewTask={view.type === "task-input"}
isOnNewTask={view.type === "task-input" || view.type === "task-pending"}
onNavigateToTask={navigateToTask}
onNewTask={navigateToTaskInput}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { UserMessageAttachment } from "@features/sessions/components/session-update/UserMessage";
import { CHAT_CONTENT_MAX_WIDTH } from "@features/sessions/constants";
import { Brain } from "@phosphor-icons/react";
import { Box, Flex, Text } from "@radix-ui/themes";
import { PendingInputPlaceholder } from "./PendingInputPlaceholder";
import { UserMessage } from "./session-update/UserMessage";

interface PendingChatViewProps {
promptText: string;
attachments?: UserMessageAttachment[];
}

export function PendingChatView({
promptText,
attachments,
}: PendingChatViewProps) {
return (
<Flex direction="column" className="absolute inset-0 bg-background">
<Box className="min-h-0 flex-1 overflow-y-auto">
<Box
className="mx-auto flex flex-col gap-3 px-2 py-1.5"
style={{ maxWidth: CHAT_CONTENT_MAX_WIDTH }}
>
<UserMessage
content={promptText}
attachments={attachments}
animate={false}
/>
<Flex align="center" gap="2" className="pl-3">
<Brain size={12} className="ph-pulse text-accent-11" />
<Text className="text-[13px] text-accent-11">Starting task...</Text>
</Flex>
</Box>
</Box>
<Box
className="mx-auto w-full p-2"
style={{ maxWidth: CHAT_CONTENT_MAX_WIDTH }}
>
<PendingInputPlaceholder />
</Box>
</Flex>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Box, Flex } from "@radix-ui/themes";

/**
* Non-interactive skeleton sized to match {@link PromptInput} so the chat
* shell does not jump when the real editor mounts after session init.
*/
export function PendingInputPlaceholder() {
return (
<Box
aria-hidden
className="w-full rounded-(--radius-2) border border-(--gray-5) bg-card opacity-70"
>
<Box className="min-h-[50px] px-2 py-2">
<Box className="h-3 w-2/5 animate-pulse rounded bg-gray-4" />
</Box>
<Flex
align="center"
gap="2"
className="border-(--gray-4) border-t px-2 py-1.5"
>
<Box className="h-5 w-5 animate-pulse rounded bg-gray-4" />
<Box className="h-5 w-16 animate-pulse rounded bg-gray-4" />
<Box className="h-5 w-20 animate-pulse rounded bg-gray-4" />
<Box className="ml-auto h-6 w-6 animate-pulse rounded bg-gray-5" />
</Flex>
</Box>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import {
isJsonRpcNotification,
isJsonRpcResponse,
} from "@shared/types/session-events";
import {
pendingTaskPromptStoreApi,
usePendingTaskPrompt,
} from "@stores/pendingTaskPromptStore";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { getSessionService } from "../service/service";
import { flattenSelectOptions } from "../stores/sessionStore";
Expand All @@ -40,6 +44,7 @@ import { CloudInitializingView } from "./CloudInitializingView";
import { ConversationView } from "./ConversationView";
import { DropZoneOverlay } from "./DropZoneOverlay";
import { ModelSelector } from "./ModelSelector";
import { PendingChatView } from "./PendingChatView";
import { PlanStatusBar } from "./PlanStatusBar";
import { ReasoningLevelSelector } from "./ReasoningLevelSelector";
import { RawLogsView } from "./raw-logs/RawLogsView";
Expand Down Expand Up @@ -128,6 +133,7 @@ export function SessionView({
}: SessionViewProps) {
const showRawLogs = useShowRawLogs();
const { setShowRawLogs } = useSessionViewActions();
const pendingTaskPrompt = usePendingTaskPrompt(taskId);
const pendingPermissions = usePendingPermissionsForTask(taskId);
const modeOption = useModeConfigOptionForTask(taskId);
const thoughtOption = useThoughtLevelConfigOptionForTask(taskId);
Expand All @@ -138,6 +144,12 @@ export function SessionView({
const handoffInProgress =
useSessionForTask(taskId)?.handoffInProgress ?? false;

useEffect(() => {
if (!taskId) return;
if (isInitializing) return;
pendingTaskPromptStoreApi.clear(taskId);
}, [taskId, isInitializing]);

useEffect(() => {
if (allowBypassPermissions) return;
// Cloud runs execute in an isolated sandbox where bypass is safe, and the
Expand Down Expand Up @@ -512,6 +524,11 @@ export function SessionView({
) : isInitializing ? (
isCloud ? (
<CloudInitializingView cloudStatus={cloudStatus} />
) : pendingTaskPrompt?.promptText ? (
<PendingChatView
promptText={pendingTaskPrompt.promptText}
attachments={pendingTaskPrompt.attachments}
/>
) : (
<Flex
align="center"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,8 @@ export function TaskListView({
(state) => state.navigateToTaskInput,
);
const isOnTaskInput = useNavigationStore(
(state) => state.view.type === "task-input",
(state) =>
state.view.type === "task-input" || state.view.type === "task-pending",
);

// biome-ignore lint/correctness/useExhaustiveDependencies: reset pagination when filters change
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export interface SidebarData {
interface ViewState {
type:
| "task-detail"
| "task-pending"
| "task-input"
| "settings"
| "folder-settings"
Expand Down Expand Up @@ -217,7 +218,8 @@ export function useSidebarData({
const sortMode = useSidebarStore((state) => state.sortMode);
const folderOrder = useSidebarStore((state) => state.folderOrder);

const isHomeActive = activeView.type === "task-input";
const isHomeActive =
activeView.type === "task-input" || activeView.type === "task-pending";
const isInboxActive = activeView.type === "inbox";
const isCommandCenterActive = activeView.type === "command-center";
const isSkillsActive = activeView.type === "skills";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { PendingChatView } from "@features/sessions/components/PendingChatView";
import { Box } from "@radix-ui/themes";
import { usePendingTaskPrompt } from "@stores/pendingTaskPromptStore";

interface TaskPendingViewProps {
pendingTaskKey: string;
}

export function TaskPendingView({ pendingTaskKey }: TaskPendingViewProps) {
const pending = usePendingTaskPrompt(pendingTaskKey);

return (
<Box className="relative h-full w-full bg-background">
<PendingChatView
promptText={pending?.promptText ?? ""}
attachments={pending?.attachments}
/>
</Box>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { buildCloudTaskDescription } from "@features/editor/utils/cloud-prompt";
import { useTaskInputHistoryStore } from "@features/message-editor/stores/taskInputHistoryStore";
import type { EditorHandle } from "@features/message-editor/types";
import {
contentToPlainText,
contentToXml,
type EditorContent,
extractFilePaths,
Expand All @@ -20,6 +21,7 @@ import { toast } from "@renderer/utils/toast";
import type { ExecutionMode, Task } from "@shared/types";
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
import { useNavigationStore } from "@stores/navigationStore";
import { pendingTaskPromptStoreApi } from "@stores/pendingTaskPromptStore";
import { track } from "@utils/analytics";
import { logger } from "@utils/logger";
import { useCallback, useState } from "react";
Expand Down Expand Up @@ -187,8 +189,12 @@ export function useTaskCreation({
onTaskCreated,
}: UseTaskCreationOptions): UseTaskCreationReturn {
const [isCreatingTask, setIsCreatingTask] = useState(false);
const { clearTaskInputReportAssociation, navigateToTask } =
useNavigationStore();
const {
clearTaskInputReportAssociation,
navigateToTask,
navigateToPendingTask,
navigateToTaskInput,
} = useNavigationStore();
const isAuthenticated = useAuthStateValue(
(state) => state.status === "authenticated",
);
Expand All @@ -210,11 +216,30 @@ export function useTaskCreation({

setIsCreatingTask(true);

try {
const content = contentOverride ?? editor.getContent();
const content = contentOverride ?? editor.getContent();
const plainPromptText = contentToPlainText(content).trim();
const shouldShowPendingView = !onTaskCreated && !!plainPromptText;
const pendingTaskKey = shouldShowPendingView
? (globalThis.crypto?.randomUUID?.() ?? `pending-${Date.now()}`)
: null;

if (pendingTaskKey) {
pendingTaskPromptStoreApi.set(pendingTaskKey, {
promptText: plainPromptText,
attachments: (content.attachments ?? []).map((a) => ({
id: a.id,
label: a.label,
})),
});
navigateToPendingTask(pendingTaskKey);
if (!contentOverride) {
editor.clear();
}
}

try {
if (!contentOverride) {
const plainText = editor.getText()?.trim();
const plainText = editor.getText()?.trim() ?? plainPromptText;
if (plainText) {
useTaskInputHistoryStore.getState().addPrompt(plainText);
}
Expand Down Expand Up @@ -246,13 +271,16 @@ export function useTaskCreation({
if (signalReportId) {
clearTaskInputReportAssociation();
}
if (pendingTaskKey) {
pendingTaskPromptStoreApi.move(pendingTaskKey, output.task.id);
}
if (onTaskCreated) {
onTaskCreated(output.task);
} else {
navigateToTask(output.task);
}
useTourStore.getState().completeTour(createFirstTaskTour.id);
if (!contentOverride) {
if (!pendingTaskKey && !contentOverride) {
editor.clear();
}
});
Expand All @@ -268,13 +296,21 @@ export function useTaskCreation({
failedStep: result.failedStep,
error: result.error,
});
if (pendingTaskKey) {
pendingTaskPromptStoreApi.clear(pendingTaskKey);
navigateToTaskInput({ initialPrompt: plainPromptText });
}
}
return result.success;
} catch (error) {
const description =
error instanceof Error ? error.message : "Unknown error";
toast.error("Failed to create task", { description });
log.error("Unexpected error during task creation", { error });
if (pendingTaskKey) {
pendingTaskPromptStoreApi.clear(pendingTaskKey);
navigateToTaskInput({ initialPrompt: plainPromptText });
}
return false;
} finally {
setIsCreatingTask(false);
Expand All @@ -300,6 +336,8 @@ export function useTaskCreation({
clearTaskInputReportAssociation,
invalidateTasks,
navigateToTask,
navigateToPendingTask,
navigateToTaskInput,
onTaskCreated,
],
);
Expand Down
21 changes: 21 additions & 0 deletions apps/code/src/renderer/stores/navigationStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,27 @@ describe("navigationStore", () => {
type: "inbox",
});
});

it("navigates to pending task with key", () => {
getStore().navigateToPendingTask("pending-key-123");
expect(getView()).toMatchObject({
type: "task-pending",
pendingTaskKey: "pending-key-123",
});
});

it("replaces task-pending in history when navigating to real task", async () => {
getStore().navigateToTaskInput();
getStore().navigateToPendingTask("pending-key-123");
const indexBeforeReal = getStore().history.length - 1;
expect(getStore().history[indexBeforeReal].type).toBe("task-pending");

await getStore().navigateToTask(mockTask);

const finalHistory = getStore().history;
expect(finalHistory[finalHistory.length - 1].type).toBe("task-detail");
expect(finalHistory.some((v) => v.type === "task-pending")).toBe(false);
});
});

describe("history", () => {
Expand Down
Loading
Loading