tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mbrowser/components/aiwindow/ui/modules/ChatConstants.sys.mjs | 2+-
Mbrowser/components/aiwindow/ui/modules/ChatMessage.sys.mjs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/aiwindow/ui/modules/ChatMigrations.sys.mjs | 36++++++++----------------------------
Mbrowser/components/aiwindow/ui/modules/ChatSql.sys.mjs | 27+++++++++++++++++++++++++++
Mbrowser/components/aiwindow/ui/modules/ChatStore.sys.mjs | 29+++++++++++++++++++++++++++++
Mbrowser/components/aiwindow/ui/modules/ChatUtils.sys.mjs | 24+++++++++++++++++++++++-
Mbrowser/components/aiwindow/ui/test/xpcshell/test_ChatStore.js | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/aiwindow/ui/test/xpcshell/test_chat-utils.js | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
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")]); + }); +});