mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-20 17:13:44 +00:00
🔒 fix: Scope, Cap, and De-Execute the In-Flight Steer Stack
Codex review on 9594ee7146. Three valid P2s, all fallout from moving the
steers out of the message region into the composer.
- Run scope: the in-thread slot was gated on `effectiveIsSubmitting`, but
the new one only checked `steering.enabled` (= steerable endpoint +
primary composer), which is true with no run in flight. A chip that
outlives its run — cancel's onError restoring one the final event
already converted to a queued follow-up — stranded a bubble above the
composer, possibly beside the queued row for the same text. Restores
the run gate.
- Height cap: a steer runs to 16k chars (DEFAULT_STEER_MAX_LENGTH) and a
run takes up to 10 (STEER_QUEUE_MAX_DEPTH). Unbounded in the composer,
that pushes the input off-screen; the old slot could grow freely
because it scrolled with the thread. Caps the stack at 35vh.
- Code execution: MarkdownLite defaults `codeExecution` on, but this
bubble renders outside MessageContext, so Run Code would fire the tool
mutation with no messageId and an empty conversationId. Passes
codeExecution={false} — a provisional steer has nothing to run against.
This commit is contained in:
parent
8f712259ea
commit
de9ede2aad
3 changed files with 35 additions and 5 deletions
|
|
@ -413,7 +413,9 @@ const ChatForm = memo(function ChatForm({
|
||||||
{/* Primary composer owns the selection popup so split-view doesn't double it. */}
|
{/* Primary composer owns the selection popup so split-view doesn't double it. */}
|
||||||
{index === 0 && quotesEnabled && <QuoteButton conversationId={conversationId} />}
|
{index === 0 && quotesEnabled && <QuoteButton conversationId={conversationId} />}
|
||||||
<div className="flex w-full flex-col">
|
<div className="flex w-full flex-col">
|
||||||
{steering.enabled && <InFlightSteers conversationId={conversationId} />}
|
{/* 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 && <InFlightSteers conversationId={conversationId} />}
|
||||||
<div className={cn('flex w-full items-center', isRTL && 'flex-row-reverse')}>
|
<div className={cn('flex w-full items-center', isRTL && 'flex-row-reverse')}>
|
||||||
<Mention
|
<Mention
|
||||||
index={index}
|
index={index}
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,13 @@ const InFlightSteer = memo(function InFlightSteer({
|
||||||
!enableUserMsgMarkdown && 'whitespace-pre-wrap',
|
!enableUserMsgMarkdown && 'whitespace-pre-wrap',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{enableUserMsgMarkdown ? <MarkdownLite content={steer.text} /> : steer.text}
|
{/* No code execution: this bubble sits outside MessageContext, so
|
||||||
|
* Run Code would fire with no message/part to target. */}
|
||||||
|
{enableUserMsgMarkdown ? (
|
||||||
|
<MarkdownLite content={steer.text} codeExecution={false} />
|
||||||
|
) : (
|
||||||
|
steer.text
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!sending && (
|
{!sending && (
|
||||||
|
|
@ -159,7 +165,10 @@ const InFlightSteers = memo(function InFlightSteers({
|
||||||
role="list"
|
role="list"
|
||||||
aria-label={localize('com_ui_steer_in_flight')}
|
aria-label={localize('com_ui_steer_in_flight')}
|
||||||
data-testid="in-flight-steers"
|
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) => (
|
{inFlight.map((steer) => (
|
||||||
<InFlightSteer key={steer.steerId} steer={steer} conversationId={conversationId} />
|
<InFlightSteer key={steer.steerId} steer={steer} conversationId={conversationId} />
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,10 @@ jest.mock('~/components/Chat/Messages/Content/FilePreviewDialog', () => ({
|
||||||
|
|
||||||
jest.mock('~/components/Chat/Messages/Content/MarkdownLite', () => ({
|
jest.mock('~/components/Chat/Messages/Content/MarkdownLite', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: ({ content }: { content: string }) => (
|
default: ({ content, codeExecution }: { content: string; codeExecution?: boolean }) => (
|
||||||
<span data-testid="steer-markdown">{content}</span>
|
<span data-testid="steer-markdown" data-code-execution={String(codeExecution)}>
|
||||||
|
{content}
|
||||||
|
</span>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -193,6 +195,23 @@ describe('InFlightSteers', () => {
|
||||||
expect(screen.getByTestId('steer-markdown')).toHaveTextContent('**bold** steer');
|
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', () => {
|
it('renders raw text when user-message markdown is off', () => {
|
||||||
renderSteers([{ steerId: 's1', text: '**bold** steer', status: 'pending', createdAt: 1 }], {
|
renderSteers([{ steerId: 's1', text: '**bold** steer', status: 'pending', createdAt: 1 }], {
|
||||||
enableUserMsgMarkdown: false,
|
enableUserMsgMarkdown: false,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue