Preise

Architektur im Überblick

Quelldateien

Diese Seite wurde aus den folgenden Quelldateien erstellt:

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:

typescript
1webPreferences: {
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:

typescript
1contextBridge.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:

typescript
1const 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:

typescript
1const 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):

  1. Parallele Aufrufe: Viele Tools gleichzeitig aufrufen – während einige Modelle parallele Aufrufe unterstützen, tun dies nicht alle
  2. 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.

Die Client-Seite behandelt Agent-Ereignisse in src/renderer.tsx:157-217:

typescript
1useEffect(() => {
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:

typescript
1export 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:

typescript
1// 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:

typescript
1export 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:

typescript
1export 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:

typescript
1export 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:

typescript
1function 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:

  1. Renderer Process: Führt React-Komponenten aus, verwaltet UI-State via React Query und rendert Dyad-Tags durch den spezialisierten Markdown-Parser
  2. Preload Script: Validiert alle IPC-Kanäle gegen Whitelists und exponiert nur die electron.ipcRenderer-API
  3. Main Process: Verwaltet 40+ IPC-Handler-Kategorien, SQLite-Datenbank und den Local Agent für Tool-Ausführung
  4. External Services: KI-Modelle für Code-Generierung und Dateisystem für App-Speicherung

Datenfluss für LLM-Anfragen

正在加载图表渲染器...

Erklärung des Datenflusses:

  1. Prompt-Eingabe: Benutzer sendet Prompt an Renderer Process
  2. Context-Zusammenstellung: Main Process lädt Codebase und kombiniert mit System-Prompt
  3. Streaming: LLM-Antwort wird gestreamt und <dyad-*>-Tags in Echtzeit gerendert
  4. Bestätigung: Benutzer muss Änderungen vor Anwendung bestätigen
  5. Anwendung: Response Processor führt Tag-spezifische Operationen aus

Modulabhängigkeiten

正在加载图表渲染器...

Erklärung der Abhängigkeiten:

  1. main.ts: Zentraler Einstiegspunkt, initialisiert Datenbank und registriert alle IPC-Handler
  2. ipc_host.ts: Aggregiert alle Handler-Module und stellt sie dem Main Process zur Verfügung
  3. Handler-Module: Jeder Handler ist einer Domäne zugeordnet und kann auf die Datenbank zugreifen
  4. Renderer: Unabhängig vom Main Process, kommuniziert nur über IPC

Technologie-Stack

TechnologieVerwendungszweckAuswahlbegründungAlternativen
ElectronDesktop-App-FrameworkCross-Platform, Web-Technologien, große CommunityTauri, Neutralino
ReactUI-FrameworkKomponenten-basiert, große Ökosystem, Virtual DOMVue, Svelte
TanStack RouterRoutingTypsicheres Routing, integrierte Data LoadingReact Router
React QueryServer StateCaching, Optimistic Updates, DevToolsSWR, RTK Query
SQLiteLokale DatenbankEmbedded, keine Server-Abhängigkeit, ACIDIndexedDB, PouchDB
Drizzle ORMDatenbank-AbstraktionTypeScript-first, leichtgewichtig, MigrationenPrisma, TypeORM
better-sqlite3SQLite-BindingSynchrones API, hohe Performancenode-sqlite3
i18nextInternationalisierungReact-Integration, Namespaces, Pluralisierungreact-intl
PostHogAnalyticsSelf-hosted Option, Feature Flags, Session RecordingMixpanel, Amplitude
electron-logLoggingMulti-Transport, Rotation, Electron-spezifischwinston, pino
StorybookUI-EntwicklungIsolierte Komponenten-Entwicklung, Visual TestingLadle
ViteBuild-ToolSchnelles HMR, ES Modules, TypeScript-SupportWebpack, 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:

  1. Umgebungsvariablen laden: dotenv.config() in src/main.ts:52
  2. IPC-Handler registrieren: registerIpcHandlers() vor App-Ready in src/main.ts:55
  3. Git-Verzeichnis auflösen: Platform-spezifische Git-Konfiguration in src/main.ts:63-76
  4. Protocol-Handler registrieren: dyad:// Deep Links in src/main.ts:79-87
  5. 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

  1. Neue Datei in src/ipc/handlers/ erstellen
  2. Handler-Funktion mit handle() oder createTypedHandler() registrieren
  3. Kanal zu VALID_INVOKE_CHANNELS oder VALID_RECEIVE_CHANNELS hinzufügen
  4. Handler in registerIpcHandlers() in src/ipc/ipc_host.ts aufrufen

Neue Agent-Tools hinzufügen

  1. Tool in src/pro/main/ipc/handlers/local_agent/tools/ erstellen
  2. Tool in src/pro/main/ipc/handlers/local_agent/tool_definitions.ts importieren
  3. React-Komponente für Rendering in src/components/chat/DyadMarkdownParser.tsx erstellen
  4. E2E-Test in e2e-tests/local_agent*.spec.ts hinzufügen