commit ccef3f1f9aaed55baa05e78974c25a0566ce44c1
parent 0f63db97b11af22e52ae77cb3d250b53bfd31989
Author: Omar Gonzalez <s9tpepper@apache.org>
Date: Tue, 30 Dec 2025 17:56:42 +0000
Bug 2007454 - Add chatHistoryView() method to support Chat History View r=mlucks,mak,ai-frontend-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D277397
Diffstat:
8 files changed, 280 insertions(+), 30 deletions(-)
diff --git a/browser/components/aiwindow/ui/modules/ChatConstants.sys.mjs b/browser/components/aiwindow/ui/modules/ChatConstants.sys.mjs
@@ -6,7 +6,7 @@
/**
* The current SQLite database schema version
*/
-export const CURRENT_SCHEMA_VERSION = 1;
+export const CURRENT_SCHEMA_VERSION = 2;
/**
* The directory that the SQLite database lives in
diff --git a/browser/components/aiwindow/ui/modules/ChatMessage.sys.mjs b/browser/components/aiwindow/ui/modules/ChatMessage.sys.mjs
@@ -211,3 +211,57 @@ export class ChatMinimal {
return this.#title;
}
}
+
+/**
+ * Used to retrieve chat entries for Chat History view
+ */
+export class ChatHistoryResult {
+ #convId;
+ #title;
+ #createdDate;
+ #updatedDate;
+ #urls;
+
+ constructor({ convId, title, createdDate, updatedDate, urls }) {
+ this.#convId = convId;
+ this.#title = title;
+ this.#createdDate = createdDate;
+ this.#updatedDate = updatedDate;
+ this.#urls = urls;
+ }
+
+ /**
+ * @returns {string}
+ */
+ get convId() {
+ return this.#convId;
+ }
+
+ /**
+ * @returns {string}
+ */
+ get title() {
+ return this.#title;
+ }
+
+ /**
+ * @returns {Date}
+ */
+ get createdDate() {
+ return this.#createdDate;
+ }
+
+ /**
+ * @returns {Date}
+ */
+ get updatedDate() {
+ return this.#updatedDate;
+ }
+
+ /**
+ * @returns {Array<URL>}
+ */
+ get urls() {
+ return this.#urls;
+ }
+}
diff --git a/browser/components/aiwindow/ui/modules/ChatMigrations.sys.mjs b/browser/components/aiwindow/ui/modules/ChatMigrations.sys.mjs
@@ -1,3 +1,5 @@
+import { MESSAGE_CONV_ID_INDEX } from "./ChatSql.sys.mjs";
+
/*
This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -8,35 +10,13 @@
*
* - List each change here and what it's for.
*
- * @param {OpenedConnection} _conn - The SQLite connection to use for the migration
- * @param {number} _version - The version number of the current schema
+ * @param {OpenedConnection} conn - The SQLite connection to use for the migration
+ * @param {number} version - The version number of the current schema
*/
-async function applyV2(_conn, _version) {
- // if (version < 2) {
- // // Drop the summary table, it is no longer needed
- // await conn.execute(`DROP TABLE IF EXISTS summary;`);
- //
- // try {
- // await conn.execute("SELECT page_url FROM message LIMIT 1;");
- // } catch (e) {
- // // Add a page_url column to the message table to keep track
- // // of the pages visitedjduring a conversation and when they
- // // were visited
- // await conn.execute(`
- // ALTER TABLE message ADD COLUMN page_url TEXT;
- // `);
- // }
- //
- // try {
- // await conn.execute("SELECT turn_index FROM message LIMIT 1;");
- // } catch (e) {
- // // Add a turn_index column to group all types of messages into a
- // // particular turn, ex: prompt -> response would be a single turn.
- // await conn.execute(`
- // ALTER TABLE message ADD COLUMN turn_index INTEGER;
- // `);
- // }
- // }
+async function applyV2(conn, version) {
+ if (version < 2) {
+ await conn.execute(MESSAGE_CONV_ID_INDEX);
+ }
}
/**
diff --git a/browser/components/aiwindow/ui/modules/ChatSql.sys.mjs b/browser/components/aiwindow/ui/modules/ChatSql.sys.mjs
@@ -77,6 +77,10 @@ export const MESSAGE_CREATED_DATE_INDEX = `
CREATE INDEX message_created_date_idx ON message(created_date);
`;
+export const MESSAGE_CONV_ID_INDEX = `
+CREATE INDEX IF NOT EXISTS message_conv_id_idx ON message(conv_id);
+`;
+
export const CONVERSATION_INSERT = `
INSERT INTO conversation (
conv_id, title, description, page_url, page_meta_jsonb,
@@ -242,3 +246,26 @@ LIMIT :limit OFFSET :offset;
export const DELETE_CONVERSATION_BY_ID = `
DELETE FROM conversation WHERE conv_id = :conv_id;
`;
+
+export const CONVERSATION_HISTORY = `
+SELECT c.conv_id, c.title, c.created_date, c.updated_date, (
+ SELECT group_concat(t.page_url)
+ FROM (
+ SELECT
+ m.page_url
+ FROM message m
+ WHERE m.conv_id = c.conv_id
+ AND m.page_url IS NOT NULL
+ GROUP BY m.page_url
+ ORDER BY MAX(m.created_date) ASC
+ ) AS t
+) AS urls
+FROM conversation c
+WHERE EXISTS (
+ SELECT 1
+ FROM message AS m
+ WHERE m.conv_id = c.conv_id
+)
+ORDER BY c.updated_date {sort}
+LIMIT :limit OFFSET :offset;
+`;
diff --git a/browser/components/aiwindow/ui/modules/ChatStore.sys.mjs b/browser/components/aiwindow/ui/modules/ChatStore.sys.mjs
@@ -24,6 +24,7 @@ import {
MESSAGE_ORDINAL_INDEX,
MESSAGE_URL_INDEX,
MESSAGE_CREATED_DATE_INDEX,
+ MESSAGE_CONV_ID_INDEX,
MESSAGE_INSERT,
CONVERSATIONS_MOST_RECENT,
CONVERSATION_BY_ID,
@@ -36,6 +37,7 @@ import {
MESSAGES_BY_DATE_AND_ROLE,
DELETE_CONVERSATION_BY_ID,
CONVERSATIONS_OLDEST,
+ CONVERSATION_HISTORY,
ESCAPE_CHAR,
getConversationMessagesSql,
} from "./ChatSql.sys.mjs";
@@ -61,6 +63,7 @@ import {
import {
parseConversationRow,
parseMessageRows,
+ parseChatHistoryViewRows,
toJSONOrNull,
} from "./ChatUtils.sys.mjs";
@@ -72,6 +75,7 @@ import {
import { migrations } from "./ChatMigrations.sys.mjs";
const MAX_DB_SIZE_BYTES = 75 * 1024 * 1024;
+const SORTS = ["ASC", "DESC"];
/**
* Simple interface to store and retrieve chat conversations and messages.
@@ -398,6 +402,30 @@ export class ChatStore {
}
/**
+ * Gets a list of chat history items to display in Chat History view.
+ *
+ * @param {number} [pageNumber=1] - The page number to get, 1 based indexing
+ * @param {number} [pageSize=20] - Number of items to get per page
+ * @param {string} [sort="desc"] - desc|asc The sorting order based on updated_date for conversations
+ */
+ async chatHistoryView(pageNumber = 1, pageSize = 20, sort = "desc") {
+ const sorting = SORTS.find(item => item === sort.toUpperCase()) ?? "DESC";
+ const offset = pageSize * (pageNumber - 1);
+ const limit = pageSize;
+ const params = {
+ limit,
+ offset,
+ };
+
+ const rows = await this.#conn.executeCached(
+ CONVERSATION_HISTORY.replace("{sort}", sorting),
+ params
+ );
+
+ return parseChatHistoryViewRows(rows);
+ }
+
+ /**
* Prunes the database of old conversations in order to get the
* database file size to the specified maximum size.
*
@@ -762,6 +790,7 @@ export class ChatStore {
await this.#conn.execute(MESSAGE_ORDINAL_INDEX);
await this.#conn.execute(MESSAGE_URL_INDEX);
await this.#conn.execute(MESSAGE_CREATED_DATE_INDEX);
+ await this.#conn.execute(MESSAGE_CONV_ID_INDEX);
}
get #removeDatabaseOnStartup() {
diff --git a/browser/components/aiwindow/ui/modules/ChatUtils.sys.mjs b/browser/components/aiwindow/ui/modules/ChatUtils.sys.mjs
@@ -11,7 +11,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
import { MESSAGE_ROLE } from "./ChatConstants.sys.mjs";
import { ChatConversation } from "./ChatConversation.sys.mjs";
-import { ChatMessage } from "./ChatMessage.sys.mjs";
+import { ChatMessage, ChatHistoryResult } from "./ChatMessage.sys.mjs";
/**
* Creates a 12 characters GUID with 72 bits of entropy.
@@ -79,6 +79,28 @@ export function parseMessageRows(rows) {
}
/**
+ * Parse conversation rows from the database into an array of ChatHistoryResult
+ * objects.
+ *
+ * @param {Array<object>} rows - The database rows to parse.
+ * @returns {Array<ChatHistoryResult>} The parsed chat history result entries
+ */
+export function parseChatHistoryViewRows(rows) {
+ return rows.map(row => {
+ return new ChatHistoryResult({
+ convId: row.getResultByName("conv_id"),
+ title: row.getResultByName("title"),
+ createdDate: row.getResultByName("created_date"),
+ updatedDate: row.getResultByName("updated_date"),
+ urls: row
+ .getResultByName("urls")
+ .split(",")
+ .map(url => new URL(url)),
+ });
+ });
+}
+
+/**
* Try to parse a JSON string, returning null if it fails or the value is falsy.
*
* @param {string} value - The JSON string to parse.
diff --git a/browser/components/aiwindow/ui/test/xpcshell/test_ChatStore.js b/browser/components/aiwindow/ui/test/xpcshell/test_ChatStore.js
@@ -611,3 +611,87 @@ add_atomic_task(
});
}
);
+
+async function addChatHistoryTestData() {
+ await addConvoWithSpecificTestData(
+ new Date("1/2/2025"),
+ new URL("https://www.firefox.com"),
+ new URL("https://www.mozilla.org"),
+ "Mozilla.org conversation 1",
+ "a random message"
+ );
+
+ await addConvoWithSpecificTestData(
+ new Date("1/3/2025"),
+ new URL("https://www.firefox.com"),
+ new URL("https://www.mozilla.org"),
+ "Mozilla.org interesting conversation 2",
+ "a random message again"
+ );
+
+ await addConvoWithSpecificTestData(
+ new Date("1/4/2025"),
+ new URL("https://www.firefox.com"),
+ new URL("https://www.mozilla.org"),
+ "Mozilla.org conversation 3",
+ "some other message"
+ );
+}
+
+add_atomic_task(async function test_chatHistoryView() {
+ await addChatHistoryTestData();
+
+ const entries = await gChatStore.chatHistoryView();
+
+ Assert.withSoftAssertions(function (soft) {
+ soft.equal(entries.length, 3);
+ soft.equal(entries[0].title, "Mozilla.org conversation 3");
+ soft.equal(entries[1].title, "Mozilla.org interesting conversation 2");
+ soft.equal(entries[2].title, "Mozilla.org conversation 1");
+ });
+});
+
+add_atomic_task(async function test_chatHistoryView_sorting_desc() {
+ await addChatHistoryTestData();
+
+ const entries = await gChatStore.chatHistoryView(1, 20, "desc");
+
+ Assert.withSoftAssertions(function (soft) {
+ soft.equal(entries.length, 3);
+ soft.equal(entries[0].title, "Mozilla.org conversation 3");
+ soft.equal(entries[1].title, "Mozilla.org interesting conversation 2");
+ soft.equal(entries[2].title, "Mozilla.org conversation 1");
+ });
+});
+
+add_atomic_task(async function test_chatHistoryView_sorting_asc() {
+ await addChatHistoryTestData();
+
+ const entries = await gChatStore.chatHistoryView(1, 20, "asc");
+
+ Assert.withSoftAssertions(function (soft) {
+ soft.equal(entries.length, 3);
+ soft.equal(entries[0].title, "Mozilla.org conversation 1");
+ soft.equal(entries[1].title, "Mozilla.org interesting conversation 2");
+ soft.equal(entries[2].title, "Mozilla.org conversation 3");
+ });
+});
+
+add_atomic_task(async function test_chatHistoryView_pageSize() {
+ await addChatHistoryTestData();
+
+ const entries = await gChatStore.chatHistoryView(1, 2, "asc");
+
+ Assert.equal(entries.length, 2);
+});
+
+add_atomic_task(async function test_chatHistoryView_pageNumber() {
+ await addChatHistoryTestData();
+
+ const entries = await gChatStore.chatHistoryView(3, 1, "asc");
+
+ Assert.withSoftAssertions(function (soft) {
+ soft.equal(entries.length, 1);
+ soft.equal(entries[0].title, "Mozilla.org conversation 3");
+ });
+});
diff --git a/browser/components/aiwindow/ui/test/xpcshell/test_chat-utils.js b/browser/components/aiwindow/ui/test/xpcshell/test_chat-utils.js
@@ -10,6 +10,7 @@ const {
makeGuid,
parseConversationRow,
parseMessageRows,
+ parseChatHistoryViewRows,
parseJSONOrNull,
getRoleLabel,
} = ChromeUtils.importESModule(
@@ -204,3 +205,56 @@ add_task(function test_user_getRoleLabel() {
Assert.equal(role, "Tool");
});
+
+add_task(function test_parseChatHistoryViewRows() {
+ const row1 = new RowStub({
+ conv_id: "1",
+ title: "conv 1",
+ created_date: 116952982,
+ updated_date: 116952982,
+ urls: "https://www.firefox.com,https://www.mozilla.com",
+ });
+
+ const row2 = new RowStub({
+ conv_id: "2",
+ title: "conv 2",
+ created_date: 117189198,
+ updated_date: 117189198,
+ urls: "https://www.mozilla.org",
+ });
+
+ const row3 = new RowStub({
+ conv_id: "3",
+ title: "conv 3",
+ created_date: 168298919,
+ updated_date: 168298919,
+ urls: "https://www.firefox.com",
+ });
+
+ const rows = [row1, row2, row3];
+
+ const viewRows = parseChatHistoryViewRows(rows);
+
+ Assert.withSoftAssertions(function (soft) {
+ soft.equal(viewRows[0].convId, "1");
+ soft.equal(viewRows[0].title, "conv 1");
+ soft.equal(viewRows[0].createdDate, 116952982);
+ soft.equal(viewRows[0].updatedDate, 116952982);
+ soft.deepEqual(viewRows[0].urls, [
+ new URL("https://www.firefox.com"),
+ new URL("https://www.mozilla.com"),
+ ]);
+
+ soft.equal(viewRows[1].convId, "2");
+ soft.equal(viewRows[1].title, "conv 2");
+ soft.equal(viewRows[1].createdDate, 117189198);
+ soft.equal(viewRows[1].updatedDate, 117189198);
+ soft.deepEqual(viewRows[1].urls, [new URL("https://www.mozilla.org")]);
+
+ soft.equal(viewRows[2].convId, "3");
+ soft.equal(viewRows[2].title, "conv 3");
+ soft.equal(viewRows[2].createdDate, 168298919);
+ soft.equal(viewRows[2].updatedDate, 168298919);
+ soft.deepEqual(viewRows[2].urls, [new URL("https://www.firefox.com")]);
+ });
+});