Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 72 additions & 3 deletions apps/desktop/src/main/app-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { IPC_CHANNELS } from '../shared/constants';
import type { AppUpdateInfo } from '../shared/types';
import { compareVersions } from './updater/version-manager';
import { isMacOS } from './platform';
import { readSettingsFile, writeSettingsFile } from './settings-utils';

// GitHub repo info for API calls
const GITHUB_OWNER = 'AndyMik90';
Expand Down Expand Up @@ -155,6 +156,59 @@ function formatReleaseNotes(releaseNotes: UpdateInfo['releaseNotes']): string |
return undefined;
}

/**
* Read update suppression preferences from settings.
*/
function getUpdatePreferences(): { skippedVersion: string | null; remindAfter: number | null } {
const settings = readSettingsFile();
return {
skippedVersion: (settings?.skippedUpdateVersion as string) || null,
remindAfter: (settings?.updateRemindAfter as number) || null,
Comment on lines +165 to +166
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The type assertions as string and as number are not type-safe. If the settings file gets corrupted or manually edited with incorrect types for skippedUpdateVersion or updateRemindAfter, this could lead to runtime errors. It's safer to use typeof checks to validate the types before using them.

Suggested change
skippedVersion: (settings?.skippedUpdateVersion as string) || null,
remindAfter: (settings?.updateRemindAfter as number) || null,
skippedVersion: typeof settings?.skippedUpdateVersion === 'string' ? settings.skippedUpdateVersion : null,
remindAfter: typeof settings?.updateRemindAfter === 'number' ? settings.updateRemindAfter : null,

};
}

/**
* Check if notifications for a version should be suppressed.
* Returns true if the user has skipped this exact version, or snoozed and the snooze hasn't expired.
*/
export function shouldSuppressUpdate(version: string): boolean {
const prefs = getUpdatePreferences();
if (prefs.skippedVersion === version) return true;
if (prefs.remindAfter && Date.now() < prefs.remindAfter) return true;
return false;
}

/**
* Persist a request to skip a specific version permanently.
* That version will never trigger notifications again unless a newer version arrives.
*/
export function skipUpdateVersion(version: string): void {
const settings = readSettingsFile() || {};
settings.skippedUpdateVersion = version;
delete settings.updateRemindAfter;
writeSettingsFile(settings);
console.warn('[app-updater] Skipped version:', version);
}

/**
* Snooze update notifications for 24 hours.
*/
export function snoozeUpdate(): void {
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
const settings = readSettingsFile() || {};
settings.updateRemindAfter = Date.now() + TWENTY_FOUR_HOURS;
writeSettingsFile(settings);
console.warn('[app-updater] Snoozed updates until:', new Date(settings.updateRemindAfter as number).toISOString());
}

/**
* Check if update notifications are currently suppressed for a given version.
* Used by the renderer via IPC to avoid showing stale banners.
*/
export function isUpdateSuppressed(version: string): boolean {
return shouldSuppressUpdate(version);
}
Comment on lines +204 to +210
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if isUpdateSuppressed is used anywhere in the codebase
rg -n 'isUpdateSuppressed' --type=ts

Repository: AndyMik90/Aperant

Length of output: 164


Remove the unused isUpdateSuppressed wrapper function.

isUpdateSuppressed is not called anywhere in the codebase—ripgrep found only its definition. It simply wraps shouldSuppressUpdate without adding value. Remove it to reduce API surface.

♻️ Remove unused function
-/**
- * Check if update notifications are currently suppressed for a given version.
- * Used by the renderer via IPC to avoid showing stale banners.
- */
-export function isUpdateSuppressed(version: string): boolean {
-  return shouldSuppressUpdate(version);
-}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Check if update notifications are currently suppressed for a given version.
* Used by the renderer via IPC to avoid showing stale banners.
*/
export function isUpdateSuppressed(version: string): boolean {
return shouldSuppressUpdate(version);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/app-updater.ts` around lines 204 - 210, Remove the
unused wrapper function isUpdateSuppressed by deleting its exported declaration
that simply calls shouldSuppressUpdate; ensure you also remove any export
references to isUpdateSuppressed (so only shouldSuppressUpdate remains public)
and run a quick grep to confirm there are no remaining callers or IPC handlers
referencing isUpdateSuppressed before committing.

Comment on lines +208 to +210
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The function isUpdateSuppressed is a wrapper around shouldSuppressUpdate and is exported but appears to be unused. The corresponding IPC handler in app-update-handlers.ts calls shouldSuppressUpdate directly. To improve maintainability and reduce redundant code, this function should be removed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exported isUpdateSuppressed function is never imported

Low Severity

isUpdateSuppressed is exported but never imported anywhere in the codebase. The IPC handler for APP_UPDATE_IS_SUPPRESSED imports shouldSuppressUpdate directly instead. This function is a trivial wrapper that adds no value — it's dead code.

Fix in Cursor Fix in Web


/**
* Set the update channel for electron-updater.
* - 'latest': Only receive stable releases (default)
Expand Down Expand Up @@ -233,6 +287,16 @@ export function initializeAppUpdater(window: BrowserWindow, betaUpdates = false)
return;
}

// Check if user has skipped or snoozed this version
if (shouldSuppressUpdate(info.version)) {
console.warn('[app-updater] Update suppressed (skipped or snoozed):', info.version);
// Still download silently so it's ready when the suppression expires
autoUpdater.downloadUpdate().catch((error) => {
console.error('[app-updater] Failed to download update:', error.message);
});
return;
}

if (mainWindow) {
mainWindow.webContents.send(IPC_CHANNELS.APP_UPDATE_AVAILABLE, {
version: info.version,
Expand Down Expand Up @@ -265,9 +329,14 @@ export function initializeAppUpdater(window: BrowserWindow, betaUpdates = false)
releaseNotes: formatReleaseNotes(info.releaseNotes),
releaseDate: info.releaseDate
};
if (mainWindow) {
// Reuse downloadedUpdateInfo instead of calling formatReleaseNotes again
mainWindow.webContents.send(IPC_CHANNELS.APP_UPDATE_DOWNLOADED, downloadedUpdateInfo);

// Only notify renderer if update is not suppressed
if (!shouldSuppressUpdate(info.version)) {
if (mainWindow) {
mainWindow.webContents.send(IPC_CHANNELS.APP_UPDATE_DOWNLOADED, downloadedUpdateInfo);
}
} else {
console.warn('[app-updater] Downloaded update suppressed (skipped or snoozed):', info.version);
}
});

Expand Down
58 changes: 57 additions & 1 deletion apps/desktop/src/main/ipc-handlers/app-update-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import {
downloadStableVersion,
quitAndInstall,
getCurrentVersion,
getDownloadedUpdateInfo
getDownloadedUpdateInfo,
skipUpdateVersion,
snoozeUpdate,
shouldSuppressUpdate
} from '../app-updater';

/**
Expand Down Expand Up @@ -151,5 +154,58 @@ export function registerAppUpdateHandlers(): void {
}
);

/**
* APP_UPDATE_SKIP_VERSION: Permanently skip a specific version
* That version will never show notifications again until a newer version arrives
*/
ipcMain.handle(
IPC_CHANNELS.APP_UPDATE_SKIP_VERSION,
async (_event, version: string): Promise<IPCResult> => {
try {
// Validate version format (semver-like, max 50 chars)
if (!version || typeof version !== 'string' || version.length > 50 || !/^[\d\w.-]+$/.test(version)) {
return { success: false, error: 'Invalid version format' };
}
skipUpdateVersion(version);
return { success: true };
} catch (error) {
console.error('[app-update-handlers] Skip version failed:', error);
return { success: false, error: error instanceof Error ? error.message : 'Failed to skip version' };
}
}
);

/**
* APP_UPDATE_SNOOZE: Snooze update notifications for 24 hours
*/
ipcMain.handle(
IPC_CHANNELS.APP_UPDATE_SNOOZE,
async (): Promise<IPCResult> => {
try {
snoozeUpdate();
return { success: true };
} catch (error) {
console.error('[app-update-handlers] Snooze update failed:', error);
return { success: false, error: error instanceof Error ? error.message : 'Failed to snooze update' };
}
}
);

/**
* APP_UPDATE_IS_SUPPRESSED: Check if update notifications are suppressed for a version
* Used by the renderer to avoid showing stale banners after a skip/snooze
*/
ipcMain.handle(
IPC_CHANNELS.APP_UPDATE_IS_SUPPRESSED,
async (_event, version: string): Promise<IPCResult<boolean>> => {
try {
return { success: true, data: shouldSuppressUpdate(version) };
} catch (error) {
console.error('[app-update-handlers] Check suppression failed:', error);
return { success: false, error: error instanceof Error ? error.message : 'Failed to check suppression' };
}
}
);
Comment on lines +198 to +208
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding version validation for consistency.

The APP_UPDATE_IS_SUPPRESSED handler accepts a version string without validation, while APP_UPDATE_SKIP_VERSION (lines 166-168) validates the format. Although shouldSuppressUpdate only performs string comparison (no security risk), adding consistent validation would:

  1. Ensure uniform input handling across related handlers
  2. Fail fast on malformed input rather than silently returning false
♻️ Optional: Add validation for consistency
   ipcMain.handle(
     IPC_CHANNELS.APP_UPDATE_IS_SUPPRESSED,
     async (_event, version: string): Promise<IPCResult<boolean>> => {
       try {
+        if (!version || typeof version !== 'string' || version.length > 50 || !/^[\d\w.-]+$/.test(version)) {
+          return { success: false, error: 'Invalid version format' };
+        }
         return { success: true, data: shouldSuppressUpdate(version) };
       } catch (error) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ipcMain.handle(
IPC_CHANNELS.APP_UPDATE_IS_SUPPRESSED,
async (_event, version: string): Promise<IPCResult<boolean>> => {
try {
return { success: true, data: shouldSuppressUpdate(version) };
} catch (error) {
console.error('[app-update-handlers] Check suppression failed:', error);
return { success: false, error: error instanceof Error ? error.message : 'Failed to check suppression' };
}
}
);
ipcMain.handle(
IPC_CHANNELS.APP_UPDATE_IS_SUPPRESSED,
async (_event, version: string): Promise<IPCResult<boolean>> => {
try {
if (!version || typeof version !== 'string' || version.length > 50 || !/^[\d\w.-]+$/.test(version)) {
return { success: false, error: 'Invalid version format' };
}
return { success: true, data: shouldSuppressUpdate(version) };
} catch (error) {
console.error('[app-update-handlers] Check suppression failed:', error);
return { success: false, error: error instanceof Error ? error.message : 'Failed to check suppression' };
}
}
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/ipc-handlers/app-update-handlers.ts` around lines 198 -
208, The APP_UPDATE_IS_SUPPRESSED IPC handler currently accepts the version
string without validation; update the handler
(IPC_CHANNELS.APP_UPDATE_IS_SUPPRESSED) to validate the incoming version format
the same way APP_UPDATE_SKIP_VERSION does before calling shouldSuppressUpdate,
and return a failure IPCResult with a clear error message when format validation
fails; reuse the same validation logic/function used by APP_UPDATE_SKIP_VERSION
to ensure consistency and fail fast on malformed input.


console.warn('[IPC] App update handlers registered successfully');
}
12 changes: 12 additions & 0 deletions apps/desktop/src/preload/api/app-update-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export interface AppUpdateAPI {
installAppUpdate: () => void;
getAppVersion: () => Promise<string>;
getDownloadedAppUpdate: () => Promise<IPCResult<AppUpdateInfo | null>>;
skipAppUpdate: (version: string) => Promise<IPCResult>;
snoozeAppUpdate: () => Promise<IPCResult>;
isAppUpdateSuppressed: (version: string) => Promise<IPCResult<boolean>>;

// Event Listeners
onAppUpdateAvailable: (
Expand Down Expand Up @@ -69,6 +72,15 @@ export const createAppUpdateAPI = (): AppUpdateAPI => ({
getDownloadedAppUpdate: (): Promise<IPCResult<AppUpdateInfo | null>> =>
invokeIpc(IPC_CHANNELS.APP_UPDATE_GET_DOWNLOADED),

skipAppUpdate: (version: string): Promise<IPCResult> =>
invokeIpc(IPC_CHANNELS.APP_UPDATE_SKIP_VERSION, version),

snoozeAppUpdate: (): Promise<IPCResult> =>
invokeIpc(IPC_CHANNELS.APP_UPDATE_SNOOZE),

isAppUpdateSuppressed: (version: string): Promise<IPCResult<boolean>> =>
invokeIpc(IPC_CHANNELS.APP_UPDATE_IS_SUPPRESSED, version),

// Event Listeners
onAppUpdateAvailable: (
callback: (info: AppUpdateAvailableEvent) => void
Expand Down
27 changes: 26 additions & 1 deletion apps/desktop/src/renderer/components/AppUpdateNotification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,26 @@ export function AppUpdateNotification() {
setIsOpen(false);
};

const handleSkipVersion = async () => {
try {
if (updateInfo) {
await window.electronAPI.skipAppUpdate(updateInfo.version);
}
} catch {
// Skip is best-effort; close the dialog regardless
}
setIsOpen(false);
};

const handleRemindLater = async () => {
try {
await window.electronAPI.snoozeAppUpdate();
} catch {
// Snooze is best-effort; close the dialog regardless
}
setIsOpen(false);
};
Comment on lines +168 to +186
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The catch blocks in handleSkipVersion and handleRemindLater are empty. While the comment explains that the action is "best-effort", swallowing errors completely can make debugging difficult if there's a persistent problem with skipping or snoozing updates. It would be better to log the error to the console.

  const handleSkipVersion = async () => {
    try {
      if (updateInfo) {
        await window.electronAPI.skipAppUpdate(updateInfo.version);
      }
    } catch (error) {
      // Skip is best-effort; close the dialog regardless
      console.error('[AppUpdateNotification] Failed to skip version:', error);
    }
    setIsOpen(false);
  };

  const handleRemindLater = async () => {
    try {
      await window.electronAPI.snoozeAppUpdate();
    } catch (error) {
      // Snooze is best-effort; close the dialog regardless
      console.error('[AppUpdateNotification] Failed to snooze update:', error);
    }
    setIsOpen(false);
  };


if (!updateInfo) {
return null;
}
Expand Down Expand Up @@ -298,7 +318,12 @@ export function AppUpdateNotification() {
</div>

<DialogFooter className="flex flex-col sm:flex-row gap-3">
<Button variant="outline" onClick={handleDismiss} disabled={isDownloading}>
{!isDownloaded && !isDownloading && (
<Button variant="ghost" onClick={handleSkipVersion} className="text-muted-foreground">
{t("dialogs:appUpdate.skipThisVersion", "Skip This Version")}
</Button>
)}
<Button variant="outline" onClick={isDownloaded ? handleDismiss : handleRemindLater} disabled={isDownloading}>
{isDownloaded
? t("dialogs:appUpdate.installLater", "Install Later")
: t("dialogs:appUpdate.remindMeLater", "Remind Me Later")}
Expand Down
24 changes: 22 additions & 2 deletions apps/desktop/src/renderer/components/UpdateBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ export function UpdateBanner({ className }: UpdateBannerProps) {
const result = await window.electronAPI.checkAppUpdate();
if (result.success && result.data) {
const newVersion = result.data.version;

// Check if this version is suppressed (skipped or snoozed)
const suppressed = await window.electronAPI.isAppUpdateSuppressed(newVersion);
if (suppressed.success && suppressed.data) {
setUpdateInfo(null);
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Banner snooze becomes permanent within a single session

Medium Severity

After the 24-hour snooze expires, the banner never reappears within the same app session. When handleDismiss fires, it sets isDismissed = true while currentVersionRef.current retains the version string. After snooze expiry, checkForUpdate finds the update is no longer suppressed, but the currentVersionRef.current !== newVersion check is false (same version), so isDismissed is never reset to false. The render guard !updateInfo || isDismissed then permanently hides the banner. The same issue affects the onAppUpdateAvailable push listener. The snooze effectively becomes permanent until restart or a new version, contradicting the intended 24-hour behavior.

Additional Locations (2)
Fix in Cursor Fix in Web

// New update available - show banner (unless same version already dismissed)
if (currentVersionRef.current !== newVersion) {
setIsDismissed(false);
Expand All @@ -62,6 +70,11 @@ export function UpdateBanner({ className }: UpdateBannerProps) {
try {
const result = await window.electronAPI.getDownloadedAppUpdate();
if (result.success && result.data) {
// Check if this downloaded version is suppressed (skipped or snoozed)
const suppressed = await window.electronAPI.isAppUpdateSuppressed(result.data.version);
if (suppressed.success && suppressed.data) {
return; // Don't show banner for suppressed downloaded updates
}
currentVersionRef.current = result.data.version;
setUpdateInfo({
version: result.data.version,
Expand Down Expand Up @@ -173,8 +186,15 @@ export function UpdateBanner({ className }: UpdateBannerProps) {
}
};

// Handle dismiss
const handleDismiss = () => {
// Handle dismiss - snoozes for 24 hours so the banner doesn't reappear on next poll
const handleDismiss = async () => {
try {
if (updateInfo) {
await window.electronAPI.snoozeAppUpdate();
}
} catch {
// Snooze is best-effort; dismiss the banner regardless
}
setIsDismissed(true);
};
Comment on lines +190 to 199
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The catch block in handleDismiss is empty. While the comment explains the rationale, swallowing the error makes it difficult to diagnose issues if snoozing updates consistently fails. Please log the error to the console for better debuggability.

Suggested change
const handleDismiss = async () => {
try {
if (updateInfo) {
await window.electronAPI.snoozeAppUpdate();
}
} catch {
// Snooze is best-effort; dismiss the banner regardless
}
setIsDismissed(true);
};
const handleDismiss = async () => {
try {
if (updateInfo) {
await window.electronAPI.snoozeAppUpdate();
}
} catch (error) {
// Snooze is best-effort; dismiss the banner regardless
console.error('[UpdateBanner] Failed to snooze update:', error);
}
setIsDismissed(true);
};


Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/renderer/lib/mocks/settings-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export const settingsMock = {
downloadStableUpdate: async () => ({ success: true }),
installAppUpdate: () => { console.warn('[browser-mock] installAppUpdate called'); },
getDownloadedAppUpdate: async () => ({ success: true, data: null }),
skipAppUpdate: async (_version: string) => ({ success: true }),
snoozeAppUpdate: async () => ({ success: true }),
isAppUpdateSuppressed: async (_version: string) => ({ success: true, data: false }),

// App Update Event Listeners (no-op in browser mode)
onAppUpdateAvailable: () => () => {},
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/shared/constants/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,9 @@ export const IPC_CHANNELS = {
APP_UPDATE_INSTALL: 'app-update:install',
APP_UPDATE_GET_VERSION: 'app-update:get-version',
APP_UPDATE_GET_DOWNLOADED: 'app-update:get-downloaded', // Get downloaded update info (for showing Install button on Settings open)
APP_UPDATE_SKIP_VERSION: 'app-update:skip-version', // Skip a specific version permanently
APP_UPDATE_SNOOZE: 'app-update:snooze', // Snooze update notifications for 24 hours
APP_UPDATE_IS_SUPPRESSED: 'app-update:is-suppressed', // Check if update notifications are suppressed

// App auto-update events (main -> renderer)
APP_UPDATE_AVAILABLE: 'app-update:available',
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/shared/i18n/locales/en/dialogs.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@
"claudeCodeChangelog": "View Claude Code Changelog",
"claudeCodeChangelogAriaLabel": "View Claude Code Changelog (opens in new window)",
"readOnlyVolumeTitle": "Cannot install from disk image",
"readOnlyVolumeDescription": "Please move Aperant to your Applications folder before updating."
"readOnlyVolumeDescription": "Please move Aperant to your Applications folder before updating.",
"skipThisVersion": "Skip This Version"
},
"addCompetitor": {
"title": "Add Competitor",
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/shared/i18n/locales/fr/dialogs.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@
"claudeCodeChangelog": "Voir le journal des modifications Claude Code",
"claudeCodeChangelogAriaLabel": "Voir le journal des modifications Claude Code (s'ouvre dans une nouvelle fenêtre)",
"readOnlyVolumeTitle": "Impossible d'installer depuis une image disque",
"readOnlyVolumeDescription": "Veuillez déplacer Aperant dans votre dossier Applications avant de mettre à jour."
"readOnlyVolumeDescription": "Veuillez déplacer Aperant dans votre dossier Applications avant de mettre à jour.",
"skipThisVersion": "Ignorer cette version"
},
"addCompetitor": {
"title": "Ajouter un concurrent",
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/shared/types/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,9 @@ export interface ElectronAPI {
downloadStableUpdate: () => Promise<IPCResult>;
installAppUpdate: () => void;
getDownloadedAppUpdate: () => Promise<IPCResult<AppUpdateInfo | null>>;
skipAppUpdate: (version: string) => Promise<IPCResult>;
snoozeAppUpdate: () => Promise<IPCResult>;
isAppUpdateSuppressed: (version: string) => Promise<IPCResult<boolean>>;

// Electron app update event listeners
onAppUpdateAvailable: (
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/shared/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,10 @@ export interface AppSettings {
autoNameClaudeTerminals?: boolean;
// Track which version warnings have been shown (e.g., ["2.7.5"])
seenVersionWarnings?: string[];
// Version the user has permanently skipped for update notifications
skippedUpdateVersion?: string;
// Timestamp (ms) after which update notifications should resume (snooze)
updateRemindAfter?: number;
// Sidebar collapsed state (icons only when true)
sidebarCollapsed?: boolean;
// GPU acceleration for terminal rendering (WebGL)
Expand Down
Loading