Initial commit: NexusOS project + desktop theme baseline

Captures the known-good state after theme consolidation and
consistency reconciliation:
- NexusOS GTK/xfwm4/icon theme assets (assets/themes, management/Mint-Y-Nexus)
- Tightened right-click menus, visible separators, no menu icons
- assets/themes/install-theme.sh: idempotent restore of all wiring
  (symlinks, xfconf xsettings+xfwm4, GTK 3/4 settings.ini)
- .gitignore excludes venv/ollama/models/runtime/db

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jon
2026-05-18 13:39:48 -05:00
commit b0aa0438af
1264 changed files with 19255 additions and 0 deletions
+532
View File
@@ -0,0 +1,532 @@
#!/usr/bin/env python3
"""Nexus Chat — interactive TUI for the Synapse backend."""
import curses
import httpx
import json
import os
import signal
import subprocess
import sys
import textwrap
import threading
import time
import uuid
NEXUS_ROOT = os.path.expanduser("~/nexus-core")
API_BASE = "http://localhost:8000"
MEMORY_BASE = "http://localhost:8001"
PID_FILE = os.path.join(NEXUS_ROOT, "runtime/pids/backend.pid")
LOG_FILE = os.path.join(NEXUS_ROOT, "runtime/backend.log")
VENV_PY = os.path.join(NEXUS_ROOT, "Promethean/bin/python3")
BACKEND_CMD = [
"uvicorn", "synapse.main:sio_app",
"--host", "0.0.0.0", "--port", "8000",
]
# ─── backend helpers ──────────────────────────────────────────────────────────
def backend_running() -> bool:
try:
return httpx.get(f"{API_BASE}/", timeout=2.0).status_code == 200
except Exception:
return False
def start_backend() -> int | None:
"""Start the backend and return its PID, or None on failure."""
os.makedirs(os.path.dirname(PID_FILE), exist_ok=True)
python = VENV_PY if os.path.exists(VENV_PY) else sys.executable
cmd = [python, "-m"] + BACKEND_CMD
with open(LOG_FILE, "a") as log:
proc = subprocess.Popen(
cmd, cwd=NEXUS_ROOT, stdout=log, stderr=log, start_new_session=True
)
with open(PID_FILE, "w") as f:
f.write(str(proc.pid))
for _ in range(30):
time.sleep(1)
if backend_running():
return proc.pid
return None
def stop_backend(pid: int):
"""Gracefully stop the backend process we started."""
try:
os.kill(pid, signal.SIGTERM)
except ProcessLookupError:
return
for _ in range(10):
time.sleep(0.5)
try:
os.kill(pid, 0)
except ProcessLookupError:
break
else:
try:
os.kill(pid, signal.SIGKILL)
except ProcessLookupError:
pass
try:
os.remove(PID_FILE)
except FileNotFoundError:
pass
def fetch_models() -> tuple[list[str], str]:
"""Return (model_list, selected_model)."""
try:
r = httpx.get(f"{API_BASE}/models", timeout=2.0)
if r.status_code == 200:
data = r.json()
models = data.get("models", [])
selected = data.get("selected") or (models[0] if models else "mistral")
return models, selected
except Exception:
pass
return ["mistral"], "mistral"
# ─── TUI ──────────────────────────────────────────────────────────────────────
SHORTCUTS = [
("^X", "Exit"),
("^R", "Regen"),
("^N", "New"),
("^L", "Clear"),
("^T", "Model"),
("PgUp", "Up"),
("PgDn", "Down"),
("/remember", "Save fact"),
]
# color pair indices
CP_HEADER = 1 # header bar
CP_SHORTBAR = 2 # shortcut bar bg
CP_USER = 3 # "You" label
CP_NEXUS = 4 # "Nexus" label
CP_DIVIDER = 5 # divider line
CP_STREAMING = 6 # streaming indicator
CP_STATS = 7 # token stats line
class NexusChat:
def __init__(self, stdscr):
self.scr = stdscr
self.conv_id = str(uuid.uuid4())
self.history = [] # [{"role": ..., "content": ..., "stats": {...}|None}]
self.input_buf = ""
self.scroll = 0 # lines scrolled up from bottom
self.streaming = False
self.stream_buf = ""
self._last_meta: dict | None = None
self._memory_saved: dict | None = None # set when Mistral saves a fact
self.models, self.model = fetch_models()
self.model_idx = self.models.index(self.model) if self.model in self.models else 0
self._lock = threading.Lock()
curses.start_color()
curses.use_default_colors()
curses.init_pair(CP_HEADER, curses.COLOR_BLACK, curses.COLOR_CYAN)
curses.init_pair(CP_SHORTBAR, curses.COLOR_BLACK, curses.COLOR_WHITE)
curses.init_pair(CP_USER, curses.COLOR_CYAN, -1)
curses.init_pair(CP_NEXUS, curses.COLOR_MAGENTA, -1)
curses.init_pair(CP_DIVIDER, curses.COLOR_CYAN, -1)
curses.init_pair(CP_STREAMING,curses.COLOR_YELLOW, -1)
curses.init_pair(CP_STATS, curses.COLOR_WHITE, -1)
curses.curs_set(1)
self.scr.keypad(True)
self.scr.timeout(50)
# ── geometry ──────────────────────────────────────────────────────────────
def dims(self):
return self.scr.getmaxyx() # (h, w)
def msg_rows(self, h):
# row 0 = header, row h-3 = divider, row h-2 = input, row h-1 = shortbar
return range(1, h - 3)
# ── drawing ───────────────────────────────────────────────────────────────
def _put(self, row, col, text, attr=0):
h, w = self.dims()
if row < 0 or row >= h:
return
try:
self.scr.addstr(row, col, text[:max(0, w - col)], attr)
except curses.error:
pass
def draw_header(self):
h, w = self.dims()
left = " NEXUS CHAT "
right = f" model: {self.model} "
if self.streaming:
mid = " streaming… "
mid_attr = curses.color_pair(CP_STREAMING) | curses.A_BOLD
else:
mid = ""
mid_attr = 0
gap = max(0, w - len(left) - len(right) - len(mid))
bar = left + " " * (gap // 2) + mid + " " * (gap - gap // 2) + right
self.scr.attron(curses.color_pair(CP_HEADER) | curses.A_BOLD)
self._put(0, 0, bar.ljust(w)[:w])
self.scr.attroff(curses.color_pair(CP_HEADER) | curses.A_BOLD)
def draw_shortbar(self):
h, w = self.dims()
# Fill bg
self.scr.attron(curses.color_pair(CP_SHORTBAR))
self._put(h - 1, 0, " " * (w - 1))
self.scr.attroff(curses.color_pair(CP_SHORTBAR))
# Draw key/label pairs
x = 1
for key, label in SHORTCUTS:
if x + len(key) + len(label) + 2 >= w:
break
self.scr.attron(curses.A_REVERSE)
self._put(h - 1, x, key)
self.scr.attroff(curses.A_REVERSE)
x += len(key)
self.scr.attron(curses.color_pair(CP_SHORTBAR))
self._put(h - 1, x, f" {label} ")
self.scr.attroff(curses.color_pair(CP_SHORTBAR))
x += len(label) + 3
def draw_divider(self):
h, w = self.dims()
self.scr.attron(curses.color_pair(CP_DIVIDER))
self._put(h - 3, 0, "" * (w - 1))
self.scr.attroff(curses.color_pair(CP_DIVIDER))
def draw_input(self):
h, w = self.dims()
prompt = " " # prompt
avail = w - len(prompt) - 1
display = self.input_buf[-avail:] if len(self.input_buf) > avail else self.input_buf
try:
self.scr.move(h - 2, 0)
self.scr.clrtoeol()
except curses.error:
pass
self._put(h - 2, 0, prompt)
self._put(h - 2, len(prompt), display)
def _build_lines(self, w):
"""Render history (+ live stream) into a flat list of (text, attr) tuples."""
lines = []
with self._lock:
history = list(self.history)
sbuf = self.stream_buf
in_stream = self.streaming
for msg in history:
role = msg["role"]
content = msg["content"]
if role == "user":
label = " You "
attr = curses.color_pair(CP_USER) | curses.A_BOLD
else:
label = " Nexus "
attr = curses.color_pair(CP_NEXUS) | curses.A_BOLD
lines.append((label, attr))
for wl in textwrap.wrap(content, w - 4) or [""]:
lines.append((f" {wl}", 0))
if role == "assistant":
stats = msg.get("stats")
if stats:
tok_s = f" {stats['tokens']} tok · {stats['elapsed_s']}s · {stats['tokens_per_s']} t/s"
else:
est = max(1, len(content) // 4)
tok_s = f" ~{est} tok (est)"
lines.append((tok_s, curses.color_pair(CP_STATS)))
mem = msg.get("memory")
if mem:
mem_s = f" + memory saved [{mem['section']}]: {mem['text'][:60]}"
lines.append((mem_s, curses.color_pair(CP_STREAMING) | curses.A_BOLD))
lines.append(("", 0))
if in_stream:
lines.append((" Nexus ", curses.color_pair(CP_NEXUS) | curses.A_BOLD))
for wl in textwrap.wrap(sbuf, w - 4) or [""]:
lines.append((f" {wl}", 0))
return lines
def draw_messages(self):
h, w = self.dims()
rows = self.msg_rows(h)
height = len(rows)
if height <= 0:
return
lines = self._build_lines(w)
total = len(lines)
if self.scroll == 0:
start = max(0, total - height)
else:
start = max(0, total - height - self.scroll)
visible = lines[start:start + height]
for i, row in enumerate(rows):
try:
self.scr.move(row, 0)
self.scr.clrtoeol()
except curses.error:
pass
if i < len(visible):
text, attr = visible[i]
if attr:
self.scr.attron(attr)
self._put(row, 0, text)
if attr:
self.scr.attroff(attr)
def draw(self):
h, w = self.dims()
self.draw_header()
self.draw_messages()
self.draw_divider()
self.draw_input()
self.draw_shortbar()
# cursor to end of input
prompt_len = 4 # " "
avail = w - prompt_len - 1
cx = prompt_len + min(len(self.input_buf), avail)
try:
self.scr.move(h - 2, cx)
except curses.error:
pass
self.scr.refresh()
# ── actions ───────────────────────────────────────────────────────────────
def send(self, message: str):
with self._lock:
self.history.append({"role": "user", "content": message})
prior = [m for m in self.history[:-1]]
self.streaming = True
self.stream_buf = ""
self.scroll = 0
def _stream():
capture_meta = False
capture_memory = False
try:
payload = {
"message": message,
"model": self.model,
"conversation_id": self.conv_id,
"history": prior,
}
with httpx.Client(timeout=180.0) as client:
with client.stream("POST", f"{API_BASE}/chat/stream",
json=payload) as resp:
for line in resp.iter_lines():
if line == "event: meta":
capture_meta = True
continue
if line == "event: memory":
capture_memory = True
continue
if capture_meta:
capture_meta = False
if line.startswith("data: "):
try:
with self._lock:
self._last_meta = json.loads(line[6:])
except Exception:
pass
continue
if capture_memory:
capture_memory = False
if line.startswith("data: "):
try:
with self._lock:
self._memory_saved = json.loads(line[6:])
except Exception:
pass
continue
if line.startswith("data: "):
with self._lock:
self.stream_buf += line[6:]
except Exception as e:
with self._lock:
self.stream_buf += f"\n[Error: {e}]"
finally:
with self._lock:
if self.stream_buf:
self.history.append({
"role": "assistant",
"content": self.stream_buf,
"stats": self._last_meta,
"memory": self._memory_saved,
})
self.streaming = False
self.stream_buf = ""
self._last_meta = None
self._memory_saved = None
threading.Thread(target=_stream, daemon=True).start()
def regenerate(self):
if self.streaming:
return
with self._lock:
if not self.history:
return
if self.history[-1]["role"] == "assistant":
self.history.pop()
last_user = None
for i in range(len(self.history) - 1, -1, -1):
if self.history[i]["role"] == "user":
last_user = self.history.pop(i)["content"]
break
if last_user:
self.send(last_user)
def cycle_model(self):
if self.streaming or not self.models:
return
self.model_idx = (self.model_idx + 1) % len(self.models)
self.model = self.models[self.model_idx]
def new_conversation(self):
if self.streaming:
return
with self._lock:
self.history = []
self.conv_id = str(uuid.uuid4())
self.stream_buf = ""
self.scroll = 0
def _cmd_remember(self, text: str):
"""Write a fact directly to the memory service, bypassing the LLM curator.
Syntax: /remember <text>
/remember [Section] <text>
"""
import re as _re
section = "General"
m = _re.match(r'^\[([^\]]+)\]\s*(.+)', text)
if m:
section, text = m.group(1).strip(), m.group(2).strip()
if not text:
return
try:
r = httpx.post(
f"{MEMORY_BASE}/memories",
json={"section": section, "text": text},
timeout=5.0,
)
if r.status_code == 200:
data = r.json()
saved = {"section": data["section"], "text": data["text"]}
with self._lock:
self.history.append({
"role": "assistant",
"content": f"Saved to memory [{saved['section']}]: {saved['text']}",
"stats": None,
"memory": saved,
})
else:
with self._lock:
self.history.append({
"role": "assistant",
"content": f"Memory service returned {r.status_code}",
"stats": None,
"memory": None,
})
except Exception as e:
with self._lock:
self.history.append({
"role": "assistant",
"content": f"Memory service unreachable: {e}",
"stats": None,
"memory": None,
})
# ── main loop ─────────────────────────────────────────────────────────────
def run(self):
while True:
self.draw()
try:
key = self.scr.getch()
except KeyboardInterrupt:
break
if key == -1:
continue
if key == 24: # ^X exit
break
elif key == 18 and not self.streaming: # ^R regen
self.regenerate()
elif key == 14: # ^N new conversation
self.new_conversation()
elif key == 12: # ^L clear (alias new)
self.new_conversation()
elif key == 20 and not self.streaming: # ^T cycle model
self.cycle_model()
elif key in (10, 13): # Enter send
msg = self.input_buf.strip()
if msg and not self.streaming:
self.input_buf = ""
if msg.lower().startswith("/remember "):
self._cmd_remember(msg[10:].strip())
else:
self.send(msg)
elif key in (127, curses.KEY_BACKSPACE, 8):
self.input_buf = self.input_buf[:-1]
elif key == curses.KEY_UP:
self.scroll += 1
elif key == curses.KEY_DOWN:
self.scroll = max(0, self.scroll - 1)
elif key == curses.KEY_PPAGE: # PgUp
h, _ = self.dims()
self.scroll += max(1, h - 6)
elif key == curses.KEY_NPAGE: # PgDn
h, _ = self.dims()
self.scroll = max(0, self.scroll - max(1, h - 6))
elif key == curses.KEY_HOME:
self.scroll = 9999 # jump to top
elif key == curses.KEY_END:
self.scroll = 0 # jump to bottom
elif 32 <= key <= 126:
self.input_buf += chr(key)
# ─── entry point ──────────────────────────────────────────────────────────────
def main():
print("Checking Nexus backend…", flush=True)
started_pid = None
if not backend_running():
print("Synapse is not running — starting backend…", flush=True)
started_pid = start_backend()
if started_pid is None:
print(f"ERROR: backend failed to start. Logs: {LOG_FILE}")
sys.exit(1)
print("Backend ready.", flush=True)
else:
print("Backend is running.", flush=True)
time.sleep(0.2)
try:
curses.wrapper(lambda s: NexusChat(s).run())
finally:
if started_pid is not None:
print("\nShutting down Synapse backend…", flush=True)
stop_backend(started_pid)
print("Done.", flush=True)
if __name__ == "__main__":
main()