Markdown Converter
Agent skill for markdown-converter
Witsy is a cross-platform Electron-based desktop AI assistant that serves as a universal MCP (Model Context Protocol) client. Built with Electron, TypeScript, Vue 3, and Vite, it integrates multiple LLM providers and supports features like chat completion, image generation, speech-to-text, text-to-s
Sign in to like and favorite skills
Witsy is a cross-platform Electron-based desktop AI assistant that serves as a universal MCP (Model Context Protocol) client. Built with Electron, TypeScript, Vue 3, and Vite, it integrates multiple LLM providers and supports features like chat completion, image generation, speech-to-text, text-to-speech, document search (RAG), and automation capabilities.
src/main/): Electron main process handling system integration, IPC, and native APIssrc/renderer): Vue 3 frontend with Vite bundlingsrc/preload.ts): Secure IPC bridge between main and renderersrc/renderer/services/llms/): Multi-provider LLM abstraction layer using multi-llm-tssrc/renderer/services/plugins/): Extensible tools for search, filesystem, python execution, etc.src/main/automations/): Cross-platform automation for "Prompt Anywhere" and AI commandsnpm start - runs with hot reloadnpm test (Vitest unit tests), npm run test:e2e (E2E tests)make mac-arm64, make win-x64, etc. - platform-specific builds via Makefileforge.config.ts handles Electron Forge setup, Vite configs handle bundlingIn all cases, implementation should be done in small increments: code, lint, test. Always break down tasks into small, manageable pieces. This allows for easier debugging and testing, and ensures that the codebase remains stable. At the end of each task, ensure that the code is properly tested and linted before moving on to the next task. Confirm with the user that the task is complete and that they are satisfied with the implementation and that you can move on to the next task.
Tests should be written or updated as soon as possible and kept passing before moving on to the next task. This ensures that the codebase remains stable and maintainable. Always assume when starting a new feature, that the code is already in a good state and that all existing tests are passing. Be mindful of that when fixing non-passing tests.
Linting can be done with
npm run lint, which will also check for Vue type errors. It is mandatory to run this command before running tests to ensure code quality. When this command produces no output, it means that the code is properly formatted and does not contain any linting errors.
Tests can be run using
npm run test:ai -- followed by the test name or path.
Never run end-to-end tests during your process. Never try to build the application during your process. Never try to run the application during your process.
In the renderer process, the state is managed through the store object:
import { store } from '@services/store'
This store is a Vue 3 reactive object that holds the application state, including user preferences, conversation history and other relevant data.
// Centralized config in src/types/config.ts with backwards compatibility export type Configuration = { engines: Record<string, EngineConfig>, // LLM provider configs plugins: Record<string, PluginConfig>, // Plugin settings // ... other sections }
// Organized constants in src/ipc_consts.ts export const CHAT = { STREAM: 'chat-stream', CANCEL: 'chat-cancel' } as const // Main process handlers in src/main/ipc.ts // Preload exposures in src/preload.ts
IPC Events (main→renderer): Use
emitIpcEvent/emitIpcEventToAll/emitIpcEventToFocused from main and the useIpcListener composable in renderer. Handlers are auto-cleaned on component unmount.
import useIpcListener from '@composables/ipc_listener' const { onIpcEvent } = useIpcListener() onIpcEvent('docrepo-modified', (data) => { /* handle */ })
Bus Events (renderer↔renderer): Use
useEventBus for cross-branch communication between Vue components. Prefer Vue emit/provide-inject for parent-child communication.
import useEventBus from '@composables/event_bus' const { onBusEvent, emitBusEvent } = useEventBus() onBusEvent('fullscreen', (data) => { /* handle */ }) emitBusEvent('new-chat', payload)
If you encounter
window.api.* lint errors, add the method signature to declare global { interface Window { in src/types/index.ts.
The project includes a variety of CSS classes available globally:
Make sure you use globally available CSS variables and don't create your own styles.
CSS variable usage is validated with Stylelint. Run
npm run lint:css to check that all var(--...) references use defined variables. The linter imports variable definitions from css/variables.css and css/index.css.
tests/unit/ using Vitest + Vue Test UtilsLlmMock classtests/e2e/ using PlaywrightWhen writing tests, prioritize testing user interactions by triggering HTML events and then checking state updates, whether on the UI itself or updats to store. As much possible avoid injecting data in Vue
vm and call methods on it. We want to test as if the user is interacting with the application, so prefer using trigger or setValue to simulate user actions.
Never use simulated
wait statements using an "await Promise" pattern. In most cases awaiting nextTick should be sufficient to ensure the UI is updated before assertions. If really needed you can use vi.waitFor or vi.waitUntil to wait for a specific condition, but this should be avoided unless absolutely necessary and always using a reasonable timeout.
All the IPC methods are mocked in
tests/mocks/window.ts. In most cases, if you need to test IPC calls, you should
import { useWindowMock } from '@tests/mocks/window'
and use the
windowMock in beforeAll (or maybe beforeEach).
Witsy is localized using
vue-i18n. All translations are stored in ./locales/*.json and can be added or modified as needed. The main language is English, and other languages can be added by creating new JSON files in the locales directory. Only add translation for English when creating a new feature. If asked to add translations for other languages you can run ./tools/i18n_check.ts --fix but never run this based on your own initiative.
In the renderer process, always
import { t } from '@services/i18n'.
All the CSS variables are defined in
./css/index.css. Use those variables and only those variables. Do not come up with new variables or use hardcoded values. If you need to add a new variable, please discuss it with the team first.
npm run test:ai # Unit tests npm run test:ci # With coverage
To identify files with the most uncovered lines and prioritize testing efforts:
node tools/coverage_gaps.js # Show top 20 files with most uncovered lines node tools/coverage_gaps.js --limit 10 # Show top 10 files node tools/coverage_gaps.js --filter src/components # Filter to specific directory node tools/coverage_gaps.js --show-lines # Show which lines are uncovered
This script runs coverage analysis and outputs files sorted by absolute number of uncovered lines, making it easy to identify the biggest testing gaps. Unlike percentage-based coverage, this helps find files where adding tests will have the most impact (e.g., a 1000-line file at 80% coverage has more uncovered code than a 10-line file at 0%).
Use
--show-lines to see exactly which line numbers are not covered, formatted as ranges (e.g., "132,361-367,372-375").