Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7221657
chore(gui): scaffold the apps/gui workspace and wire the toolchain
hutusi Jul 12, 2026
191b723
feat(gui): tracer bullet — Rust sidecar manager, transport adapter, m…
hutusi Jul 12, 2026
19693a7
feat(gui): packaged-app pipeline — externalBin sidecars and local tau…
hutusi Jul 12, 2026
c1388aa
feat(gui): transcript parity rendering
hutusi Jul 12, 2026
b4625f8
feat(cli,gui): keyless acp hosting and the GUI permission dialog
hutusi Jul 12, 2026
0176da3
feat(protocol,kernel,client,gui): first-run config over the protocol
hutusi Jul 12, 2026
fe5c476
feat(gui): sessions, resume, modes, profiles, compact, skills
hutusi Jul 12, 2026
fb29d5e
feat(gui): multi-project tabs and kernel crash recovery
hutusi Jul 12, 2026
c5891ca
feat(gui): side-by-side diff viewer
hutusi Jul 12, 2026
dbb16b4
feat(gui): native notifications when a long turn finishes unfocused
hutusi Jul 12, 2026
ec67281
docs: mark the Tauri sidecar shipped and document GUI development
hutusi Jul 12, 2026
5707467
fix(gui): close codex review round (graceful shutdown, model refs, lo…
hutusi Jul 13, 2026
e40aefa
fix(gui): close coderabbit review round (drafts, dialogs, handshake t…
hutusi Jul 13, 2026
4559cbb
fix(gui): close codex round 2 (kill/restart race, real stale errors, …
hutusi Jul 13, 2026
d3c6c25
fix(gui): close codex round 3 (lifecycle serialization, session insta…
hutusi Jul 13, 2026
f220cad
fix(gui): close review round 4 (reap-path reader join, guarded sessio…
hutusi Jul 13, 2026
82126b0
fix(gui): preserve lifecycle and profile ordering
hutusi Jul 13, 2026
7b77504
fix(gui,providers,client): close the consolidated final-state review …
hutusi Jul 13, 2026
fe1e0cf
fix(gui): close final lifecycle races
hutusi Jul 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ coverage/
# Project-layer settings written by running the minerva TUI inside this repo
# (persisted "always allow" rules) — local dev state, not shared config.
.minerva/
# Tauri GUI build state; binaries/ holds the prepared sidecar pair (host-built).
apps/gui/src-tauri/target/
apps/gui/src-tauri/binaries/
apps/gui/src-tauri/gen/schemas/
31 changes: 30 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
## Setup

```sh
bun install # Bun ≥ 1.3 (CI pins 1.3.12)
bun install # Bun ≥ 1.3 (CI pins 1.3.14)
bun run verify # typecheck + lint + all tests — the gate for everything
```

The repo is a Bun workspace; packages export TypeScript source directly (no
build step). `tsc --noEmit` typechecks, Biome lints/formats, `bun test` runs
everything, and knip guards against dead exports and unused dependencies.

Working on the GUI (`apps/gui`) additionally needs the Rust toolchain
([rustup](https://rustup.rs)) — plus webkit2gtk on Linux. The webview's
TypeScript is covered by the normal gate; the Rust side builds locally only
(`tauri dev` / `tauri build`), never in CI.

## The verify gate

`bun run verify` must be green before anything lands on `main`. CI enforces it
Expand Down Expand Up @@ -42,8 +47,14 @@ on every PR (ubuntu + macos), plus:
| `packages/cli/test/acp.test.ts` | Real child process over stdio (ACP wire contract) |
| `packages/cli/test/app.test.tsx` | Ink UI via ink-testing-library, real kernel underneath |
| `packages/kernel/test/mcp.test.ts` | Real spawned MCP server from project settings |
| `packages/cli/test/acp.test.ts` (keyless cases) | `acp --allow-unconfigured` hosting contract |
| `apps/gui/test/` | GUI pure modules: transport framing, kernel manager (fake JSON-RPC bridge), tabs, permission queue, diff alignment, notification matrix |
| `scripts/*.exp` | The TUI under a genuine pseudo-terminal |

GUI tests deliberately import only pure modules — never React components or
the one file touching `@tauri-apps/api` — so the per-file coverage threshold
stays meaningful (files loaded during a test run are measured).

Tests never touch `~/.minerva`: they pass a temp `dataDir` to the kernel (or
set `MINERVA_DATA_DIR` for spawned processes). Do the same for manual testing:

Expand All @@ -57,8 +68,26 @@ MINERVA_DATA_DIR=$(mktemp -d) bun run --cwd packages/cli dev
bun run --cwd packages/cli dev # interactive TUI (needs ANTHROPIC_API_KEY)
bun run packages/cli/src/index.tsx acp # kernel on stdio for editors
bun run build:release # single-file executable (see README caveat)
bun run --cwd apps/gui dev # Tauri GUI, kernel from source
bun run --cwd apps/gui prepare-sidecar && bun run --cwd apps/gui build # packaged app
```

## GUI manual smoke (before landing apps/gui changes)

`tauri dev` with a temp `MINERVA_DATA_DIR` and walk:

1. First run (no keys in env, empty data dir) → config dialog appears; store
a key → first prompt streams without a restart.
2. Prompt that writes a file → permission dialog (y/a/n hotkeys, Esc cancels
the turn); diff preview renders; toggle unified/split.
3. Sessions → resume a TUI-created session (shared JSONL store).
4. Second tab on another repo; prompt both concurrently over one kernel.
5. `kill -9` the kernel process → banner, auto-restart, tabs restore;
kill it again → stays down until the restart button.
6. Reload the webview (dev) → still exactly one kernel process.
7. Packaged app: run a grep-using prompt (proves the bundled rg sidecar),
then Cmd+Q → kernel process gone.

## Design context

Read [docs/DESIGN.md](docs/DESIGN.md) (architecture decisions and their
Expand Down
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
[![CI](https://github.com/hutusi/minerva/actions/workflows/ci.yml/badge.svg)](https://github.com/hutusi/minerva/actions/workflows/ci.yml)

A cross-platform, model-agnostic code agent — a headless kernel with multiple
frontends (CLI now, GUI later).
frontends (Ink CLI and Tauri desktop GUI).

- **Architecture**: protocol everywhere ([ACP](https://agentclientprotocol.com)
core + `minerva/*` extensions), one kernel, swappable transports (in-process,
stdio; WebSocket planned).
- **Stack**: TypeScript, Bun, Vercel AI SDK, Ink (CLI), Tauri 2 (GUI, planned).
- **Stack**: TypeScript, Bun, Vercel AI SDK, Ink (CLI), Tauri 2 (GUI).

Docs: [design record](docs/DESIGN.md) · [wire protocol](docs/PROTOCOL.md) ·
[contributing](CONTRIBUTING.md) · [changelog](CHANGELOG.md)
Expand Down Expand Up @@ -342,7 +342,26 @@ bun test packages/kernel
The repo is a Bun workspace: `packages/protocol` (JSON-RPC + ACP types +
transports), `packages/kernel` (agent loop, sessions, tools, permissions,
MCP), `packages/providers` (model adapters), `packages/client` (shared
frontend core), `packages/cli` (Ink UI + acp host), `apps/gui` (planned).
frontend core), `packages/cli` (Ink UI + acp host), `apps/gui` (Tauri 2
desktop app).

## Desktop GUI

The GUI spawns the kernel as a stdio sidecar (`minerva acp
--allow-unconfigured`) and reuses `@minerva/client` for everything the TUI
renders, plus GUI-native extras: multi-project tabs over one kernel, a
side-by-side diff viewer, and native notifications when a long turn finishes
in the background. Sessions are shared with the TUI — resume either's
sessions from the other.

```sh
# prerequisites: the Rust toolchain (rustup) — plus, on Linux, webkit2gtk
bun run --cwd apps/gui dev # tauri dev: kernel runs from source

# packaged app (unsigned, local): bundles the compiled minerva + rg pair
bun run --cwd apps/gui prepare-sidecar
bun run --cwd apps/gui build # .app/.dmg under apps/gui/src-tauri/target
```

## Release build

Expand Down
21 changes: 21 additions & 0 deletions apps/gui/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
12 changes: 12 additions & 0 deletions apps/gui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Minerva</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
36 changes: 36 additions & 0 deletions apps/gui/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@minerva/gui",
"version": "0.3.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tauri dev --config src-tauri/tauri.dev.conf.json",
"dev:web": "vite",
"build": "tauri build",
"build:web": "vite build",
"prepare-sidecar": "bun run scripts/prepare-sidecar.ts",
"tauri": "tauri"
},
"dependencies": {
"@minerva/client": "workspace:*",
"@minerva/protocol": "workspace:*",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2",
"@tauri-apps/plugin-notification": "^2",
"dompurify": "^3",
"marked": "^16",
"react": "^19",
"react-dom": "^19"
},
"devDependencies": {
"@minerva/kernel": "workspace:*",
"@minerva/providers": "workspace:*",
"@tailwindcss/vite": "^4.1.11",
"@tauri-apps/cli": "^2",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^5",
"tailwindcss": "^4.1.11",
"vite": "^7"
}
}
58 changes: 58 additions & 0 deletions apps/gui/scripts/prepare-sidecar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Prepare the kernel sidecar pair for a packaged GUI build.
*
* Runs the repo's release build (the `dist/{minerva, rg}` pair — see
* scripts/build-release.ts for why the pair is host-only) and copies both
* into src-tauri/binaries/ with the host target-triple suffix Tauri requires
* for externalBin sources. At bundle time Tauri strips the suffix and places
* both next to the app executable (Contents/MacOS/ on macOS), so the kernel's
* process.execPath sits beside rg and resolveRgPath works unchanged.
*
* Only needed before `tauri build`; dev spawns the kernel from source.
*/
import { chmodSync, copyFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";

const repoRoot = join(import.meta.dir, "..", "..", "..");

const build = Bun.spawnSync(["bun", "run", "build:release"], {
cwd: repoRoot,
stdout: "inherit",
stderr: "inherit",
});
if (!build.success) process.exit(build.exitCode || 1);

// The triple must match what the tauri CLI expects for this host, and rustc
// is authoritative (and always present — tauri build needs it too). Guard
// the whole probe: a missing rustc can throw from spawnSync or leave stdout
// unset, and either must fail with the hint rather than a stack trace.
const triple = (() => {
try {
const rustc = Bun.spawnSync(["rustc", "-vV"]);
if (!rustc.success || !rustc.stdout) return null;
return (
rustc.stdout
.toString()
.split("\n")
.find((line) => line.startsWith("host: "))
?.slice("host: ".length)
.trim() ?? null
);
} catch {
return null;
}
})();
if (!triple) {
console.error("cannot determine host target triple: is rustc installed?");
process.exit(1);
}

const binariesDir = join(import.meta.dir, "..", "src-tauri", "binaries");
mkdirSync(binariesDir, { recursive: true });
for (const name of ["minerva", "rg"]) {
const source = join(repoRoot, "dist", name);
const dest = join(binariesDir, `${name}-${triple}`);
copyFileSync(source, dest);
chmodSync(dest, 0o755);
console.log(`prepared sidecar: ${dest}`);
}
Loading
Loading