feat: SSH dev routing, model registry UX, chat input toolbar, doc sync

Backend / infrastructure:
- cortex/tools/_projects.py (new): shared project alias registry with ssh_host
  for workstation projects (aether_api, aether_frontend, aether_container)
- cortex/tools/git.py: all git tools route to workstation via SSH when ssh_host set
- cortex/tools/aider.py: aider_run SSH-routes to workstation using bash -l -c
- cortex/routers/local_llm.py: POST /api/models/{id}/edit AJAX endpoint — save
  model edits without page reload or tab reset; returns JSON {ok, label, model_name}
- cortex/llm_client.py: remove Gemini CLI and Claude CLI backends; clean up
  fallback chain and process group tracking (continuation of Gemini CLI removal)
- cortex/routers/auth.py: strip Claude/Gemini CLI auth status checks (CLI removed)
- cortex/routers/chat.py: remove legacy claude/gemini backend fields
- cortex/config.py: clean up CLI-related settings
- cortex/main.py: remove CLI lifecycle hooks

UI:
- cortex/static/local_llm.html: model edit forms now save via fetch() + toast;
  stay on Models tab; update row header label in place on success
- cortex/static/index.html: restructure input area to column layout — textarea
  above, compact toolbar below (Chat/Tools/Attach + Send); fixes dead space at
  M/L/XL sizes; context panel "Role" → "Model" section label
- cortex/static/style.css: column input-area layout; #input-toolbar; flex:1 →
  width:100% on textarea (fixes scrollHeight in column flex context); compact
  send/stop button padding
- cortex/static/app.js: add XL (720px) to height cycle; default M (240px)

Docs:
- cortex/static/HELP.md: S/M/L → S/M/L/XL; add Rebuild to distill table; fix
  "Role selector" references (no such UI); fix "your active role" → Chat role;
  fix  toggle description; Model Registry section cleanup
- documentation/ARCH__BACKENDS.md: reflect CLI removal, current backend state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-06-18 22:14:07 -04:00
parent 85223326b0
commit b144d8385f
15 changed files with 378 additions and 586 deletions

View File

@@ -6,7 +6,7 @@
and are appended automatically by help.html when present.
-->
*Last updated: 2026-05-13*
*Last updated: 2026-06-18* <!-- input toolbar refactor; XL size added; help doc sync -->
---
@@ -44,7 +44,7 @@ The **Context & Memory** panel (sliders icon with tier number) contains all conf
| **Memory Layers** | Toggle Long / Mid / Short memory on/off |
| **Distill Memory** | Manually trigger Short / Mid / Long / All distillation |
| **Model** | Active chat model — click to cycle through your configured slot models (Primary → Backup 1 → …) |
| **Display** | **Aa** cycles font size · **☾** toggles theme · **S/M/L** cycles input area height · **⌃↵** toggles send shortcut |
| **Display** | **Aa** cycles font size · **☾** toggles theme · **S/M/L/XL** cycles input area height · **⌃↵** toggles send shortcut |
All settings persist in `localStorage` across page refreshes.
@@ -74,7 +74,7 @@ The orchestrator runs a multi-step tool loop:
3. The model produces the final user-facing reply — when the orchestrator role uses Gemini, Claude writes the final response; when it uses a local model, that same model writes it
4. Expandable tool-call cards appear above the response — click any card to see the arguments sent and the result returned
The ⚡ toggle is **independent of the Role selector** — you can use any role (chat, coder, research, etc.) with or without tools. The orchestrator model is configured in **Account → Model Registry → Role Assignments → Orchestrator**.
The ⚡ toggle routes requests through the **Orchestrator** role model regardless of which chat model is active. Configure it in **Account → Model Registry → Role Assignments → Orchestrator**.
Tools mode is best for tasks requiring research, multi-step reasoning, or side effects (e.g. "search for X", "add a task", "what's on my list?", "append this to my journal"). Regular chat is faster for conversational turns.
@@ -156,7 +156,7 @@ Once installed, opening Cortex from the home screen or app launcher skips the br
## Switching Models
The **Model** button in the Context & Memory panel cycles through the slot models configured for your active role (Primary → Backup 1). Click it to switch between models mid-session.
The **Model** button in the Context & Memory panel cycles through the slot models configured for your **Chat** role (Primary → Backup 1). Click it to switch between models mid-session.
- The button label shows the active model (e.g. "GPT-4o", "Gemini 2.5 Flash")
- The selected slot is sent with each chat request so the correct model is used
@@ -205,12 +205,11 @@ The table shows all-time totals per model key, with columns for:
Values ≥ 1,000 are displayed as `k` (e.g. `24.3k`).
**What is and isn't tracked:**
**What is tracked:**
-Gemini API calls (orchestrator, distillation)
-Anthropic API calls (direct SDK)
- ✅ Local OpenAI-compatible calls (Open WebUI, Ollama, OpenRouter)
- ✗ Claude CLI — no structured token data is returned by the subprocess
- ✗ Gemini CLI — same reason
- ✅ Gemini API calls (orchestrator, distillation)
The raw data lives in `home/{username}/usage.json` and is also accessible via the Files panel or the API.
@@ -230,9 +229,10 @@ Configure which AI models are available and which handles each task type.
Do this before adding models — models need a provider account or local host to attach to.
**Anthropic (Claude):** Two options:
- **CLI (OAuth):** Nothing to configure — uses your existing `claude auth login` session. If Claude isn't working, run `claude auth login` in a terminal.
- **Direct API key:** Scroll to **Cloud Providers → Anthropic** → click **+ Add API key**. Enter a label and your `sk-ant-…` key from [console.anthropic.com/keys](https://console.anthropic.com/keys). When you add a model using an API key credential, it routes through the Anthropic SDK instead of the CLI.
**Anthropic (Claude):** Uses a direct API key — no Claude CLI required:
- Scroll to **Cloud Providers → Anthropic** → click **+ Add API key**
- Enter a label and your `sk-ant-…` key from [console.anthropic.com/keys](https://console.anthropic.com/keys)
- Models added with this credential call the Anthropic API directly via the SDK
**Google (Gemini):** Add one entry per API key you want to use:
1. Scroll to **Cloud Providers → Google** → click **+ Add Google account**
@@ -261,7 +261,7 @@ Scroll to **Add Model**. Select the provider tab, fill in the details, click **A
|---|---|
| **Local** | Select a host (from Step 1) → enter model name, or use **Fetch from host** to pick from a live list |
| **Google** | Select a Gemini model from the catalog → select a Google account (from Step 1) |
| **Anthropic** | Select a credential (CLI OAuth or an API key added in Step 1) → select a Claude model from the catalog |
| **Anthropic** | Select an API key credential (from Step 1) → select a Claude model from the catalog |
The label and context window size auto-fill from the catalog — edit them if you want. Tags are optional.
@@ -286,7 +286,7 @@ Scroll to **Role Assignments** at the bottom of the page. Each role has **Primar
| **Coder** | Code-focused tasks — larger context window, code-aware model |
| **Research** | Long-context research — high-token model, web tools prioritized |
Switch roles via the **Role** selector in the Context & Memory panel (⚙). Leave all slots empty to use the server default.
Leave all slots empty to use the server default.
**Per-role tool sets:** Expand any role card to configure which tool categories the orchestrator can use when that role is active. Unchecked categories are hidden from the model entirely — reducing token overhead on every orchestrated call. Leaving all categories unchecked means all tools the user has access to are available (the default).
@@ -390,6 +390,7 @@ Distillation builds up the memory layers from raw session logs. Runs automatical
| **mid** | LLM summarizes `MEMORY_SHORT.md``MEMORY_MID.md` |
| **long** | LLM integrates `MEMORY_MID.md``MEMORY_LONG.md` |
| **all** | Runs short → mid → long in sequence |
| **Rebuild** | ⚠ Wipes Mid + Long memories and rebuilds from session logs. Use to recover from distillation drift. Hand-edited content will be replaced. |
**Recommended workflow:** run **short** after any productive session; **mid** weekly; **long** monthly.
@@ -462,8 +463,7 @@ For direct access or scripting:
| Method | Endpoint | Description |
|---|---|---|
| `POST` | `/chat` | Send a message — returns SSE stream |
| `GET` | `/backend` | Get current primary/fallback backends |
| `POST` | `/backend` | Set primary backend (`{"primary": "claude"}`) |
| `GET` | `/backend` | Get configured model slots and orchestrator |
| `GET` | `/sessions` | List all sessions |
| `GET` | `/history/{id}` | Get session message history |
| `PUT` | `/history/{id}` | Replace full session history |

View File

@@ -140,15 +140,16 @@
});
// ── Textarea height ──────────────────────────────────────────
const HEIGHT_SIZES = [120, 240, 480];
const HEIGHT_LABELS = ['S', 'M', 'L'];
const HEIGHT_SIZES = [120, 240, 480, 720];
const HEIGHT_LABELS = ['S', 'M', 'L', 'XL'];
const HEIGHT_TITLES = [
'Input size: Compact — click to cycle',
'Input size: Medium — click to cycle',
'Input size: Large — click to cycle',
'Input size: Extra Large — click to cycle',
];
let maxHeight = parseInt(localStorage.getItem('maxHeight') || '120');
let maxHeight = parseInt(localStorage.getItem('maxHeight') || '240');
const heightCycleBtn = document.getElementById('height-cycle-btn');
function syncHeight() {

View File

@@ -115,9 +115,9 @@
<div id="ctx-schedule"></div>
</div>
<div class="ctx-section">
<div class="ctx-section-title">Role</div>
<div class="ctx-section-title">Model</div>
<div class="ctx-row">
<button id="backend-toggle" class="ctx-btn" title="Active role — click to cycle">chat</button>
<button id="backend-toggle" class="ctx-btn" title="Active model — click to cycle chat role slots">chat</button>
</div>
<div id="backend-model-hint"></div>
</div>
@@ -167,24 +167,6 @@
<div id="messages"></div>
<div id="input-area">
<!-- Mode select — compact dropdown, opens upward, MRU sorted -->
<div id="mode-select">
<button id="mode-select-btn" title="Input mode">
<span id="mode-icon">💬</span>
<span id="mode-label">Chat</span>
<span class="mode-arrow"></span>
</button>
<!-- Populated dynamically in MRU order -->
<div id="mode-dropdown"></div>
<!-- Note visibility sub-toggle — only shown when note mode is active -->
<button id="note-vis-btn" title="Toggle note visibility (private / public)">prv</button>
<!-- Tools toggle — routes through the orchestrator tool loop when active -->
<button id="tools-toggle" title="Tools disabled — click to enable"></button>
<!-- Attach file — images (vision) or text/code files -->
<button id="attach-btn" title="Attach image or text file">📎</button>
<input type="file" id="file-input" style="display:none"
accept="image/png,image/jpeg,image/webp,image/gif,text/plain,text/markdown,.md,.txt,.py,.js,.ts,.jsx,.tsx,.json,.yaml,.yml,.toml,.html,.css,.sh,.csv,.xml,.rs,.go,.java,.c,.cpp,.h,.rb,.php,.swift,.kt,.sql">
</div>
<!-- Attachment preview — shown when a file is pending -->
<div id="attachment-row" style="display:none">
<div id="attachment-preview">
@@ -195,7 +177,26 @@
</div>
</div>
<textarea id="input" rows="1" placeholder="Message…" autofocus></textarea>
<div id="send-col">
<!-- Compact toolbar: mode, tools, attach | spacer | send/stop -->
<div id="input-toolbar">
<div id="mode-select">
<button id="mode-select-btn" title="Input mode">
<span id="mode-icon">💬</span>
<span id="mode-label">Chat</span>
<span class="mode-arrow"></span>
</button>
<!-- Populated dynamically in MRU order -->
<div id="mode-dropdown"></div>
</div>
<!-- Note visibility sub-toggle — only shown when note mode is active -->
<button id="note-vis-btn" title="Toggle note visibility (private / public)">prv</button>
<!-- Tools toggle — routes through the orchestrator tool loop when active -->
<button id="tools-toggle" title="Tools disabled — click to enable"></button>
<!-- Attach file — images (vision) or text/code files -->
<button id="attach-btn" title="Attach image or text file">📎</button>
<input type="file" id="file-input" style="display:none"
accept="image/png,image/jpeg,image/webp,image/gif,text/plain,text/markdown,.md,.txt,.py,.js,.ts,.jsx,.tsx,.json,.yaml,.yml,.toml,.html,.css,.sh,.csv,.xml,.rs,.go,.java,.c,.cpp,.h,.rb,.php,.swift,.kt,.sql">
<div style="flex:1"></div>
<button id="send">Send</button>
<button id="stop"><svg data-lucide="square" width="14" height="14" class="btn-icon"></svg> Stop</button>
</div>

View File

@@ -982,6 +982,42 @@
});
});
// ── Model edit: AJAX save (stay on Models tab) ────────────────────────────
document.querySelectorAll('.model-edit-form').forEach(form => {
form.addEventListener('submit', async e => {
e.preventDefault();
const id = form.id.replace('edit-form-', '');
const saveBtn = form.querySelector('button[type="submit"]');
saveBtn.disabled = true;
try {
const res = await fetch(`/api/models/${id}/edit`, {method: 'POST', body: new FormData(form)});
const data = await res.json();
if (data.ok) {
// Update the row header label in place
const row = document.getElementById('model-' + id);
if (row && data.label) {
const labelEl = row.querySelector('.model-label');
if (labelEl) labelEl.textContent = data.label;
}
if (row && data.model_name) {
const nameEl = row.querySelector('.model-name');
if (nameEl) nameEl.textContent = data.model_name;
}
// Close the edit panel
form.style.display = 'none';
document.querySelector(`.model-edit-btn[data-id="${id}"]`).textContent = 'Edit';
showToast('Model saved');
} else {
showToast(data.error || 'Save failed', true);
}
} catch (err) {
showToast(err.message, true);
} finally {
saveBtn.disabled = false;
}
});
});
// ── Edit form: fetch from host ────────────────────────────────────────────
document.querySelectorAll('.edit-fetch-btn').forEach(btn => {
btn.addEventListener('click', async () => {

View File

@@ -735,35 +735,28 @@
.message.note-private .note-content { color: #c9a84c; white-space: pre-wrap; }
.message.note-public .note-content { color: #4abfb0; white-space: pre-wrap; }
/* ── Input area — 3-col: [mode-toggle] [textarea] [send-col] ── */
/* ── Input area — column: [attachment?] [textarea] [toolbar] ── */
#input-area {
padding: 12px 20px;
padding: 10px 20px 12px;
background: var(--surface);
border-top: 1px solid var(--border);
display: flex;
flex-direction: row;
gap: 10px;
align-items: flex-end;
flex-direction: column;
gap: 6px;
}
/* ── Mode select — compact dropdown ─────────────────────────── */
/* ── Compact toolbar below the textarea ─────────────────────── */
#input-toolbar {
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
}
/* ── Mode select — positioned container for dropdown only ────── */
#mode-select {
position: relative;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 4px;
}
/* S: collapse to a single row — mode button + compact tools toggle */
#mode-select[data-size="s"] {
flex-direction: row;
align-items: center;
}
#mode-select[data-size="s"] #tools-toggle {
padding: 3px 7px;
font-size: 0.75rem;
}
#mode-select-btn {
@@ -874,8 +867,7 @@
#attach-btn:hover { color: rgba(255,255,255,0.6); border-color: rgba(255,255,255,0.25); }
#attachment-row {
padding: 0.3rem 0.5rem;
border-bottom: 1px solid var(--border);
padding: 0.2rem 0;
}
#attachment-preview {
display: inline-flex;
@@ -914,7 +906,8 @@
#attachment-clear:hover { color: var(--text); }
#input {
flex: 1;
width: 100%;
box-sizing: border-box;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
@@ -936,16 +929,7 @@
#input.mode-note.public:focus { border-color: rgba(40,170,150,0.85); }
#input.mode-otr { border-color: rgba(120,80,160,0.4); background: rgba(120,80,160,0.04); }
/* Send column — right side, stacked */
#send-col {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 4px;
flex-shrink: 0;
}
/* Send button */
/* Send button — sits in #input-toolbar row */
#send {
display: flex;
align-items: center;
@@ -955,11 +939,12 @@
border: 1px solid var(--user-border);
color: var(--text);
border-radius: 8px;
padding: 10px 14px;
padding: 7px 16px;
cursor: pointer;
font-size: 0.9rem;
text-align: center;
white-space: nowrap;
flex-shrink: 0;
transition: background 0.15s;
}
@@ -977,10 +962,11 @@
border: 1px solid var(--error-border);
color: var(--error-text);
border-radius: 8px;
padding: 10px 14px;
padding: 7px 14px;
cursor: pointer;
font-size: 0.9rem;
text-align: center;
flex-shrink: 0;
transition: background 0.15s;
}