Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
useSessionActions,
} from "@features/sessions/stores/sessionStore";
import type { Plan } from "@features/sessions/types";
import { Box, ContextMenu, Flex } from "@radix-ui/themes";
import { Warning } from "@phosphor-icons/react";
import { Box, Button, ContextMenu, Flex, Text } from "@radix-ui/themes";
import {
type AcpMessage,
isJsonRpcNotification,
Expand Down Expand Up @@ -42,8 +43,15 @@ interface SessionViewProps {
onCancelPrompt: () => void;
repoPath?: string | null;
isCloud?: boolean;
hasError?: boolean;
errorMessage?: string;
onRetry?: () => void;
onDelete?: () => void;
}

const DEFAULT_ERROR_MESSAGE =
"Failed to resume this session. The working directory may have been deleted. Please start a new task.";

export function SessionView({
events,
taskId,
Expand All @@ -54,6 +62,10 @@ export function SessionView({
onCancelPrompt,
repoPath,
isCloud = false,
hasError = false,
errorMessage = DEFAULT_ERROR_MESSAGE,
onRetry,
onDelete,
}: SessionViewProps) {
const showRawLogs = useShowRawLogs();
const { setShowRawLogs } = useSessionViewActions();
Expand Down Expand Up @@ -233,7 +245,44 @@ export function SessionView({

<PlanStatusBar plan={latestPlan} />

{firstPendingPermission ? (
{hasError ? (
<Flex
align="center"
justify="center"
direction="column"
gap="2"
className="absolute inset-0"
>
<Warning size={32} weight="duotone" color="var(--red-9)" />
<Text size="3" weight="medium" color="red">
Session Error
</Text>
<Text
size="2"
align="center"
className="max-w-md px-4 text-gray-11"
>
{errorMessage}
</Text>
<Flex gap="2" mt="2">
{onRetry && (
<Button variant="soft" size="2" onClick={onRetry}>
Retry
</Button>
)}
{onDelete && (
<Button
variant="soft"
size="2"
color="red"
onClick={onDelete}
>
Delete Task
</Button>
)}
</Flex>
</Flex>
) : firstPendingPermission ? (
<InlinePermissionSelector
title={firstPendingPermission.title}
options={firstPendingPermission.options}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface AgentSession {
events: AcpMessage[];
startedAt: number;
status: "connecting" | "connected" | "disconnected" | "error";
errorMessage?: string;
isPromptPending: boolean;
isCloud: boolean;
logUrl?: string;
Expand Down Expand Up @@ -91,6 +92,7 @@ interface SessionActions {
customInput?: string,
) => Promise<void>;
cancelPermission: (taskId: string, toolCallId: string) => Promise<void>;
clearSessionError: (taskId: string) => void;
}

interface AuthCredentials {
Expand Down Expand Up @@ -160,6 +162,14 @@ function subscribeToChannel(taskRunId: string) {
},
onError: (err) => {
log.error("Session subscription error", { taskRunId, error: err });
useStore.setState((state) => {
const session = state.sessions[taskRunId];
if (session) {
session.status = "error";
session.errorMessage =
"Lost connection to the agent. Please restart the task.";
}
});
},
},
);
Expand Down Expand Up @@ -640,7 +650,11 @@ const useStore = create<SessionStore>()(
}
} else {
unsubscribeFromChannel(taskRunId);
removeSession(taskRunId);
updateSession(taskRunId, {
status: "error",
errorMessage:
"Failed to reconnect to the agent. Please restart the task.",
});
}
};

Expand All @@ -652,14 +666,14 @@ const useStore = create<SessionStore>()(
executionMode?: "plan" | "acceptEdits",
) => {
if (!auth.client) {
log.error("API client not available");
return;
throw new Error(
"Unable to reach server. Please check your connection.",
);
}

const taskRun = await auth.client.createTaskRun(taskId);
if (!taskRun?.id) {
log.error("Task run created without ID");
return;
throw new Error("Failed to create task run. Please try again.");
}

const persistedMode = getPersistedTaskMode(taskId);
Expand Down Expand Up @@ -776,6 +790,12 @@ const useStore = create<SessionStore>()(
const auth = getAuthCredentials();
if (!auth) {
log.error("Missing auth credentials");
const taskRunId = latestRun?.id ?? `error-${taskId}`;
const session = createBaseSession(taskRunId, taskId, isCloud);
session.status = "error";
session.errorMessage =
"Authentication required. Please sign in to continue.";
addSession(session);
return;
}

Expand All @@ -787,6 +807,32 @@ const useStore = create<SessionStore>()(
taskDescription,
);
} else if (latestRun?.id && latestRun?.log_url) {
// Check if workspace still exists before attempting reconnect
const workspaceExists = await trpcVanilla.workspace.verify.query({
taskId,
});

if (!workspaceExists) {
// Workspace was deleted - show historical logs in error state
log.warn("Workspace no longer exists, showing error state", {
taskId,
});
const { rawEntries } = await fetchSessionLogs(
latestRun.log_url,
);
const events = convertStoredEntriesToEvents(rawEntries);

const session = createBaseSession(latestRun.id, taskId, false);
session.events = events;
session.logUrl = latestRun.log_url;
session.status = "error";
session.errorMessage =
"The working directory for this task no longer exists. Please start a new task.";

addSession(session);
return;
}

await reconnectToLocalSession(
taskId,
latestRun.id,
Expand All @@ -807,6 +853,27 @@ const useStore = create<SessionStore>()(
const message =
error instanceof Error ? error.message : String(error);
log.error("Failed to connect to task", { message });

// Create session in error state so user sees what happened
const taskRunId = latestRun?.id ?? `error-${taskId}`;
const session = createBaseSession(taskRunId, taskId, isCloud);
session.status = "error";
session.errorMessage = `Failed to connect to the agent: ${message}`;

// Try to load historical logs if available
if (latestRun?.log_url) {
try {
const { rawEntries } = await fetchSessionLogs(
latestRun.log_url,
);
session.events = convertStoredEntriesToEvents(rawEntries);
session.logUrl = latestRun.log_url;
} catch {
// Ignore log fetch errors - just show error state without logs
}
}

addSession(session);
} finally {
connectAttempts.delete(taskId);
}
Expand Down Expand Up @@ -1106,6 +1173,14 @@ const useStore = create<SessionStore>()(
});
}
},

clearSessionError: (taskId: string) => {
const session = getSessionByTaskId(taskId);
if (session) {
removeSession(session.taskRunId);
}
connectAttempts.delete(taskId);
},
},
};
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { useSettingsStore } from "@features/settings/stores/settingsStore";
import { useTaskViewedStore } from "@features/sidebar/stores/taskViewedStore";
import { useTaskData } from "@features/task-detail/hooks/useTaskData";
import { useDeleteTask } from "@features/tasks/hooks/useTasks";
import {
selectWorktreePath,
useWorkspaceStore,
Expand All @@ -32,13 +33,17 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
const repoPath = worktreePath ?? taskData.repoPath;

const session = useSessionForTask(taskId);
const { connectToTask, sendPrompt, cancelPrompt } = useSessionActions();
const { connectToTask, sendPrompt, cancelPrompt, clearSessionError } =
useSessionActions();
const { deleteWithConfirm } = useDeleteTask();
const markActivity = useTaskViewedStore((state) => state.markActivity);
const markAsViewed = useTaskViewedStore((state) => state.markAsViewed);
const requestFocus = useDraftStore((s) => s.actions.requestFocus);

const isRunning =
session?.status === "connected" || session?.status === "connecting";
const hasError = session?.status === "error";
const errorMessage = session?.errorMessage;

const events = session?.events ?? [];
const isPromptPending = session?.isPromptPending ?? false;
Expand All @@ -54,7 +59,12 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
if (!repoPath) return;
if (isConnecting.current) return;

if (session?.status === "connected" || session?.status === "connecting") {
// Don't reconnect if already connected, connecting, or in error state
if (
session?.status === "connected" ||
session?.status === "connecting" ||
session?.status === "error"
) {
return;
}

Expand Down Expand Up @@ -122,6 +132,21 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {

const { appendUserShellExecute } = useSessionActions();

const handleRetry = useCallback(() => {
if (!repoPath) return;
clearSessionError(taskId);
connectToTask({ task, repoPath });
}, [taskId, repoPath, task, clearSessionError, connectToTask]);

const handleDelete = useCallback(() => {
const hasWorktree = !!worktreePath;
deleteWithConfirm({
taskId,
taskTitle: task.title ?? task.description ?? "Untitled",
hasWorktree,
});
}, [taskId, task, worktreePath, deleteWithConfirm]);

const handleBashCommand = useCallback(
async (command: string) => {
if (!repoPath) return;
Expand Down Expand Up @@ -152,6 +177,10 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
onCancelPrompt={handleCancelPrompt}
repoPath={repoPath}
isCloud={session?.isCloud ?? false}
hasError={hasError}
errorMessage={errorMessage}
onRetry={handleRetry}
onDelete={handleDelete}
/>
</Box>
</BackgroundWrapper>
Expand Down