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

🚀 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,
- react app -ipc-> main process
- 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,
- You need to select content, copy, open LLM UI, and paste ...slow
- 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
- get shortcut trigger,
- 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
- Creating multiple Electron windows is more straightforward than using the same Electron window for different UI
- A lot of the AI assistant code was actually co-written by AI itself—I mostly helped debug and refine it.
- We tried the below approaches, but we turned to ask users to copy the content to the clipboard manually first.
- While pressing [⌘] [⌃] [E], try simulating copy—slower and buggy for edge cases.
- 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