[PR #5421] [MERGED] feat(desktop): portable phase-3: instance manager #5199

Closed
opened 2026-03-17 02:40:07 +03:00 by kerem · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/hoppscotch/hoppscotch/pull/5421
Author: @CuriousCorrelation
Created: 10/1/2025
Status: Merged
Merged: 11/25/2025
Merged by: @jamesgeorge007

Base: nextHead: desktop-feat-portable-app-phase-3


📝 Commits (10+)

  • bee32dd feat(desktop): portable phase-3: instance manager
  • d1be901 fix: unused vars and lint from merge conflicts
  • 1d7c1d4 chore: update devenv
  • ce42336 chore: safely deprecate selfhost-desktop
  • b7687c0 chore: fix merge artifacts
  • d05f884 fix: rebase artifacts
  • 6318f5c fix(desktop): missing await
  • a581103 fix: rebase artifact
  • 292019a fix: rebase artifact
  • 93fec9c chore: minor formatting update

📊 Changes

218 files changed (+5112 additions, -16795 deletions)

View changed files

📝 devenv.lock (+33 -13)
📝 devenv.nix (+5 -6)
📝 devenv.yaml (+7 -16)
📝 packages/hoppscotch-common/.eslintrc.js (+7 -1)
📝 packages/hoppscotch-common/locales/en.json (+22 -1)
📝 packages/hoppscotch-common/src/components/app/Header.vue (+12 -41)
📝 packages/hoppscotch-common/src/components/instance/Switcher.vue (+578 -176)
📝 packages/hoppscotch-common/src/kernel/store.ts (+50 -9)
📝 packages/hoppscotch-common/src/platform/index.ts (+1 -0)
📝 packages/hoppscotch-common/src/platform/instance.ts (+219 -7)
packages/hoppscotch-common/src/services/instance-switcher.service.ts (+0 -508)
📝 packages/hoppscotch-desktop/.gitignore (+4 -0)
📝 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/dist-js/index.js (+8 -5)
📝 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src/main.js (+4 -4)
📝 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/vite.config.js (+10 -8)
📝 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/guest-js/index.ts (+9 -7)
📝 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/permissions/schemas/schema.json (+10 -0)
📝 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/rollup.config.js (+13 -13)
📝 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/kernel.js (+4 -4)
📝 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/dist-js/index.js (+18 -18)

...and 80 more files

📄 Description

This is an experimental first look at the portable version for the
desktop app.

This implementation activates the FE integration, finalizing the
instance switching architecture, plus portable functionality with
better compliance and security capabilities.

Closes FE-758
Closes FE-829
Closes FE-849
Closes FE-857
Closes FE-861
Closes FE-862
Closes FE-866
Closes FE-868
Closes FE-888
Closes FE-899
Closes FE-910
Closes FE-912
Closes FE-919
Closes FE-921
Closes FE-931
Closes FE-932
Closes FE-933
Closes FE-934
Closes FE-935
Closes FE-937
Closes FE-938
Closes FE-939
Closes FE-961
Closes FE-963
Closes FE-964
Closes FE-965
Closes FE-966
Closes FE-1053
Closes https://github.com/hoppscotch/hoppscotch/issues/4978 (Portable version now has disable updates toggle) [caveat: just for Windows]
Closes https://github.com/hoppscotch/hoppscotch/issues/4119 (Portable version can choose the current directory as the default storage location) [requested just for Windows]
Closes https://github.com/hoppscotch/hoppscotch/issues/3526 (Portable version in general) [requested for Windows]
Closes https://github.com/hoppscotch/hoppscotch/issues/4828 (Bug fix + Feature) [applies to all platforms]

The desktop app infrastructure established in phase-1 and phase-2
provided path management, data migration, and service architecture.

This phase completes the implementation by connecting frontend
components to the backend systems and enabling those managers and
paths.

Instance Switching

Desktop Instance Service

The DesktopInstanceService is now fully integrated with the frontend
instance switcher component. The service provides instance management
with the persistent storage layer introduced in phase-1.

export class DesktopInstanceService extends Service<ConnectionState> {
  public readonly instanceSwitchingEnabled: boolean =
    getKernelMode() === "desktop"

  public async connectToInstance(
    serverUrl: string,
    instanceKind: InstanceKind,
    displayName?: string,
    options?: Partial<LoadOptions>
  ): Promise<OperationResult> {
    // ...
  }
}

NOTE: Implementation uses the appload plugin for bundle loading
and window management. See those files in file view for
better context.

The service manages connection state through observables (so it's
reactive enough, the idea is to rely solely on store, since it's
the main source of truth but because there are ways to bypass it when
switching, see more details below, this approach works better), handles
bundle loading via the appload plugin, maintains recent instances via
the persistence layer (see phase-1). Window management during instance
switching prevents the multiple window issue identified in earlier
development (see https://github.com/hoppscotch/hoppscotch/pull/5381).

NOTE: Instance switching functionality is desktop-only. Web platform
continues using the no-op WebInstanceService implementation.

Switcher Component

The instance switcher component in hoppscotch-common now adapts to
platform:

<component
  :is="platform.instance.customInstanceSwitcherComponent"
  v-if="platform.instance?.customInstanceSwitcherComponent"
  @close-dropdown="$emit('close-dropdown')"
/>

<div v-else-if="isInstanceSwitchingEnabled"
     class="flex flex-col space-y-1 w-full">
  // Default implementation
</div>

<div v-else class="flex items-center justify-center px-4 py-3">
  <span class="text-secondaryLight text-sm">
    Instance switching not available
  </span>
</div>

The component subscribes to platform-provided observables for
connection state, recent instances, and current instance updates.

Notice the difference between the prior service-in-common to
platform-from-web change.

It also now includes better error handling with some new toasts
for notifications since instance closing is now handled client-side.
In this case client-side means the hosted web-app, host-side means
the underlying machinery run by plugin-appload.

Web platform sees no instance switching UI.
Desktop platform gets full functionality with fallback to default
implementation if custom components are unavailable.

Route Resolution

Updated router configuration detects portable mode at runtime and loads
appropriate home view:

{
  path: "/",
  name: "home",
  component: async () => {
    try {
      const isPortable = await invoke<boolean>("is_portable_mode")
      return isPortable
        ? import("./views/PortableHome.vue")
        : import("./views/StandardHome.vue")
    } catch (error) {
      return import("./views/StandardHome.vue")
    }
  },
},

In contrast to what we had prior, a consolidated Home.vue file
which would actually prevent updater because the Rust updater part that
handles portable version would setup before it gets called in the FE
causing a perpetual null value return by check() function.
Basically dynamic imports prevent race conditions (not exactly
but close enough) with the updater while ensuring correct component
loading based on build configuration.

Standard mode loads StandardHome.vue with all the existing
functionality. Portable mode loads PortableHome.vue with
portable-specific stuff.

Portable Mode Welcome Flow

Implemented first-launch experience for portable mode with information
related to what to expect with this version:

const showPortableWelcome = ref(false)
const portableSettings = reactive<PortableSettings>({
  disableUpdateNotifications: false,
  autoSkipWelcome: false,
})

const handlePortableWelcomeContinue = async () => {
  await persistence.setPortableSettings(portableSettings)
  showPortableWelcome.value = false
  await loadRecent()
}

The welcome screen explains portable mode behavior including data
isolation from installed versions, manual update requirements, and
enterprise deployment constraints. Users can configure update
notification preferences and opt to skip the welcome screen in future
launches.

Welcome screen only appears in portable mode and only on first launch
unless re-enabled by user preferences.

Update Flow Differentiation

Standard and portable modes now have distinct update handling:

StandardHome.vue

const installUpdate = async () => {
  try {
    appState.value = AppState.UPDATE_IN_PROGRESS
    await updaterClient.downloadAndInstall()
  } catch (err) {
    error.value = `Failed to install update: ${err.message}`
    appState.value = AppState.ERROR
  }
}

PortableHome.vue

const checkForUpdatesPortable = async () => {
  if (portableSettings.disableUpdateNotifications) return

  try {
    await updaterClient.checkForUpdates(true) // Shows native dialog
  } catch (err) {
    console.error("Error checking for portable updates:", err)
  }
}

Essentially:

Standard mode: provides automatic download and installation with
progress tracking.

Portable mode: shows native dialogs for manual download from releases
page, respecting enterprise requirements for controlled versioning.

Standard mode update behavior remains (mostly) identical.
Portable mode adds new manual update workflow without affecting
existing installations.

Backend

Updater

Extended the updater system with commands for different scenarios:

#[tauri::command]
pub async fn check_for_updates(
    app: AppHandle,
    show_native_dialog: bool,
) -> Result<UpdateInfo, String> {
    // Platform-specific update checking with native dialog
}

#[tauri::command]
pub async fn download_and_install_update(app: AppHandle)
  -> Result<(), String> {
    // Progress tracking with event (for UI updates, see
    // `PortableHome.vue`)
}

#[tauri::command]
pub async fn is_portable_mode() -> bool {
    cfg!(feature = "portable")
}

The updater emits progress events that frontend components can
subscribe to. It also handles both portable and standard mode update
flows. It also has some error handling with the same event emission
mechanics for UI feedback as well.

App Initialization

Initialization Composable

Created useAppInitialization() composable that handles all startup
scenarios, the idea is basically this:

export function useAppInitialization() {
  const performBasicInitialization = async () => {
    appVersion.value = await getVersion()
    await invoke("check_and_backup_on_version_change")
    await migration.initialize()
    await persistence.init()
  }

  const initialize = async (customLogic?: () => Promise<void>) => {
    await performBasicInitialization()

    if (customLogic) {
      await customLogic()
    } else {
      await loadRecent()
    }
  }
}

Initialization flow maintains identical behavior for existing
functionality, this is mainly a portable mode extension.

Migration System Activation

The InstanceStoreMigrationService now actively migrates data from
legacy stores to the new kernel-based system:

export class InstanceStoreMigrationService {
  async initialize(): Promise<void> {
    const isMigrated = await this.isMigrationComplete()
    if (isMigrated) return

    await this.performMigration()
  }

  private async performMigration(): Promise<void> {
    await this.ensureDirectoryStructure()
    await this.migrateDataSafely()
    await this.migrateHoppscotchStoreFiles()
    await this.markMigrationComplete()
    await this.cleanupOldFilesSafely()
  }
}

The migration system automatically detects existing data from legacy
LazyStore files, transfers connection state and recent instances to
kernel store, moves .hoppscotch.store files to organized directory
structure, and cleans up old files after successful migration.

Migration system preserves all existing user data and settings.
Users should experience no disruption during the upgrade process.

ESLint Infra

Added ESLint setup matching patterns from hoppscotch-common and
hoppscotch-selfhost-web:

// .eslintrc.cjs
module.exports = {
  extends: [
    "@vue/typescript/recommended",
    "plugin:prettier/recommended",
  ],
  rules: {
    "@typescript-eslint/no-unused-vars": [
      process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
      {
        "argsIgnorePattern": "^_",
        "varsIgnorePattern": "^_",
        "caughtErrorsIgnorePattern": "^_"
      }
    ],
  },
}

Kernel Integration

Multi-Store Path Resolution

Completed kernel store integration with dynamic path resolution:

const getStorePath = async (): Promise<string> => {
  try {
    const storeDir = await getStoreDir()
    return join(storeDir, STORE_PATH)
  } catch (error) {
    console.error("Failed to get store directory:", error)
    return "hoppscotch-unified.store"
  }
}

export const Store = (() => {
  return {
    set: async (namespace, key, value, options?) => {
      const storePath = await getStorePath()
      return module().set(storePath, namespace, key, value, options)
    },
    // ... other operations ...
  }
})()

This is overall a better approach than the isolated calls to
LazyStore since the kernel store can manage multiple concurrent store
instances with different paths for different data types while
maintaining all the namespace isolation guarantees we need when
switching between contexts.

Portable Mode Isolation

This also implemented complete data boundary isolation between portable
and installed versions:

    if cfg!(feature = "portable") {
        let config_dir = path::config_dir()?;
   }

That data isolation makes sure portable versions never access system
directories or data from installed versions, to meet the enterprise
security requirements for locked-down environments.

Instance Platform Definition

Extended the instance platform definition with operations for
instance management:

export type InstancePlatformDef = {
  instanceSwitchingEnabled: boolean

  customInstanceSwitcherComponent?: Component

  getConnectionStateStream?: () => Observable<ConnectionState>
  getRecentInstancesStream?: () => Observable<Instance[]>
  getCurrentInstanceStream?: () => Observable<Instance | null>

  getCurrentConnectionState?: () => ConnectionState
  getRecentInstances?: () => Instance[]
  getCurrentInstance?: () => Instance | null

  connectToInstance?: (
    serverUrl: string,
    instanceKind: InstanceKind,
    displayName?: string,
    options?: Partial<LoadOptions>
  ) => Promise<OperationResult>

  // ... other operations
}

The definition includes lifecycle hooks (beforeConnect,
afterConnect, etc.) and error handlers for platform-specific
behavior customization.

Localization Updates

Added instance switcher translations to en.json:

"instances": {
  "opening_add_modal": "Opening add instance dialog",
  "closed_add_modal": "Add instance dialog closed",
  "connection_cancelled": "Connection cancelled by pre-connect validation",
  "connecting": "Connecting to instance...",
  "connected_state": "Successfully connected to instance",
  "not_available": "Instance switching is not available"
}

Shared View Components

Created shared view components in src/views/shared/:

AppHeader.vue: Displays app branding with optional mode indicator
LoadingState.vue: Shows loading spinner with status message
UpdateFlow.vue: Handles update UI with progress tracking
ErrorState.vue: Displays error messages with retry option
VersionInfo.vue: Shows version and data directory information

These components are used by both StandardHome.vue and
PortableHome.vue for consistent UI across modes.

Header Integration

Updated Header.vue to use platform-provided instance information:

<span class="!font-bold uppercase tracking-wide !text-secondaryDark pr-1">
  {{
    platform.instance.getCurrentInstance?.()?.displayName ||
    "Hoppscotch"
  }}
</span>

Removed the service-in-common pattern in favor of platform-provided
observables for cleaner separation of concerns.

Plugin Type Updates

Updated plugin-appload guest-js types to include close operation
with proper TypeScript definitions. The close operation allows
controlled window management during instance switching.

Path Import Reorganization

Restructured imports in hoppscotch-selfhost-web with new alias
structure:

// Application layer (new)
"@app/platform": "./src/platform",
"@app/services": "./src/services",
"@app/components": "./src/components",
"@app/kernel": "./src/kernel",

// Common package (existing)
"@hoppscotch/common": "../hoppscotch-common/src",
"@composables": "../hoppscotch-common/src/composables",
"@helpers": "../hoppscotch-common/src/helpers",

This creates clear boundaries between application-specific code
(@app/*) and shared common code (@hoppscotch/common and legacy
@composables, @helpers aliases).

NOTE: Prior to this the loading path difference between portable
and standard would prevent either from building.

All platform implementation files now use @app/platform/* instead of
@platform/* for consistency. API files moved from @api/* to
@app/api/*. Library utilities moved from @lib/* to @app/lib/*.

Main Entry Point Updates

Updated main.ts to initialize kernel on startup:

import { initKernel } from "@hoppscotch/kernel"

const app = createApp(App)
app.use(router)
app.mount("#app")

initKernel("desktop")

The kernel initialization sets up the store system and IO operations
before the app mounts.

NOTE: This is important to make sure the plugin initializers in
the host-side are initialized before the kernel components users.

Also consolidated platform configuration into a single PLATFORM_CONFIG
object that defines all platform-specific settings (interceptors,
default values, menu items) for both web and desktop modes.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/hoppscotch/hoppscotch/pull/5421 **Author:** [@CuriousCorrelation](https://github.com/CuriousCorrelation) **Created:** 10/1/2025 **Status:** ✅ Merged **Merged:** 11/25/2025 **Merged by:** [@jamesgeorge007](https://github.com/jamesgeorge007) **Base:** `next` ← **Head:** `desktop-feat-portable-app-phase-3` --- ### 📝 Commits (10+) - [`bee32dd`](https://github.com/hoppscotch/hoppscotch/commit/bee32dd0d5e0ae3f1253cc297aeaec751ddf85a4) feat(desktop): portable phase-3: instance manager - [`d1be901`](https://github.com/hoppscotch/hoppscotch/commit/d1be901b3802ce6418fa5cb4f2383447939afc9f) fix: unused vars and lint from merge conflicts - [`1d7c1d4`](https://github.com/hoppscotch/hoppscotch/commit/1d7c1d411ac51cc4875c9811488c502ea0aeb219) chore: update `devenv` - [`ce42336`](https://github.com/hoppscotch/hoppscotch/commit/ce4233610b7332c9cbd23c8c9f9ce0913ac8531e) chore: safely deprecate `selfhost-desktop` - [`b7687c0`](https://github.com/hoppscotch/hoppscotch/commit/b7687c0a44e17ef79435a184facb231b23bb9320) chore: fix merge artifacts - [`d05f884`](https://github.com/hoppscotch/hoppscotch/commit/d05f8844f3c134b84faa8c8acb4ef25e90272d91) fix: rebase artifacts - [`6318f5c`](https://github.com/hoppscotch/hoppscotch/commit/6318f5c169534f076bfb6b8b39c80ae127417826) fix(desktop): missing `await` - [`a581103`](https://github.com/hoppscotch/hoppscotch/commit/a5811033b1010c8119b1860ab2f2b33cf9ffe0b8) fix: rebase artifact - [`292019a`](https://github.com/hoppscotch/hoppscotch/commit/292019aa9267cec236de0f60d8843d800dfecc5b) fix: rebase artifact - [`93fec9c`](https://github.com/hoppscotch/hoppscotch/commit/93fec9c94d8eafcbe882401dc20ff288965feb4d) chore: minor formatting update ### 📊 Changes **218 files changed** (+5112 additions, -16795 deletions) <details> <summary>View changed files</summary> 📝 `devenv.lock` (+33 -13) 📝 `devenv.nix` (+5 -6) 📝 `devenv.yaml` (+7 -16) 📝 `packages/hoppscotch-common/.eslintrc.js` (+7 -1) 📝 `packages/hoppscotch-common/locales/en.json` (+22 -1) 📝 `packages/hoppscotch-common/src/components/app/Header.vue` (+12 -41) 📝 `packages/hoppscotch-common/src/components/instance/Switcher.vue` (+578 -176) 📝 `packages/hoppscotch-common/src/kernel/store.ts` (+50 -9) 📝 `packages/hoppscotch-common/src/platform/index.ts` (+1 -0) 📝 `packages/hoppscotch-common/src/platform/instance.ts` (+219 -7) ➖ `packages/hoppscotch-common/src/services/instance-switcher.service.ts` (+0 -508) 📝 `packages/hoppscotch-desktop/.gitignore` (+4 -0) 📝 `packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/dist-js/index.js` (+8 -5) 📝 `packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src/main.js` (+4 -4) 📝 `packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/vite.config.js` (+10 -8) 📝 `packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/guest-js/index.ts` (+9 -7) 📝 `packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/permissions/schemas/schema.json` (+10 -0) 📝 `packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/rollup.config.js` (+13 -13) 📝 `packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/kernel.js` (+4 -4) 📝 `packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/dist-js/index.js` (+18 -18) _...and 80 more files_ </details> ### 📄 Description This is an experimental first look at the portable version for the desktop app. This implementation activates the FE integration, finalizing the instance switching architecture, plus portable functionality with better compliance and security capabilities. Closes FE-758 Closes FE-829 Closes FE-849 Closes FE-857 Closes FE-861 Closes FE-862 Closes FE-866 Closes FE-868 Closes FE-888 Closes FE-899 Closes FE-910 Closes FE-912 Closes FE-919 Closes FE-921 Closes FE-931 Closes FE-932 Closes FE-933 Closes FE-934 Closes FE-935 Closes FE-937 Closes FE-938 Closes FE-939 Closes FE-961 Closes FE-963 Closes FE-964 Closes FE-965 Closes FE-966 Closes FE-1053 Closes https://github.com/hoppscotch/hoppscotch/issues/4978 (Portable version now has disable updates toggle) [caveat: just for Windows] Closes https://github.com/hoppscotch/hoppscotch/issues/4119 (Portable version can choose the current directory as the default storage location) [requested just for Windows] Closes https://github.com/hoppscotch/hoppscotch/issues/3526 (Portable version in general) [requested for Windows] Closes https://github.com/hoppscotch/hoppscotch/issues/4828 (Bug fix + Feature) [applies to all platforms] The desktop app infrastructure established in phase-1 and phase-2 provided path management, data migration, and service architecture. This phase completes the implementation by connecting frontend components to the backend systems and enabling those managers and paths. ## Instance Switching ### Desktop Instance Service The `DesktopInstanceService` is now fully integrated with the frontend instance switcher component. The service provides instance management with the persistent storage layer introduced in phase-1. ```typescript export class DesktopInstanceService extends Service<ConnectionState> { public readonly instanceSwitchingEnabled: boolean = getKernelMode() === "desktop" public async connectToInstance( serverUrl: string, instanceKind: InstanceKind, displayName?: string, options?: Partial<LoadOptions> ): Promise<OperationResult> { // ... } } ``` NOTE: Implementation uses the appload plugin for bundle loading and window management. See those files in file view for better context. The service manages connection state through observables (so it's reactive **enough**, the idea is to rely solely on store, since it's the main source of truth but because there are ways to bypass it when switching, see more details below, this approach works better), handles bundle loading via the appload plugin, maintains recent instances via the persistence layer (see phase-1). Window management during instance switching prevents the multiple window issue identified in earlier development (see https://github.com/hoppscotch/hoppscotch/pull/5381). NOTE: Instance switching functionality is desktop-only. Web platform continues using the no-op `WebInstanceService` implementation. ### Switcher Component The instance switcher component in `hoppscotch-common` now adapts to platform: ```typescript <component :is="platform.instance.customInstanceSwitcherComponent" v-if="platform.instance?.customInstanceSwitcherComponent" @close-dropdown="$emit('close-dropdown')" /> <div v-else-if="isInstanceSwitchingEnabled" class="flex flex-col space-y-1 w-full"> // Default implementation </div> <div v-else class="flex items-center justify-center px-4 py-3"> <span class="text-secondaryLight text-sm"> Instance switching not available </span> </div> ``` The component subscribes to platform-provided observables for connection state, recent instances, and current instance updates. Notice the difference between the prior service-in-common to platform-from-web change. It also now includes better error handling with some new toasts for notifications since instance closing is now handled client-side. In this case `client-side` means the hosted web-app, `host-side` means the underlying machinery run by `plugin-appload`. Web platform sees no instance switching UI. Desktop platform gets full functionality with fallback to default implementation if custom components are unavailable. ### Route Resolution Updated router configuration detects portable mode at runtime and loads appropriate home view: ```typescript { path: "/", name: "home", component: async () => { try { const isPortable = await invoke<boolean>("is_portable_mode") return isPortable ? import("./views/PortableHome.vue") : import("./views/StandardHome.vue") } catch (error) { return import("./views/StandardHome.vue") } }, }, ``` In contrast to what we had prior, a consolidated `Home.vue` file which would actually prevent updater because the Rust updater part that handles portable version would setup before it gets called in the FE causing a perpetual `null` value return by `check()` function. Basically dynamic imports prevent race conditions (not exactly but close enough) with the updater while ensuring correct component loading based on build configuration. Standard mode loads `StandardHome.vue` with all the existing functionality. Portable mode loads `PortableHome.vue` with portable-specific stuff. ### Portable Mode Welcome Flow Implemented first-launch experience for portable mode with information related to what to expect with this version: ```typescript const showPortableWelcome = ref(false) const portableSettings = reactive<PortableSettings>({ disableUpdateNotifications: false, autoSkipWelcome: false, }) const handlePortableWelcomeContinue = async () => { await persistence.setPortableSettings(portableSettings) showPortableWelcome.value = false await loadRecent() } ``` The welcome screen explains portable mode behavior including data isolation from installed versions, manual update requirements, and enterprise deployment constraints. Users can configure update notification preferences and opt to skip the welcome screen in future launches. Welcome screen only appears in portable mode and only on first launch unless re-enabled by user preferences. ### Update Flow Differentiation Standard and portable modes now have distinct update handling: `StandardHome.vue` ```typescript const installUpdate = async () => { try { appState.value = AppState.UPDATE_IN_PROGRESS await updaterClient.downloadAndInstall() } catch (err) { error.value = `Failed to install update: ${err.message}` appState.value = AppState.ERROR } } ``` `PortableHome.vue` ```typescript const checkForUpdatesPortable = async () => { if (portableSettings.disableUpdateNotifications) return try { await updaterClient.checkForUpdates(true) // Shows native dialog } catch (err) { console.error("Error checking for portable updates:", err) } } ``` Essentially: Standard mode: provides automatic download and installation with progress tracking. Portable mode: shows native dialogs for manual download from releases page, respecting enterprise requirements for controlled versioning. Standard mode update behavior remains (mostly) identical. Portable mode adds new manual update workflow without affecting existing installations. ## Backend ### Updater Extended the updater system with commands for different scenarios: ```rust #[tauri::command] pub async fn check_for_updates( app: AppHandle, show_native_dialog: bool, ) -> Result<UpdateInfo, String> { // Platform-specific update checking with native dialog } #[tauri::command] pub async fn download_and_install_update(app: AppHandle) -> Result<(), String> { // Progress tracking with event (for UI updates, see // `PortableHome.vue`) } #[tauri::command] pub async fn is_portable_mode() -> bool { cfg!(feature = "portable") } ``` The updater emits progress events that frontend components can subscribe to. It also handles both portable and standard mode update flows. It also has some error handling with the same event emission mechanics for UI feedback as well. ## App Initialization ### Initialization Composable Created `useAppInitialization()` composable that handles all startup scenarios, the idea is basically this: ```typescript export function useAppInitialization() { const performBasicInitialization = async () => { appVersion.value = await getVersion() await invoke("check_and_backup_on_version_change") await migration.initialize() await persistence.init() } const initialize = async (customLogic?: () => Promise<void>) => { await performBasicInitialization() if (customLogic) { await customLogic() } else { await loadRecent() } } } ``` Initialization flow maintains identical behavior for existing functionality, this is mainly a portable mode extension. ### Migration System Activation The `InstanceStoreMigrationService` now actively migrates data from legacy stores to the new kernel-based system: ```typescript export class InstanceStoreMigrationService { async initialize(): Promise<void> { const isMigrated = await this.isMigrationComplete() if (isMigrated) return await this.performMigration() } private async performMigration(): Promise<void> { await this.ensureDirectoryStructure() await this.migrateDataSafely() await this.migrateHoppscotchStoreFiles() await this.markMigrationComplete() await this.cleanupOldFilesSafely() } } ``` The migration system automatically detects existing data from legacy LazyStore files, transfers connection state and recent instances to kernel store, moves `.hoppscotch.store` files to organized directory structure, and cleans up old files after successful migration. Migration system preserves all existing user data and settings. Users should experience no disruption during the upgrade process. ## ESLint Infra Added ESLint setup matching patterns from `hoppscotch-common` and `hoppscotch-selfhost-web`: ```javascript // .eslintrc.cjs module.exports = { extends: [ "@vue/typescript/recommended", "plugin:prettier/recommended", ], rules: { "@typescript-eslint/no-unused-vars": [ process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" } ], }, } ``` ## Kernel Integration ### Multi-Store Path Resolution Completed kernel store integration with dynamic path resolution: ```typescript const getStorePath = async (): Promise<string> => { try { const storeDir = await getStoreDir() return join(storeDir, STORE_PATH) } catch (error) { console.error("Failed to get store directory:", error) return "hoppscotch-unified.store" } } export const Store = (() => { return { set: async (namespace, key, value, options?) => { const storePath = await getStorePath() return module().set(storePath, namespace, key, value, options) }, // ... other operations ... } })() ``` This is overall a better approach than the isolated calls to `LazyStore` since the kernel store can manage multiple concurrent store instances with different paths for different data types while maintaining all the namespace isolation guarantees we need when switching between contexts. ## Portable Mode Isolation This also implemented complete data boundary isolation between portable and installed versions: ```rust if cfg!(feature = "portable") { let config_dir = path::config_dir()?; } ``` That data isolation makes sure portable versions never access system directories or data from installed versions, to meet the enterprise security requirements for locked-down environments. ## Instance Platform Definition Extended the instance platform definition with operations for instance management: ```typescript export type InstancePlatformDef = { instanceSwitchingEnabled: boolean customInstanceSwitcherComponent?: Component getConnectionStateStream?: () => Observable<ConnectionState> getRecentInstancesStream?: () => Observable<Instance[]> getCurrentInstanceStream?: () => Observable<Instance | null> getCurrentConnectionState?: () => ConnectionState getRecentInstances?: () => Instance[] getCurrentInstance?: () => Instance | null connectToInstance?: ( serverUrl: string, instanceKind: InstanceKind, displayName?: string, options?: Partial<LoadOptions> ) => Promise<OperationResult> // ... other operations } ``` The definition includes lifecycle hooks (`beforeConnect`, `afterConnect`, etc.) and error handlers for platform-specific behavior customization. ## Localization Updates Added instance switcher translations to `en.json`: ```json "instances": { "opening_add_modal": "Opening add instance dialog", "closed_add_modal": "Add instance dialog closed", "connection_cancelled": "Connection cancelled by pre-connect validation", "connecting": "Connecting to instance...", "connected_state": "Successfully connected to instance", "not_available": "Instance switching is not available" } ``` ## Shared View Components Created shared view components in `src/views/shared/`: `AppHeader.vue`: Displays app branding with optional mode indicator `LoadingState.vue`: Shows loading spinner with status message `UpdateFlow.vue`: Handles update UI with progress tracking `ErrorState.vue`: Displays error messages with retry option `VersionInfo.vue`: Shows version and data directory information These components are used by both `StandardHome.vue` and `PortableHome.vue` for consistent UI across modes. ## Header Integration Updated `Header.vue` to use platform-provided instance information: ```typescript <span class="!font-bold uppercase tracking-wide !text-secondaryDark pr-1"> {{ platform.instance.getCurrentInstance?.()?.displayName || "Hoppscotch" }} </span> ``` Removed the service-in-common pattern in favor of platform-provided observables for cleaner separation of concerns. ## Plugin Type Updates Updated `plugin-appload` guest-js types to include `close` operation with proper TypeScript definitions. The close operation allows controlled window management during instance switching. ## Path Import Reorganization Restructured imports in `hoppscotch-selfhost-web` with new alias structure: ```typescript // Application layer (new) "@app/platform": "./src/platform", "@app/services": "./src/services", "@app/components": "./src/components", "@app/kernel": "./src/kernel", // Common package (existing) "@hoppscotch/common": "../hoppscotch-common/src", "@composables": "../hoppscotch-common/src/composables", "@helpers": "../hoppscotch-common/src/helpers", ``` This creates clear boundaries between application-specific code (`@app/*`) and shared common code (`@hoppscotch/common` and legacy `@composables`, `@helpers` aliases). NOTE: Prior to this the loading path difference between portable and standard would prevent either from building. All platform implementation files now use `@app/platform/*` instead of `@platform/*` for consistency. API files moved from `@api/*` to `@app/api/*`. Library utilities moved from `@lib/*` to `@app/lib/*`. ## Main Entry Point Updates Updated `main.ts` to initialize kernel on startup: ```typescript import { initKernel } from "@hoppscotch/kernel" const app = createApp(App) app.use(router) app.mount("#app") initKernel("desktop") ``` The kernel initialization sets up the store system and IO operations before the app mounts. NOTE: This is important to make sure the plugin initializers in the `host-side` are initialized before the kernel components users. Also consolidated platform configuration into a single `PLATFORM_CONFIG` object that defines all platform-specific settings (interceptors, default values, menu items) for both web and desktop modes. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
kerem 2026-03-17 02:40:07 +03:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/hoppscotch#5199
No description provided.