@vsuite/ui
vsuite desktop app (Electron + electron-vite, React, Tailwind CSS 4 + daisyUI 5). It manages a list of projects and runs the same workflows as the CLI (init, generate, migrate, graph, stack and config edits) through @vsuite/workflows. The package is private and not published; macOS installers are ad-hoc signed, Windows and Linux installers are unsigned.
Features
Each project opens in tabs: Overview, Stack, Agents, Skills, Packs, MCP & capabilities, Targets, Models, Preferences, Metrics (the job log in .vsuite/agent-metrics/jobs.jsonl), Graph (the agent call graph, with export), Preview (the exact generated Markdown), and Raw JSON. Edits are kept as a draft until you save; generating writes the same files as vsuite generate.
New project opens a wizard (Folder, Targets, Stack, Agents, Skills, Capabilities, Preferences, MCP env, Models, Review). Its optional AI assistance step sends a project description and detected context to an Ollama server (by default http://localhost:11434, configurable in settings) and marks the suggested agents, skills, and tools. Add existing registers a folder that already has vsuite.json, or starts the wizard for one that does not.
Screenshots of these features are in the root README.
Scripts
Run from the repository root:
| Script | What it does |
|---|---|
pnpm run ui:dev |
Start electron-vite in development (hot reload for the renderer). |
pnpm run ui:build |
Build main, preload, and renderer bundles to packages/ui/out/. |
pnpm run ui:dist |
Build, then package installers with electron-builder into packages/ui/release/. |
pnpm run ui:e2e |
Playwright smoke tests against the built app. |
Also: pnpm --filter @vsuite/ui run typecheck; unit tests run in the @vsuite/ui Vitest project.
Inspecting the app in development
pnpm run ui:dev (from the repository root) or pnpm --filter @vsuite/ui dev starts electron-vite: it bundles the main and preload sources, serves the renderer from Vite, and launches Electron. The renderer hot-reloads on change.
Built-in Chromium DevTools
Electron bundles the same DevTools front end as Chrome. Open it with Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows/Linux), or from the app menu View → Toggle Developer Tools. The Network panel shows renderer traffic only: Vite dev-server module loads, the HMR WebSocket, and renderer image/font requests.
Attach external Chrome
Start the app with the remote debugging port:
pnpm --filter @vsuite/ui dev --remoteDebuggingPort=9222Then open chrome://inspect in Chrome and choose inspect next to the app target, or open http://localhost:9222 to list targets. The equivalent raw Chromium flag is pnpm --filter @vsuite/ui dev -- --remote-debugging-port=9222.
Debug the main process
The renderer Network panel never shows main-process traffic. Renderer ↔ main communication is IPC (window.vsuite.invoke), not HTTP, and outbound HTTP from main (for example the JEV client’s Node fetch) is invisible in any renderer panel. To debug main, enable the V8 inspector:
pnpm --filter @vsuite/ui dev --inspect --sourcemap--inspect listens on port 5858 by default (--inspect=9229 changes it); --inspectBrk pauses on the first line instead. --sourcemap emits source maps so breakpoints and stack traces resolve to the TypeScript sources. Attach to the Node target from chrome://inspect.
To inspect a specific request, attach the Node target, set a breakpoint in src/main/features/jev/client.ts (the private SystemOneClient.json method used by listModels, testConnection, and recommend), and step through the request and response. Main-process output—including anything logged around the request—appears in the same terminal that runs pnpm --filter @vsuite/ui dev.
Pass Electron flags
Flags for Electron itself go after --, which electron-vite forwards to the app:
pnpm --filter @vsuite/ui dev -- --trace-warningselectron-vite’s own flags (--inspect, --remoteDebuggingPort, --sourcemap) go before that --.
Process model
| Process | Source | May import @vsuite/* |
Notes |
|---|---|---|---|
| Main | src/main |
Yes, at runtime: workflows, config, catalog, detection, packs |
Owns the file system, SQLite, dialogs, the watcher, and every IPC handler. electron-vite inlines @vsuite/* into the main bundle. |
| Preload | src/preload |
Types only | Exposes window.vsuite (invoke, on) via contextBridge; rejects channels not listed in src/shared/channels.ts. |
| Renderer | src/renderer |
Types only | React UI. No Node access. |
| Shared | src/shared |
Types only | Channel list, IPC contract (zod), Result envelope. |
BrowserWindow uses contextIsolation: true, nodeIntegration: false, and sandbox: true. The renderer is served with a strict Content-Security-Policy (default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none'; see contentSecurityPolicy in src/main/window.ts).
IPC contract and Result envelope
src/shared/channels.tslists every invoke and event channel;src/shared/ipc-contract.tsdefines a zod input and output schema per channel. Main validates input before calling the handler.- Every invoke resolves to a
Result<T>(src/shared/result.ts):{ ok: true, data }or{ ok: false, error: { code, message, details? } }. Codes:VALIDATION,NOT_FOUND,CONFLICT,BUSY,IO,CANCELLED,INTERNAL. Handlers never leak exceptions across the bridge; they throwAppError(code, …)to choose a code. - Config validation failures return
VALIDATIONwithdetails.issues({ path, message }[]), which forms map onto fields.
Security
- Trusted folders. Renderer-supplied write targets (create project, add existing) must be folders main itself returned from a native picker within the last 30 minutes (
src/main/trusted-folders.ts). TheuserDatafolder is never accepted as a project. - Per-project write lock. Every write on a project (save, generate, migrate, stack, create over an existing row) runs through one single-flight key per project (
busy/busyFolderinsrc/main/features/shared.ts); a concurrent request returnsBUSYnaming the running action. - Writes to project files use the core atomic write plan.
Feature modules
Features are folders plus one registry line:
- Main:
src/main/features/<name>/index.tsexports aMainFeature({ name, handlers }) keyed by channel. Register it inmainFeaturesinsrc/main/features/registry.ts. Registration fails on unknown channels and on a channel owned by two features. - Renderer:
src/renderer/features/<name>/index.tsexports aRendererFeaturecontributing project tabs and/or wizard steps (each with a uniqueidand anorder). Register it inrendererFeaturesinsrc/renderer/features/registry.ts. Duplicate ids throw.
To add a feature:
- Add channel names to
src/shared/channels.tsand their input/output schemas tosrc/shared/ipc-contract.ts. - Create
src/main/features/<name>/with handlers (and tests); add it tomainFeatures. - Create
src/renderer/features/<name>/with its components (and tests); add it torendererFeatures. - Keep renderer, preload, and shared imports of
@vsuite/*type-only (see the boundary check in docs/architecture.md).
Packs and preview
Packs are handled by the packs and preview features (main and renderer), which call the same @vsuite/workflows and @vsuite/packs code as the CLI; preview and generate share one render path. See the pack author guide.
- Packs tab: add a pack (folder picker, npm, or git), remove it, update it, and see its locked version and integrity status.
- Agents and skills: each entry shows a source badge (catalog or
pack:<name>). Where a pack overrides a built-in, a Definition select switches between them. Pack-only entries appear in a From packs group. - Per-target models: the Models tab (per-agent rows) and the Agents tab (the active profile’s model field) edit a model as either a single string or a per-target object. Use Per-target to switch to the object form, which shows a
Defaultinput plus one input per selected target, and Single to collapse back (keeping thedefault, else the first set target). The object keys aredefault,opencode,claude-code, andgithub-copilot; a target resolves to its own key, elsedefault. OpenCode values must beprovider/model-id(for examplegithub-copilot/claude-opus-5.5). “Apply to all agents” stays single-model only and converts any per-target object to a string. - Preview tab: read-only. Shows the exact generated Markdown for the selected agent or skill and target, marks auto-appended required sections, lists diagnostics with file and line, and offers copy and refresh. When an
npm:/git:pack no longer matchesvsuite.lock.json, the preview shows an integrity notice instead of rendering, asgeneratedoes. - Dirty drafts: pack mutations (add, remove, update) are disabled while the config draft has unsaved changes; save or discard first.
App data
- Database:
<userData>/app.db, opened with Node’s built-innode:sqlite. - Tables:
projects(id,pathunique,name,added_at,last_opened_at,pinned),settings(key,value), andschema_migrations(version,applied_at). - Migrations: ordered
{ version, sql }entries insrc/main/db/migrations.ts, applied at startup, each in its own transaction, and recorded inschema_migrations. Add a new entry with the next version; never edit an applied one. VSUITE_USER_DATA: an absolute path that overrides Electron’suserData(applied beforeapp.whenReady()), so tests and e2e runs never touch the real profile.
Platforms and code signing
Released installers are published at dl.vedrankrizic.com/vsuite: latest/ holds the newest release and <tag>/ (for example v0.7.1/) each release, both with SHA256SUMS. ui:dist produces macOS dmg/zip, Windows nsis, and Linux AppImage/deb installers named vsuite-<version>-<os>-<arch>.<ext>. CI builds them on macOS, Windows, and Ubuntu (.github/workflows/ui.yml). The macOS build is ad-hoc signed (identity: "-"), so it runs on the machine that built it, but it has no Developer ID certificate or notarization and is not distributable to other machines; Windows and Linux builds are unsigned. There is no auto-update:
- macOS: Gatekeeper blocks the first launch. Open System Settings → Privacy & Security and choose Open Anyway (or right-click the app → Open).
- Windows: SmartScreen shows “Windows protected your PC”. Choose More info → Run anyway.
Build hardening
ui:dist obfuscates the shipped JavaScript before packaging: the main bundle and the renderer’s app-owned chunks (index-*.js, chunk-*.js) are run through javascript-obfuscator; third-party vendor/diagram chunks and the preload are left as-is. Obfuscation runs only in the packaging path, never in dev or the non-obfuscated ui:e2e run. No sourcemaps ship; the obfuscation script fails the build if any appear.
The packaged app flips Electron fuses (runAsNode, enableNodeOptionsEnvironmentVariable, enableNodeCliInspectArguments off; enableEmbeddedAsarIntegrityValidation, onlyLoadAppFromAsar on). Flipping fuses patches Electron Framework.framework and invalidates its pre-existing ad-hoc signature, so macOS builds are ad-hoc signed (identity: "-") to re-sign the framework afterward. Asar integrity validation is supported on macOS and Windows only; Linux keeps the other fuses but has no asar integrity.