The previous attempt split text vs reasoning by the PartDelta.Field value, but opencode flags both with field='text' — only the partID differs. Result: every reasoning delta still landed in the main answer area while streaming, identical to the original bug. handleRunStream now carries a partID→type map across deltas, populated from message.part.updated events (and pre-seeded from the message's existing parts during replay). When a text delta arrives, the map says whether its partID belongs to a reasoning part (emit a think event) or a text part (filter for <think> tags and emit a delta event). Unknown partIDs fall through to the text path so a delta arriving before its part.updated event doesn't drop content. The previous runStreamState is a tiny struct so both the filter and the map travel together through translateEvent. |
||
|---|---|---|
| cmd | ||
| internal | ||
| third_party | ||
| .env.example | ||
| .gitignore | ||
| DEPLOY.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| Dockerfile.opencode | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| opencode.json | ||
| README.md | ||
| ROADMAP.md | ||
SHAI — Self-Hosted AI
Personal, single-user AI workspace for the homelab. A thin Go web layer over opencode — opencode owns the agent loop, tool use, provider routing, and persistence; SHAI owns the chat UI and (eventually) the integration surfaces (webhooks, REST, MCP server, scheduling) and the OpenViking memory bridge.
Quick start
You need opencode installed locally (brew install anomalyco/tap/opencode
or see opencode.ai/docs).
# Terminal 1 — start opencode serve
make opencode
# Terminal 2 — start the SHAI web server
cp .env.example .env
make run
# open http://127.0.0.1:7100
Or in Docker (homelab deploy):
docker compose up -d --build
The opencode.json at the repo root configures the Ollama provider
against the homelab proxy. Edit it (or use ~/.config/opencode/opencode.json)
to point at a different Ollama instance, add Anthropic/OpenAI, or change the
default model. Restart opencode after edits.
Status
M1.5 — opencode cutover. Chat works end-to-end through opencode serve
with streaming, rename, delete, and the sidebar menu. The Python sidecar
and SHAI's own message/run/event tables have been deleted; opencode owns
persistence. No tools yet exposed in the UI, no API surface, no MCP server,
no scheduling.
See ROADMAP.md for the full plan.
Architecture
browser ──HTMX+SSE──▶ Go web (cmd/shai)
│
└── HTTP/SSE ──▶ opencode serve (REST + /event)
│
├── Drizzle/SQLite (sessions, messages, parts, events)
├── AI SDK providers (Ollama, Anthropic, OpenAI, …)
└── tool loop (file/bash/MCP)
Layout
cmd/shai/ main.go (Go entry)
internal/
config/ env-driven config
opencode/ REST/SSE client for opencode serve
server/ HTTP handlers, templates, SSE bridge
web/
templates/ HTMX templates
static/ htmx.min.js, marked.min.js, app.css, app.js
opencode.json opencode provider config (homelab Ollama)
Dockerfile Go-only image for the web layer
Dockerfile.opencode opencode serve image
docker-compose.yml runs both