From a06646a15cc61571b675c55c1c1afe3e1ec87c82 Mon Sep 17 00:00:00 2001 From: Kevin-Tucuxi Date: Sat, 13 Jun 2026 20:08:22 -0700 Subject: [PATCH] feat(desktop): Reset/Uninstall action + live panel progress + direct Docker link; guide download links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reset… control-panel button (two-click confirm) → down -v + clears config/.env → re-runs the wizard. Fixes the 'delete config alone leaves a colliding volume' gap. (downVArgs + resetStack + clearConfig + stack:reset IPC.) - Control panel shows live 'N/8 services ready' during startup (wizard already did). - 'Install Docker Desktop' button → direct Apple-Silicon dmg link. - INSTALL-MAC.md: direct Docker Desktop + Ollama download links; document Reset. Co-Authored-By: Claude Opus 4.8 (1M context) --- desktop/src/core/compose.test.ts | 4 +++ desktop/src/core/compose.ts | 2 ++ desktop/src/main/index.ts | 25 ++++++++++++++++--- desktop/src/main/orchestrator.ts | 5 +++- desktop/src/main/store.ts | 9 ++++++- desktop/src/preload/index.ts | 1 + desktop/src/renderer/panel.ts | 42 +++++++++++++++++++++++++++++--- docs/INSTALL-MAC.md | 10 +++++--- 8 files changed, 87 insertions(+), 11 deletions(-) diff --git a/desktop/src/core/compose.test.ts b/desktop/src/core/compose.test.ts index 057281f9..aa7bcf43 100644 --- a/desktop/src/core/compose.test.ts +++ b/desktop/src/core/compose.test.ts @@ -5,6 +5,7 @@ import { psArgs, upArgs, downArgs, + downVArgs, logsArgs, adminFixtureArgs } from './compose' @@ -33,6 +34,9 @@ describe('argv builders', () => { it('down keeps volumes (no -v) so user data survives a stop', () => { expect(downArgs(base)).toEqual([...base, 'down']) }) + it('down -v also removes volumes (Reset)', () => { + expect(downVArgs(base)).toEqual([...base, 'down', '-v']) + }) it('logs follow a single service', () => { expect(logsArgs(base, 'donna-web')).toEqual([...base, 'logs', '-f', '--tail', '200', 'donna-web']) }) diff --git a/desktop/src/core/compose.ts b/desktop/src/core/compose.ts index 6c1114dd..bf6d73f7 100644 --- a/desktop/src/core/compose.ts +++ b/desktop/src/core/compose.ts @@ -8,6 +8,8 @@ export function composeBaseArgs(composeFile: string, projectName: string): strin export const psArgs = (base: string[]): string[] => [...base, 'ps', '--format', 'json'] export const upArgs = (base: string[]): string[] => [...base, 'up', '-d'] export const downArgs = (base: string[]): string[] => [...base, 'down'] +/** `down -v` — also removes volumes. Used by Reset to wipe all data for a fresh setup. */ +export const downVArgs = (base: string[]): string[] => [...base, 'down', '-v'] export const logsArgs = (base: string[], service: string): string[] => [ ...base, 'logs', diff --git a/desktop/src/main/index.ts b/desktop/src/main/index.ts index 347c52b9..4e705c16 100644 --- a/desktop/src/main/index.ts +++ b/desktop/src/main/index.ts @@ -5,9 +5,16 @@ import { resolvePorts } from '../core/ports' import { generateSecrets } from '../core/secrets' import { DEFAULT_PORTS } from '../core/types' import type { InferenceChoice, LauncherConfig } from '../core/config' -import { loadConfig, saveConfig, writeEnvFile } from './store' +import { loadConfig, saveConfig, writeEnvFile, clearConfig } from './store' import { composeFilePath, envPath, PROJECT_NAME } from './paths' -import { snapshot, startStack, stopStack, runAdminFixture, type StackSnapshot } from './orchestrator' +import { + snapshot, + startStack, + stopStack, + resetStack, + runAdminFixture, + type StackSnapshot +} from './orchestrator' import { streamDocker } from './runner' import { isPortFreeSync } from './netcheck' @@ -98,8 +105,20 @@ ipcMain.handle('stack:openDonna', () => { const port = cfg?.ports.donnaWeb ?? DEFAULT_PORTS.donnaWeb win?.loadURL(`http://localhost:${port}`) }) +// Reset: stop the stack, remove its volumes (down -v), and delete the stored config/.env +// so the next launch re-runs the first-run wizard. down -v runs while .env still exists. +ipcMain.handle('stack:reset', async () => { + try { + await resetStack(base()) + clearConfig() + return { ok: true } + } catch (err) { + return { ok: false, error: String(err) } + } +}) ipcMain.handle('engine:installDocker', () => - shell.openExternal('https://www.docker.com/products/docker-desktop/') + // Direct Apple-Silicon Docker Desktop download (this launcher is arm64-only). + shell.openExternal('https://desktop.docker.com/mac/main/arm64/Docker.dmg') ) app.whenReady().then(() => { diff --git a/desktop/src/main/orchestrator.ts b/desktop/src/main/orchestrator.ts index cd7220af..127e79a1 100644 --- a/desktop/src/main/orchestrator.ts +++ b/desktop/src/main/orchestrator.ts @@ -1,5 +1,5 @@ import { parseEngineProbe } from '../core/engine' -import { parseComposePs, psArgs, upArgs, downArgs, adminFixtureArgs } from '../core/compose' +import { parseComposePs, psArgs, upArgs, downArgs, downVArgs, adminFixtureArgs } from '../core/compose' import { deriveLauncherState } from '../core/state' import type { LauncherState, ServiceStatus } from '../core/types' import { runDocker, type RunResult } from './runner' @@ -29,6 +29,9 @@ export const startStack = (base: string[], env: NodeJS.ProcessEnv): Promise => runDocker(downArgs(base)) +/** Reset: stop the stack AND remove its volumes (wipes all data) for a fresh setup. */ +export const resetStack = (base: string[]): Promise => runDocker(downVArgs(base)) + export const runAdminFixture = ( base: string[], email: string, diff --git a/desktop/src/main/store.ts b/desktop/src/main/store.ts index 6e7bc29c..afebc2b3 100644 --- a/desktop/src/main/store.ts +++ b/desktop/src/main/store.ts @@ -1,5 +1,5 @@ import { safeStorage } from 'electron' -import { writeFileSync, readFileSync, existsSync, chmodSync } from 'node:fs' +import { writeFileSync, readFileSync, existsSync, chmodSync, rmSync } from 'node:fs' import { configPath, envPath } from './paths' import { renderEnv } from '../core/env' import type { LauncherConfig } from '../core/config' @@ -23,6 +23,13 @@ export function loadConfig(): LauncherConfig | null { return JSON.parse(json) as LauncherConfig } +/** Delete the persisted config + .env so the next launch re-runs the first-run wizard. */ +export function clearConfig(): void { + for (const p of [configPath(), envPath()]) { + if (existsSync(p)) rmSync(p) + } +} + /** Write the chmod-600 .env the compose command reads, into the app data dir. */ export function writeEnvFile(cfg: LauncherConfig): string { const path = envPath() diff --git a/desktop/src/preload/index.ts b/desktop/src/preload/index.ts index 1ab1fb0e..78b0fc04 100644 --- a/desktop/src/preload/index.ts +++ b/desktop/src/preload/index.ts @@ -8,6 +8,7 @@ const api = { status: (): Promise => ipcRenderer.invoke('stack:status'), start: (): Promise => ipcRenderer.invoke('stack:start'), stop: (): Promise => ipcRenderer.invoke('stack:stop'), + reset: (): Promise<{ ok: boolean; error?: string }> => ipcRenderer.invoke('stack:reset'), openDonna: (): Promise => ipcRenderer.invoke('stack:openDonna'), installDocker: (): Promise => ipcRenderer.invoke('engine:installDocker'), onLog: (cb: (line: string) => void): void => { diff --git a/desktop/src/renderer/panel.ts b/desktop/src/renderer/panel.ts index d65575ce..ae513c1e 100644 --- a/desktop/src/renderer/panel.ts +++ b/desktop/src/renderer/panel.ts @@ -25,15 +25,26 @@ export function renderPanel(root: HTMLElement): void {

Logs

+

+ + +

` const stateEl = document.getElementById('state')! const logsEl = document.getElementById('logs')! const open = document.getElementById('open') as HTMLButtonElement const msgEl = document.getElementById('msg')! const install = document.getElementById('install') as HTMLButtonElement + const reset = document.getElementById('reset') as HTMLButtonElement + const resetHint = document.getElementById('resethint')! const apply = (snap: Snapshot): void => { - stateEl.textContent = LABELS[snap.state] ?? snap.state + let label = LABELS[snap.state] ?? snap.state + if (snap.state === 'STACK_STARTING') { + const healthy = (snap.services ?? []).filter((s) => s.health === 'healthy').length + label = `Starting… ${healthy}/8 services ready` + } + stateEl.textContent = label open.disabled = snap.state !== 'HEALTHY' const noEngine = snap.state === 'NO_ENGINE' msgEl.textContent = noEngine ? (snap.engineMessage ?? '') : '' @@ -41,10 +52,35 @@ export function renderPanel(root: HTMLElement): void { } document.getElementById('open')!.addEventListener('click', () => window.donna.openDonna()) - document.getElementById('start')!.addEventListener('click', () => window.donna.start()) - document.getElementById('stop')!.addEventListener('click', () => window.donna.stop()) + document.getElementById('start')!.addEventListener('click', async () => { + await window.donna.start() + tick() + }) + document.getElementById('stop')!.addEventListener('click', async () => { + await window.donna.stop() + tick() + }) install.addEventListener('click', () => window.donna.installDocker()) + // Reset wipes the stack + all data and re-runs first-run setup — two-click confirm. + let resetArmed = false + reset.addEventListener('click', async () => { + if (!resetArmed) { + resetArmed = true + resetHint.textContent = 'This erases all Donna data on this Mac. Click again to confirm.' + return + } + reset.disabled = true + resetHint.textContent = 'Resetting…' + const res = await window.donna.reset() + if (res.ok) window.location.reload() + else { + reset.disabled = false + resetArmed = false + resetHint.textContent = res.error ?? 'Reset failed.' + } + }) + const MAX_LOG_LINES = 2000 window.donna.onLog((line) => { const lines = (logsEl.textContent + line + '\n').split('\n') diff --git a/docs/INSTALL-MAC.md b/docs/INSTALL-MAC.md index 7474a50b..c3e3c28e 100644 --- a/docs/INSTALL-MAC.md +++ b/docs/INSTALL-MAC.md @@ -6,8 +6,9 @@ terminal, no GitHub, no config files**. This guide walks through it with picture > **What you need** > - A **Mac with Apple Silicon** (M1/M2/M3/M4). *(An Intel build is coming.)* > - **Docker Desktop** installed and running — Donna uses it to run its private engine on your -> machine. Get it free at [docker.com/products/docker-desktop](https://www.docker.com/products/docker-desktop/). -> (If Docker isn't installed, Donna will tell you and link you to it.) +> machine. **[Download Docker Desktop for Apple Silicon →](https://desktop.docker.com/mac/main/arm64/Docker.dmg)** +> (or browse [docker.com/products/docker-desktop](https://www.docker.com/products/docker-desktop/)). +> If Docker isn't installed, Donna links you straight to it. > - **~12 GB of free disk** for the engine images, and an internet connection for the first run. Everything Donna does runs **locally on your Mac**. Your documents never leave your computer unless @@ -44,7 +45,8 @@ The first time you open Donna, a short **Welcome** screen appears. Two quick cho - **Use a cloud API key (recommended)** — paste an Anthropic API key for the best quality and speed. *(Optional — you can leave it blank and add a key later in Settings.)* - **Run fully local with Ollama** — keep everything on your Mac with no cloud at all. This needs - [Ollama](https://ollama.com) installed and running with a model pulled (e.g. `ollama pull qwen2.5`). + **[Ollama](https://ollama.com/download)** installed and running with a model pulled (e.g. + `ollama pull qwen2.5`). **2. Set your password** — your login is **`admin@lq.ai`** (shown on screen); choose a password of at least 12 characters. You can change the email and password later in **Settings → Account**. @@ -85,6 +87,8 @@ The Donna app window is your **control panel**: - **Start / Stop** — start or stop the engine. Stopping frees up your Mac's resources; your data is kept and is there next time you Start. - **Logs** — a live view of what the engine is doing, handy if something looks stuck. +- **Reset…** — erases all Donna data on this Mac and re‑runs first‑time setup (two clicks to confirm). + Use this only if you want to start completely fresh. You can quit the app when you're done. Re‑opening it goes straight to the control panel — the setup wizard only runs the very first time.