SwitchV: Streamlining Developer Workflow with an Open Source VS Code Launcher

Grimmer Kang @Taiwan | Fireflies.AI

2025.03 @FOSSASIA Summit 2025

GitHub repo: grimmerk/switchv

🚀 Features

  • Instant project switching: Quickly launch the right VS Code window (macOS app).
  • AI integration (alpha): CodeV powered by Anthropic API
    • No subscription limits, bring your own API key
    • Easily customizable prompts and behavior
    • Privacy first: Manage your data locally and privately

🛠 Technical Learnings

    •    Building Spotlight-like apps with Electron framework

    •    Publishing Electron apps to the macOS App Store

    •    Integrating LLM APIs into desktop applications

Agenda 

Problem 1

You are switching to another project in VS Code. How?

  • Use built-in open recent UI (via ctrl+r) Too Small !!
  • Use ⌘+~ to navigate to the previous one? What if it is not opened now

Problem 2

You are launching some VS Code project folder when VS Code is not launched. 

  • use terminal? You need to remember and type correctly and quickly
  • move your cursor on VS Code to show the open recent list? Too Slow!!

Solution: Just press  + Ctrl + R to quickly launch the SwitchV launcher.

Instanly

Recent Opened list, order by time, item could be outside working directory

Preload list

Menu bar icon, clicking it would app SwitchV too

Technical implementation 

VS Code extension

Electron UI renderer process - Chromium

Electron main process

  • managing app lifecycle
  • Node.js, Electron C++ runtime

VS Code

listening opened window event

Send opened window record to to NestJS server in main process of SwitchV app

NestJS using Prisma to save info. to sqlite (preference, recent open window path)

IPC (shared memory based)

React UI

OS Native UI

/API: Menu bar

When invoking the shortcut, react app request the list from server in electron,
When clicking one project item,

  1. react app -ipc-> main process
  2. use child_process' exec(`open code://file/${path}`) or exec(`open -b ${bundleId} ${path}`) to open VS Code
/** webpack.main.config.ts : entry: './src/main.ts' 
/** main.ts: using electron": 29.4.6 */ 
import { app, BrowserWindow, ipcMain, Tray, Menu } from 'electron'

// window (Chromium)
const createSwitcherWindow = (): BrowserWindow => {
  const window = new BrowserWindow({
    height: 600,
    width: 800,
    webPreferences: {
      // webpack entry, inject by forge
      preload: SWITCHER_WINDOW_PRELOAD_WEBPACK_ENTRY, 
      devTools: true,
    },
  });
};  

// app
app.dock.hide();
app.once('before-quit', () => {
  // clean up resources
});

// macOS menu bar  
class TrayGenerator {
  this.tray = new Tray(icon); // in some method
  const menu = { /* */ }
  this.tray.popUpContextMenu(Menu.buildFromTemplate(menu));
}

(async () => {
  await app.whenReady(); // equivalent to old  app.on('ready)
  // some setup, e.g. createSwitcherWindow(), new TrayGenerator 
})();  
/** webpack.renderer.config.ts: 
 * some normal webpack compile setting */

/** switcher-renderer.ts content: */
import './switcher-ui'; 

/** switcher-ui.tsx (react app) */ 
import { FC, useCallback, useEffect, useRef, useState } from 'react';
// ...
const config: ForgeConfig = {
  plugins: [
   new WebpackPlugin({
    // from webpack.main.config.ts
    mainConfig: mainConfig
    // from webpack.renderer.config.ts
    renderer: rendererConfig,
    entryPoints: [
          {
            html: './src/switcher.html',
            js: './src/switcher-renderer.ts',
            name: 'switcher_window',
            preload: {
              js: './src/preload.ts',
            },
    

main.ts

forge.config.ts

(Electron Forge all-in-one tool)

UI entry files

IPC (Inter-process communication)

renderer -> main

main-> renderer

// preload.ts where we set up IPC events
contextBridge.exposeInMainWorld('electronAPI', {
  // react -> main.ts 
  // swithcer-ui.tsx: in popup event handler, calls
  //    (window as any).electronAPI.openFolderSelector()
  // main.ts: register  
  //    ipcMain.on('open-folder-selector', async (event) => {
  //    const result = await dialog.showOpenDialog({ 
  openFolderSelector: () => ipcRenderer.send('open-folder-selector')
  
  
  // main -> react 
  // main.ts
  //   switcherWindow.webContents.send('folder-selected', folderPath);
  // switcher-ui.tsx register
  //   (window as any).electronAPI.onFolderSelected(
  //     async (_event: any, folderPath: string) => {
           //if (!folderPath) {
           // return;        
  onFolderSelected: (callback: any) => // we can add TS type !
    ipcRenderer.on('folder-selected', callback),  

  //...  
}

key notes 

1. Background app: do not call `window.show()`
2. DB: Using Prisma to get flexibility for easily migrating to cloud DB
3. macOS App version: Use `version` field of `package.json` to represent
4. Auto DB migration: 
  1. Read app version while starting up, 
  2. then compare it with the version stored in `${app.getPath('userData')}/dbUsedVersion.txt`, 
      1. if not the same and not exist, execute Prisma migrate 
          1. (important!) use `import { fork } from 'child_process')` to let Prisma migration using Electron's node              
      2. if the same, not migrate
  3. Sync app version with the dbUsedVersion.txt
  4. You need to specifcy the SQLite path and Prisma binary path in the packaged app. e.g., 




const resourcePath = path.resolve(`${app.getAppPath()}/../`);
process.env.PRISMA_MIGRATION_ENGINE_BINARY  = `${resourcePath}/migration-engine-darwin`

process.env.DATABASE_URL = `file:${app.getPath('userData'}/${dbFileName}`;

Tip1: Use Prisma Studio to debug DB: npm run db:view, which opens http://localhost:5555/

Tip 2: Electron lifecycles need to be handled well. Window close, widow hide, or creating window without showing to speed up

Packaging and publishing

1. Set up app name, requested permission in the `parent.plist` and `child.plist` (below ref1)  
2. Execute `npm run make_mas` to build switchv.app: `electron-forge make --platform=darwin -- --mas`
    1. invokes prebuild.sh: cp files you additinally use. Prisma binary files and index.js file  
3. Get macOS app embedded.provisionprofile on Apple developer site and put it the project root 
4. Get the cerficate. ref: https://www.electronjs.org/docs/latest/tutorial/code-signing#signing--notarizing-macos-builds
5. Execute `sign.sh` (customize it for your need, e.g. app name, cerficate name)
    1. copy the provisionprofile
    2. use `plutil` to update `ElectronTeamID` Contents/Info.plist`
    3. `codesign` for the Electron built-in binary files and your Prisma binary files
    4. `productbuild` (app -> pkg) 
6. Submit the pkg to app store (or TestFlight first). 
7. if you are not submitting to app store but want to share the app with others, one more step: **notarization**, 
https://www.electronjs.org/docs/latest/tutorial/code-signing
ref1: key>com.apple.security.files.user-selected.read-write</key> 

Debugging tips of a packaged app (and also MAS built ver. for app store):

Tip 1: Inject the log in the menu bar app icon title, like.               SwitchV(project_log:...).

Tip 2 (not for MAS): You can execute the built-in final executable file (e.g. "switchv/out/SwitchV-darwin arm64/SwitchV.app/Contents/MacOS/SwitchV" to check log, rather than clicking the built-in macOS app.
Tip 3: devtool.

I had tried to bundle an individual NestJS node.js program in electron, but the takeaway is the increased overhead and complexity resulted in it being harder to package the whole app

One more thing

  • Cursor integration is done

  • use VS Code and Cursor built-in sqlite instead of the workaround way (extension->server->our_own_sqlite), borrowing the idea from Raycast 

Comparisons with

Raycast Search Recent Project for vscode/cursor

  • Raycast

    • Pros:

      • A lot of out-of-box extensions, and extensible

    • Cons: 
      • core source is not open-source (not 100% customizable) 

      • smaller UI
      • one more step (use shortcut to launch raycast, select vscode/cursor search command, then select item)
      • no pre-loaded working folder

 

AI Assistant features powered by LLM

SwitchV -> CodeV 

Problem 3: 

When you want to feed some code or text content to LLM AI to get some feedback or analysis, 

 

  1. You need to select content, copy, open LLM UI, and paste ...slow
  2. Or, you can select content, right mouse click, and trigger some pre-defined AI quickly, but this is limited to specific AI tools/IDEs. ...limited 

Solution: AI Assistant (alpha)

  • Define your LLM API key on menu setting
  • Use default prompt or define your prompt
  • Flow
    • Select code or text
    • copy it to clipboard via [⌘] [C]
    • Trigger [⌘] [⌃] [E] for Insight Chat mode
    • Streaming AI results
    • Ask follow - up question !!

auto-scroll

Insight Split mode

Selection Chat mode

Settings

Custom insight prompt

const stream = await this.anthropic.messages.create({
  model: 'claude-3-7-sonnet-20250219',
  max_tokens: 4000,
  messages: [
    {
      role: 'user',
      // // selected code/text
      content: prompt, 
    },
  ],
  stream: true,
});

for await (const chunk of stream) { // JavaScript AsyncIterator
  //..
  window.webContents.send('detected-language', detectedLanguage);
  window.webContents.send(
    'ai-assistant-insight-chunk',
     chunk.delta.text,
  );
  //..
}

Technical implementation 

AnthropicService.ts in main process

main.ts in main process

  1. get shortcut trigger, 
  2. read clipboard 

renderer process (ai-assistant-ui.tsx)

- get streaming resp and show it

One more thing:  

Trigger [⌘] [⌃] [C] for Smart Chat mode

Chat/Insight history

You can use [⌘][n] to

open a new chat quickly

🛠 Technical Learnings while implementing AI assistant

  1. Creating multiple Electron windows is more straightforward than using the same Electron window for different UI
  2. A lot of the AI assistant code was actually co-written by AI itself—I mostly helped debug and refine it.
  3. We tried the below approaches, but we turned to ask users to copy the content to the clipboard manually first.
    1. While pressing [⌘] [⌃] [E], try simulating copy—slower and buggy for edge cases.
    2. Use macOS accessibility API to get the selected text: It requires handling one case by case on different focused macOS apps (and its windows).

✅ What we learned

  • ☑️ Building a simple, efficient VS Code project switcher.
  • ☑️ Integrating AI smoothly into everyday dev workflow.
  • ☑️ Enhancing developer productivity through thoughtful UX.
  • ☑️ Publishing Electron apps to the macOS App Store。                                                  


🔭 What’s next? (Future improvements)

  • Support more IDE (e.g. Cursor) for quick swither
  • Keep improving UI and UX (e.g. customize shortkey)
  • Better customization for prompts (or even prompt caching), AI behaviors (e.g. search history, export, train your agent), and AI backends (local models, GPT, etc.)
  • Add more use cases beyond code—like template translation, text summarization, etc.
  • Enhance cross-platform compatibility beyond macOS.
  • Community-driven plugins and extensions.

Thank You!  

Special thanks to:

• Vivy for app icon design

• Fireflies.AI for support

• FOSSASIA Summit 2025 organizers and community

 

GitHub: grimmerk/switchv

Contact: https://linkedin.com/in/grimmerk

SwitchV: Streamlining Developer Workflow with an Open Source VS Code Launcher

By Grimmer

SwitchV: Streamlining Developer Workflow with an Open Source VS Code Launcher

  • 50