A live forecast of the context window, drawn above the prompt.

A local-first personal AI agent that runs on your own machine.
The shape of Claude Code, all-file storage (no DB · no account · no cloud), and a visual Workflow Studio.

Ginno turns the capabilities an AI agent should have into a complete desktop app, instead of scattering them across a terminal and a dozen browser tabs.
Full feature walkthrough:
docs/user-guide.md.
| Workflow Studio | Local knowledge base |
|---|---|
![]() | ![]() |
One design rule: your data should not leave your machine.
| Ginno | |
|---|---|
| Database | ❌ none — all state is JSON / JSONL / Markdown under ~/.ginno/ |
| Account | ❌ none — open it and go |
| Cloud sync | ❌ none — everything stays local |
| Portable | ✅ copy the folder to move it; readable, hand-editable, git-able |
You bring your own model key (OpenAI / Anthropic / DeepSeek, or any compatible endpoint). Ginno is just a client — it never takes custody of your key or your data.
Option A — download (macOS, Apple Silicon): grab the latest .dmg from Releases. It is not code-signed yet, so on first launch use right-click → Open to get past Gatekeeper.
Option B — build from source:
git clone https://github.com/robscc/ginno
cd ginno
pnpm install # web + desktop shell
cd packages/runtime && uv sync # Python runtime
cd ../..
pnpm dev # web + runtime + desktop shell, all at once
Requires: Node ≥ 20 · pnpm ≥ 9 · Python ≥ 3.11 (via uv) · Rust (Tauri).
Package a .app: make app → apps/desktop/target/release/bundle/dmg/Ginno_*.dmg (see docs/p3-packaging-notes.md).
| Layer | Choice |
|---|---|
| Shell | Tauri 2 (Rust) — process management + webview only |
| UI | Next.js 14 (static export) + React 18 + Tailwind |
| Runtime | Python + FastAPI + LangGraph (sidecar, bundled with PyInstaller) |
| Storage | plain files (no DB); optional LanceDB for semantic-search vectors |
Architecture and subsystem designs:
docs/architecture.md.
MIT © 2026 ChuanchuanSong
hooks/token-weather.mjs 88 lines1// Token Weather, from "Getting started with Claude Code mods"
2// (https://claude.dev/blog/getting-started-with-claude-code-mods/, Anthropic, 2026-10-01).
3// The published module, unchanged.
4
5// Token Weather: a live forecast of the context window, above the prompt.
6
7const HISTORY = 12;
8const BARS = "▁▂▃▄▅▆▇█";
9const FORECAST = [
10 { upTo: 25, icon: "☀", word: "Clear", color: "yellow" },
11 { upTo: 50, icon: "☁", word: "Cloudy", color: "cyan" },
12 { upTo: 75, icon: "☂", word: "Showers", color: "blue" },
13 { upTo: 90, icon: "☇", word: "Storm", color: "magenta" },
14 { upTo: Infinity, icon: "↯", word: "Compact soon", color: "red" },
15];
16
17// Held by the host, so the history survives a hot reload of this file.
18const readings = { plugin: "token-weather", key: "readings" };
19
20export function register(on) {
21 on("session.start", async ($, e, next) => {
22 const result = await next(e);
23 await takeReading($);
24 return result;
25 });
26
27 on("turn.complete", async ($, e, next) => {
28 const result = await next(e);
29 if (!e.agentId) {
30 await takeReading($); // main-loop turns only, not subagents
31 }
32 return result;
33 });
34
35 on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
36 const { value: history = [] } = await $.state.get(readings);
37 if (e.props.hasSurvey || history.length === 0) {
38 return next(e);
39 }
40 const { Box, Text } = $.ui.resolve(e);
41 return band(Box, Text, history, e.props.bodyColumns);
42 });
43}
44
45async function takeReading($) {
46 const { context } = await $.session.usage();
47 if (!context?.window) return;
48 const tokens = context.tokens ?? 0;
49 const percent = context.percent ?? Math.round((tokens / context.window) * 100);
50 const { value: history = [] } = await $.state.get(readings);
51 await $.state.set(readings, [...history, { tokens, window: context.window, percent }].slice(-HISTORY));
52}
53
54function band(Box, Text, history, columns) {
55 const now = history[history.length - 1];
56 const f = FORECAST.find((b) => now.percent < b.upTo);
57 const parts = [
58 Text({ color: f.color, bold: true, children: `${f.icon} ${f.word}` }),
59 Text({ children: ` ${now.percent}% of context` }),
60 Text({ dimColor: true, children: ` ${short(now.tokens)} / ${short(now.window)}` }),
61 ];
62 if (columns >= 60) {
63 parts.push(Text({ dimColor: true, children: " last turns " }));
64 parts.push(Text({ color: f.color, children: sparkline(history) }));
65 if (history.length > 1) {
66 parts.push(Text({ dimColor: true, children: trend(history) }));
67 }
68 }
69 return Box({ flexDirection: "row", paddingX: 1, children: parts });
70}
71
72function sparkline(history) {
73 const top = Math.max(...history.map((r) => r.tokens), 1);
74 return history.map((r) => BARS[Math.floor((r.tokens / top) * (BARS.length - 1))]).join("");
75}
76
77function trend(history) {
78 const delta = history[history.length - 1].tokens - history[history.length - 2].tokens;
79 if (delta === 0) return " steady";
80 return delta > 0 ? ` ▲ +${short(delta)} last turn` : ` ▼ ${short(-delta)} last turn`;
81}
82
83function short(n) {
84 if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(1)}M`;
85 if (n >= 1_000) return `${+(n / 1_000).toFixed(1)}k`;
86 return String(n);
87}
88types/index.d.ts 11 lines1// Token Weather's type contract, from https://claude.dev/blog/getting-started-with-claude-code-mods/
2// (Anthropic, 2026-10-01). The published file, unchanged.
3
4export type TokenWeatherReading = { tokens: number; window: number; percent: number }
5
6declare module 'claude-code' {
7 interface PluginState {
8 'token-weather': { readings: TokenWeatherReading[] }
9 }
10}
11