Skip to content

Commit 637b323

Browse files
committed
feat(code): warn on local task branch mismatch
1 parent b6365c0 commit 637b323

9 files changed

Lines changed: 751 additions & 9 deletions

File tree

apps/code/src/renderer/features/message-editor/components/MessageEditor.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ function ModeAndBranchRow({
136136
interface MessageEditorProps {
137137
sessionId: string;
138138
placeholder?: string;
139+
onBeforeSubmit?: (text: string, clearEditor: () => void) => boolean;
139140
onSubmit?: (text: string) => void;
140141
onBashCommand?: (command: string) => void;
141142
onBashModeChange?: (isBashMode: boolean) => void;
@@ -154,6 +155,7 @@ export const MessageEditor = forwardRef<EditorHandle, MessageEditorProps>(
154155
{
155156
sessionId,
156157
placeholder = "Type a message... @ to mention files, ! for bash mode, / for skills",
158+
onBeforeSubmit,
157159
onSubmit,
158160
onBashCommand,
159161
onBashModeChange,
@@ -213,6 +215,7 @@ export const MessageEditor = forwardRef<EditorHandle, MessageEditorProps>(
213215
context: { taskId, repoPath },
214216
getPromptHistory,
215217
capabilities: { bashMode: !isCloud },
218+
onBeforeSubmit,
216219
onSubmit,
217220
onBashCommand,
218221
onBashModeChange,

apps/code/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface UseTiptapEditorOptions {
2929
};
3030
clearOnSubmit?: boolean;
3131
getPromptHistory?: () => string[];
32+
onBeforeSubmit?: (text: string, clearEditor: () => void) => boolean;
3233
onSubmit?: (text: string) => void;
3334
onBashCommand?: (command: string) => void;
3435
onBashModeChange?: (isBashMode: boolean) => void;
@@ -84,6 +85,7 @@ export function useTiptapEditor(options: UseTiptapEditorOptions) {
8485
capabilities = {},
8586
clearOnSubmit = true,
8687
getPromptHistory,
88+
onBeforeSubmit,
8789
onSubmit,
8890
onBashCommand,
8991
onBashModeChange,
@@ -99,6 +101,7 @@ export function useTiptapEditor(options: UseTiptapEditorOptions) {
99101
} = capabilities;
100102

101103
const callbackRefs = useRef({
104+
onBeforeSubmit,
102105
onSubmit,
103106
onBashCommand,
104107
onBashModeChange,
@@ -107,6 +110,7 @@ export function useTiptapEditor(options: UseTiptapEditorOptions) {
107110
onBlur,
108111
});
109112
callbackRefs.current = {
113+
onBeforeSubmit,
110114
onSubmit,
111115
onBashCommand,
112116
onBashModeChange,
@@ -450,26 +454,39 @@ export function useTiptapEditor(options: UseTiptapEditorOptions) {
450454

451455
const text = editor.getText().trim();
452456

457+
const doClear = () => {
458+
if (!clearOnSubmit) return;
459+
editor.commands.clearContent();
460+
prevBashModeRef.current = false;
461+
pasteCountRef.current = 0;
462+
setAttachments([]);
463+
draft.clearDraft();
464+
};
465+
453466
if (enableBashMode && text.startsWith("!")) {
454-
// Bash mode requires immediate execution, can't be queued
467+
// Bash mode requires immediate execution, can't be queued.
468+
// Intentionally bypasses onBeforeSubmit — bash commands run inline and
469+
// cannot be deferred the way normal prompts can.
455470
if (isLoading) {
456471
toast.error("Cannot run shell commands while agent is generating");
457472
return;
458473
}
459474
const command = text.slice(1).trim();
460475
if (command) callbackRefs.current.onBashCommand?.(command);
461476
} else {
477+
const serialized = contentToXml(content);
478+
479+
if (
480+
callbackRefs.current.onBeforeSubmit?.(serialized, doClear) === false
481+
) {
482+
return;
483+
}
484+
462485
// Normal prompts can be queued when loading
463-
callbackRefs.current.onSubmit?.(contentToXml(content));
486+
callbackRefs.current.onSubmit?.(serialized);
464487
}
465488

466-
if (clearOnSubmit) {
467-
editor.commands.clearContent();
468-
prevBashModeRef.current = false;
469-
pasteCountRef.current = 0;
470-
setAttachments([]);
471-
draft.clearDraft();
472-
}
489+
doClear();
473490
}, [
474491
editor,
475492
disabled,

apps/code/src/renderer/features/sessions/components/SessionView.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ interface SessionViewProps {
3838
isRunning: boolean;
3939
isPromptPending?: boolean | null;
4040
promptStartedAt?: number | null;
41+
onBeforeSubmit?: (text: string, clearEditor: () => void) => boolean;
4142
onSendPrompt: (text: string) => void;
4243
onBashCommand?: (command: string) => void;
4344
onCancelPrompt: () => void;
@@ -73,6 +74,7 @@ export function SessionView({
7374
isRunning,
7475
isPromptPending = false,
7576
promptStartedAt,
77+
onBeforeSubmit,
7678
onSendPrompt,
7779
onBashCommand,
7880
onCancelPrompt,
@@ -538,6 +540,7 @@ export function SessionView({
538540
ref={editorRef}
539541
sessionId={sessionId}
540542
placeholder="Type a message... @ to mention files, ! for bash mode, / for skills"
543+
onBeforeSubmit={onBeforeSubmit}
541544
onSubmit={handleSubmit}
542545
onBashCommand={onBashCommand}
543546
onCancel={onCancelPrompt}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { GitBranch, Warning } from "@phosphor-icons/react";
2+
import {
3+
AlertDialog,
4+
Button,
5+
Callout,
6+
Code,
7+
Flex,
8+
Text,
9+
} from "@radix-ui/themes";
10+
11+
interface BranchMismatchDialogProps {
12+
open: boolean;
13+
linkedBranch: string;
14+
currentBranch: string;
15+
hasUncommittedChanges: boolean;
16+
switchError: string | null;
17+
onSwitch: () => void;
18+
onContinue: () => void;
19+
onCancel: () => void;
20+
isSwitching?: boolean;
21+
}
22+
23+
function BranchLabel({ name }: { name: string }) {
24+
return (
25+
<Code
26+
size="2"
27+
variant="ghost"
28+
truncate
29+
style={{
30+
maxWidth: "100%",
31+
display: "inline-flex",
32+
alignItems: "center",
33+
gap: "4px",
34+
}}
35+
>
36+
<GitBranch size={12} style={{ flexShrink: 0 }} />
37+
<span
38+
style={{
39+
overflow: "hidden",
40+
textOverflow: "ellipsis",
41+
whiteSpace: "nowrap",
42+
}}
43+
>
44+
{name}
45+
</span>
46+
</Code>
47+
);
48+
}
49+
50+
export function BranchMismatchDialog({
51+
open,
52+
linkedBranch,
53+
currentBranch,
54+
hasUncommittedChanges,
55+
switchError,
56+
onSwitch,
57+
onContinue,
58+
onCancel,
59+
isSwitching,
60+
}: BranchMismatchDialogProps) {
61+
return (
62+
<AlertDialog.Root
63+
open={open}
64+
onOpenChange={(isOpen) => {
65+
if (!isOpen) onCancel();
66+
}}
67+
>
68+
<AlertDialog.Content maxWidth="420px" size="2">
69+
<AlertDialog.Title size="3">
70+
<Flex align="center" gap="2">
71+
<Warning size={18} weight="fill" color="var(--orange-9)" />
72+
Wrong branch
73+
</Flex>
74+
</AlertDialog.Title>
75+
<AlertDialog.Description size="2">
76+
This task is linked to a different branch than the one you're
77+
currently on. The agent will make changes on the current branch.
78+
</AlertDialog.Description>
79+
<Flex direction="column" gap="1" mt="3" style={{ minWidth: 0 }}>
80+
<Flex align="center" gap="2" style={{ minWidth: 0 }}>
81+
<Text
82+
size="1"
83+
color="gray"
84+
style={{ flexShrink: 0, width: "64px" }}
85+
>
86+
Linked
87+
</Text>
88+
<BranchLabel name={linkedBranch} />
89+
</Flex>
90+
<Flex align="center" gap="2" style={{ minWidth: 0 }}>
91+
<Text
92+
size="1"
93+
color="gray"
94+
style={{ flexShrink: 0, width: "64px" }}
95+
>
96+
Current
97+
</Text>
98+
<BranchLabel name={currentBranch} />
99+
</Flex>
100+
</Flex>
101+
102+
{hasUncommittedChanges && !switchError && (
103+
<Callout.Root size="1" color="gray" mt="3">
104+
<Callout.Text size="1">
105+
You have uncommitted changes on your current branch. If needed,
106+
commit or stash them first.
107+
</Callout.Text>
108+
</Callout.Root>
109+
)}
110+
111+
{switchError && (
112+
<Callout.Root size="1" color="red" mt="3">
113+
<Callout.Text size="1">{switchError}</Callout.Text>
114+
</Callout.Root>
115+
)}
116+
117+
<Flex justify="end" gap="2" mt="4">
118+
<AlertDialog.Cancel>
119+
<Button
120+
variant="soft"
121+
color="gray"
122+
size="1"
123+
onClick={onCancel}
124+
disabled={isSwitching}
125+
>
126+
Cancel
127+
</Button>
128+
</AlertDialog.Cancel>
129+
130+
<Button
131+
variant="soft"
132+
color="orange"
133+
size="1"
134+
onClick={onContinue}
135+
disabled={isSwitching}
136+
>
137+
Continue anyway
138+
</Button>
139+
140+
<AlertDialog.Action>
141+
<Button
142+
variant="solid"
143+
size="1"
144+
onClick={onSwitch}
145+
loading={isSwitching}
146+
>
147+
Switch branch
148+
</Button>
149+
</AlertDialog.Action>
150+
</Flex>
151+
</AlertDialog.Content>
152+
</AlertDialog.Root>
153+
);
154+
}

apps/code/src/renderer/features/task-detail/components/TaskLogsPanel.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import { useSessionConnection } from "@features/sessions/hooks/useSessionConnect
1515
import { useSessionViewState } from "@features/sessions/hooks/useSessionViewState";
1616
import { useRestoreTask } from "@features/suspension/hooks/useRestoreTask";
1717
import { useSuspendedTaskIds } from "@features/suspension/hooks/useSuspendedTaskIds";
18+
import { BranchMismatchDialog } from "@features/task-detail/components/BranchMismatchDialog";
1819
import { WorkspaceSetupPrompt } from "@features/task-detail/components/WorkspaceSetupPrompt";
20+
import { useBranchMismatchDialog } from "@features/workspace/hooks/useBranchMismatchDialog";
1921
import {
2022
useCreateWorkspace,
2123
useWorkspaceLoaded,
@@ -81,6 +83,12 @@ export function TaskLogsPanel({ taskId, task, hideInput }: TaskLogsPanelProps) {
8183
handleBashCommand,
8284
} = useSessionCallbacks({ taskId, task, session, repoPath });
8385

86+
const { handleBeforeSubmit, dialogProps } = useBranchMismatchDialog({
87+
taskId,
88+
repoPath,
89+
onSendPrompt: handleSendPrompt,
90+
});
91+
8492
const cloudOutput = session?.cloudOutput ?? null;
8593
const prUrl =
8694
isCloud && cloudOutput?.pr_url ? (cloudOutput.pr_url as string) : null;
@@ -147,6 +155,7 @@ export function TaskLogsPanel({ taskId, task, hideInput }: TaskLogsPanelProps) {
147155
isRestoring={isRestoring}
148156
isPromptPending={isPromptPending}
149157
promptStartedAt={promptStartedAt}
158+
onBeforeSubmit={handleBeforeSubmit}
150159
onSendPrompt={handleSendPrompt}
151160
onBashCommand={isCloud ? undefined : handleBashCommand}
152161
onCancelPrompt={handleCancelPrompt}
@@ -165,6 +174,8 @@ export function TaskLogsPanel({ taskId, task, hideInput }: TaskLogsPanelProps) {
165174
</ErrorBoundary>
166175
</Box>
167176
</Flex>
177+
178+
{dialogProps && <BranchMismatchDialog {...dialogProps} />}
168179
</BackgroundWrapper>
169180
);
170181
}

0 commit comments

Comments
 (0)