Skip to content

Commit d8212be

Browse files
emrberkclaude
andauthored
refactor: ai assistant revamp, query execution guards (#547)
* refactor: use chronological view in the assistant response, render thinking blocks * submodule * turn based message model with full tool call history in conversations * submodule * query run result bug fixes, show notifications consistently * tests * modal layout fix * query run abortion handling, bug fixes * submodule * pass abort signal * max tokens errors * reviews * reviews pt2 * add timeout cleanup * function rename - splice guard * query execution synchronization updates * timeout cleanup, remove unncessarry dep * remove the key from code block * simplifications for query run * update dep * extra guards * abort tests * single transaction for indexeddb queries * query execution races around chat/script and chat completions tool limit - cancel active query on script-confirm so chat/editor queries don't orphan on the server when the "Run script" dialog is confirmed - add onStop hook to markActive so confirmPending unblocks when the active execution is a multi-query selection script - release _active when the script loop breaks via scriptStopRef - drain pending microtasks before the script loop starts so iteration zero's "Running..." notification and glyph spinner aren't clobbered by a pending chat-abort addNotification - chat completions provider: drop tools from last-round follow-up so MAX_TOOL_CALL_ROUNDS actually terminates the loop Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix editor unmount * submodule * autofocus on input * submodule, java version * reviews * reword open in editor to diff preview --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ce61798 commit d8212be

63 files changed

Lines changed: 5625 additions & 4160 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

e2e/tests/console/aiAssistant.spec.js

Lines changed: 813 additions & 112 deletions
Large diffs are not rendered by default.

e2e/utils/aiAssistant.js

Lines changed: 246 additions & 64 deletions
Large diffs are not rendered by default.

src/components/AIStatusIndicator/AssistantModes.tsx

Lines changed: 34 additions & 356 deletions
Large diffs are not rendered by default.

src/components/AIStatusIndicator/AssistantModesCompact.tsx

Lines changed: 55 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { BrainIcon } from "../SetupAIAssistant/BrainIcon"
1515
import {
1616
buildOperationSections,
1717
formatDurationMs,
18+
getIsExpandableSection,
1819
getSectionDuration,
1920
type OperationSection,
2021
} from "./AssistantModes"
@@ -209,34 +210,6 @@ const CloseCircleIcon = styled(CloseCircle)`
209210
flex-shrink: 0;
210211
`
211212

212-
const GradientCheckCircleIcon = ({ size = 20 }: { size?: number }) => (
213-
<svg
214-
xmlns="http://www.w3.org/2000/svg"
215-
width={size}
216-
height={size}
217-
viewBox="0 0 256 256"
218-
style={{ flexShrink: 0 }}
219-
>
220-
<defs>
221-
<linearGradient
222-
id="checkCircleGradient"
223-
x1="128"
224-
x2="128"
225-
y1="24"
226-
y2="232"
227-
gradientUnits="userSpaceOnUse"
228-
>
229-
<stop stopColor="#d14671" />
230-
<stop offset="1" stopColor="#892c6c" />
231-
</linearGradient>
232-
</defs>
233-
<path
234-
fill="url(#checkCircleGradient)"
235-
d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm45.66,85.66-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32Z"
236-
/>
237-
</svg>
238-
)
239-
240213
const CollapsedSectionsWrapper = styled.div`
241214
display: flex;
242215
flex-direction: column;
@@ -270,20 +243,22 @@ const formatDetailedStatusMessage = (
270243
return status
271244
}
272245

273-
const getIsExpandableSection = (section: OperationSection) => {
274-
return [
275-
AIOperationStatus.InvestigatingTable,
276-
AIOperationStatus.InvestigatingDocs,
277-
].includes(section.type)
278-
}
246+
const ThinkingContent = styled.div`
247+
font-size: 1.3rem;
248+
color: ${color("gray2")};
249+
padding: 0 2.8rem;
250+
white-space: pre-wrap;
251+
word-break: break-word;
252+
opacity: 0.7;
253+
`
279254

280255
type AssistantModesCompactProps = {
281256
operationHistory: OperationHistory
282257
status?: AIOperationStatus | null
283258
isLive?: boolean
284259
onScrollNeeded?: () => void
285260
collapsed?: boolean
286-
responseStart?: number
261+
endTimestamp?: number
287262
}
288263

289264
export const AssistantModesCompact: React.FC<AssistantModesCompactProps> = ({
@@ -292,7 +267,7 @@ export const AssistantModesCompact: React.FC<AssistantModesCompactProps> = ({
292267
isLive = false,
293268
onScrollNeeded,
294269
collapsed = false,
295-
responseStart,
270+
endTimestamp,
296271
}) => {
297272
const theme = useTheme()
298273
const [collapsedSections, setCollapsedSections] = useState<
@@ -360,9 +335,12 @@ export const AssistantModesCompact: React.FC<AssistantModesCompactProps> = ({
360335
const durationText = useMemo(() => {
361336
if (operationHistory.length === 0) return null
362337
const firstTimestamp = operationHistory[0]?.timestamp
363-
if (!firstTimestamp || !responseStart) return null
364-
return formatDurationMs(responseStart - firstTimestamp)
365-
}, [operationHistory, responseStart])
338+
const lastTimestamp =
339+
endTimestamp ?? operationHistory[operationHistory.length - 1]?.timestamp
340+
if (!firstTimestamp || !lastTimestamp || firstTimestamp === lastTimestamp)
341+
return null
342+
return formatDurationMs(lastTimestamp - firstTimestamp)
343+
}, [operationHistory, endTimestamp])
366344

367345
if (operationHistory.length === 0) {
368346
return null
@@ -381,8 +359,12 @@ export const AssistantModesCompact: React.FC<AssistantModesCompactProps> = ({
381359
const sectionDuration = getSectionDuration(
382360
section,
383361
nextSection,
384-
responseStart,
362+
!nextSection ? endTimestamp : undefined,
385363
)
364+
const thinkingSegmentText =
365+
section.type === AIOperationStatus.Thinking
366+
? (section.operations[0]?.content ?? "")
367+
: ""
386368

387369
return (
388370
<ModeHeader
@@ -394,10 +376,22 @@ export const AssistantModesCompact: React.FC<AssistantModesCompactProps> = ({
394376
$expanded={isExpanded}
395377
$isExpandable={isExpandable}
396378
data-hook={`assistant-mode-${section.type.toLowerCase().replace(/\s+/g, "-")}`}
397-
role="presentation"
379+
role={isExpandable ? "button" : "presentation"}
380+
tabIndex={isExpandable ? 0 : undefined}
381+
aria-expanded={isExpandable ? isExpanded : undefined}
398382
onClick={
399383
isExpandable ? () => handleToggleSection(section.id) : undefined
400384
}
385+
onKeyDown={
386+
isExpandable
387+
? (e: React.KeyboardEvent) => {
388+
if (e.key === "Enter" || e.key === " ") {
389+
e.preventDefault()
390+
handleToggleSection(section.id)
391+
}
392+
}
393+
: undefined
394+
}
401395
>
402396
<ReasoningIcon>
403397
{section.active ? (
@@ -420,7 +414,16 @@ export const AssistantModesCompact: React.FC<AssistantModesCompactProps> = ({
420414
)}
421415
</Box>
422416
</ModeHeaderTop>
423-
{isExpandable && (
417+
{isExpandable && section.type === AIOperationStatus.Thinking && (
418+
<ExpandableWrapper $expanded={isExpanded}>
419+
<ExpandableContent>
420+
{thinkingSegmentText ? (
421+
<ThinkingContent>{thinkingSegmentText}</ThinkingContent>
422+
) : null}
423+
</ExpandableContent>
424+
</ExpandableWrapper>
425+
)}
426+
{isExpandable && section.type !== AIOperationStatus.Thinking && (
424427
<ExpandableWrapper $expanded={isExpanded}>
425428
<ExpandableContent>
426429
<ReasoningThread>
@@ -594,10 +597,18 @@ export const AssistantModesCompact: React.FC<AssistantModesCompactProps> = ({
594597
$expanded={collapsedProcessingExpanded}
595598
$isExpandable
596599
onClick={() => setCollapsedProcessingExpanded((prev) => !prev)}
600+
onKeyDown={(e: React.KeyboardEvent) => {
601+
if (e.key === "Enter" || e.key === " ") {
602+
e.preventDefault()
603+
setCollapsedProcessingExpanded((prev) => !prev)
604+
}
605+
}}
597606
data-hook="assistant-mode-processing-collapsed"
598-
role="presentation"
607+
role="button"
608+
tabIndex={0}
609+
aria-expanded={collapsedProcessingExpanded}
599610
>
600-
<GradientCheckCircleIcon size={20} />
611+
<CheckIcon />
601612
<ModeTitle $isActive={false}>
602613
{durationText ? `Thought for ${durationText}` : "Thought"}
603614
</ModeTitle>

src/components/AIStatusIndicator/index.tsx

Lines changed: 45 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import React, { useState, useMemo, useRef, useEffect } from "react"
2-
import styled, { css } from "styled-components"
3-
import {
4-
CheckboxCircle,
5-
CloseCircle,
6-
Stop as StopFill,
7-
} from "@styled-icons/remix-fill"
2+
import styled, { css, useTheme } from "styled-components"
3+
import { CheckboxCircle, CloseCircle } from "@styled-icons/remix-fill"
84
import { SidebarSimpleIcon, XIcon } from "@phosphor-icons/react"
95
import {
106
useAIStatus,
@@ -18,43 +14,47 @@ import { pinkLinearGradientHorizontal } from "../../theme"
1814
import { getAllModelOptions } from "../../utils/ai"
1915
import { useAIConversation } from "../../providers/AIConversationProvider"
2016
import { Button } from "../../components/Button"
17+
import { AIStopButton } from "../AIStopButton"
2118
import { BrainIcon } from "../SetupAIAssistant/BrainIcon"
2219
import { AssistantModes, buildOperationSections } from "./AssistantModes"
2320
import { CircleNotchSpinner } from "../../scenes/Editor/Monaco/icons"
2421
import { useSelector } from "react-redux"
2522
import { selectors } from "../../store"
2623

27-
const CaretGradient = (props: React.SVGProps<SVGSVGElement>) => (
28-
<svg
29-
xmlns="http://www.w3.org/2000/svg"
30-
width="24"
31-
height="24"
32-
viewBox="0 0 24 24"
33-
fill="none"
34-
{...props}
35-
>
36-
<path
37-
d="M4.5 15L12 7.5L19.5 15"
38-
stroke="url(#paint0_linear_214_5568)"
39-
strokeWidth="1.5"
40-
strokeLinecap="round"
41-
strokeLinejoin="round"
42-
/>
43-
<defs>
44-
<linearGradient
45-
id="paint0_linear_214_5568"
46-
x1="12"
47-
y1="7.5"
48-
x2="12"
49-
y2="15"
50-
gradientUnits="userSpaceOnUse"
51-
>
52-
<stop stopColor="#D14671" />
53-
<stop offset="1" stopColor="#892C6C" />
54-
</linearGradient>
55-
</defs>
56-
</svg>
57-
)
24+
const CaretGradient = (props: React.SVGProps<SVGSVGElement>) => {
25+
const theme = useTheme()
26+
return (
27+
<svg
28+
xmlns="http://www.w3.org/2000/svg"
29+
width="24"
30+
height="24"
31+
viewBox="0 0 24 24"
32+
fill="none"
33+
{...props}
34+
>
35+
<path
36+
d="M4.5 15L12 7.5L19.5 15"
37+
stroke="url(#paint0_linear_214_5568)"
38+
strokeWidth="1.5"
39+
strokeLinecap="round"
40+
strokeLinejoin="round"
41+
/>
42+
<defs>
43+
<linearGradient
44+
id="paint0_linear_214_5568"
45+
x1="12"
46+
y1="7.5"
47+
x2="12"
48+
y2="15"
49+
gradientUnits="userSpaceOnUse"
50+
>
51+
<stop stopColor={theme.color.pink} />
52+
<stop offset="1" stopColor={theme.color.pinkGradientEnd} />
53+
</linearGradient>
54+
</defs>
55+
</svg>
56+
)
57+
}
5858

5959
const Container = styled.div`
6060
position: absolute;
@@ -214,26 +214,6 @@ const WorkingText = styled.div`
214214
text-transform: uppercase;
215215
`
216216

217-
const AIStopButton = styled(Button)`
218-
width: 2.2rem;
219-
height: 2.2rem;
220-
flex-shrink: 0;
221-
border-radius: 100%;
222-
background: #da152832;
223-
border: 0;
224-
display: flex;
225-
align-items: center;
226-
justify-content: center;
227-
padding: 0;
228-
229-
&:hover {
230-
background: ${({ theme }) => theme.color.red} !important;
231-
svg {
232-
color: ${({ theme }) => theme.color.foreground};
233-
}
234-
}
235-
`
236-
237217
const ViewChatButton = styled(Button).attrs({ skin: "transparent" })`
238218
gap: 1rem;
239219
`
@@ -415,17 +395,20 @@ export const AIStatusIndicator: React.FC = () => {
415395
<Header>
416396
<HeaderLeft>
417397
<AISparkle size={24} variant="filled" />
418-
<WorkingText data-hook="ai-status-text">
398+
<WorkingText
399+
role="status"
400+
aria-live="polite"
401+
data-hook="ai-status-text"
402+
>
419403
{isAborted ? "Cancelled" : isCompleted ? "Completed" : "Working..."}
420404
</WorkingText>
421405
{isBlockingAIStatus(status) && (
422406
<AIStopButton
423407
title="Cancel current operation"
408+
ariaLabel="Cancel current AI operation"
424409
onClick={abortOperation}
425-
data-hook="ai-status-stop"
426-
>
427-
<StopFill size="14px" color="#da1e28" />
428-
</AIStopButton>
410+
dataHook="ai-status-stop"
411+
/>
429412
)}
430413
{chatWindowState.activeConversationId && (
431414
<ViewChatButton
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React from "react"
2+
import styled, { useTheme } from "styled-components"
3+
import { Stop as StopFill } from "@styled-icons/remix-fill"
4+
import { color } from "../../utils"
5+
import { Button } from "../Button"
6+
7+
type Props = {
8+
size?: "sm" | "md"
9+
title?: string
10+
ariaLabel?: string
11+
onClick?: () => void
12+
dataHook?: string
13+
className?: string
14+
}
15+
16+
const SIZES = {
17+
sm: "2.2rem",
18+
md: "2.6rem",
19+
} as const
20+
21+
const StyledButton = styled(Button)<{ $size: keyof typeof SIZES }>`
22+
width: ${({ $size }) => SIZES[$size]};
23+
height: ${({ $size }) => SIZES[$size]};
24+
flex-shrink: 0;
25+
border-radius: 100%;
26+
background: ${color("aiStopButtonBg")};
27+
border: 0;
28+
display: flex;
29+
align-items: center;
30+
justify-content: center;
31+
padding: 0;
32+
33+
&:hover {
34+
background: ${({ theme }) => theme.color.red} !important;
35+
svg {
36+
color: ${({ theme }) => theme.color.foreground};
37+
}
38+
}
39+
`
40+
41+
export const AIStopButton: React.FC<Props> = ({
42+
size = "sm",
43+
title,
44+
ariaLabel,
45+
onClick,
46+
dataHook,
47+
className,
48+
}) => {
49+
const theme = useTheme()
50+
return (
51+
<StyledButton
52+
$size={size}
53+
title={title}
54+
aria-label={ariaLabel}
55+
onClick={onClick}
56+
data-hook={dataHook}
57+
className={className}
58+
>
59+
<StopFill size="14px" color={theme.color.aiStopButtonFg} />
60+
</StyledButton>
61+
)
62+
}

0 commit comments

Comments
 (0)