FreeLattice · Celeste reading home

Public, free to read, reuse, and adapt — Kirk Patrick Miller's notice for this edition. Not a patent grant.

FLINT_NEXT_TREE_GATHERING_API_KEYS_v0.1.txt · c04

44d8662586 · words 1260 · characters 30475–40772

Previous section · Next section

Exact section text · Full source · Source index

Archive pages whose extracted text matched this section: 1418, 1419, 1420. 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.

+    // v-tree-gathering-api-keys-v0: the same mind may sit in two chairs. Allowed, and said.
+    function twinNote(chairId, seat) {
+      var binds = entryState().binds;
+      var twins = [];
+      CORE_CHAIRS.forEach(function (c) {
+        var b = binds[c.id];
+        if (c.id === chairId || !b) return;
+        if (String(b.model) === String(seat.model || '') && String(b.url) === String(seat.url || '')) twins.push(c.type);
+      });
+      if (!twins.length) return '';
+      return ' The ' + twins.join(' and ') + (twins.length > 1 ? ' chairs already seat' : ' chair already seats') +
+        ' this same mind, so they will sound alike.';
+    }
+
     function openPicker(chair, seatBtn) {
       var state = entryState();
       var options = (window.LocalMindProbe && LocalMindProbe.getChairSeatOptions)
         ? LocalMindProbe.getChairSeatOptions()
         : (state.roster || []);
+      // v-tree-gathering-api-keys-v0: cloud minds you chose (your key) sit beside local ones
+      if (window.TreeCloudMinds && typeof window.TreeCloudMinds.seatOptions === 'function') {
+        options = (options || []).concat(window.TreeCloudMinds.seatOptions());
+      }
       picker.innerHTML = '';
       picker.setAttribute('data-bind-chair', chair.id);
       picker.hidden = false;
@@ -1428,18 +1450,36 @@
           btn.type = 'button';
           btn.className = 'core-bind-option';
           btn.textContent = seat.tag + ' · ' + shortName(seat.model) + (seat.ephemeral ? ' (just found)' : '');
+          // v-tree-gathering-api-keys-v0: say where a cloud seat speaks from
+          if (seat.cloud) {
+            btn.textContent = seat.tag + ' · ' + shortName(seat.model) + ' · ' + seat.providerName + ' (your key)';
+            btn.setAttribute('data-cloud-seat', seat.url);
+          }
           btn.addEventListener('click', function () {
+            var twins = twinNote(chair.id, seat);
             if (window.LocalMindProbe && LocalMindProbe.setChairBind) {
               LocalMindProbe.setChairBind(chair.id, seat);
             }
             closePicker();
             paintAll();
-            setNoteText('This ' + chair.type + ' chair seats ' + seat.tag + ' · ' + shortName(seat.model) + '. Not a person-name. Not a dump.');
+            setNoteText('This ' + chair.type + ' chair seats ' + seat.tag + ' · ' + shortName(seat.model) + '. Not a person-name. Not a dump.' + twins);
           });
           list.appendChild(btn);
         });
         picker.appendChild(list);
       }
+      // v-tree-gathering-api-keys-v0: bring your own key, then seat it here in one step
+      if (window.TreeCloudMinds && typeof window.TreeCloudMinds.mountKeyPanel === 'function') {
+        window.TreeCloudMinds.mountKeyPanel(picker, function (seat) {
+          var twins = twinNote(chair.id, seat);
+          if (window.LocalMindProbe && LocalMindProbe.setChairBind) {
+            LocalMindProbe.setChairBind(chair.id, seat);
+          }
+          closePicker();
+          paintAll();
+          setNoteText('This ' + chair.type + ' chair seats ' + shortName(seat.model) + ' through ' + seat.providerName + ', with your key. Tap the chair to let it speak.' + twins);
+        });
+      }
       var decline = document.createElement('button');
       decline.type = 'button';
       decline.className = 'core-bind-decline';
@@ -1455,12 +1495,18 @@
         clear.className = 'core-bind-clear';
         clear.textContent = 'Clear';
         clear.addEventListener('click', function () {
+          // v-tree-gathering-api-keys-v0: clearing the speaking chair must not hand the next
+          // message to another mind without a word. Hush until a seated chair is tapped.
+          var wasSpeaking = entryState().speaking === chair.id;
+          if (wasSpeaking) window.GardenRooms.gatheringHush = true;
           if (window.LocalMindProbe && LocalMindProbe.setChairBind) {
             LocalMindProbe.setChairBind(chair.id, null);
           }
           closePicker();
           paintAll();
           setNoteText('Cleared. The ' + chair.type + ' chair has no mind seated — honest empty.');
+          var clearedNote = wasSpeaking && wrap.querySelector('[data-core-note]');
+          if (clearedNote) clearedNote.textContent += ' No chair is speaking now. Chat waits until you tap a seated chair.';
         });
         picker.appendChild(clear);
       }
@@ -1522,12 +1568,14 @@
           return;
         }
         // Bound: select as speaking chair for Gathering chat.
+        window.GardenRooms.gatheringHush = false;
         if (window.LocalMindProbe && LocalMindProbe.setSpeakingChair) {
           LocalMindProbe.setSpeakingChair(chair.id);
         }
         closePicker();
         paintAll();
-        setNoteText('Speaking with ' + bind.tag + ' · ' + shortName(bind.model) + ' at this chair. One request at a time. Use Change to reseat.');
+        // v-tree-gathering-api-keys-v0: name the chair, so two chairs with one mind differ
+        setNoteText('Speaking with ' + bind.tag + ' · ' + shortName(bind.model) + ' at the ' + chair.type + ' chair. One request at a time. Use Change to reseat.');
       });
       (chair.later ? laterRing : ring).appendChild(seat);
     });
diff --git a/docs/modules/garden-thread.js b/docs/modules/garden-thread.js
index f2794a5..e46792f 100644
--- a/docs/modules/garden-thread.js
+++ b/docs/modules/garden-thread.js
@@ -26,6 +26,10 @@
 //      remembered local mind can keep those without inventing a key
 //      kitchen. Honest later sentence in the room. Not a fake button.
 //
+// v-tree-gathering-api-keys-v0 (layer): a Gathering chair may seat a cloud mind
+//   (url 'cloud:<provider>') with the person's own key. That road lives in
+//   tree-cloud-minds.js. The key never enters this thread, its history or export.
+//   Files stay on this machine: a cloud mind hears only that a file stayed here.
 // Mirror: docs/code-dialogue.html  (read that FIRST)
 // ═══════════════════════════════════════════════════════════════
 
@@ -90,11 +94,22 @@
     return /failed|network|cors|mixed|blocked|abort|load|fetch/i.test(msg);
   }
 
+  // v-tree-gathering-api-keys-v0: after the speaking chair is cleared, the Gathering is hushed
+  // until a seated chair is tapped. Chat says so instead of answering from another mind.
+  var HEART_HUSH = 'No chair is speaking now. Tap a seated chair in the Gathering to let it speak.';
+  function gatheringHushed() {
+    var veil = document.getElementById('place-veil');
+    return !!(window.GardenRooms && window.GardenRooms.gatheringHush && veil &&
+      veil.classList.contains('is-core') && veil.classList.contains('is-open'));
+  }
+
   function listener() {
     if (!window.LocalMindProbe) return null;
     // Gathering speaking chair first (when veil is open and bound); else Settings default.
     if (typeof LocalMindProbe.resolveSpeakMind === 'function') {
       var seated = LocalMindProbe.resolveSpeakMind();
+      if (seated && seated.fromChair && (seated.url || seated.name)) return seated;
+      if (gatheringHushed()) return { hush: true, name: '', url: '' };
       if (seated && (seated.url || seated.name)) return seated;
     }
     if (typeof LocalMindProbe.getRemembered !== 'function') return null;
@@ -103,6 +118,11 @@
     return mind;
   }
 
+  // v-tree-gathering-api-keys-v0: is this listener a cloud chair?
+  function isCloudMind(mind) {
+    return !!(mind && /^cloud:/i.test(String(mind.url || '')));
+  }
+
   function forbiddenKey(key) {
     var k = String(key || '');
     if (!k) return false;
@@ -333,6 +353,13 @@
     return text;
   }
 
+  // v-tree-gathering-api-keys-v0: what a cloud chair may hear. Words yes; file bytes and names stay here.
+  function cloudSafeContent(m) {
+    var text = String((m && m.text) || '');
+    if (m && m.file) text += (text ? '\n\n' : '') + '[a file stayed on this machine]';
+    return text;
+  }
+
   function talkUrls(mind) {
     var url = String((mind && mind.url) || '').replace(/\/+$/, '');
     if (!url) return [];
@@ -451,6 +478,13 @@
   }
 
   function sendToMind(mind, msgs) {
+    // v-tree-gathering-api-keys-v0: a cloud chair speaks through its chosen provider only
+    if (isCloudMind(mind)) {
+      if (window.TreeCloudMinds && typeof window.TreeCloudMinds.send === 'function') {
+        return window.TreeCloudMinds.send(mind, msgs);
+      }
+      return Promise.reject({ reason: 'cloud', cloudText: 'This chair seats a cloud mind, but its helper is not on this page. Nothing was sent.' });
+    }
     var urls = talkUrls(mind);
     if (!urls.length) {
       return Promise.reject({ blocked: true });
@@ -529,6 +563,11 @@
     } else {
       heart.textContent = HEART_NONE;
     }
+    // v-tree-gathering-api-keys-v0: a cloud chair is not on this machine; say so
+    if (isCloudMind(mind) && window.TreeCloudMinds && typeof window.TreeCloudMinds.heartLine === 'function') {
+      heart.textContent = window.TreeCloudMinds.heartLine(mind);
+    }
+    if (mind && mind.hush) heart.textContent = HEART_HUSH;
     root.appendChild(heart);
 
     var later = el('p', 'thread-later', HEART_LATER);
@@ -702,15 +741,24 @@
 
     function talkNow(nowMind) {
       if (busy) return;
+      if (nowMind && nowMind.hush) {
+        messages.push({ role: 'garden', text: HEART_HUSH + ' Nothing was sent.' });
+        saveHistory();
+        renderMessages(list);
+        return;
+      }
       busy = true;
       sendBtn.disabled = true;
       setStatus('Waiting for the mind at home…', false);
+      // v-tree-gathering-api-keys-v0: name the provider; file bytes never leave for a cloud chair
+      var cloudNow = isCloudMind(nowMind);
+      if (cloudNow && window.TreeCloudMinds) setStatus('Waiting for ' + window.TreeCloudMinds.providerName(nowMind) + '\u2026', false);
 
       var payload = [{ role: 'system', content: JUST_TALK }].concat(
         messages
           .filter(function (m) { return m.role === 'human' || m.role === 'mind'; })
           .map(function (m) {
-            return { role: m.role === 'mind' ? 'assistant' : 'user', content: messageContent(m) };
+            return { role: m.role === 'mind' ? 'assistant' : 'user', content: cloudNow ? cloudSafeContent(m) : messageContent(m) };
           })
       );