Commit Graph

55 Commits

Author SHA1 Message Date
Scott Idem
fc6600c33e feat: Home Assistant API tools (ha_get_state, ha_get_states, ha_call_service)
Register three HA orchestrator tools so Inara can read device states and
control devices via the HA REST API. ha_call_service requires admin role
and user confirmation. Also includes accumulated UI fixes (setProcessing
helper, wasNewSession flag cleanup).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:39:35 -04:00
Scott Idem
85792a7bcf feat: per-role inject_mode, OTR fixes, hover metadata, send/stop tooltip
- inject_mode: per-role toggle (parallel to inject_datetime) gates the
  "Current mode: Off The Record" line in the system prompt; wired through
  model_registry, context_loader, chat router, orchestrator router, and
  local_llm settings UI

- OTR orchestrator fix: OrchestrateRequest now carries off_record;
  _finalize_job stores it per message and gates log_turn on it; JS
  orchestrate payload sends off_record correctly

- Per-message hover metadata: removed always-visible .model-tag; replaced
  with .msg-meta strip in the action bar (hover-only); shows model label,
  host, fallback indicator, and OTR badge; stored in session JSON

- Send/stop button tooltip: shows role + model and (when tools on)
  separate orchestrator model + engine label; live elapsed timer on stop
  button via startRunTimer/stopRunTimer

- OrchestratorResult.backend_label: new field; openai_orchestrator fills
  it; finalize_job propagates it to job dict and session messages

- GET /backend: exposes orchestrator_model label so the frontend tooltip
  can show both models separately

- TODO: session delete confirmation added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 16:12:03 -04:00
Scott Idem
a99ebb8c30 feat: retry button for orchestrator errors + explicit client timeout
Extract orchestrator inner loop into _doOrchestrate() so the retry button
can re-run without re-adding the user message to DOM or history — same
pattern as the existing chat retry.

Also set AsyncOpenAI(timeout=settings.timeout_local) so slow remote models
(OpenRouter/DeepSeek) get the same 300s budget as local chat calls instead
of the SDK default which varies by connection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 12:39:34 -04:00
Scott Idem
f8f7cd75da feat: audit log, usage tracking UI, OpenAI orchestrator compaction, onboarding + docs
Tool audit log:
- Every orchestrator tool call logged to home/{user}/tool_audit/YYYY-MM-DD.jsonl
- Files panel sidebar: audit log group (collapsed), date-linked read-only table
- Admin endpoints: /api/audit/files, /api/audit/day, /api/audit/recent, /api/audit/stats
- Engine and model name recorded per entry

OpenAI orchestrator improvements:
- Context budget enforcement: 75% of model context_k (min 16k)
- Message compaction: truncates old tool results when approaching budget
- max_rounds respected per model config (intersected with server cap)

OpenRouter onboarding (setup.html, onboarding.py, app.js, settings.html):
- Step 3 of 3: /setup/model with curated model picker
- Chat banner for users on server-default model (informational, not alarmist)
- Settings quick-link card; /setup/model works standalone for existing users

Model registry + session store:
- set_role_config / get_role_config for per-role tool lists and system_append
- session_store: session rename, session name backfill endpoint

UI updates (app.js, index.html, style.css, local_llm.html):
- Role toggle in context panel
- Off-the-record mode
- Agent notes read-only viewer
- OPERATIONS.md loaded at T2+ in context

Documentation:
- HELP.md: full tool table, per-role tool sets, Agent Notes, usage tracking
- TOOLS.md: Agent Notes section, count corrected to 44
- ARCH__SYSTEM.md, ARCH__BACKENDS.md, MASTER.md updated to match reality
- CLAUDE.md: onboarding flow, documentation philosophy sections
- README.md: stack in practice, DeepSeek TUI mention, architecture diagram updated
- TODO__Agents.md: onboarding task completed with deviation notes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 21:26:43 -04:00
Scott Idem
7d221863dc feat: engine/model in audit log + docs update
- tool_audit: ContextVars (engine, model) set at orchestrator run start; fields added to every entry
- orchestrator_engine: tool_audit.set_context("gemini", model_name) at run() start
- openai_orchestrator: tool_audit.set_context("openai", model label) at run() start
- audit table: Model column between Status and Args
- HELP.md: push notifications section, audit log in Files section, tool count 30→40, new API endpoints
- TODO__Agents.md: web_push and audit log marked complete with full detail

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 20:42:32 -04:00
Scott Idem
02accefe8f feat: audit log in Files panel sidebar
Adds an "Audit Log" section (collapsed by default) at the bottom of the Files
panel showing tool_audit/YYYY-MM-DD.jsonl files for the current user.

- GET /api/audit/files  — lists available dates (newest first, any auth user)
- GET /api/audit/day    — returns entries for one date as JSON (any auth user)
- tool_audit.read_day() — reads a single day's JSONL file chronologically
- Clicking a date renders a read-only table: time / tool / status / args / result
- Status cells are colour-coded (green ok, red error, amber denied)
- Edit/Raw/Preview/Save buttons are hidden in audit view, restored on file switch
- Audit group starts collapsed; expands on click like other file groups

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 20:36:08 -04:00
Scott Idem
ddf44a2aee feat: web push notifications (VAPID)
- push_utils.py: subscription storage + send helper (auto-prunes 410 endpoints)
- routers/push.py: GET /api/push/vapid-key (public), POST/DELETE /api/push/subscribe
- sw.js: push event listener shows notification; notificationclick focuses/opens tab
- app.js: subscribe/unsubscribe flow + "Enable notifications" toggle in settings dropdown
- tools/notify.py: web_push orchestrator tool (user-level, no admin required)
- VAPID keys in .env; pywebpush added to requirements.txt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 19:38:58 -04:00
Scott Idem
0b96772fa6 fix: show session friendly name in resume message and status bar
/history/{session_id} now returns a 'name' field alongside messages.
resumeSession() uses data.name first, then the sessionNames map, then
raw ID as fallback — so named sessions display correctly even on page
load before the sessions panel has been opened.

'Resumed session X' message also now shows the friendly name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 19:14:59 -04:00
Scott Idem
5d23d04e7e fix: session panel wider + two-line layout for session names
Root cause: 300px panel minus edit btn (28px) + meta (~130px) + delete
btn (28px) + gaps/padding left only ~70px (~7 chars) for the session name.

- Panel: 300px → 420px desktop, 300px → 380px mobile drawer
- Max-height: 340px → 400px
- Session item: name and meta now in a .session-body flex column, so the
  name gets full body width (panel minus two buttons) — meta lives below
- Edit mode: hides .session-body + delete, input takes the full body slot

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 19:07:33 -04:00
Scott Idem
7a0fbdb659 feat: session rename UX overhaul
- Edit button (✎) moved to left of row, separated from delete (×)
- Clicking ✎ hides name/meta/delete and expands input to full row width
- Button changes to ✓ (accent color) while editing
- Enter or ✓ click = save; Escape = cancel without saving
- Removed accidental-save-on-blur behavior
- Edit button: 30% opacity at rest, 75% on row hover, 100% on direct hover
- Touch devices: edit button always at 60% opacity (no hover to reveal it)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 19:00:39 -04:00
Scott Idem
0ffcd57c95 fix: multi-user distillation + datetime in context + session log labels
Distillation was silently operating on scott/inara for all users due to
ContextVar defaults. All three distill endpoints now require ?user=&persona=
query params and validate them via persona.validate(). Memory distiller
signatures changed from Optional to required positional args — no more
global settings fallback. Scheduler now iterates all users/personas instead
of hardcoding the primary user.

- context_loader: inject current date/time as first system prompt section
- session_logger: use get_user()/get_persona() from context instead of
  settings globals so Holly/Brian sessions show correct speaker labels
- memory_distiller: system prompts now reference u.title()/p.title()
  instead of settings.user_name/settings.agent_name
- distill router: Query(...) enforces params; _resolve() validates persona
- scheduler: _all_personas() helper iterates every user/persona for distill
- app.js: runDistill() now appends ?user=&persona= via _fileParams

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 18:44:51 -04:00
Scott Idem
6405dd338d feat: proper confirmation-resume flow + per-user tool policy
Fixes the broken confirmation gate where users had no way to approve
or deny a blocked tool call in the web UI.

Changes:
- orchestrator_engine.py: add OrchestrateCheckpoint dataclass, extract
  loop into _run_from_contents(), add resume() function
- openai_orchestrator.py: same treatment — _run_from_messages(), resume()
- routers/orchestrator.py: POST /{job_id}/confirm and /deny endpoints,
  separate _checkpoints store, _resume_job() + _finalize_job() helpers,
  "awaiting_confirmation" job status with pending_confirmation payload
- auth_utils.py: get_tool_policy() and save_tool_policy() helpers reading
  home/{user}/tool_policy.json (allow/deny lists)
- routers/orchestrator.py: loads tool_policy per user and passes
  confirm_allow/confirm_deny to both engines
- app.js: poll loop handles awaiting_confirmation — shows Confirm/Deny
  buttons inline, resumes polling after user action
- settings.html + settings.py: Tool Permissions section with allow/deny
  textareas, POST /settings/tool-policy route
- style.css: .confirm-gate, .confirm-btn, .deny-btn styles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 19:14:53 -04:00
Scott Idem
db3dd465b2 feat: email allowlist management in Settings + Files panel
Settings page gets an editable textarea (POST /settings/email-allowlist)
so users can view and update their per-user regex allowlist without
touching the raw JSON file.

Files panel gains a "Settings" group containing email_allowlist.json as
a raw JSON editor backup — served from home/{user}/ via files.py USER_FILES.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 21:56:45 -04:00
Scott Idem
25182a1765 feat: PWA support — manifest, service worker, icons, public auth exemption
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 18:46:33 -04:00
Scott Idem
217c7c3d6a feat: CodeMirror markdown editor for identity/memory file editor
Replace plain textarea with CodeMirror 5 + markdown mode loaded from
jsDelivr CDN. Editor fills the modal body via flex layout, theme-aware
via CSS vars (cursor, selection, headings, bold/em/links/code all mapped
to Cortex dark/light palette). Lazy init on first file open; history
cleared per-file so undo doesn't bleed across files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:05:57 -04:00
Scott Idem
66cb197de0 feat: last-used persona cookie, emoji dropdown, theme support, auth status move
- cx_last_persona cookie set on serve_ui; root/login/help/settings
  redirects use preferred persona from cookie instead of alphabetically first
- /api/personas returns [{name, emoji}] objects; persona switcher dropdown
  renders emoji + name with flex layout and .pd-emoji span
- Help, Settings, Model Registry pages apply localStorage theme on load
  (no flash); CSS variables for dark/light replacing all hardcoded hex values
- Claude CLI auth status moved from prominent chat banner to Anthropic
  provider block in Model Registry — live dot indicator (ok/warn/err)
- Auth banner removed from main chat UI (index.html, app.js, style.css)
- Add Model collapsed into Models section as <details> to shorten page
- Light-mode overrides for provider icons, model badges, ctx-badge, tags
  (Anthropic/Google/local colors now readable in both themes)
- Help page gains table, pre/code, hr styles for HELP.md rendered content

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 22:52:34 -04:00
Scott Idem
d61e39d614 feat: S/M/L height drives mode-select row vs column layout
When height is set to S, mode-select collapses to a row (mode button +
compact tools toggle side by side). M and L keep the vertical column
layout where each control gets its own full-width row. Driven by
data-size attribute set in JS so the switch is instant on click, not
reliant on a viewport media query. Removed the redundant max-height
landscape query.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:20:28 -04:00
Scott Idem
af4d78136a fix: textarea height setting now visibly changes empty-state size
The previous approach used a 600ms preview animation + syncHeight() which
collapsed the textarea back to 1 line (empty scrollHeight). Now syncHeight
enforces minHeight = maxHeight/3, so each setting (S/M/L) has a visibly
distinct resting height even when the input is empty.

  S (120px): min ~40px  ≈ 1-2 lines at rest
  M (240px): min ~80px  ≈ 3 lines at rest
  L (480px): min ~160px ≈ 5-6 lines at rest

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:08:46 -04:00
Scott Idem
af7d8b40e2 feat: single cycling height button, panel mutual exclusion, consistent shadows
- Replace 3 S/M/L height buttons with one cycling button (like font size)
- Fix closeAllPanels() to include ctx-panel so Context and Settings menus
  cannot be open simultaneously
- Fix ctxOpenBtn handler to use the same toggle-via-closeAllPanels pattern
  as the settings button
- Align .hdr-dropdown shadow to var(--shadow) instead of hardcoded rgba
- Align #ctx-panel z-index to 200 (match .hdr-dropdown)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:03:56 -04:00
Scott Idem
4159f470d6 fix: context panel polish — height buttons, amber theme vars, label cleanup
- Replace height <select> with S/M/L buttons (data-height); active class shows
  current setting; clicking an empty textarea briefly expands it as a preview
  so the effect is immediately visible, then auto-shrinks back
- Add --amber/--amber-border/--amber-glow CSS vars to all 4 theme blocks:
  dark=#f59e0b (bright), light=#b45309 (deep, 4:1 contrast on light bg)
  Fixes local-on/tools-toggle/backend-hint being nearly invisible in light mode
- Rename "Backend" ctx-section to "Role" (matches the role-cycle toggle)
- Update backend-toggle title from stale "primary backend" to "Active role"
- Capitalize distill buttons (Short/Mid/Long/All) to match Memory layer style
- Improve all ctx-panel button titles for clarity

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 20:53:16 -04:00
Scott Idem
2b9dd53566 feat: replace Agent mode with independent Tools toggle
- Remove 'agent' from mode dropdown; Chat/Note/OTR remain
- Add  tools toggle button in input bar (persisted in localStorage)
  When on: routes to POST /orchestrate (Gemini tool loop); send btn → "Run"
  When off: routes to POST /chat (direct to active role); no change
- Role selector and tools toggle are now fully independent:
  active chat_role sent in orchestrate payload → used for final response
- orchestrator_engine.run() accepts response_role param; passes it to
  complete(role=...) instead of hardcoded model="claude"
- OrchestrateRequest gains chat_role field (default "chat")
- Migrate stored 'agent' mode/MRU entries to 'chat' on load

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 20:36:15 -04:00
Scott Idem
8baab874f1 feat: replace backend/slot toggle with role selector
The backend toggle now cycles through configured roles (chat, coder,
research, distill, etc.) instead of backup model slots within the chat
role. Each role uses its own primary→backup chain from the registry.

- ChatRequest.slot replaced by chat_role (default "chat")
- GET /backend returns available_roles instead of chat_models
- _available_roles_for_toggle() builds list from defined_roles, excluding
  orchestrator (which has its own Agent mode)
- Model label on responses now reflects the actual role's assigned model
- Toggle is inert when only one role is configured (avoids useless cycling)
- Add "Clear browser cache" button to Account Settings (Connected Accounts)
- Add _role_model_label() helper for cleaner response tag labeling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:23:18 -04:00
Scott Idem
962d58d2e2 feat: model registry Phase 3 — slot-based backend toggle
Backend toggle now cycles through chat role models by label instead of
cycling service type strings (auto/claude/gemini/local).

- model_registry: get_model_for_slot() — resolves a specific priority
  slot without walking the fallback chain
- llm_client: complete() gains slot param; explicit slot selection
  dispatches directly to that model with no silent fallback
- routers/chat.py: ChatRequest.slot; GET /backend returns chat_models
  [{slot, label, type}] for the UI; _stream_chat uses resolved model
  label for the response tag when a slot is pinned
- app.js: toggle loads chat_models from /backend, cycles by label,
  sends slot in chat payload; legacy model field removed from payload
- app.js: fix Gap B — agent mode placeholder no longer says "Gemini
  tool loop"; now says "orchestrator"
- DESIGN doc: updated to reflect phases 1+2 complete, catalog-as-code
  decision, Gap A/B documented, Phase 3 implementation details

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 21:43:08 -04:00
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
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
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
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
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
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
46b65d087c feat: persona onboarding — invite tokens, self-service setup, persona creation, switcher
New user flow:
  1. Admin: python manage_passwords.py invite <username>  → generates URL
  2. User visits /setup/<token> → sets own password → logged in
  3. User redirected to /setup/persona → fills name/emoji/description
  4. persona_template.py generates all starter files → lands at /{user}/{persona}

Multiple personas:
  - Header persona name is now a clickable dropdown listing all personas
  - "New persona" link at bottom → /setup/persona (available to logged-in users)
  - /api/personas endpoint returns persona list for current session user

New files:
  - persona_template.py: generates IDENTITY/SOUL/PROTOCOLS/USER/HELP.md + data files
  - routers/onboarding.py: /setup/{token}, /setup/persona GET+POST
  - static/setup.html: two-step form (password → persona), emoji picker, mobile-friendly

Updated:
  - auth_utils.py: create_invite(), validate_invite(), consume_invite()
  - manage_passwords.py: invite command with URL output
  - auth_middleware.py: /setup/* prefix is public (invite tokens need no auth)
  - routers/ui.py: /api/personas endpoint; post-login redirect if no personas
  - static/app.js: persona switcher dropdown with navigation + Add persona link
  - static/style.css: .persona-switcher, .persona-dropdown, mobile adjustments

Mobile: login/setup pages are card-centered with responsive padding;
dropdown avoids edge-clipping on narrow screens; logout button stays visible.

All 80 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 23:10:32 -04:00
Scott Idem
a9bbb668b5 feat: session auth + per-user/persona UI at /{user}/{persona}
Replaces nginx basic auth with a proper per-user session system:

- auth_utils.py: bcrypt password hashing, JWT cookie creation/decode
- auth_middleware.py: validates JWT cookie on all routes except /login,
  /health, /static/, and webhook endpoints (/channels/, /webhook/)
- routers/ui.py: GET /login, POST /login, POST /logout,
  GET /{username}/{persona} — serves index.html with CORTEX_CONFIG injected
- static/login.html: minimal login form (dark theme, matches UI)
- main.py: registers SessionAuthMiddleware + ui.router
- config.py: jwt_secret, jwt_expire_days settings
- manage_passwords.py: CLI tool to set/check/list user passwords
- app.js: reads window.CORTEX_CONFIG (user + persona), sends both on
  every /chat and /orchestrate request; persona name shown in header;
  logout button (⏏) added to header
- requirements.txt: bcrypt, PyJWT, python-multipart
- .env.default: JWT_SECRET, JWT_EXPIRE_DAYS documented
- tests: client fixture injects JWT cookie; security test assertions
  updated for URL-normalized path traversal paths (still secure, codes differ)

All 80 tests pass.

Setup for a new user:
  python manage_passwords.py set scott
  python manage_passwords.py set holly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 22:54:12 -04:00
Scott Idem
aaac3e1353 feat: persist orchestrator sessions + user service + docs update
- Orchestrator now saves turns to session store so history survives page refresh
- UI session_id updated from job result; history controls attached to agent turns
- Cortex migrated from system service to systemd user service (no more sudo)
- Update README.md and CLAUDE.md with correct service commands

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 23:08:38 -04:00
Scott Idem
9b818aa5c7 feat: orchestrator Agent mode UI + claude_allow_dir tool + fix DDG search
- Add Agent mode toggle to web UI input row — routes through POST /orchestrate
  instead of /chat; polls for result with live tool-call count in thinking bubble
- Add cortex/tools/system.py with claude_allow_dir tool; registers in tool registry
- Fix web search: duckduckgo_search renamed to ddgs, update import + requirements.txt
- Allow WebSearch and WebFetch in ~/.claude/settings.json for Claude CLI fallback
- Add claude-allow-dir script docs and security note to CLAUDE.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 22:42:44 -04:00
Scott Idem
f935fc4a7f feat: session delete + touch-friendly message controls
Session delete:
- DELETE /sessions/{session_id} endpoint (chat.py + session_store.py)
- × button on each session item in the panel (hover-reveal on desktop)
- Clears UI if the active session is deleted

Touch accessibility:
- @media (hover: none) rule makes msg-actions always visible on touch devices
- msg-act-btn tap targets enlarged to 36px min-height, readable font size
- session-delete-btn also always visible and finger-sized on touch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 19:43:20 -04:00
Scott Idem
fe6561bf6a feat: add Gemini auth check to token warning banner
/auth/status now returns per-backend status: Claude warns on <24h expiry,
Gemini warns only when oauth_creds.json is missing or has no refresh_token
(access token rotates automatically so expiry_date is not a useful signal).
Banner shows warnings for both backends when needed, and the hint text
names the specific CLI commands to run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 23:29:25 -04:00
Scott Idem
1127610752 UI: show distill schedule next-run times in settings panel
Fetches /distill/status when the ⚙ panel opens and renders next run
times below the distill buttons (monospace, muted). Shows "today",
"tomorrow", or "Mar 18" format depending on how far away.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 23:22:47 -04:00