function isVision(name) { return VISION_RE.test(name || ''); } + + // v-model-choice-sticks-v0: is the chosen model still installed? ('qwen2.5' matches 'qwen2.5:latest') + function onInstalledList(models, name) { + if (!name) return false; + var n = String(name); + return (models || []).some(function(m) { + var mn = String((m && (m.name || m.model)) || ''); + return mn === n || mn === n + ':latest' || (n.indexOf(':') === -1 && mn.split(':')[0] === n); + }); + } + // v-model-choice-sticks-v0: a user choice is only replaced when Ollama answers with a list that + // no longer has it. A quiet or empty answer never replaces anything. The words go in with textContent (showToast). + function checkChosenStillHere(current, tabId) { + if (!current || !current.model) return; + if (current.provider && current.provider !== 'ollama') return; + getInstalledModels(function(models) { + if (!models || !models.length) return; + if (typeof FLActiveModel === 'undefined' || !FLActiveModel.isUserChosen()) return; + if (FLActiveModel.get().model !== current.model) return; + if (onInstalledList(models, current.model)) return; + var needsVision = VISION_TABS.indexOf(tabId) !== -1; + var pool = models.filter(function(m) { return needsVision ? isVision(m.name) : !isVision(m.name); }); + if (!pool.length) pool = models; + var pick = String((pool[0] && (pool[0].name || pool[0].model)) || ''); + if (!pick) return; + safeSet('fl_modelChoiceGone', current.model); + FLActiveModel.set(pick, 'ollama', 'auto'); + try { + if (typeof showToast === 'function') showToast('The model you chose, ' + current.model + ', is no longer on this computer. Using ' + pick + ' for now. You can pick another in Connect.', 7000); + } catch (eT) {} + try { if (typeof AiSetup !== 'undefined' && AiSetup.refreshSettingsCard) AiSetup.refreshSettingsCard(); } catch (eS) {} + }); + } + function getPreferred(type) { return safeGet('fl_preferred_' + type + '_model', null); } function setPreferred(type, model) { safeSet('fl_preferred_' + type + '_model', model); } @@ -36269,6 +36326,9 @@ window.FLAutoModel = (function() { // "User choice is sacred" — the auto-switcher must never lose it if (typeof FLActiveModel !== 'undefined' && FLActiveModel.isUserChosen()) { var current = FLActiveModel.get(); + // v-model-choice-sticks-v0: the person's model stays. Only if it left this computer + // does the picker fill in, and then it says so in plain words. + checkChosenStillHere(current, tabId); var needsVision = VISION_TABS.indexOf(tabId) !== -1; var currentIsVision = isVision(current.model); if (needsVision && !currentIsVision) { ====================================================================== APPENDIX B: theLatticeTree full diff vs main 395c761 (git apply --check clean) ====================================================================== diff --git a/docs/library/CONNECT_PORT_PICKER_SISTER_v0.md b/docs/library/CONNECT_PORT_PICKER_SISTER_v0.md index d2e6341..cd688da 100644 --- a/docs/library/CONNECT_PORT_PICKER_SISTER_v0.md +++ b/docs/library/CONNECT_PORT_PICKER_SISTER_v0.md @@ -20,3 +20,14 @@ Marker `v-connect-heal-v0.4`. Test `docs/modules/connect-heal-v04.test.js` print - Shared `fl-connect.js` heals ride along byte-identical with FreeLattice. **Temperature:** Mom opens Settings and the first thing she sees is the door. + + +## Heal: model choice sticks v0.1 (Hypha walk 4) + +Marker `v-model-choice-sticks-v0`. Test `docs/modules/model-choice-sticks.test.js` prints `SMOKE_OK model choice sticks alpha v0.1`. + +- FreeLattice's Connect now records a tapped model as the person's own choice, so FreeLattice's automatic picker leaves it alone. The Tree has no automatic picker: the remembered entry (`fl_alpha_local_mind`) is the choice, and the test proves it survives a reload, Settings and a Find at the same door, and that the thread request uses it. +- Shared `fl-connect.js` rides along byte-identical: `markUserChoice` (a quiet no-op on the Tree), and a helped Bridge with no mind behind it is named honestly and not saved. +- A Find through a different door (Bridge gone, direct 11434 answers) is healed by the Tree any-model paste (same-family keep). + +**Temperature:** green braided gold, the second cup stays where she set it down. diff --git a/docs/modules/fl-connect.js b/docs/modules/fl-connect.js index 5317005..2e02eae 100644 --- a/docs/modules/fl-connect.js +++ b/docs/modules/fl-connect.js @@ -5,6 +5,7 @@ // Layer, never delete. Prefer helped Bridge (11435…) before bare 11434. // Port-only loopback: fl_localPort_manual holds "11500" only — never foreign hosts. // Soft leave sw.js. Alpha: fl_alpha_local_mind. Never bare *. +// Marker: v-model-choice-sticks-v0 (a tapped model is the person's own choice; it stays) // ═══════════════════════════════════════════════════════════════ (function (root) { @@ -201,12 +202,18 @@ report.bridge = mh; report.port = mh.port || parseInt(manPort, 10); report.helped = true; - report.base = preferHelpedBridge(report.port) || manBase; + // v-model-choice-sticks-v0: ask the helped Bridge first; save it as the Ollama door + // (fl_ollamaHost, fl_bridgePort) only once a mind really answers behind it. + report.base = (report.port && report.port !== 11434) ? 'http://127.0.0.1:' + report.port : manBase; try { report.models = await tagsAt(report.base); + report.base = preferHelpedBridge(report.port) || report.base; report.ollama = { via: 'manual-bridge', base: report.base }; return report; - } catch (eMB) {} + } catch (eMB) { + report.bridgeNoMind = true; + report.models = []; + } } else if (mh) { report.bridge = mh; report.port = mh.port || parseInt(manPort, 10); @@ -357,6 +364,9 @@ // v-connect-heal-v0.4 item 10: handleLocalToggle reads #localToggle, so check it first try { var lt = document.getElementById('localToggle'); if (lt) lt.checked = true; } catch (eLt) {} if (typeof root.handleLocalToggle === 'function') root.handleLocalToggle(true); + // v-model-choice-sticks-v0: the tap is the person's own choice, so the automatic + // picker leaves it alone on tab changes, reloads and Settings. + markUserChoice(name, 'ollama'); if (typeof root.updateStatus === 'function') root.updateStatus(); if (root.AiSetup && typeof root.AiSetup.updateStatus === 'function') root.AiSetup.updateStatus(); // v-connect-heal-v0.4 item 7: once they chose a mind, the Local AI toast stops asking @@ -366,6 +376,29 @@ return { name: name, base: base }; } + // v-model-choice-sticks-v0: FreeLattice keeps one active model (FLActiveModel). A model the + // person tapped is recorded with source 'user' and as their preferred text (or vision) model. + // The Tree has neither helper, so this is a quiet no-op there (it keeps entry.model instead). + function markUserChoice(name, provider) { + if (!name) return false; + var marked = false; + try { + if (root.FLActiveModel && typeof root.FLActiveModel.set === 'function') { + root.FLActiveModel.set(String(name), provider || 'ollama', 'user'); + marked = true; + } + } catch (e0) {} + try { + var auto = root.FLAutoModel; + if (auto && typeof auto.setPreferred === 'function') { + var kind = (typeof auto.isVision === 'function' && auto.isVision(String(name))) ? 'vision' : 'text'; + auto.setPreferred(kind, String(name)); + if (typeof auto.invalidateCache === 'function') auto.invalidateCache(); + } + } catch (e1) {} + return marked; + } + function open() { try { if (!isAlpha() && typeof root.switchTab === 'function') { root.switchTab('connect'); return true; } @@ -539,7 +572,12 @@