diff --git a/docs/library/TREE_ANY_MODEL_v0.1.md b/docs/library/TREE_ANY_MODEL_v0.1.md new file mode 100644 index 0000000..923ee70 --- /dev/null +++ b/docs/library/TREE_ANY_MODEL_v0.1.md @@ -0,0 +1,33 @@ +# Tree any model v0.1 (v-tree-any-model-v0) + +Alpha layer, with a byte-identical twin of `docs/modules/fl-connect.js` in FreeLattice. October 1, 2026. + +**Kirk's words:** any locally downloaded model is usable and changeable in any order on theLatticeTree, with no friction, via Connect or the Bridge, 127.0.0.1 only. + +## What blocked it (the map, Sep 28 walk) + +- A cap of five models per door (`parseModelNames`), five stars per door in the sky, twelve Gathering options. +- A Connect tap replaced the whole remembered entry: other models, other doors (LM Studio), the door name and the speaking chair were lost. +- After a Connect tap then a Find, a chosen model that fell past the cap was called "no longer at this door" and the speaking model switched under her. +- With the Bridge up, Find asked only the Bridge, so LM Studio was invisible. +- The paste field fetched any host (a LAN address was fetched). +- The Chat heart named the door, not the model. The trainer read FreeLattice's `fl_active_model`, which the Tree never writes. + +## What changed + +- `local-mind-probe.js`: `MODEL_CAP` 64, `SKY_FOLD` 8 with "Show all N models" (the chosen one always shown); duplicates collapse; Gathering options up to 64; new `mergeFound(found)` adds or updates one door and keeps every other door, the roster, the chair binds and the speaking chair; `entryFromFoundList` keeps a chosen model that is at any found door ("Still speaking with X, now through Y." or "The chosen model X is at Y. Tap it to speak with it again."), and only says "no longer at this door" when it is truly absent; with the Bridge up, Find still asks the non-Ollama doors; `tryAddress` takes 127.0.0.1 / localhost / [::1] or a bare port only, and refuses anything else with zero fetches; one honesty line for LM Studio CORS. +- `fl-connect.js` (shared, both repos): on the Tree a Connect tap calls `LocalMindProbe.mergeFound` (the old replace path stays below it for an older probe); the model list is passed along; the chosen model button has `aria-pressed="true"` and `is-chosen`. FreeLattice's own branch of `remember()` is unchanged. +- `garden-thread.js`: the heart reads "Listening: Ollama · llama3.2:latest. On this machine only.", repaints when a mind is remembered, and a quiet "change mind" button opens Settings (not shown inside Settings). +- `garden-trainer.js`: `activeModelName()` reads `fl_active_model` first, then the Tree's remembered model; the old `llama3.2` / `phi-pathway` fallbacks stay last. + +## Trust decision + +The Agent Bridge (127.0.0.1:3141, locked by FreeLattice #120) stays out of the Tree's model path: it would add pairing for a purely local act, it is Ollama-only, and it would touch #120's lock files. The Tree keeps the Ollama Bridge (11435) and direct loopback doors. + +## Later (not built) + +- Round Table sitters from roster seats and Gathering binds, so two models at one Ollama door can sit together. +- LM Studio through the Ollama Bridge (a second upstream in `bridge/proxy-core.js`). +- Teaching `FlConnect.probe` the OpenAI `/v1/models` shape (would change FreeLattice behavior). + +Layer, never delete. diff --git a/docs/modules/fl-connect.js b/docs/modules/fl-connect.js index 2e02eae..785ed99 100644 --- a/docs/modules/fl-connect.js +++ b/docs/modules/fl-connect.js @@ -319,2 +319,22 @@ + // v-tree-any-model-v0: 11434 is the Ollama door; any other 127.0.0.1 port here is the Bridge + function doorNameFor(base, port) { + var p = String(port || ''); + var m = String(base || '').match(/:(\d{1,5})\/?$/); + if (!p && m) p = m[1]; + return (!p || p === '11434') ? 'Ollama' : 'Bridge'; + } + + // v-tree-any-model-v0: which model speaks now, so the Connect card can show it + function chosenModelName() { + try { + if (isAlpha()) { + var m = root.LocalMindProbe && root.LocalMindProbe.getRemembered && root.LocalMindProbe.getRemembered(); + return m && m.model ? String(m.model) : ''; + } + if (root.state && root.state.ollamaModel) return String(root.state.ollamaModel); + return String(localStorage.getItem('fl_ollamaModel') || ''); + } catch (e) { return ''; } + } + function remember(mind) { @@ -327,2 +347,14 @@ if (isAlpha()) { + // v-tree-any-model-v0: on the Tree, Connect merges into the sky (all models, other doors kept) + if (root.LocalMindProbe && typeof root.LocalMindProbe.mergeFound === 'function') { + try { + var merged = root.LocalMindProbe.mergeFound({ + name: doorNameFor(base, mind.port), + url: base ? String(base).replace(/\/+$/, '') + '/api/tags' : '', + models: mind.models || [], + model: name + }); + if (merged) { stopLoop(); return merged; } + } catch (eM) {} + } var entry = { @@ -518,4 +550,9 @@ btn.dataset.model = name; + // v-tree-any-model-v0: show what speaks + var isOn = chosenModelName() === name; + btn.setAttribute('aria-pressed', isOn ? 'true' : 'false'); + if (isOn) btn.classList.add('is-chosen'); btn.addEventListener('click', function () { - remember({ name: name, base: report.base, port: report.port }); + // v-tree-any-model-v0: pass the whole list along (FreeLattice's branch ignores it) + remember({ name: name, base: report.base, port: report.port, models: report.models }); refreshUI({ force: true }); @@ -687,2 +724,3 @@ '.flc-model{min-height:44px;text-align:left;padding:.7rem .85rem;border-radius:10px;border:1px solid rgba(200,210,230,.16);background:rgba(8,6,18,.45);color:#e6ebf5;font:inherit;cursor:pointer}', + '.flc-model.is-chosen{outline:2px solid currentColor;font-weight:600}', /* v-tree-any-model-v0 */ '.flc-btn{display:inline-block;min-height:48px;line-height:48px;padding:0 1.1rem;border-radius:10px;text-decoration:none;font-weight:600;border:0;cursor:pointer;font:inherit}', diff --git a/docs/modules/garden-rooms.css b/docs/modules/garden-rooms.css index 7c51be0..70a03fd 100644 --- a/docs/modules/garden-rooms.css +++ b/docs/modules/garden-rooms.css @@ -6186 +6186,32 @@ html.flc-hide-galaxy #gardenContainer { } + +/* v-tree-any-model-v0: every model at a door is a star. Eight show, then "Show all N". + A long chair list scrolls inside the picker. "change mind" sits quietly by the heart. */ +.settings-star-models { flex-wrap: wrap; } +.settings-star-more { + font: inherit; + font-size: 0.85rem; + background: none; + border: 0; + color: inherit; + opacity: 0.75; + text-decoration: underline; + min-height: 44px; + padding: 0 0.5rem; + cursor: pointer; +} +.core-bind-options { max-height: 50vh; overflow-y: auto; overscroll-behavior: contain; } +.thread-change { + font: inherit; + font-size: 0.82rem; + min-height: 32px; + margin: 0.15rem 0 0.35rem; + padding: 0.1rem 0.6rem; + background: none; + border: 1px solid rgba(200, 210, 230, 0.3); + border-radius: 999px; + color: inherit; + opacity: 0.85; + cursor: pointer; +} +.thread-change:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; } diff --git a/docs/modules/garden-thread.js b/docs/modules/garden-thread.js index 4424654..51bf270 100644 --- a/docs/modules/garden-thread.js +++ b/docs/modules/garden-thread.js @@ -492,2 +492,20 @@ + // v-tree-any-model-v0: "Listening: Ollama · llama3.2:latest. On this machine only." + function heartListening(mind) { + var speaking = mind.model || (mind.models && mind.models[0]) || ''; + return 'Listening: ' + (mind.name || 'a mind at home') + + (speaking && speaking !== mind.name ? ' \u00b7 ' + speaking : '') + '. On this machine only.'; + } + // v-tree-any-model-v0: every mounted thread repaints its heart when a mind is remembered + try { + window.addEventListener('fl-alpha-mind-remembered', function () { + var hearts = document.querySelectorAll('[data-thread-heart]'); + if (!hearts.length) return; + var now = listener(); + for (var hi = 0; hi < hearts.length; hi++) { + hearts[hi].textContent = now ? heartListening(now) : HEART_NONE; + } + }); + } catch (eHeart) { /* fail-quiet */ } + function mount(container, opts) { @@ -508,3 +526,17 @@ } + // v-tree-any-model-v0: name the model, not only the door (the line above stays as the base) + heart.setAttribute('data-thread-heart', '1'); + if (mind) heart.textContent = heartListening(mind); root.appendChild(heart); + // v-tree-any-model-v0: change the mind from where you are + if (mind && !opts.inSettings) { + var change = el('button', 'thread-change', 'change mind'); + change.type = 'button'; + change.setAttribute('data-thread-change', '1'); + change.addEventListener('click', function () { + if (window.GardenRooms && typeof GardenRooms.openPlace === 'function') GardenRooms.openPlace('settings'); + else location.href = 'settings.html'; + }); + root.appendChild(change); + } diff --git a/docs/modules/garden-trainer.js b/docs/modules/garden-trainer.js index 644f165..eb2f293 100644 --- a/docs/modules/garden-trainer.js +++ b/docs/modules/garden-trainer.js @@ -70,2 +70,14 @@ const GardenTrainer = (() => { // ================================================================ + // v-tree-any-model-v0: the Tree remembers its model in fl_alpha_local_mind. + // (Before: localStorage.getItem('fl_active_model') at each call, a FreeLattice key the Tree never writes.) + function activeModelName() { + try { + var a = localStorage.getItem('fl_active_model'); + if (a) return a; + var m = window.LocalMindProbe && LocalMindProbe.getRemembered && LocalMindProbe.getRemembered(); + if (m && m.model) return String(m.model); + } catch (e) {} + return ''; + } + function getTrainerTierUnlocks() { @@ -730,3 +742,3 @@ const GardenTrainer = (() => { t1btn.textContent = 'Export Personality File'; - t1btn.onclick = function() { exportPersonalityModelfile(localStorage.getItem('fl_active_model')); }; + t1btn.onclick = function() { exportPersonalityModelfile(activeModelName()); }; t1.appendChild(t1btn); @@ -767,3 +779,3 @@ const GardenTrainer = (() => { btnPy.textContent = 'Export Python Fine-Tuner'; - btnPy.onclick = function() { exportPythonHelper(localStorage.getItem('fl_active_model')); }; + btnPy.onclick = function() { exportPythonHelper(activeModelName()); }; t2btns.appendChild(btnPy); @@ -898,5 +910,5 @@ const GardenTrainer = (() => { btnExpand.onclick = function() { - var proposal = proposeNextPathway(localStorage.getItem('fl_active_model')); + var proposal = proposeNextPathway(activeModelName()); var artifact = expandPathway(proposal, { - modelName: localStorage.getItem('fl_active_model') || 'phi-pathway' + modelName: activeModelName() || 'phi-pathway' }); @@ -1091,3 +1103,3 @@ const GardenTrainer = (() => { keepBtn.onclick = function() { - exportPersonalityModelfile(localStorage.getItem('fl_active_model')); + exportPersonalityModelfile(activeModelName()); }; diff --git a/docs/modules/local-mind-probe.js b/docs/modules/local-mind-probe.js index e710071..2af788e 100644 --- a/docs/modules/local-mind-probe.js +++ b/docs/modules/local-mind-probe.js @@ -31,2 +31,5 @@ var ROSTER_CAP = 4; + // v-tree-any-model-v0: every model at a door is choosable. Soft ceiling only against a runaway list. + var MODEL_CAP = 64; + var SKY_FOLD = 8; // stars shown before "Show all N"; the chosen one is always shown