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 @@ -136,6 +136,7 @@ function ModeAndBranchRow({
interface MessageEditorProps {
sessionId: string;
placeholder?: string;
onBeforeSubmit?: (text: string) => boolean;
onSubmit?: (text: string) => void;
onBashCommand?: (command: string) => void;
onBashModeChange?: (isBashMode: boolean) => void;
Expand All @@ -154,6 +155,7 @@ export const MessageEditor = forwardRef<EditorHandle, MessageEditorProps>(
{
sessionId,
placeholder = "Type a message... @ to mention files, ! for bash mode, / for skills",
onBeforeSubmit,
onSubmit,
onBashCommand,
onBashModeChange,
Expand Down Expand Up @@ -213,6 +215,7 @@ export const MessageEditor = forwardRef<EditorHandle, MessageEditorProps>(
context: { taskId, repoPath },
getPromptHistory,
capabilities: { bashMode: !isCloud },
onBeforeSubmit,
onSubmit,
onBashCommand,
onBashModeChange,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export interface UseTiptapEditorOptions {
};
clearOnSubmit?: boolean;
getPromptHistory?: () => string[];
/**
* Called before submit with the serialized text. Return `false` to block
* the submit — `onSubmit` will not fire and the editor will not clear.
*/
onBeforeSubmit?: (text: string) => boolean;
onSubmit?: (text: string) => void;
onBashCommand?: (command: string) => void;
onBashModeChange?: (isBashMode: boolean) => void;
Expand Down Expand Up @@ -84,6 +89,7 @@ export function useTiptapEditor(options: UseTiptapEditorOptions) {
capabilities = {},
clearOnSubmit = true,
getPromptHistory,
onBeforeSubmit,
onSubmit,
onBashCommand,
onBashModeChange,
Expand All @@ -99,6 +105,7 @@ export function useTiptapEditor(options: UseTiptapEditorOptions) {
} = capabilities;

const callbackRefs = useRef({
onBeforeSubmit,
onSubmit,
onBashCommand,
onBashModeChange,
Expand All @@ -107,6 +114,7 @@ export function useTiptapEditor(options: UseTiptapEditorOptions) {
onBlur,
});
callbackRefs.current = {
onBeforeSubmit,
onSubmit,
onBashCommand,
onBashModeChange,
Expand Down Expand Up @@ -459,8 +467,15 @@ export function useTiptapEditor(options: UseTiptapEditorOptions) {
const command = text.slice(1).trim();
if (command) callbackRefs.current.onBashCommand?.(command);
} else {
const serialized = contentToXml(content);

// Allow callers to block submit (e.g., branch mismatch warning)
if (callbackRefs.current.onBeforeSubmit?.(serialized) === false) {
return;
}

// Normal prompts can be queued when loading
callbackRefs.current.onSubmit?.(contentToXml(content));
callbackRefs.current.onSubmit?.(serialized);
}

if (clearOnSubmit) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface SessionViewProps {
isRunning: boolean;
isPromptPending?: boolean | null;
promptStartedAt?: number | null;
onBeforeSubmit?: (text: string) => boolean;
onSendPrompt: (text: string) => void;
onBashCommand?: (command: string) => void;
onCancelPrompt: () => void;
Expand Down Expand Up @@ -73,6 +74,7 @@ export function SessionView({
isRunning,
isPromptPending = false,
promptStartedAt,
onBeforeSubmit,
onSendPrompt,
onBashCommand,
onCancelPrompt,
Expand Down Expand Up @@ -538,6 +540,7 @@ export function SessionView({
ref={editorRef}
sessionId={sessionId}
placeholder="Type a message... @ to mention files, ! for bash mode, / for skills"
onBeforeSubmit={onBeforeSubmit}
onSubmit={handleSubmit}
onBashCommand={onBashCommand}
onCancel={onCancelPrompt}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { GitBranch, Warning } from "@phosphor-icons/react";
import { AlertDialog, Button, Code, Flex, Text } from "@radix-ui/themes";

interface BranchMismatchDialogProps {
open: boolean;
linkedBranch: string;
currentBranch: string;
onSwitch: () => void;
onContinue: () => void;
onCancel: () => void;
isSwitching?: boolean;
}

function BranchLabel({ name }: { name: string }) {
return (
<Code
size="2"
variant="ghost"
truncate
style={{
maxWidth: "100%",
display: "inline-flex",
alignItems: "center",
gap: "4px",
}}
>
<GitBranch size={12} style={{ flexShrink: 0 }} />
<span
style={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{name}
</span>
</Code>
);
}

export function BranchMismatchDialog({
open,
linkedBranch,
currentBranch,
onSwitch,
onContinue,
onCancel,
isSwitching,
}: BranchMismatchDialogProps) {
return (
<AlertDialog.Root open={open}>
<AlertDialog.Content maxWidth="420px" size="2">
<AlertDialog.Title size="3">
<Flex align="center" gap="2">
<Warning size={18} weight="fill" color="var(--orange-9)" />
Wrong branch
</Flex>
</AlertDialog.Title>
<AlertDialog.Description size="2">
This task is linked to a different branch than the one you're
currently on. The agent will make changes on the current branch.
</AlertDialog.Description>
<Flex direction="column" gap="1" mt="3" style={{ minWidth: 0 }}>
<Flex align="center" gap="2" style={{ minWidth: 0 }}>
<Text
size="1"
color="gray"
style={{ flexShrink: 0, width: "64px" }}
>
Linked
</Text>
<BranchLabel name={linkedBranch} />
</Flex>
<Flex align="center" gap="2" style={{ minWidth: 0 }}>
<Text
size="1"
color="gray"
style={{ flexShrink: 0, width: "64px" }}
>
Current
</Text>
<BranchLabel name={currentBranch} />
</Flex>
</Flex>

<Flex justify="end" gap="2" mt="4">
<AlertDialog.Cancel>
<Button
variant="soft"
color="gray"
size="1"
onClick={onCancel}
disabled={isSwitching}
>
Cancel
</Button>
</AlertDialog.Cancel>

<Button
variant="soft"
size="1"
onClick={onContinue}
disabled={isSwitching}
>
Continue anyway
</Button>

<AlertDialog.Action>
<Button
variant="solid"
size="1"
onClick={onSwitch}
loading={isSwitching}
>
Switch branch
</Button>
</AlertDialog.Action>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,19 @@ import { useSessionConnection } from "@features/sessions/hooks/useSessionConnect
import { useSessionViewState } from "@features/sessions/hooks/useSessionViewState";
import { useRestoreTask } from "@features/suspension/hooks/useRestoreTask";
import { useSuspendedTaskIds } from "@features/suspension/hooks/useSuspendedTaskIds";
import { BranchMismatchDialog } from "@features/task-detail/components/BranchMismatchDialog";
import { WorkspaceSetupPrompt } from "@features/task-detail/components/WorkspaceSetupPrompt";
import { useBranchMismatchGuard } from "@features/workspace/hooks/useBranchMismatch";
import {
useCreateWorkspace,
useWorkspaceLoaded,
} from "@features/workspace/hooks/useWorkspace";
import { Box, Flex } from "@radix-ui/themes";
import { trpcClient } from "@renderer/trpc/client";
import type { Task } from "@shared/types";
import { logger } from "@utils/logger";
import { getTaskRepository } from "@utils/repository";
import { useCallback, useEffect, useMemo } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";

interface TaskLogsPanelProps {
taskId: string;
Expand Down Expand Up @@ -81,6 +85,61 @@ export function TaskLogsPanel({ taskId, task, hideInput }: TaskLogsPanelProps) {
handleBashCommand,
} = useSessionCallbacks({ taskId, task, session, repoPath });

// Branch mismatch guard
const { shouldWarn, linkedBranch, currentBranch, dismissWarning } =
useBranchMismatchGuard(taskId);
const [pendingMessage, setPendingMessage] = useState<string | null>(null);
const [isSwitchingBranch, setIsSwitchingBranch] = useState(false);

const handleBeforeSubmit = useCallback(
(text: string): boolean => {
if (shouldWarn) {
setPendingMessage(text);
return false;
}
return true;
},
[shouldWarn],
);

const handleMismatchSwitch = useCallback(async () => {
if (!linkedBranch || !repoPath) return;
setIsSwitchingBranch(true);
try {
await trpcClient.git.checkoutBranch.mutate({
directoryPath: repoPath,
branchName: linkedBranch,
});
dismissWarning();
if (pendingMessage) {
handleSendPrompt(pendingMessage);
}
} catch (error) {
logger.scope("task-logs-panel").error("Failed to switch branch", error);
} finally {
setIsSwitchingBranch(false);
setPendingMessage(null);
}
}, [
linkedBranch,
repoPath,
dismissWarning,
pendingMessage,
handleSendPrompt,
]);

const handleMismatchContinue = useCallback(() => {
dismissWarning();
if (pendingMessage) {
handleSendPrompt(pendingMessage);
}
setPendingMessage(null);
}, [dismissWarning, pendingMessage, handleSendPrompt]);

const handleMismatchCancel = useCallback(() => {
setPendingMessage(null);
}, []);

const cloudOutput = session?.cloudOutput ?? null;
const prUrl =
isCloud && cloudOutput?.pr_url ? (cloudOutput.pr_url as string) : null;
Expand Down Expand Up @@ -147,6 +206,7 @@ export function TaskLogsPanel({ taskId, task, hideInput }: TaskLogsPanelProps) {
isRestoring={isRestoring}
isPromptPending={isPromptPending}
promptStartedAt={promptStartedAt}
onBeforeSubmit={handleBeforeSubmit}
onSendPrompt={handleSendPrompt}
onBashCommand={isCloud ? undefined : handleBashCommand}
onCancelPrompt={handleCancelPrompt}
Expand All @@ -165,6 +225,18 @@ export function TaskLogsPanel({ taskId, task, hideInput }: TaskLogsPanelProps) {
</ErrorBoundary>
</Box>
</Flex>

{linkedBranch && currentBranch && (
<BranchMismatchDialog
open={pendingMessage !== null}
linkedBranch={linkedBranch}
currentBranch={currentBranch}
onSwitch={handleMismatchSwitch}
onContinue={handleMismatchContinue}
onCancel={handleMismatchCancel}
isSwitching={isSwitchingBranch}
/>
)}
</BackgroundWrapper>
);
}
Loading
Loading