diff --git a/apps/code/src/renderer/components/MainLayout.tsx b/apps/code/src/renderer/components/MainLayout.tsx
index 70b0dee87..ce3690e68 100644
--- a/apps/code/src/renderer/components/MainLayout.tsx
+++ b/apps/code/src/renderer/components/MainLayout.tsx
@@ -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 {
@@ -158,6 +159,10 @@ export function MainLayout() {
)}
+ {view.type === "task-pending" && view.pendingTaskKey && (
+
+ )}
+
{view.type === "folder-settings" && }
{view.type === "inbox" && }
@@ -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}
/>
diff --git a/apps/code/src/renderer/features/sessions/components/PendingChatView.tsx b/apps/code/src/renderer/features/sessions/components/PendingChatView.tsx
new file mode 100644
index 000000000..e657e41f3
--- /dev/null
+++ b/apps/code/src/renderer/features/sessions/components/PendingChatView.tsx
@@ -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 (
+
+
+
+
+
+
+ Starting task...
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/code/src/renderer/features/sessions/components/PendingInputPlaceholder.tsx b/apps/code/src/renderer/features/sessions/components/PendingInputPlaceholder.tsx
new file mode 100644
index 000000000..571fb1cdf
--- /dev/null
+++ b/apps/code/src/renderer/features/sessions/components/PendingInputPlaceholder.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/code/src/renderer/features/sessions/components/SessionView.tsx b/apps/code/src/renderer/features/sessions/components/SessionView.tsx
index b95675e7d..49b9cdfa9 100644
--- a/apps/code/src/renderer/features/sessions/components/SessionView.tsx
+++ b/apps/code/src/renderer/features/sessions/components/SessionView.tsx
@@ -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";
@@ -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";
@@ -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);
@@ -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
@@ -512,6 +524,11 @@ export function SessionView({
) : isInitializing ? (
isCloud ? (
+ ) : pendingTaskPrompt?.promptText ? (
+
) : (
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
diff --git a/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts b/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts
index 66a6ef00e..c7a49b150 100644
--- a/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts
+++ b/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts
@@ -65,6 +65,7 @@ export interface SidebarData {
interface ViewState {
type:
| "task-detail"
+ | "task-pending"
| "task-input"
| "settings"
| "folder-settings"
@@ -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";
diff --git a/apps/code/src/renderer/features/task-detail/components/TaskPendingView.tsx b/apps/code/src/renderer/features/task-detail/components/TaskPendingView.tsx
new file mode 100644
index 000000000..37563e108
--- /dev/null
+++ b/apps/code/src/renderer/features/task-detail/components/TaskPendingView.tsx
@@ -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 (
+
+
+
+ );
+}
diff --git a/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts b/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts
index 061d40a91..3d4fe4d7c 100644
--- a/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts
+++ b/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts
@@ -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,
@@ -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";
@@ -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",
);
@@ -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);
}
@@ -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();
}
});
@@ -268,6 +296,10 @@ export function useTaskCreation({
failedStep: result.failedStep,
error: result.error,
});
+ if (pendingTaskKey) {
+ pendingTaskPromptStoreApi.clear(pendingTaskKey);
+ navigateToTaskInput({ initialPrompt: plainPromptText });
+ }
}
return result.success;
} catch (error) {
@@ -275,6 +307,10 @@ export function useTaskCreation({
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);
@@ -300,6 +336,8 @@ export function useTaskCreation({
clearTaskInputReportAssociation,
invalidateTasks,
navigateToTask,
+ navigateToPendingTask,
+ navigateToTaskInput,
onTaskCreated,
],
);
diff --git a/apps/code/src/renderer/stores/navigationStore.test.ts b/apps/code/src/renderer/stores/navigationStore.test.ts
index 0a6584ee8..c5fa8232c 100644
--- a/apps/code/src/renderer/stores/navigationStore.test.ts
+++ b/apps/code/src/renderer/stores/navigationStore.test.ts
@@ -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", () => {
diff --git a/apps/code/src/renderer/stores/navigationStore.ts b/apps/code/src/renderer/stores/navigationStore.ts
index bb43e334a..c3f4d8b2a 100644
--- a/apps/code/src/renderer/stores/navigationStore.ts
+++ b/apps/code/src/renderer/stores/navigationStore.ts
@@ -14,6 +14,7 @@ const log = logger.scope("navigation-store");
type ViewType =
| "task-detail"
+ | "task-pending"
| "task-input"
| "folder-settings"
| "inbox"
@@ -43,6 +44,7 @@ interface ViewState {
initialPrompt?: string;
initialCloudRepository?: string;
reportAssociation?: TaskInputReportAssociation;
+ pendingTaskKey?: string;
}
interface NavigationStore {
@@ -52,6 +54,7 @@ interface NavigationStore {
taskInputReportAssociation?: TaskInputReportAssociation;
taskInputCloudRepository?: string;
navigateToTask: (task: Task) => void;
+ navigateToPendingTask: (pendingTaskKey: string) => void;
navigateToTaskInput: (
folderIdOrOptions?: string | TaskInputNavigationOptions,
) => void;
@@ -74,6 +77,9 @@ const isSameView = (view1: ViewState, view2: ViewState): boolean => {
if (view1.type === "task-detail" && view2.type === "task-detail") {
return view1.data?.id === view2.data?.id;
}
+ if (view1.type === "task-pending" && view2.type === "task-pending") {
+ return view1.pendingTaskKey === view2.pendingTaskKey;
+ }
if (view1.type === "task-input" && view2.type === "task-input") {
return (
view1.folderId === view2.folderId &&
@@ -109,7 +115,14 @@ export const useNavigationStore = create()(
if (isSameView(view, newView)) {
return;
}
- const newHistory = [...history.slice(0, historyIndex + 1), newView];
+ // Replace transient task-pending entries instead of stacking them in
+ // history — going back to a pending view after the real task lands
+ // would render an empty placeholder.
+ const baseHistory =
+ view.type === "task-pending"
+ ? history.slice(0, historyIndex)
+ : history.slice(0, historyIndex + 1);
+ const newHistory = [...baseHistory, newView];
set({
view: newView,
history: newHistory,
@@ -186,6 +199,10 @@ export const useNavigationStore = create()(
}
},
+ navigateToPendingTask: (pendingTaskKey: string) => {
+ navigate({ type: "task-pending", pendingTaskKey });
+ },
+
navigateToTaskInput: (folderIdOrOptions) => {
const options =
typeof folderIdOrOptions === "string"
@@ -326,11 +343,14 @@ export const useNavigationStore = create()(
name: "navigation-storage",
storage: electronStorage,
partialize: (state) => ({
- view: {
- type: state.view.type,
- taskId: state.view.taskId,
- folderId: state.view.folderId,
- },
+ view:
+ state.view.type === "task-pending"
+ ? { type: "task-input" as const }
+ : {
+ type: state.view.type,
+ taskId: state.view.taskId,
+ folderId: state.view.folderId,
+ },
}),
},
),
diff --git a/apps/code/src/renderer/stores/pendingTaskPromptStore.ts b/apps/code/src/renderer/stores/pendingTaskPromptStore.ts
new file mode 100644
index 000000000..2412bd954
--- /dev/null
+++ b/apps/code/src/renderer/stores/pendingTaskPromptStore.ts
@@ -0,0 +1,56 @@
+import type { UserMessageAttachment } from "@features/sessions/components/session-update/UserMessage";
+import { create } from "zustand";
+
+export interface PendingTaskPrompt {
+ promptText: string;
+ attachments: UserMessageAttachment[];
+}
+
+interface PendingTaskPromptStore {
+ byKey: Record;
+ set: (key: string, prompt: PendingTaskPrompt) => void;
+ get: (key: string) => PendingTaskPrompt | undefined;
+ move: (fromKey: string, toKey: string) => void;
+ clear: (key: string) => void;
+}
+
+export const usePendingTaskPromptStore = create(
+ (set, get) => ({
+ byKey: {},
+ set: (key, prompt) =>
+ set((state) => ({ byKey: { ...state.byKey, [key]: prompt } })),
+ get: (key) => get().byKey[key],
+ move: (fromKey, toKey) => {
+ if (fromKey === toKey) return;
+ set((state) => {
+ const entry = state.byKey[fromKey];
+ if (!entry) return state;
+ const { [fromKey]: _removed, ...rest } = state.byKey;
+ return { byKey: { ...rest, [toKey]: entry } };
+ });
+ },
+ clear: (key) =>
+ set((state) => {
+ if (!(key in state.byKey)) return state;
+ const { [key]: _removed, ...rest } = state.byKey;
+ return { byKey: rest };
+ }),
+ }),
+);
+
+export const pendingTaskPromptStoreApi = {
+ set: (key: string, prompt: PendingTaskPrompt) =>
+ usePendingTaskPromptStore.getState().set(key, prompt),
+ get: (key: string) => usePendingTaskPromptStore.getState().get(key),
+ move: (fromKey: string, toKey: string) =>
+ usePendingTaskPromptStore.getState().move(fromKey, toKey),
+ clear: (key: string) => usePendingTaskPromptStore.getState().clear(key),
+};
+
+export function usePendingTaskPrompt(
+ key: string | undefined,
+): PendingTaskPrompt | undefined {
+ return usePendingTaskPromptStore((state) =>
+ key ? state.byKey[key] : undefined,
+ );
+}