Skip to content

Commit be45174

Browse files
committed
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 3c5bb91 commit be45174

File tree

71 files changed

+5978
-2392
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

+5978
-2392
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,11 @@ grd_files_bundled_sources = [
630630
"front_end/panels/ai_assistance/ai_assistance.js",
631631
"front_end/panels/ai_chat/ui/AIChatPanel.js",
632632
"front_end/panels/ai_chat/ui/ChatView.js",
633+
"front_end/panels/ai_chat/ui/LiveAgentSessionComponent.js",
634+
"front_end/panels/ai_chat/ui/ToolCallComponent.js",
635+
"front_end/panels/ai_chat/ui/ToolResultComponent.js",
636+
"front_end/panels/ai_chat/ui/AgentSessionHeaderComponent.js",
637+
"front_end/panels/ai_chat/ui/ToolDescriptionFormatter.js",
633638
"front_end/panels/ai_chat/ui/chatView.css.js",
634639
"front_end/panels/ai_chat/ui/HelpDialog.js",
635640
"front_end/panels/ai_chat/ui/PromptEditDialog.js",
@@ -661,7 +666,9 @@ grd_files_bundled_sources = [
661666
"front_end/panels/ai_chat/LLM/GroqProvider.js",
662667
"front_end/panels/ai_chat/LLM/OpenRouterProvider.js",
663668
"front_end/panels/ai_chat/LLM/LLMClient.js",
669+
"front_end/panels/ai_chat/LLM/MessageSanitizer.js",
664670
"front_end/panels/ai_chat/tools/Tools.js",
671+
"front_end/panels/ai_chat/tools/SequentialThinkingTool.js",
665672
"front_end/panels/ai_chat/tools/CombinedExtractionTool.js",
666673
"front_end/panels/ai_chat/tools/CritiqueTool.js",
667674
"front_end/panels/ai_chat/tools/FetcherTool.js",
@@ -674,12 +681,27 @@ grd_files_bundled_sources = [
674681
"front_end/panels/ai_chat/tools/VectorDBClient.js",
675682
"front_end/panels/ai_chat/tools/BookmarkStoreTool.js",
676683
"front_end/panels/ai_chat/tools/DocumentSearchTool.js",
677-
"front_end/panels/ai_chat/tools/SequentialThinkingTool.js",
678684
"front_end/panels/ai_chat/tools/ThinkingTool.js",
679685
"front_end/panels/ai_chat/common/utils.js",
680686
"front_end/panels/ai_chat/common/log.js",
681687
"front_end/panels/ai_chat/common/context.js",
682688
"front_end/panels/ai_chat/common/page.js",
689+
"front_end/panels/ai_chat/core/structured_response.js",
690+
"front_end/panels/ai_chat/models/ChatTypes.js",
691+
"front_end/panels/ai_chat/ui/input/ChatInput.js",
692+
"front_end/panels/ai_chat/ui/input/InputBar.js",
693+
"front_end/panels/ai_chat/ui/markdown/MarkdownRenderers.js",
694+
"front_end/panels/ai_chat/ui/message/MessageList.js",
695+
"front_end/panels/ai_chat/ui/message/ModelMessage.js",
696+
"front_end/panels/ai_chat/ui/message/MessageCombiner.js",
697+
"front_end/panels/ai_chat/ui/message/StructuredResponseRender.js",
698+
"front_end/panels/ai_chat/ui/message/StructuredResponseController.js",
699+
"front_end/panels/ai_chat/ui/message/GlobalActionsRow.js",
700+
"front_end/panels/ai_chat/ui/message/ToolResultMessage.js",
701+
"front_end/panels/ai_chat/ui/message/UserMessage.js",
702+
"front_end/panels/ai_chat/ui/model_selector/ModelSelector.js",
703+
"front_end/panels/ai_chat/ui/oauth/OAuthConnectPanel.js",
704+
"front_end/panels/ai_chat/ui/version/VersionBanner.js",
683705
"front_end/panels/ai_chat/common/WebSocketRPCClient.js",
684706
"front_end/panels/ai_chat/common/EvaluationConfig.js",
685707
"front_end/panels/ai_chat/evaluation/remote/EvaluationProtocol.js",
@@ -693,6 +715,7 @@ grd_files_bundled_sources = [
693715
"front_end/panels/ai_chat/ai_chat.js",
694716
"front_end/panels/ai_chat/ai_chat_impl.js",
695717
"front_end/panels/ai_chat/agent_framework/AgentRunner.js",
718+
"front_end/panels/ai_chat/agent_framework/AgentRunnerEventBus.js",
696719
"front_end/panels/ai_chat/agent_framework/AgentSessionTypes.js",
697720
"front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.js",
698721
"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/PerformanceAgentMarkdownRenderer.ts",

front_end/panels/ai_assistance/components/ChatView.ts

Lines changed: 117 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import * as Buttons from '../../../ui/components/buttons/buttons.js';
2020
import type * as MarkdownView from '../../../ui/components/markdown_view/markdown_view.js';
2121
import type {MarkdownLitRenderer} from '../../../ui/components/markdown_view/MarkdownView.js';
2222
import * as UI from '../../../ui/legacy/legacy.js';
23+
import { ScrollPinHelper } from './ScrollPinHelper.js';
2324
import * as Lit from '../../../ui/lit/lit.js';
2425
import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';
2526
import {PatchWidget} from '../PatchWidget.js';
@@ -308,28 +309,14 @@ export interface Props {
308309

309310
export class ChatView extends HTMLElement {
310311
readonly #shadow = this.attachShadow({mode: 'open'});
311-
#scrollTop?: number;
312+
#markdownRenderer = new MarkdownRendererWithCodeBlock();
313+
// Scroll management helper replaces ad-hoc state/logic
314+
#scrollHelper = new ScrollPinHelper();
312315
#props: Props;
313316
#messagesContainerElement?: Element;
314317
#mainElementRef?: Lit.Directives.Ref<Element> = Lit.Directives.createRef();
315318
#messagesContainerResizeObserver = new ResizeObserver(() => this.#handleMessagesContainerResize());
316-
/**
317-
* Indicates whether the chat scroll position should be pinned to the bottom.
318-
*
319-
* This is true when:
320-
* - The scroll is at the very bottom, allowing new messages to push the scroll down automatically.
321-
* - The panel is initially rendered and the user hasn't scrolled yet.
322-
*
323-
* It is set to false when the user scrolls up to view previous messages.
324-
*/
325-
#pinScrollToBottom = true;
326-
/**
327-
* Indicates whether the scroll event originated from code
328-
* or a user action. When set to `true`, `handleScroll` will ignore the event,
329-
* allowing it to only handle user-driven scrolls and correctly decide
330-
* whether to pin the content to the bottom.
331-
*/
332-
#isProgrammaticScroll = false;
319+
#popoverHelper: UI.PopoverHelper.PopoverHelper|null = null;
333320

334321
constructor(props: Props) {
335322
super();
@@ -353,16 +340,21 @@ export class ChatView extends HTMLElement {
353340
this.#messagesContainerResizeObserver.disconnect();
354341
}
355342

343+
// Centralize access to the textarea to avoid repeated querySelector casts
344+
#getTextArea(): HTMLTextAreaElement|null {
345+
return this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement | null;
346+
}
347+
356348
clearTextInput(): void {
357-
const textArea = this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement;
349+
const textArea = this.#getTextArea();
358350
if (!textArea) {
359351
return;
360352
}
361353
textArea.value = '';
362354
}
363355

364356
focusTextInput(): void {
365-
const textArea = this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement;
357+
const textArea = this.#getTextArea();
366358
if (!textArea) {
367359
return;
368360
}
@@ -371,51 +363,88 @@ export class ChatView extends HTMLElement {
371363
}
372364

373365
restoreScrollPosition(): void {
374-
if (this.#scrollTop === undefined) {
375-
return;
376-
}
377-
378-
if (!this.#mainElementRef?.value) {
379-
return;
366+
// Ensure helper has latest element
367+
if (this.#mainElementRef?.value) {
368+
this.#scrollHelper.setElement(this.#mainElementRef.value as HTMLElement);
380369
}
381-
382-
this.#setMainElementScrollTop(this.#scrollTop);
370+
this.#scrollHelper.restoreLastPosition();
383371
}
384372

385373
scrollToBottom(): void {
386-
if (!this.#mainElementRef?.value) {
387-
return;
374+
if (this.#mainElementRef?.value) {
375+
this.#scrollHelper.setElement(this.#mainElementRef.value as HTMLElement);
388376
}
389-
390-
this.#setMainElementScrollTop(this.#mainElementRef.value.scrollHeight);
377+
this.#scrollHelper.scrollToBottom();
391378
}
392379

393-
#handleMessagesContainerResize(): void {
394-
if (!this.#pinScrollToBottom) {
380+
#handleChatUiRef(el: Element|undefined): void {
381+
if (!el || this.#popoverHelper) {
395382
return;
396383
}
397384

398-
if (!this.#mainElementRef?.value) {
399-
return;
400-
}
385+
// TODO: Update here when b/409965560 is fixed.
386+
this.#popoverHelper = new UI.PopoverHelper.PopoverHelper((el as HTMLElement), event => {
387+
const popoverShownNode =
388+
event.target instanceof HTMLElement && event.target.id === RELEVANT_DATA_LINK_ID ? event.target : null;
389+
if (!popoverShownNode) {
390+
return null;
391+
}
401392

402-
if (this.#pinScrollToBottom) {
403-
this.#setMainElementScrollTop(this.#mainElementRef.value.scrollHeight);
404-
}
393+
// We move the glass pane to be a bit lower so
394+
// that it does not disappear when moving the cursor
395+
// over to link.
396+
const nodeBox = popoverShownNode.boxInWindow();
397+
nodeBox.y = nodeBox.y + TOOLTIP_POPOVER_OFFSET;
398+
return {
399+
box: nodeBox,
400+
show: async (popover: UI.GlassPane.GlassPane) => {
401+
// clang-format off
402+
Lit.render(html`
403+
<style>
404+
.info-tooltip-container {
405+
max-width: var(--sys-size-28);
406+
padding: var(--sys-size-4) var(--sys-size-5);
407+
408+
.tooltip-link {
409+
display: block;
410+
margin-top: var(--sys-size-4);
411+
color: var(--sys-color-primary);
412+
padding-left: 0;
413+
}
414+
}
415+
</style>
416+
<div class="info-tooltip-container">
417+
${this.#props.disclaimerText}
418+
<button
419+
class="link tooltip-link"
420+
role="link"
421+
jslog=${VisualLogging.link('open-ai-settings').track({
422+
click: true,
423+
})}
424+
@click=${() => {
425+
void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
426+
}}
427+
>${i18nString(UIStrings.learnAbout)}</button>
428+
</div>`, popover.contentElement, {host: this});
429+
// clang-format on
430+
return true;
431+
},
432+
};
433+
});
434+
this.#popoverHelper.setTimeout(0);
405435
}
406436

407-
#setMainElementScrollTop(scrollTop: number): void {
408-
if (!this.#mainElementRef?.value) {
409-
return;
437+
#handleMessagesContainerResize(): void {
438+
if (this.#mainElementRef?.value) {
439+
this.#scrollHelper.setElement(this.#mainElementRef.value as HTMLElement);
410440
}
411-
412-
this.#scrollTop = scrollTop;
413-
this.#isProgrammaticScroll = true;
414-
this.#mainElementRef.value.scrollTop = scrollTop;
441+
this.#scrollHelper.handleResize();
415442
}
416443

444+
// Removed ad-hoc scroll setter in favor of ScrollPinHelper
445+
417446
#setInputText(text: string): void {
418-
const textArea = this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement;
447+
const textArea = this.#getTextArea();
419448
if (!textArea) {
420449
return;
421450
}
@@ -430,7 +459,6 @@ export class ChatView extends HTMLElement {
430459
if (el) {
431460
this.#messagesContainerResizeObserver.observe(el);
432461
} else {
433-
this.#pinScrollToBottom = true;
434462
this.#messagesContainerResizeObserver.disconnect();
435463
}
436464
}
@@ -439,18 +467,10 @@ export class ChatView extends HTMLElement {
439467
if (!ev.target || !(ev.target instanceof HTMLElement)) {
440468
return;
441469
}
442-
443-
// Do not handle scroll events caused by programmatically
444-
// updating the scroll position. We want to know whether user
445-
// did scroll the container from the user interface.
446-
if (this.#isProgrammaticScroll) {
447-
this.#isProgrammaticScroll = false;
448-
return;
470+
if (this.#mainElementRef?.value) {
471+
this.#scrollHelper.setElement(this.#mainElementRef.value as HTMLElement);
449472
}
450-
451-
this.#scrollTop = ev.target.scrollTop;
452-
this.#pinScrollToBottom =
453-
ev.target.scrollTop + ev.target.clientHeight + SCROLL_ROUNDING_OFFSET > ev.target.scrollHeight;
473+
this.#scrollHelper.handleScroll(ev.target);
454474
};
455475

456476
#handleSubmit = (ev: SubmitEvent): void => {
@@ -459,7 +479,7 @@ export class ChatView extends HTMLElement {
459479
return;
460480
}
461481

462-
const textArea = this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement;
482+
const textArea = this.#getTextArea();
463483
if (!textArea?.value) {
464484
return;
465485
}
@@ -514,44 +534,44 @@ export class ChatView extends HTMLElement {
514534
Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistanceDynamicSuggestionClicked);
515535
};
516536

517-
#render(): void {
518-
const renderFooter = (): Lit.LitTemplate => {
519-
const classes = Lit.Directives.classMap({
520-
'chat-view-footer': true,
521-
'has-conversation': !!this.#props.conversationType,
522-
'is-read-only': this.#props.isReadOnly,
523-
});
524-
525-
// clang-format off
526-
const footerContents = this.#props.conversationType
527-
? renderRelevantDataDisclaimer({
537+
#renderFooter(): Lit.LitTemplate {
538+
const classes = Lit.Directives.classMap({
539+
'chat-view-footer': true,
540+
'has-conversation': !!this.#props.conversationType,
541+
'is-read-only': this.#props.isReadOnly,
542+
});
543+
544+
// clang-format off
545+
const footerContents = this.#props.conversationType
546+
? renderRelevantDataDisclaimer({
528547
isLoading: this.#props.isLoading,
529548
blockedByCrossOrigin: this.#props.blockedByCrossOrigin,
530-
tooltipId: RELEVANT_DATA_LINK_FOOTER_ID,
531-
disclaimerText: this.#props.disclaimerText,
532549
})
533-
: html`<p>
534-
${lockedString(UIStringsNotTranslate.inputDisclaimerForEmptyState)}
535-
<button
536-
class="link"
537-
role="link"
538-
jslog=${VisualLogging.link('open-ai-settings').track({
539-
click: true,
540-
})}
541-
@click=${() => {
542-
void UI.ViewManager.ViewManager.instance().showView(
543-
'chrome-ai',
544-
);
545-
}}
546-
>${i18nString(UIStrings.learnAbout)}</button>
547-
</p>`;
548-
549-
return html`
550-
<footer class=${classes} jslog=${VisualLogging.section('footer')}>
551-
${footerContents}
552-
</footer>
553-
`;
554-
};
550+
: html`<p>
551+
${lockedString(UIStringsNotTranslate.inputDisclaimerForEmptyState)}
552+
<button
553+
class="link"
554+
role="link"
555+
jslog=${VisualLogging.link('open-ai-settings').track({
556+
click: true,
557+
})}
558+
@click=${() => {
559+
void UI.ViewManager.ViewManager.instance().showView(
560+
'chrome-ai',
561+
);
562+
}}
563+
>${i18nString(UIStrings.learnAbout)}</button>
564+
</p>`;
565+
566+
return html`
567+
<footer class=${classes} jslog=${VisualLogging.section('footer')}>
568+
${footerContents}
569+
</footer>
570+
`;
571+
// clang-format on
572+
}
573+
574+
#render(): void {
555575
// clang-format off
556576
Lit.render(html`
557577
<style>${chatViewStyles}</style>
@@ -609,7 +629,7 @@ export class ChatView extends HTMLElement {
609629
})
610630
}
611631
</main>
612-
${renderFooter()}
632+
${this.#renderFooter()}
613633
</div>
614634
`, this.#shadow, {host: this});
615635
// clang-format on

0 commit comments

Comments
 (0)