diff --git a/ts/packages/shell/src/main/index.ts b/ts/packages/shell/src/main/index.ts index efe97bc9d..76f95eb99 100644 --- a/ts/packages/shell/src/main/index.ts +++ b/ts/packages/shell/src/main/index.ts @@ -25,7 +25,7 @@ import { } from "default-agent-provider"; import { ShellSettings } from "./shellSettings.js"; import { unlinkSync } from "fs"; -import { existsSync } from "node:fs"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { shellAgentProvider } from "./agent.js"; import { BrowserAgentIpc } from "./browserIpc.js"; import { WebSocketMessageV2 } from "common-utils"; @@ -59,6 +59,10 @@ process.argv.forEach((arg) => { } }); +export function runningTests(): boolean { + return process.env["INSTANCE_NAME"] !== undefined && process.env["INSTANCE_NAME"].startsWith("test_") === true; +} + let mainWindow: BrowserWindow | null = null; let inlineWebContentView: WebContentsView | null = null; let chatView: WebContentsView | null = null; @@ -614,6 +618,15 @@ async function initialize() { // Send settings asap ShellSettings.getinstance().onSettingsChanged!(); + // Load chat history if enabled + const chatHistory: string = path.join(getInstanceDir(), "chat_history.html") + if (ShellSettings.getinstance().chatHistory && existsSync(chatHistory)) { + chatView?.webContents.send( + "chat-history", + readFileSync(path.join(getInstanceDir(), "chat_history.html"), "utf-8"), + ); + } + // make sure links are opened in the external browser mainWindow.webContents.setWindowOpenHandler((details) => { require("electron").shell.openExternal(details.url); @@ -623,8 +636,31 @@ async function initialize() { // The dispatcher can be use now that dom is ready and the client is ready to receive messages const dispatcher = await dispatcherP; updateSummary(dispatcher); + + // send the agent greeting if it's turned on if (ShellSettings.getinstance().agentGreeting) { dispatcher.processCommand("@greeting", "agent-0", []); + } + }); + + // Store the chat history whenever the DOM changes + // this let's us rehydrate the chat when reopening the shell + ipcMain.on("dom changed", async(_event, html) => { + // store the modified DOM contents + const file: string = path.join(getInstanceDir(), "chat_history.html"); + + debugShell( + `Saving chat history to '${file}'.`, + performance.now(), + ); + + try { + writeFileSync(file, html); + } catch (e) { + debugShell( + `Unable to save history to '${file}'. Error: ${e}`, + performance.now(), + ); } }); @@ -674,10 +710,7 @@ async function initialize() { // for pen events which will trigger speech reco // Don't spin this up during testing if ( - process.platform == "win32" && - (process.env["INSTANCE_NAME"] == undefined || - process.env["INSTANCE_NAME"].startsWith("test_") == false) - ) { + process.platform == "win32" && !runningTests()) { const pipePath = path.join("\\\\.\\pipe\\TypeAgent", "speech"); const server = net.createServer((stream) => { stream.on("data", (c) => { diff --git a/ts/packages/shell/src/main/shellSettings.ts b/ts/packages/shell/src/main/shellSettings.ts index 5cd06f21c..dbf29339e 100644 --- a/ts/packages/shell/src/main/shellSettings.ts +++ b/ts/packages/shell/src/main/shellSettings.ts @@ -44,6 +44,7 @@ export class ShellSettings public onOpenInlineBrowser: ((targetUrl: URL) => void) | null; public onCloseInlineBrowser: EmptyFunction | null; public darkMode: boolean; + public chatHistory: boolean; public get width(): number { return this.size[0] ?? defaultSettings.size[0]; @@ -91,6 +92,7 @@ export class ShellSettings this.onOpenInlineBrowser = null; this.onCloseInlineBrowser = null; this.darkMode = settings.darkMode; + this.chatHistory = settings.chatHistory; } public static get filePath(): string { diff --git a/ts/packages/shell/src/main/shellSettingsType.ts b/ts/packages/shell/src/main/shellSettingsType.ts index c25f869b4..43bdd93e7 100644 --- a/ts/packages/shell/src/main/shellSettingsType.ts +++ b/ts/packages/shell/src/main/shellSettingsType.ts @@ -23,6 +23,7 @@ export type ShellSettingsType = { partialCompletion: boolean; disallowedDisplayType: DisplayType[]; darkMode: boolean; + chatHistory: boolean; }; export const defaultSettings: ShellSettingsType = { @@ -38,4 +39,5 @@ export const defaultSettings: ShellSettingsType = { partialCompletion: true, disallowedDisplayType: [], darkMode: false, + chatHistory: true }; diff --git a/ts/packages/shell/src/preload/electronTypes.ts b/ts/packages/shell/src/preload/electronTypes.ts index 389cf2bfc..45fdffc57 100644 --- a/ts/packages/shell/src/preload/electronTypes.ts +++ b/ts/packages/shell/src/preload/electronTypes.ts @@ -72,6 +72,7 @@ export interface ClientAPI { settings: ShellSettings, ) => void, ): void; + onChatHistory(callback: (e: Electron.IpcRendererEvent, chatHistory: string) => void): void; registerClientIO(clientIO: ClientIO); } diff --git a/ts/packages/shell/src/preload/index.ts b/ts/packages/shell/src/preload/index.ts index c8b7cba3c..9cfb25e20 100644 --- a/ts/packages/shell/src/preload/index.ts +++ b/ts/packages/shell/src/preload/index.ts @@ -43,6 +43,9 @@ const api: ClientAPI = { onSettingsChanged(callback) { ipcRenderer.on("settings-changed", callback); }, + onChatHistory(callback) { + ipcRenderer.on("chat-history", callback); + }, registerClientIO: (clientIO: ClientIO) => { if (clientIORegistered) { throw new Error("ClientIO already registered"); diff --git a/ts/packages/shell/src/renderer/src/chatView.ts b/ts/packages/shell/src/renderer/src/chatView.ts index 030e34b32..1f6f37718 100644 --- a/ts/packages/shell/src/renderer/src/chatView.ts +++ b/ts/packages/shell/src/renderer/src/chatView.ts @@ -552,6 +552,9 @@ export class ChatView { getMessageElm() { return this.topDiv; } + getScollContainer() { + return this.messageDiv; + } async showInputText(message: string) { const input = this.inputContainer.querySelector( diff --git a/ts/packages/shell/src/renderer/src/main.ts b/ts/packages/shell/src/renderer/src/main.ts index 981c633f7..14bf4c5c3 100644 --- a/ts/packages/shell/src/renderer/src/main.ts +++ b/ts/packages/shell/src/renderer/src/main.ts @@ -269,6 +269,10 @@ function addEvents( settingsView.shellSettings = value; }); + api.onChatHistory((_, history: string) => { + // TODO: rehydrate these into the UI + console.log(history); + }); } function showNotifications( @@ -425,7 +429,40 @@ document.addEventListener("DOMContentLoaded", async function () { } } + watchForDOMChanges(chatView.getScollContainer()); + if ((window as any).electron) { (window as any).electron.ipcRenderer.send("dom ready"); } }); + +function watchForDOMChanges(element: HTMLDivElement) { + + // ignore attribute changes but wach for + const config = { attributes: false, childList: true, subtree: true }; + + // timeout + let idleCounter: number = 0; + + // observer callback + const observer = new MutationObserver(() => { + + // increment the idle counter + idleCounter++; + + // decrement the idle counter + setTimeout(() => { + if (--idleCounter == 0) { + // last one notifies main process + if ((window as any).electron) { + (window as any).electron.ipcRenderer.send("dom changed", element.innerHTML); + } + } + }, 3000); + }); + + // start observing + observer.observe(element!, config); + + // observer.disconnect(); +} diff --git a/ts/packages/shell/src/renderer/src/webSocketAPI.ts b/ts/packages/shell/src/renderer/src/webSocketAPI.ts index c3454a909..b1a15b347 100644 --- a/ts/packages/shell/src/renderer/src/webSocketAPI.ts +++ b/ts/packages/shell/src/renderer/src/webSocketAPI.ts @@ -62,6 +62,10 @@ export const webapi: ClientAPI = { // TODO: figure out solution for mobile fnMap.set("settings-changed", callback); }, + onChatHistory(callback) { + // TODO: implement proper message rehydration on mobile + fnMap.set("chat-history", callback); + }, registerClientIO(clientIO: ClientIO) { if (clientIORegistered) { throw new Error("ClientIO already registered");