feat: edit existing model entries in the Model Registry

- Inline edit form per model row (label, model name/ID, host/account, context, tags)
- Fetch models button in edit form for local models — same live-picker UX as Add Model
- POST /settings/local/models/{id}/edit route in local_llm.py
- Admin role badge (ADMIN/USER pill) in Account Settings page
- HELP.md updated: new tools table with admin/confirm markers, PWA install section
- TODO updated: tool expansions marked done, distill review and Unsloth resolved,
  role-based access and admin badge added to completed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-29 21:08:09 -04:00
parent 334e7f0dea
commit a5658eb3c4
6 changed files with 289 additions and 41 deletions

View File

@@ -6,7 +6,7 @@
and are appended automatically by help.html when present.
-->
*Last updated: 2026-04-28*
*Last updated: 2026-04-29*
---
@@ -63,15 +63,20 @@ The ⚡ toggle is **independent of the Role selector** — you can use any role
| Category | Tools |
|---|---|
| Web | `web_search` |
| Files | `file_read` |
| Shell | `shell_exec`, `claude_allow_dir` |
| Web | `web_search`, `http_fetch` |
| Files | `file_read` ¹, `file_list` ¹, `file_write` ¹ ² |
| Shell | `shell_exec` ¹ ², `claude_allow_dir` ¹ |
| System | `cortex_restart` ¹ ², `cortex_logs` ¹ |
| Tasks | `task_list`, `task_create`, `task_update`, `task_complete` |
| Cron | `cron_list`, `cron_add`, `cron_remove`, `cron_toggle` |
| Reminders | `reminders_add`, `reminders_list`, `reminders_clear` |
| Cron | `cron_list`, `cron_add`, `cron_remove` ², `cron_toggle` |
| Reminders | `reminders_add`, `reminders_list`, `reminders_clear` ² |
| Scratchpad | `scratch_read`, `scratch_write`, `scratch_append`, `scratch_clear` |
| Notifications | `nc_talk_send` ¹ |
| Aether Journals | `ae_journal_list`, `ae_journal_search`, `ae_journal_entry_create`, `ae_journal_entry_update`, `ae_journal_entry_disable`, `ae_journal_entry_append`, `ae_journal_entry_prepend` |
| Aether Tasks | `ae_task_list` |
| Aether Tasks | `ae_task_list` ¹ |
¹ **Admin only** — requires the `admin` role. These tools are invisible to regular users.
² **Confirmation required** — the orchestrator pauses and asks you to confirm before executing. Reply to confirm and it will proceed.
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.
@@ -101,6 +106,18 @@ Notes are injected into a session without triggering an LLM response.
---
## Install as App (PWA)
Cortex supports installation as a Progressive Web App — it runs in its own window with no browser chrome.
- **Chrome / Edge (desktop):** Look for the install icon in the address bar, or open the browser menu → **Install Cortex…**
- **Android (Chrome):** Tap ⋮ → **Add to Home Screen**
- **iOS (Safari):** Tap the Share button → **Add to Home Screen**
Once installed, opening Cortex from the home screen or app launcher skips the browser UI entirely.
---
## Backends
Three backends are available:

View File

@@ -182,10 +182,18 @@
.fetch-status.err { color: #f87171; }
.model-row {
background: var(--pg-bg); border: 1px solid var(--pg-border); border-radius: 8px;
margin-bottom: 0.5rem; overflow: hidden;
}
.model-row-header {
display: flex; align-items: flex-start; justify-content: space-between;
gap: 0.75rem; padding: 0.75rem 0.9rem;
background: var(--pg-bg); border: 1px solid var(--pg-border); border-radius: 8px;
margin-bottom: 0.5rem;
}
.model-btns { display: flex; gap: 0.4rem; align-items: flex-start; flex-shrink: 0; }
.model-edit-form {
padding: 0.85rem 0.9rem 0.9rem;
border-top: 1px solid var(--pg-border);
background: var(--pg-surface);
}
.model-info { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
.model-label { font-size: 0.9rem; font-weight: 600; color: var(--pg-text); }
@@ -670,6 +678,78 @@
// Hide fetch button initially if no hosts
if (!HAS_HOSTS) fetchBtn.style.display = 'none';
// ── Model inline edit toggle ──────────────────────────────────────────
document.querySelectorAll('.model-edit-btn').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const form = document.getElementById('edit-form-' + id);
const open = form.style.display !== 'none';
form.style.display = open ? 'none' : 'block';
btn.textContent = open ? 'Edit' : 'Close';
});
});
document.querySelectorAll('.model-edit-cancel').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
document.getElementById('edit-form-' + id).style.display = 'none';
document.querySelector(`.model-edit-btn[data-id="${id}"]`).textContent = 'Edit';
});
});
// ── Edit form: fetch models from host ────────────────────────────────
document.querySelectorAll('.edit-fetch-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const id = btn.dataset.id;
const hostSel = document.getElementById('edit-host-' + id);
const hostId = hostSel ? hostSel.value : '';
const statusEl = document.getElementById('edit-fetch-status-' + id);
btn.disabled = true;
statusEl.textContent = 'Fetching…'; statusEl.className = 'fetch-status';
const url = '/api/local-llm/fetch-models' + (hostId ? '?host_id=' + encodeURIComponent(hostId) : '');
try {
const res = await fetch(url);
const data = await res.json();
if (data.error) {
statusEl.textContent = '✗ ' + data.error; statusEl.className = 'fetch-status err';
return;
}
const picker = document.getElementById('edit-model-picker-' + id);
const wrap = document.getElementById('edit-model-select-wrap-' + id);
picker.innerHTML = '<option value="">— select to fill —</option>';
data.models.forEach(m => {
const opt = document.createElement('option');
opt.value = m.id;
opt.textContent = m.name !== m.id ? `${m.name} (${m.id})` : m.id;
opt.dataset.id = m.id;
opt.dataset.name = m.name;
picker.appendChild(opt);
});
wrap.style.display = 'block';
statusEl.textContent = `${data.models.length} model${data.models.length !== 1 ? 's' : ''}`;
statusEl.className = 'fetch-status ok';
} catch (e) {
statusEl.textContent = '✗ ' + e.message; statusEl.className = 'fetch-status err';
} finally {
btn.disabled = false;
}
});
});
document.querySelectorAll('[id^="edit-model-picker-"]').forEach(picker => {
picker.addEventListener('change', () => {
const opt = picker.options[picker.selectedIndex];
if (!opt.value) return;
const id = picker.id.replace('edit-model-picker-', '');
const form = document.getElementById('edit-form-' + id);
form.querySelector('input[name="model_name"]').value = opt.dataset.id || opt.value;
const labelInput = form.querySelector('input[name="label"]');
if (!labelInput.value) {
const n = opt.dataset.name;
labelInput.value = (n && n !== opt.dataset.id) ? n : '';
}
});
});
// ── Claude CLI auth status ─────────────────────────────────────────────
(async function() {
const el = document.getElementById('claude-auth-status');

View File

@@ -220,6 +220,26 @@
text-decoration: none;
}
.add-persona:hover { color: #a78bfa; }
.role-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.role-badge.role-admin {
background: rgba(124, 58, 237, 0.15);
color: #a78bfa;
border: 1px solid rgba(124, 58, 237, 0.4);
}
.role-badge.role-user {
background: rgba(100, 116, 139, 0.12);
color: var(--pg-muted);
border: 1px solid var(--pg-border);
}
</style>
</head>
<body>
@@ -247,6 +267,10 @@
<label>Username</label>
<input type="text" value="{{ username }}" readonly>
</div>
<div class="field">
<label>Role</label>
<span class="role-badge role-{{ user_role }}">{{ user_role }}</span>
</div>
<button type="button" id="show-rename-user" class="persona-rename-toggle"
style="opacity:0.7; font-size:0.8rem; padding:0.3rem 0.6rem; border:1px solid var(--pg-border); border-radius:6px; margin-top:0.25rem;">
✏ Change username