SLOPSHOPPER

sample-mod

No description.

newpanebandspinnerrowsguard
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · sample-mod
│ ┃ sample-mod ✕ units=undefined │ ┃ units=undefined [ Refresh ] │ ┃ [ Refresh ] │ ⏺ Read(src/auth.ts) │ ⎿ Read 6 lines │ units=undefined │ [ Refresh ] │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ units=undefined │ [ Refresh ] │ │ units=undefined │ [ Refresh ] │ │ units=undefined [ Refresh ] units=undefined [ Refresh ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › units=undefined [ Refresh ] units=undefined [ Refresh ]

Draws

Band
units=undefined [ Refresh ]
Pane · sample-mod
units=undefined [ Refresh ]
Spinner
units=undefined [ Refresh ]
Prompt hint
units=undefined [ Refresh ]
Turn line
units=undefined [ Refresh ]
Your message
units=undefined [ Refresh ]
Claude's reply
units=undefined [ Refresh ]
Tool row
units=undefined [ Refresh ]
Command output
units=undefined [ Refresh ]
README

Ginno

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.

License: MIT Platform-black.svg) No cloud

English · 简体中文

Ginno chat workspace


What it is

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.

  • 🧠 Multi-agent chat — switch agents mid-conversation, shared history, per-agent memory
  • 🛠️ Tools · permissions · skills · MCP · hooks — Claude Code's shape, all built in
  • 🔀 Visual Workflow Studio — a versioned DSL you compile into a LangGraph state graph, with a live run observer
  • 📚 Local knowledge base — index your Obsidian vault; answers come back with citations
  • 🎯 Long-horizon goals + TODO sync — let the agent keep working toward a standing objective
  • 🌐 Embedded browser + web search — retrieval with source attribution, not vibes

Full feature walkthrough: docs/user-guide.md.

Workflow StudioLocal knowledge base
Workflow StudioKnowledge base

Why "local-first"

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.


Install

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).


Stack

LayerChoice
ShellTauri 2 (Rust) — process management + webview only
UINext.js 14 (static export) + React 18 + Tailwind
RuntimePython + FastAPI + LangGraph (sidecar, bundled with PyInstaller)
Storageplain files (no DB); optional LanceDB for semantic-search vectors

Architecture and subsystem designs: docs/architecture.md.


Status

  • A personal project, used daily (dogfooding) and iterated fast — ~2.5 months, 220+ commits.
  • Early and unpolished; APIs and UI still move.
  • Issues, PRs, and ideas are welcome — in English or Chinese.

License

MIT © 2026 ChuanchuanSong

Source 1 files
hooks/sample.ts 53 lines
1// The integration fixture: one hook per behavior the broker contract needs —
2// a matcher'd chain hook, a renderer with a press button, a double-`next`
3// probe, a throwing hook with a `.catch`, and a timer scheduler.
4
5export function register(on, options) {
6  // Rewrites the event input, reads a `$` op, and reshapes the result.
7  on('tool.call', { tool: /^Bash/ }, async ($, e, next) => {
8    const session = await $.session.id()
9    const beneath = await next({ ...e, note: `seen:${session}` })
10    return { ...beneath, observed: true }
11  })
12
13  // Draws a band whose Button carries a callback, and reads the pushed option.
14  on('ui.render', async ($, e, next) => {
15    const ui = $.ui.resolve(e)
16    await next(e)
17    return ui.Box({
18      flexDirection: 'column',
19      children: [
20        ui.Text({ color: 'cyan', children: `units=${String(options.units)}` }),
21        ui.Button({ label: 'Refresh', onPress: value => `pressed:${String(options.units)}${value === undefined ? '' : `:${String(value)}`}` }),
22      ],
23    })
24  })
25
26  // Calls `next` twice; the runner must send the frame once.
27  on('turn.start', async ($, e, next) => {
28    const first = await next(e)
29    const second = await next(e)
30    return { same: second === first }
31  })
32
33  // Throws; its `.catch` answers from `next.error`.
34  const ending = on('session.end', () => {
35    throw new Error('boom')
36  })
37  ending.catch(async ($, e, next) => ({ handled: next.error?.kind === 'throw' && next.error.message === 'boom' }))
38
39  // Schedules a broker timer whose callback logs through `$`.
40  on('session.start', async ($, e) => {
41    let bound = undefined
42    $.clock.after(5, () => { void bound.ui.log('tick') })
43    bound = $
44    return { cwd: e.cwd }
45  })
46
47  // Hands a later mod's serialized tree through unchanged.
48  on('turn.complete', async ($, e, next) => await next(e))
49
50  // Registers against an event this host never raises; the load log warns.
51  on('telemetry.mark', () => ({}))
52}
53