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
1 change: 0 additions & 1 deletion src/crates/core/src/agentic/agents/agentic_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ impl AgenticMode {
"Glob".to_string(),
"WebSearch".to_string(),
"TodoWrite".to_string(),
"MermaidInteractive".to_string(),
"GenerativeUI".to_string(),
"Skill".to_string(),
"AskUserQuestion".to_string(),
Expand Down
1 change: 0 additions & 1 deletion src/crates/core/src/agentic/agents/claw_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ impl ClawMode {
"Grep".to_string(),
"Glob".to_string(),
"WebSearch".to_string(),
"MermaidInteractive".to_string(),
"Skill".to_string(),
"Git".to_string(),
"TerminalControl".to_string(),
Expand Down
1 change: 0 additions & 1 deletion src/crates/core/src/agentic/agents/debug_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,6 @@ impl Agent for DebugMode {
"Glob".to_string(),
"WebSearch".to_string(),
"TodoWrite".to_string(),
"MermaidInteractive".to_string(),
"Log".to_string(),
"TerminalControl".to_string(),
"ControlHub".to_string(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ impl PromptBuilder {
if enabled {
r"# Visualizing complex logic as you explain
Use Mermaid diagrams to visualize complex logic, workflows, architectures, and data flows whenever it helps clarify the explanation.
Prefer MermaidInteractive tool when available, otherwise output Mermaid code blocks directly.
Output Mermaid in fenced code blocks (```mermaid) so the UI can render them.
".to_string()
} else {
String::new()
Expand Down
2 changes: 1 addition & 1 deletion src/crates/core/src/agentic/agents/prompts/debug_mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ MOST IMPORTANT: Always use the exact logfile path: `{LOG_PATH}`
- **Delete**: clear `{LOG_PATH}` before each run
- **Grep / Glob**: locate code, search for patterns
- **Edit / Write**: insert instrumentation code, implement fixes
- **MermaidInteractive**: visualize execution flow
- **Mermaid code blocks**: visualize execution flow when helpful
- **Log**: record findings for the user
- **TodoWrite**: track hypotheses and their status

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl GenerativeUITool {
}

fn bitfun_design_system_reminder() -> &'static str {
"BitFun design-system reminder: when the widget should feel native to the host BitFun app, style it with BitFun theme tokens instead of hard-coded design values. Prefer CSS variables such as `var(--color-bg-primary)`, `var(--color-bg-secondary)`, `var(--color-bg-scene)`, `var(--color-bg-elevated)`, `var(--color-text-primary)`, `var(--color-text-secondary)`, `var(--color-text-muted)`, `var(--color-accent-500)`, `var(--color-accent-600)`, `var(--border-subtle)`, `var(--border-base)`, `var(--border-medium)`, `var(--element-bg-subtle)`, `var(--element-bg-soft)`, `var(--element-bg-base)`, `var(--element-bg-medium)`, `var(--shadow-*)`, `var(--radius-*)`, `var(--spacing-*)`, `var(--motion-*)`, `var(--easing-*)`, `var(--font-sans)`, and `var(--font-mono)`. Support both `bitfun-dark` and `bitfun-light`; do not assume dark-only, purple-only, or landing-page styling. Favor compact desktop workbench layouts, panel/card surfaces, strong information hierarchy, and reusable BitFun component patterns. Avoid hard-coded colors, arbitrary spacing, giant hero sections, fake mobile chrome, and full marketing-page shells."
"BitFun design-system reminder: when the widget should feel native to the host BitFun app, style it with BitFun theme tokens instead of hard-coded design values. Prefer CSS variables such as `var(--color-bg-primary)`, `var(--color-bg-secondary)`, `var(--color-bg-scene)`, `var(--color-bg-elevated)`, `var(--color-text-primary)`, `var(--color-text-secondary)`, `var(--color-text-muted)`, `var(--color-accent-500)`, `var(--color-accent-600)`, `var(--border-subtle)`, `var(--border-base)`, `var(--border-medium)`, `var(--element-bg-subtle)`, `var(--element-bg-soft)`, `var(--element-bg-base)`, `var(--element-bg-medium)`, `var(--shadow-*)`, `var(--radius-*)`, `var(--spacing-*)`, `var(--motion-*)`, `var(--easing-*)`, `var(--font-sans)`, and `var(--font-mono)`. Support both `bitfun-dark` and `bitfun-light`; do not assume dark-only, purple-only, or landing-page styling. Favor compact desktop workbench layouts, panel/card surfaces, strong information hierarchy, and reusable BitFun component patterns. Avoid hard-coded colors, arbitrary spacing, giant hero sections, fake mobile chrome, and full marketing-page shells; prefer understated, premium UI with layered surfaces, restrained contrast, subtle borders, and do not use thick left-accent emphasis blocks."
}

fn bitfun_widget_scaffold_reminder() -> &'static str {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export interface FlowChatContextValue {
* Collapse the specified explore group.
*/
onCollapseGroup?: (groupId: string) => void;

// Message search state
searchQuery?: string;
searchMatchIndices?: ReadonlySet<number>;
searchCurrentMatchVirtualIndex?: number;
}

export const FlowChatContext = createContext<FlowChatContextValue>({});
Expand Down
104 changes: 104 additions & 0 deletions src/web-ui/src/flow_chat/components/modern/FlowChatHeader.scss
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,103 @@
}
}

&__search-btn,
&__search-close {
flex: 0 0 auto;

&:not(:disabled):hover {
background: color-mix(in srgb, var(--element-bg-soft) 82%, transparent);
}
}

&__search {
display: flex;
align-items: center;
gap: $size-gap-1;
min-width: min(320px, 38vw);
max-width: min(420px, 42vw);
}

&__search-field {
flex: 1;
min-width: 0;

&.bitfun-input-wrapper {
gap: 0;
}

.bitfun-input-container {
padding-right: 2px;
}

.bitfun-input-suffix {
display: flex;
align-items: center;
padding: 0;
}
}

&__search-prefix-icon {
flex-shrink: 0;
color: var(--color-text-secondary);
opacity: 0.75;
}

&__search-inline-controls {
display: flex;
align-items: center;
gap: 0;
padding-left: 2px;
margin-left: 2px;
border-left: 1px solid var(--color-border-subtle, rgba(128, 128, 128, 0.2));
}

&__search-count {
flex-shrink: 0;
padding: 0 3px;
color: var(--color-text-secondary);
font-size: var(--flowchat-font-size-xs);
white-space: nowrap;

&:empty {
display: none;
}
}

&__search-nav {
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
border-radius: 3px;
}

&__search-nav-btn {
display: flex;
align-items: center;
justify-content: center;
width: 14px;
height: 10px;
padding: 0;
border: none;
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
opacity: 0.8;
transition: opacity $motion-base $easing-standard, background $motion-base $easing-standard;
line-height: 0;

&:hover:not(:disabled) {
opacity: 1;
background: var(--element-bg-soft, rgba(128, 128, 128, 0.1));
}

&:disabled {
opacity: 0.25;
cursor: default;
}
}

&__turn-list-panel {
position: absolute;
top: calc(100% + 8px);
Expand Down Expand Up @@ -230,5 +327,12 @@
white-space: nowrap;
}
}

@media (max-width: 900px) {
&__search {
min-width: min(220px, 52vw);
max-width: min(300px, 56vw);
}
}
}

160 changes: 157 additions & 3 deletions src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
* Height matches side panel headers (40px).
*/

import React, { useEffect, useMemo, useRef, useState } from 'react';
import { ChevronDown, ChevronUp, CornerUpLeft, List, GitBranch } from 'lucide-react';
import { Tooltip, IconButton } from '@/component-library';
import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
import { ChevronDown, ChevronUp, CornerUpLeft, List, GitBranch, Search, X } from 'lucide-react';
import { Tooltip, IconButton, Input } from '@/component-library';
import { useTranslation } from 'react-i18next';
import { globalEventBus } from '@/infrastructure/event-bus';
import { SessionFilesBadge } from './SessionFilesBadge';
Expand Down Expand Up @@ -46,6 +46,22 @@ export interface FlowChatHeaderProps {
onJumpToPreviousTurn?: () => void;
/** Jump to the next turn. */
onJumpToNextTurn?: () => void;
/** Current search query string. */
searchQuery?: string;
/** Called when the user types in the search box. */
onSearchChange?: (query: string) => void;
/** Total number of search matches. */
searchMatchCount?: number;
/** 1-based index of the currently focused match. */
searchCurrentMatch?: number;
/** Navigate to the next match. */
onSearchNext?: () => void;
/** Navigate to the previous match. */
onSearchPrev?: () => void;
/** Called when the user closes the search bar. */
onSearchClose?: () => void;
/** Increments each time the parent requests to open the search bar. */
searchOpenRequest?: number;
}
export const FlowChatHeader: React.FC<FlowChatHeaderProps> = ({
currentTurn,
Expand All @@ -60,12 +76,22 @@ export const FlowChatHeader: React.FC<FlowChatHeaderProps> = ({
onJumpToTurn,
onJumpToPreviousTurn,
onJumpToNextTurn,
searchQuery = '',
onSearchChange,
searchMatchCount = 0,
searchCurrentMatch = 0,
onSearchNext,
onSearchPrev,
onSearchClose,
searchOpenRequest = 0,
}) => {
const { t } = useTranslation('flow-chat');
const { currentBranch, isRepository } = useGitBasicInfo(workspacePath ?? '');
const [isTurnListOpen, setIsTurnListOpen] = useState(false);
const [isSearchOpen, setIsSearchOpen] = useState(false);
const turnListRef = useRef<HTMLDivElement | null>(null);
const activeTurnItemRef = useRef<HTMLButtonElement | null>(null);
const searchInputRef = useRef<HTMLInputElement | null>(null);

// Truncate long messages.
const truncatedMessage = currentUserMessage.length > 50
Expand Down Expand Up @@ -101,6 +127,7 @@ export const FlowChatHeader: React.FC<FlowChatHeaderProps> = ({
title: turn.title.trim() || untitledTurnLabel,
}))
), [turns, untitledTurnLabel]);
const hasNoResults = searchQuery.trim().length > 0 && searchMatchCount === 0;

useEffect(() => {
if (!isTurnListOpen) return;
Expand All @@ -126,10 +153,31 @@ export const FlowChatHeader: React.FC<FlowChatHeaderProps> = ({
};
}, [isTurnListOpen]);

const prevSearchOpenRequestRef = useRef(0);
useEffect(() => {
if (searchOpenRequest > 0 && searchOpenRequest !== prevSearchOpenRequestRef.current) {
prevSearchOpenRequestRef.current = searchOpenRequest;
setIsSearchOpen(true);
}
}, [searchOpenRequest]);

useEffect(() => {
setIsTurnListOpen(false);
}, [currentTurn]);

useEffect(() => {
if (!isSearchOpen) return;

const frameId = requestAnimationFrame(() => {
searchInputRef.current?.focus();
searchInputRef.current?.select();
});

return () => {
cancelAnimationFrame(frameId);
};
}, [isSearchOpen]);

useEffect(() => {
if (!isTurnListOpen) return;

Expand All @@ -145,6 +193,35 @@ export const FlowChatHeader: React.FC<FlowChatHeaderProps> = ({
};
}, [currentTurn, displayTurns.length, isTurnListOpen]);

const handleOpenSearch = useCallback(() => {
setIsSearchOpen(true);
}, []);

const handleCloseSearch = useCallback(() => {
setIsSearchOpen(false);
onSearchClose?.();
}, [onSearchClose]);

const handleSearchKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Escape') {
handleCloseSearch();
e.preventDefault();
return;
}

if (e.key === 'Enter') {
if (e.shiftKey) {
onSearchPrev?.();
} else {
onSearchNext?.();
}
e.preventDefault();
}
},
[handleCloseSearch, onSearchNext, onSearchPrev],
);

const handleBackToParent = () => {
const parentId = btwOrigin?.parentSessionId;
if (!parentId) return;
Expand Down Expand Up @@ -198,6 +275,83 @@ export const FlowChatHeader: React.FC<FlowChatHeaderProps> = ({
</Tooltip>

<div className="flowchat-header__actions">
{isSearchOpen ? (
<div className="flowchat-header__search" role="search" data-testid="flowchat-header-search-bar">
<Input
ref={searchInputRef}
className="flowchat-header__search-field"
variant="filled"
inputSize="small"
prefix={<Search size={12} className="flowchat-header__search-prefix-icon" aria-hidden="true" />}
suffix={
<span className="flowchat-header__search-inline-controls">
<span className="flowchat-header__search-count" aria-live="polite">
{searchQuery.trim()
? hasNoResults
? t('flowChatHeader.searchNoResults', { defaultValue: 'No results' })
: t('flowChatHeader.searchResult', {
current: searchCurrentMatch,
total: searchMatchCount,
defaultValue: `${searchCurrentMatch} / ${searchMatchCount}`,
})
: null}
</span>
<span className="flowchat-header__search-nav">
<button
className="flowchat-header__search-nav-btn"
onClick={onSearchPrev}
disabled={searchMatchCount === 0}
title={t('flowChatHeader.searchPrevious', { defaultValue: 'Previous match' })}
aria-label={t('flowChatHeader.searchPrevious', { defaultValue: 'Previous match' })}
type="button"
>
<ChevronUp size={10} />
</button>
<button
className="flowchat-header__search-nav-btn"
onClick={onSearchNext}
disabled={searchMatchCount === 0}
title={t('flowChatHeader.searchNext', { defaultValue: 'Next match' })}
aria-label={t('flowChatHeader.searchNext', { defaultValue: 'Next match' })}
type="button"
>
<ChevronDown size={10} />
</button>
</span>
</span>
}
type="text"
value={searchQuery}
onChange={e => onSearchChange?.(e.target.value)}
onKeyDown={handleSearchKeyDown}
placeholder={t('flowChatHeader.searchPlaceholder', { defaultValue: 'Search messages' })}
aria-label={t('flowChatHeader.searchPlaceholder', { defaultValue: 'Search messages' })}
error={hasNoResults}
/>
<IconButton
className="flowchat-header__search-close"
variant="ghost"
size="xs"
onClick={handleCloseSearch}
tooltip={t('flowChatHeader.searchClose', { defaultValue: 'Close search' })}
aria-label={t('flowChatHeader.searchClose', { defaultValue: 'Close search' })}
>
<X size={14} />
</IconButton>
</div>
) : (
<IconButton
className="flowchat-header__search-btn"
variant="ghost"
size="xs"
onClick={handleOpenSearch}
tooltip={t('flowChatHeader.searchOpen', { defaultValue: 'Search messages' })}
aria-label={t('flowChatHeader.searchOpen', { defaultValue: 'Search messages' })}
data-testid="flowchat-header-search"
>
<Search size={14} />
</IconButton>
)}
<div className="flowchat-header__turn-nav" ref={turnListRef}>
<IconButton
className={`flowchat-header__turn-nav-button${isTurnListOpen ? ' flowchat-header__turn-nav-button--active' : ''}`}
Expand Down
Loading
Loading