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
25 changes: 24 additions & 1 deletion config/gni/devtools_grd_files.gni
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,11 @@ grd_files_bundled_sources = [
"front_end/panels/ai_assistance/ai_assistance.js",
"front_end/panels/ai_chat/ui/AIChatPanel.js",
"front_end/panels/ai_chat/ui/ChatView.js",
"front_end/panels/ai_chat/ui/LiveAgentSessionComponent.js",
"front_end/panels/ai_chat/ui/ToolCallComponent.js",
"front_end/panels/ai_chat/ui/ToolResultComponent.js",
"front_end/panels/ai_chat/ui/AgentSessionHeaderComponent.js",
"front_end/panels/ai_chat/ui/ToolDescriptionFormatter.js",
"front_end/panels/ai_chat/ui/chatView.css.js",
Comment on lines +606 to 611
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

New AI Chat UI files added to GRD manifest — ensure files exist.

Good additions. Please verify the files exist at these paths (case and subdir correctness), otherwise grd gen will break.


🏁 Script executed:

#!/bin/bash
paths=(
front_end/panels/ai_chat/ui/LiveAgentSessionComponent.js
front_end/panels/ai_chat/ui/ToolCallComponent.js
front_end/panels/ai_chat/ui/ToolResultComponent.js
front_end/panels/ai_chat/ui/AgentSessionHeaderComponent.js
front_end/panels/ai_chat/ui/ToolDescriptionFormatter.js
)
for p in "${paths[@]}"; do [[ -f "$p" ]] || echo "MISSING: $p"; done

Length of output: 1412


Correct missing AI Chat UI component entries in config/gni/devtools_grd_files.gni.

The manifest lists these files, but none exist at the given paths—update to the actual filenames (e.g. .tsx extensions) or remove the invalid entries:

  • front_end/panels/ai_chat/ui/LiveAgentSessionComponent.js
  • front_end/panels/ai_chat/ui/ToolCallComponent.js
  • front_end/panels/ai_chat/ui/ToolResultComponent.js
  • front_end/panels/ai_chat/ui/AgentSessionHeaderComponent.js
  • front_end/panels/ai_chat/ui/ToolDescriptionFormatter.js
🤖 Prompt for AI Agents
In config/gni/devtools_grd_files.gni around lines 606 to 611, the manifest lists
several AI Chat UI files that do not exist at the given .js paths; update the
entries to point to the actual filenames (e.g. replace .js with the correct
.tsx/.jsx names) or remove the invalid lines. Verify the real file locations and
extensions in front_end/panels/ai_chat/ui, update each listed entry
(LiveAgentSessionComponent, ToolCallComponent, ToolResultComponent,
AgentSessionHeaderComponent, ToolDescriptionFormatter, chatView.css.js) to the
correct filenames or drop them from the manifest, then run a build/check to
ensure no missing-file errors remain.

"front_end/panels/ai_chat/ui/HelpDialog.js",
"front_end/panels/ai_chat/ui/PromptEditDialog.js",
Expand Down Expand Up @@ -634,7 +639,9 @@ grd_files_bundled_sources = [
"front_end/panels/ai_chat/LLM/GroqProvider.js",
"front_end/panels/ai_chat/LLM/OpenRouterProvider.js",
"front_end/panels/ai_chat/LLM/LLMClient.js",
"front_end/panels/ai_chat/LLM/MessageSanitizer.js",
"front_end/panels/ai_chat/tools/Tools.js",
"front_end/panels/ai_chat/tools/SequentialThinkingTool.js",
"front_end/panels/ai_chat/tools/CombinedExtractionTool.js",
"front_end/panels/ai_chat/tools/CritiqueTool.js",
"front_end/panels/ai_chat/tools/FetcherTool.js",
Expand All @@ -647,12 +654,27 @@ grd_files_bundled_sources = [
"front_end/panels/ai_chat/tools/VectorDBClient.js",
"front_end/panels/ai_chat/tools/BookmarkStoreTool.js",
"front_end/panels/ai_chat/tools/DocumentSearchTool.js",
"front_end/panels/ai_chat/tools/SequentialThinkingTool.js",
"front_end/panels/ai_chat/tools/ThinkingTool.js",
"front_end/panels/ai_chat/common/utils.js",
"front_end/panels/ai_chat/common/log.js",
"front_end/panels/ai_chat/common/context.js",
"front_end/panels/ai_chat/common/page.js",
"front_end/panels/ai_chat/core/structured_response.js",
"front_end/panels/ai_chat/models/ChatTypes.js",
"front_end/panels/ai_chat/ui/input/ChatInput.js",
"front_end/panels/ai_chat/ui/input/InputBar.js",
"front_end/panels/ai_chat/ui/markdown/MarkdownRenderers.js",
"front_end/panels/ai_chat/ui/message/MessageList.js",
"front_end/panels/ai_chat/ui/message/ModelMessage.js",
"front_end/panels/ai_chat/ui/message/MessageCombiner.js",
"front_end/panels/ai_chat/ui/message/StructuredResponseRender.js",
"front_end/panels/ai_chat/ui/message/StructuredResponseController.js",
"front_end/panels/ai_chat/ui/message/GlobalActionsRow.js",
"front_end/panels/ai_chat/ui/message/ToolResultMessage.js",
"front_end/panels/ai_chat/ui/message/UserMessage.js",
"front_end/panels/ai_chat/ui/model_selector/ModelSelector.js",
"front_end/panels/ai_chat/ui/oauth/OAuthConnectPanel.js",
"front_end/panels/ai_chat/ui/version/VersionBanner.js",
"front_end/panels/ai_chat/common/WebSocketRPCClient.js",
Comment on lines +662 to 678
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Bulk add of AI Chat core/models/ui files — sanity-check presence and typos.

Large manifest updates are brittle. Quick presence check avoids GRD churn.


🏁 Script executed:

#!/bin/bash
paths=(
front_end/panels/ai_chat/core/structured_response.js
front_end/panels/ai_chat/models/ChatTypes.js
front_end/panels/ai_chat/ui/input/ChatInput.js
front_end/panels/ai_chat/ui/input/InputBar.js
front_end/panels/ai_chat/ui/markdown/MarkdownRenderers.js
front_end/panels/ai_chat/ui/message/MessageList.js
front_end/panels/ai_chat/ui/message/ModelMessage.js
front_end/panels/ai_chat/ui/message/MessageCombiner.js
front_end/panels/ai_chat/ui/message/StructuredResponseRender.js
front_end/panels/ai_chat/ui/message/StructuredResponseController.js
front_end/panels/ai_chat/ui/message/GlobalActionsRow.js
front_end/panels/ai_chat/ui/message/ToolResultMessage.js
front_end/panels/ai_chat/ui/message/UserMessage.js
front_end/panels/ai_chat/ui/model_selector/ModelSelector.js
front_end/panels/ai_chat/ui/oauth/OAuthConnectPanel.js
front_end/panels/ai_chat/ui/version/VersionBanner.js
)
missing=0; for p in "${paths[@]}"; do [[ -f "$p" ]] || { echo "MISSING: $p"; missing=1; }; done; exit $missing

Length of output: 4714


Manifest references nonexistent AI Chat files
All 16 entries added in config/gni/devtools_grd_files.gni (lines 662–678) point to files that aren’t present in the repository. Correct the paths or add the missing source files before landing this change.

🤖 Prompt for AI Agents
In config/gni/devtools_grd_files.gni around lines 662–678 the 16 added entries
reference AI Chat source files that do not exist in the repo; either add the
missing files to the repository at the paths listed, or correct the paths to the
actual file locations (or remove the entries if those features are not ready).
Verify each filename and path against the source tree, add the missing JS
modules with the expected exports if needed, update the GN manifest to the
correct relative paths, and run the build/gn check to ensure no unresolved file
references remain before landing.

"front_end/panels/ai_chat/common/EvaluationConfig.js",
"front_end/panels/ai_chat/evaluation/remote/EvaluationProtocol.js",
Expand All @@ -666,6 +688,7 @@ grd_files_bundled_sources = [
"front_end/panels/ai_chat/ai_chat.js",
"front_end/panels/ai_chat/ai_chat_impl.js",
"front_end/panels/ai_chat/agent_framework/AgentRunner.js",
"front_end/panels/ai_chat/agent_framework/AgentRunnerEventBus.js",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

AgentRunnerEventBus added — good; verify usage and existence.

This aligns with the agent runtime changes. Confirm the file exists and at least one import references it.


🏁 Script executed:

#!/bin/bash
# File existence
test -f front_end/panels/ai_chat/agent_framework/AgentRunnerEventBus.js || echo "MISSING: AgentRunnerEventBus.js"
# Referenced by AgentRunner or others
rg -n 'AgentRunnerEventBus' front_end/panels/ai_chat

Length of output: 1716


🏁 Script executed:

#!/bin/bash
# Verify TypeScript source exists
test -f front_end/panels/ai_chat/agent_framework/AgentRunnerEventBus.ts || echo "MISSING: AgentRunnerEventBus.ts"

Length of output: 73


Fix missing AgentRunnerEventBus.js in devtools_grd_files.gni

config/gni/devtools_grd_files.gni lists front_end/panels/ai_chat/agent_framework/AgentRunnerEventBus.js but no .js exists in source (only AgentRunnerEventBus.ts), so update this entry to point at the generated .js artifact or switch to the .ts path to match the codebase.

🤖 Prompt for AI Agents
In config/gni/devtools_grd_files.gni around line 691, the list contains
"front_end/panels/ai_chat/agent_framework/AgentRunnerEventBus.js" but only
AgentRunnerEventBus.ts exists in source; update this entry to point to the
TypeScript source
"front_end/panels/ai_chat/agent_framework/AgentRunnerEventBus.ts" (or
alternatively replace it with the correct generated JS artifact path used by
your build output), and verify the GN rules include .ts files or the generated
.js location so the file is present in the built GRD.

"front_end/panels/ai_chat/agent_framework/AgentSessionTypes.js",
"front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.js",
"front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.js",
Expand Down
2 changes: 2 additions & 0 deletions front_end/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ group("front_end") {
"entrypoints/shell",
"entrypoints/wasmparser_worker:worker_entrypoint",
"entrypoints/worker_app:entrypoint",
"panels/ai_chat:ai_chat_release_js_metadata",
"panels/ai_chat:ai_chat_release_css_metadata",
Comment on lines +32 to +33
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Additions look correct; verify targets exist across build configs.

The new ai_chat release metadata deps make sense for flowing GRD metadata. Please confirm these GN targets are defined (and not gated behind a condition that would make them unavailable in some configs), otherwise the root group will fail to resolve.

Run to confirm target definitions and single inclusion:


🏁 Script executed:

#!/bin/bash
# 1) Targets exist somewhere under front_end/**/BUILD.gn
rg -n 'ai_chat_release_(js|css)_metadata' front_end/**/BUILD.gn -C2

# 2) Only referenced once in front_end/BUILD.gn
rg -n 'panels/ai_chat:ai_chat_release_(js|css)_metadata' front_end/BUILD.gn

Length of output: 352


Define missing GN targets or remove dependencies
The targets panels/ai_chat:ai_chat_release_js_metadata and panels/ai_chat:ai_chat_release_css_metadata aren’t defined in any front_end/**/BUILD.gn (no matches found via ripgrep). The root GN group will fail to resolve these deps—please add or correct these target definitions.

🤖 Prompt for AI Agents
In front_end/BUILD.gn around lines 32-33, the GN deps reference two targets that
don't exist: panels/ai_chat:ai_chat_release_js_metadata and
panels/ai_chat:ai_chat_release_css_metadata; add definitions for these targets
(or remove the dependencies) in panels/ai_chat/BUILD.gn so the root group can
resolve them. To fix, open panels/ai_chat/BUILD.gn and add two metadata/group
(or action) targets named ai_chat_release_js_metadata and
ai_chat_release_css_metadata that expose the JS and CSS release metadata files
or depend on the actual targets that produce those artifacts, ensuring names
exactly match the deps, or alternatively remove the two lines from the root
BUILD.gn if those artifacts are not required.

"third_party/vscode.web-custom-data:web_custom_data",
]
}
Expand Down
1 change: 1 addition & 0 deletions front_end/panels/ai_assistance/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ devtools_module("ai_assistance") {
"PatchWidget.ts",
"SelectWorkspaceDialog.ts",
"components/ChatView.ts",
"components/ScrollPinHelper.ts",
"components/ExploreWidget.ts",
"components/MarkdownRendererWithCodeBlock.ts",
"components/UserActionRow.ts",
Expand Down
165 changes: 64 additions & 101 deletions front_end/panels/ai_assistance/components/ChatView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import * as Marked from '../../../third_party/marked/marked.js';
import * as Buttons from '../../../ui/components/buttons/buttons.js';
import type * as MarkdownView from '../../../ui/components/markdown_view/markdown_view.js';
import * as UI from '../../../ui/legacy/legacy.js';
import { ScrollPinHelper } from './ScrollPinHelper.js';
import * as Lit from '../../../ui/lit/lit.js';
import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';
import {PatchWidget} from '../PatchWidget.js';
Expand Down Expand Up @@ -303,29 +304,13 @@ export interface Props {
export class ChatView extends HTMLElement {
readonly #shadow = this.attachShadow({mode: 'open'});
#markdownRenderer = new MarkdownRendererWithCodeBlock();
#scrollTop?: number;
// Scroll management helper replaces ad-hoc state/logic
#scrollHelper = new ScrollPinHelper();
#props: Props;
#messagesContainerElement?: Element;
#mainElementRef?: Lit.Directives.Ref<Element> = Lit.Directives.createRef();
#messagesContainerResizeObserver = new ResizeObserver(() => this.#handleMessagesContainerResize());
#popoverHelper: UI.PopoverHelper.PopoverHelper|null = null;
/**
* Indicates whether the chat scroll position should be pinned to the bottom.
*
* This is true when:
* - The scroll is at the very bottom, allowing new messages to push the scroll down automatically.
* - The panel is initially rendered and the user hasn't scrolled yet.
*
* It is set to false when the user scrolls up to view previous messages.
*/
#pinScrollToBottom = true;
/**
* Indicates whether the scroll event originated from code
* or a user action. When set to `true`, `handleScroll` will ignore the event,
* allowing it to only handle user-driven scrolls and correctly decide
* whether to pin the content to the bottom.
*/
#isProgrammaticScroll = false;

constructor(props: Props) {
super();
Expand All @@ -350,16 +335,21 @@ export class ChatView extends HTMLElement {
this.#messagesContainerResizeObserver.disconnect();
}

// Centralize access to the textarea to avoid repeated querySelector casts
#getTextArea(): HTMLTextAreaElement|null {
return this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement | null;
}

clearTextInput(): void {
const textArea = this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement;
const textArea = this.#getTextArea();
if (!textArea) {
return;
}
textArea.value = '';
}

focusTextInput(): void {
const textArea = this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement;
const textArea = this.#getTextArea();
if (!textArea) {
return;
}
Expand All @@ -368,23 +358,18 @@ export class ChatView extends HTMLElement {
}

restoreScrollPosition(): void {
if (this.#scrollTop === undefined) {
return;
}

if (!this.#mainElementRef?.value) {
return;
// Ensure helper has latest element
if (this.#mainElementRef?.value) {
this.#scrollHelper.setElement(this.#mainElementRef.value as HTMLElement);
}

this.#setMainElementScrollTop(this.#scrollTop);
this.#scrollHelper.restoreLastPosition();
}

scrollToBottom(): void {
if (!this.#mainElementRef?.value) {
return;
if (this.#mainElementRef?.value) {
this.#scrollHelper.setElement(this.#mainElementRef.value as HTMLElement);
}

this.#setMainElementScrollTop(this.#mainElementRef.value.scrollHeight);
this.#scrollHelper.scrollToBottom();
}

#handleChatUiRef(el: Element|undefined): void {
Expand Down Expand Up @@ -445,31 +430,16 @@ export class ChatView extends HTMLElement {
}

#handleMessagesContainerResize(): void {
if (!this.#pinScrollToBottom) {
return;
}

if (!this.#mainElementRef?.value) {
return;
}

if (this.#pinScrollToBottom) {
this.#setMainElementScrollTop(this.#mainElementRef.value.scrollHeight);
if (this.#mainElementRef?.value) {
this.#scrollHelper.setElement(this.#mainElementRef.value as HTMLElement);
}
this.#scrollHelper.handleResize();
}

#setMainElementScrollTop(scrollTop: number): void {
if (!this.#mainElementRef?.value) {
return;
}

this.#scrollTop = scrollTop;
this.#isProgrammaticScroll = true;
this.#mainElementRef.value.scrollTop = scrollTop;
}
// Removed ad-hoc scroll setter in favor of ScrollPinHelper

#setInputText(text: string): void {
const textArea = this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement;
const textArea = this.#getTextArea();
if (!textArea) {
return;
}
Expand All @@ -484,7 +454,6 @@ export class ChatView extends HTMLElement {
if (el) {
this.#messagesContainerResizeObserver.observe(el);
} else {
this.#pinScrollToBottom = true;
this.#messagesContainerResizeObserver.disconnect();
}
}
Expand All @@ -493,18 +462,10 @@ export class ChatView extends HTMLElement {
if (!ev.target || !(ev.target instanceof HTMLElement)) {
return;
}

// Do not handle scroll events caused by programmatically
// updating the scroll position. We want to know whether user
// did scroll the container from the user interface.
if (this.#isProgrammaticScroll) {
this.#isProgrammaticScroll = false;
return;
if (this.#mainElementRef?.value) {
this.#scrollHelper.setElement(this.#mainElementRef.value as HTMLElement);
}

this.#scrollTop = ev.target.scrollTop;
this.#pinScrollToBottom =
ev.target.scrollTop + ev.target.clientHeight + SCROLL_ROUNDING_OFFSET > ev.target.scrollHeight;
this.#scrollHelper.handleScroll(ev.target);
};

#handleSubmit = (ev: SubmitEvent): void => {
Expand All @@ -513,7 +474,7 @@ export class ChatView extends HTMLElement {
return;
}

const textArea = this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement;
const textArea = this.#getTextArea();
if (!textArea?.value) {
return;
}
Expand Down Expand Up @@ -567,42 +528,44 @@ export class ChatView extends HTMLElement {
Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistanceDynamicSuggestionClicked);
};

#renderFooter(): Lit.LitTemplate {
const classes = Lit.Directives.classMap({
'chat-view-footer': true,
'has-conversation': !!this.#props.conversationType,
'is-read-only': this.#props.isReadOnly,
});

// clang-format off
const footerContents = this.#props.conversationType
? renderRelevantDataDisclaimer({
isLoading: this.#props.isLoading,
blockedByCrossOrigin: this.#props.blockedByCrossOrigin,
})
: html`<p>
${lockedString(UIStringsNotTranslate.inputDisclaimerForEmptyState)}
<button
class="link"
role="link"
jslog=${VisualLogging.link('open-ai-settings').track({
click: true,
})}
@click=${() => {
void UI.ViewManager.ViewManager.instance().showView(
'chrome-ai',
);
}}
>${i18nString(UIStrings.learnAbout)}</button>
</p>`;

return html`
<footer class=${classes} jslog=${VisualLogging.section('footer')}>
${footerContents}
</footer>
`;
// clang-format on
}

#render(): void {
const renderFooter = (): Lit.LitTemplate => {
const classes = Lit.Directives.classMap({
'chat-view-footer': true,
'has-conversation': !!this.#props.conversationType,
'is-read-only': this.#props.isReadOnly,
});

// clang-format off
const footerContents = this.#props.conversationType
? renderRelevantDataDisclaimer({
isLoading: this.#props.isLoading,
blockedByCrossOrigin: this.#props.blockedByCrossOrigin,
})
: html`<p>
${lockedString(UIStringsNotTranslate.inputDisclaimerForEmptyState)}
<button
class="link"
role="link"
jslog=${VisualLogging.link('open-ai-settings').track({
click: true,
})}
@click=${() => {
void UI.ViewManager.ViewManager.instance().showView(
'chrome-ai',
);
}}
>${i18nString(UIStrings.learnAbout)}</button>
</p>`;

return html`
<footer class=${classes} jslog=${VisualLogging.section('footer')}>
${footerContents}
</footer>
`;
};
// clang-format off
Lit.render(html`
<style>${chatViewStyles}</style>
Expand Down Expand Up @@ -658,7 +621,7 @@ export class ChatView extends HTMLElement {
})
}
</main>
${renderFooter()}
${this.#renderFooter()}
</div>
`, this.#shadow, {host: this});
// clang-format on
Expand Down
59 changes: 59 additions & 0 deletions front_end/panels/ai_assistance/components/ScrollPinHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2025 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
* Utility to manage scroll pin-to-bottom behavior for a scrollable container.
*/
export class ScrollPinHelper {
#el: HTMLElement | null = null;
#scrollTop?: number;
#pinToBottom = true;
#isProgrammatic = false;
static readonly ROUNDING_OFFSET = 1;

setElement(el: HTMLElement | undefined): void {
if (el) {
this.#el = el;
} else {
this.#el = null;
this.#pinToBottom = true;
}
}

handleResize(): void {
if (!this.#el) return;
if (this.#pinToBottom) {
this.setScrollTop(this.#el.scrollHeight);
}
}

handleScroll(target: HTMLElement): void {
if (this.#isProgrammatic) {
this.#isProgrammatic = false;
return;
}
this.#scrollTop = target.scrollTop;
this.#pinToBottom = target.scrollTop + target.clientHeight + ScrollPinHelper.ROUNDING_OFFSET > target.scrollHeight;
}

setScrollTop(value: number): void {
if (!this.#el) return;
this.#scrollTop = value;
this.#isProgrammatic = true;
this.#el.scrollTop = value;
}

scrollToBottom(): void {
if (!this.#el) return;
this.setScrollTop(this.#el.scrollHeight);
}

restoreLastPosition(): void {
if (this.#scrollTop === undefined) {
return;
}
this.setScrollTop(this.#scrollTop);
}
}

Loading