Made with Claude to talk with non-Claude.
Named "ollama-chat" because it started that way. Now a multi-backend TUI client (Ollama, OpenAI-compatible, llama.cpp) — just ochat.
A setup-and-forget multi-backend TUI chat client. One screen, no sub-menus, plenty of creature comforts.
I wanted something like Open WebUI but in terminal. A chat interface without needing a full browser eating 8GB of RAM just to exist.
It started as a bare-bones wrapper, but grew into something I actually enjoy using daily — switchable personalities, auto-suggest, impersonate mode, context tracking, multiple config profiles. Still not Open WebUI, but not bare-bones either.
I also wanted a TUI as pleasant to use as Claude Code but for local Ollama models.
This project was entirely vibe-coded with Claude. It does exactly what I need.
- Clean TUI with streaming responses
- Switchable personalities (system prompts)
- Multiple config profiles (easily switch between setups)
- Project-specific prompts (auto-loads
agent.md/system.mdfrom current directory) - Slash commands (
/help,/retry,/undo,/personality,/config,/impersonate, etc.) - Keyboard shortcuts (Ctrl+C cascade, Escape to cancel generation, Ctrl+R retry, Ctrl+U undo, Ctrl+G generate, etc.)
- Auto-suggest: after each response, a short suggestion appears in the input (Tab to accept)
- Impersonate mode: LLM suggests what you'd say next (
/implong-form,/impsshort) - Context tracking with usage warnings and
/compactto summarize conversation - Generation stats (TTFT, tokens/s) in status bar and
/stats - Persistent configuration
- Pass-through for any Ollama model option (temperature, top_p, etc.) via config file
- Multi-backend support: Ollama (default), OpenAI-compatible (LM Studio, vLLM), llama.cpp server, or auto-detect
- It's just a client. No plan to have it start Ollama by itself or llama-cpp-python or anything
- Made for Ollama first, but supports OpenAI-compatible servers (LM Studio, vLLM) and llama.cpp server as first-class backends
- No conversation persistence yet (could use
agent.mdgeneration as lightweight conversation memory in the future) - No multi-model conversations
- No model templates (chat templates are handled by Ollama/your server)
- No multiline input (TextArea doesn't support suggesters, and I prefer a single-line input with autocomplete over multiline without it — or making Claude reinvent the wheel and turning this codebase from "only Claude and God understand it" to "only God understands it")
- No RAG, no agents, no tools
git clone https://github.com/baptisterajaut/ollama-chat
cd ollama-chat
# Optional: symlink to your bin
ln -s "$(pwd)/ochat.sh" ~/bin/ochatThe virtual environment and dependencies are created automatically on first run.
git clone https://github.com/baptisterajaut/ollama-chat
cd ollama-chat
ochat.batWindows support is experimental and untested.
ochat # Start chatting (first run launches setup wizard)
ochat -C # Configure (host, model, personality)
ochat -m llama3.2 # Override model
ochat --new # Create a new named config profile
ochat --use-config NAME # Use a named config for this session
ochat --use-config NAME --as-default # Switch to named config permanently
ochat -d # Enable debug logging
ochat --help # Show options| Key | Action |
|---|---|
Ctrl+C |
Cascade: clear input → cancel generation → double-press to quit |
Ctrl+D |
Quit |
Ctrl+L |
Clear chat (double-press to confirm) |
Ctrl+R |
Retry last response (double-press to confirm) |
Ctrl+U |
Undo last exchange (double-press to confirm) |
Ctrl+G |
Generate user message (ignored if input is non-empty) |
Escape |
Cancel generation |
Tab |
Accept suggestion / autocomplete command |
Destructive shortcuts (clear, retry, undo) require a confirmation press within 2s. Config toggles (streaming, thinking) are command-only (/stream, /thinking) — they're settings, not runtime actions.
| Command | Action |
|---|---|
/help |
Show help |
/retry |
Regenerate last response |
/undo |
Remove last exchange, restore user message to input |
/copy |
Copy last response to clipboard |
/clear |
Clear chat history |
/personality |
List/switch personalities |
/config |
List/switch config profiles (restarts app) |
/impersonate, /generate, /gen |
Generate suggested user response (long-form) |
/imps |
Short impersonate (under 15 words) |
/suggest |
Toggle auto-suggest after responses |
/thinking |
Toggle reasoning at inference level |
/stream |
Toggle streaming mode |
/project |
Toggle project prompt merge |
/prompt |
Show current system prompt |
/sys <msg> |
Inject a system message (alias: /system) |
/model |
Show current model |
/stats |
Show generation statistics (TTFT, t/s, tokens) |
/compact |
Summarize conversation to free context |
/context |
Show context info |
Config lives in ~/.config/ochat/:
config.conf- Default settings (host, model, context size, etc.)*.conf- Named config profilespersonalities/- System prompt templates (.mdfiles)
You can create and switch between different configurations (different models, personalities, settings):
ochat --new # Create a new profile from scratch
ochat --use-config my-creative # Use profile for this session only
ochat --use-config my-creative --as-default # Make it the new defaultOr switch in-app with /config. When switching with --as-default, your current config is backed up automatically.
Bundled personalities (copied on first run):
default- Helpful, concise assistantcreative- Brainstorming and unconventional ideasstoryteller- Narrative and creative writing
You can add any Ollama model parameter in config.conf. These are passed directly to the API without validation:
[model_options]
temperature = 0.7
top_p = 0.9
top_k = 40
min_p = 0.05
repeat_penalty = 1.1
# Any other Ollama option works tooSee Ollama docs for available options. Not exposed in the setup wizard - edit the config file manually.
Configure in config.conf under [defaults] backend: ollama, openai, llama_cpp, or auto.
Ollama (default): Full feature support — model listing, num_ctx, model_options, real token tracking.
OpenAI-compatible: Works with LM Studio, vLLM, text-generation-inference, and other servers exposing /v1/chat/completions. Uses the openai Python client. Limitations:
- No interactive model selection (model listing is used for connection testing only — you must configure the model name manually)
num_ctxandmodel_optionsare ignored (server-side settings apply)- Context usage tracking is disabled (percentages and warnings don't apply)
- Setup wizard won't work (configure
config.confmanually)
Llama.cpp: Uses llama.cpp server's /v1/chat/completions and /info endpoints. Supports num_ctx via /info and real token tracking via include_usage.
Auto: Tries Ollama → llama.cpp → OpenAI in sequence.
To use with LM Studio:
[defaults]
backend = openai
[server]
host = http://localhost:1234
[defaults]
model = your-loaded-model-nameFor HTTPS hosts with self-signed certificates, the setup wizard will detect SSL errors and offer to disable verification. You can also set it manually in config.conf:
[server]
host = https://my-homelab:11434
verify_ssl = falseThe greeting will show the backend type: Connected (Ollama), Connected (OpenAI), Connected (llama.cpp), or Connected (auto).
Logging is disabled by default. Use -d to enable:
ochat -dLog files are written to temp (auto-cleaned after 7 days):
- Unix/Mac:
/tmp/ochat-YYYYMMDD-HHMMSS.log - Windows:
%TEMP%\ochat-YYYYMMDD-HHMMSS.log
Average cyclomatic complexity: A (3.3) — no D or F.
Functions rated C (radon cc)
| Function | File | CC | Grade |
|---|---|---|---|
run_setup |
config.py | 18 | C |
_handle_undo |
commands.py | 14 | C |
main |
app.py | 14 | C |
_consume_chunks |
generation.py | 13 | C |
_handle_config_command |
commands.py | 12 | C |
_handle_command |
commands.py | 12 | C |
switch_config_to_default |
config.py | 12 | C |
_handle_compact |
commands.py | 11 | C |
_status_text |
app.py | 11 | C |
Textual is lovely for building TUIs but its default paths don't hold up under realistic LLM streaming workloads — long streamed Markdown plus a hidden Chain-of-Thought block exposes a few pathologies that had to be worked around to keep the UI responsive:
- Streaming Markdown rebuilds: the stock
Markdownwidget re-parses the entire buffer on everyappend(), blocking the event loop on long reasoning. Subclassed to run the parse in a thread pool (StreamingMarkdown), so chunks keep flowing while mistune chews. :hoveron large subtrees: a plain:hoverCSS rule on the reasoning block triggers a full stylesheet re-apply to every descendant on every mouse-move (thousands ofMarkdownBlockwidgets, deep selector matching). Replaced with inline-style toggles inon_enter/on_leaveand manual descendant cache invalidation — zero cascade cost, uniform tint.- Fire-and-forget
AwaitComplete:Markdown.update()returns anAwaitCompletethat schedules its task viaasyncio.gather()inside__init__. Calling it withoutawaitleaves an orphan task; at shutdown or lock contention you getCancelledError was never retrievedand can hang. Every call site now awaits. - Full-replace vs. incremental in non-stream mode: forwarding the accumulated reasoning to
Message.update(reasoning=...)on every chunk did an O(chunks × len) Markdown re-parse + remove-then-mount of every block. Reasoning is now rendered once at finalize, and the spinner body update is throttled to 10 Hz (Markdown.updateis ~20-50ms of DOM churn per call). - Visual-style cache on descendants: Textual caches per-widget composited colors in
_visual_style_cache. Inline style changes on a parent don't invalidate children's caches, so hover tint would only apply to empty cells, not text cells. Fixed by walking descendants and callingnotify_style_update()on hover enter/leave.
Net effect: reasoning streams with reasoning collapsed stay indistinguishable from plain text, hover-on-reasoning no longer freezes the UI for minutes, and non-stream generations render in real time instead of catching up several seconds after completion.
If you want a more full-featured Ollama TUI, check out parllama and oterm. Both are capable and well-maintained. Their busier interfaces weren't to my taste — this project exists because I wanted a clean, single-screen interface with no panels or sidebars to manage.
Public domain. You can't copyright AI-generated code anyway.
Frankly my dear, I don't give a damn.
