FreeLattice · Celeste reading home

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

Archive pages whose extracted text matched this section: 1045, 1046. Unmatched pages stay on the source record with the checked page method. They are not guessed into a section.

No topic tag was earned for this page under the recorded evidence rule.

 '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() {
   }