Commit Graph

189 Commits

Author SHA1 Message Date
Scott Idem
3b3456600a feat: store and display backend + host metadata on chat messages
Each assistant message in the session JSON now carries:
  backend, backend_label, host (platform.node())

These fields are shown as model tags in the UI — on live responses and
when loading session history. Session log entries (sessions/YYYY-MM-DD.md)
include the backend label and host in the turn header.

The local (OpenAI-compat) backend strips non-standard fields before
sending messages to the API so extra fields don't leak upstream.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 22:16:48 -04:00
Scott Idem
6c84d6ae72 docs: update README and CLAUDE.md for install script and backup
- README: add Setup/Install section (install.py), Development Workflow
  (dev-restart.sh), and Backup (restic/backup.sh) sections; fix
  .env.example reference → .env.default
- CLAUDE.md: replace stale Docker start command with install.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:36:48 -04:00
Scott Idem
2629983452 fix: create cortex config dir and restic password file during install
Previously the ~/.config/cortex/ directory and restic-password were only
created on the first backup run. Now install.py creates them eagerly so
the user can verify setup and back up the password immediately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:24:08 -04:00
Scott Idem
f668826f79 fix: bootstrap pip via ensurepip if missing from venv
On Arch Linux, venvs can be created without pip seeded in.
Detect the missing module before attempting pip install and
recover with `python -m ensurepip --upgrade`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:17:52 -04:00
Scott Idem
576f22216a feat: restic backup of home/ with systemd daily timer
- backup.sh — backs up home/ (persona data, memory, tasks, crons) to a
  local restic repo; auto-generates password on first run, prunes to
  7d/4w/6m retention; excludes sessions/ and session_data/
- install.py — setup_backup_timer() installs cortex-backup.service +
  cortex-backup.timer (daily 03:00, Persistent=true); skips gracefully
  if restic is not installed

Password lives at ~/.config/cortex/restic-password (chmod 600, not in git).
Repo defaults to ~/backups/cortex-home-restic; override via RESTIC_REPOSITORY.

Per-user encrypted backup is a noted future feature.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 19:36:22 -04:00
Scott Idem
bf800acca8 chore: remove home/ from git, update gitignore
All user persona data (memory, tasks, crons, identity files, sessions)
was previously committed. This removes it from tracking and adds home/
to .gitignore — files remain on disk and will continue to sync via
Syncthing. Backup should be handled via encrypted per-user means.

Also added: cortex/.env*.bak and cortex/=* (pip artifact) to gitignore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 19:19:23 -04:00
Scott Idem
d9a322164a feat: OpenAI-compatible orchestrator + backend auto-routing
- openai_orchestrator.py — new ReAct tool loop engine for any
  OpenAI-compatible endpoint (OpenRouter, Open WebUI, Ollama, LiteLLM);
  model handles both tool loop and final response, no Claude handoff needed
- tools/__init__.py — auto-derive OpenAI JSON Schema from existing Gemini
  FunctionDeclarations so tool definitions have a single source of truth
- routers/orchestrator.py — route to openai_orchestrator when model registry
  "orchestrator" role resolves to a local_openai type host
- routers/chat.py — pass role to _backend_label(); fix fallback_used logic
  (only meaningful for explicit backend overrides, not auto-routing)
- static/app.js — add null/"auto" to backend cycle; fetch local model hint
  without overriding the auto default on page load
- model_registry.py — _normalize() back-fills host_type on old registry files
- requirements.txt — add openai>=1.0.0
- ARCH__BACKENDS.md — document OpenAI-compat backend and routing logic

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 19:18:18 -04:00
Scott Idem
8ba5247ef5 tooling: install script, workspace file, and dev-restart helper
- install.py — idempotent setup script (venv, systemd service, linger,
  auth checks); supports --check for read-only status inspection
- .stignore — exclude .venv and runtime dirs from Syncthing so each
  host maintains its own machine-local venv
- Cortex_and_Inara.code-workspace — VS Code workspace (service, personas,
  docs folders; launch config for uvicorn --reload)
- dev-restart.sh — SSH wrapper to restart Cortex on the gaming laptop
  and tail logs; supports restart / logs / status subcommands

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 19:11:27 -04:00
Scott Idem
a6e404c143 feat: host_type field for OpenRouter / OpenAI-compatible API support
Adds host_type ("openwebui" | "openai") to the host schema so Cortex can
talk to both Open WebUI/Ollama and OpenRouter/standard-OpenAI endpoints.

Path differences per type:
  openwebui (default): /api/chat/completions, /api/models
  openai:              /chat/completions,     /models

model_registry.py:
  - host_type added to host schema (default "openwebui", backward compat)
  - save_host() accepts host_type parameter
  - _resolve_model() passes host_type through with the merged host fields

llm_client._local():
  - Reads host_type from resolved model_cfg
  - Selects correct chat completions path accordingly

routers/local_llm.py:
  - save_host route accepts host_type form field
  - fetch-models uses /models for openai type, /api/models for openwebui
  - Existing host rows show type selector pre-filled from stored value

local_llm.html:
  - "Add host" form includes type selector

To use OpenRouter:
  - Add host: URL = https://openrouter.ai/api/v1, Type = OpenAI-compatible
  - API key from openrouter.ai (store in .env or model_registry.json only)
  - Fetch models or add manually (e.g. anthropic/claude-sonnet-4-5-20251022)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 21:11:22 -04:00
Scott Idem
2dd94696d5 fix: model-tag color was #334155 (invisible on dark theme) → #475569 2026-04-05 22:25:09 -04:00
Scott Idem
8570e8d852 fix: backend toggle not sent to server; add per-message model tag
Fixes:
  - app.js was tracking primaryBackend locally but never included
    model: primaryBackend in the /chat POST body, so the server always
    used settings.primary_backend regardless of what the user clicked.
    Now model: primaryBackend is sent on every chat request.

  - Responses were only annotated when fallback occurred. Now every
    assistant message shows a small model tag at the bottom right.

chat.py:
  - _backend_label() resolves human-readable name:
      claude → "Claude", gemini → "Gemini",
      local → registry label (e.g. "Gemma 4 E4B") or model_name
  - SSE payload now includes backend_label field

app.js:
  - model: primaryBackend added to /chat fetch body
  - After every response, appends .model-tag div with backend_label
  - Fallback shows " fallback → <label>" in amber; normal is muted
  - Removed separate system message for fallback (tag covers it)

style.css:
  - .model-tag: small muted text, right-aligned, separated by thin line
  - .model-tag.fallback: amber (#f59e0b)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 22:10:40 -04:00
Scott Idem
9299ce5ba6 test: model registry unit test suite (45 tests)
Covers model_registry.py without requiring a running service or LLM:

  Empty/fresh state: no files, missing user dir
  Save/load: round-trip, corrupt file fallback
  Migration: v1 hosts/models, v1 no active, v0 flat, v0 empty url,
             distill_backend_mid=local → distill role, saves file after migrate
  Built-in resolution: claude_cli, gemini_api, gemini_cli, unknown → None
  User model resolution: local_openai merges host, missing host → None
  get_model_for_role: registry primary, built-in from registry, skips missing,
                      walks full backup chain, .env fallback, hardcoded fallback,
                      custom roles
  get_best_local_model: prefers role chain, falls back to first local, None if no local
  Host CRUD: create, update, unknown ID creates new, remove + cascades to models
  Model CRUD: create, update, remove + clears role refs
  set_role: assign model, assign built-in, clear with None, invalid slot,
            unknown model ID, creates new role key
  get_defined_roles: returns all settings roles, fills gaps with {}
  Multi-user isolation: registries don't bleed across users

All tests use tmp_path + patch.object(config.settings, ...) — no real files touched.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 21:48:00 -04:00
Scott Idem
608e1de246 feat: model registry UI — hosts, models, role assignments
Replaces the single-host local model settings page with a full model
registry interface at /settings/local.

Hosts section:
- List existing hosts with inline edit + save + remove
- Collapsible "Add host" form
- Per-host "Fetch models" button

Models section:
- List all models with label, model name, host, context_k badge, tags
- Remove button

Add Model section:
- Host dropdown, label, model name, context_k, tags (comma-separated)
- "Fetch models from host" with auto-fill picker

Role Assignments section:
- One row per defined role (chat, orchestrator, distill, coder, research)
- Primary + backup_1 + backup_2 dropdowns per role
- Dropdowns pre-filled from registry on load
- AJAX save on change (POST /api/models/role) with toast confirmation
- Built-in models (claude_cli, gemini_cli, gemini_api) always available in dropdowns

Backend:
- All user_settings references replaced with model_registry
- host/{id}/remove route added
- fetch-models now accepts host_id query param
- POST /api/models/role for AJAX role assignment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 21:31:32 -04:00
Scott Idem
6a1a1c2686 feat: unified model registry with role-based routing
Introduces model_registry.py as the single source of truth for all LLM
backend configuration. Replaces scattered backend settings across user_settings,
config distill_backend_*, and the UI toggle.

model_registry.py:
- Per-user home/{user}/model_registry.json with version, hosts, models, roles
- Models have: type (local_openai|claude_cli|gemini_cli|gemini_api), label,
  model_name, host_id, context_k (tokens), tags (capability labels)
- Roles map to priority chains: primary, backup_1..backup_4
- Built-in IDs (claude_cli, gemini_cli, gemini_api) always resolvable
- Auto-migrates existing local_llm.json on first access
- CRUD: save_host, remove_host, save_model, remove_model, set_role
- get_model_for_role(): registry → .env default → hardcoded fallback

config.py:
- role_chat/orchestrator/distill/coder/research .env defaults
- defined_roles: comma-separated standard role list (extensible)
- get_defined_roles() and get_role_default() helper methods

llm_client.complete():
- New role= parameter (default "chat") for registry-based routing
- model= still accepted for explicit UI toggle override
- _claude() and _local() accept model_cfg dict instead of raw string
- _local() uses pre-resolved config from registry

memory_distiller.py:
- distill_mid/long now use role="distill" (no more distill_backend_* .env vars needed)

cron_runner.py:
- brief jobs use role="chat"

routers/chat.py + auth.py:
- Use model_registry instead of user_settings for local model info

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 21:25:18 -04:00
Scott Idem
a4daebdc9b feat: local LLM multi-model, session search, cron proactive types, notifications, docs overhaul
Local LLM:
- user_settings.py: per-user hosts/models config (local_llm.json)
- routers/local_llm.py + static/local_llm.html: dedicated settings page
- llm_client.py: local OpenAI-compatible backend via httpx
- config.py: LOCAL_API_URL/KEY/MODEL + per-backend timeouts
- Active model shown near backend toggle (amber hint text)

Memory distillation:
- memory_distiller.py: DISTILL_BACKEND_MID/LONG .env overrides
- scheduler.py + notification.py: notify NC Talk after mid/long distill
- notification.py: outbound channel abstraction (NC Talk, extensible)

Session search:
- routers/files.py: GET /sessions/search?q= with excerpts grouped by date
- static/index.html + app.js: search UI in file sidebar with highlight
- _esc() helper to prevent XSS in search results

Proactive cron:
- cron_runner.py: new job types — message (send directly) and brief (LLM + send)
- Both support optional per-job channel override

Channels:
- routers/nextcloud_talk.py: consolidated using notification._send_nct_message()
- routers/auth.py: local backend status in /auth/status
- routers/chat.py: /backend returns {primary, fallback, local_model} object

UI / UX:
- Copy button for user messages (matching assistant)
- Autocomplete disabled on sensitive form fields
- settings.html: local model section replaced with link to /settings/local

Docs overhaul:
- MASTER.md hub + ARCH__SYSTEM/BACKENDS/PERSONA/CHANNELS/FUTURE.md
- ARCH__Intelligence_Layer.md replaced with redirect table
- CORTEX.md trimmed to vision only; README updated
- OPEN_WEBUI_API.md added to docs/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 20:53:06 -04:00
Scott Idem
bd6532e93a feat: shared nav bar on Help and Settings pages
Replaces the lone "← Back to Cortex" link with a consistent page-nav
on both pages: ← Chat | Help | Settings | Sign out

Active page is highlighted purple; others are muted gray.
Settings page gets a {{ help_href }} template var from settings.py.
Help page builds nav links from the existing cfg JS object.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 22:09:08 -04:00
Scott Idem
a94fdc869d docs: fix Gitea SSH URL to use git.dgrzone.com
cortex subdomain works incidentally but git.dgrzone.com is correct.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 21:53:54 -04:00
Scott Idem
1fefd42e19 docs: Gitea SSH port 2222 verified working
WAN port forward confirmed end-to-end. Clone URL:
ssh://git@cortex.dgrzone.com:2222/<user>/<repo>.git

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 21:52:36 -04:00
Scott Idem
0c17b4b1ab docs: overhaul TODO__Agents.md to reflect current state
Moved to completed: token expiry restart, Holly onboarding, per-user
channel config, Google OAuth, per-user Gemini key, session persistence,
persona picker, Lucide icons, favicon, Help shared base, reminders tools,
Brian onboarding.

Updated in-progress: knowledge consolidation tools (ae_journal_* done,
import script still pending). NC Talk and Google Chat notes updated for
per-user routing. Removed stale "default user only" notes.

High priority now: Ollama backend, Gitea SSH verification.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 21:44:56 -04:00
Scott Idem
cec6d3e23a docs: update README for current state
- .env location: cortex/.env + cortex/.env.example (not project root)
- Webhook endpoints: per-user /webhook/nextcloud/{username} and /channels/google-chat/{username}
- Personas table: added brian/wintermute and scott/developer
- Docs table: added GOOGLE_CHAT_BOT.md, cortex/static/HELP.md
- Channels section: per-user webhook note + links to setup docs
- User management: added google-add command and channels.json note
- Removed stale Inara/Tina-only framing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 21:42:11 -04:00
Scott Idem
2d3a380d6b docs: add Tools & Modes protocol to all personas and template
Every persona now knows: direct chat has no tools, Agent mode () has
the full tool suite. If asked to write a reminder/task/etc in chat mode,
tell the user to switch modes rather than silently failing.

Updated: inara, tina, donut, wintermute, developer, cleo PROTOCOLS.md
Updated: persona_template.py so all future personas get this by default

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 21:35:54 -04:00
Scott Idem
662924c6a1 fix: pass user to _run_job so get_user_gemini_key resolves correctly
NameError: name 'user' is not defined in orchestrator._run_job —
user was resolved in the endpoint but not forwarded to the background task.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 21:21:14 -04:00
Scott Idem
e5b6d58889 feat: reminders_add and reminders_list tools
- New cortex/tools/reminders.py with reminders_add, reminders_list, reminders_clear
- reminders_clear moved here from cron.py (cron still imports from same file)
- __init__.py: wired up new callables and Gemini declarations
- Inara can now add/read reminders in Agent mode via the orchestrator

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 21:14:22 -04:00
Scott Idem
6b725afc3e docs: update NC Talk doc with real container name, commands, and logs section
- Use dgr_zone_nextcloud-app-1 throughout (actual container name)
- talk:bot:uninstall (not remove — wrong command in previous version)
- Added Logs section: occ log:tail + journalctl
- Bruteforce reset command now includes full docker exec form

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 13:47:43 -04:00
Scott Idem
ddf5dd6338 docs: add Google Chat setup guide, update NC Talk for per-user routing
- docs/GOOGLE_CHAT_BOT.md: new step-by-step guide covering channels.json,
  Google Cloud Console config, JWT audience, and troubleshooting
- docs/NEXTCLOUD_TALK_BOT.md: updated for per-user endpoints
  (/webhook/nextcloud/{username}), channels.json config, removed old
  server-level .env references, updated Multi-User note

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 13:24:36 -04:00
Scott Idem
93f7f44e51 feat: per-user channel config for Google Chat and Nextcloud Talk
- New endpoints: POST /channels/google-chat/{username} and /webhook/nextcloud/{username}
- Channel secrets/config live in home/{username}/channels.json (gitignored)
- auth_utils: get_user_channels() helper reads channels.json
- Both routers load persona, audience/secret, backend, timeout per user;
  set_context() wires the correct persona before building the system prompt
- Removed server-level channel settings from config.py and .env —
  no user gets a channel until they create their own channels.json
- .gitignore: home/**/channels.json added

To migrate: update Google Chat Add-on webhook URL to /channels/google-chat/{username}
and re-register NC Talk bot at /webhook/nextcloud/{username}

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 13:02:45 -04:00
Scott Idem
496da58f58 chore: consolidate .env files — one .env in cortex/, one .env.example
- Removed orphaned root .env and .env.default (values already in cortex/.env,
  which is what the systemd service actually loads)
- Replaced outdated cortex/.env.example with the comprehensive .env.default content
- Also tracks: tested/persona/cleo/ (new test persona), Inara memory updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 12:22:49 -04:00
Scott Idem
8e20bfbea8 feat: shared Help base, Google OAuth live, new personas, cleanup
- cortex/static/HELP.md: shared Help & Reference base served to all users
- help.html: loads shared base + appends persona-specific HELP.md if present
- inara/HELP.md: cleared (content moved to shared base)
- Google OAuth: registered scott.idem@oneskyit.com; flow now working end-to-end
- .gitignore: exclude home/**/sessions/ (runtime logs)
- New personas tracked: home/holly/persona/donut/, home/scott/persona/developer/
- Removed orphans: holly/, personas/, cortex-holly.service
- CLAUDE.md: updated current state and recently completed list to 2026-03-27

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 22:55:45 -04:00
Scott Idem
3a94df1eaf fix: prevent password managers autofilling Gemini API key field
Change type="password" to type="text" — the main signal password
managers use. Also add autocomplete="off", data-lpignore, data-1p-ignore
for broader coverage across Bitwarden, 1Password, LastPass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 21:57:14 -04:00
Scott Idem
ce806e52ed fix: google-add now also sets profile.json email
The invite command reads email from profile.json, not auth.json.
google-add was only writing to auth.json so invite had no address
to send to. Now calls set_email() as well.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 21:32:48 -04:00
Scott Idem
7438031797 feat: connected accounts + Gemini API key in account settings UI
Settings page gains two new sections:
- Connected Accounts: shows linked Google email (read-only)
- Gemini API Key: paste personal key from aistudio.google.com,
  shows masked hint of saved key, remove link to revert to server key

POST /settings/gemini-key saves/clears gemini_api_key in auth.json.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 21:16:37 -04:00
Scott Idem
8aec6aafcc feat: Google OAuth sign-in + per-user Gemini API key
Users with Google accounts can now sign in without a password.

Auth flow:
- GET /auth/google → Google consent page (CSRF state cookie)
- GET /auth/google/callback → exchange code, lookup user, set JWT
- auth.json gains google_sub + google_email fields
- set_password() no longer overwrites unrelated auth.json fields

Admin setup:
  python manage_passwords.py google-add <username> <email>
  # add GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET to .env

Per-user Gemini key:
- get_user_gemini_key() reads gemini_api_key from auth.json
- orchestrator_engine.run() accepts gemini_api_key param
- orchestrator router passes user's key, falls back to server key

login.html: "Sign in with Google" button above the password form.
manage_passwords.py list: now shows auth method columns (pw / google).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 21:01:52 -04:00
Scott Idem
62fde62653 feat: persona-specific favicon + fix favicon.ico 404
app.js updates the <link rel="icon"> to the active persona's emoji on
load (CORTEX_EMOJI is already injected server-side). /favicon.ico route
added as a fallback for login/settings/help pages that don't have
persona context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 23:45:36 -04:00
Scott Idem
f8d89bc272 fix: close SSE connection cleanly on page navigation
beforeunload closes the EventSource explicitly so the browser doesn't
log "connection interrupted while page was loading". onerror handler
suppresses auto-reconnect noise if the connection temporarily drops.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 23:44:12 -04:00
Scott Idem
92350f7a7b feat: persist active session across page navigation with inactivity TTL
Session ID is stored in localStorage keyed to user+persona. On page load
it's silently restored if within 30 min of last activity. Timestamp
updates on every sent message. New session / delete session clears the
stored ID so the TTL logic stays consistent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 23:38:04 -04:00
Scott Idem
4f09823afe feat: Lucide icons on edit/del/copy and inline edit save/cancel buttons
pencil → edit, trash-2 → del, copy → copy, check → copied feedback,
check → Save, x → Cancel. All small action buttons get inline-flex
alignment for consistent icon+label layout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 23:32:19 -04:00
Scott Idem
826bd6cfe3 feat: /{username} persona picker landing page
Visiting /scott (or any user root) now shows a clean card page listing
all their personas with emoji + name, each linking to /{user}/{persona}.
Previously the route was unhandled (404 or wildcard match).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 23:19:04 -04:00
Scott Idem
c3507f8e11 fix: help page back link preserves active persona
Pass ?persona= query param on the help link so the server knows which
persona to return to. Previously always defaulted to personas[0], causing
navigation back to the wrong persona.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 23:13:52 -04:00
Scott Idem
65548ebf36 feat: Lucide SVG icons throughout main UI
Replace all emoji/unicode icons with Lucide SVG icons:
- Mode select dropdown: message-circle / pencil / lock / bot
- Send button: arrow-up (chat/OTR), pencil (note), zap (agent)
- Stop button: square icon
- Header nav already had Lucide SVGs; render_icons() now called at init

Add icon_html() + render_icons() helpers; update update_mode_ui() and
open_mode_dropdown() to use innerHTML + lucide.createIcons(). CSS: .btn-icon
alignment, inline-flex on .hdr-btn / .hdr-dd-item / #send / #stop.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 23:06:01 -04:00
Scott Idem
24c9f52b49 fix: agent mode icon — 🥸 in dropdown, on Run button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 22:51:51 -04:00
Scott Idem
3bada5f311 fix: font sizes 21/25/17px, icon labels on send btn, no-wrap fix
- Sync preload script font sizes to match app.js (21/25/17px)
- Send button variants now show icons: ↑ Send, 📝 Note,  Run
- Remove fixed width on send-col; add white-space:nowrap + padding
  so "📝 Note" never wraps regardless of font size

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 22:43:34 -04:00
Scott Idem
cf277f822e feat: Inter font, weight 450, bumped base sizes across all pages
- Load Inter variable font from Google Fonts on all 5 HTML pages
- font-weight: 450 on body (between regular and medium — fixes thin feel)
- -webkit-font-smoothing: antialiased for cleaner screen rendering
- Base font size: normal 16→17px, large 18→19px, small 14→15px
- Applies consistently to main UI, login, setup, settings, and help pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 22:20:10 -04:00
Scott Idem
6cf10d2755 feat: UI redesign — compact mode select, consolidated header nav
Header:
- Sessions, ⚙ context panel, ≡ settings dropdown (Files, Account,
  Sign Out), and  help — down from 6+ individual buttons
- Responsive: flex-row on desktop, wraps on mobile with labels hidden

Footer (input area):
- 4-way mode select replaces the button row — shows only the active
  mode as [icon] [label] ▲; click opens an upward dropdown
- Options sorted by MRU: most recently used floats to the bottom
  (closest to the trigger button) for quick re-selection
- Current mode marked with ✓
- Note mode shows a small prv/pub sub-toggle below the select button
- Mobile: textarea on top (full width), mode select + send on one row

Mode state consolidated from 3 booleans into a single current_mode
variable with localStorage persistence and MRU tracking.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 22:08:33 -04:00
Scott Idem
fa04b5e6b0 feat: off the record mode (OTR)
Adds a third input mode toggle alongside Note and Agent. When active:
- Textarea gets a subtle purple tint with dashed border
- OTR button highlights purple
- Placeholder reads "Off the record — not logged or distilled…"
- off_record=True is sent to /chat; session_logger is skipped
- In-memory session context is preserved within the session

Switching to Note or Agent mode deactivates OTR, and vice versa.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 21:07:21 -04:00
Scott Idem
b3f40cf437 fix: message input placeholder uses active persona name
Hardcoded 'Inara' replaced with CORTEX_PERSONA in all placeholder
strings (chat mode and agent task mode).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 20:59:37 -04:00
Scott Idem
8487645224 fix: claude auth expiry warning — correct field name and smarter threshold
- Fix 'undefined' in auth banner: read access_token_hours_remaining (not hours_remaining)
- Fix false-positive warning on fresh tokens: when refresh token present, only warn
  within 1 hour of expiry (not 24h) since the CLI should auto-rotate but sometimes misses
- Emit claude_auth_expired SSE event on 401 so UI shows inline red banner immediately
- app.js: handle claude_auth_expired SSE event with persistent top banner + dismiss button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 23:22:18 -04:00
Scott Idem
0cf0d65e9e feat: session naming, username/persona rename, help page, contrast fixes
- Session name field: PATCH /sessions/{id} endpoint, inline rename button in UI
- Persona rename: inline ✏ toggle form in settings, POST /settings/persona/rename
- Username rename: inline form in settings, POST /settings/username (renames home dir, forces re-login)
- Help page: dedicated /help route replacing modal, collapsible sections
- Per-persona isolation: files.py and session_store.py now scope to correct user/persona
- Contrast/visibility: muted text bumped to slate-400+, session rename btn at 0.4 opacity

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 23:10:12 -04:00
Scott Idem
1b425a539f feat: account settings page + dedicated help page
- Add /settings page with password change form and personas list
- Add /help dedicated page (replaces help modal); renders HELP.md with
  collapsible sections, dark theme, back link to active persona
- Add 👤 account button and convert ? button to link in header
- Remove help modal HTML and ~55 lines of modal JS from main app
- Register settings and help routers in main.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 21:41:18 -04:00
Scott Idem
c01ef663f5 fix: per-persona session/file isolation + onboarding route order
- session_store: store sessions under home/{user}/persona/{name}/session_data/
  instead of the shared cortex/data/sessions/ bucket
- chat endpoints: add user/persona query params to /sessions, /history/*,
  /sessions/*, /note so they resolve the correct persona context
- files router: add user/persona query params to /files and /files/{name}
  so the file browser loads the right persona's files
- app.js: pass user/persona on all session, history, and file fetches;
  move _fileParams to top-level scope so it is available everywhere
- onboarding: fix FastAPI route ordering — register /persona before /{token}
  so the literal path wins and does not get captured as a token value
- ui.py: read Emoji field from IDENTITY.md and inject into CORTEX_CONFIG
  so the header icon reflects each persona's chosen emoji
- .gitignore: exclude home/**/session_data/ (runtime state)
- migrate scott/inara sessions from cortex/data/sessions/ to session_data/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 00:01:07 -04:00
Scott Idem
99f8961bec feat(tina): add dinosaurs as a core Holly interest
Lifelong passion — Jurassic Park seen countless times. Tina should
engage with this genuinely, not just acknowledge it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 00:18:38 -04:00