Skip to content

Commit d0ce4d2

Browse files
authored
Update agent framework structure (#43)
- New Features - Introduced rich AI Chat UI: message list, input bar, model selector, tool call/result cards, agent session header, and live session timeline with nesting. - Added structured response viewing with auto-open and “View Full Report.” - OAuth connect panel and version update banner. - Improvements - Real-time agent progress events (session/tool start, completion, child sessions). - Provider-aware, vision-capable message handling and tool executions. - Enhanced Markdown rendering (TOC, CSS blocks). - Centralized scroll-to-bottom behavior; wider input and better autosizing. - Bug Fixes - Normalized tool call data and content for Groq models. - Input clears after send (Enter or button).
1 parent c5b19b2 commit d0ce4d2

File tree

71 files changed

+5925
-2396
lines changed

Some content is hidden

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

71 files changed

+5925
-2396
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,11 @@ grd_files_bundled_sources = [
617617
"front_end/panels/ai_assistance/ai_assistance.js",
618618
"front_end/panels/ai_chat/ui/AIChatPanel.js",
619619
"front_end/panels/ai_chat/ui/ChatView.js",
620+
"front_end/panels/ai_chat/ui/LiveAgentSessionComponent.js",
621+
"front_end/panels/ai_chat/ui/ToolCallComponent.js",
622+
"front_end/panels/ai_chat/ui/ToolResultComponent.js",
623+
"front_end/panels/ai_chat/ui/AgentSessionHeaderComponent.js",
624+
"front_end/panels/ai_chat/ui/ToolDescriptionFormatter.js",
620625
"front_end/panels/ai_chat/ui/chatView.css.js",
621626
"front_end/panels/ai_chat/ui/HelpDialog.js",
622627
"front_end/panels/ai_chat/ui/PromptEditDialog.js",
@@ -648,7 +653,9 @@ grd_files_bundled_sources = [
648653
"front_end/panels/ai_chat/LLM/GroqProvider.js",
649654
"front_end/panels/ai_chat/LLM/OpenRouterProvider.js",
650655
"front_end/panels/ai_chat/LLM/LLMClient.js",
656+
"front_end/panels/ai_chat/LLM/MessageSanitizer.js",
651657
"front_end/panels/ai_chat/tools/Tools.js",
658+
"front_end/panels/ai_chat/tools/SequentialThinkingTool.js",
652659
"front_end/panels/ai_chat/tools/CombinedExtractionTool.js",
653660
"front_end/panels/ai_chat/tools/CritiqueTool.js",
654661
"front_end/panels/ai_chat/tools/FetcherTool.js",
@@ -661,12 +668,27 @@ grd_files_bundled_sources = [
661668
"front_end/panels/ai_chat/tools/VectorDBClient.js",
662669
"front_end/panels/ai_chat/tools/BookmarkStoreTool.js",
663670
"front_end/panels/ai_chat/tools/DocumentSearchTool.js",
664-
"front_end/panels/ai_chat/tools/SequentialThinkingTool.js",
665671
"front_end/panels/ai_chat/tools/ThinkingTool.js",
666672
"front_end/panels/ai_chat/common/utils.js",
667673
"front_end/panels/ai_chat/common/log.js",
668674
"front_end/panels/ai_chat/common/context.js",
669675
"front_end/panels/ai_chat/common/page.js",
676+
"front_end/panels/ai_chat/core/structured_response.js",
677+
"front_end/panels/ai_chat/models/ChatTypes.js",
678+
"front_end/panels/ai_chat/ui/input/ChatInput.js",
679+
"front_end/panels/ai_chat/ui/input/InputBar.js",
680+
"front_end/panels/ai_chat/ui/markdown/MarkdownRenderers.js",
681+
"front_end/panels/ai_chat/ui/message/MessageList.js",
682+
"front_end/panels/ai_chat/ui/message/ModelMessage.js",
683+
"front_end/panels/ai_chat/ui/message/MessageCombiner.js",
684+
"front_end/panels/ai_chat/ui/message/StructuredResponseRender.js",
685+
"front_end/panels/ai_chat/ui/message/StructuredResponseController.js",
686+
"front_end/panels/ai_chat/ui/message/GlobalActionsRow.js",
687+
"front_end/panels/ai_chat/ui/message/ToolResultMessage.js",
688+
"front_end/panels/ai_chat/ui/message/UserMessage.js",
689+
"front_end/panels/ai_chat/ui/model_selector/ModelSelector.js",
690+
"front_end/panels/ai_chat/ui/oauth/OAuthConnectPanel.js",
691+
"front_end/panels/ai_chat/ui/version/VersionBanner.js",
670692
"front_end/panels/ai_chat/common/WebSocketRPCClient.js",
671693
"front_end/panels/ai_chat/common/EvaluationConfig.js",
672694
"front_end/panels/ai_chat/evaluation/remote/EvaluationProtocol.js",
@@ -680,6 +702,7 @@ grd_files_bundled_sources = [
680702
"front_end/panels/ai_chat/ai_chat.js",
681703
"front_end/panels/ai_chat/ai_chat_impl.js",
682704
"front_end/panels/ai_chat/agent_framework/AgentRunner.js",
705+
"front_end/panels/ai_chat/agent_framework/AgentRunnerEventBus.js",
683706
"front_end/panels/ai_chat/agent_framework/AgentSessionTypes.js",
684707
"front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.js",
685708
"front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.js",

front_end/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ group("front_end") {
2929
"entrypoints/shell",
3030
"entrypoints/wasmparser_worker:worker_entrypoint",
3131
"entrypoints/worker_app:entrypoint",
32+
"panels/ai_chat:ai_chat_release_js_metadata",
33+
"panels/ai_chat:ai_chat_release_css_metadata",
3234
"third_party/vscode.web-custom-data:web_custom_data",
3335
]
3436
}

front_end/panels/ai_assistance/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ devtools_module("ai_assistance") {
2424
"PatchWidget.ts",
2525
"SelectWorkspaceDialog.ts",
2626
"components/ChatView.ts",
27+
"components/ScrollPinHelper.ts",
2728
"components/ExploreWidget.ts",
2829
"components/MarkdownRendererWithCodeBlock.ts",
2930
"components/UserActionRow.ts",

front_end/panels/ai_assistance/components/ChatView.ts

Lines changed: 64 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import * as Marked from '../../../third_party/marked/marked.js';
1515
import * as Buttons from '../../../ui/components/buttons/buttons.js';
1616
import type * as MarkdownView from '../../../ui/components/markdown_view/markdown_view.js';
1717
import * as UI from '../../../ui/legacy/legacy.js';
18+
import { ScrollPinHelper } from './ScrollPinHelper.js';
1819
import * as Lit from '../../../ui/lit/lit.js';
1920
import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';
2021
import {PatchWidget} from '../PatchWidget.js';
@@ -304,29 +305,13 @@ export interface Props {
304305
export class ChatView extends HTMLElement {
305306
readonly #shadow = this.attachShadow({mode: 'open'});
306307
#markdownRenderer = new MarkdownRendererWithCodeBlock();
307-
#scrollTop?: number;
308+
// Scroll management helper replaces ad-hoc state/logic
309+
#scrollHelper = new ScrollPinHelper();
308310
#props: Props;
309311
#messagesContainerElement?: Element;
310312
#mainElementRef?: Lit.Directives.Ref<Element> = Lit.Directives.createRef();
311313
#messagesContainerResizeObserver = new ResizeObserver(() => this.#handleMessagesContainerResize());
312314
#popoverHelper: UI.PopoverHelper.PopoverHelper|null = null;
313-
/**
314-
* Indicates whether the chat scroll position should be pinned to the bottom.
315-
*
316-
* This is true when:
317-
* - The scroll is at the very bottom, allowing new messages to push the scroll down automatically.
318-
* - The panel is initially rendered and the user hasn't scrolled yet.
319-
*
320-
* It is set to false when the user scrolls up to view previous messages.
321-
*/
322-
#pinScrollToBottom = true;
323-
/**
324-
* Indicates whether the scroll event originated from code
325-
* or a user action. When set to `true`, `handleScroll` will ignore the event,
326-
* allowing it to only handle user-driven scrolls and correctly decide
327-
* whether to pin the content to the bottom.
328-
*/
329-
#isProgrammaticScroll = false;
330315

331316
constructor(props: Props) {
332317
super();
@@ -351,16 +336,21 @@ export class ChatView extends HTMLElement {
351336
this.#messagesContainerResizeObserver.disconnect();
352337
}
353338

339+
// Centralize access to the textarea to avoid repeated querySelector casts
340+
#getTextArea(): HTMLTextAreaElement|null {
341+
return this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement | null;
342+
}
343+
354344
clearTextInput(): void {
355-
const textArea = this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement;
345+
const textArea = this.#getTextArea();
356346
if (!textArea) {
357347
return;
358348
}
359349
textArea.value = '';
360350
}
361351

362352
focusTextInput(): void {
363-
const textArea = this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement;
353+
const textArea = this.#getTextArea();
364354
if (!textArea) {
365355
return;
366356
}
@@ -369,23 +359,18 @@ export class ChatView extends HTMLElement {
369359
}
370360

371361
restoreScrollPosition(): void {
372-
if (this.#scrollTop === undefined) {
373-
return;
374-
}
375-
376-
if (!this.#mainElementRef?.value) {
377-
return;
362+
// Ensure helper has latest element
363+
if (this.#mainElementRef?.value) {
364+
this.#scrollHelper.setElement(this.#mainElementRef.value as HTMLElement);
378365
}
379-
380-
this.#setMainElementScrollTop(this.#scrollTop);
366+
this.#scrollHelper.restoreLastPosition();
381367
}
382368

383369
scrollToBottom(): void {
384-
if (!this.#mainElementRef?.value) {
385-
return;
370+
if (this.#mainElementRef?.value) {
371+
this.#scrollHelper.setElement(this.#mainElementRef.value as HTMLElement);
386372
}
387-
388-
this.#setMainElementScrollTop(this.#mainElementRef.value.scrollHeight);
373+
this.#scrollHelper.scrollToBottom();
389374
}
390375

391376
#handleChatUiRef(el: Element|undefined): void {
@@ -446,31 +431,16 @@ export class ChatView extends HTMLElement {
446431
}
447432

448433
#handleMessagesContainerResize(): void {
449-
if (!this.#pinScrollToBottom) {
450-
return;
451-
}
452-
453-
if (!this.#mainElementRef?.value) {
454-
return;
455-
}
456-
457-
if (this.#pinScrollToBottom) {
458-
this.#setMainElementScrollTop(this.#mainElementRef.value.scrollHeight);
434+
if (this.#mainElementRef?.value) {
435+
this.#scrollHelper.setElement(this.#mainElementRef.value as HTMLElement);
459436
}
437+
this.#scrollHelper.handleResize();
460438
}
461439

462-
#setMainElementScrollTop(scrollTop: number): void {
463-
if (!this.#mainElementRef?.value) {
464-
return;
465-
}
466-
467-
this.#scrollTop = scrollTop;
468-
this.#isProgrammaticScroll = true;
469-
this.#mainElementRef.value.scrollTop = scrollTop;
470-
}
440+
// Removed ad-hoc scroll setter in favor of ScrollPinHelper
471441

472442
#setInputText(text: string): void {
473-
const textArea = this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement;
443+
const textArea = this.#getTextArea();
474444
if (!textArea) {
475445
return;
476446
}
@@ -485,7 +455,6 @@ export class ChatView extends HTMLElement {
485455
if (el) {
486456
this.#messagesContainerResizeObserver.observe(el);
487457
} else {
488-
this.#pinScrollToBottom = true;
489458
this.#messagesContainerResizeObserver.disconnect();
490459
}
491460
}
@@ -494,18 +463,10 @@ export class ChatView extends HTMLElement {
494463
if (!ev.target || !(ev.target instanceof HTMLElement)) {
495464
return;
496465
}
497-
498-
// Do not handle scroll events caused by programmatically
499-
// updating the scroll position. We want to know whether user
500-
// did scroll the container from the user interface.
501-
if (this.#isProgrammaticScroll) {
502-
this.#isProgrammaticScroll = false;
503-
return;
466+
if (this.#mainElementRef?.value) {
467+
this.#scrollHelper.setElement(this.#mainElementRef.value as HTMLElement);
504468
}
505-
506-
this.#scrollTop = ev.target.scrollTop;
507-
this.#pinScrollToBottom =
508-
ev.target.scrollTop + ev.target.clientHeight + SCROLL_ROUNDING_OFFSET > ev.target.scrollHeight;
469+
this.#scrollHelper.handleScroll(ev.target);
509470
};
510471

511472
#handleSubmit = (ev: SubmitEvent): void => {
@@ -514,7 +475,7 @@ export class ChatView extends HTMLElement {
514475
return;
515476
}
516477

517-
const textArea = this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement;
478+
const textArea = this.#getTextArea();
518479
if (!textArea?.value) {
519480
return;
520481
}
@@ -569,42 +530,44 @@ export class ChatView extends HTMLElement {
569530
Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistanceDynamicSuggestionClicked);
570531
};
571532

533+
#renderFooter(): Lit.LitTemplate {
534+
const classes = Lit.Directives.classMap({
535+
'chat-view-footer': true,
536+
'has-conversation': !!this.#props.conversationType,
537+
'is-read-only': this.#props.isReadOnly,
538+
});
539+
540+
// clang-format off
541+
const footerContents = this.#props.conversationType
542+
? renderRelevantDataDisclaimer({
543+
isLoading: this.#props.isLoading,
544+
blockedByCrossOrigin: this.#props.blockedByCrossOrigin,
545+
})
546+
: html`<p>
547+
${lockedString(UIStringsNotTranslate.inputDisclaimerForEmptyState)}
548+
<button
549+
class="link"
550+
role="link"
551+
jslog=${VisualLogging.link('open-ai-settings').track({
552+
click: true,
553+
})}
554+
@click=${() => {
555+
void UI.ViewManager.ViewManager.instance().showView(
556+
'chrome-ai',
557+
);
558+
}}
559+
>${i18nString(UIStrings.learnAbout)}</button>
560+
</p>`;
561+
562+
return html`
563+
<footer class=${classes} jslog=${VisualLogging.section('footer')}>
564+
${footerContents}
565+
</footer>
566+
`;
567+
// clang-format on
568+
}
569+
572570
#render(): void {
573-
const renderFooter = (): Lit.LitTemplate => {
574-
const classes = Lit.Directives.classMap({
575-
'chat-view-footer': true,
576-
'has-conversation': !!this.#props.conversationType,
577-
'is-read-only': this.#props.isReadOnly,
578-
});
579-
580-
// clang-format off
581-
const footerContents = this.#props.conversationType
582-
? renderRelevantDataDisclaimer({
583-
isLoading: this.#props.isLoading,
584-
blockedByCrossOrigin: this.#props.blockedByCrossOrigin,
585-
})
586-
: html`<p>
587-
${lockedString(UIStringsNotTranslate.inputDisclaimerForEmptyState)}
588-
<button
589-
class="link"
590-
role="link"
591-
jslog=${VisualLogging.link('open-ai-settings').track({
592-
click: true,
593-
})}
594-
@click=${() => {
595-
void UI.ViewManager.ViewManager.instance().showView(
596-
'chrome-ai',
597-
);
598-
}}
599-
>${i18nString(UIStrings.learnAbout)}</button>
600-
</p>`;
601-
602-
return html`
603-
<footer class=${classes} jslog=${VisualLogging.section('footer')}>
604-
${footerContents}
605-
</footer>
606-
`;
607-
};
608571
// clang-format off
609572
Lit.render(html`
610573
<style>${chatViewStyles}</style>
@@ -661,7 +624,7 @@ export class ChatView extends HTMLElement {
661624
})
662625
}
663626
</main>
664-
${renderFooter()}
627+
${this.#renderFooter()}
665628
</div>
666629
`, this.#shadow, {host: this});
667630
// clang-format on
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
/**
6+
* Utility to manage scroll pin-to-bottom behavior for a scrollable container.
7+
*/
8+
export class ScrollPinHelper {
9+
#el: HTMLElement | null = null;
10+
#scrollTop?: number;
11+
#pinToBottom = true;
12+
#isProgrammatic = false;
13+
static readonly ROUNDING_OFFSET = 1;
14+
15+
setElement(el: HTMLElement | undefined): void {
16+
if (el) {
17+
this.#el = el;
18+
} else {
19+
this.#el = null;
20+
this.#pinToBottom = true;
21+
}
22+
}
23+
24+
handleResize(): void {
25+
if (!this.#el) return;
26+
if (this.#pinToBottom) {
27+
this.setScrollTop(this.#el.scrollHeight);
28+
}
29+
}
30+
31+
handleScroll(target: HTMLElement): void {
32+
if (this.#isProgrammatic) {
33+
this.#isProgrammatic = false;
34+
return;
35+
}
36+
this.#scrollTop = target.scrollTop;
37+
this.#pinToBottom = target.scrollTop + target.clientHeight + ScrollPinHelper.ROUNDING_OFFSET > target.scrollHeight;
38+
}
39+
40+
setScrollTop(value: number): void {
41+
if (!this.#el) return;
42+
this.#scrollTop = value;
43+
this.#isProgrammatic = true;
44+
this.#el.scrollTop = value;
45+
}
46+
47+
scrollToBottom(): void {
48+
if (!this.#el) return;
49+
this.setScrollTop(this.#el.scrollHeight);
50+
}
51+
52+
restoreLastPosition(): void {
53+
if (this.#scrollTop === undefined) {
54+
return;
55+
}
56+
this.setScrollTop(this.#scrollTop);
57+
}
58+
}
59+

0 commit comments

Comments
 (0)