Skip to content

Commit 6caa4e7

Browse files
committed
Improve ACP search and release anycode-base 1.0.7
1 parent d54d399 commit 6caa4e7

6 files changed

Lines changed: 93 additions & 43 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ promo_ru.md
2121
.cargo-home/
2222
.cargo-home-sparse/
2323
.pnpm-store/v11/index.db
24+
anycode-videos/

anycode-base/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "anycode-base",
3-
"version": "1.0.6",
3+
"version": "1.0.7",
44
"main": "src/index.ts",
55
"description": "Anycode Editor component",
66
"keywords": [

anycode-base/package_npm.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "anycode-base",
3-
"version": "1.0.6",
3+
"version": "1.0.7",
44
"description": "Anycode Editor component",
55
"type": "module",
66
"main": "dist/index.js",
@@ -33,7 +33,7 @@
3333
"url": ""
3434
},
3535
"dependencies": {
36-
"diff": "^8.0.2",
36+
"diff": "^9.0.0",
3737
"vscode-textbuffer": "^1.0.0",
3838
"web-tree-sitter": "^0.26.9"
3939
},

anycode/components/agent/AcpMessages.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface AcpMessagesProps {
1717
expandedToolResults: Set<number>;
1818
expandedThoughts: Set<number>;
1919
activeSearchMessageIndex?: number;
20+
onWorkGroupExpansionChange?: () => void;
2021
onToggleToolCall: (index: number) => void;
2122
onToggleToolResult: (index: number) => void;
2223
onToggleThought: (index: number) => void;
@@ -32,6 +33,7 @@ const AcpMessagesComponent: React.FC<AcpMessagesProps> = ({
3233
expandedToolResults,
3334
expandedThoughts,
3435
activeSearchMessageIndex,
36+
onWorkGroupExpansionChange,
3537
onToggleToolCall,
3638
onToggleToolResult,
3739
onToggleThought,
@@ -212,6 +214,7 @@ const AcpMessagesComponent: React.FC<AcpMessagesProps> = ({
212214
messageCount={item.messages.length}
213215
searchActive={activeSearchMessageIndex !== undefined}
214216
isSearchMatch={item.messages.some(({ index }) => index === activeSearchMessageIndex)}
217+
onExpansionChange={onWorkGroupExpansionChange}
215218
>
216219
{item.messages.map((m) => renderMessage(m.message, m.index))}
217220
</AcpWorkGroup>

anycode/components/agent/AcpSession.tsx

Lines changed: 68 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,17 @@ const ACP_INPUT_DRAFTS_STORAGE_KEY = 'acpInputDrafts';
1818
const EMPTY_ARRAY: any[] = [];
1919

2020
const findTextMatches = (messages: AcpMessage[], query: string) => {
21-
const normalizedQuery = query.toLocaleLowerCase();
21+
const normalizedQuery = query.toLowerCase();
2222
if (!normalizedQuery) return [];
2323

2424
return messages.flatMap((message, index) => {
2525
if (message.role !== 'user' && message.role !== 'assistant' && message.role !== 'thought') return [];
26-
const count = message.content.toLocaleLowerCase().split(normalizedQuery).length - 1;
26+
let count = 0;
27+
let offset = message.content.toLowerCase().indexOf(normalizedQuery);
28+
while (offset >= 0) {
29+
count += 1;
30+
offset = message.content.toLowerCase().indexOf(normalizedQuery, offset + normalizedQuery.length);
31+
}
2732
return Array.from({ length: count }, (_, occurrence) => ({ messageIndex: index, occurrence }));
2833
});
2934
};
@@ -276,6 +281,7 @@ const AcpSessionComponent: React.FC<AcpSessionProps> = ({
276281
const [searchOpen, setSearchOpen] = useState(false);
277282
const [searchQuery, setSearchQuery] = useState('');
278283
const [currentSearchMatch, setCurrentSearchMatch] = useState(0);
284+
const [searchRenderVersion, setSearchRenderVersion] = useState(0);
279285
const [inputValues, setInputValues] = useState<Record<string, string>>(() => {
280286
const savedDrafts = loadItem<Record<string, unknown>>(ACP_INPUT_DRAFTS_STORAGE_KEY);
281287
if (!savedDrafts || typeof savedDrafts !== 'object') {
@@ -302,6 +308,9 @@ const AcpSessionComponent: React.FC<AcpSessionProps> = ({
302308
);
303309
const activeSearchMessageIndex = searchMatches[currentSearchMatch]?.messageIndex;
304310
const activeOccurrence = searchMatches[currentSearchMatch]?.occurrence ?? 0;
311+
const handleWorkGroupExpansionChange = useCallback(() => {
312+
setSearchRenderVersion((version) => version + 1);
313+
}, []);
305314

306315
useEffect(() => {
307316
if (searchMatches.length === 0 || currentSearchMatch < searchMatches.length) return;
@@ -369,37 +378,70 @@ const AcpSessionComponent: React.FC<AcpSessionProps> = ({
369378
}, [closeSearch, openSearch, searchOpen]);
370379

371380
useEffect(() => {
372-
if (activeSearchMessageIndex === undefined) return;
381+
const clearHighlights = () => {
382+
CSS.highlights.delete('acp-search-match');
383+
CSS.highlights.delete('acp-search-current');
384+
};
385+
386+
if (activeSearchMessageIndex === undefined || !searchQuery) {
387+
clearHighlights();
388+
return;
389+
}
390+
373391
const frame = requestAnimationFrame(() => {
374-
const target = innerRef.current?.querySelector<HTMLElement>(
375-
`[data-message-index="${activeSearchMessageIndex}"]`,
376-
);
377-
if (!target) return;
392+
const messageTargets = innerRef.current
393+
? Array.from(innerRef.current.querySelectorAll<HTMLElement>('[data-message-index]'))
394+
: [];
395+
if (messageTargets.length === 0) {
396+
clearHighlights();
397+
return;
398+
}
378399

379-
const query = searchQuery.toLocaleLowerCase();
400+
const query = searchQuery.toLowerCase();
380401
const ranges: Range[] = [];
381-
const walker = document.createTreeWalker(target, NodeFilter.SHOW_TEXT);
382-
let node = walker.nextNode();
383-
while (node) {
384-
const text = node.textContent?.toLocaleLowerCase() ?? '';
385-
let offset = text.indexOf(query);
386-
while (offset >= 0) {
387-
const range = document.createRange();
388-
range.setStart(node, offset);
389-
range.setEnd(node, offset + searchQuery.length);
390-
ranges.push(range);
391-
offset = text.indexOf(query, offset + query.length);
402+
let currentRange: Range | undefined;
403+
404+
for (const target of messageTargets) {
405+
const messageIndex = Number(target.dataset.messageIndex);
406+
const targetRanges: Range[] = [];
407+
const walker = document.createTreeWalker(target, NodeFilter.SHOW_TEXT);
408+
let node = walker.nextNode();
409+
while (node) {
410+
// Do not search text that is present in the DOM but is not visible.
411+
// ACP messages contain collapsed/auxiliary controls whose text can
412+
// otherwise produce a misleading highlight.
413+
const parent = node.parentElement;
414+
const isVisible = !!parent && parent.getClientRects().length > 0;
415+
const text = isVisible ? node.textContent?.toLowerCase() ?? '' : '';
416+
let offset = text.indexOf(query);
417+
while (offset >= 0) {
418+
const range = document.createRange();
419+
range.setStart(node, offset);
420+
range.setEnd(node, offset + query.length);
421+
targetRanges.push(range);
422+
offset = text.indexOf(query, offset + query.length);
423+
}
424+
node = walker.nextNode();
392425
}
393-
node = walker.nextNode();
426+
427+
if (messageIndex === activeSearchMessageIndex) {
428+
currentRange = targetRanges[activeOccurrence];
429+
}
430+
ranges.push(...targetRanges);
394431
}
432+
clearHighlights();
395433
CSS.highlights.set('acp-search-match', new Highlight(...ranges));
396-
if (ranges[activeOccurrence]) {
397-
CSS.highlights.set('acp-search-current', new Highlight(ranges[activeOccurrence]));
434+
if (currentRange) {
435+
CSS.highlights.set('acp-search-current', new Highlight(currentRange));
398436
}
399437

400438
const scroller = contentRef.current;
401439
if (scroller) {
402-
const matchRect = (ranges[activeOccurrence] ?? target).getBoundingClientRect();
440+
const activeTarget = messageTargets.find(
441+
(target) => Number(target.dataset.messageIndex) === activeSearchMessageIndex,
442+
);
443+
const matchRect = (currentRange ?? activeTarget)?.getBoundingClientRect();
444+
if (!matchRect) return;
403445
const scrollerRect = scroller.getBoundingClientRect();
404446
scroller.scrollTo({
405447
top: scroller.scrollTop + matchRect.top - scrollerRect.top
@@ -411,10 +453,9 @@ const AcpSessionComponent: React.FC<AcpSessionProps> = ({
411453

412454
return () => {
413455
cancelAnimationFrame(frame);
414-
CSS.highlights.delete('acp-search-match');
415-
CSS.highlights.delete('acp-search-current');
456+
clearHighlights();
416457
};
417-
}, [activeOccurrence, activeSearchMessageIndex, contentRef, innerRef, searchQuery]);
458+
}, [activeOccurrence, activeSearchMessageIndex, contentRef, innerRef, searchQuery, searchRenderVersion]);
418459

419460
const handleUndoMessage = useCallback(
420461
(message: AcpMessage) => {
@@ -556,6 +597,7 @@ const AcpSessionComponent: React.FC<AcpSessionProps> = ({
556597
expandedToolResults={expandedToolResults}
557598
expandedThoughts={searchExpandedThoughts}
558599
activeSearchMessageIndex={activeSearchMessageIndex}
600+
onWorkGroupExpansionChange={handleWorkGroupExpansionChange}
559601
onToggleToolCall={toggleToolCall}
560602
onToggleToolResult={toggleToolResult}
561603
onToggleThought={toggleThought}

anycode/components/agent/AcpWorkGroup.tsx

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ interface AcpWorkGroupProps {
77
messageCount: number;
88
searchActive?: boolean;
99
isSearchMatch?: boolean;
10+
onExpansionChange?: () => void;
1011
children: React.ReactNode;
1112
}
1213

@@ -15,23 +16,21 @@ export const AcpWorkGroup: React.FC<AcpWorkGroupProps> = ({
1516
messageCount,
1617
searchActive = false,
1718
isSearchMatch = false,
19+
onExpansionChange,
1820
children,
1921
}) => {
2022
const [isExpanded, setIsExpanded] = useState(isLatest);
2123

2224
useEffect(() => {
23-
setIsExpanded(isLatest);
24-
}, [isLatest]);
25-
26-
if (isSearchMatch) {
27-
return (
28-
<div className="acp-work-group expanded acp-work-group-search-match" data-search-expanded="true">
29-
<div className="acp-work-group-content">
30-
{children}
31-
</div>
32-
</div>
33-
);
34-
}
25+
// Search should reveal the matching group, but must not replace the
26+
// group's own state: the header remains interactive while searching.
27+
if (isLatest || isSearchMatch) {
28+
setIsExpanded(true);
29+
if (isSearchMatch) {
30+
onExpansionChange?.();
31+
}
32+
}
33+
}, [isLatest, isSearchMatch, onExpansionChange]);
3534

3635
if (isLatest && !searchActive) {
3736
return (
@@ -45,7 +44,12 @@ export const AcpWorkGroup: React.FC<AcpWorkGroupProps> = ({
4544
<div className={`acp-work-group ${isExpanded ? 'expanded' : 'collapsed'}`}>
4645
<div
4746
className="acp-work-group-header"
48-
onClick={() => setIsExpanded(!isExpanded)}
47+
onClick={() => {
48+
setIsExpanded(!isExpanded);
49+
if (searchActive) {
50+
onExpansionChange?.();
51+
}
52+
}}
4953
>
5054
<span className="acp-work-group-icon">
5155
<AcpIcons.ChevronRight />
@@ -54,7 +58,7 @@ export const AcpWorkGroup: React.FC<AcpWorkGroupProps> = ({
5458
worked ({messageCount} step{messageCount !== 1 ? 's' : ''})
5559
</span>
5660
</div>
57-
{isExpanded && !searchActive && (
61+
{isExpanded && (
5862
<div className="acp-work-group-content">
5963
{children}
6064
</div>

0 commit comments

Comments
 (0)