Skip to content
Merged
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 @@ -210,7 +210,7 @@


.markdown-renderer .inline-code {
padding: 0.2em 0.4em;
padding: 0.1em 0.4em;
margin: 0 0.05em;
font-size: 0.9em;
background: rgba(255, 255, 255, 0.05);
Expand All @@ -224,6 +224,8 @@
color: #a8b4c8;
font-weight: 500;
transition: all 0.15s ease;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
}


Expand Down
4 changes: 2 additions & 2 deletions src/web-ui/src/flow_chat/components/ChatInput.scss
Original file line number Diff line number Diff line change
Expand Up @@ -580,8 +580,8 @@
background: color-mix(in srgb, var(--color-text-muted) 14%, transparent);

&--Plan {
background: rgba(6, 182, 212, 0.2);
color: rgba(14, 116, 144, 0.98);
background: rgba(245, 158, 11, 0.15);
color: rgba(180, 110, 0, 0.98);
}

&--debug {
Expand Down
19 changes: 18 additions & 1 deletion src/web-ui/src/flow_chat/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({

const setChatInputActive = useChatInputState(state => state.setActive);
const setChatInputExpanded = useChatInputState(state => state.setExpanded);
const setChatInputHeight = useChatInputState(state => state.setInputHeight);

useEffect(() => {
const unsubscribe = FlowChatStore.getInstance().subscribe(setFlowChatState);
Expand Down Expand Up @@ -1328,10 +1329,13 @@ export const ChatInput: React.FC<ChatInputProps> = ({

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Node;
// Do not collapse when clicking the scroll-to-latest bar.
if ((target as Element)?.closest?.('.scroll-to-latest-bar')) return;
if (
inputState.isActive &&
containerRef.current &&
!containerRef.current.contains(event.target as Node)
!containerRef.current.contains(target)
) {
if (inputState.value.trim() === '') {
dispatchInput({ type: 'DEACTIVATE' });
Expand All @@ -1344,6 +1348,19 @@ export const ChatInput: React.FC<ChatInputProps> = ({
document.removeEventListener('mousedown', handleClickOutside);
};
}, [inputState.isActive, inputState.value]);

useEffect(() => {
const dropZone = containerRef.current?.closest('.bitfun-chat-input-drop-zone') as HTMLElement | null;
const el = dropZone ?? containerRef.current;
if (!el) return;
const observer = new ResizeObserver(() => {
setChatInputHeight(el.offsetHeight);
});
observer.observe(el);
setChatInputHeight(el.offsetHeight);
return () => observer.disconnect();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const renderActionButton = () => {
if (!derivedState) return <IconButton className="bitfun-chat-input__send-button" disabled size="small"><ArrowUp size={11} /></IconButton>;
Expand Down
24 changes: 12 additions & 12 deletions src/web-ui/src/flow_chat/components/PlannerButton.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
border-radius: 11px;

// Default state without glass effect
background: rgba(59, 130, 246, 0.08);
border: 1px solid rgba(59, 130, 246, 0.2);
background: rgba(245, 158, 11, 0.08);
border: 1px solid rgba(245, 158, 11, 0.2);
color: rgba(255, 255, 255, 0.7);
box-shadow: none;
backdrop-filter: none;
Expand Down Expand Up @@ -57,18 +57,18 @@
// Hover: enable glass effect
&:hover {
background: linear-gradient(135deg,
rgba(59, 130, 246, 0.20) 0%,
rgba(96, 165, 250, 0.17) 30%,
rgba(59, 130, 246, 0.13) 60%,
rgba(59, 130, 246, 0.23) 100%
rgba(245, 158, 11, 0.20) 0%,
rgba(251, 191, 36, 0.17) 30%,
rgba(245, 158, 11, 0.13) 60%,
rgba(245, 158, 11, 0.23) 100%
);
border-color: rgba(59, 130, 246, 0.5);
border-color: rgba(245, 158, 11, 0.5);
color: var(--color-text-primary);
box-shadow:
0 10px 20px rgba(59, 130, 246, 0.25),
0 6px 12px rgba(96, 165, 250, 0.2),
0 10px 20px rgba(245, 158, 11, 0.2),
0 6px 12px rgba(251, 191, 36, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.25),
inset 0 -1px 0 rgba(59, 130, 246, 0.15);
inset 0 -1px 0 rgba(245, 158, 11, 0.15);
backdrop-filter: blur(12px) saturate(1.2) brightness(1.05);
-webkit-backdrop-filter: blur(12px) saturate(1.2) brightness(1.05);
transform: translateY(-1px);
Expand All @@ -87,8 +87,8 @@
width: 5px;
height: 5px;
border-radius: 50%;
background: rgba(59, 130, 246, 0.9);
box-shadow: 0 0 6px rgba(59, 130, 246, 0.6);
background: rgba(245, 158, 11, 0.9);
box-shadow: 0 0 6px rgba(245, 158, 11, 0.6);
animation: bitfun-planner-button-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
flex-shrink: 0;
position: relative;
Expand Down
6 changes: 3 additions & 3 deletions src/web-ui/src/flow_chat/components/ScrollToLatestBar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
cursor: pointer;
pointer-events: none;

// Default height: ChatInput active (box ~80px + bottom gap 16px = 96px, add 24px margin)
height: 120px;
// Default height: ChatInput active (box ~80px + bottom gap 16px = 96px, add 49px margin for content visibility)
height: 145px;

transition: height 0.35s cubic-bezier(0.4, 0, 0.2, 1);

Expand Down Expand Up @@ -138,7 +138,7 @@
// ========== Responsive tweaks ==========
@media (max-width: 768px) {
.scroll-to-latest-bar {
height: 110px;
height: 135px;

&--input-collapsed {
height: 70px;
Expand Down
11 changes: 11 additions & 0 deletions src/web-ui/src/flow_chat/components/ScrollToLatestBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ interface ScrollToLatestBarProps {
isInputExpanded?: boolean;
/** Whether ChatInput is active. */
isInputActive?: boolean;
/** Measured height of the ChatInput container in pixels (0 if unknown). */
inputHeight?: number;
className?: string;
}

Expand All @@ -22,6 +24,7 @@ export const ScrollToLatestBar: React.FC<ScrollToLatestBarProps> = ({
onClick,
isInputExpanded = false,
isInputActive = true,
inputHeight = 0,
className = ''
}) => {
const { t } = useTranslation('flow-chat');
Expand All @@ -35,9 +38,17 @@ export const ScrollToLatestBar: React.FC<ScrollToLatestBarProps> = ({
? 'scroll-to-latest-bar--input-expanded'
: '';

// Dynamically offset the bar height based on measured ChatInput height.
// bottom: 16px (drop-zone offset) + inputHeight + 28px (content margin above input)
const dynamicStyle: React.CSSProperties =
isInputActive && !isInputExpanded && inputHeight > 0
? { height: `${inputHeight + 16 + 28}px` }
: {};

return (
<div
className={`scroll-to-latest-bar ${inputStateClass} ${className}`}
style={dynamicStyle}
onClick={onClick}
role="button"
tabIndex={0}
Expand Down
35 changes: 32 additions & 3 deletions src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,13 @@ export const ModelRoundItem = React.memo<ModelRoundItemProps>(
/>
));

case 'critical':
case 'critical': {
// If next group is the matching subagent, skip here — rendered by subagent case.
const nextGroup = groupedItems[groupIndex + 1];
const isTaskForSubagent = group.item.type === 'tool' &&
nextGroup?.type === 'subagent' &&
nextGroup.parentTaskToolId === group.item.id;
if (isTaskForSubagent) return null;
return (
<FlowItemRenderer
key={group.item.id}
Expand All @@ -295,9 +301,16 @@ export const ModelRoundItem = React.memo<ModelRoundItemProps>(
isLastItem={isLast}
/>
);
}

case 'subagent':
return (
case 'subagent': {
// If previous group is the matching task tool, wrap both in a unified card.
const prevGroup = groupedItems[groupIndex - 1];
const hasPairedTask = prevGroup?.type === 'critical' &&
prevGroup.item.type === 'tool' &&
group.parentTaskToolId === prevGroup.item.id;

const subagentContainer = (
<SubagentItemsContainer
key={`subagent-group-${group.parentTaskToolId}-${groupIndex}`}
parentTaskToolId={group.parentTaskToolId}
Expand All @@ -306,6 +319,22 @@ export const ModelRoundItem = React.memo<ModelRoundItemProps>(
roundId={round.id}
/>
);

if (hasPairedTask) {
return (
<div key={`task-with-subagent-${prevGroup.item.id}`} className="task-with-subagent-wrapper">
<FlowItemRenderer
item={prevGroup.item}
turnId={turnId}
roundId={round.id}
isLastItem={false}
/>
{subagentContainer}
</div>
);
}
return subagentContainer;
}

default:
return null;
Expand Down
70 changes: 10 additions & 60 deletions src/web-ui/src/flow_chat/components/modern/SubagentItems.scss
Original file line number Diff line number Diff line change
@@ -1,52 +1,11 @@
/**
* Subagent items styles.
* Controls visibility for subagent text and tool cards.
* Blends with the TaskTool card design.
* When expanded, the subagent container merges with the task tool card header
* to form a single unified card with squared top corners.
*/

// Subagent outer wrapper with relative positioning context.
.subagent-items-wrapper {
position: relative;
margin-top: -6px; // Counter the base-tool-card margin.

// Top fade mask.
&::before {
content: '';
position: absolute;
top: 0;
left: 1px;
right: 1px;
height: 24px;
pointer-events: none;
z-index: 3;
background: linear-gradient(
to bottom,
var(--color-bg-flowchat, #121214) 0%,
color-mix(in srgb, var(--color-bg-flowchat, #121214) 80%, transparent) 40%,
color-mix(in srgb, var(--color-bg-flowchat, #121214) 40%, transparent) 70%,
transparent 100%
);
}

// Bottom fade mask (inside the border).
&::after {
content: '';
position: absolute;
bottom: 1px; // Avoid the bottom border.
left: 1px;
right: 1px;
height: 24px;
pointer-events: none;
z-index: 3;
border-radius: 0 0 7px 7px;
background: linear-gradient(
to top,
var(--color-bg-flowchat, #121214) 0%,
color-mix(in srgb, var(--color-bg-flowchat, #121214) 80%, transparent) 40%,
color-mix(in srgb, var(--color-bg-flowchat, #121214) 40%, transparent) 70%,
transparent 100%
);
}
}

.subagent-items-wrapper--collapsed {
Expand All @@ -59,23 +18,17 @@

// Subagent container for items under the same parent task.
.subagent-items-container {
// Blend with the TaskTool card.
margin-left: 0;
margin-right: 0;
padding: 12px 14px;
// Match BaseToolCard expanded area styling.

// Continue the task card border on left/right/bottom; top border is hidden.
background: var(--color-bg-flowchat);
border: 1px solid var(--border-base);
border-top: none;
// Top corners are square to merge with the header card above.
border-radius: 0 0 8px 8px;
backdrop-filter: blur(8px);

// Top divider.
border-top: none;
box-shadow: inset 0 1px 0 0 var(--border-base, rgba(107, 114, 128, 0.25));

// Fixed height to avoid growth during streaming output.
// Keep height even when content is short; overflow scrolls.

// Fixed height; overflow scrolls.
height: 400px;
overflow-y: auto;
}
Expand All @@ -84,18 +37,15 @@
overflow: hidden;
}

// TaskTool card bottom radius is controlled by TaskToolDisplay.scss.

// Keep the legacy subagent-item class for compatibility.
.subagent-item {
display: block;

&.subagent-item--collapsed {
display: none;
}

&.subagent-item--expanded {
display: block;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@
position: relative;

&:hover {
background: var(--element-bg-strong);
border-color: var(--border-prominent);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);

.user-message-item__copy-btn {
opacity: 1;
}
Expand Down Expand Up @@ -72,9 +68,6 @@
user-select: text;
transition: all 0.2s ease;

&:hover {
opacity: 0.8;
}
}

// Expanded state.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ export const VirtualMessageList = forwardRef<VirtualMessageListRef>((_, ref) =>

const isInputActive = useChatInputState(state => state.isActive);
const isInputExpanded = useChatInputState(state => state.isExpanded);
const inputHeight = useChatInputState(state => state.inputHeight);

const activeSessionState = useActiveSessionState();
const isProcessing = activeSessionState.isProcessing;
Expand Down Expand Up @@ -1918,6 +1919,7 @@ export const VirtualMessageList = forwardRef<VirtualMessageListRef>((_, ref) =>
onClick={scrollToLatestEndPosition}
isInputActive={isInputActive}
isInputExpanded={isInputExpanded}
inputHeight={inputHeight}
/>
</div>
);
Expand Down
5 changes: 5 additions & 0 deletions src/web-ui/src/flow_chat/store/chatInputStateStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,21 @@ interface ChatInputStateStore {
isActive: boolean;
/** Whether ChatInput is expanded (full height mode) */
isExpanded: boolean;
/** Measured height of the ChatInput container in pixels (0 if unknown) */
inputHeight: number;

setActive: (isActive: boolean) => void;
setExpanded: (isExpanded: boolean) => void;
setInputHeight: (height: number) => void;
}

export const useChatInputState = create<ChatInputStateStore>((set) => ({
isActive: true,
isExpanded: false,
inputHeight: 0,

setActive: (isActive) => set({ isActive }),
setExpanded: (isExpanded) => set({ isExpanded }),
setInputHeight: (inputHeight) => set({ inputHeight }),
}));

Loading
Loading