Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 83 additions & 1 deletion codemem/viewer_static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,9 @@
const query = buildProjectParams(project, options?.limit, options?.offset, options?.scope);
return fetchJson(`/api/summaries?${query}`);
}
async function loadObserverStatus() {
return fetchJson("/api/observer-status");
}
async function loadConfig() {
return fetchJson("/api/config");
}
Expand Down Expand Up @@ -3452,11 +3455,90 @@ Global: ${Number(totalsGlobal.tokens_saved || 0).toLocaleString()} saved` : "";
saveBtn.disabled = !state.settingsDirty;
}
}
function formatAuthMethod(method) {
switch (method) {
case "anthropic_consumer":
return "OAuth (Claude Max/Pro)";
case "codex_consumer":
return "OAuth (ChatGPT subscription)";
case "sdk_client":
return "API key";
case "claude_sidecar":
return "Local Claude session";
case "opencode_run":
return "OpenCode sidecar";
default:
return method;
}
}
function formatCredentialSources(creds) {
const parts = [];
if (creds.oauth) parts.push("OAuth");
if (creds.api_key) parts.push("API key");
if (creds.env_var) parts.push("env var");
return parts.length ? parts.join(", ") : "none";
}
function createEl(tag, className, text) {
const el2 = document.createElement(tag);
if (className) el2.className = className;
if (text) el2.textContent = text;
return el2;
}
function renderObserverStatusBanner(status) {
const banner = $("observerStatusBanner");
if (!banner) return;
if (!status || typeof status !== "object") {
banner.hidden = true;
return;
}
banner.textContent = "";
const active = status.active;
const available = status.available_credentials || {};
if (active) {
const provider = String(active.provider || "unknown");
const model = String(active.model || "");
const method = formatAuthMethod(active.auth?.method || "none");
const tokenOk = active.auth?.token_present === true;
banner.append(createEl("div", "status-label", "Active observer"));
const row = createEl("div", "status-active");
row.textContent = `${provider} → ${model} via ${method} `;
const tokenSpan = createEl("span", tokenOk ? "cred-ok" : "cred-none", tokenOk ? "✓" : "✗");
row.append(tokenSpan);
banner.append(row);
} else {
banner.append(createEl("div", "status-label", "Observer status"));
banner.append(createEl("div", "status-active", "Not yet initialized (waiting for first session)"));
}
const credEntries = Object.entries(available).filter(
([, creds]) => creds && typeof creds === "object"
);
if (credEntries.length) {
banner.append(createEl("div", "status-label", "Available credentials"));
const row = createEl("div");
credEntries.forEach(([provider, creds], idx) => {
const c = creds;
const sources = formatCredentialSources(c);
const hasAny = Object.values(c).some(Boolean);
const span = createEl("span", "status-cred");
const icon = createEl("span", hasAny ? "cred-ok" : "cred-none", hasAny ? "✓" : "–");
span.append(icon);
span.append(` ${String(provider)}: ${sources}`);
if (idx > 0) row.append(" · ");
row.append(span);
});
banner.append(row);
}
banner.hidden = false;
}
async function loadConfigData() {
if (settingsOpen) return;
try {
const payload = await loadConfig();
const [payload, status] = await Promise.all([
loadConfig(),
loadObserverStatus().catch(() => null)
]);
renderConfigModal(payload);
renderObserverStatusBanner(status);
} catch {
}
}
Expand Down
30 changes: 30 additions & 0 deletions codemem/viewer_static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,35 @@
}
.settings-panel { display: none; flex-direction: column; gap: var(--sp-3); }
.settings-panel.active { display: flex; }
.observer-status-banner {
display: flex;
flex-direction: column;
gap: var(--sp-1);
padding: var(--sp-2) var(--sp-3);
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface-0);
font-size: 12px;
color: var(--text-secondary);
line-height: 1.5;
}
.observer-status-banner .status-active {
color: var(--text-primary);
font-weight: 500;
}
.observer-status-banner .status-label {
color: var(--text-tertiary);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.observer-status-banner .status-cred {
display: inline-flex;
align-items: center;
gap: 4px;
}
.observer-status-banner .cred-ok { color: var(--semantic-success, #4ade80); }
.observer-status-banner .cred-none { color: var(--text-tertiary); }
.settings-group {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -1466,6 +1495,7 @@ <h2 id="settingsTitle">Memory & model settings</h2>
</div>

<div class="settings-panel active" id="settingsPanelObserver" data-settings-panel="observer">
<div id="observerStatusBanner" class="observer-status-banner" hidden></div>
<div class="settings-group">
<h3 class="settings-group-title">Connection</h3>
<div class="field">
Expand Down
4 changes: 4 additions & 0 deletions viewer_ui/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export async function loadSummariesPage(
return fetchJson(`/api/summaries?${query}`);
}

export async function loadObserverStatus(): Promise<any> {
return fetchJson('/api/observer-status');
}

export async function loadConfig(): Promise<any> {
return fetchJson('/api/config');
}
Expand Down
91 changes: 90 additions & 1 deletion viewer_ui/src/tabs/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -786,11 +786,100 @@ export async function saveSettings(startPolling: () => void, refreshCallback: ()
}
}

function formatAuthMethod(method: string): string {
switch (method) {
case 'anthropic_consumer':
return 'OAuth (Claude Max/Pro)';
case 'codex_consumer':
return 'OAuth (ChatGPT subscription)';
case 'sdk_client':
return 'API key';
case 'claude_sidecar':
return 'Local Claude session';
case 'opencode_run':
return 'OpenCode sidecar';
default:
return method || 'none';
}
}

function formatCredentialSources(creds: Record<string, boolean>): string {
const parts: string[] = [];
if (creds.oauth) parts.push('OAuth');
if (creds.api_key) parts.push('API key');
if (creds.env_var) parts.push('env var');
return parts.length ? parts.join(', ') : 'none';
}

function createEl(tag: string, className?: string, text?: string): HTMLElement {
const el = document.createElement(tag);
if (className) el.className = className;
if (text) el.textContent = text;
return el;
}

function renderObserverStatusBanner(status: any) {
const banner = $('observerStatusBanner');
if (!banner) return;

if (!status || typeof status !== 'object') {
banner.hidden = true;
return;
}

banner.textContent = '';
const active = status.active;
const available = status.available_credentials || {};

if (active) {
const provider = String(active.provider || 'unknown');
const model = String(active.model || '');
const method = formatAuthMethod(active.auth?.method || 'none');
const tokenOk = active.auth?.token_present === true;

banner.append(createEl('div', 'status-label', 'Active observer'));
const row = createEl('div', 'status-active');
row.textContent = `${provider} \u2192 ${model} via ${method} `;
const tokenSpan = createEl('span', tokenOk ? 'cred-ok' : 'cred-none', tokenOk ? '\u2713' : '\u2717');
row.append(tokenSpan);
banner.append(row);
} else {
banner.append(createEl('div', 'status-label', 'Observer status'));
banner.append(createEl('div', 'status-active', 'Not yet initialized (waiting for first session)'));
}

const credEntries = Object.entries(available).filter(
([, creds]) => creds && typeof creds === 'object',
);
if (credEntries.length) {
banner.append(createEl('div', 'status-label', 'Available credentials'));
const row = createEl('div');
credEntries.forEach(([provider, creds], idx) => {
const c = creds as Record<string, boolean>;
const sources = formatCredentialSources(c);
const hasAny = Object.values(c).some(Boolean);
const span = createEl('span', 'status-cred');
const icon = createEl('span', hasAny ? 'cred-ok' : 'cred-none', hasAny ? '\u2713' : '\u2013');
span.append(icon);
span.append(` ${String(provider)}: ${sources}`);
if (idx > 0) row.append(' \u00b7 ');
row.append(span);
});
banner.append(row);
}

banner.hidden = false;
}

export async function loadConfigData() {
if (settingsOpen) return;
try {
const payload = await api.loadConfig();
const [payload, status] = await Promise.all([
api.loadConfig(),
api.loadObserverStatus().catch(() => null),
]);
renderConfigModal(payload);
renderObserverStatusBanner(status);
} catch {}
}

Expand Down
Loading