| // Copyright 2026 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import * as Common from '../../core/common/common.js'; | |
| import * as Host from '../../core/host/host.js'; | |
| import { | |
| type Comment, | |
| type CommentAnchorSignature, | |
| CommentThread, | |
| type CommentThreadOptions, | |
| type CommentThreadStatus, | |
| type DOMNodeAnchorSignature, | |
| type EditorAnchorSignature, | |
| Events as CommentThreadEvents, | |
| type TimelineAnchorSignature, | |
| } from './CommentThread.js'; | |
| export { | |
| type Comment, | |
| type CommentAnchorSignature, | |
| CommentThread, | |
| type CommentThreadOptions, | |
| type CommentThreadStatus, | |
| type DOMNodeAnchorSignature, | |
| type EditorAnchorSignature, | |
| type TimelineAnchorSignature, | |
| }; | |
| export const enum Events { | |
| COMMENT_THREADS_CHANGED = 'CommentThreadsChanged', | |
| COMMENT_MODE_CHANGED = 'CommentModeChanged', | |
| AGENT_ATTACHED_CHANGED = 'AgentAttachedChanged', | |
| } | |
| export interface EventTypes { | |
| [Events.COMMENT_THREADS_CHANGED]: CommentThread[]; | |
| [Events.COMMENT_MODE_CHANGED]: boolean; | |
| [Events.AGENT_ATTACHED_CHANGED]: boolean; | |
| } | |
| /** | |
| * Headless model managing comment thread data, CRUD operations, and comment mode. | |
| */ | |
| export class CommentManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes> { | |
| readonly #commentThreads = new Map<string, CommentThread>(); | |
| #commentMode = false; | |
| #agentAttached = false; | |
| #onThreadChanged(): void { | |
| this.dispatchEventToListeners(Events.COMMENT_THREADS_CHANGED, this.getCommentThreads()); | |
| } | |
| setAgentAttached(value: boolean): void { | |
| if (this.#agentAttached === value) { | |
| return; | |
| } | |
| this.#agentAttached = value; | |
| if (!value) { | |
| this.setCommentMode(false); | |
| } | |
| this.dispatchEventToListeners(Events.AGENT_ATTACHED_CHANGED, value); | |
| } | |
| isAgentAttached(): boolean { | |
| return this.#agentAttached; | |
| } | |
| setCommentMode(active: boolean): void { | |
| if (active && !this.#agentAttached) { | |
| return; | |
| } | |
| if (this.#commentMode === active) { | |
| return; | |
| } | |
| this.#commentMode = active; | |
| this.dispatchEventToListeners(Events.COMMENT_MODE_CHANGED, active); | |
| } | |
| isCommentMode(): boolean { | |
| return this.#commentMode; | |
| } | |
| createCommentThread( | |
| anchor: CommentAnchorSignature, | |
| text?: string, | |
| author: 'DEVELOPER'|'AGENT' = 'DEVELOPER', | |
| isGeneratedComment?: boolean, | |
| ): CommentThread { | |
| const comments: Comment[] = text !== undefined ? [{ | |
| author, | |
| text, | |
| timestamp: Date.now(), | |
| }] : | |
| []; | |
| const thread = new CommentThread({ | |
| anchor, | |
| comments, | |
| isGeneratedComment, | |
| }); | |
| thread.addEventListener(CommentThreadEvents.CHANGED, this.#onThreadChanged, this); | |
| this.#commentThreads.set(thread.id, thread); | |
| this.dispatchEventToListeners(Events.COMMENT_THREADS_CHANGED, this.getCommentThreads()); | |
| return thread; | |
| } | |
| getCommentThread(id: string): CommentThread|undefined { | |
| return this.#commentThreads.get(id); | |
| } | |
| getCommentThreads(): CommentThread[] { | |
| return Array.from(this.#commentThreads.values()); | |
| } | |
| takeComments(): CommentThread[] { | |
| const threads: CommentThread[] = []; | |
| for (const thread of this.#commentThreads.values()) { | |
| if (thread.status === 'SENT_TO_AGENT' && !thread.transmitted) { | |
| thread.markTransmitted(); | |
| threads.push(thread); | |
| } | |
| } | |
| const changeCount = threads.filter(thread => thread.isGeneratedComment).length; | |
| const commentCount = threads.length - changeCount; | |
| if (commentCount > 0) { | |
| Host.userMetrics.commentSharedWithAgent(commentCount); | |
| } | |
| if (changeCount > 0) { | |
| Host.userMetrics.changeSharedWithAgent(changeCount); | |
| } | |
| return threads; | |
| } | |
| sendQueuedThreads(): void { | |
| for (const thread of this.getCommentThreads()) { | |
| if (thread.status === 'ACTIVE') { | |
| thread.sendToAgent(); | |
| } | |
| } | |
| } | |
| resolveCommentThread(threadId: string, replyText?: string): boolean { | |
| const thread = this.#commentThreads.get(threadId); | |
| if (!thread) { | |
| return false; | |
| } | |
| if (thread.status !== 'RESOLVED') { | |
| Host.userMetrics.commentResolved('Agent', 1); | |
| } | |
| thread.resolve(replyText); | |
| return true; | |
| } | |
| removeCommentThread(id: string): void { | |
| const thread = this.#commentThreads.get(id); | |
| if (!thread) { | |
| return; | |
| } | |
| thread.removeEventListener(CommentThreadEvents.CHANGED, this.#onThreadChanged, this); | |
| this.#commentThreads.delete(id); | |
| this.dispatchEventToListeners(Events.COMMENT_THREADS_CHANGED, this.getCommentThreads()); | |
| } | |
| clear(): void { | |
| this.setCommentMode(false); | |
| for (const thread of this.#commentThreads.values()) { | |
| thread.removeEventListener(CommentThreadEvents.CHANGED, this.#onThreadChanged, this); | |
| } | |
| this.#commentThreads.clear(); | |
| this.dispatchEventToListeners(Events.COMMENT_THREADS_CHANGED, []); | |
| } | |
| } |