Architektur im Überblick
Quelldateien
Diese Seite wurde aus den folgenden Quelldateien erstellt:
- docs/architecture.md
- docs/agent_architecture.md
- src/main.ts
- src/preload.ts
- src/renderer.tsx
- src/ipc/ipc_host.ts
- src/ipc/handlers/import_handlers.ts
- src/ipc/handlers/image_generation_handlers.ts
- src/ipc/handlers/chat_stream_handlers.ts
- src/ipc/handlers/debug_handlers.ts
- .storybook/main.ts
- src/db/index.ts
- src/i18n/index.ts
- testing/fake-llm-server/index.ts
- src/ipc/types/index.ts
- e2e-tests/helpers/utils/index.ts
- e2e-tests/helpers/page-objects/index.ts
- e2e-tests/helpers/page-objects/dialogs/index.ts
- e2e-tests/helpers/page-objects/components/index.ts
- packages/@dyad-sh/react-vite-component-tagger/src/index.ts
Dyad ist eine Electron-basierte Desktop-Anwendung, die als lokale, Open-Source-Alternative zu KI-App-Buildern wie Lovable, v0 und Bolt fungiert. Die Architektur folgt einem klassischen Electron-Muster mit strikter Trennung zwischen privilegiertem Main Process und sandboxed Renderer Process, wobei die Kommunikation über ein typsicheres IPC-System erfolgt.
Electron-Architektur
Die Anwendung basiert auf dem Electron-Framework, das eine Client-Server-ähnliche Struktur innerhalb einer einzigen Desktop-Anwendung implementiert. Der Renderer Process führt den UI-Code (React) aus und ist sandboxed, während der Main Process als Node.js-Prozess mit vollem Dateisystem-Zugriff und Systemressourcen-Berechtigung fungiert. Diese Trennung ähnelt der Kommunikation zwischen Browser und Server über HTTP-Anfragen, verwendet jedoch IPC (Inter-Process Communication) als Transportmechanismus (docs/architecture.md:9-11).
Sicherheitsmodell
Die BrowserWindow-Konfiguration in src/main.ts:255-277 implementiert kritische Sicherheitsfeatures:
typescript1webPreferences: { 2 nodeIntegration: false, 3 contextIsolation: true, 4 preload: path.join(__dirname, "preload.js"), 5}
- nodeIntegration: false: Verhindert direkten Node.js-Zugriff aus dem Renderer
- contextIsolation: true: Isoliert JavaScript-Kontexte zwischen Preload-Script und Renderer
- Preload-Script: Dient als sichere Brücke für IPC-Kommunikation
IPC-Brücke
Das Preload-Script in src/preload.ts:1-25 exponiert eine kontrollierte API an den Renderer:
typescript1contextBridge.exposeInMainWorld("electron", { 2 ipcRenderer: { 3 invoke: (channel: ValidInvokeChannel | string, ...args: unknown[]) => { 4 if ((validInvokeChannels as readonly string[]).includes(channel)) { 5 return ipcRenderer.invoke(channel, ...args); 6 } 7 throw new Error(`Invalid channel: ${channel}`); 8 }, 9 // ... 10 } 11});
Diese Implementierung validiert alle Kanäle gegen Whitelists (VALID_INVOKE_CHANNELS, VALID_RECEIVE_CHANNELS), wodurch nur vordefinierte IPC-Kanäle verwendet werden können. Dies verhindert Injection-Angriffe über dynamische Kanalnamen.
Anfrage-Lebenszyklus
Der Kern-Workflow von Dyad besteht aus drei Phasen: LLM-Request-Konstruktion, Streaming der Antwort zur UI und Verarbeitung der Dyad-Tags. Dieser Zyklus transformiert Benutzereingaben in Code-Änderungen, die in der Vorschau reflektiert werden (docs/architecture.md:13-21).
Phase 1: LLM-Request-Konstruktion
Der LLM-Request umfasst mehr als nur den Benutzereingabe-Prompt. Standardmäßig wird die gesamte Codebase zusammen mit einem detaillierten System-Prompt gesendet, der die KI anweist, in einem spezifischen XML-ähnlichen Format zu antworten (z.B. <dyad-write path="path/to/file.ts">console.log("hi")</dyad-write>).
Phase 2: Streaming zur UI
Die Streaming-Implementierung in src/ipc/handlers/chat_stream_handlers.ts:1000-1039 zeigt die Kernlogik:
typescript1const simpleStreamText = async ({ 2 chatMessages, 3 modelClient, 4 tools, 5 systemPromptOverride = systemPrompt, 6 dyadDisableFiles = false, 7 files, 8}: {...}) => { 9 if (isEngineEnabled) { 10 logger.log("sending AI request to engine with request id:", dyadRequestId); 11 } else { 12 logger.log("sending AI request"); 13 } 14 // Versioned Files für Deep Context 15 let versionedFiles: VersionedFiles | undefined; 16 if (isDeepContextEnabled) { 17 versionedFiles = await getVersionedFiles({ 18 files, 19 chatMessages, 20 appPath, 21 }); 22 } 23 // ... 24};
Ein spezialisierter Markdown-Parser parst die <dyad-*>-Tags, um die LLM-Ausgabe in einer strukturierten UI anzuzeigen statt als reiner XML-Text.
Phase 3: Response-Verarbeitung
Nach Abschluss der LLM-Antwort und Benutzerbestätigung wendet der Response Processor im Main Process die Änderungen an. Jedes <dyad-*>-Tag wird auf spezifische Logik abgebildet: Datei schreiben, Datei löschen, NPM-Paket hinzufügen etc.
Client-Seitige Event-Behandlung
Die Renderer-Seite in src/renderer.tsx:1-50 verwendet React Query für State-Management:
typescript1const queryClient = new QueryClient({ 2 defaultOptions: { 3 queries: { 4 staleTime: 60_000, 5 retry: false, 6 }, 7 mutations: { 8 retry: false, 9 }, 10 }, 11 queryCache: new QueryCache({ 12 onError: (error, query) => { 13 if (query.meta?.showErrorToast) { 14 showError(error); 15 } 16 }, 17 }), 18});
Agent-Architektur
Dyad hat seine Architektur von einer XML-basierten Pseudo-Tool-Calling-Strategie zu einem formalen Tool-Calling-Ansatz weiterentwickelt. Da moderne Modelle insbesondere bei parallelen Tool-Aufrufen deutlich besser geworden sind, ist dieser standardisierte Ansatz nun vorteilhafter und erleichtert das Hinzufügen neuer Tools (docs/agent_architecture.md:1-14).
Design-Entscheidung: XML vs. Tool-Calling
Die ursprüngliche Entscheidung für XML-ähnliche Tags statt formaler Tool-Calling-Fähigkeiten basierte auf zwei Beobachtungen (docs/architecture.md:25-34):
- Parallele Aufrufe: Viele Tools gleichzeitig aufrufen – während einige Modelle parallele Aufrufe unterstützen, tun dies nicht alle
- Code-Qualität: Es gibt Belege, dass das Erzwingen von Code in JSON (was Tool-Calling erfordern würde) die Qualität negativ beeinflusst
Agent-Loop-Implementierung
Das Herzstück des lokalen Agents befindet sich in src/pro/main/ipc/handlers/local_agent/local_agent_handler.ts mit dem Kern-Loop: Die KI wird so lange aufgerufen, bis sie keinen Tool-Aufruf mehr wählt oder die maximale Anzahl von Schritten für den Zug erreicht ist.
Event-Behandlung für Agent-Consent
Die Client-Seite behandelt Agent-Ereignisse in src/renderer.tsx:157-217:
typescript1useEffect(() => { 2 const unsubscribe = ipc.events.agent.onConsentRequest((payload) => { 3 setPendingAgentConsents((prev) => [ 4 ...prev, 5 { 6 requestId: payload.requestId, 7 chatId: payload.chatId, 8 toolName: payload.toolName, 9 toolDescription: payload.toolDescription, 10 inputPreview: payload.inputPreview, 11 }, 12 ]); 13 }); 14 return () => unsubscribe(); 15}, [setPendingAgentConsents]);
Tool-Erweiterbarkeit
Neue Tools werden im Verzeichnis src/pro/main/ipc/handlers/local_agent/tools erstellt, in src/pro/main/ipc/handlers/local_agent/tool_definitions.ts importiert und durch eine React-Komponente in src/components/chat/DyadMarkdownParser.tsx für das Rendering ergänzt.
IPC-Handler-Struktur
Die IPC-Handler sind modular nach Domänen organisiert und werden zentral beim Anwendungsstart registriert. src/ipc/ipc_host.ts:43-82 zeigt alle Handler-Kategorien:
typescript1export function registerIpcHandlers() { 2 registerAppHandlers(); 3 registerChatHandlers(); 4 registerChatStreamHandlers(); 5 registerSettingsHandlers(); 6 registerShellHandlers(); 7 registerDependencyHandlers(); 8 registerGithubHandlers(); 9 registerGithubBranchHandlers(); 10 registerVercelHandlers(); 11 registerNodeHandlers(); 12 registerProblemsHandlers(); 13 registerProposalHandlers(); 14 registerDebugHandlers(); 15 registerSupabaseHandlers(); 16 registerNeonHandlers(); 17 registerLocalModelHandlers(); 18 registerTokenCountHandlers(); 19 registerWindowHandlers(); 20 registerUploadHandlers(); 21 registerVersionHandlers(); 22 registerLanguageModelHandlers(); 23 registerReleaseNoteHandlers(); 24 registerImportHandlers(); 25 registerSessionHandlers(); 26 registerProHandlers(); 27 registerContextPathsHandlers(); 28 registerAppUpgradeHandlers(); 29 registerCapacitorHandlers(); 30 registerAppEnvVarsHandlers(); 31 registerTemplateHandlers(); 32 registerThemesHandlers(); 33 registerPortalHandlers(); 34 registerPromptHandlers(); 35 registerHelpBotHandlers(); 36 registerMcpHandlers(); 37 registerSecurityHandlers(); 38 registerVisualEditingHandlers(); 39}
Registrierung beim App-Start
In src/main.ts:54-56 werden die Handler vor dem App-Ready registriert:
typescript1// Register IPC handlers before app is ready 2registerIpcHandlers();
Beispiel-Handler: Import-Funktionalität
src/ipc/handlers/import_handlers.ts:19-46 zeigt einen typischen Handler:
typescript1export function registerImportHandlers() { 2 // Handler for selecting an app folder 3 handle("select-app-folder", async () => { 4 const result = await dialog.showOpenDialog({ 5 properties: ["openDirectory"], 6 title: "Select App Folder to Import", 7 }); 8 9 if (result.canceled) { 10 return { path: null, name: null }; 11 } 12 13 const selectedPath = result.filePaths[0]; 14 const folderName = path.basename(selectedPath); 15 16 return { path: selectedPath, name: folderName }; 17 }); 18 19 // Handler for checking if AI_RULES.md exists 20 handle("check-ai-rules", async (_, { path: appPath }: { path: string }) => { 21 try { 22 await fs.access(path.join(appPath, "AI_RULES.md")); 23 return { exists: true }; 24 } catch { 25 return { exists: false }; 26 } 27 }); 28}
Datenhaltung und Initialisierung
Dyad verwendet SQLite als lokale Datenbank mit Drizzle ORM als Abstraktionsschicht. Die Datenbank-Initialisierung erfolgt beim Anwendungsstart mit automatischer Migration.
Datenbank-Setup
src/db/index.ts:1-71 implementiert die Datenbank-Factory:
typescript1export function initializeDatabase(): BetterSQLite3Database<typeof schema> & { 2 $client: Database.Database; 3} { 4 if (_db) return _db as any; 5 6 const dbPath = getDatabasePath(); 7 logger.log("Initializing database at:", dbPath); 8 9 // Check if the database file exists and remove it if it has issues 10 try { 11 if (fs.existsSync(dbPath)) { 12 const stats = fs.statSync(dbPath); 13 if (stats.size < 100) { 14 logger.log("Database file exists but may be corrupted. Removing it..."); 15 fs.unlinkSync(dbPath); 16 } 17 } 18 } catch (error) { 19 logger.error("Error checking database file:", error); 20 } 21 22 fs.mkdirSync(getUserDataPath(), { recursive: true }); 23 fs.mkdirSync(getDyadAppPath("."), { recursive: true }); 24 25 const sqlite = new Database(dbPath, { timeout: 10000 }); 26 sqlite.pragma("foreign_keys = ON"); 27 28 _db = drizzle(sqlite, { schema }); 29 30 try { 31 const migrationsFolder = path.join(__dirname, "..", "..", "drizzle"); 32 if (!fs.existsSync(migrationsFolder)) { 33 logger.error("Migrations folder not found:", migrationsFolder); 34 } else { 35 logger.log("Running migrations from:", migrationsFolder); 36 migrate(_db, { migrationsFolder }); 37 } 38 } catch (error) { 39 logger.error("Migration error:", error); 40 } 41 42 return _db as any; 43}
App-Start-Routine
src/main.ts:89-106 zeigt die vollständige Initialisierung:
typescript1export async function onReady() { 2 try { 3 const backupManager = new BackupManager({ 4 settingsFile: getSettingsFilePath(), 5 dbFile: getDatabasePath(), 6 }); 7 await backupManager.initialize(); 8 } catch (e) { 9 logger.error("Error initializing backup manager", e); 10 } 11 initializeDatabase(); 12 13 // Cleanup old ai_messages_json entries to prevent database bloat 14 cleanupOldAiMessagesJson(); 15 16 // Cleanup old media files to reclaim disk space 17 cleanupOldMediaFiles(); 18 // ... 19}
Debug-Funktionalität
src/ipc/handlers/debug_handlers.ts:200-233 implementiert Hilfsfunktionen für Debugging:
typescript1function stripImagesFromAiMessagesJson(json: AiMessagesJsonV6 | null): unknown { 2 if (!json || !json.messages) return json; 3 4 // Work on raw JSON to avoid AI SDK type constraints when modifying content 5 const raw = JSON.parse(JSON.stringify(json)); 6 for (const msg of raw.messages) { 7 if (!Array.isArray(msg.content)) continue; 8 for (let i = 0; i < msg.content.length; i++) { 9 const part = msg.content[i]; 10 if ( 11 part.type === "image" && 12 typeof part.image === "string" && 13 part.image.length > 200 14 ) { 15 msg.content[i] = { 16 ...part, 17 _strippedByteLength: part.image.length, 18 image: "[stripped]", 19 }; 20 } 21 // ... 22 } 23 } 24 return raw; 25}
Systemarchitektur-Übersicht
正在加载图表渲染器...
Erklärung der Architekturkomponenten:
- Renderer Process: Führt React-Komponenten aus, verwaltet UI-State via React Query und rendert Dyad-Tags durch den spezialisierten Markdown-Parser
- Preload Script: Validiert alle IPC-Kanäle gegen Whitelists und exponiert nur die
electron.ipcRenderer-API - Main Process: Verwaltet 40+ IPC-Handler-Kategorien, SQLite-Datenbank und den Local Agent für Tool-Ausführung
- External Services: KI-Modelle für Code-Generierung und Dateisystem für App-Speicherung
Datenfluss für LLM-Anfragen
正在加载图表渲染器...
Erklärung des Datenflusses:
- Prompt-Eingabe: Benutzer sendet Prompt an Renderer Process
- Context-Zusammenstellung: Main Process lädt Codebase und kombiniert mit System-Prompt
- Streaming: LLM-Antwort wird gestreamt und
<dyad-*>-Tags in Echtzeit gerendert - Bestätigung: Benutzer muss Änderungen vor Anwendung bestätigen
- Anwendung: Response Processor führt Tag-spezifische Operationen aus
Modulabhängigkeiten
正在加载图表渲染器...
Erklärung der Abhängigkeiten:
- main.ts: Zentraler Einstiegspunkt, initialisiert Datenbank und registriert alle IPC-Handler
- ipc_host.ts: Aggregiert alle Handler-Module und stellt sie dem Main Process zur Verfügung
- Handler-Module: Jeder Handler ist einer Domäne zugeordnet und kann auf die Datenbank zugreifen
- Renderer: Unabhängig vom Main Process, kommuniziert nur über IPC
Technologie-Stack
| Technologie | Verwendungszweck | Auswahlbegründung | Alternativen |
|---|---|---|---|
| Electron | Desktop-App-Framework | Cross-Platform, Web-Technologien, große Community | Tauri, Neutralino |
| React | UI-Framework | Komponenten-basiert, große Ökosystem, Virtual DOM | Vue, Svelte |
| TanStack Router | Routing | Typsicheres Routing, integrierte Data Loading | React Router |
| React Query | Server State | Caching, Optimistic Updates, DevTools | SWR, RTK Query |
| SQLite | Lokale Datenbank | Embedded, keine Server-Abhängigkeit, ACID | IndexedDB, PouchDB |
| Drizzle ORM | Datenbank-Abstraktion | TypeScript-first, leichtgewichtig, Migrationen | Prisma, TypeORM |
| better-sqlite3 | SQLite-Binding | Synchrones API, hohe Performance | node-sqlite3 |
| i18next | Internationalisierung | React-Integration, Namespaces, Pluralisierung | react-intl |
| PostHog | Analytics | Self-hosted Option, Feature Flags, Session Recording | Mixpanel, Amplitude |
| electron-log | Logging | Multi-Transport, Rotation, Electron-spezifisch | winston, pino |
| Storybook | UI-Entwicklung | Isolierte Komponenten-Entwicklung, Visual Testing | Ladle |
| Vite | Build-Tool | Schnelles HMR, ES Modules, TypeScript-Support | Webpack, esbuild |
Design-Entscheidungen und Kompromisse
1. XML-basiertes Tool-Calling vs. Formales Tool-Calling
Entscheidung: Dyad verwendete ursprünglich XML-ähnliche Tags (<dyad-write>, <dyad-delete>) statt formaler Tool-Calling-APIs.
Begründung (docs/architecture.md:25-34):
- Ermöglicht viele parallele Tool-Aufrufe in einer einzigen Antwort
- Vermeidet Qualitätsprobleme bei Code-in-JSON-Erzwigung
- Bessere Kontrolle über Output-Format
Kompromiss: Weniger standardisiert, erfordert eigenen Parser. Wird mittlerweile zu formalem Tool-Calling migriert.
2. Gesamte Codebase als Context
Entscheidung: Standardmäßig wird die gesamte Codebase mit jeder KI-Anfrage gesendet.
Begründung (docs/architecture.md:44-51):
- Einfachster Ansatz, effektiv für kleine Codebases
- Vermeidet manuelle Dateiauswahl durch Benutzer
- Smart Context Feature filtert automatisch wichtige Dateien
Kompromiss: Höhere Token-Kosten, begrenzt durch Context-Window-Größe. Für große Apps ist manueller Context-Management erforderlich.
3. Begrenzter Agent-Loop
Entscheidung: Dyad verwendet einen relativ einfachen Agent-Loop statt komplexer agenter Workflows.
Begründung (docs/architecture.md:36-42):
- Kosten-Effizienz: Komplexe Agenten können pro Anfrage mehrere Dollar kosten
- Vorhersagbares Verhalten: Meist nur eine LLM-Anfrage pro Benutzer-Interaktion
- Auto-Fix für TypeScript-Fehler ist optional aktivierbar
Kompromiss: Weniger automatische Problemlösung im Vergleich zu Tools wie Cursor oder Claude Code.
4. Electron vs. Native Alternativen
Entscheidung: Electron als Framework statt nativer Lösungen.
Begründung:
- Web-Technologien (React, TypeScript) ermöglichen schnelle Entwicklung
- Cross-Platform-Support (macOS, Windows, Linux) aus einer Codebase
- Große Ökosystem-Unterstützung und Community
Kompromiss: Höherer Speicherverbrauch und größere App-Größe im Vergleich zu nativen Apps.
5. Lokale SQLite-Datenbank
Entscheidung: SQLite als lokale Datenbank statt Cloud-basierter Lösungen.
Begründung (src/db/index.ts:1-71):
- Keine Netzwerkabhängigkeit, funktioniert offline
- Einfache Installation, kein separater Server
- ACID-Transaktionen für Datenintegrität
Kompromiss: Keine eingebaute Synchronisation zwischen Geräten, Backup muss manuell verwaltet werden.
6. Context Isolation und Sicherheitsmodell
Entscheidung: Strikte Trennung zwischen Renderer und Main Process mit Context Isolation.
Begründung (src/main.ts:255-277):
- Verhindert Prototype-Pollution-Angriffe
- Isoliert Preload-Script vom Renderer-JavaScript
- Erlaubt selektive API-Exposition über contextBridge
Kompromiss: Komplexere IPC-Kommunikation, alle Systemaufrufe müssen durch Main Process geleitet werden.
7. Typsichere IPC-Kanäle
Entscheidung: Whitelist-basierte Kanal-Validierung im Preload-Script.
Begründung (src/preload.ts:1-25):
- Verhindert dynamische Kanal-Injection
- Compile-Time-Typprüfung für Kanäle
- Explizite Deklaration aller erlaubten Kommunikation
Kompromiss: Weniger Flexibilität, neue IPC-Kanäle müssen explizit registriert werden.
Startup-Sequenz
Die Initialisierung von Dyad folgt einer definierten Sequenz:
- Umgebungsvariablen laden:
dotenv.config()in src/main.ts:52 - IPC-Handler registrieren:
registerIpcHandlers()vor App-Ready in src/main.ts:55 - Git-Verzeichnis auflösen: Platform-spezifische Git-Konfiguration in src/main.ts:63-76
- Protocol-Handler registrieren:
dyad://Deep Links in src/main.ts:79-87 - App-Ready-Handler:
- Backup-Manager initialisieren
- Datenbank initialisieren und Migrationen ausführen
- Cleanup-Routinen für alte Daten
- Git Safe Directory konfigurieren
- BrowserWindow erstellen
- Auto-Update prüfen (falls aktiviert)
Erweiterbarkeit
Neue IPC-Handler hinzufügen
- Neue Datei in
src/ipc/handlers/erstellen - Handler-Funktion mit
handle()odercreateTypedHandler()registrieren - Kanal zu
VALID_INVOKE_CHANNELSoderVALID_RECEIVE_CHANNELShinzufügen - Handler in
registerIpcHandlers()in src/ipc/ipc_host.ts aufrufen
Neue Agent-Tools hinzufügen
- Tool in
src/pro/main/ipc/handlers/local_agent/tools/erstellen - Tool in
src/pro/main/ipc/handlers/local_agent/tool_definitions.tsimportieren - React-Komponente für Rendering in
src/components/chat/DyadMarkdownParser.tsxerstellen - E2E-Test in
e2e-tests/local_agent*.spec.tshinzufügen
