From 9ad00e6c9ecc53ef6154e0167edaf35a52ff4635 Mon Sep 17 00:00:00 2001 From: witbrock Date: Mon, 14 Sep 2026 23:31:29 -0400 Subject: [PATCH] Show configured reasoning level in model footer --- .../web/von_interface/static/js/domUtils.js | 22 +++- .../web/von_interface/static/styles.css | 2 +- tests/browser/footerReasoning.cjs | 114 ++++++++++++++++++ .../frontend/footerLlmExecutionStatus.test.js | 40 ++++++ 4 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 tests/browser/footerReasoning.cjs diff --git a/src/frontend/web/von_interface/static/js/domUtils.js b/src/frontend/web/von_interface/static/js/domUtils.js index aadb91e6b..b0f4ee144 100644 --- a/src/frontend/web/von_interface/static/js/domUtils.js +++ b/src/frontend/web/von_interface/static/js/domUtils.js @@ -1473,7 +1473,7 @@ export async function setModelInfoFooterText() { const effectiveLlm = localModelUnavailable ? null : localModelPref.requestedLlm - ? { provider: localModelPref.requestedLlm.provider, model: localModelPref.requestedLlm.model } + ? localModelPref.requestedLlm : activeLlm; const userInfo = await getCurrentUserInfo(settings); @@ -1784,6 +1784,26 @@ export async function setModelInfoFooterText() { ); if (llmClass) modelSettingsSegment.classList.add('llm-status-badge', llmClass); segments.push(modelSettingsSegment); + // Show the configured request preference, independently of last-call model + // telemetry. The existing Settings selector owns supported values and writes. + const reasoningEffort = effectiveLlm?.model_parameters?.reasoning_effort; + const reasoningLabel = !effectiveLlm?.model + ? 'unselected' + : (typeof reasoningEffort === 'string' && reasoningEffort.trim()) || 'provider default'; + const reasoningSegment = makeActionButton( + 'Reasoning', + reasoningLabel, + () => { openSettingsForModelControls(); }, + { + ariaLabel: `Configured reasoning level ${reasoningLabel}. Open language model settings`, + title: `Configured reasoning level for ${configuredModel || 'the next request'}: ${reasoningLabel}. ` + + 'Provider default means no reasoning-effort override is requested. ' + + 'Change it in language model settings where the selected model supports it. ' + + 'This is a request preference, not confirmation of the last execution.', + }, + ); + reasoningSegment.classList.add('conversation-model-controls', 'footer-reasoning-level'); + segments.push(reasoningSegment); // This is deliberately a lightweight, independently repaintable telemetry // segment: cost updates must not retrigger settings, DB, or auth footer loads. segments.push(makeConversationRuntimeCostFooterSegment()); diff --git a/src/frontend/web/von_interface/static/styles.css b/src/frontend/web/von_interface/static/styles.css index d09917f0d..3f65846fb 100644 --- a/src/frontend/web/von_interface/static/styles.css +++ b/src/frontend/web/von_interface/static/styles.css @@ -18801,7 +18801,7 @@ html[data-footer-uptime="false"][data-footer-runtimecost="false"][data-footer-bu /* Compact status details expand in the row, preserving its scroll behaviour. Keep clipped text available to assistive technology even before expansion. */ .footer-container .db-conn-badge:not(:hover):not(:focus-within) .footer-quiet-metric, -.footer-container :is(.conversation-model-controls, .footer-user-segment, .footer-org-switcher):not(:hover):not(:focus-within) > .footer-label-inline { +.footer-container :is(.conversation-model-controls, .footer-user-segment, .footer-org-switcher):not(.footer-reasoning-level):not(:hover):not(:focus-within) > .footer-label-inline { position: absolute; width: 1px; height: 1px; diff --git a/tests/browser/footerReasoning.cjs b/tests/browser/footerReasoning.cjs new file mode 100644 index 000000000..00174f9b6 --- /dev/null +++ b/tests/browser/footerReasoning.cjs @@ -0,0 +1,114 @@ +// Isolated browser acceptance: real Settings/footer modules and template, fixture APIs. +// No live identity, database changes, or provider calls. +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const http = require('node:http'); +const path = require('node:path'); +const { chromium, expect } = require('@playwright/test'); +const root = path.resolve(__dirname, '../../src/frontend/web/von_interface'); +const evidence = process.argv[2] || '/tmp/footer-reasoning'; +fs.mkdirSync(evidence, { recursive: true }); +const model = 'gpt-5.6-luna'; +const llm = { provider: 'openai', model, model_parameters: { reasoning_effort: 'low' } }; +const requests = []; +const server = http.createServer((req, res) => { + const pathname = new URL(req.url, 'http://localhost').pathname; + if (pathname === '/') { + const html = fs.readFileSync(path.join(root, 'templates/settings_tab.html'), 'utf8') + .replace(/]*>[\s\S]*?<\/script>/g, '') + .replace(/{{ url_for\('static', filename='([^']+)'\) }}/g, '/static/$1') + .replace(/{%[\s\S]*?%}|{{[\s\S]*?}}/g, ''); + res.setHeader('Content-Type', 'text/html'); + return res.end(html.replace('', '')); + } + if (pathname.startsWith('/api/') || pathname.startsWith('/von/api/')) { + res.setHeader('Content-Type', 'application/json'); + let payload = {}; + if (pathname === '/api/settings/') payload = { + active_llm: llm, resolved_llm: llm, enabled_llms: [llm], + openai_api_key_env_var: 'OPENAI_API_KEY', server_default_llm: llm, + }; + if (pathname.includes('model_parameters/capabilities')) payload = { + success: true, parameters: { reasoning_effort: { supported: true, allowed_values: ['low', 'medium', 'high'] } }, + }; + if (pathname.includes('auth/status')) payload = { authenticated: true, user_concept_id: '#V#fixture', name: 'Fixture' }; + if (pathname.includes('session/context')) payload = { authenticated: true, context_source: 'window_session', user_id: '#V#fixture', role: 'user', namespace: null }; + if (pathname.includes('llm/info')) payload = { ...llm, status: 'ready' }; + if (pathname.includes('capability-index/status')) payload = { ready: true, status: 'ready' }; + if (pathname.endsWith('/models/openai')) payload = [model]; + if (pathname.includes('organisations')) payload = []; + if (pathname.endsWith('/ollama/models')) payload = { models: [] }; + if (pathname.endsWith('/ollama/hosts')) payload = { hosts: [] }; + if (pathname.endsWith('/openai/test_model')) { + let body = ''; + req.on('data', chunk => { body += chunk; }); + return req.on('end', () => { + requests.push(JSON.parse(body)); + res.end(JSON.stringify({ usable: true, model, reason: 'fixture' })); + }); + } + return res.end(JSON.stringify(payload)); + } + const file = path.resolve(root, `.${pathname}`); + if (!file.startsWith(`${root}/static/`) || !fs.existsSync(file)) return res.writeHead(404).end(); + res.setHeader('Content-Type', file.endsWith('.css') ? 'text/css' : 'text/javascript'); + res.end(fs.readFileSync(file)); +}); +(async () => { + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + console.log('Fixture listening'); + const browser = await chromium.launch({ headless: true, timeout: 20000 }); + console.log('Browser launched'); + try { + const page = await browser.newPage({ viewport: { width: 1280, height: 900 } }); + page.on('pageerror', error => console.error('Page error:', error.message)); + await page.goto(`http://127.0.0.1:${server.address().port}`); + console.log('Template loaded'); + await page.evaluate(({ model }) => { + localStorage.setItem('von_current_user', JSON.stringify({ concept_id: '#V#fixture', name: 'Fixture' })); + localStorage.setItem('von:localModelPreference', JSON.stringify({ + activeSource: 'openai', openaiModel: model, openaiModelParameters: { reasoning_effort: 'low' }, + })); + }, { model }); + async function mount() { + await page.evaluate(async () => { + const footer = await import('/static/js/domUtils.js'); + window.updateModelInfoFooterDisplay = footer.setModelInfoFooterText; + await import('/static/js/settingsPage.js'); + document.dispatchEvent(new Event('DOMContentLoaded')); + await footer.setModelInfoFooterText(); + }); + await page.locator('[data-settings-concern-tab="models"]').click(); + } + await mount(); + console.log('Mounted'); + const select = page.locator('#openaiReasoningEffortSelect'); + await expect(select).toHaveValue('low'); + await expect(page.locator('.footer-reasoning-level')).toHaveText('Reasoning: low'); + await select.selectOption('high'); + await expect(page.locator('.footer-reasoning-level')).toHaveText('Reasoning: high'); + await page.reload(); + await mount(); + console.log('Mounted'); + await expect(select).toHaveValue('high'); + await expect(page.locator('.footer-reasoning-level')).toHaveText('Reasoning: high'); + await page.locator('#testOpenAiModelButton').click(); + await expect.poll(() => requests.length).toBe(1); + assert.equal(requests[0].model_parameters.reasoning_effort, 'high'); + const requestFields = await page.evaluate(async () => (await import('/static/js/utils/localModelPreferences.js')).buildLocalModelRequestFields()); + assert.equal(requestFields.model_parameters.reasoning_effort, 'high'); + const measurements = []; + for (const width of [1280, 390]) { + await page.setViewportSize({ width, height: 900 }); + const label = page.locator('.footer-reasoning-level .footer-label-inline'); + assert.ok((await label.boundingBox()).width > 20, 'Reasoning label remains visible when compact'); + await page.locator('.footer-reasoning-level').screenshot({ path: path.join(evidence, `reasoning-${width}.png`) }); + measurements.push({ width, label: await label.boundingBox() }); + } + await select.selectOption(''); + await expect(page.locator('.footer-reasoning-level')).toHaveText('Reasoning: provider default'); + assert.equal(await page.evaluate(async () => (await import('/static/js/utils/localModelPreferences.js')).buildLocalModelRequestFields().model_parameters), undefined); + fs.writeFileSync(path.join(evidence, 'receipt.json'), JSON.stringify({ mode: 'isolated fixture APIs; real template and modules', requests, requestFields, measurements, reload: 'high restored', default: 'override omitted' }, null, 2)); + console.log('PASS: Settings → persisted preference → footer → reload → requested effort; desktop/mobile labels; provider default.'); + } finally { await browser.close(); } +})().catch(error => { console.error(error); process.exitCode = 1; }).finally(() => server.close()); diff --git a/tests/frontend/footerLlmExecutionStatus.test.js b/tests/frontend/footerLlmExecutionStatus.test.js index 86a1365e8..70c31965d 100644 --- a/tests/frontend/footerLlmExecutionStatus.test.js +++ b/tests/frontend/footerLlmExecutionStatus.test.js @@ -152,6 +152,46 @@ describe('footer latest LLM execution status', () => { delete window.__VON_RESTORE_TITLES; }); + test.each(['openai', 'gemini', 'openrouter', 'meta'])('reasoning follows the persisted %s request and opens settings', async (provider) => { + localStorage.setItem('von:localModelPreference', JSON.stringify({ + schemaVersion: 'localModelPreference.v1', activeSource: provider, + [`${provider}Model`]: 'fixture-model', + [`${provider}ModelParameters`]: { reasoning_effort: 'high' }, + })); + const { setModelInfoFooterText } = require(domUtilsPath); + const { buildLocalModelRequestFields } = require('../../src/frontend/web/von_interface/static/js/utils/localModelPreferences.js'); + await setModelInfoFooterText(); + const segment = document.querySelector('.footer-reasoning-level'); + expect(segment.textContent).toBe('Reasoning: high'); + expect(buildLocalModelRequestFields()).toMatchObject({ + model_provider: provider, model_parameters: { reasoning_effort: 'high' }, + }); + const settingsClick = jest.fn(); + document.querySelector('[data-tab="settingsTab"]').addEventListener('click', settingsClick); + segment.querySelector('button').click(); + expect(settingsClick).toHaveBeenCalledTimes(1); + expect(segment.querySelector('button').getAttribute('aria-label')).toContain('Configured reasoning level high'); + }); + + test('reasoning shows server preference, default and unselected without leaking inactive effort', async () => { + installFetchMock({ settingsPayload: { resolved_llm: { + provider: 'openai', model: 'fixture-model', model_parameters: { reasoning_effort: 'low' }, + } } }); + const { setModelInfoFooterText } = require(domUtilsPath); + await setModelInfoFooterText(); + expect(document.querySelector('.footer-reasoning-level').textContent).toBe('Reasoning: low'); + localStorage.setItem('von:localModelPreference', JSON.stringify({ + activeSource: 'ollama', openaiModel: 'fixture-model', + openaiModelParameters: { reasoning_effort: 'high' }, + ollamaSelection: { model: 'local-model' }, + })); + await setModelInfoFooterText(); + expect(document.querySelector('.footer-reasoning-level').textContent).toBe('Reasoning: provider default'); + localStorage.setItem('von:localModelPreference', JSON.stringify({ activeSource: 'ollama' })); + await setModelInfoFooterText(); + expect(document.querySelector('.footer-reasoning-level').textContent).toBe('Reasoning: unselected'); + }); + test('shows unselected model when premium is disabled and no Ollama model is selected', async () => { localStorage.setItem('von:localModelPreference', JSON.stringify({ schemaVersion: 'localModelPreference.v1',