Public, free to read, reuse, and adapt — Kirk Patrick Miller's notice for this edition. Not a patent grant.
FLINT_HEAL_MODEL_CHOICE_STICKS_v0.1_tiny.txt · c06
c12bf357bc · words 1450 · characters 50222–62276
Previous section · Next section
Exact section text · Full source · Source index
'FLActiveModel says user');
+ assert.strictEqual(store.s.fl_preferred_text_model, 'qwen2.5:latest', 'preferred text model follows the tap');
+ p.FlConnect.unmount();
+
+ // ── 2. Tab change straight to Chat, then Settings (the automatic picker runs on every tab change)
+ p.FLAutoModel.onTabChanged('chat');
+ await flush();
+ assert.strictEqual(p.state.ollamaModel, 'qwen2.5:latest', 'Chat tab: still qwen2.5');
+ p.FLAutoModel.onTabChanged('settings');
+ await flush();
+ assert.strictEqual(p.state.ollamaModel, 'qwen2.5:latest', 'Settings: still qwen2.5');
+ assert.strictEqual(store.s.fl_ollamaModel, 'qwen2.5:latest', 'storage still qwen2.5');
+
+ // ── 3. The chat request uses it
+ let reply = null;
+ p.FreeLattice.callAI('sys', 'hello', { callback: (r) => { reply = r; } });
+ await flush();
+ assert.strictEqual(p._posts.length, 1, 'one chat request');
+ assert.strictEqual(p._posts[0].body.model, 'qwen2.5:latest', 'chat request body uses the chosen model');
+ assert.strictEqual(reply, 'ok', 'reply arrives');
+ assert.ok(/modelId = state\.ollamaModel \|\| 'llama3\.2';/.test(app), 'main Chat send also reads state.ollamaModel');
+
+ // ── 4. Reload: a fresh page from the same storage, then a tab change and Settings
+ p = page(store);
+ assert.strictEqual(p.state.ollamaModel, 'qwen2.5:latest', 'reload: state from storage');
+ assert.strictEqual(p.FLActiveModel.isUserChosen(), true, 'reload: still the person\'s choice');
+ p.FLAutoModel.onTabChanged('chat');
+ p.FLAutoModel.onTabChanged('settings');
+ await flush();
+ assert.strictEqual(p.state.ollamaModel, 'qwen2.5:latest', 'reload + tabs: still qwen2.5');
+ p.FreeLattice.callAI('sys', 'again', { callback() {} });
+ await flush();
+ assert.strictEqual(p._posts[0].body.model, 'qwen2.5:latest', 'after reload the chat request still uses qwen2.5');
+ assert.strictEqual(p.toasts.length, 0, 'nothing to say while the choice is installed');
+
+ // ── 5. The chosen model left this computer: fill in, and say so (textContent via showToast)
+ p.installed = [{ name: 'llama3.2:latest' }, { name: 'llava:7b' }];
+ p.FLAutoModel.invalidateCache();
+ p.FLAutoModel.onTabChanged('chat');
+ await flush();
+ assert.strictEqual(p.state.ollamaModel, 'llama3.2:latest', 'gone: first installed text model fills in');
+ assert.strictEqual(p.FLActiveModel.get().source, 'auto', 'fill-in is automatic, not the person\'s choice');
+ assert.ok(p.toasts.some((t) => /qwen2\.5:latest, is no longer on this computer\. Using llama3\.2:latest for now\./.test(t)), 'says so plainly: ' + JSON.stringify(p.toasts));
+ assert.strictEqual(store.s.fl_modelChoiceGone, 'qwen2.5:latest', 'the gone choice is remembered');
+
+ // ── 6. A quiet Ollama never replaces a choice
+ {
+ const st = store0({ fl_isLocal: 'true', fl_provider: 'ollama', fl_ollamaModel: 'qwen2.5:latest', fl_modelSource: 'user' });
+ const q = page(st);
+ q.installed = [];
+ q.FLAutoModel.onTabChanged('chat');
+ await flush();
+ assert.strictEqual(q.state.ollamaModel, 'qwen2.5:latest', 'empty answer: choice kept');
+ assert.strictEqual(q.toasts.length, 0, 'empty answer: nothing said');
+ // 'qwen2.5' (no tag) still matches 'qwen2.5:latest'
+ const st2 = store0({ fl_isLocal: 'true', fl_provider: 'ollama', fl_ollamaModel: 'qwen2.5', fl_modelSource: 'user' });
+ const q2 = page(st2);
+ q2.FLAutoModel.onTabChanged('chat');
+ await flush();
+ assert.strictEqual(q2.state.ollamaModel, 'qwen2.5', 'untagged name counts as installed');
+ }
+
+ // ── 7. No user choice yet: the automatic picker still fills in (unchanged behavior)
+ {
+ const st = store0({ fl_isLocal: 'true', fl_provider: 'ollama', fl_ollamaModel: 'qwen2.5:latest' });
+ const a = page(st);
+ a.FLAutoModel.onTabChanged('chat');
+ await flush();
+ assert.strictEqual(a.state.ollamaModel, 'llama3.2:latest', 'no choice: auto picks the first text model as before');
+ }
+
+ // ── 8. Tree: markUserChoice is a quiet no-op (no FLActiveModel there)
+ {
+ const st = store0();
+ const t = { localStorage: st.ls, location: { hostname: 'thelatticetree.com', protocol: 'https:', hash: '', search: '' },
+ document: { documentElement: { getAttribute: () => '' }, getElementById: () => null, addEventListener() {}, body: { classList: { add() {}, remove() {} } } },
+ console, JSON, String, parseInt, URL, Date, Math, Promise, setTimeout: () => 1, clearTimeout() {} };
+ t.window = t;
+ vm.runInNewContext(mod, t);
+ assert.strictEqual(t.FlConnect.markUserChoice('qwen2.5:latest'), false, 'Tree: no-op');
+ }
+
+ // ── 9. Settings row comes back after Local (createElement/textContent)
+ {
+ const actions = fakeEl('div');
+ const ctx = { document: { createElement: (t) => fakeEl(t) }, window: {} };
+ ctx.window = ctx;
+ vm.runInNewContext(settingsSrc, ctx);
+ ctx.flRefillSettingsActions(actions);
+ assert.deepStrictEqual(actions.children.map((b) => b.textContent), ['Change Provider', 'Test Connection', '\uD83C\uDFE0 a mind at home'], 'row refilled');
+ ctx.flRefillSettingsActions(actions);
+ assert.strictEqual(actions.children.length, 3, 'never doubled');
+ const local = between(app, "if (mode === 'local') {", 'v-connect-card-yesno-wizard-v0 — Local mode');
+ assert.ok(/actionsDiv\.innerHTML = '';\s*\n\s*\/\/ v-model-choice-sticks-v0[^\n]*\n\s*flRefillSettingsActions\(actionsDiv\);/.test(local), 'Local refills the row right after emptying it');
+ }
+
+ // ── 10. Helped Bridge on a typed port, no mind behind it: honest words, nothing saved as the Ollama door
+ {
+ const st = store0({ fl_localPort_manual: '11500' });
+ const b = page(st, {
+ fetch: async (url) => {
+ url = String(url);
+ if (/127\.0\.0\.1:11500\/bridge\/health$/.test(url)) return { ok: true, status: 200, json: async () => ({ bridge: true, helped: true, port: 11500 }) };
+ if (/127\.0\.0\.1:11500\/api\/tags$/.test(url)) return { ok: false, status: 502, json: async () => ({}) };
+ throw new Error('quiet');
+ }
+ });
+ const rep = await b.FlConnect.probe({ gesture: true });
+ assert.strictEqual(rep.manualQuiet, true, 'still quiet');
+ assert.strictEqual(rep.bridgeNoMind, true, 'Bridge answered, no mind');
+ assert.ok(!('fl_ollamaHost' in st.s), 'not saved as the Ollama host');
+ assert.ok(!('fl_bridgePort' in st.s), 'not saved as the Bridge port until a mind answers');
+ const h = connectHost();
+ b.FlConnect.mount(h);
+ await flush();
+ assert.ok(/Your Bridge answered on 11500, but no mind is running behind it yet\./.test(h.innerHTML), 'honest words');
+ assert.ok(!/Nothing answered on/.test(h.innerHTML), 'no false "Nothing answered"');
+ b.FlConnect.unmount();
+ // and when the mind is there, it is saved as before
+ const st2 = store0({ fl_localPort_manual: '11500' });
+ const b2 = page(st2, {
+ fetch: async (url) => {
+ url = String(url);
+ if (/127\.0\.0\.1:11500\/bridge\/health$/.test(url)) return { ok: true, status: 200, json: async () => ({ bridge: true, helped: true, port: 11500 }) };
+ if (/127\.0\.0\.1:11500\/api\/tags$/.test(url)) {
+ b2._n = (b2._n || 0) + 1;
+ if (b2._n === 1) throw new Error('first ask before the Bridge said yes');
+ return { ok: true, status: 200, json: async () => ({ models: MODELS }) };
+ }
+ throw new Error('quiet');
+ }
+ });
+ const rep2 = await b2.FlConnect.probe({ gesture: true });
+ assert.strictEqual(rep2.models.length, 2, 'mind behind the Bridge found');
+ assert.strictEqual(st2.s.fl_ollamaHost, '127.0.0.1:11500', 'saved once a mind answers');
+ assert.strictEqual(st2.s.fl_bridgePort, '11500', 'Bridge port saved once a mind answers');
+ }
+
+ // ── Locks
+ assert.strictEqual(fs.readFileSync(path.join(repo, 'index.html'), 'utf8'), app, 'index.html identical to docs/app.html');
+ let mainSw = null;
+ try { mainSw = execSync('git show origin/main:sw.js', { cwd: repo, encoding: 'utf8' }); } catch (e) {}
+ if (mainSw !== null) {
+ assert.strictEqual(fs.readFileSync(path.join(repo, 'sw.js'), 'utf8'), mainSw, 'root sw.js byte-identical to main');
+ const changed = execSync('git diff --name-only origin/main', { cwd: repo, encoding: 'utf8' }).split('\n');
+ assert.ok(!changed.includes('docs/sw.js'), 'docs/sw.js untouched');
+ const added = execSync('git diff origin/main -- docs/app.html docs/modules/fl-connect.js', { cwd: repo, encoding: 'utf8' })
+ .split('\n').filter((l) => l.startsWith('+') && !l.startsWith('+++'));
+ assert.ok(!added.some((l) => /\bconfirm\(/.test(l)), 'no confirm() added');
+ assert.ok(!added.some((l) => /\u2014/.test(l)), 'no em dashes added');
+ assert.ok(!added.some((l) => /['"]\*['"]/.test(l) && /origin/i.test(l)), 'no bare * origin');
+ }
+ const markers = execSync("grep -rnE '^(<<<<<<<|=======|>>>>>>>)( |$)' docs index.html tests || true", { cwd: repo, encoding: 'utf8' });
+ assert.ok(!markers.trim(), 'no conflict markers');
+ const re = /<script(\s[^>]*)?>/gi; let m; let n = 0;
+ while ((m = re.exec(app))) {
+ const attrs = m[1] || '';
+ if (/\bsrc\s*=/i.test(attrs) || /type\s*=\s*["']application\/(ld\+)?json/i.test(attrs)) continue;
+ const start = m.index + m[0].length; const end = app.indexOf('</script>', start);
+ if (end < 0) continue;
+ const s = app.slice(start, end).trim();
+ if (!s || !/^(?:\/[\/*]|function\b|var\b|let\b|const\b|window\.|document\.|\(|\{|if\b)/.test(s)) continue;
+ try { new Function(s); n++; } catch (e) { assert.fail('inline script parse fail: ' + e.message); }
+ }
+ assert.ok(n > 10, 'inline scripts parse: ' + n);
+ console.log('SMOKE_OK model choice sticks v0.1');
+})().catch((e) => { console.error(e); process.exit(1); });
diff --git a/index.html b/index.html
index fa946fa..5003526 100644
--- a/index.html
+++ b/index.html
@@ -31549,6 +31549,27 @@ window.flUseBridgePort = flUseBridgePort;
window.flBridgeHealth = flBridgeHealth;
window.flRememberHelpedBridge = flRememberHelpedBridge;
+// v-model-choice-sticks-v0: the Settings button row (Change Provider, Test Connection,
+// a mind at home) comes back after Local empties it. Built with createElement/textContent.
+function flRefillSettingsActions(actionsDiv) {
+ if (!actionsDiv || actionsDiv.children.length) return;
+ function add(label, cls, onTap, green) {
+ var b = document.createElement('button');
+ b.type = 'button';
+ b.className = cls;
+ b.textContent = label;
+ if (green) { b.style.borderColor = 'var(--emerald)'; b.style.color = 'var(--emerald)'; }
+ b.addEventListener('click', function() { try { onTap(); } catch (e) {} });
+ actionsDiv.appendChild(b);
+ }
+ add('Change Provider', 'ai-status-btn ai-status-btn-primary', function() { if (typeof openProviderModal === 'function') openProviderModal(); });
+ add('Test Connection', 'ai-status-btn', function() { if (typeof AiSetup !== 'undefined' && AiSetup.testFromSettings) AiSetup.testFromSettings(); });
+ add('\uD83C\uDFE0 a mind at home', 'ai-status-btn', function() {
+ if (window.FlConnect && FlConnect.open) FlConnect.open(); else if (typeof switchTab === 'function') switchTab('connect');
+ }, true);
+}
+window.flRefillSettingsActions = flRefillSettingsActions;
+
function settingsSetMode(mode) {
var browserBtn = document.getElementById('settingsModeBrowser');
var cloudBtn = document.getElementById('settingsModeCloud');
@@ -31589,6 +31610,8 @@ function settingsSetMode(mode) {
var stLocal = document.getElementById('settingsStatusText');
if (stLocal) stLocal.textContent = 'Opening Connect. Tap the mind you want there.';
if (actionsDiv) actionsDiv.innerHTML = '';
+ // v-model-choice-sticks-v0: the old wizard buttons are gone; put the everyday row back
+ flRefillSettingsActions(actionsDiv);
try { FlConnect.open(); } catch (eOpenL) {}
return;
}
@@ -36259,6 +36282,40 @@ window.FLAutoModel = (function() {
}