diff --git a/e2e-tests/performance_monitor.spec.ts b/e2e-tests/performance_monitor.spec.ts new file mode 100644 index 0000000..c15f1a2 --- /dev/null +++ b/e2e-tests/performance_monitor.spec.ts @@ -0,0 +1,150 @@ +import { expect } from "@playwright/test"; +import { Timeout, testWithConfig } from "./helpers/test_helper"; +import * as fs from "node:fs"; +import * as path from "node:path"; + +testWithConfig({ + preLaunchHook: async ({ userDataDir }) => { + // Set up a force-close scenario by creating settings with isRunning: true + // and lastKnownPerformance data + const settingsPath = path.join(userDataDir, "user-settings.json"); + const settings = { + hasRunBefore: true, + isRunning: true, // Simulate force-close + enableAutoUpdate: false, + releaseChannel: "stable", + lastKnownPerformance: { + timestamp: Date.now() - 5000, // 5 seconds ago + memoryUsageMB: 256, + cpuUsagePercent: 45.5, + systemMemoryUsageMB: 8192, + systemMemoryTotalMB: 16384, + systemMemoryPercent: 50.0, + systemCpuPercent: 35.2, + }, + }; + + fs.mkdirSync(userDataDir, { recursive: true }); + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); + }, +})( + "force-close detection shows dialog with performance data", + async ({ po }) => { + // Wait for the home page to be visible first + await expect(po.getHomeChatInputContainer()).toBeVisible({ + timeout: Timeout.LONG, + }); + + // Check if the force-close dialog is visible by looking for the heading + await expect( + po.page.getByRole("heading", { name: "Force Close Detected" }), + ).toBeVisible({ timeout: Timeout.MEDIUM }); + + // Verify the warning message + await expect( + po.page.getByText( + "The app was not closed properly the last time it was running", + ), + ).toBeVisible(); + + // Verify performance data is displayed + await expect(po.page.getByText("Last Known State:")).toBeVisible(); + + // Check Process Metrics section + await expect(po.page.getByText("Process Metrics")).toBeVisible(); + await expect(po.page.getByText("256 MB")).toBeVisible(); + await expect(po.page.getByText("45.5%")).toBeVisible(); + + // Check System Metrics section + await expect(po.page.getByText("System Metrics")).toBeVisible(); + await expect(po.page.getByText("8192 / 16384 MB")).toBeVisible(); + await expect(po.page.getByText("35.2%")).toBeVisible(); + + // Close the dialog + await po.page.getByRole("button", { name: "OK" }).click(); + + // Verify dialog is closed by checking the heading is no longer visible + await expect( + po.page.getByRole("heading", { name: "Force Close Detected" }), + ).not.toBeVisible(); + }, +); + +testWithConfig({ + preLaunchHook: async ({ userDataDir }) => { + // Set up scenario without force-close (proper shutdown) + const settingsPath = path.join(userDataDir, "user-settings.json"); + const settings = { + hasRunBefore: true, + isRunning: false, // Proper shutdown - no force-close + enableAutoUpdate: false, + releaseChannel: "stable", + lastKnownPerformance: { + timestamp: Date.now() - 5000, + memoryUsageMB: 256, + cpuUsagePercent: 45.5, + systemMemoryUsageMB: 8192, + systemMemoryTotalMB: 16384, + systemMemoryPercent: 50.0, + systemCpuPercent: 35.2, + }, + }; + + fs.mkdirSync(userDataDir, { recursive: true }); + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); + }, +})("no force-close dialog when app was properly shut down", async ({ po }) => { + // Verify the home page loaded normally + await expect(po.getHomeChatInputContainer()).toBeVisible({ + timeout: Timeout.LONG, + }); + + // Verify that the force-close dialog is NOT shown + await expect( + po.page.getByRole("heading", { name: "Force Close Detected" }), + ).not.toBeVisible(); +}); + +testWithConfig({})( + "performance information is being captured during normal operation", + async ({ po, electronApp }) => { + // Wait for the app to load + await expect(po.getHomeChatInputContainer()).toBeVisible({ + timeout: Timeout.LONG, + }); + + // Get the user data directory + const userDataDir = (electronApp as any).$dyadUserDataDir; + const settingsPath = path.join(userDataDir, "user-settings.json"); + + // Wait a bit to allow performance monitoring to capture at least one data point + // Performance monitoring runs every 30 seconds, but we'll wait 35 seconds to be safe + await po.page.waitForTimeout(35000); + + // Read the settings file + const settingsContent = fs.readFileSync(settingsPath, "utf-8"); + const settings = JSON.parse(settingsContent); + + // Verify that lastKnownPerformance exists and has all required fields + expect(settings.lastKnownPerformance).toBeDefined(); + expect(settings.lastKnownPerformance.timestamp).toBeGreaterThan(0); + expect(settings.lastKnownPerformance.memoryUsageMB).toBeGreaterThan(0); + expect( + settings.lastKnownPerformance.cpuUsagePercent, + ).toBeGreaterThanOrEqual(0); + expect(settings.lastKnownPerformance.systemMemoryUsageMB).toBeGreaterThan( + 0, + ); + expect(settings.lastKnownPerformance.systemMemoryTotalMB).toBeGreaterThan( + 0, + ); + expect( + settings.lastKnownPerformance.systemCpuPercent, + ).toBeGreaterThanOrEqual(0); + + // Verify the timestamp is recent (within the last minute) + const now = Date.now(); + const timeDiff = now - settings.lastKnownPerformance.timestamp; + expect(timeDiff).toBeLessThan(60000); // Less than 1 minute old + }, +); diff --git a/e2e-tests/snapshots/auto_update.spec.ts_auto-update---disable-and-enable-1.txt b/e2e-tests/snapshots/auto_update.spec.ts_auto-update---disable-and-enable-1.txt index 4f464e8..102efc8 100644 --- a/e2e-tests/snapshots/auto_update.spec.ts_auto-update---disable-and-enable-1.txt +++ b/e2e-tests/snapshots/auto_update.spec.ts_auto-update---disable-and-enable-1.txt @@ -16,5 +16,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": false, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/auto_update.spec.ts_auto-update---disable-and-enable-2.txt b/e2e-tests/snapshots/auto_update.spec.ts_auto-update---disable-and-enable-2.txt index 281cd57..4ca646a 100644 --- a/e2e-tests/snapshots/auto_update.spec.ts_auto-update---disable-and-enable-2.txt +++ b/e2e-tests/snapshots/auto_update.spec.ts_auto-update---disable-and-enable-2.txt @@ -16,5 +16,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/context_window.spec.ts_context-window-4.txt b/e2e-tests/snapshots/context_window.spec.ts_context-window-4.txt index f86a076..6afd715 100644 --- a/e2e-tests/snapshots/context_window.spec.ts_context-window-4.txt +++ b/e2e-tests/snapshots/context_window.spec.ts_context-window-4.txt @@ -18,5 +18,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/release_channel.spec.ts_release-channel---change-from-stable-to-beta-and-back-1.txt b/e2e-tests/snapshots/release_channel.spec.ts_release-channel---change-from-stable-to-beta-and-back-1.txt index 5d35995..537679f 100644 --- a/e2e-tests/snapshots/release_channel.spec.ts_release-channel---change-from-stable-to-beta-and-back-1.txt +++ b/e2e-tests/snapshots/release_channel.spec.ts_release-channel---change-from-stable-to-beta-and-back-1.txt @@ -16,5 +16,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "beta", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/release_channel.spec.ts_release-channel---change-from-stable-to-beta-and-back-2.txt b/e2e-tests/snapshots/release_channel.spec.ts_release-channel---change-from-stable-to-beta-and-back-2.txt index 281cd57..4ca646a 100644 --- a/e2e-tests/snapshots/release_channel.spec.ts_release-channel---change-from-stable-to-beta-and-back-2.txt +++ b/e2e-tests/snapshots/release_channel.spec.ts_release-channel---change-from-stable-to-beta-and-back-2.txt @@ -16,5 +16,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/smart_context_options.spec.ts_switching-smart-context-mode-saves-the-right-setting-1.txt b/e2e-tests/snapshots/smart_context_options.spec.ts_switching-smart-context-mode-saves-the-right-setting-1.txt index c2c5fc5..2ff2131 100644 --- a/e2e-tests/snapshots/smart_context_options.spec.ts_switching-smart-context-mode-saves-the-right-setting-1.txt +++ b/e2e-tests/snapshots/smart_context_options.spec.ts_switching-smart-context-mode-saves-the-right-setting-1.txt @@ -24,5 +24,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/smart_context_options.spec.ts_switching-smart-context-mode-saves-the-right-setting-2.txt b/e2e-tests/snapshots/smart_context_options.spec.ts_switching-smart-context-mode-saves-the-right-setting-2.txt index 21b8b65..e6ff548 100644 --- a/e2e-tests/snapshots/smart_context_options.spec.ts_switching-smart-context-mode-saves-the-right-setting-2.txt +++ b/e2e-tests/snapshots/smart_context_options.spec.ts_switching-smart-context-mode-saves-the-right-setting-2.txt @@ -25,5 +25,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/smart_context_options.spec.ts_switching-smart-context-mode-saves-the-right-setting-3.txt b/e2e-tests/snapshots/smart_context_options.spec.ts_switching-smart-context-mode-saves-the-right-setting-3.txt index 2c5acbf..113435b 100644 --- a/e2e-tests/snapshots/smart_context_options.spec.ts_switching-smart-context-mode-saves-the-right-setting-3.txt +++ b/e2e-tests/snapshots/smart_context_options.spec.ts_switching-smart-context-mode-saves-the-right-setting-3.txt @@ -24,5 +24,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/smart_context_options.spec.ts_switching-smart-context-mode-saves-the-right-setting-4.txt b/e2e-tests/snapshots/smart_context_options.spec.ts_switching-smart-context-mode-saves-the-right-setting-4.txt index d09266f..77fa9df 100644 --- a/e2e-tests/snapshots/smart_context_options.spec.ts_switching-smart-context-mode-saves-the-right-setting-4.txt +++ b/e2e-tests/snapshots/smart_context_options.spec.ts_switching-smart-context-mode-saves-the-right-setting-4.txt @@ -25,5 +25,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/telemetry.spec.ts_telemetry---accept-1.txt b/e2e-tests/snapshots/telemetry.spec.ts_telemetry---accept-1.txt index b02db82..914acf3 100644 --- a/e2e-tests/snapshots/telemetry.spec.ts_telemetry---accept-1.txt +++ b/e2e-tests/snapshots/telemetry.spec.ts_telemetry---accept-1.txt @@ -15,5 +15,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/telemetry.spec.ts_telemetry---accept-2.txt b/e2e-tests/snapshots/telemetry.spec.ts_telemetry---accept-2.txt index 5bfa0ca..044c49e 100644 --- a/e2e-tests/snapshots/telemetry.spec.ts_telemetry---accept-2.txt +++ b/e2e-tests/snapshots/telemetry.spec.ts_telemetry---accept-2.txt @@ -16,5 +16,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/telemetry.spec.ts_telemetry---later-1.txt b/e2e-tests/snapshots/telemetry.spec.ts_telemetry---later-1.txt index b02db82..914acf3 100644 --- a/e2e-tests/snapshots/telemetry.spec.ts_telemetry---later-1.txt +++ b/e2e-tests/snapshots/telemetry.spec.ts_telemetry---later-1.txt @@ -15,5 +15,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/telemetry.spec.ts_telemetry---later-2.txt b/e2e-tests/snapshots/telemetry.spec.ts_telemetry---later-2.txt index 281cd57..4ca646a 100644 --- a/e2e-tests/snapshots/telemetry.spec.ts_telemetry---later-2.txt +++ b/e2e-tests/snapshots/telemetry.spec.ts_telemetry---later-2.txt @@ -16,5 +16,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/telemetry.spec.ts_telemetry---reject-1.txt b/e2e-tests/snapshots/telemetry.spec.ts_telemetry---reject-1.txt index b02db82..914acf3 100644 --- a/e2e-tests/snapshots/telemetry.spec.ts_telemetry---reject-1.txt +++ b/e2e-tests/snapshots/telemetry.spec.ts_telemetry---reject-1.txt @@ -15,5 +15,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/telemetry.spec.ts_telemetry---reject-2.txt b/e2e-tests/snapshots/telemetry.spec.ts_telemetry---reject-2.txt index 56c238b..71525f4 100644 --- a/e2e-tests/snapshots/telemetry.spec.ts_telemetry---reject-2.txt +++ b/e2e-tests/snapshots/telemetry.spec.ts_telemetry---reject-2.txt @@ -16,5 +16,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/template-community.spec.ts_template---community-1.txt b/e2e-tests/snapshots/template-community.spec.ts_template---community-1.txt index 281cd57..4ca646a 100644 --- a/e2e-tests/snapshots/template-community.spec.ts_template---community-1.txt +++ b/e2e-tests/snapshots/template-community.spec.ts_template---community-1.txt @@ -16,5 +16,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/template-community.spec.ts_template---community-2.txt b/e2e-tests/snapshots/template-community.spec.ts_template---community-2.txt index ae8943a..01f8070 100644 --- a/e2e-tests/snapshots/template-community.spec.ts_template---community-2.txt +++ b/e2e-tests/snapshots/template-community.spec.ts_template---community-2.txt @@ -17,5 +17,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/template-create-nextjs.spec.ts_create-next-js-app-1.txt b/e2e-tests/snapshots/template-create-nextjs.spec.ts_create-next-js-app-1.txt index 1332f0b..692875c 100644 --- a/e2e-tests/snapshots/template-create-nextjs.spec.ts_create-next-js-app-1.txt +++ b/e2e-tests/snapshots/template-create-nextjs.spec.ts_create-next-js-app-1.txt @@ -17,5 +17,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-1.txt b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-1.txt index 2b90eb5..0a69125 100644 --- a/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-1.txt +++ b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-1.txt @@ -25,5 +25,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-2.txt b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-2.txt index 247502f..4334f60 100644 --- a/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-2.txt +++ b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-2.txt @@ -8,14 +8,6 @@ "role": "system", "content": "[[SYSTEM_MESSAGE]]" }, - { - "role": "user", - "content": "tc=1" - }, - { - "role": "assistant", - "content": "Error: Test case file not found: 1.md" - }, { "role": "user", "content": "[dump] hi" diff --git a/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-3.txt b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-3.txt index a5ac511..abac104 100644 --- a/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-3.txt +++ b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-3.txt @@ -25,5 +25,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-4.txt b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-4.txt index a73e5b0..aa87e68 100644 --- a/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-4.txt +++ b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-4.txt @@ -8,22 +8,6 @@ "role": "system", "content": "[[SYSTEM_MESSAGE]]" }, - { - "role": "user", - "content": "tc=1" - }, - { - "role": "assistant", - "content": "Error: Test case file not found: 1.md" - }, - { - "role": "user", - "content": "[dump] hi" - }, - { - "role": "assistant", - "content": "[[dyad-dump-path=*]]" - }, { "role": "user", "content": "[dump] hi" diff --git a/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-5.txt b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-5.txt index b198ccc..3b8b1ce 100644 --- a/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-5.txt +++ b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-5.txt @@ -25,5 +25,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-6.txt b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-6.txt index a2dfe51..7118400 100644 --- a/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-6.txt +++ b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-6.txt @@ -8,30 +8,6 @@ "role": "system", "content": "[[SYSTEM_MESSAGE]]" }, - { - "role": "user", - "content": "tc=1" - }, - { - "role": "assistant", - "content": "Error: Test case file not found: 1.md" - }, - { - "role": "user", - "content": "[dump] hi" - }, - { - "role": "assistant", - "content": "[[dyad-dump-path=*]]" - }, - { - "role": "user", - "content": "[dump] hi" - }, - { - "role": "assistant", - "content": "[[dyad-dump-path=*]]" - }, { "role": "user", "content": "[dump] hi" diff --git a/e2e-tests/snapshots/turbo_edits_options.spec.ts_switching-turbo-edits-saves-the-right-setting-1.txt b/e2e-tests/snapshots/turbo_edits_options.spec.ts_switching-turbo-edits-saves-the-right-setting-1.txt index c2c5fc5..2ff2131 100644 --- a/e2e-tests/snapshots/turbo_edits_options.spec.ts_switching-turbo-edits-saves-the-right-setting-1.txt +++ b/e2e-tests/snapshots/turbo_edits_options.spec.ts_switching-turbo-edits-saves-the-right-setting-1.txt @@ -24,5 +24,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/turbo_edits_options.spec.ts_switching-turbo-edits-saves-the-right-setting-2.txt b/e2e-tests/snapshots/turbo_edits_options.spec.ts_switching-turbo-edits-saves-the-right-setting-2.txt index 04a2ceb..9f23d58 100644 --- a/e2e-tests/snapshots/turbo_edits_options.spec.ts_switching-turbo-edits-saves-the-right-setting-2.txt +++ b/e2e-tests/snapshots/turbo_edits_options.spec.ts_switching-turbo-edits-saves-the-right-setting-2.txt @@ -25,5 +25,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/turbo_edits_options.spec.ts_switching-turbo-edits-saves-the-right-setting-3.txt b/e2e-tests/snapshots/turbo_edits_options.spec.ts_switching-turbo-edits-saves-the-right-setting-3.txt index 4b335a0..19ecb64 100644 --- a/e2e-tests/snapshots/turbo_edits_options.spec.ts_switching-turbo-edits-saves-the-right-setting-3.txt +++ b/e2e-tests/snapshots/turbo_edits_options.spec.ts_switching-turbo-edits-saves-the-right-setting-3.txt @@ -25,5 +25,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/e2e-tests/snapshots/turbo_edits_options.spec.ts_switching-turbo-edits-saves-the-right-setting-4.txt b/e2e-tests/snapshots/turbo_edits_options.spec.ts_switching-turbo-edits-saves-the-right-setting-4.txt index 9abbe60..a5810ad 100644 --- a/e2e-tests/snapshots/turbo_edits_options.spec.ts_switching-turbo-edits-saves-the-right-setting-4.txt +++ b/e2e-tests/snapshots/turbo_edits_options.spec.ts_switching-turbo-edits-saves-the-right-setting-4.txt @@ -25,5 +25,6 @@ "enableAutoFixProblems": false, "enableAutoUpdate": true, "releaseChannel": "stable", + "isRunning": true, "isTestMode": true } \ No newline at end of file diff --git a/src/__tests__/readSettings.test.ts b/src/__tests__/readSettings.test.ts index f2b400f..31f16b0 100644 --- a/src/__tests__/readSettings.test.ts +++ b/src/__tests__/readSettings.test.ts @@ -59,6 +59,8 @@ describe("readSettings", () => { "enableProSmartFilesContextMode": true, "experiments": {}, "hasRunBefore": false, + "isRunning": false, + "lastKnownPerformance": undefined, "providerSettings": {}, "releaseChannel": "stable", "selectedChatMode": "build", @@ -305,6 +307,8 @@ describe("readSettings", () => { "enableProSmartFilesContextMode": true, "experiments": {}, "hasRunBefore": false, + "isRunning": false, + "lastKnownPerformance": undefined, "providerSettings": {}, "releaseChannel": "stable", "selectedChatMode": "build", diff --git a/src/components/ForceCloseDialog.tsx b/src/components/ForceCloseDialog.tsx new file mode 100644 index 0000000..beb8f7d --- /dev/null +++ b/src/components/ForceCloseDialog.tsx @@ -0,0 +1,128 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { AlertTriangle } from "lucide-react"; + +interface ForceCloseDialogProps { + isOpen: boolean; + onClose: () => void; + performanceData?: { + timestamp: number; + memoryUsageMB: number; + cpuUsagePercent?: number; + systemMemoryUsageMB?: number; + systemMemoryTotalMB?: number; + systemCpuPercent?: number; + }; +} + +export function ForceCloseDialog({ + isOpen, + onClose, + performanceData, +}: ForceCloseDialogProps) { + const formatTimestamp = (timestamp: number) => { + return new Date(timestamp).toLocaleString(); + }; + + return ( + !open && onClose()}> + + +
+ + Force Close Detected +
+ +
+
+ The app was not closed properly the last time it was running. + This could indicate a crash or unexpected termination. +
+ + {performanceData && ( +
+
+ Last Known State:{" "} + + {formatTimestamp(performanceData.timestamp)} + +
+ +
+ {/* Process Metrics */} +
+
+ Process Metrics +
+
+
+ Memory: + + {performanceData.memoryUsageMB} MB + +
+ {performanceData.cpuUsagePercent !== undefined && ( +
+ CPU: + + {performanceData.cpuUsagePercent}% + +
+ )} +
+
+ + {/* System Metrics */} + {(performanceData.systemMemoryUsageMB !== undefined || + performanceData.systemCpuPercent !== undefined) && ( +
+
+ System Metrics +
+
+ {performanceData.systemMemoryUsageMB !== undefined && + performanceData.systemMemoryTotalMB !== + undefined && ( +
+ + Memory: + + + {performanceData.systemMemoryUsageMB} /{" "} + {performanceData.systemMemoryTotalMB} MB + +
+ )} + {performanceData.systemCpuPercent !== undefined && ( +
+ + CPU: + + + {performanceData.systemCpuPercent}% + +
+ )} +
+
+ )} +
+
+ )} +
+
+
+ + OK + +
+
+ ); +} diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts index bd67614..fc2b116 100644 --- a/src/ipc/ipc_client.ts +++ b/src/ipc/ipc_client.ts @@ -1064,6 +1064,28 @@ export class IpcClient { }; } + // Listen for force close detected events + public onForceCloseDetected( + callback: (data: { + performanceData?: { + timestamp: number; + memoryUsageMB: number; + cpuUsagePercent?: number; + systemMemoryUsageMB?: number; + systemMemoryTotalMB?: number; + systemCpuPercent?: number; + }; + }) => void, + ): () => void { + const listener = (data: any) => { + callback(data); + }; + this.ipcRenderer.on("force-close-detected", listener); + return () => { + this.ipcRenderer.removeListener("force-close-detected", listener); + }; + } + // Count tokens for a chat and input public async countTokens( params: TokenCountParams, diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index c164d96..38dfe53 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -257,6 +257,17 @@ export const UserSettingsSchema = z.object({ releaseChannel: ReleaseChannelSchema, runtimeMode2: RuntimeMode2Schema.optional(), customNodePath: z.string().optional().nullable(), + isRunning: z.boolean().optional(), + lastKnownPerformance: z + .object({ + timestamp: z.number(), + memoryUsageMB: z.number(), + cpuUsagePercent: z.number().optional(), + systemMemoryUsageMB: z.number().optional(), + systemMemoryTotalMB: z.number().optional(), + systemCpuPercent: z.number().optional(), + }) + .optional(), //////////////////////////////// // E2E TESTING ONLY. diff --git a/src/main.ts b/src/main.ts index 50fd15f..0a4f5eb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,6 +24,10 @@ import { AddPromptDataSchema, AddPromptPayload, } from "./ipc/deep_link_data"; +import { + startPerformanceMonitoring, + stopPerformanceMonitoring, +} from "./utils/performance_monitor"; import fs from "fs"; log.errorHandler.startCatching(); @@ -82,6 +86,24 @@ export async function onReady() { } initializeDatabase(); const settings = readSettings(); + + // Check if app was force-closed + if (settings.isRunning) { + logger.warn("App was force-closed on previous run"); + + // Store performance data to send after window is created + if (settings.lastKnownPerformance) { + logger.warn("Last known performance:", settings.lastKnownPerformance); + pendingForceCloseData = settings.lastKnownPerformance; + } + } + + // Set isRunning to true at startup + writeSettings({ isRunning: true }); + + // Start performance monitoring + startPerformanceMonitoring(); + await onFirstRunMaybe(settings); createWindow(); @@ -151,6 +173,7 @@ declare global { } let mainWindow: BrowserWindow | null = null; +let pendingForceCloseData: any = null; const createWindow = () => { // Create the browser window. @@ -187,6 +210,16 @@ const createWindow = () => { mainWindow.webContents.openDevTools(); } + // Send force-close event if it was detected + if (pendingForceCloseData) { + mainWindow.webContents.once("did-finish-load", () => { + mainWindow?.webContents.send("force-close-detected", { + performanceData: pendingForceCloseData, + }); + pendingForceCloseData = null; + }); + } + // Enable native context menu on right-click mainWindow.webContents.on("context-menu", (event, params) => { // Prevent any default behavior and show our own menu @@ -414,6 +447,16 @@ app.on("window-all-closed", () => { } }); +// Only set isRunning to false when the app is properly quit by the user +app.on("will-quit", () => { + logger.info("App is quitting, setting isRunning to false"); + + // Stop performance monitoring and capture final metrics + stopPerformanceMonitoring(); + + writeSettings({ isRunning: false }); +}); + app.on("activate", () => { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. diff --git a/src/main/settings.ts b/src/main/settings.ts index 284e89f..ccb06eb 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -34,6 +34,8 @@ const DEFAULT_SETTINGS: UserSettings = { enableAutoUpdate: true, releaseChannel: "stable", selectedTemplateId: DEFAULT_TEMPLATE_ID, + isRunning: false, + lastKnownPerformance: undefined, }; const SETTINGS_FILE = "user-settings.json"; diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 2f3879c..ebd7166 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -28,6 +28,7 @@ import { ImportAppButton } from "@/components/ImportAppButton"; import { showError } from "@/lib/toast"; import { invalidateAppQuery } from "@/hooks/useLoadApp"; import { useQueryClient } from "@tanstack/react-query"; +import { ForceCloseDialog } from "@/components/ForceCloseDialog"; import type { FileAttachment } from "@/ipc/ipc_types"; import { NEON_TEMPLATE_IDS } from "@/shared/templates"; @@ -48,6 +49,8 @@ export default function HomePage() { const { settings, updateSettings } = useSettings(); const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom); const [isLoading, setIsLoading] = useState(false); + const [forceCloseDialogOpen, setForceCloseDialogOpen] = useState(false); + const [performanceData, setPerformanceData] = useState(undefined); const { streamMessage } = useStreamChat({ hasChatId: false }); const posthog = usePostHog(); const appVersion = useAppVersion(); @@ -55,6 +58,17 @@ export default function HomePage() { const [releaseUrl, setReleaseUrl] = useState(""); const { theme } = useTheme(); const queryClient = useQueryClient(); + + // Listen for force-close events + useEffect(() => { + const ipc = IpcClient.getInstance(); + const unsubscribe = ipc.onForceCloseDetected((data) => { + setPerformanceData(data.performanceData); + setForceCloseDialogOpen(true); + }); + return () => unsubscribe(); + }, []); + useEffect(() => { const updateLastVersionLaunched = async () => { if ( @@ -189,6 +203,11 @@ export default function HomePage() { // Main Home Page Content return (
+ setForceCloseDialogOpen(false)} + performanceData={performanceData} + />
diff --git a/src/preload.ts b/src/preload.ts index d894ba1..8f15900 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -154,6 +154,7 @@ const validReceiveChannels = [ "github:flow-success", "github:flow-error", "deep-link-received", + "force-close-detected", // Help bot "help:chat:response:chunk", "help:chat:response:end", diff --git a/src/utils/performance_monitor.ts b/src/utils/performance_monitor.ts new file mode 100644 index 0000000..92d7a9e --- /dev/null +++ b/src/utils/performance_monitor.ts @@ -0,0 +1,201 @@ +import log from "electron-log"; +import { writeSettings } from "../main/settings"; +import os from "node:os"; + +const logger = log.scope("performance-monitor"); + +// Constants +const MONITOR_INTERVAL_MS = 30000; // 30 seconds +const BYTES_PER_MB = 1024 * 1024; + +let monitorInterval: NodeJS.Timeout | null = null; +let lastCpuUsage: NodeJS.CpuUsage | null = null; +let lastTimestamp: number | null = null; +let lastSystemCpuInfo: os.CpuInfo[] | null = null; +let lastSystemTimestamp: number | null = null; + +/** + * Get current memory usage in MB + */ +function getMemoryUsageMB(): number { + const memoryUsage = process.memoryUsage(); + // Use RSS (Resident Set Size) for total memory used by the process + return Math.round(memoryUsage.rss / BYTES_PER_MB); +} + +/** + * Get CPU usage percentage + * This measures CPU time used by this process relative to wall clock time + */ +function getCpuUsagePercent(): number | null { + const currentCpuUsage = process.cpuUsage(); + const currentTimestamp = Date.now(); + + // On first call, just initialize and return null + if (lastCpuUsage === null || lastTimestamp === null) { + lastCpuUsage = currentCpuUsage; + lastTimestamp = currentTimestamp; + return null; + } + + // Calculate elapsed wall clock time in microseconds + const elapsedTimeMs = currentTimestamp - lastTimestamp; + const elapsedTimeMicros = elapsedTimeMs * 1000; + + // Calculate CPU time used (user + system) in microseconds + const cpuTimeMicros = + currentCpuUsage.user - + lastCpuUsage.user + + (currentCpuUsage.system - lastCpuUsage.system); + + // CPU percentage = (CPU time / wall clock time) * 100 + // This gives percentage across all cores (can exceed 100% on multi-core systems) + const cpuPercent = (cpuTimeMicros / elapsedTimeMicros) * 100; + + // Update for next calculation + lastCpuUsage = currentCpuUsage; + lastTimestamp = currentTimestamp; + + return Math.round(cpuPercent * 100) / 100; // Round to 2 decimal places +} + +/** + * Get system memory usage + */ +function getSystemMemoryUsage(): { + totalMemoryMB: number; + usedMemoryMB: number; + freeMemoryMB: number; + usagePercent: number; +} { + const totalMemory = os.totalmem(); + const freeMemory = os.freemem(); + const usedMemory = totalMemory - freeMemory; + + return { + totalMemoryMB: Math.round(totalMemory / BYTES_PER_MB), + usedMemoryMB: Math.round(usedMemory / BYTES_PER_MB), + freeMemoryMB: Math.round(freeMemory / BYTES_PER_MB), + usagePercent: Math.round((usedMemory / totalMemory) * 100 * 100) / 100, + }; +} + +/** + * Get system CPU usage percentage + */ +function getSystemCpuUsagePercent(): number | null { + const cpus = os.cpus(); + const currentTimestamp = Date.now(); + + // On first call, just initialize and return null + if (lastSystemCpuInfo === null || lastSystemTimestamp === null) { + lastSystemCpuInfo = cpus; + lastSystemTimestamp = currentTimestamp; + return null; + } + + // Calculate total CPU time for all cores + let totalIdle = 0; + let totalTick = 0; + let lastTotalIdle = 0; + let lastTotalTick = 0; + + // Current CPU times + for (const cpu of cpus) { + for (const type in cpu.times) { + totalTick += cpu.times[type as keyof typeof cpu.times]; + } + totalIdle += cpu.times.idle; + } + + // Last CPU times + for (const cpu of lastSystemCpuInfo) { + for (const type in cpu.times) { + lastTotalTick += cpu.times[type as keyof typeof cpu.times]; + } + lastTotalIdle += cpu.times.idle; + } + + // Calculate differences + const totalTickDiff = totalTick - lastTotalTick; + const idleDiff = totalIdle - lastTotalIdle; + + // Calculate usage percentage + const usage = 100 - (100 * idleDiff) / totalTickDiff; + + // Update for next calculation + lastSystemCpuInfo = cpus; + lastSystemTimestamp = currentTimestamp; + + return Math.round(usage * 100) / 100; +} + +/** + * Capture and save current performance metrics + */ +function capturePerformanceMetrics() { + try { + const memoryUsageMB = getMemoryUsageMB(); + const cpuUsagePercent = getCpuUsagePercent(); + const systemMemory = getSystemMemoryUsage(); + const systemCpuPercent = getSystemCpuUsagePercent(); + + // Skip saving if CPU is null (first call for either metric) + if (cpuUsagePercent === null || systemCpuPercent === null) { + logger.debug( + `Performance: Memory=${memoryUsageMB}MB, CPU=initializing, System Memory=${systemMemory.usagePercent}%, System CPU=initializing`, + ); + return; + } + + logger.debug( + `Performance: Memory=${memoryUsageMB}MB, CPU=${cpuUsagePercent}%, System Memory=${systemMemory.usedMemoryMB}/${systemMemory.totalMemoryMB}MB (${systemMemory.usagePercent}%), System CPU=${systemCpuPercent}%`, + ); + + writeSettings({ + lastKnownPerformance: { + timestamp: Date.now(), + memoryUsageMB, + cpuUsagePercent, + systemMemoryUsageMB: systemMemory.usedMemoryMB, + systemMemoryTotalMB: systemMemory.totalMemoryMB, + systemCpuPercent, + }, + }); + } catch (error) { + logger.error("Error capturing performance metrics:", error); + } +} + +/** + * Start monitoring performance metrics + * Captures metrics every 30 seconds + */ +export function startPerformanceMonitoring() { + if (monitorInterval) { + logger.warn("Performance monitoring already started"); + return; + } + + logger.info("Starting performance monitoring"); + + // Capture initial metrics + capturePerformanceMetrics(); + + // Capture every 30 seconds + monitorInterval = setInterval(capturePerformanceMetrics, MONITOR_INTERVAL_MS); +} + +/** + * Stop monitoring performance metrics + */ +export function stopPerformanceMonitoring() { + if (monitorInterval) { + logger.info("Stopping performance monitoring"); + clearInterval(monitorInterval); + monitorInterval = null; + + // Capture final metrics before stopping + capturePerformanceMetrics(); + } +}