Compare commits
7 Commits
ce1561572a
...
23f8659aaa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23f8659aaa | ||
|
|
fe6561bf6a | ||
|
|
1127610752 | ||
|
|
2144d7c2c0 | ||
|
|
2ca02006dd | ||
|
|
48a6734ec3 | ||
|
|
7b51e7cc44 |
@@ -8,7 +8,7 @@ import uvicorn
|
|||||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s: %(message)s")
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s: %(message)s")
|
||||||
|
|
||||||
from config import settings
|
from config import settings
|
||||||
from routers import chat, google_chat, nextcloud_talk, files, distill
|
from routers import chat, google_chat, nextcloud_talk, files, distill, auth
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -28,6 +28,7 @@ app.include_router(google_chat.router)
|
|||||||
app.include_router(nextcloud_talk.router)
|
app.include_router(nextcloud_talk.router)
|
||||||
app.include_router(files.router)
|
app.include_router(files.router)
|
||||||
app.include_router(distill.router)
|
app.include_router(distill.router)
|
||||||
|
app.include_router(auth.router)
|
||||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
69
cortex/routers/auth.py
Normal file
69
cortex/routers/auth.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
"""
|
||||||
|
CLI auth status for both Claude and Gemini backends.
|
||||||
|
|
||||||
|
GET /auth/status — returns per-backend auth info and warning flags
|
||||||
|
|
||||||
|
Claude: warns when OAuth token is < WARN_HOURS from expiry (requires
|
||||||
|
user to re-run `claude` to refresh via browser flow).
|
||||||
|
Gemini: warns only when oauth_creds.json is missing or has no
|
||||||
|
refresh_token (access token rotates automatically every ~1h).
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
router = APIRouter(prefix="/auth")
|
||||||
|
|
||||||
|
CLAUDE_CREDS = Path.home() / ".claude" / ".credentials.json"
|
||||||
|
GEMINI_CREDS = Path.home() / ".gemini" / "oauth_creds.json"
|
||||||
|
GEMINI_ACCTS = Path.home() / ".gemini" / "google_accounts.json"
|
||||||
|
WARN_HOURS = 24
|
||||||
|
|
||||||
|
|
||||||
|
def _claude_status() -> dict:
|
||||||
|
try:
|
||||||
|
data = json.loads(CLAUDE_CREDS.read_text())
|
||||||
|
oauth = data["claudeAiOauth"]
|
||||||
|
expires_dt = datetime.fromtimestamp(oauth["expiresAt"] / 1000, tz=timezone.utc)
|
||||||
|
now = datetime.now(tz=timezone.utc)
|
||||||
|
hours_remaining = (expires_dt - now).total_seconds() / 3600
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"expires_at": expires_dt.isoformat(),
|
||||||
|
"hours_remaining": round(hours_remaining, 1),
|
||||||
|
"warning": hours_remaining < WARN_HOURS,
|
||||||
|
"expired": hours_remaining <= 0,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("claude auth check failed: %s", e)
|
||||||
|
return {"ok": False, "error": str(e), "warning": True, "expired": False}
|
||||||
|
|
||||||
|
|
||||||
|
def _gemini_status() -> dict:
|
||||||
|
try:
|
||||||
|
creds = json.loads(GEMINI_CREDS.read_text())
|
||||||
|
if not creds.get("refresh_token"):
|
||||||
|
return {"ok": True, "authenticated": False, "warning": True, "account": None}
|
||||||
|
account = None
|
||||||
|
try:
|
||||||
|
accts = json.loads(GEMINI_ACCTS.read_text())
|
||||||
|
account = accts.get("active")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"ok": True, "authenticated": True, "warning": False, "account": account}
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {"ok": True, "authenticated": False, "warning": True, "account": None}
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("gemini auth check failed: %s", e)
|
||||||
|
return {"ok": False, "error": str(e), "warning": True, "authenticated": False}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/status")
|
||||||
|
async def auth_status() -> dict:
|
||||||
|
return {
|
||||||
|
"claude": _claude_status(),
|
||||||
|
"gemini": _gemini_status(),
|
||||||
|
}
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
function setBackendUI(backend) {
|
function setBackendUI(backend) {
|
||||||
primaryBackend = backend;
|
primaryBackend = backend;
|
||||||
backendToggle.textContent = backend;
|
backendToggle.textContent = backend;
|
||||||
backendToggle.className = 'hdr-btn' + (backend === 'gemini' ? ' gemini' : '');
|
backendToggle.className = 'ctx-btn' + (backend === 'gemini' ? ' mem-on' : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
backendToggle.addEventListener('click', async () => {
|
backendToggle.addEventListener('click', async () => {
|
||||||
@@ -813,9 +813,39 @@
|
|||||||
document.getElementById('mem-short-btn').classList.toggle('mem-on', memShort);
|
document.getElementById('mem-short-btn').classList.toggle('mem-on', memShort);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatNextRun(iso) {
|
||||||
|
if (!iso) return 'n/a';
|
||||||
|
const dt = new Date(iso);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = dt - now;
|
||||||
|
const diffDays = Math.floor(diffMs / 86400000);
|
||||||
|
const time = dt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
|
if (diffDays === 0) return `today ${time}`;
|
||||||
|
if (diffDays === 1) return `tomorrow ${time}`;
|
||||||
|
return dt.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' + time;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSchedule() {
|
||||||
|
const schedEl = document.getElementById('ctx-schedule');
|
||||||
|
try {
|
||||||
|
const res = await fetch('/distill/status');
|
||||||
|
const d = await res.json();
|
||||||
|
if (!d.enabled || !d.jobs.length) {
|
||||||
|
schedEl.textContent = 'auto-distill disabled';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
schedEl.innerHTML = d.jobs
|
||||||
|
.map(j => `${j.id.replace('distill_', '').padEnd(6)} → ${formatNextRun(j.next_run)}`)
|
||||||
|
.join('<br>');
|
||||||
|
} catch {
|
||||||
|
schedEl.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctxOpenBtn.addEventListener('click', (e) => {
|
ctxOpenBtn.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
ctxPanel.classList.toggle('open');
|
ctxPanel.classList.toggle('open');
|
||||||
|
if (ctxPanel.classList.contains('open')) loadSchedule();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
@@ -884,6 +914,33 @@
|
|||||||
const helpBody = document.getElementById('help-modal-body');
|
const helpBody = document.getElementById('help-modal-body');
|
||||||
const helpClose = document.getElementById('help-close-btn');
|
const helpClose = document.getElementById('help-close-btn');
|
||||||
|
|
||||||
|
// Sections open by default — everything after "Notes" starts collapsed
|
||||||
|
const HELP_OPEN_SECTIONS = new Set(['Header Controls', 'Chat', 'Sessions', 'Notes']);
|
||||||
|
|
||||||
|
function makeCollapsible(container) {
|
||||||
|
const h2s = Array.from(container.querySelectorAll('h2'));
|
||||||
|
for (const h2 of h2s) {
|
||||||
|
const title = h2.textContent.trim();
|
||||||
|
const details = document.createElement('details');
|
||||||
|
if (HELP_OPEN_SECTIONS.has(title)) details.open = true;
|
||||||
|
|
||||||
|
const summary = document.createElement('summary');
|
||||||
|
summary.textContent = title;
|
||||||
|
details.appendChild(summary);
|
||||||
|
|
||||||
|
// Collect all following siblings until the next h2
|
||||||
|
const siblings = [];
|
||||||
|
let node = h2.nextSibling;
|
||||||
|
while (node && node.nodeName !== 'H2') {
|
||||||
|
siblings.push(node);
|
||||||
|
node = node.nextSibling;
|
||||||
|
}
|
||||||
|
for (const sib of siblings) details.appendChild(sib);
|
||||||
|
|
||||||
|
h2.parentNode.replaceChild(details, h2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function openHelp() {
|
async function openHelp() {
|
||||||
helpBody.textContent = 'Loading…';
|
helpBody.textContent = 'Loading…';
|
||||||
helpModal.classList.add('open');
|
helpModal.classList.add('open');
|
||||||
@@ -895,6 +952,7 @@
|
|||||||
helpBody.querySelectorAll('a').forEach(a => {
|
helpBody.querySelectorAll('a').forEach(a => {
|
||||||
a.target = '_blank'; a.rel = 'noopener noreferrer';
|
a.target = '_blank'; a.rel = 'noopener noreferrer';
|
||||||
});
|
});
|
||||||
|
makeCollapsible(helpBody);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
helpBody.textContent = `Failed to load help: ${err.message}`;
|
helpBody.textContent = `Failed to load help: ${err.message}`;
|
||||||
}
|
}
|
||||||
@@ -905,3 +963,54 @@
|
|||||||
helpModal.addEventListener('click', (e) => {
|
helpModal.addEventListener('click', (e) => {
|
||||||
if (e.target === helpModal) helpModal.classList.remove('open');
|
if (e.target === helpModal) helpModal.classList.remove('open');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Auth token warning banner ─────────────────────────────
|
||||||
|
const authBanner = document.getElementById('auth-banner');
|
||||||
|
const authBannerMsg = document.getElementById('auth-banner-msg');
|
||||||
|
const authBannerHint = document.getElementById('auth-banner-hint');
|
||||||
|
const authBannerClose = document.getElementById('auth-banner-close');
|
||||||
|
|
||||||
|
async function checkAuthStatus() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/auth/status');
|
||||||
|
if (!res.ok) return;
|
||||||
|
const d = await res.json();
|
||||||
|
|
||||||
|
const warnings = [];
|
||||||
|
const fixes = [];
|
||||||
|
let anyExpired = false;
|
||||||
|
|
||||||
|
if (d.claude?.warning) {
|
||||||
|
if (d.claude.expired) {
|
||||||
|
warnings.push('✕ Claude CLI token has expired');
|
||||||
|
anyExpired = true;
|
||||||
|
} else {
|
||||||
|
warnings.push(`⚠ Claude CLI token expires in ${d.claude.hours_remaining}h`);
|
||||||
|
}
|
||||||
|
fixes.push('<code>claude</code>');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.gemini?.warning) {
|
||||||
|
warnings.push('⚠ Gemini CLI not authenticated');
|
||||||
|
fixes.push('<code>gemini</code>');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!warnings.length) {
|
||||||
|
authBanner.classList.remove('show');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
authBannerMsg.innerHTML = warnings.join('<br>');
|
||||||
|
authBannerHint.innerHTML =
|
||||||
|
`To fix: SSH into the Cortex host and run ${fixes.join(' and/or ')} — `
|
||||||
|
+ 'follow the login prompt, then restart Cortex.';
|
||||||
|
authBanner.classList.toggle('expired', anyExpired);
|
||||||
|
authBanner.classList.add('show');
|
||||||
|
} catch { /* silently ignore — don't break the UI */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
authBannerClose.addEventListener('click', () => authBanner.classList.remove('show'));
|
||||||
|
|
||||||
|
checkAuthStatus();
|
||||||
|
// Re-check every 30 minutes
|
||||||
|
setInterval(checkAuthStatus, 30 * 60 * 1000);
|
||||||
|
|||||||
@@ -28,10 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button id="sessions-btn" class="hdr-btn">Sessions</button>
|
<button id="sessions-btn" class="hdr-btn">Sessions</button>
|
||||||
<button id="files-btn" class="hdr-btn">Files</button>
|
<button id="files-btn" class="hdr-btn">Files</button>
|
||||||
<button id="ctx-open-btn" class="hdr-btn" title="Context & memory settings">⚙<span class="tier-badge">2</span></button>
|
<button id="ctx-open-btn" class="hdr-btn" title="Settings">⚙<span class="tier-badge">2</span></button>
|
||||||
<button id="backend-toggle" class="hdr-btn" title="Click to switch primary backend">claude</button>
|
|
||||||
<button id="font-size-btn" class="hdr-btn" title="Cycle font size">Aa</button>
|
|
||||||
<button id="theme-btn" class="hdr-btn" title="Toggle light/dark mode">☾</button>
|
|
||||||
<button id="help-btn" class="hdr-btn" title="Help & reference">?</button>
|
<button id="help-btn" class="hdr-btn" title="Help & reference">?</button>
|
||||||
|
|
||||||
<div id="sessions-panel"></div>
|
<div id="sessions-panel"></div>
|
||||||
@@ -64,6 +61,20 @@
|
|||||||
<button class="ctx-btn" id="distill-all-btn" title="Run all three steps in sequence">all</button>
|
<button class="ctx-btn" id="distill-all-btn" title="Run all three steps in sequence">all</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="ctx-distill-status"></div>
|
<div id="ctx-distill-status"></div>
|
||||||
|
<div id="ctx-schedule"></div>
|
||||||
|
</div>
|
||||||
|
<div class="ctx-section">
|
||||||
|
<div class="ctx-section-title">Backend</div>
|
||||||
|
<div class="ctx-row">
|
||||||
|
<button id="backend-toggle" class="ctx-btn" title="Click to switch primary backend">claude</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ctx-section">
|
||||||
|
<div class="ctx-section-title">Display</div>
|
||||||
|
<div class="ctx-row">
|
||||||
|
<button id="font-size-btn" class="ctx-btn" title="Cycle font size: normal → large → small">Aa</button>
|
||||||
|
<button id="theme-btn" class="ctx-btn" title="Toggle light/dark mode">☾</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -98,6 +109,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Auth warning banner — shown when Claude CLI token is near expiry -->
|
||||||
|
<div id="auth-banner">
|
||||||
|
<div id="auth-banner-text">
|
||||||
|
<span id="auth-banner-msg"></span>
|
||||||
|
<span id="auth-banner-hint"></span>
|
||||||
|
</div>
|
||||||
|
<button id="auth-banner-close" title="Dismiss">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="messages"></div>
|
<div id="messages"></div>
|
||||||
<div id="session-id"></div>
|
<div id="session-id"></div>
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,6 @@
|
|||||||
|
|
||||||
.hdr-btn:hover { border-color: var(--muted); color: var(--text); }
|
.hdr-btn:hover { border-color: var(--muted); color: var(--text); }
|
||||||
|
|
||||||
#backend-toggle.gemini { border-color: var(--success-dim); color: var(--success); }
|
|
||||||
#sessions-btn { margin-left: auto; }
|
#sessions-btn { margin-left: auto; }
|
||||||
|
|
||||||
/* Sessions panel */
|
/* Sessions panel */
|
||||||
@@ -150,8 +149,8 @@
|
|||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + 4px);
|
top: calc(100% + 4px);
|
||||||
right: 20px;
|
right: 12px;
|
||||||
width: 300px;
|
width: min(300px, calc(100vw - 24px));
|
||||||
max-height: 340px;
|
max-height: 340px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
@@ -710,8 +709,8 @@
|
|||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + 4px);
|
top: calc(100% + 4px);
|
||||||
right: 20px;
|
right: 12px;
|
||||||
width: 280px;
|
width: min(280px, calc(100vw - 24px));
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -767,9 +766,13 @@
|
|||||||
#ctx-distill-status.show { opacity: 1; }
|
#ctx-distill-status.show { opacity: 1; }
|
||||||
#ctx-distill-status.err { color: var(--error-text); }
|
#ctx-distill-status.err { color: var(--error-text); }
|
||||||
|
|
||||||
/* Theme toggle + font size */
|
#ctx-schedule {
|
||||||
#theme-btn { font-size: 0.85rem; padding: 5px 8px; }
|
margin-top: 6px;
|
||||||
#font-size-btn { min-width: 32px; text-align: center; }
|
font-size: 0.66rem;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.7;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
/* Ctx header button — shows current tier as a dim superscript */
|
/* Ctx header button — shows current tier as a dim superscript */
|
||||||
#ctx-open-btn .tier-badge {
|
#ctx-open-btn .tier-badge {
|
||||||
@@ -902,3 +905,132 @@
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Help modal collapsible sections ────────────────────── */
|
||||||
|
#help-modal-body details {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
#help-modal-body details:last-of-type { border-bottom: none; }
|
||||||
|
|
||||||
|
#help-modal-body summary {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 10px 4px;
|
||||||
|
font-size: 1.0em;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
user-select: none;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
#help-modal-body summary::-webkit-details-marker { display: none; }
|
||||||
|
|
||||||
|
#help-modal-body summary::before {
|
||||||
|
content: '▶';
|
||||||
|
font-size: 0.6em;
|
||||||
|
opacity: 0.55;
|
||||||
|
transition: transform 0.15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
#help-modal-body details[open] > summary::before {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
#help-modal-body summary:hover { color: var(--text); }
|
||||||
|
|
||||||
|
/* Content inside a details block gets a little left indent */
|
||||||
|
#help-modal-body details > *:not(summary) {
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Auth warning banner ─────────────────────────────────── */
|
||||||
|
#auth-banner {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: rgba(160, 100, 0, 0.18);
|
||||||
|
border-bottom: 1px solid rgba(200, 140, 20, 0.45);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: #c9a84c;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-banner.show { display: flex; }
|
||||||
|
#auth-banner.expired {
|
||||||
|
background: rgba(120, 20, 20, 0.25);
|
||||||
|
border-color: rgba(200, 60, 60, 0.45);
|
||||||
|
color: var(--error-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-banner-text { flex: 1; display: flex; flex-direction: column; gap: 2px; }
|
||||||
|
#auth-banner-msg { font-weight: 500; }
|
||||||
|
#auth-banner-hint {
|
||||||
|
font-size: 0.76rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
#auth-banner-hint code {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-banner-close {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 2px 7px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-banner-close:hover { opacity: 1; }
|
||||||
|
|
||||||
|
/* ── Mobile responsive ───────────────────────────────────── */
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
header { padding: 8px 12px; gap: 8px; }
|
||||||
|
header .subtitle { display: none; }
|
||||||
|
#messages { padding: 12px; }
|
||||||
|
|
||||||
|
/* dvh adjusts as soft keyboard opens/closes */
|
||||||
|
body { height: 100dvh; }
|
||||||
|
|
||||||
|
/* Hide session ID — saves vertical space */
|
||||||
|
#session-id { display: none; }
|
||||||
|
|
||||||
|
/* Input area: stack textarea above button row */
|
||||||
|
#input-area {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 8px 12px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 16px minimum prevents iOS Safari auto-zoom on focus */
|
||||||
|
#input { font-size: 16px; }
|
||||||
|
|
||||||
|
/* Right col goes horizontal, full width */
|
||||||
|
#right-col {
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop-only controls — hide on mobile */
|
||||||
|
#height-row,
|
||||||
|
#enter-toggle { display: none !important; }
|
||||||
|
|
||||||
|
/* Larger touch targets */
|
||||||
|
#send, #stop { padding: 12px 0; flex: 1; font-size: 1rem; }
|
||||||
|
#note-btn { padding: 12px 14px; }
|
||||||
|
#note-type-btn { padding: 6px 10px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 380px) {
|
||||||
|
header .name { font-size: 1rem; }
|
||||||
|
.header-emoji { font-size: 1.3rem; }
|
||||||
|
.hdr-btn { padding: 5px 8px; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,12 +12,19 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| **Sessions** | Open the sessions panel — list, resume, or start sessions |
|
| **Sessions** | Open the sessions panel — list, resume, or start sessions |
|
||||||
| **Files** | Open the identity file editor (SOUL, MEMORY, etc.) |
|
| **Files** | Open the identity file editor (SOUL, MEMORY, etc.) |
|
||||||
| **⚙ N** | Open the Context & Memory panel (N = current tier) |
|
| **⚙ N** | Open the Settings panel (N = current context tier) |
|
||||||
| **claude / gemini** | Active backend — click to toggle primary |
|
|
||||||
| **Aa / A+ / A−** | Cycle font size: normal (16px) → large (18px) → small (14px) |
|
|
||||||
| **☾ / ☀** | Toggle dark / light theme |
|
|
||||||
| **?** | Open this help panel |
|
| **?** | Open this help panel |
|
||||||
|
|
||||||
|
The **⚙ Settings** panel contains all configuration options:
|
||||||
|
|
||||||
|
| Section | Controls |
|
||||||
|
|---|---|
|
||||||
|
| **Context Tier** | T1 – T4 context depth |
|
||||||
|
| **Memory Layers** | Toggle Long / Mid / Short memory on/off |
|
||||||
|
| **Distill Memory** | Manually trigger short / mid / long / all distillation |
|
||||||
|
| **Backend** | Active LLM backend — click to toggle claude ↔ gemini |
|
||||||
|
| **Display** | Aa/A+/A− font size cycle · ☾/☀ theme toggle |
|
||||||
|
|
||||||
All header settings (theme, font size, tier, memory layers) persist in `localStorage` across page refreshes.
|
All header settings (theme, font size, tier, memory layers) persist in `localStorage` across page refreshes.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -58,7 +65,7 @@ Notes are injected into a session without triggering an LLM response.
|
|||||||
## Backends
|
## Backends
|
||||||
|
|
||||||
- **Claude CLI** and **Gemini CLI** are both available. One is primary, the other is fallback.
|
- **Claude CLI** and **Gemini CLI** are both available. One is primary, the other is fallback.
|
||||||
- Click the backend button (`claude` or `gemini`) to switch which is primary.
|
- Click **⚙** → **Backend** to toggle between `claude` and `gemini` as the primary.
|
||||||
- If the primary fails or times out, the fallback is used automatically. A **⚡** notice appears in the chat when this happens.
|
- If the primary fails or times out, the fallback is used automatically. A **⚡** notice appears in the chat when this happens.
|
||||||
- Timeouts: Claude 60s, Gemini 120s.
|
- Timeouts: Claude 60s, Gemini 120s.
|
||||||
|
|
||||||
@@ -199,7 +206,7 @@ Chat request body (`POST /chat`):
|
|||||||
|
|
||||||
- **Auto memory distillation (long)** — short and mid run automatically; long-term integration is off by default (set `AUTO_DISTILL_LONG=true` in `.env` to enable)
|
- **Auto memory distillation (long)** — short and mid run automatically; long-term integration is off by default (set `AUTO_DISTILL_LONG=true` in `.env` to enable)
|
||||||
- **Ollama local model backend** — direct Ollama API support (no CLI wrapper)
|
- **Ollama local model backend** — direct Ollama API support (no CLI wrapper)
|
||||||
- **OAuth token auto-refresh notifications** — alert when Claude CLI token is near expiry
|
- **OAuth token auto-refresh notifications** — ✓ implemented: amber banner shown when Claude CLI token is within 24h of expiry (check `GET /auth/status`)
|
||||||
- **Multi-user support** — per-user identity/memory files; currently single-user (Scott)
|
- **Multi-user support** — per-user identity/memory files; currently single-user (Scott)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user