@jossuealcala/madre 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +76 -1
- package/CHANGELOG.md +76 -1
- package/README.en.md +11 -15
- package/README.md +3 -3
- package/bin/madre.cjs +40 -0
- package/bin/madre.mjs +5 -0
- package/docs/REFERENCE.md +29 -3
- package/docs/SDK.md +4 -2
- package/docs/en/REFERENCE.md +29 -3
- package/docs/en/SDK.md +4 -2
- package/docs/training/README.md +28 -1
- package/docs/training/train.sh +5 -1
- package/package.json +3 -3
- package/public/app.js +1635 -159
- package/public/attribution.js +44 -1
- package/public/brands.js +20 -5
- package/public/code-tools.js +143 -0
- package/public/editor.js +20 -4
- package/public/es.js +395 -26
- package/public/hardware.js +50 -0
- package/public/index.html +5 -1
- package/public/macroverso.js +187 -96
- package/public/market-art.js +161 -0
- package/public/market-view.js +529 -0
- package/public/market.js +189 -0
- package/public/neural.js +110 -0
- package/public/nostromo-engine.js +412 -47
- package/public/power.js +100 -0
- package/public/resay.js +5 -0
- package/public/salas-mac.css +105 -14
- package/public/salas-mac.html +9 -1
- package/public/salas-mac.js +350 -31
- package/public/styles.css +844 -61
- package/public/tour.js +48 -6
- package/public/troubleshooting.js +6 -1
- package/public/wallpaper.js +275 -0
- package/src/adapters/cursor.mjs +96 -0
- package/src/adapters/gemini.mjs +10 -3
- package/src/adapters/madre.mjs +2 -1
- package/src/auth-probe.mjs +143 -12
- package/src/bridge/policy.mjs +1 -1
- package/src/capabilities.mjs +9 -0
- package/src/checkpoint.mjs +16 -1
- package/src/credentials.mjs +3 -0
- package/src/dataset.mjs +76 -4
- package/src/enlaces.mjs +208 -2
- package/src/extensions.mjs +11 -3
- package/src/i18n.mjs +16 -0
- package/src/looks.mjs +125 -2
- package/src/market-key.mjs +8 -0
- package/src/market-remote.mjs +239 -0
- package/src/models.mjs +4 -0
- package/src/module-scope.mjs +68 -0
- package/src/modules/ahp.mjs +102 -7
- package/src/modules/bishop-exam.mjs +122 -0
- package/src/modules/bishop.mjs +71 -12
- package/src/modules/enlaces.mjs +87 -6
- package/src/modules/image-studio.mjs +10 -1
- package/src/modules/intents.mjs +3 -1
- package/src/modules/local-models.mjs +124 -0
- package/src/modules/ollama.mjs +12 -3
- package/src/modules/playwright.mjs +73 -15
- package/src/modules/sdk.mjs +61 -9
- package/src/npm-launch.mjs +18 -0
- package/src/ollama.mjs +6 -5
- package/src/outbound.mjs +9 -1
- package/src/ports.mjs +8 -8
- package/src/room/control.mjs +1 -1
- package/src/room/prompt.mjs +13 -2
- package/src/room.mjs +14 -4
- package/src/router.mjs +1 -1
- package/src/runs.mjs +4 -1
- package/src/runtime-detection.mjs +9 -0
- package/src/salas.mjs +26 -0
- package/src/server-sentinel.mjs +7 -3
- package/src/server.mjs +617 -48
- package/src/setup.mjs +1 -1
package/public/salas-mac.js
CHANGED
|
@@ -12,11 +12,17 @@ import { brandOf, monogram } from './brands.js';
|
|
|
12
12
|
import { startReviewAsk } from './review-ask.js';
|
|
13
13
|
import { startTour } from './tour.js';
|
|
14
14
|
import { dropStickyHover } from './touch.js';
|
|
15
|
-
import { drawAttribution, drawLegal } from './attribution.js';
|
|
15
|
+
import { drawAttribution, drawColophonKeys, drawLegal } from './attribution.js';
|
|
16
16
|
|
|
17
17
|
// Where nothing can hover (a tablet), a tapped key does not stay lit (public/touch.js).
|
|
18
18
|
dropStickyHover();
|
|
19
19
|
import { creep, finishLoader, makeLoader, setLoader, showCurtain } from './loader.js';
|
|
20
|
+
import { labelPowerKey, powerKey } from './power.js';
|
|
21
|
+
import { drawHardware } from './hardware.js';
|
|
22
|
+
import { CUSTOM_WALL, addPicture, customTile, customTools, customWall, paintCustomWall, rememberedWall } from './wallpaper.js';
|
|
23
|
+
import { neuralChosen, rememberNeural } from './neural.js';
|
|
24
|
+
import { openMarket } from './market-view.js';
|
|
25
|
+
import { loadMarket } from './market.js';
|
|
20
26
|
|
|
21
27
|
const $ = (id) => document.getElementById(id);
|
|
22
28
|
const root = document.documentElement;
|
|
@@ -66,8 +72,8 @@ const ACCENTS = ['madre', 'mountain', 'caribbean', 'phantom', 'malbec', 'dijon',
|
|
|
66
72
|
const ACCENT_WORDS = { madre: () => t('Madre green'), mountain: () => t('Mountain green'), caribbean: () => t('Caribbean blue'), phantom: () => t('Phantom purple'), malbec: () => t('Malbec red'), dijon: () => t('Dijon yellow'), pacific: () => t('Pacific blue'), love: () => t('Love pink'), sierra: () => t('Sierra blue'), pitahaya: () => 'Pitahaya', papaya: () => 'Papaya', agave: () => 'Agave', dinamo: () => t('Dynamo black'), marina: () => t('Marina blue'), pulso: () => t('Pulse green'), leal: () => t('Loyal orange'), dulce: () => t('Sweet yellow') };
|
|
67
73
|
// The wallpapers are the room's (app.js, LOOKS.wallpaper; styles.css, --wp-*): the same images, the
|
|
68
74
|
// same veil, the same «mesh» and blur the room's PERSONALIZE sets, read from the same keys.
|
|
69
|
-
const WALLPAPERS = ['default', 'jarrito', 'canela', 'paradise', 'ventura', 'magna', 'biznaga', 'oceano', 'quila', 'xico', 'tropical', 'tradicion', 'urbania'];
|
|
70
|
-
const WALLPAPER_WORDS = { default: () => t('Default'), jarrito: () => 'Jarrito', canela: () => 'Canela', paradise: () => t('Paradise'), ventura: () => 'Ventura', magna: () => 'Magna', biznaga: () => 'Biznaga', oceano: () => 'Oceano', quila: () => 'Quila', xico: () => 'Xico', tropical: () => 'Tropical', tradicion: () => 'Tradicion', urbania: () => 'Urbania' };
|
|
75
|
+
const WALLPAPERS = ['default', 'jarrito', 'canela', 'paradise', 'ventura', 'magna', 'biznaga', 'oceano', 'quila', 'xico', 'tropical', 'tradicion', 'urbania', CUSTOM_WALL];
|
|
76
|
+
const WALLPAPER_WORDS = { default: () => t('Default'), jarrito: () => 'Jarrito', canela: () => 'Canela', paradise: () => t('Paradise'), ventura: () => 'Ventura', magna: () => 'Magna', biznaga: () => 'Biznaga', oceano: () => 'Oceano', quila: () => 'Quila', xico: () => 'Xico', tropical: () => 'Tropical', tradicion: () => 'Tradicion', urbania: () => 'Urbania', custom: () => t('Your picture') };
|
|
71
77
|
// The conversation's own looks (app.js, LOOKS.bubbles and LOOKS.text): the lobby has no
|
|
72
78
|
// conversation, so here they are only chosen, for the room served at this address.
|
|
73
79
|
const BUBBLES = ['comfortable', 'large', 'compact', 'fitted'];
|
|
@@ -85,7 +91,7 @@ const HOME_LOOKS = {
|
|
|
85
91
|
text: { key: 'pulse.home.text', values: ['comfortable', 'larger', 'smaller'], words: { comfortable: () => t('Comfortable'), larger: () => t('Larger'), smaller: () => t('Smaller') } },
|
|
86
92
|
order: { key: 'pulse.home.order', values: ['attention', 'name', 'recent', 'mine'], words: { attention: () => t('Attention'), name: () => t('Name'), recent: () => t('Recent'), mine: () => t('Your order') } },
|
|
87
93
|
};
|
|
88
|
-
const LOOK_KEYS = ['pulse.theme', 'pulse.accent', 'pulse.wallpaper', 'pulse.wallpaper.style', 'pulse.wallpaper.veil', 'pulse.wallpaper.blur', 'pulse.bubbles', 'pulse.text', ...Object.values(HOME_LOOKS).map((one) => one.key)];
|
|
94
|
+
const LOOK_KEYS = ['pulse.theme', 'pulse.accent', 'pulse.wallpaper', 'pulse.wallpaper.style', 'pulse.wallpaper.veil', 'pulse.wallpaper.blur', 'pulse.bubbles', 'pulse.text', 'pulse.neural', ...Object.values(HOME_LOOKS).map((one) => one.key)];
|
|
89
95
|
function readLooks() {
|
|
90
96
|
let theme = 'auto';
|
|
91
97
|
let accent = null;
|
|
@@ -104,7 +110,8 @@ function readLooks() {
|
|
|
104
110
|
return {
|
|
105
111
|
theme: ['auto', 'light', 'dark'].includes(theme) ? theme : 'auto',
|
|
106
112
|
accent: ACCENTS.includes(accent) ? accent : 'madre',
|
|
107
|
-
|
|
113
|
+
// The person's own picture only while it is here (public/wallpaper.js); taken away, the sky.
|
|
114
|
+
wallpaper: WALLPAPERS.includes(wallpaper) && (wallpaper !== CUSTOM_WALL || customWall()) ? wallpaper : 'default',
|
|
108
115
|
style: style === 'mesh' ? 'mesh' : 'veil',
|
|
109
116
|
veil: number(veil, 0, 140, 100),
|
|
110
117
|
blur: number(blur, 0, 24, 0),
|
|
@@ -135,6 +142,8 @@ function applyLooks() {
|
|
|
135
142
|
if (looks.home.cards !== 'full') root.dataset.homeCards = looks.home.cards; else delete root.dataset.homeCards;
|
|
136
143
|
if (looks.home.text !== 'comfortable') root.dataset.homeText = looks.home.text; else delete root.dataset.homeText;
|
|
137
144
|
}
|
|
145
|
+
// The person's picture as this browser last knew it, before the first look is painted.
|
|
146
|
+
paintCustomWall(rememberedWall());
|
|
138
147
|
applyLooks();
|
|
139
148
|
function saveLooks({ theme, accent, wallpaper, style, bubbles, text }) {
|
|
140
149
|
// The first value of each look is the room as it always was, kept as no choice at all (app.js, saveLook).
|
|
@@ -154,6 +163,7 @@ function saveLooks({ theme, accent, wallpaper, style, bubbles, text }) {
|
|
|
154
163
|
function saveHome(name, value) {
|
|
155
164
|
const spec = HOME_LOOKS[name];
|
|
156
165
|
try { if (value === spec.values[0]) localStorage.removeItem(spec.key); else localStorage.setItem(spec.key, value); } catch { /* the choice lasts as long as the page */ }
|
|
166
|
+
pushHomeLayout();
|
|
157
167
|
applyLooks();
|
|
158
168
|
if (name === 'order' && typeof draw === 'function') draw();
|
|
159
169
|
if (name === 'order' && typeof paintOrderKey === 'function') paintOrderKey();
|
|
@@ -172,7 +182,7 @@ window.matchMedia?.('(prefers-color-scheme: light)').addEventListener?.('change'
|
|
|
172
182
|
// app.js's pullLooks; Home's own card choices (HOME_LOOKS) stay here.
|
|
173
183
|
// The wallpaper is each page's own (2026-10-13): Home keeps the one chosen in Home, each project
|
|
174
184
|
// the one chosen in it (src/looks.mjs, `rooms`). The rest of the looks are shared.
|
|
175
|
-
const SHARED_LOOKS = ['pulse.theme', 'pulse.accent', 'pulse.wallpaper.style', 'pulse.wallpaper.veil', 'pulse.wallpaper.blur', 'pulse.bubbles', 'pulse.text'];
|
|
185
|
+
const SHARED_LOOKS = ['pulse.theme', 'pulse.accent', 'pulse.wallpaper.style', 'pulse.wallpaper.veil', 'pulse.wallpaper.blur', 'pulse.bubbles', 'pulse.text', 'pulse.neural'];
|
|
176
186
|
// Before a page had its own, every page wore the shared one: that is where a page starts.
|
|
177
187
|
const wallpaperOf = (answer) => answer.own?.wallpaper ?? answer.looks?.['pulse.wallpaper'] ?? null;
|
|
178
188
|
function saveOwnWallpaper(value) {
|
|
@@ -187,15 +197,49 @@ function pushLooks() {
|
|
|
187
197
|
void fetch('/api/looks', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(patch) }).catch(() => null);
|
|
188
198
|
}, 250);
|
|
189
199
|
}
|
|
200
|
+
// How Home is arranged, the same from every port it is shown on and after MADRE comes back
|
|
201
|
+
// (src/looks.mjs, home.json): written when the person changes it, read when Home opens or comes
|
|
202
|
+
// back into view. The browser's copy stays, so Home paints as it was before it hears from there.
|
|
203
|
+
const HOME_LAYOUT_KEYS = ['pulse.home.layout', 'pulse.home.projects', 'pulse.lobbyBar', 'pulse.home.cards', 'pulse.home.text', 'pulse.home.order'];
|
|
204
|
+
let homePush = 0;
|
|
205
|
+
function pushHomeLayout() {
|
|
206
|
+
clearTimeout(homePush);
|
|
207
|
+
homePush = setTimeout(() => {
|
|
208
|
+
const patch = {};
|
|
209
|
+
try { for (const key of HOME_LAYOUT_KEYS) patch[key] = localStorage.getItem(key); } catch { return; }
|
|
210
|
+
void fetch('/api/home-layout', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(patch) }).catch(() => null);
|
|
211
|
+
}, 250);
|
|
212
|
+
}
|
|
213
|
+
async function pullHomeLayout() {
|
|
214
|
+
let answer = null;
|
|
215
|
+
try { answer = await fetch('/api/home-layout', { cache: 'no-store' }).then((response) => (response.ok ? response.json() : null)); } catch { return false; }
|
|
216
|
+
if (!answer?.home) return false;
|
|
217
|
+
try {
|
|
218
|
+
// Nothing kept yet: the arrangement this browser had becomes the one every page shows.
|
|
219
|
+
if (!answer.saved) { if (HOME_LAYOUT_KEYS.some((key) => localStorage.getItem(key) !== null)) pushHomeLayout(); return false; }
|
|
220
|
+
let changed = false;
|
|
221
|
+
for (const key of HOME_LAYOUT_KEYS) {
|
|
222
|
+
const value = answer.home[key] ?? null;
|
|
223
|
+
if (localStorage.getItem(key) === value) continue;
|
|
224
|
+
if (value === null) localStorage.removeItem(key); else localStorage.setItem(key, value);
|
|
225
|
+
changed = true;
|
|
226
|
+
}
|
|
227
|
+
return changed;
|
|
228
|
+
} catch { return false; }
|
|
229
|
+
}
|
|
190
230
|
async function pullLooks() {
|
|
191
231
|
let answer = null;
|
|
192
232
|
try { answer = await fetch('/api/looks', { cache: 'no-store' }).then((response) => (response.ok ? response.json() : null)); } catch { return; }
|
|
193
233
|
if (!answer?.looks) return;
|
|
234
|
+
// The person's own picture, kept once for every page (public/wallpaper.js): painted as it is now.
|
|
235
|
+
const before = customWall()?.v ?? null;
|
|
236
|
+
paintCustomWall(answer.custom ?? null);
|
|
194
237
|
try {
|
|
195
238
|
// Nothing kept yet: the first page that has a choice of its own hands it to the others.
|
|
196
239
|
if (!answer.saved) { if (SHARED_LOOKS.some((key) => localStorage.getItem(key) !== null)) pushLooks(); return; }
|
|
197
|
-
let changed =
|
|
198
|
-
|
|
240
|
+
let changed = before !== (answer.custom?.v ?? null);
|
|
241
|
+
let wall = wallpaperOf(answer);
|
|
242
|
+
if (wall === CUSTOM_WALL && !answer.custom) wall = null;
|
|
199
243
|
for (const [key, value] of [...SHARED_LOOKS.map((one) => [one, answer.looks[one] ?? null]), ['pulse.wallpaper', wall === 'default' ? null : wall]]) {
|
|
200
244
|
if (localStorage.getItem(key) === value) continue;
|
|
201
245
|
if (value === null) localStorage.removeItem(key); else localStorage.setItem(key, value);
|
|
@@ -286,6 +330,28 @@ function button(className, words, onClick) {
|
|
|
286
330
|
return node;
|
|
287
331
|
}
|
|
288
332
|
|
|
333
|
+
// The other way in for a connection: what the room does, typed in a terminal. Folded, so whoever
|
|
334
|
+
// has never opened one is never asked to; a comment line is said, the rest is what gets copied.
|
|
335
|
+
// The panel repaints every few seconds: which folds are open is kept here, so one never closes on
|
|
336
|
+
// the person reading it. So is which agent is choosing the account to sign in again with.
|
|
337
|
+
const openWays = new Set();
|
|
338
|
+
const choosingAccount = new Set();
|
|
339
|
+
function terminalWay(lines, key) {
|
|
340
|
+
const fold = el('details', 'sm-terminal-way');
|
|
341
|
+
fold.open = openWays.has(key);
|
|
342
|
+
fold.addEventListener('toggle', () => { if (fold.open) openWays.add(key); else openWays.delete(key); });
|
|
343
|
+
fold.append(el('summary', null, t('Prefer your terminal?')));
|
|
344
|
+
const block = el('div', 'sm-terminal-lines');
|
|
345
|
+
for (const line of lines) block.append(line.startsWith('#') ? el('small', 'sm-agent-note', line.slice(1).trim()) : el('code', 'sm-agent-command', line));
|
|
346
|
+
const runnable = lines.filter((line) => !line.startsWith('#'));
|
|
347
|
+
const copy = button('sm-btn small', t('Copy'), async () => {
|
|
348
|
+
try { await navigator.clipboard.writeText(runnable.join('\n')); copy.textContent = t('Copied'); setTimeout(() => { copy.textContent = t('Copy'); }, 1500); }
|
|
349
|
+
catch { copy.textContent = t('Select it'); }
|
|
350
|
+
});
|
|
351
|
+
fold.append(block, copy);
|
|
352
|
+
return fold;
|
|
353
|
+
}
|
|
354
|
+
|
|
289
355
|
function card(room) {
|
|
290
356
|
const glance = room.glance ?? {};
|
|
291
357
|
const state = stateOf(room);
|
|
@@ -810,6 +876,9 @@ function linkRow(enlace) {
|
|
|
810
876
|
if (enlace.orders?.allowed) what.push(t('orders up to #{n}', { n: enlace.orders.maxMode }));
|
|
811
877
|
// D-S13: a link BISHOP signed on its own says so, so it is never mistaken for one you signed.
|
|
812
878
|
if (enlace.signed?.by === 'bishop') what.push(t('signed by the Coordinator'));
|
|
879
|
+
if (enlace.train) what.push(t('the Coordinator learns from it'));
|
|
880
|
+
if (enlace.state) what.push(t('shares its state'));
|
|
881
|
+
if (enlace.deliver) what.push(t('receives delivered notes'));
|
|
813
882
|
node.append(el('p', 'what', what.join(' · ')));
|
|
814
883
|
const traffic = enlace.traffic ?? {};
|
|
815
884
|
// S-021: the last reading says who read, as the room that asked said it.
|
|
@@ -828,6 +897,36 @@ function linkRow(enlace) {
|
|
|
828
897
|
});
|
|
829
898
|
recall.append(recallBox, document.createTextNode(t('Always include (up to 3 notes)')));
|
|
830
899
|
node.append(recall);
|
|
900
|
+
// D-S15: on a link the Coordinator reads, whether it also learns from that project.
|
|
901
|
+
if (reader === data.salas) {
|
|
902
|
+
const learn = el('label', 'row sm-recall');
|
|
903
|
+
const learnBox = el('input'); learnBox.type = 'checkbox'; learnBox.checked = Boolean(enlace.train);
|
|
904
|
+
learnBox.addEventListener('change', async () => {
|
|
905
|
+
learnBox.disabled = true;
|
|
906
|
+
const answer = await fetch('/api/salas/enlaces/train', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: enlace.id, train: learnBox.checked }) })
|
|
907
|
+
.then((response) => response.ok).catch(() => false);
|
|
908
|
+
if (!answer) learnBox.checked = !learnBox.checked;
|
|
909
|
+
learnBox.disabled = false;
|
|
910
|
+
await refresh();
|
|
911
|
+
});
|
|
912
|
+
learn.append(learnBox, document.createTextNode(t('The Coordinator learns from it')));
|
|
913
|
+
node.append(learn);
|
|
914
|
+
}
|
|
915
|
+
// D-S16 and D-S17: what else the link allows, ticked or cleared here by you.
|
|
916
|
+
for (const [field, label] of [['state', t('It shares its state')], ['deliver', t('It receives delivered notes')]]) {
|
|
917
|
+
const allow = el('label', 'row sm-recall');
|
|
918
|
+
const box = el('input'); box.type = 'checkbox'; box.checked = Boolean(enlace[field]);
|
|
919
|
+
box.addEventListener('change', async () => {
|
|
920
|
+
box.disabled = true;
|
|
921
|
+
const answer = await fetch('/api/salas/enlaces/allow', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: enlace.id, field, on: box.checked }) })
|
|
922
|
+
.then((response) => response.ok).catch(() => false);
|
|
923
|
+
if (!answer) box.checked = !box.checked;
|
|
924
|
+
box.disabled = false;
|
|
925
|
+
await refresh();
|
|
926
|
+
});
|
|
927
|
+
allow.append(box, document.createTextNode(label));
|
|
928
|
+
node.append(allow);
|
|
929
|
+
}
|
|
831
930
|
const actions = el('div', 'sm-actions');
|
|
832
931
|
if (revoking.has(enlace.id)) actions.append(el('span', 'sm-ask', t('Nothing crosses from then on.')), button('sm-btn danger', t('Revoke it'), () => revokeLink(enlace.id)), button('sm-btn ghost', t('Cancel'), () => { revoking.delete(enlace.id); draw(); }));
|
|
833
932
|
else actions.append(button('sm-btn ghost', t('Revoke'), () => { revoking.add(enlace.id); draw(); }));
|
|
@@ -1308,6 +1407,7 @@ function saveLayout() {
|
|
|
1308
1407
|
if (JSON.stringify(layout) === JSON.stringify(DEFAULT_LAYOUT)) localStorage.removeItem(LAYOUT_KEY);
|
|
1309
1408
|
else localStorage.setItem(LAYOUT_KEY, JSON.stringify(layout));
|
|
1310
1409
|
} catch { /* the arrangement lasts as long as the page */ }
|
|
1410
|
+
pushHomeLayout();
|
|
1311
1411
|
applyLayout();
|
|
1312
1412
|
}
|
|
1313
1413
|
const placed = (id) => layout.order.includes(id);
|
|
@@ -1608,7 +1708,10 @@ async function openHomeAdd() {
|
|
|
1608
1708
|
const list = el('div', 'sm-add-list');
|
|
1609
1709
|
body.append(list);
|
|
1610
1710
|
homeAddSheet.append(body);
|
|
1611
|
-
|
|
1711
|
+
// The key's squares stay filled while the sheet it opened is there.
|
|
1712
|
+
const key = document.getElementById('sm-add-widgets');
|
|
1713
|
+
key?.classList.add('filled');
|
|
1714
|
+
homeAddSheet.addEventListener('close', () => { homeAddSheet?.remove(); homeAddSheet = null; key?.classList.remove('filled'); });
|
|
1612
1715
|
document.body.append(homeAddSheet);
|
|
1613
1716
|
homeAddSheet.showModal();
|
|
1614
1717
|
void homeTour.hint('home-add', list, { inside: true, title: t('Add to Home'), text: t('The tiles you took off, and the widgets. A widget is offered only when something on this Mac feeds it; right-click any tile to move it, resize it or take it off.') });
|
|
@@ -1794,6 +1897,10 @@ function makeWidget(id) {
|
|
|
1794
1897
|
};
|
|
1795
1898
|
node.start = () => { if (timer) return; void read(); timer = setInterval(() => void read(), 20000); };
|
|
1796
1899
|
node.stop = () => { clearInterval(timer); timer = 0; };
|
|
1900
|
+
// Said again in the language Home settled on: a widget is made once, before the first answer
|
|
1901
|
+
// tells Home which language MADRE speaks (a new MADRE starts in Spanish and may adopt English),
|
|
1902
|
+
// and its title stayed in the first one (2026-10-17).
|
|
1903
|
+
node.say = () => { head.firstChild.textContent = spec.name(); if (timer) void read(); };
|
|
1797
1904
|
return node;
|
|
1798
1905
|
}
|
|
1799
1906
|
applyLayout();
|
|
@@ -1837,6 +1944,8 @@ homeTour.firstOpen('home-links', ui.bar.links, {
|
|
|
1837
1944
|
text: () => t('Which projects can look up what another learned, and the contract each one signed. Nothing is shared without your signature, and you can cancel any connection here.'),
|
|
1838
1945
|
});
|
|
1839
1946
|
homeTour.firstOpen('home-map', ui.macro, {
|
|
1947
|
+
// Under the map's own head, never beside Home's key, which the map covers.
|
|
1948
|
+
opened: () => document.querySelector('.mv .mv-head .mother-title'),
|
|
1840
1949
|
title: () => t('Project map'),
|
|
1841
1950
|
text: () => t('Your projects as a map. Drag one onto another to connect them, click one to go to it, and press Esc to come back.'),
|
|
1842
1951
|
after: 900,
|
|
@@ -1934,7 +2043,7 @@ function openLinkSheet({ reader = null, source = null } = {}) {
|
|
|
1934
2043
|
const linkedPair = (x, y) => (x && y ? (data.enlaces ?? []).find((one) => [one.rooms.a, one.rooms.b].sort().join('\n') === [x, y].sort().join('\n')) ?? null : null);
|
|
1935
2044
|
const pick = {
|
|
1936
2045
|
reader: reader ?? (source !== data.salas && rooms.some((room) => room.id === data.salas) ? data.salas : rooms.find((room) => room.id !== source)?.id),
|
|
1937
|
-
source: null, both: false, kinds: new Set(['decision', 'fact']), days: 7, recall: false,
|
|
2046
|
+
source: null, both: false, kinds: new Set(['decision', 'fact']), days: 7, recall: false, train: false, state: false, deliver: false,
|
|
1938
2047
|
orders: false, maxMode: 1, perHour: 6, designation: '',
|
|
1939
2048
|
};
|
|
1940
2049
|
const free = (room) => room.id !== pick.reader && room.id !== data.salas && !linkedPair(room.id, pick.reader);
|
|
@@ -2215,6 +2324,11 @@ function openLinkSheet({ reader = null, source = null } = {}) {
|
|
|
2215
2324
|
ends.append(rail(LINK_DAYS.map(([value, words]) => [value, words()]), pick.days, (value) => { pick.days = value; }, t('It ends')));
|
|
2216
2325
|
box.append(row(t('It ends'), t('A week is enough to try it; signing it again takes a moment.'), ends, { recommended: pick.days === 7 }));
|
|
2217
2326
|
box.append(row(t('Always include'), t('With every question it gets up to 3 related notes without looking for them; each time counts toward the hourly limit. Off, its agents look them up only when they need to.'), toggle(pick.recall, (on) => { pick.recall = on; render(); }, t('Always include'))));
|
|
2327
|
+
// D-S15: only with the Coordinator reading, and only by you.
|
|
2328
|
+
if (coordinator(pick.reader)) box.append(row(t('The Coordinator learns from it'), t("Its notes also go into the training of the Coordinator's local AI, so it knows this project without opening it. Clearing it or revoking takes it out of the next export; a model already trained keeps what it learned until you train it again."), toggle(pick.train, (on) => { pick.train = on; render(); }, t('The Coordinator learns from it'))));
|
|
2329
|
+
// D-S16 and D-S17: by you only, off unless you turn them on.
|
|
2330
|
+
box.append(row(t('It shares its state'), t('Besides its notes, its canonical state as Continuity keeps it: objective, phase, next step, its last handoff, decisions and what is still open. Only if the project has Continuity installed; never its files or its code.'), toggle(pick.state, (on) => { pick.state = on; render(); }, t('It shares its state'))));
|
|
2331
|
+
box.append(row(t('It receives delivered notes'), t('The project that reads can hand it notes. They wait there until you accept them (or an agent of that project in #3 EDIT or above): nothing enters its memory on its own.'), toggle(pick.deliver, (on) => { pick.deliver = on; render(); }, t('It receives delivered notes'))));
|
|
2218
2332
|
page.append(box);
|
|
2219
2333
|
actions.append(next());
|
|
2220
2334
|
} else if (kind === 'orders') {
|
|
@@ -2246,8 +2360,12 @@ function openLinkSheet({ reader = null, source = null } = {}) {
|
|
|
2246
2360
|
said(pick.both ? sayBold('{a} and {b} will look up each other\'s {kinds}.', { a: nameOf(pick.reader), b: nameOf(pick.source), kinds: kindsSaid() }, ['a', 'b']) : sayBold('{reader} will look up the {kinds} of {source}.', { reader: nameOf(pick.reader), source: nameOf(pick.source), kinds: kindsSaid() }, ['reader', 'source']));
|
|
2247
2361
|
said(pick.days ? t('Until {date}.', { date: until() }) : t('With no expiry: it lasts until you revoke it.'));
|
|
2248
2362
|
said(pick.recall ? t('With every question, up to 3 of these notes arrive without being looked for.') : t('Its agents look them up only when they need them.'));
|
|
2363
|
+
if (coordinator(pick.reader)) said(pick.train ? sayBold('The Coordinator learns from {room}: its notes go into its training.', { room: nameOf(pick.source) }) : t('The Coordinator does not learn from it: it only looks it up.'));
|
|
2249
2364
|
if (ordersInto()) said(pick.orders ? sayBold('The Coordinator may send orders into {room}, up to #{n}, {k} an hour.', { room: nameOf(ordersInto()), n: pick.maxMode, k: pick.perHour }, ['room']) : t('No orders: the Coordinator only looks up.'));
|
|
2250
|
-
said(t('
|
|
2365
|
+
if (pick.state) said(pick.both ? t('Both also read each other\'s state, as Continuity keeps it.') : sayBold('{reader} also reads the state of {source}, as Continuity keeps it.', { reader: nameOf(pick.reader), source: nameOf(pick.source) }, ['reader', 'source']));
|
|
2366
|
+
said(pick.deliver
|
|
2367
|
+
? (pick.both ? t('Each can deliver notes to the other; they wait until that project accepts them. Nothing is written directly, and you can revoke it at any moment from Connections.') : sayBold('{reader} can deliver notes to {source}; they wait there until accepted. Nothing is written directly, and you can revoke it at any moment from Connections.', { reader: nameOf(pick.reader), source: nameOf(pick.source) }, ['reader', 'source']))
|
|
2368
|
+
: t('Nothing is written across, and you can revoke it at any moment from Connections.'));
|
|
2251
2369
|
page.append(list);
|
|
2252
2370
|
const warn = cautions();
|
|
2253
2371
|
if (warn.length) { const box = card(); for (const words of warn) box.append(caution(words)); page.append(box); }
|
|
@@ -2259,6 +2377,9 @@ function openLinkSheet({ reader = null, source = null } = {}) {
|
|
|
2259
2377
|
orders: pick.orders && ordersInto() ? { allowed: true, maxMode: pick.maxMode, perHour: pick.perHour } : { allowed: false, maxMode: 0, perHour: pick.perHour },
|
|
2260
2378
|
designation: pick.orders && ordersInto() ? pick.designation.trim() : undefined,
|
|
2261
2379
|
recall: pick.recall,
|
|
2380
|
+
train: pick.train && coordinator(pick.reader),
|
|
2381
|
+
state: pick.state,
|
|
2382
|
+
deliver: pick.deliver,
|
|
2262
2383
|
};
|
|
2263
2384
|
sign.disabled = true;
|
|
2264
2385
|
sign.textContent = t('Signing…');
|
|
@@ -2302,13 +2423,13 @@ async function refresh() {
|
|
|
2302
2423
|
const mine = chosenHere === 'es' || chosenHere === 'en' ? chosenHere : browserLanguage();
|
|
2303
2424
|
if (await post('/api/language', { language: mine }).then((response) => response?.ok !== false).catch(() => false)) answer.language = mine;
|
|
2304
2425
|
}
|
|
2305
|
-
if (answer.language && answer.language !== language) { language = setLanguage(answer.language); root.lang = language; translateStatic(); drawAttention(); drawDecisions(); drawWork(); homeTour.repaint(); }
|
|
2426
|
+
if (answer.language && answer.language !== language) { language = setLanguage(answer.language); root.lang = language; translateStatic(); drawAttention(); drawDecisions(); drawWork(); for (const widget of document.querySelectorAll('.sm-widget')) widget.say?.(); homeTour.repaint(); }
|
|
2306
2427
|
data = answer;
|
|
2307
2428
|
// A project that came up after Home stopped waiting is open: its card no longer says it did not.
|
|
2308
2429
|
for (const room of answer.rooms ?? []) if (room.open) failed.delete(room.id);
|
|
2309
2430
|
ui.foot.textContent = t('Every project runs on this Mac, on its own, with its own memory.');
|
|
2310
2431
|
// The room's corner (app.js, #colophon): the version this Home runs, and NOTICE's own line.
|
|
2311
|
-
if (answer.version) drawAttribution(ui.colophon, answer.version);
|
|
2432
|
+
if (answer.version) { drawAttribution(ui.colophon, answer.version); void drawColophonKeys(ui.colophon); }
|
|
2312
2433
|
draw();
|
|
2313
2434
|
}
|
|
2314
2435
|
|
|
@@ -2492,7 +2613,10 @@ ui.macro.addEventListener('click', async () => {
|
|
|
2492
2613
|
// BISHOP at the centre (D-S12): the macroverse draws its body itself (macroverso.js, bishopOrb).
|
|
2493
2614
|
bishop: data.salas ? { id: data.salas } : null,
|
|
2494
2615
|
words: macroWords(),
|
|
2616
|
+
// Drawn as neurons when the person chose it (public/neural.js), here or in a project.
|
|
2617
|
+
neural: neuralChosen(),
|
|
2495
2618
|
actions: {
|
|
2619
|
+
neural: (on) => { rememberNeural(on); pushLooks(); },
|
|
2496
2620
|
wake: async (room) => { await startRoom(room); },
|
|
2497
2621
|
// Dragged from one universe onto another: the notes of the first would travel to the second.
|
|
2498
2622
|
link: (source, reader) => openLinkSheet({ source: source?.id ?? null, reader: reader?.id ?? null }),
|
|
@@ -2538,7 +2662,8 @@ function drawTopbar() {
|
|
|
2538
2662
|
b.agents.replaceChildren();
|
|
2539
2663
|
for (const agent of (agentsSeen?.agents ?? []).filter((one) => one.id !== 'madre')) {
|
|
2540
2664
|
const state = agentState(agent, agentsSeen);
|
|
2541
|
-
|
|
2665
|
+
// An agent that cannot answer (no session, not installed) looks switched off, as in the room.
|
|
2666
|
+
const key = button(`sm-agent-key ${state.key}${!state.ok && !state.busyNow ? ' off' : ''}`, null, () => openSettings('agents'));
|
|
2542
2667
|
key.append(mark(agent.id, 24));
|
|
2543
2668
|
key.title = `${agent.label ?? agent.id} · ${state.words}`;
|
|
2544
2669
|
key.setAttribute('aria-label', key.title);
|
|
@@ -2614,6 +2739,7 @@ function pinsShown() {
|
|
|
2614
2739
|
function pinsWrite(list) {
|
|
2615
2740
|
pins.saved = [...list];
|
|
2616
2741
|
try { localStorage.setItem(PIN_KEY, JSON.stringify(pins.saved)); } catch { /* this page only */ }
|
|
2742
|
+
pushHomeLayout();
|
|
2617
2743
|
drawTopbar();
|
|
2618
2744
|
settingsSheet?.sync({ detail: true });
|
|
2619
2745
|
}
|
|
@@ -2629,6 +2755,7 @@ function pinRemove(id) { pinsWrite(pinsShown().filter((one) => one !== id)); }
|
|
|
2629
2755
|
function pinsReset() {
|
|
2630
2756
|
pins.saved = null;
|
|
2631
2757
|
try { localStorage.removeItem(PIN_KEY); } catch { /* this page only */ }
|
|
2758
|
+
pushHomeLayout();
|
|
2632
2759
|
drawTopbar();
|
|
2633
2760
|
settingsSheet?.sync({ detail: true });
|
|
2634
2761
|
}
|
|
@@ -3040,6 +3167,47 @@ function pinsIndexAt(x) {
|
|
|
3040
3167
|
window.addEventListener('storage', (event) => { if (event.key === PIN_KEY) { pins.saved = null; if (data) drawPins(); } });
|
|
3041
3168
|
}
|
|
3042
3169
|
|
|
3170
|
+
// THE BAR FITS ITS ROW (2026-10-18). Everything on it keeps its width but the projects, so with the
|
|
3171
|
+
// Market and Modules keys it ran under the right group and left the projects a sliver («+5»), with
|
|
3172
|
+
// no room to drop or move one. Now, when it does not fit, the computer's name steps out first; then
|
|
3173
|
+
// the keys keep only their icons (their name stays in the tooltip); then the agents, as on a narrow
|
|
3174
|
+
// screen. The
|
|
3175
|
+
// projects keep at least BAR_ROOMS of the row, and all of it while the bar is being arranged.
|
|
3176
|
+
const BAR_ROOMS = 220;
|
|
3177
|
+
let barWatch = null;
|
|
3178
|
+
function fitBar() {
|
|
3179
|
+
const bar = document.querySelector('.sm-top > .sm-topbar:first-child');
|
|
3180
|
+
const rooms = ui.bar.open;
|
|
3181
|
+
if (!bar) return;
|
|
3182
|
+
for (const key of bar.querySelectorAll('.sm-barkey')) { const words = key.querySelector('span')?.textContent?.trim(); if (words) { key.title = words; key.setAttribute('aria-label', words); } }
|
|
3183
|
+
bar.classList.remove('is-tight', 'is-tighter', 'is-tightest');
|
|
3184
|
+
// The projects fold the ones that do not fit behind «+n» on their own (pinsFit): asked again at
|
|
3185
|
+
// each step, they are short while some are folded and they have less than BAR_ROOMS.
|
|
3186
|
+
const roomsShort = () => {
|
|
3187
|
+
if (!data || !rooms || rooms.hidden) return false;
|
|
3188
|
+
if (pins.editing) return rooms.scrollWidth > rooms.clientWidth + 1;
|
|
3189
|
+
pinsFit();
|
|
3190
|
+
return pins.spilled.length > 0 && rooms.clientWidth < BAR_ROOMS;
|
|
3191
|
+
};
|
|
3192
|
+
const short = () => bar.scrollWidth > bar.clientWidth + 1 || roomsShort();
|
|
3193
|
+
for (const step of ['is-tight', 'is-tighter', 'is-tightest']) { if (!short()) break; bar.classList.add(step); }
|
|
3194
|
+
if (data && !pins.editing) pinsFit();
|
|
3195
|
+
// What fitting changed on the bar is its own doing, not a change to fit again.
|
|
3196
|
+
barWatch?.takeRecords();
|
|
3197
|
+
}
|
|
3198
|
+
{
|
|
3199
|
+
const bar = document.querySelector('.sm-top > .sm-topbar:first-child');
|
|
3200
|
+
let queued = false;
|
|
3201
|
+
const later = () => { if (queued) return; queued = true; requestAnimationFrame(() => { queued = false; fitBar(); }); };
|
|
3202
|
+
if (bar) {
|
|
3203
|
+
new ResizeObserver(later).observe(bar);
|
|
3204
|
+
// What sits on the bar changes it: rooms pinned or opened, agents, who waits, a newer MADRE.
|
|
3205
|
+
barWatch = new MutationObserver((changes) => { if (changes.some((change) => change.target !== bar)) later(); });
|
|
3206
|
+
barWatch.observe(bar, { childList: true, subtree: true, attributes: true, attributeFilter: ['hidden', 'class'] });
|
|
3207
|
+
}
|
|
3208
|
+
later();
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3043
3211
|
ui.bar.links.addEventListener('click', () => { if ((data?.enlaces ?? []).length) { if (!ui.linksSheet.open) ui.linksSheet.showModal(); } else openLinkSheet(); });
|
|
3044
3212
|
ui.bar.update.addEventListener('click', () => openSettings('release'));
|
|
3045
3213
|
// RELOAD, as a room reloads (app.js, reloadWithGrace): the curtain of Home's own ground with MADRE's
|
|
@@ -3087,17 +3255,19 @@ document.addEventListener('click', (event) => {
|
|
|
3087
3255
|
// and over any open sheet, for a few seconds.
|
|
3088
3256
|
let toastNode = null;
|
|
3089
3257
|
let toastTimer = 0;
|
|
3090
|
-
|
|
3258
|
+
// `action` ({ label, run }) puts one key at its end: the Market's news says «See it» (2026-10-18).
|
|
3259
|
+
function toast(words, tone = 'done', action = null) {
|
|
3091
3260
|
toastNode?.remove();
|
|
3092
3261
|
const node = el('div', 'toast sm-toast');
|
|
3093
3262
|
node.setAttribute('role', 'status');
|
|
3094
3263
|
node.dataset.tone = tone;
|
|
3095
3264
|
node.append(el('i', 'toast-dot'), el('span', 'toast-text', words));
|
|
3265
|
+
if (action) node.append(button('toast-act', action.label, () => { node.remove(); action.run(); }));
|
|
3096
3266
|
document.body.append(node);
|
|
3097
3267
|
if (typeof node.showPopover === 'function') { node.popover = 'manual'; node.showPopover(); }
|
|
3098
3268
|
toastNode = node;
|
|
3099
3269
|
clearTimeout(toastTimer);
|
|
3100
|
-
toastTimer = setTimeout(() => { node.remove(); if (toastNode === node) toastNode = null; }, 5000);
|
|
3270
|
+
toastTimer = setTimeout(() => { node.remove(); if (toastNode === node) toastNode = null; }, action ? 9000 : 5000);
|
|
3101
3271
|
}
|
|
3102
3272
|
|
|
3103
3273
|
// ---- THE LOBBY (2026-10-10). The first screen of MADRE: every project on this Mac, BISHOP above
|
|
@@ -3294,7 +3464,7 @@ function projectPicker({ onAdded = null, onChange = null, withAdd = true } = {})
|
|
|
3294
3464
|
const chosen = new Map(); // path → name
|
|
3295
3465
|
const node = el('div', 'sm-picker');
|
|
3296
3466
|
const pages = foldPages();
|
|
3297
|
-
let source = 'found'; // found · browse
|
|
3467
|
+
let source = 'found'; // found · browse · new
|
|
3298
3468
|
let found = null; // the suggestions, once read
|
|
3299
3469
|
let here = null; // the folder being walked, as /api/salas/folders answered
|
|
3300
3470
|
let query = '';
|
|
@@ -3319,7 +3489,46 @@ function projectPicker({ onAdded = null, onChange = null, withAdd = true } = {})
|
|
|
3319
3489
|
const error = el('p', 'error');
|
|
3320
3490
|
footer.append(pages.pager, count);
|
|
3321
3491
|
if (addButton) footer.append(addButton);
|
|
3322
|
-
|
|
3492
|
+
// A project from nothing (2026-10-17): its name, where the folder goes (the one being walked in
|
|
3493
|
+
// Your folders, else the home folder) and one key. Made, it is added like any chosen folder.
|
|
3494
|
+
const fresh = el('form', 'sm-new-project');
|
|
3495
|
+
fresh.hidden = true;
|
|
3496
|
+
const nameField = el('label', 'sm-new-name');
|
|
3497
|
+
const nameInput = el('input');
|
|
3498
|
+
nameInput.type = 'text';
|
|
3499
|
+
nameInput.autocomplete = 'off';
|
|
3500
|
+
nameInput.spellcheck = false;
|
|
3501
|
+
nameInput.maxLength = 120;
|
|
3502
|
+
nameInput.placeholder = t('my-new-project');
|
|
3503
|
+
nameField.append(el('span', null, t('Project name')), nameInput);
|
|
3504
|
+
const whereLine = el('p', 'sm-new-where');
|
|
3505
|
+
const makeKey = button('sm-btn primary', t('Create project'), () => {});
|
|
3506
|
+
makeKey.type = 'submit';
|
|
3507
|
+
const freshError = el('p', 'error');
|
|
3508
|
+
fresh.append(nameField, whereLine, makeKey, freshError);
|
|
3509
|
+
const parentNow = () => (here?.path && here.path !== here.home ? here.path : here?.home ?? null);
|
|
3510
|
+
const sayWhere = () => {
|
|
3511
|
+
whereLine.replaceChildren(el('span', null, t('The folder is made in')), ' ', el('code', null, parentNow() ? homeWord(parentNow()) : '~'), ' ');
|
|
3512
|
+
const other = button('sm-new-other', t('Choose another place'), () => { touched = true; source = 'browse'; draw(); if (!here) void walk(); });
|
|
3513
|
+
other.type = 'button';
|
|
3514
|
+
whereLine.append(other);
|
|
3515
|
+
};
|
|
3516
|
+
fresh.addEventListener('submit', async (event) => {
|
|
3517
|
+
event.preventDefault();
|
|
3518
|
+
freshError.textContent = '';
|
|
3519
|
+
makeKey.disabled = true;
|
|
3520
|
+
const answer = await post('/api/salas/new', { name: nameInput.value, parent: parentNow() });
|
|
3521
|
+
makeKey.disabled = false;
|
|
3522
|
+
if (!answer.ok) { freshError.textContent = answer.body.error ?? t('It could not be created.'); nameInput.focus(); return; }
|
|
3523
|
+
const added = answer.body.added ?? [];
|
|
3524
|
+
nameInput.value = '';
|
|
3525
|
+
await refresh();
|
|
3526
|
+
if (added[0]) toast(t('{name} was created and added to Home.', { name: added[0].name }));
|
|
3527
|
+
onAdded?.(added);
|
|
3528
|
+
void loadFound();
|
|
3529
|
+
if (here) void walk(here.path === here.home ? null : here.path);
|
|
3530
|
+
});
|
|
3531
|
+
node.append(head, line, pages.grid, fresh, footer, error);
|
|
3323
3532
|
const sync = () => {
|
|
3324
3533
|
count.textContent = chosen.size ? (chosen.size === 1 ? t('1 folder chosen') : t('{n} folders chosen', { n: chosen.size })) : t('Choose one or more folders');
|
|
3325
3534
|
if (addButton) {
|
|
@@ -3336,14 +3545,25 @@ function projectPicker({ onAdded = null, onChange = null, withAdd = true } = {})
|
|
|
3336
3545
|
onToggle: (on) => { if (on) chosen.set(one.path, one.name); else chosen.delete(one.path); sync(); },
|
|
3337
3546
|
});
|
|
3338
3547
|
function draw({ keepPage = false } = {}) {
|
|
3339
|
-
const
|
|
3548
|
+
const options = customSeg([['found', t('Found on this Mac')], ['browse', t('Your folders')], ['new', t('New project')]], source, (value) => {
|
|
3340
3549
|
touched = true;
|
|
3341
3550
|
source = value;
|
|
3342
3551
|
draw();
|
|
3343
|
-
if (value
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
seg
|
|
3552
|
+
if (value !== 'found' && !here) void walk();
|
|
3553
|
+
if (value === 'new') nameInput.focus();
|
|
3554
|
+
});
|
|
3555
|
+
seg.replaceWith(options);
|
|
3556
|
+
seg = options;
|
|
3557
|
+
const making = source === 'new';
|
|
3558
|
+
fresh.hidden = !making;
|
|
3559
|
+
pages.grid.hidden = making;
|
|
3560
|
+
footer.hidden = making;
|
|
3561
|
+
search.hidden = making;
|
|
3562
|
+
if (making) {
|
|
3563
|
+
line.replaceChildren(el('span', 'sm-picker-note', t('An empty folder, made for you, that becomes a project.')));
|
|
3564
|
+
sayWhere();
|
|
3565
|
+
return;
|
|
3566
|
+
}
|
|
3347
3567
|
const noMatch = () => t('No folder matches «{query}».', { query: input.value.trim() });
|
|
3348
3568
|
if (source === 'found') {
|
|
3349
3569
|
line.replaceChildren(el('span', 'sm-picker-note', found ? t('Folders that look like projects, in the places people keep them.') : t('Looking…')));
|
|
@@ -3382,7 +3602,7 @@ function projectPicker({ onAdded = null, onChange = null, withAdd = true } = {})
|
|
|
3382
3602
|
const answer = await fetch(`/api/salas/folders${path ? `?path=${encodeURIComponent(path)}` : ''}`, { cache: 'no-store' }).then((response) => response.json()).catch(() => null);
|
|
3383
3603
|
here = answer ?? { error: t('That folder is not there.') };
|
|
3384
3604
|
if (path !== null) { query = ''; input.value = ''; }
|
|
3385
|
-
if (source
|
|
3605
|
+
if (source !== 'found') draw();
|
|
3386
3606
|
}
|
|
3387
3607
|
async function add() {
|
|
3388
3608
|
error.textContent = '';
|
|
@@ -3438,6 +3658,30 @@ function openAddSheet() {
|
|
|
3438
3658
|
}
|
|
3439
3659
|
ui.add.addEventListener('click', openAddSheet);
|
|
3440
3660
|
|
|
3661
|
+
// THE MARKET and MODULES (2026-10-18), full screen (market-view.js): the store, and what is on in
|
|
3662
|
+
// which project. From Home a module is turned on for every project or only in one; one that writes
|
|
3663
|
+
// into a project's folder opens that project, on its card, where its commands are read first.
|
|
3664
|
+
const openHomeMarket = (tab = 'shop', focus = null) => openMarket({
|
|
3665
|
+
where: 'home',
|
|
3666
|
+
tab,
|
|
3667
|
+
focus,
|
|
3668
|
+
host: {
|
|
3669
|
+
localAi: () => openSettings('local'),
|
|
3670
|
+
toast: (words) => toast(words),
|
|
3671
|
+
changed: () => drawColophonKeys(ui.colophon),
|
|
3672
|
+
openProject: async (projectId, item) => {
|
|
3673
|
+
const answer = await post('/api/salas/start', { room: projectId });
|
|
3674
|
+
if (!answer.ok || !answer.body.port) return false;
|
|
3675
|
+
window.open(`http://127.0.0.1:${answer.body.port}/#modulo=${encodeURIComponent(item.id)}`, '_blank', 'noopener');
|
|
3676
|
+
return true;
|
|
3677
|
+
},
|
|
3678
|
+
},
|
|
3679
|
+
});
|
|
3680
|
+
document.querySelector('#sm-tb-market')?.addEventListener('click', () => void openHomeMarket('shop'));
|
|
3681
|
+
document.querySelector('#sm-tb-modules')?.addEventListener('click', () => void openHomeMarket('mine'));
|
|
3682
|
+
// The Market's catalog from madre.run, and what it updated on its own since, said once.
|
|
3683
|
+
void loadMarket({ toast: (words, action) => toast(words, action ? 'news' : 'done', action), open: (id) => openHomeMarket('shop', id) });
|
|
3684
|
+
|
|
3441
3685
|
// The agents of this computer, with everything a connection needs in one place: which account
|
|
3442
3686
|
// each one signs in with, the doors it has, the box where a pasted key goes, the address a
|
|
3443
3687
|
// sign-in opened and the code that page gives back. Home used to show a name, a lamp and one
|
|
@@ -3446,7 +3690,7 @@ ui.add.addEventListener('click', openAddSheet);
|
|
|
3446
3690
|
// `compact` is the welcome's first step: there, a connected agent is done, and a button to change
|
|
3447
3691
|
// the account it signs in with is a question nobody asked on their first minute.
|
|
3448
3692
|
// Who makes each agent, said under its name as the project's AJUSTES says it.
|
|
3449
|
-
const VENDOR_WORDS = { codex: 'OpenAI', claude: 'Anthropic', gemini: 'Google', opencode: 'OpenCode' };
|
|
3693
|
+
const VENDOR_WORDS = { codex: 'OpenAI', claude: 'Anthropic', gemini: 'Google', opencode: 'OpenCode', cursor: 'Cursor' };
|
|
3450
3694
|
|
|
3451
3695
|
// OpenCode's providers in Home, the same block as the project's AJUSTES › Agents › OpenCode: a line
|
|
3452
3696
|
// per provider with a key or an account and its switch for the chat, one row to add another, and
|
|
@@ -3571,6 +3815,16 @@ function agentsPanel({ onPaint = null, compact = false } = {}) {
|
|
|
3571
3815
|
open.rel = 'noopener';
|
|
3572
3816
|
box.append(open);
|
|
3573
3817
|
}
|
|
3818
|
+
// A device sign-in (GitHub Copilot through OpenCode) shows its code instead of asking for one.
|
|
3819
|
+
if (live.deviceCode) {
|
|
3820
|
+
const said = el('div', 'sm-agent-device');
|
|
3821
|
+
said.append(el('span', null, t('Type this code on that page:')), el('code', null, live.deviceCode));
|
|
3822
|
+
const copy = button('sm-btn small', t('Copy'), async () => {
|
|
3823
|
+
try { await navigator.clipboard.writeText(live?.deviceCode ?? ''); copy.textContent = t('Copied'); } catch { copy.textContent = t('Select it'); }
|
|
3824
|
+
});
|
|
3825
|
+
said.append(copy);
|
|
3826
|
+
box.append(said);
|
|
3827
|
+
}
|
|
3574
3828
|
if (live.asksForCode || live.awaitingCode) {
|
|
3575
3829
|
const form = el('form', 'sm-agent-code');
|
|
3576
3830
|
const input = el('input');
|
|
@@ -3682,10 +3936,19 @@ function agentsPanel({ onPaint = null, compact = false } = {}) {
|
|
|
3682
3936
|
// One button per account the CLI can sign in with: Claude has two, and only the human can
|
|
3683
3937
|
// say which one is paying.
|
|
3684
3938
|
const doorKeys = () => {
|
|
3685
|
-
|
|
3939
|
+
// With a session, every agent offers the same «sign in again» as Codex. A single door
|
|
3940
|
+
// (Gemini's Google account) is that door; two doors (Claude's subscription and Console)
|
|
3941
|
+
// are chosen after the click.
|
|
3942
|
+
const methods = agent.login?.methods ?? [];
|
|
3943
|
+
const choosing = choosingAccount.has(agent.id);
|
|
3944
|
+
const doors = !state.ok ? (methods.length ? methods : [{ id: null, label: null, note: agent.login?.note }])
|
|
3945
|
+
: methods.length > 1 && choosing ? methods
|
|
3946
|
+
: [{ id: methods.length === 1 ? methods[0].id : null, label: null, note: methods.length === 1 ? methods[0].note : agent.login?.note, choose: methods.length > 1 }];
|
|
3686
3947
|
return doors.map((door) => {
|
|
3687
3948
|
const name = door.label ? t(door.label) : state.ok ? t('Sign in again') : t('Sign in');
|
|
3688
|
-
const key = button(`sm-btn ${state.ok ? '' : 'primary'} small`, name, async (event) => {
|
|
3949
|
+
const key = button(`sm-btn ${(state.ok && !choosing) || agent.key?.first ? '' : 'primary'} small`, name, async (event) => {
|
|
3950
|
+
if (door.choose) { choosingAccount.add(agent.id); void paint(); return; }
|
|
3951
|
+
choosingAccount.delete(agent.id);
|
|
3689
3952
|
event.currentTarget.disabled = true;
|
|
3690
3953
|
await post(`/api/agents/${agent.id}/login`, { method: door.id });
|
|
3691
3954
|
void paint();
|
|
@@ -3694,6 +3957,9 @@ function agentsPanel({ onPaint = null, compact = false } = {}) {
|
|
|
3694
3957
|
return key;
|
|
3695
3958
|
});
|
|
3696
3959
|
};
|
|
3960
|
+
// Gemini's key is its way in (agent.key.first): the field before the Google door.
|
|
3961
|
+
const keyFirst = Boolean(agent.detected && agent.key?.first);
|
|
3962
|
+
if (keyFirst) body.append(keyForm(agent));
|
|
3697
3963
|
const acts = el('div', 'sm-agent-acts');
|
|
3698
3964
|
if (!state.busyNow) {
|
|
3699
3965
|
if (!agent.detected && agent.install) acts.append(button('sm-btn primary small', t('Install'), async (event) => { event.currentTarget.disabled = true; await post(`/api/agents/${agent.id}/install`); void paint(); }));
|
|
@@ -3703,9 +3969,12 @@ function agentsPanel({ onPaint = null, compact = false } = {}) {
|
|
|
3703
3969
|
const ask = askBlock(agent);
|
|
3704
3970
|
if (ask) body.append(ask);
|
|
3705
3971
|
// A key, pasted here; OpenCode's are its providers', in the block below.
|
|
3706
|
-
if (agent.detected && agent.key && agent.id !== 'opencode') body.append(keyForm(agent));
|
|
3707
|
-
//
|
|
3708
|
-
|
|
3972
|
+
if (agent.detected && agent.key && agent.id !== 'opencode' && !keyFirst) body.append(keyForm(agent));
|
|
3973
|
+
// The same install or sign-in typed in a terminal, folded for whoever prefers it. Signed in,
|
|
3974
|
+
// OpenCode's providers are added in the block below; its terminal is for a first sign-in.
|
|
3975
|
+
const way = !agent.detected ? agent.install && [agent.install.display, ...(agent.install.alternatives ?? []).map((line) => `# ${t(line)}`)]
|
|
3976
|
+
: agent.login?.terminal && !(agent.id === 'opencode' && state.ok) ? [agent.login.terminal.command, ...(agent.login.terminal.note ? [`# ${t(agent.login.terminal.note)}`] : [])] : null;
|
|
3977
|
+
if (way && !state.busyNow) body.append(terminalWay(way, agent.id));
|
|
3709
3978
|
if (agent.id === 'opencode' && agent.detected) body.append(openCodeProviders.node());
|
|
3710
3979
|
item.append(body);
|
|
3711
3980
|
list.append(item);
|
|
@@ -3735,7 +4004,8 @@ function agentsPanel({ onPaint = null, compact = false } = {}) {
|
|
|
3735
4004
|
// scene of what it is about, moving: the agents come to MADRE and light up as they are connected;
|
|
3736
4005
|
// the folders drop into the lobby; the rooms light up one after another as the beat runs through
|
|
3737
4006
|
// them. Drawn here, from MADRE's own marks; still for whoever asked for less motion.
|
|
3738
|
-
|
|
4007
|
+
// Five since Cursor joined (1.0.3): the four around MADRE, and Cursor above her.
|
|
4008
|
+
const SCENE_AGENTS = [['codex', 17, 30], ['claude', 27, 78], ['cursor', 50, 17], ['gemini', 83, 30], ['opencode', 73, 78]];
|
|
3739
4009
|
const FOLDER_GLYPH = '<svg viewBox="0 0 32 26" aria-hidden="true"><path d="M2 5.5A2.5 2.5 0 0 1 4.5 3h7.2l3 3.2h12.8A2.5 2.5 0 0 1 30 8.7v12.8a2.5 2.5 0 0 1-2.5 2.5h-23A2.5 2.5 0 0 1 2 21.5z" fill="currentColor"/><path d="M2 10h28" stroke="rgba(255,255,255,.35)" stroke-width="1.4"/></svg>';
|
|
3740
4010
|
const BEAT_PATH = 'M0 22H70L78 22L86 6L96 38L104 22H190L198 22L206 6L216 38L224 22H320L328 22L336 6L346 38L354 22H420';
|
|
3741
4011
|
function wizardScene(kind) {
|
|
@@ -4110,6 +4380,15 @@ function fillLook(host) {
|
|
|
4110
4380
|
const papers = el('div', 'custom-papers');
|
|
4111
4381
|
papers.setAttribute('role', 'radiogroup');
|
|
4112
4382
|
for (const value of WALLPAPERS) {
|
|
4383
|
+
// The person's own picture: chosen like any other once it is here; until then, the way to add it.
|
|
4384
|
+
if (value === CUSTOM_WALL) {
|
|
4385
|
+
const mine = customWall();
|
|
4386
|
+
const paper = mine ? button('paper', null, () => saveLooks({ wallpaper: CUSTOM_WALL })) : button('paper', null, () => void addPicture({ busy: paper, chosen: () => saveLooks({ wallpaper: CUSTOM_WALL }), failed: (message) => toast(message, 'look') }));
|
|
4387
|
+
if (mine) { paper.setAttribute('role', 'radio'); paper.setAttribute('aria-checked', String(looks.wallpaper === CUSTOM_WALL)); }
|
|
4388
|
+
customTile(paper, mine);
|
|
4389
|
+
papers.append(paper);
|
|
4390
|
+
continue;
|
|
4391
|
+
}
|
|
4113
4392
|
const paper = button('paper', null, () => saveLooks({ wallpaper: value }));
|
|
4114
4393
|
paper.dataset.wp = value;
|
|
4115
4394
|
paper.setAttribute('role', 'radio');
|
|
@@ -4146,6 +4425,15 @@ function fillLook(host) {
|
|
|
4146
4425
|
return line;
|
|
4147
4426
|
};
|
|
4148
4427
|
tune.append(dial('veil', t('Intensity')), dial('blur', t('Blur')));
|
|
4428
|
+
// Your picture: where the screen falls on it, and changing it or taking it away.
|
|
4429
|
+
if (looks.wallpaper === CUSTOM_WALL && customWall()) {
|
|
4430
|
+
tune.append(customTools(customWall(), {
|
|
4431
|
+
button: (words, click) => sxButton(words, click),
|
|
4432
|
+
changed: (kept, { reframed }) => { if (!reframed) settingsSheet?.paintLooks(); },
|
|
4433
|
+
removed: () => saveLooks({ wallpaper: 'default' }),
|
|
4434
|
+
failed: (message) => toast(message, 'look'),
|
|
4435
|
+
}));
|
|
4436
|
+
}
|
|
4149
4437
|
paperBox.append(tune);
|
|
4150
4438
|
}
|
|
4151
4439
|
sxBlock(page, t('Wallpaper'), photo
|
|
@@ -4217,8 +4505,11 @@ const LOCAL_MODE_SAYS = {
|
|
|
4217
4505
|
manual: () => t('Each project decides in its own Modules › Local AI. New ones start with it.'),
|
|
4218
4506
|
};
|
|
4219
4507
|
let localFailed = false; // the last read got no answer: the sheet says so instead of «Reading…» forever
|
|
4508
|
+
let localHardware = null; // what this computer can run and train (src/modules/local-models.mjs)
|
|
4220
4509
|
async function loadLocal() {
|
|
4221
4510
|
const answer = await fetch('/api/salas/local-ai', { cache: 'no-store' }).then((response) => (response.ok ? response.json() : null)).catch(() => null);
|
|
4511
|
+
const ollama = await fetch('/api/ollama', { cache: 'no-store' }).then((response) => (response.ok ? response.json() : null)).catch(() => null);
|
|
4512
|
+
if (ollama?.recommended?.hardware) localHardware = ollama.recommended.hardware;
|
|
4222
4513
|
if (answer) localSeen = answer;
|
|
4223
4514
|
localFailed = !answer;
|
|
4224
4515
|
settingsSheet?.sync({ detail: true });
|
|
@@ -4266,6 +4557,8 @@ function fillLocal(host) {
|
|
|
4266
4557
|
sxRow(mode, t('Mode'), null, localModeSeg());
|
|
4267
4558
|
if (!localSeen.master) sxRow(mode, t('Local AI is off for every project.'), t('Turn it on in a project, in Modules › Local AI.'));
|
|
4268
4559
|
else if (!localSeen.ollama.running) sxRow(mode, t('Ollama is not running'), t('Projects keep working with their agents meanwhile. Start it from a project, in Modules › Local AI.'));
|
|
4560
|
+
// What this computer can run and train, as a project shows it in Modules › Local AI.
|
|
4561
|
+
if (localHardware) drawHardware(host, localHardware, { el, sxBlock, sxRow, sxChip: (text, tone = '') => el('span', `sx-chip${tone ? ` ${tone}` : ''}`, text) });
|
|
4269
4562
|
const list = sxBlock(host, t('Projects'), localSeen.mode === 'auto' ? t('All of them use it. Choose «One by one» or «Manual» to turn some off.') : t('A switch here is the same one the project shows in its Settings.'));
|
|
4270
4563
|
if (!localSeen.projects.length) { sxRow(list, t('No projects yet'), null); return; }
|
|
4271
4564
|
const pages = Math.ceil(localSeen.projects.length / LOCAL_PER_PAGE);
|
|
@@ -4540,6 +4833,10 @@ function openSettings(section = null) {
|
|
|
4540
4833
|
show(section);
|
|
4541
4834
|
}
|
|
4542
4835
|
ui.gear.addEventListener('click', () => openSettings());
|
|
4836
|
+
// Turning MADRE off (public/power.js), last in the bar, after the settings.
|
|
4837
|
+
const powerButton = powerKey('sm-icon-key bar');
|
|
4838
|
+
powerButton.id = 'sm-power';
|
|
4839
|
+
ui.gear.after(powerButton);
|
|
4543
4840
|
|
|
4544
4841
|
function translateStatic() {
|
|
4545
4842
|
document.title = `MADRE · ${t('Home')}`;
|
|
@@ -4549,6 +4846,8 @@ function translateStatic() {
|
|
|
4549
4846
|
ui.bar.open.setAttribute('aria-label', t('Projects on the bar'));
|
|
4550
4847
|
ui.bar.agents.setAttribute('aria-label', t('Agents'));
|
|
4551
4848
|
ui.bar.links.querySelector('span').textContent = t('Connections');
|
|
4849
|
+
document.querySelector('#sm-tb-market span')?.replaceChildren(t('Market'));
|
|
4850
|
+
document.querySelector('#sm-tb-modules span')?.replaceChildren(t('Modules'));
|
|
4552
4851
|
ui.bar.reload.title = t('Reload the page');
|
|
4553
4852
|
ui.bar.reload.setAttribute('aria-label', t('Reload the page'));
|
|
4554
4853
|
const addKey = document.getElementById('sm-add-widgets');
|
|
@@ -4558,6 +4857,7 @@ function translateStatic() {
|
|
|
4558
4857
|
ui.macro.querySelector('span').textContent = t('Project map');
|
|
4559
4858
|
ui.gear.title = t('Home settings');
|
|
4560
4859
|
ui.gear.setAttribute('aria-label', t('Home settings'));
|
|
4860
|
+
labelPowerKey(powerButton);
|
|
4561
4861
|
ui.search.placeholder = t('Search a project…');
|
|
4562
4862
|
ui.search.setAttribute('aria-label', t('Search a project…'));
|
|
4563
4863
|
ui.filter.setAttribute('aria-label', t('Show'));
|
|
@@ -4588,6 +4888,20 @@ function translateStatic() {
|
|
|
4588
4888
|
translateStatic();
|
|
4589
4889
|
drawWork();
|
|
4590
4890
|
drawDecisions();
|
|
4891
|
+
// The arrangement kept for every port, before the first full drawing, and again whenever Home
|
|
4892
|
+
// comes back into view (another page may have changed it).
|
|
4893
|
+
const homeArranged = async () => {
|
|
4894
|
+
if (!(await pullHomeLayout())) return;
|
|
4895
|
+
layout = readLayout();
|
|
4896
|
+
pins.saved = null;
|
|
4897
|
+
applyLooks();
|
|
4898
|
+
applyLayout();
|
|
4899
|
+
if (data) { draw(); drawTopbar(); }
|
|
4900
|
+
paintOrderKey();
|
|
4901
|
+
settingsSheet?.paintLooks();
|
|
4902
|
+
};
|
|
4903
|
+
await homeArranged();
|
|
4904
|
+
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') void homeArranged(); });
|
|
4591
4905
|
await refresh();
|
|
4592
4906
|
// Arriving from RELOAD (salas-mac.html put the curtain back): Home has read its projects, so the
|
|
4593
4907
|
// bar fills and the curtain clears.
|
|
@@ -4608,6 +4922,11 @@ void loadDecisions();
|
|
|
4608
4922
|
if (location.hash === '#welcome') { history.replaceState(null, '', location.pathname); welcomed = true; openWizard(); }
|
|
4609
4923
|
// Sent here for the Macroverso (the key in BISHOP's room): straight into it, on a large screen.
|
|
4610
4924
|
if (location.hash === '#macroverso' && data && !ui.macro.hidden) { history.replaceState(null, '', location.pathname); ui.macro.click(); }
|
|
4925
|
+
// Sent here for the Market (a project's MODULES, or a module offered in the conversation): straight in,
|
|
4926
|
+
// on that module's page when one is named.
|
|
4927
|
+
// The Market's addresses: #mercado, #mercado=<module>, #modulos.
|
|
4928
|
+
if (/^#mercado(=|$)/.test(location.hash)) openHomeMarket('shop', decodeURIComponent(location.hash.split('=')[1] ?? '') || null);
|
|
4929
|
+
else if (location.hash === '#modulos') openHomeMarket('mine');
|
|
4611
4930
|
timer = setInterval(() => { if (!document.hidden && !busy.size && !pins.dropping && !pins.drag) void refresh(); }, 4000);
|
|
4612
4931
|
document.addEventListener('visibilitychange', () => { if (!document.hidden) { void refresh(); void loadAgents(); void loadWork(); void loadDecisions(); } });
|
|
4613
4932
|
// The decisions are asked again every few minutes (the route itself asks the rooms no oftener).
|