diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx
index 8e601f7adf..8c56e0371e 100644
--- a/client/src/components/Chat/Input/ChatForm.tsx
+++ b/client/src/components/Chat/Input/ChatForm.tsx
@@ -413,7 +413,9 @@ const ChatForm = memo(function ChatForm({
{/* Primary composer owns the selection popup so split-view doesn't double it. */}
{index === 0 && quotesEnabled && }
- {steering.enabled &&
}
+ {/* Run-scoped: `enabled` alone is any primary composer on a steerable
+ endpoint, so a chip that outlives the run would strand a bubble. */}
+ {steering.enabled && isSubmitting &&
}
- {enableUserMsgMarkdown ? : steer.text}
+ {/* No code execution: this bubble sits outside MessageContext, so
+ * Run Code would fire with no message/part to target. */}
+ {enableUserMsgMarkdown ? (
+
+ ) : (
+ steer.text
+ )}
{!sending && (
@@ -159,7 +165,10 @@ const InFlightSteers = memo(function InFlightSteers({
role="list"
aria-label={localize('com_ui_steer_in_flight')}
data-testid="in-flight-steers"
- className="flex flex-col items-start gap-2 px-2 pb-2"
+ /* Capped: a steer runs to 16k chars and a run takes up to 10 of them.
+ * Unbounded, the stack would push the composer off-screen — the old
+ * in-thread slot could grow freely because it scrolled with the thread. */
+ className="flex max-h-[35vh] flex-col items-start gap-2 overflow-y-auto px-2 pb-2"
>
{inFlight.map((steer) => (
diff --git a/client/src/components/Chat/Input/__tests__/InFlightSteers.test.tsx b/client/src/components/Chat/Input/__tests__/InFlightSteers.test.tsx
index 59a2c68e6c..da2193d0d7 100644
--- a/client/src/components/Chat/Input/__tests__/InFlightSteers.test.tsx
+++ b/client/src/components/Chat/Input/__tests__/InFlightSteers.test.tsx
@@ -42,8 +42,10 @@ jest.mock('~/components/Chat/Messages/Content/FilePreviewDialog', () => ({
jest.mock('~/components/Chat/Messages/Content/MarkdownLite', () => ({
__esModule: true,
- default: ({ content }: { content: string }) => (
- {content}
+ default: ({ content, codeExecution }: { content: string; codeExecution?: boolean }) => (
+
+ {content}
+
),
}));
@@ -193,6 +195,23 @@ describe('InFlightSteers', () => {
expect(screen.getByTestId('steer-markdown')).toHaveTextContent('**bold** steer');
});
+ it('disables code execution: the bubble has no message/part for Run Code to target', () => {
+ renderSteers([{ steerId: 's1', text: '```js\nrun()\n```', status: 'pending', createdAt: 1 }], {
+ enableUserMsgMarkdown: true,
+ });
+ // This component renders outside MessageContext, so an executable code
+ // block would fire the tool mutation with no messageId/conversationId.
+ expect(screen.getByTestId('steer-markdown')).toHaveAttribute('data-code-execution', 'false');
+ });
+
+ it('caps the stack so a long steer cannot push the composer off-screen', () => {
+ renderSteers([{ steerId: 's1', text: 'x'.repeat(4000), status: 'pending', createdAt: 1 }]);
+ // A steer runs to 16k chars, and a run takes up to 10 of them.
+ const stack = screen.getByTestId('in-flight-steers');
+ expect(stack.className).toContain('max-h-[35vh]');
+ expect(stack.className).toContain('overflow-y-auto');
+ });
+
it('renders raw text when user-message markdown is off', () => {
renderSteers([{ steerId: 's1', text: '**bold** steer', status: 'pending', createdAt: 1 }], {
enableUserMsgMarkdown: false,