DEV Community

Cover image for NeuralCleave v2.1.5: fixing a silent CLI bug, building a thinking indicator, and lessons from a solo AI project
Amit Chandra
Amit Chandra

Posted on

NeuralCleave v2.1.5: fixing a silent CLI bug, building a thinking indicator, and lessons from a solo AI project

I've been building NeuralCleave — a local-first personal AI assistant, The intelligence layer for every conversation — as a side project for the past several months. Last week I shipped v2.1.5. This post walks through what changed, why each change happened, and a few things I learned along the way.

If you've never heard of NeuralCleave: it's a self-hosted AI gateway that connects to 32 messaging platforms (Telegram, Discord, Slack, WhatsApp, Email, and more), routes requests across 13 LLM providers, keeps a 3-tier hierarchical memory (Redis → Qdrant → SQLite), and ships as both a Python package and a native Tauri desktop app. The elevator pitch is "OpenClaw, but local-first and open source."

This is a patch release — three focused changes, no new features. But two of them taught me things worth writing down.


The bug: terminal CLI routing

NeuralCleave's desktop app has an embedded terminal. It's a real shell session running inside the Tauri WebView — you can use it as a normal terminal, but it also knows about the running gateway. Commands like neuralcleave status, neuralcleave channels list, and neuralcleave memory search "query" are supposed to route through to the gateway process and return structured output.

In v2.1.4, this was silently broken.

"Silently" is the key word. The command would execute, the terminal prompt would return, and everything looked fine. There was no error. The gateway log showed the request arriving. But the response never made it back to the terminal.

What was actually happening

The gateway communicates with the terminal over a local socket. The handshake was completing correctly — the socket was open, the connection was established. The gateway processed the request and wrote the response. The terminal read function was called. And then nothing.

The problem was in the response pipe on the terminal side. We were calling read() on the socket, but not awaiting it in the right context. The read returned immediately — the response buffer wasn't ready yet — and we discarded the result. The gateway had done its job; we just threw away what it sent back.

The reason neuralcleave --help worked fine is that --help is handled entirely before the socket call. It never touches the IPC layer. So the quick sanity check everyone does first — including me, several times — passed with no issues.

The fix

Wait for the response buffer to actually be ready before reading. There's nothing clever about it. The bug was obvious in retrospect. What made it hard to find was the complete lack of any error signal — the system completed its execution path, it just silently discarded the output.

Lesson: silent success is sometimes more dangerous than an error. An error gives you a stack trace. Silent success gives you nothing to grep for.

Commands that now work correctly from inside the embedded terminal:

neuralcleave status
neuralcleave channels list
neuralcleave memory search "your query"
neuralcleave chat "send a message"
neuralcleave config show
Enter fullscreen mode Exit fullscreen mode

The feature: multi-phase thinking indicator

This one started from a UX frustration rather than a bug report.

When you send a complex message to NeuralCleave — something that requires searching long-term memory, retrieving context from multiple tiers, and reasoning across a long prompt — the response can take 6–12 seconds. During that time, the previous implementation showed a spinner.

A spinner communicates one thing: something is happening. It doesn't tell you what, or how far along, or whether you should be worried.

I kept finding myself wondering if my message had even been sent. Or if the gateway had crashed quietly. The spinner gave me no information to act on.

The model: Claude's thinking UI

Claude does this well. While it's processing, it surfaces what it's actually doing:

  • Thinking...
  • Reading files...
  • Searching the web...
  • Writing code...

Each phase is genuinely informative. "Searching the web" tells you the request is doing retrieval. "Writing code" tells you it's in generation mode. You understand where you are in the process, which makes the wait feel intentional rather than broken.

I built something similar for NeuralCleave:

const THINKING_PHASES = [
  "Thinking",
  "Searching memory",
  "Analyzing context",
  "Reviewing context",
  "Crafting response",
];

function ThinkingIndicator() {
  const [phase, setPhase] = useState(0);
  const [visible, setVisible] = useState(true);

  useEffect(() => {
    const timer = setInterval(() => {
      setVisible(false);
      const swap = setTimeout(() => {
        setPhase((p) => (p + 1) % THINKING_PHASES.length);
        setVisible(true);
      }, 220);
      return () => clearTimeout(swap);
    }, 2000);
    return () => clearInterval(timer);
  }, []);

  return (
    <div className="flex items-center gap-3 py-4">
      <img
        src="/logo.png"
        alt=""
        className="w-6 h-6 animate-pulse opacity-80"
      />
      <span
        className="text-sm font-medium text-muted"
        style={{
          opacity: visible ? 1 : 0,
          transition: "opacity 0.2s ease",
        }}
      >
        {THINKING_PHASES[phase]}
        <span className="inline-flex gap-0.5 ml-1.5">
          {[0, 1, 2].map((i) => (
            <span
              key={i}
              className="w-1 h-1 rounded-full bg-current inline-block"
              style={{
                animation: `bounce 1.2s ease-in-out ${i * 0.2}s infinite`,
              }}
            />
          ))}
        </span>
      </span>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The logic is straightforward:

  • Every 2000ms the component fades out (visible = false)
  • After 220ms — enough for the opacity transition to finish — the phase index increments and it fades back in
  • Phases cycle continuously: Thinking → Searching memory → Analyzing context → Reviewing context → Crafting response → Thinking again

The phases don't correspond 1:1 to what the model is literally doing at each moment. The timing is approximate. But that's fine — the goal isn't accuracy, it's reducing anxiety during the wait.

The UX principle behind it

Perceived performance is not the same as actual performance.

A 6-second wait with a spinner feels broken. The same 6 seconds with cycling status messages feels like a system working through something real. You're not waiting any less — but the experience of waiting is completely different.

There's research on this going back to the 1990s (Jakob Nielsen's response time studies, later extended by Brad Myers), but you don't need the papers to feel it. Use both versions back-to-back and notice which one makes you want to close the tab.

This is also why progress bars that slightly speed up near the end feel better even when inaccurate, and why elevators have mirrors. The mirror doesn't make the elevator faster. It makes the wait feel shorter. The same principle applies directly to AI response UX.


The polish: dynamic chat width and toolbar redesign

These are smaller changes — no new capabilities, just things that bothered me every day of using the app.

Dynamic chat width

NeuralCleave has a collapsible sidebar that shows conversation history. When the sidebar is open, the chat area is narrower. When it's closed, that space becomes available.

Previously the chat had a fixed max-width: 820px regardless of sidebar state. With the sidebar closed there was a lot of unused space on both sides. With it open the chat felt slightly cramped. Neither was ideal.

The fix:

const chatWidth = sidebarOpen
  ? "min(820px, 100%)"
  : "min(1000px, 100%)";

// Applied to both the message list and the input bar:
<div
  style={{
    maxWidth: chatWidth,
    transition: "max-width 0.2s ease",
  }}
>
Enter fullscreen mode Exit fullscreen mode

Two values. One CSS transition. The chat area smoothly expands when the sidebar collapses and contracts when it opens. No layout jump, no reflow flash, no extra state to manage.

Toolbar redesign

The History and New Chat buttons were plain <button> elements with minimal styling. They worked but looked like placeholders — mismatched with the rest of the interface.

They're now a grouped pill control: a single rounded container holding both actions, separated by a 1px divider, with proper hover and active states and a subtle glass background. Entirely cosmetic. But UI elements that look like they belong together make the whole interface read as intentional, and "intentional" is what makes people trust a tool enough to use it daily.


By the numbers

This release maintains the full test suite that has been growing since v2.0:

Suite Tests
Unit — core gateway ~1,800
Integration — channel adapters ~1,400
API contract ~900
Frontend contract ~600
CLI + tools ~376
Total 5,076

All passing on v2.1.5, Python 3.12, Windows and Linux.


Installing or upgrading

Python:

# Fresh install
pip install neuralcleave==2.1.5

# Upgrade from an earlier version
pip install --upgrade neuralcleave
Enter fullscreen mode Exit fullscreen mode

Verify:

neuralcleave --version
# NeuralCleave v2.1.5
Enter fullscreen mode Exit fullscreen mode

Start the gateway:

neuralcleave start
# Gateway running at http://localhost:8000
# WebSocket at ws://localhost:8000/ws
Enter fullscreen mode Exit fullscreen mode

Desktop app (Windows):

Both .exe installer and .msi are available at neuralcleave.com/#download.


Quick-start if you're new

# Install
pip install neuralcleave

# First-run setup wizard
neuralcleave setup

# Start the gateway
neuralcleave start

# In a second terminal — send your first message
neuralcleave chat "What can you do?"
Enter fullscreen mode Exit fullscreen mode

For the desktop app, download the installer, run it, and the app handles the rest — it bundles the gateway, starts it on launch, and connects the UI automatically.

To connect Telegram (the most common first integration):

# Get a bot token from @BotFather on Telegram, then:
neuralcleave channels add telegram --token YOUR_BOT_TOKEN
neuralcleave channels start telegram
Enter fullscreen mode Exit fullscreen mode

Your Telegram bot is now connected to the gateway. Every message you send to the bot routes through NeuralCleave, hits your configured LLM, uses your memory, and responds back in Telegram.


What the project is and where it's going

NeuralCleave started as a personal tool. I wanted a single AI assistant that could reach me across every platform I use — Telegram for quick questions, email for longer threads, Discord for group channels — with memory that persists across all of them. Everything I found either required giving data to a cloud service, was limited to 2–3 integrations, or needed dedicated infrastructure I didn't want to maintain.

So I built it.

Current state of the project:

  • 32 channel adapters (Telegram, Discord, Slack, WhatsApp, Email, Signal, RSS, webhooks, and more)
  • 13 LLM providers (Claude, GPT-4o, DeepSeek, Gemini, Mistral, Ollama for local models, and more)
  • 3-tier hierarchical memory: Redis (short-term, fast), Qdrant (semantic vector search), SQLite (permanent long-term)
  • Agent Orchestrator for routing tasks across named agent nodes
  • Skills Marketplace with a PackageScanner safety gate
  • Live Canvas renderer for markdown, code, and HTML output
  • Prometheus metrics (13 built-in metrics)
  • Embedded terminal with CLI routing
  • Native Tauri desktop app for Windows
  • Python package for headless/server deployment

It is genuinely usable as a daily driver. I use it every day.

What's coming:

The roadmap is OpenClaw feature parity. The near-term priorities are:

  • Voice pipeline — wake word detection → Whisper STT → LLM → TTS → audio playback, fully local
  • Web PWA mode — run NeuralCleave in the browser without installing the desktop app
  • Improved multi-agent coordination — better task decomposition and result synthesis across Orchestrator nodes
  • Inline memory surfacing — relevant past context appearing inline in the chat, not just on explicit search

If any of that sounds interesting, follow along on GitHub. Issues, PRs, and honest critical feedback are all welcome. The project moves faster with outside eyes on it.


Links


Solo project, built on weekends and evenings. If something is broken, it is probably my fault — open an issue and I will fix it fast.

Top comments (0)