@jossuealcala/madre 1.0.1 → 1.0.3
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/ADDITIONAL-TERMS.md +93 -0
- package/CHANGELOG.en.md +2013 -0
- package/CHANGELOG.md +102 -1
- package/CONTRIBUTING.md +1 -1
- package/LICENSE +661 -202
- package/NOTICE +26 -14
- package/README.en.md +241 -0
- package/README.md +21 -12
- package/TRADEMARKS.en.md +50 -0
- package/TRADEMARKS.md +33 -16
- package/bin/madre.cjs +40 -0
- package/bin/madre.mjs +7 -0
- package/docs/INTERNALS.md +6 -0
- package/docs/REFERENCE.md +40 -5
- package/docs/SDK.md +4 -2
- package/docs/en/INTERNALS.md +103 -0
- package/docs/en/REFERENCE.md +365 -0
- package/docs/en/SDK.md +308 -0
- package/docs/en/training/README.md +59 -0
- package/docs/training/README.md +48 -9
- package/docs/training/train.sh +7 -2
- package/package.json +13 -5
- package/public/abduction.js +2 -0
- package/public/app.js +3154 -539
- package/public/attribution.js +114 -0
- package/public/brands.js +23 -6
- package/public/code-tools.js +143 -0
- package/public/editor.js +22 -4
- package/public/es.js +1004 -416
- package/public/hardware.js +50 -0
- package/public/i18n.js +25 -1
- package/public/index.html +18 -11
- package/public/inquiry.js +2 -0
- package/public/loader.js +2 -0
- package/public/macroverso.js +189 -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 +414 -47
- package/public/power.js +100 -0
- package/public/resay.js +11 -0
- package/public/review-ask.js +2 -0
- package/public/routing.js +23 -1
- package/public/salas-mac.css +119 -18
- package/public/salas-mac.html +14 -2
- package/public/salas-mac.js +358 -56
- package/public/salas.css +2 -0
- package/public/salas.html +1 -0
- package/public/salas.js +4 -2
- package/public/styles.css +1793 -77
- package/public/syntax.js +2 -0
- package/public/touch.js +2 -0
- package/public/tour-chapters.js +2 -0
- package/public/tour.js +84 -5
- package/public/troubleshooting.js +24 -17
- package/public/wallpaper.js +275 -0
- package/src/adapters/claude.mjs +2 -0
- package/src/adapters/codex.mjs +2 -0
- package/src/adapters/cursor.mjs +96 -0
- package/src/adapters/gemini.mjs +13 -4
- package/src/adapters/madre.mjs +5 -2
- package/src/adapters/opencode.mjs +7 -3
- package/src/adapters/process.mjs +2 -0
- package/src/asking.mjs +2 -0
- package/src/auth-probe.mjs +145 -12
- package/src/bridge/devices.mjs +2 -0
- package/src/bridge/handoff.mjs +2 -0
- package/src/bridge/host.mjs +2 -0
- package/src/bridge/network.mjs +2 -0
- package/src/bridge/policy.mjs +3 -1
- package/src/bridge/qr.mjs +2 -0
- package/src/bridge/server.mjs +2 -0
- package/src/capabilities.mjs +11 -0
- package/src/chats.mjs +2 -0
- package/src/checkpoint.mjs +18 -1
- package/src/choices.mjs +2 -0
- package/src/cli-args.mjs +2 -0
- package/src/cold.mjs +2 -0
- package/src/commands.mjs +2 -0
- package/src/config.mjs +2 -0
- package/src/conversation-context.mjs +15 -1
- package/src/credentials.mjs +35 -3
- package/src/dataset.mjs +78 -4
- package/src/directives.mjs +2 -0
- package/src/distiller.mjs +2 -0
- package/src/embeddings.mjs +2 -0
- package/src/enlaces.mjs +214 -6
- package/src/event-store.mjs +2 -0
- package/src/exam.mjs +6 -4
- package/src/extensions.mjs +13 -3
- package/src/eyecat-watch.mjs +2 -0
- package/src/eyecat.mjs +2 -0
- package/src/file-ops.mjs +2 -0
- package/src/files.mjs +32 -0
- package/src/github.mjs +2 -0
- package/src/heartbeat.mjs +2 -0
- package/src/i18n.mjs +31 -1
- package/src/image-studio.mjs +2 -0
- package/src/json-file.mjs +2 -0
- package/src/launch.mjs +2 -0
- package/src/lease.mjs +5 -1
- package/src/lobby-work.mjs +2 -0
- package/src/local-ai.mjs +2 -0
- package/src/looks.mjs +127 -2
- package/src/market-key.mjs +8 -0
- package/src/market-remote.mjs +239 -0
- package/src/maturity.mjs +3 -1
- package/src/mcp/connect.mjs +345 -0
- package/src/mcp/github-server.mjs +2 -0
- package/src/mcp/image-server.mjs +2 -0
- package/src/mcp/memory-server.mjs +2 -0
- package/src/mcp/proxy.mjs +153 -0
- package/src/mcp/smtp-server.mjs +2 -0
- package/src/memory-dashboard.mjs +2 -0
- package/src/memory-tools.mjs +2 -0
- package/src/memory.mjs +2 -0
- package/src/models.mjs +49 -3
- package/src/module-scope.mjs +68 -0
- package/src/modules/ahp.mjs +104 -7
- package/src/modules/ash-policy.mjs +2 -0
- package/src/modules/ash.mjs +3 -1
- package/src/modules/bishop-exam.mjs +122 -0
- package/src/modules/bishop-plan.mjs +5 -3
- package/src/modules/bishop.mjs +84 -23
- package/src/modules/choices.mjs +2 -0
- package/src/modules/correo.mjs +2 -0
- package/src/modules/enlaces.mjs +89 -6
- package/src/modules/git-pulse.mjs +2 -0
- package/src/modules/github.mjs +2 -0
- package/src/modules/helpers.mjs +2 -0
- package/src/modules/image-studio.mjs +13 -2
- package/src/modules/index.mjs +4 -1
- package/src/modules/intents-calibration.mjs +2 -0
- package/src/modules/intents-desk.mjs +4 -2
- package/src/modules/intents-observe.mjs +2 -0
- package/src/modules/intents-policy.mjs +4 -1
- package/src/modules/intents-try.mjs +2 -0
- package/src/modules/intents.mjs +11 -7
- package/src/modules/local-models.mjs +124 -0
- package/src/modules/mcp-connect.mjs +293 -0
- package/src/modules/ollama.mjs +15 -4
- package/src/modules/playwright.mjs +88 -15
- package/src/modules/ripley.mjs +2 -0
- package/src/modules/runs.mjs +5 -3
- package/src/modules/sdk.mjs +63 -9
- package/src/modules/updates.mjs +2 -0
- package/src/mother.mjs +2 -0
- package/src/npm-launch.mjs +18 -0
- package/src/ollama.mjs +8 -5
- package/src/outbound.mjs +16 -6
- package/src/override.mjs +2 -0
- package/src/ports.mjs +6 -2
- package/src/prices.mjs +2 -0
- package/src/privacy.mjs +2 -0
- package/src/quota-monitor.mjs +2 -0
- package/src/quota-sources.mjs +2 -0
- package/src/request-guard.mjs +2 -0
- package/src/review-ask.mjs +2 -0
- package/src/room/archivist.mjs +2 -0
- package/src/room/attachments.mjs +2 -0
- package/src/room/budget.mjs +2 -0
- package/src/room/context.mjs +2 -0
- package/src/room/control.mjs +7 -1
- package/src/room/copilot.mjs +76 -0
- package/src/room/economy.mjs +2 -0
- package/src/room/escalation.mjs +2 -0
- package/src/room/ghost.mjs +2 -0
- package/src/room/guard.mjs +2 -0
- package/src/room/prompt.mjs +25 -5
- package/src/room/returns.mjs +82 -0
- package/src/room/runs.mjs +4 -0
- package/src/room/vectors.mjs +2 -0
- package/src/room.mjs +223 -35
- package/src/router.mjs +3 -1
- package/src/runs.mjs +6 -1
- package/src/runtime-detection.mjs +11 -0
- package/src/salas.mjs +28 -0
- package/src/sentinel-errors.mjs +2 -0
- package/src/server-sentinel.mjs +2 -0
- package/src/server.mjs +757 -81
- package/src/setup.mjs +3 -1
- package/src/smtp.mjs +2 -0
- package/src/tour.mjs +2 -0
- package/src/updates.mjs +2 -0
- package/src/usage-sentinel.mjs +2 -0
- package/src/vault.mjs +2 -0
- package/src/verdict.mjs +3 -1
- package/src/whats-new.mjs +2 -0
package/public/salas-mac.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
// SALAS on the Mac: every room this Mac knows, open or asleep, in one place (docs/MACROVERSO.md,
|
|
2
4
|
// phase 1). The room that serves this page answers for all of them (src/salas.mjs): it lists
|
|
3
5
|
// them, opens one that is asleep, asks one to close, and stops every agent in every room. A room
|
|
@@ -5,15 +7,22 @@
|
|
|
5
7
|
//
|
|
6
8
|
// The phone has its own SALAS (salas.js), served by the bridge; the two read the same rooms.
|
|
7
9
|
|
|
8
|
-
import { setLanguage, t } from './i18n.js';
|
|
10
|
+
import { browserLanguage, setLanguage, t } from './i18n.js';
|
|
9
11
|
import { brandOf, monogram } from './brands.js';
|
|
10
12
|
import { startReviewAsk } from './review-ask.js';
|
|
11
13
|
import { startTour } from './tour.js';
|
|
12
14
|
import { dropStickyHover } from './touch.js';
|
|
15
|
+
import { drawAttribution, drawColophonKeys, drawLegal } from './attribution.js';
|
|
13
16
|
|
|
14
17
|
// Where nothing can hover (a tablet), a tapped key does not stay lit (public/touch.js).
|
|
15
18
|
dropStickyHover();
|
|
16
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';
|
|
17
26
|
|
|
18
27
|
const $ = (id) => document.getElementById(id);
|
|
19
28
|
const root = document.documentElement;
|
|
@@ -34,6 +43,7 @@ const ui = {
|
|
|
34
43
|
attention: { title: $('sm-attention-title'), count: $('sm-attention-count'), list: $('sm-attention-list') },
|
|
35
44
|
};
|
|
36
45
|
let language = 'es';
|
|
46
|
+
let languageAdopted = false;
|
|
37
47
|
let timer = 0;
|
|
38
48
|
let arming = 0;
|
|
39
49
|
let data = null;
|
|
@@ -62,8 +72,8 @@ const ACCENTS = ['madre', 'mountain', 'caribbean', 'phantom', 'malbec', 'dijon',
|
|
|
62
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') };
|
|
63
73
|
// The wallpapers are the room's (app.js, LOOKS.wallpaper; styles.css, --wp-*): the same images, the
|
|
64
74
|
// same veil, the same «mesh» and blur the room's PERSONALIZE sets, read from the same keys.
|
|
65
|
-
const WALLPAPERS = ['default', 'jarrito', 'canela', 'paradise', 'ventura', 'magna', 'biznaga', 'oceano', 'quila', 'xico', 'tropical', 'tradicion', 'urbania'];
|
|
66
|
-
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') };
|
|
67
77
|
// The conversation's own looks (app.js, LOOKS.bubbles and LOOKS.text): the lobby has no
|
|
68
78
|
// conversation, so here they are only chosen, for the room served at this address.
|
|
69
79
|
const BUBBLES = ['comfortable', 'large', 'compact', 'fitted'];
|
|
@@ -81,7 +91,7 @@ const HOME_LOOKS = {
|
|
|
81
91
|
text: { key: 'pulse.home.text', values: ['comfortable', 'larger', 'smaller'], words: { comfortable: () => t('Comfortable'), larger: () => t('Larger'), smaller: () => t('Smaller') } },
|
|
82
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') } },
|
|
83
93
|
};
|
|
84
|
-
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)];
|
|
85
95
|
function readLooks() {
|
|
86
96
|
let theme = 'auto';
|
|
87
97
|
let accent = null;
|
|
@@ -100,7 +110,8 @@ function readLooks() {
|
|
|
100
110
|
return {
|
|
101
111
|
theme: ['auto', 'light', 'dark'].includes(theme) ? theme : 'auto',
|
|
102
112
|
accent: ACCENTS.includes(accent) ? accent : 'madre',
|
|
103
|
-
|
|
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',
|
|
104
115
|
style: style === 'mesh' ? 'mesh' : 'veil',
|
|
105
116
|
veil: number(veil, 0, 140, 100),
|
|
106
117
|
blur: number(blur, 0, 24, 0),
|
|
@@ -131,6 +142,8 @@ function applyLooks() {
|
|
|
131
142
|
if (looks.home.cards !== 'full') root.dataset.homeCards = looks.home.cards; else delete root.dataset.homeCards;
|
|
132
143
|
if (looks.home.text !== 'comfortable') root.dataset.homeText = looks.home.text; else delete root.dataset.homeText;
|
|
133
144
|
}
|
|
145
|
+
// The person's picture as this browser last knew it, before the first look is painted.
|
|
146
|
+
paintCustomWall(rememberedWall());
|
|
134
147
|
applyLooks();
|
|
135
148
|
function saveLooks({ theme, accent, wallpaper, style, bubbles, text }) {
|
|
136
149
|
// The first value of each look is the room as it always was, kept as no choice at all (app.js, saveLook).
|
|
@@ -150,6 +163,7 @@ function saveLooks({ theme, accent, wallpaper, style, bubbles, text }) {
|
|
|
150
163
|
function saveHome(name, value) {
|
|
151
164
|
const spec = HOME_LOOKS[name];
|
|
152
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();
|
|
153
167
|
applyLooks();
|
|
154
168
|
if (name === 'order' && typeof draw === 'function') draw();
|
|
155
169
|
if (name === 'order' && typeof paintOrderKey === 'function') paintOrderKey();
|
|
@@ -168,7 +182,7 @@ window.matchMedia?.('(prefers-color-scheme: light)').addEventListener?.('change'
|
|
|
168
182
|
// app.js's pullLooks; Home's own card choices (HOME_LOOKS) stay here.
|
|
169
183
|
// The wallpaper is each page's own (2026-10-13): Home keeps the one chosen in Home, each project
|
|
170
184
|
// the one chosen in it (src/looks.mjs, `rooms`). The rest of the looks are shared.
|
|
171
|
-
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'];
|
|
172
186
|
// Before a page had its own, every page wore the shared one: that is where a page starts.
|
|
173
187
|
const wallpaperOf = (answer) => answer.own?.wallpaper ?? answer.looks?.['pulse.wallpaper'] ?? null;
|
|
174
188
|
function saveOwnWallpaper(value) {
|
|
@@ -183,15 +197,49 @@ function pushLooks() {
|
|
|
183
197
|
void fetch('/api/looks', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(patch) }).catch(() => null);
|
|
184
198
|
}, 250);
|
|
185
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
|
+
}
|
|
186
230
|
async function pullLooks() {
|
|
187
231
|
let answer = null;
|
|
188
232
|
try { answer = await fetch('/api/looks', { cache: 'no-store' }).then((response) => (response.ok ? response.json() : null)); } catch { return; }
|
|
189
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);
|
|
190
237
|
try {
|
|
191
238
|
// Nothing kept yet: the first page that has a choice of its own hands it to the others.
|
|
192
239
|
if (!answer.saved) { if (SHARED_LOOKS.some((key) => localStorage.getItem(key) !== null)) pushLooks(); return; }
|
|
193
|
-
let changed =
|
|
194
|
-
|
|
240
|
+
let changed = before !== (answer.custom?.v ?? null);
|
|
241
|
+
let wall = wallpaperOf(answer);
|
|
242
|
+
if (wall === CUSTOM_WALL && !answer.custom) wall = null;
|
|
195
243
|
for (const [key, value] of [...SHARED_LOOKS.map((one) => [one, answer.looks[one] ?? null]), ['pulse.wallpaper', wall === 'default' ? null : wall]]) {
|
|
196
244
|
if (localStorage.getItem(key) === value) continue;
|
|
197
245
|
if (value === null) localStorage.removeItem(key); else localStorage.setItem(key, value);
|
|
@@ -230,8 +278,8 @@ const modeName = (n) => [t('INCOGNITO'), t('CHAT ONLY'), t('CREATE'), t('EDIT'),
|
|
|
230
278
|
function askLine(ask) {
|
|
231
279
|
if (ask.kind === 'mode') return t('@{agent} asks for #{n} {label}', { agent: ask.agent, n: ask.mode, label: modeName(ask.mode) });
|
|
232
280
|
if (ask.kind === 'run') return ask.text ? t('@{agent} wants to run {line}', { agent: ask.agent, line: ask.text }) : t('@{agent} wants to run a command', { agent: ask.agent });
|
|
233
|
-
if (ask.kind === 'bishop') return t('The Coordinator proposes #{n} to @{agent}:
|
|
234
|
-
if (ask.kind === 'plan') return ask.text ? t('The Coordinator waits for
|
|
281
|
+
if (ask.kind === 'bishop') return t('The Coordinator proposes #{n} to @{agent}: confirm it to unlock it', { n: ask.mode, agent: ask.agent });
|
|
282
|
+
if (ask.kind === 'plan') return ask.text ? t('The Coordinator waits for you to confirm: {reason}', { reason: ask.text }) : t('The Coordinator waits for you to confirm');
|
|
235
283
|
return ask.text ? t('@{agent} asks you: {question}', { agent: ask.agent, question: ask.text }) : t('@{agent} asks you to choose', { agent: ask.agent });
|
|
236
284
|
}
|
|
237
285
|
// The folder, as a person writes it: the home folder is ~.
|
|
@@ -241,7 +289,7 @@ const addressOf = (room) => (room.here ? '/' : `http://127.0.0.1:${room.open.por
|
|
|
241
289
|
|
|
242
290
|
// The memory kinds, in the colours NOSTROMO gives them (styles.css, --mem-*).
|
|
243
291
|
const KINDS = ['decision', 'fact', 'preference', 'question', 'aberration'];
|
|
244
|
-
const KIND_WORDS = { decision: t('DECISION'), fact: t('FACT'), preference: t('PREFERENCE'), question: t('QUESTION'), aberration: t('
|
|
292
|
+
const KIND_WORDS = { decision: t('DECISION'), fact: t('FACT'), preference: t('PREFERENCE'), question: t('QUESTION'), aberration: t('DOUBTFUL MEMORY') };
|
|
245
293
|
// A kind as a sentence says it: «Decisión», not «DECISIÓN».
|
|
246
294
|
const kindWord = (kind) => { const word = String(KIND_WORDS[kind] ?? kind).toLowerCase(); return word.charAt(0).toUpperCase() + word.slice(1); };
|
|
247
295
|
|
|
@@ -282,6 +330,28 @@ function button(className, words, onClick) {
|
|
|
282
330
|
return node;
|
|
283
331
|
}
|
|
284
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
|
+
|
|
285
355
|
function card(room) {
|
|
286
356
|
const glance = room.glance ?? {};
|
|
287
357
|
const state = stateOf(room);
|
|
@@ -338,7 +408,7 @@ function card(room) {
|
|
|
338
408
|
if (room.open && glance.census) {
|
|
339
409
|
node.append(memoryStrip(glance.census));
|
|
340
410
|
const counts = [conversationsCount(glance.chats ?? 0), memoriesCount(glance.census.memories)];
|
|
341
|
-
if (glance.census.memories) counts.push(t('{n}
|
|
411
|
+
if (glance.census.memories) counts.push(t('{n} in use', { n: glance.census.travelling }));
|
|
342
412
|
node.append(el('p', 'sm-counts', counts.join(' · ')));
|
|
343
413
|
}
|
|
344
414
|
|
|
@@ -563,7 +633,7 @@ function drawMemory() {
|
|
|
563
633
|
const item = el('span', `k-${kind}`, `${kindWord(kind)} · ${sum.kinds[kind]}`);
|
|
564
634
|
legend.append(item);
|
|
565
635
|
}
|
|
566
|
-
ui.memory.body.append(legend, el('p', 'sm-hint', [sum.memories ? t('{n}
|
|
636
|
+
ui.memory.body.append(legend, el('p', 'sm-hint', [sum.memories ? t('{n} in use', { n: sum.travelling }) : null, t('Counted in the open projects.')].filter(Boolean).join(' · ')));
|
|
567
637
|
}
|
|
568
638
|
|
|
569
639
|
// AGENTS: the ones on this computer, with the session each has. Connected once, every project
|
|
@@ -777,7 +847,7 @@ function drawWork() {
|
|
|
777
847
|
w.note.textContent = state === 'working' ? t('MADRE is summarising your latest conversations…')
|
|
778
848
|
: state === 'unavailable' ? (bullets.length ? t('MADRE\'s local model is off: this is the last summary it wrote.') : t('MADRE\'s local model is off: these are your latest conversations, as they are.'))
|
|
779
849
|
: state === 'failed' ? t('MADRE could not summarise them this time: these are your latest conversations, as they are.')
|
|
780
|
-
: work.stale ? t('There is newer work than this summary: it
|
|
850
|
+
: work.stale ? t('There is newer work than this summary: it updates itself in a few minutes, or now if you press Summarize again.')
|
|
781
851
|
: t('Written by MADRE on this Mac from your latest conversations.');
|
|
782
852
|
if (typeof fitTiles === 'function') fitTiles();
|
|
783
853
|
}
|
|
@@ -806,6 +876,9 @@ function linkRow(enlace) {
|
|
|
806
876
|
if (enlace.orders?.allowed) what.push(t('orders up to #{n}', { n: enlace.orders.maxMode }));
|
|
807
877
|
// D-S13: a link BISHOP signed on its own says so, so it is never mistaken for one you signed.
|
|
808
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'));
|
|
809
882
|
node.append(el('p', 'what', what.join(' · ')));
|
|
810
883
|
const traffic = enlace.traffic ?? {};
|
|
811
884
|
// S-021: the last reading says who read, as the room that asked said it.
|
|
@@ -824,6 +897,36 @@ function linkRow(enlace) {
|
|
|
824
897
|
});
|
|
825
898
|
recall.append(recallBox, document.createTextNode(t('Always include (up to 3 notes)')));
|
|
826
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
|
+
}
|
|
827
930
|
const actions = el('div', 'sm-actions');
|
|
828
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(); }));
|
|
829
932
|
else actions.append(button('sm-btn ghost', t('Revoke'), () => { revoking.add(enlace.id); draw(); }));
|
|
@@ -843,7 +946,7 @@ function drawLinks(projects) {
|
|
|
843
946
|
ui.linksSum.textContent = enlaces.length
|
|
844
947
|
? [enlaces.length === 1 ? t('1 in force') : t('{n} in force', { n: enlaces.length }), t('{n} reads in 24 h', { n: reads })].join(' · ')
|
|
845
948
|
: enough ? t('No connections yet.') : t('Add a project to connect it.');
|
|
846
|
-
ui.linksNote.textContent = t('A connection lets one project read the
|
|
949
|
+
ui.linksNote.textContent = t('A connection lets one project read the summarized notes of another, read-only, by a contract you sign here. Nothing is written across, and only notes already used in answers are shared.');
|
|
847
950
|
ui.links.replaceChildren(...enlaces.map(linkRow));
|
|
848
951
|
if (!enlaces.length && ui.linksSheet.open) ui.linksSheet.close();
|
|
849
952
|
drawMap(projects, enlaces);
|
|
@@ -1247,7 +1350,7 @@ const SIZES = [[4, () => t('Small')], [6, () => t('Medium')], [8, () => t('Large
|
|
|
1247
1350
|
// not fit in a chosen height scrolls inside the tile.
|
|
1248
1351
|
const HEIGHTS = [[0, () => t('Automatic')], [2, () => t('2 rows')], [3, () => t('3 rows')], [4, () => t('4 rows')], [5, () => t('5 rows')]];
|
|
1249
1352
|
const HOME_TILES = {
|
|
1250
|
-
bishop: { id: 'sm-bishop', name: () => t('Coordinator'), about: () => t('Your
|
|
1353
|
+
bishop: { id: 'sm-bishop', name: () => t('Coordinator'), about: () => t('Your Coordinator, its state and its recent summary.'), size: 8, rows: 5 },
|
|
1251
1354
|
waiting: { id: 'sm-waiting', name: () => t('Waiting for you'), about: () => t('The questions and permissions an agent is waiting on.'), size: 4, rows: 2 },
|
|
1252
1355
|
links: { id: 'sm-links-tile', name: () => t('Connections'), about: () => t('The map of who looks up whom.'), size: 4, rows: 3 },
|
|
1253
1356
|
projects: { id: 'sm-drawer', name: () => t('Projects'), about: () => t('Every project, open or asleep, three rows at a time.'), size: 12, sizes: [12], kept: true },
|
|
@@ -1259,7 +1362,7 @@ const WIDGETS = {
|
|
|
1259
1362
|
'w-system': { kind: 'system', name: () => t('System resources'), about: () => t('How busy this Mac\'s processor is, and how much memory is free.'), size: 4, rows: 3 },
|
|
1260
1363
|
'w-servers': { kind: 'servers', name: () => t('Servers'), about: () => t('The servers running in your open projects, and whether they answer.'), size: 4, rows: 3, needs: () => t('Turn on Preview in a project\'s Modules to see its servers.') },
|
|
1261
1364
|
'w-memory': { kind: 'memory', name: () => t('Memory quality'), about: () => t('How grown each open project\'s memory is, and its weakest side.'), size: 4, rows: 3, needs: () => t('Open a project to read its memory.') },
|
|
1262
|
-
'w-coordinator': { kind: 'coordinator', name: () => t('Coordinator training'), about: () => t('How far the Coordinator\'s own record is from a model of its own.'), size: 4, rows: 3 },
|
|
1365
|
+
'w-coordinator': { kind: 'coordinator', name: () => t('Coordinator training'), about: () => t('How far the Coordinator\'s own record is from training a model of its own.'), size: 4, rows: 3 },
|
|
1263
1366
|
// Only for MADRE's author, in a copy run from source with the author's token (src/server.mjs, homeWidget):
|
|
1264
1367
|
// anywhere else the server has no such widget and Home never offers it.
|
|
1265
1368
|
'w-usage': { kind: 'usage', owner: true, name: () => t('MADRE usage'), about: () => t('How many copies of MADRE are on, which versions and whether people come back. Only you see this.'), size: 8, rows: 3 },
|
|
@@ -1304,6 +1407,7 @@ function saveLayout() {
|
|
|
1304
1407
|
if (JSON.stringify(layout) === JSON.stringify(DEFAULT_LAYOUT)) localStorage.removeItem(LAYOUT_KEY);
|
|
1305
1408
|
else localStorage.setItem(LAYOUT_KEY, JSON.stringify(layout));
|
|
1306
1409
|
} catch { /* the arrangement lasts as long as the page */ }
|
|
1410
|
+
pushHomeLayout();
|
|
1307
1411
|
applyLayout();
|
|
1308
1412
|
}
|
|
1309
1413
|
const placed = (id) => layout.order.includes(id);
|
|
@@ -1604,7 +1708,10 @@ async function openHomeAdd() {
|
|
|
1604
1708
|
const list = el('div', 'sm-add-list');
|
|
1605
1709
|
body.append(list);
|
|
1606
1710
|
homeAddSheet.append(body);
|
|
1607
|
-
|
|
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'); });
|
|
1608
1715
|
document.body.append(homeAddSheet);
|
|
1609
1716
|
homeAddSheet.showModal();
|
|
1610
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.') });
|
|
@@ -1727,7 +1834,7 @@ const WIDGET_PAINT = {
|
|
|
1727
1834
|
const top = el('div', 'sm-w-line');
|
|
1728
1835
|
top.append(el('b', null, one.name), el('span', 'sm-w-stage', one.stage ?? '—'), el('small', null, pct(one.score)));
|
|
1729
1836
|
row.append(top, meter(one.score));
|
|
1730
|
-
const notes = [one.weakest?.label ? t('weakest: {what}', { what: one.weakest.label.toLowerCase() }) : null, one.aberrations ? t('{n}
|
|
1837
|
+
const notes = [one.weakest?.label ? t('weakest: {what}', { what: one.weakest.label.toLowerCase() }) : null, one.aberrations ? t('{n} doubtful memories', { n: one.aberrations }) : null].filter(Boolean);
|
|
1731
1838
|
if (notes.length) row.append(el('small', null, notes.join(' · ')));
|
|
1732
1839
|
grid.append(row);
|
|
1733
1840
|
}
|
|
@@ -1790,6 +1897,10 @@ function makeWidget(id) {
|
|
|
1790
1897
|
};
|
|
1791
1898
|
node.start = () => { if (timer) return; void read(); timer = setInterval(() => void read(), 20000); };
|
|
1792
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(); };
|
|
1793
1904
|
return node;
|
|
1794
1905
|
}
|
|
1795
1906
|
applyLayout();
|
|
@@ -1833,6 +1944,8 @@ homeTour.firstOpen('home-links', ui.bar.links, {
|
|
|
1833
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.'),
|
|
1834
1945
|
});
|
|
1835
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'),
|
|
1836
1949
|
title: () => t('Project map'),
|
|
1837
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.'),
|
|
1838
1951
|
after: 900,
|
|
@@ -1876,6 +1989,15 @@ function welcomeHome() {
|
|
|
1876
1989
|
: t('A connection lets one project look up what another learned, by a contract you sign. Nothing is written across.'),
|
|
1877
1990
|
});
|
|
1878
1991
|
}, 2200);
|
|
1992
|
+
// 1.0.2 brings one key to Home's bar: said once, after any other card, never over a sheet.
|
|
1993
|
+
let tries = 0;
|
|
1994
|
+
const sayAdd = async () => {
|
|
1995
|
+
const key = document.getElementById('sm-add-widgets');
|
|
1996
|
+
if (!key || document.querySelector('dialog[open]') || !(await homeTour.hint('new-add-widgets', key, { title: t('New: add widgets'), text: t('This button opens «Add to Home»: choose the widgets you want to see here, or bring back the ones you removed.') }))) {
|
|
1997
|
+
if ((tries += 1) < 6) setTimeout(sayAdd, 6000)?.unref?.();
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
setTimeout(sayAdd, 5000)?.unref?.();
|
|
1879
2001
|
});
|
|
1880
2002
|
}
|
|
1881
2003
|
|
|
@@ -1921,7 +2043,7 @@ function openLinkSheet({ reader = null, source = null } = {}) {
|
|
|
1921
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);
|
|
1922
2044
|
const pick = {
|
|
1923
2045
|
reader: reader ?? (source !== data.salas && rooms.some((room) => room.id === data.salas) ? data.salas : rooms.find((room) => room.id !== source)?.id),
|
|
1924
|
-
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,
|
|
1925
2047
|
orders: false, maxMode: 1, perHour: 6, designation: '',
|
|
1926
2048
|
};
|
|
1927
2049
|
const free = (room) => room.id !== pick.reader && room.id !== data.salas && !linkedPair(room.id, pick.reader);
|
|
@@ -2202,11 +2324,16 @@ function openLinkSheet({ reader = null, source = null } = {}) {
|
|
|
2202
2324
|
ends.append(rail(LINK_DAYS.map(([value, words]) => [value, words()]), pick.days, (value) => { pick.days = value; }, t('It ends')));
|
|
2203
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 }));
|
|
2204
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'))));
|
|
2205
2332
|
page.append(box);
|
|
2206
2333
|
actions.append(next());
|
|
2207
2334
|
} else if (kind === 'orders') {
|
|
2208
2335
|
const name = nameOf(ordersInto());
|
|
2209
|
-
intro(t('Orders from the
|
|
2336
|
+
intro(t('Orders from the Coordinator'), sayBold('With orders, the Coordinator can ask {room} for a task, and its agents do it there. Without them, it only reads. #3 EDIT and #4 FULL ACCESS never go this way: they arrive as a proposal you unlock in that project.', { room: name }));
|
|
2210
2337
|
const box = card();
|
|
2211
2338
|
box.append(row(sayBold('Allow orders into {room}', { room: name }), sayBold('Leave it off unless you need the Coordinator to act in {room}.', { room: name }), toggle(pick.orders, (on) => { pick.orders = on; render(); }, t('Allow orders into {room}', { room: name })), { recommended: !pick.orders }));
|
|
2212
2339
|
if (pick.orders) {
|
|
@@ -2233,8 +2360,12 @@ function openLinkSheet({ reader = null, source = null } = {}) {
|
|
|
2233
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']));
|
|
2234
2361
|
said(pick.days ? t('Until {date}.', { date: until() }) : t('With no expiry: it lasts until you revoke it.'));
|
|
2235
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.'));
|
|
2236
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.'));
|
|
2237
|
-
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.'));
|
|
2238
2369
|
page.append(list);
|
|
2239
2370
|
const warn = cautions();
|
|
2240
2371
|
if (warn.length) { const box = card(); for (const words of warn) box.append(caution(words)); page.append(box); }
|
|
@@ -2246,6 +2377,9 @@ function openLinkSheet({ reader = null, source = null } = {}) {
|
|
|
2246
2377
|
orders: pick.orders && ordersInto() ? { allowed: true, maxMode: pick.maxMode, perHour: pick.perHour } : { allowed: false, maxMode: 0, perHour: pick.perHour },
|
|
2247
2378
|
designation: pick.orders && ordersInto() ? pick.designation.trim() : undefined,
|
|
2248
2379
|
recall: pick.recall,
|
|
2380
|
+
train: pick.train && coordinator(pick.reader),
|
|
2381
|
+
state: pick.state,
|
|
2382
|
+
deliver: pick.deliver,
|
|
2249
2383
|
};
|
|
2250
2384
|
sign.disabled = true;
|
|
2251
2385
|
sign.textContent = t('Signing…');
|
|
@@ -2280,16 +2414,22 @@ async function refresh() {
|
|
|
2280
2414
|
const answer = await refreshPending;
|
|
2281
2415
|
refreshPending = null;
|
|
2282
2416
|
if (!answer) { ui.foot.textContent = t('This project did not answer. It may have closed.'); ui.bar.live.classList.add('is-off'); ui.bar.live.lastChild.textContent = t('Not answering right now'); return; }
|
|
2283
|
-
|
|
2417
|
+
// Home is the first screen of `npx madre`: a MADRE nobody has set takes this browser's language
|
|
2418
|
+
// here, once, and every project follows (public/i18n.js, browserLanguage).
|
|
2419
|
+
if (answer.languageChosen === false && !languageAdopted) {
|
|
2420
|
+
languageAdopted = true;
|
|
2421
|
+
let chosenHere = null;
|
|
2422
|
+
try { chosenHere = localStorage.getItem('pulse.language'); } catch { /* no storage */ }
|
|
2423
|
+
const mine = chosenHere === 'es' || chosenHere === 'en' ? chosenHere : browserLanguage();
|
|
2424
|
+
if (await post('/api/language', { language: mine }).then((response) => response?.ok !== false).catch(() => false)) answer.language = mine;
|
|
2425
|
+
}
|
|
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(); }
|
|
2284
2427
|
data = answer;
|
|
2285
2428
|
// A project that came up after Home stopped waiting is open: its card no longer says it did not.
|
|
2286
2429
|
for (const room of answer.rooms ?? []) if (room.open) failed.delete(room.id);
|
|
2287
2430
|
ui.foot.textContent = t('Every project runs on this Mac, on its own, with its own memory.');
|
|
2288
2431
|
// The room's corner (app.js, #colophon): the version this Home runs, and NOTICE's own line.
|
|
2289
|
-
if (answer.version) {
|
|
2290
|
-
ui.colophon.replaceChildren(el('b', null, `MADRE ${answer.version}`), document.createTextNode('\u00a0·\u00a0© 2026 Jossue Alcalá'));
|
|
2291
|
-
ui.colophon.hidden = false;
|
|
2292
|
-
}
|
|
2432
|
+
if (answer.version) { drawAttribution(ui.colophon, answer.version); void drawColophonKeys(ui.colophon); }
|
|
2293
2433
|
draw();
|
|
2294
2434
|
}
|
|
2295
2435
|
|
|
@@ -2387,7 +2527,7 @@ function macroWords() {
|
|
|
2387
2527
|
back: t('Back to Home'),
|
|
2388
2528
|
hint: t('Drag the space to move it · wheel to come closer · click a project to look at it · drag it to turn it · carry one onto the Coordinator for the Coordinator to read it, or onto another to connect them · ← → from one to the next · Esc to come back'),
|
|
2389
2529
|
bishop: t('Coordinator'),
|
|
2390
|
-
bishopAbout: t('Your
|
|
2530
|
+
bishopAbout: t('Your Coordinator. It reads across your projects, signs the connections between them and carries an order from one to another.'),
|
|
2391
2531
|
bishopReads: (n) => (n ? (n === 1 ? t('It reads 1 project by a connection') : t('It reads {n} projects by connections', { n })) : t('It reads no project yet: each one it reads is a connection you sign.')),
|
|
2392
2532
|
notLinked: t('Not connected yet: touch one to connect it'),
|
|
2393
2533
|
talk: t('Talk to the Coordinator'),
|
|
@@ -2395,7 +2535,7 @@ function macroWords() {
|
|
|
2395
2535
|
recenter: t('RECENTER'),
|
|
2396
2536
|
// The legend under its name, in the Memory map's words.
|
|
2397
2537
|
legend: t('Memory kinds'),
|
|
2398
|
-
kinds: { ...Object.fromEntries(KINDS.map((kind) => [kind, kindWord(kind)])), dim: t('
|
|
2538
|
+
kinds: { ...Object.fromEntries(KINDS.map((kind) => [kind, kindWord(kind)])), dim: t('NOT USED') },
|
|
2399
2539
|
veins: (n) => t('{n} connections', { n }),
|
|
2400
2540
|
links: t('Connections'),
|
|
2401
2541
|
link: t('Connect…'),
|
|
@@ -2473,7 +2613,10 @@ ui.macro.addEventListener('click', async () => {
|
|
|
2473
2613
|
// BISHOP at the centre (D-S12): the macroverse draws its body itself (macroverso.js, bishopOrb).
|
|
2474
2614
|
bishop: data.salas ? { id: data.salas } : null,
|
|
2475
2615
|
words: macroWords(),
|
|
2616
|
+
// Drawn as neurons when the person chose it (public/neural.js), here or in a project.
|
|
2617
|
+
neural: neuralChosen(),
|
|
2476
2618
|
actions: {
|
|
2619
|
+
neural: (on) => { rememberNeural(on); pushLooks(); },
|
|
2477
2620
|
wake: async (room) => { await startRoom(room); },
|
|
2478
2621
|
// Dragged from one universe onto another: the notes of the first would travel to the second.
|
|
2479
2622
|
link: (source, reader) => openLinkSheet({ source: source?.id ?? null, reader: reader?.id ?? null }),
|
|
@@ -2519,7 +2662,8 @@ function drawTopbar() {
|
|
|
2519
2662
|
b.agents.replaceChildren();
|
|
2520
2663
|
for (const agent of (agentsSeen?.agents ?? []).filter((one) => one.id !== 'madre')) {
|
|
2521
2664
|
const state = agentState(agent, agentsSeen);
|
|
2522
|
-
|
|
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'));
|
|
2523
2667
|
key.append(mark(agent.id, 24));
|
|
2524
2668
|
key.title = `${agent.label ?? agent.id} · ${state.words}`;
|
|
2525
2669
|
key.setAttribute('aria-label', key.title);
|
|
@@ -2595,6 +2739,7 @@ function pinsShown() {
|
|
|
2595
2739
|
function pinsWrite(list) {
|
|
2596
2740
|
pins.saved = [...list];
|
|
2597
2741
|
try { localStorage.setItem(PIN_KEY, JSON.stringify(pins.saved)); } catch { /* this page only */ }
|
|
2742
|
+
pushHomeLayout();
|
|
2598
2743
|
drawTopbar();
|
|
2599
2744
|
settingsSheet?.sync({ detail: true });
|
|
2600
2745
|
}
|
|
@@ -2610,6 +2755,7 @@ function pinRemove(id) { pinsWrite(pinsShown().filter((one) => one !== id)); }
|
|
|
2610
2755
|
function pinsReset() {
|
|
2611
2756
|
pins.saved = null;
|
|
2612
2757
|
try { localStorage.removeItem(PIN_KEY); } catch { /* this page only */ }
|
|
2758
|
+
pushHomeLayout();
|
|
2613
2759
|
drawTopbar();
|
|
2614
2760
|
settingsSheet?.sync({ detail: true });
|
|
2615
2761
|
}
|
|
@@ -3038,6 +3184,8 @@ function reloadWithGrace() {
|
|
|
3038
3184
|
}, 900);
|
|
3039
3185
|
}
|
|
3040
3186
|
ui.bar.reload.addEventListener('click', reloadWithGrace);
|
|
3187
|
+
// «Add to Home» had only a right-click and a menu; now a key of its own in the bar (2026-10-14).
|
|
3188
|
+
document.getElementById('sm-add-widgets')?.addEventListener('click', () => openHomeAdd());
|
|
3041
3189
|
|
|
3042
3190
|
// From Home into a project, and back (2026-10-10): the page settles back under a veil of its own
|
|
3043
3191
|
// ground and goes (styles.css, madre-leaving); the room lifts the veil as it arrives. Every way in
|
|
@@ -3066,17 +3214,19 @@ document.addEventListener('click', (event) => {
|
|
|
3066
3214
|
// and over any open sheet, for a few seconds.
|
|
3067
3215
|
let toastNode = null;
|
|
3068
3216
|
let toastTimer = 0;
|
|
3069
|
-
|
|
3217
|
+
// `action` ({ label, run }) puts one key at its end: the Market's news says «See it» (2026-10-18).
|
|
3218
|
+
function toast(words, tone = 'done', action = null) {
|
|
3070
3219
|
toastNode?.remove();
|
|
3071
3220
|
const node = el('div', 'toast sm-toast');
|
|
3072
3221
|
node.setAttribute('role', 'status');
|
|
3073
3222
|
node.dataset.tone = tone;
|
|
3074
3223
|
node.append(el('i', 'toast-dot'), el('span', 'toast-text', words));
|
|
3224
|
+
if (action) node.append(button('toast-act', action.label, () => { node.remove(); action.run(); }));
|
|
3075
3225
|
document.body.append(node);
|
|
3076
3226
|
if (typeof node.showPopover === 'function') { node.popover = 'manual'; node.showPopover(); }
|
|
3077
3227
|
toastNode = node;
|
|
3078
3228
|
clearTimeout(toastTimer);
|
|
3079
|
-
toastTimer = setTimeout(() => { node.remove(); if (toastNode === node) toastNode = null; }, 5000);
|
|
3229
|
+
toastTimer = setTimeout(() => { node.remove(); if (toastNode === node) toastNode = null; }, action ? 9000 : 5000);
|
|
3080
3230
|
}
|
|
3081
3231
|
|
|
3082
3232
|
// ---- THE LOBBY (2026-10-10). The first screen of MADRE: every project on this Mac, BISHOP above
|
|
@@ -3273,7 +3423,7 @@ function projectPicker({ onAdded = null, onChange = null, withAdd = true } = {})
|
|
|
3273
3423
|
const chosen = new Map(); // path → name
|
|
3274
3424
|
const node = el('div', 'sm-picker');
|
|
3275
3425
|
const pages = foldPages();
|
|
3276
|
-
let source = 'found'; // found · browse
|
|
3426
|
+
let source = 'found'; // found · browse · new
|
|
3277
3427
|
let found = null; // the suggestions, once read
|
|
3278
3428
|
let here = null; // the folder being walked, as /api/salas/folders answered
|
|
3279
3429
|
let query = '';
|
|
@@ -3298,7 +3448,46 @@ function projectPicker({ onAdded = null, onChange = null, withAdd = true } = {})
|
|
|
3298
3448
|
const error = el('p', 'error');
|
|
3299
3449
|
footer.append(pages.pager, count);
|
|
3300
3450
|
if (addButton) footer.append(addButton);
|
|
3301
|
-
|
|
3451
|
+
// A project from nothing (2026-10-17): its name, where the folder goes (the one being walked in
|
|
3452
|
+
// Your folders, else the home folder) and one key. Made, it is added like any chosen folder.
|
|
3453
|
+
const fresh = el('form', 'sm-new-project');
|
|
3454
|
+
fresh.hidden = true;
|
|
3455
|
+
const nameField = el('label', 'sm-new-name');
|
|
3456
|
+
const nameInput = el('input');
|
|
3457
|
+
nameInput.type = 'text';
|
|
3458
|
+
nameInput.autocomplete = 'off';
|
|
3459
|
+
nameInput.spellcheck = false;
|
|
3460
|
+
nameInput.maxLength = 120;
|
|
3461
|
+
nameInput.placeholder = t('my-new-project');
|
|
3462
|
+
nameField.append(el('span', null, t('Project name')), nameInput);
|
|
3463
|
+
const whereLine = el('p', 'sm-new-where');
|
|
3464
|
+
const makeKey = button('sm-btn primary', t('Create project'), () => {});
|
|
3465
|
+
makeKey.type = 'submit';
|
|
3466
|
+
const freshError = el('p', 'error');
|
|
3467
|
+
fresh.append(nameField, whereLine, makeKey, freshError);
|
|
3468
|
+
const parentNow = () => (here?.path && here.path !== here.home ? here.path : here?.home ?? null);
|
|
3469
|
+
const sayWhere = () => {
|
|
3470
|
+
whereLine.replaceChildren(el('span', null, t('The folder is made in')), ' ', el('code', null, parentNow() ? homeWord(parentNow()) : '~'), ' ');
|
|
3471
|
+
const other = button('sm-new-other', t('Choose another place'), () => { touched = true; source = 'browse'; draw(); if (!here) void walk(); });
|
|
3472
|
+
other.type = 'button';
|
|
3473
|
+
whereLine.append(other);
|
|
3474
|
+
};
|
|
3475
|
+
fresh.addEventListener('submit', async (event) => {
|
|
3476
|
+
event.preventDefault();
|
|
3477
|
+
freshError.textContent = '';
|
|
3478
|
+
makeKey.disabled = true;
|
|
3479
|
+
const answer = await post('/api/salas/new', { name: nameInput.value, parent: parentNow() });
|
|
3480
|
+
makeKey.disabled = false;
|
|
3481
|
+
if (!answer.ok) { freshError.textContent = answer.body.error ?? t('It could not be created.'); nameInput.focus(); return; }
|
|
3482
|
+
const added = answer.body.added ?? [];
|
|
3483
|
+
nameInput.value = '';
|
|
3484
|
+
await refresh();
|
|
3485
|
+
if (added[0]) toast(t('{name} was created and added to Home.', { name: added[0].name }));
|
|
3486
|
+
onAdded?.(added);
|
|
3487
|
+
void loadFound();
|
|
3488
|
+
if (here) void walk(here.path === here.home ? null : here.path);
|
|
3489
|
+
});
|
|
3490
|
+
node.append(head, line, pages.grid, fresh, footer, error);
|
|
3302
3491
|
const sync = () => {
|
|
3303
3492
|
count.textContent = chosen.size ? (chosen.size === 1 ? t('1 folder chosen') : t('{n} folders chosen', { n: chosen.size })) : t('Choose one or more folders');
|
|
3304
3493
|
if (addButton) {
|
|
@@ -3315,14 +3504,25 @@ function projectPicker({ onAdded = null, onChange = null, withAdd = true } = {})
|
|
|
3315
3504
|
onToggle: (on) => { if (on) chosen.set(one.path, one.name); else chosen.delete(one.path); sync(); },
|
|
3316
3505
|
});
|
|
3317
3506
|
function draw({ keepPage = false } = {}) {
|
|
3318
|
-
const
|
|
3507
|
+
const options = customSeg([['found', t('Found on this Mac')], ['browse', t('Your folders')], ['new', t('New project')]], source, (value) => {
|
|
3319
3508
|
touched = true;
|
|
3320
3509
|
source = value;
|
|
3321
3510
|
draw();
|
|
3322
|
-
if (value
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
seg
|
|
3511
|
+
if (value !== 'found' && !here) void walk();
|
|
3512
|
+
if (value === 'new') nameInput.focus();
|
|
3513
|
+
});
|
|
3514
|
+
seg.replaceWith(options);
|
|
3515
|
+
seg = options;
|
|
3516
|
+
const making = source === 'new';
|
|
3517
|
+
fresh.hidden = !making;
|
|
3518
|
+
pages.grid.hidden = making;
|
|
3519
|
+
footer.hidden = making;
|
|
3520
|
+
search.hidden = making;
|
|
3521
|
+
if (making) {
|
|
3522
|
+
line.replaceChildren(el('span', 'sm-picker-note', t('An empty folder, made for you, that becomes a project.')));
|
|
3523
|
+
sayWhere();
|
|
3524
|
+
return;
|
|
3525
|
+
}
|
|
3326
3526
|
const noMatch = () => t('No folder matches «{query}».', { query: input.value.trim() });
|
|
3327
3527
|
if (source === 'found') {
|
|
3328
3528
|
line.replaceChildren(el('span', 'sm-picker-note', found ? t('Folders that look like projects, in the places people keep them.') : t('Looking…')));
|
|
@@ -3361,7 +3561,7 @@ function projectPicker({ onAdded = null, onChange = null, withAdd = true } = {})
|
|
|
3361
3561
|
const answer = await fetch(`/api/salas/folders${path ? `?path=${encodeURIComponent(path)}` : ''}`, { cache: 'no-store' }).then((response) => response.json()).catch(() => null);
|
|
3362
3562
|
here = answer ?? { error: t('That folder is not there.') };
|
|
3363
3563
|
if (path !== null) { query = ''; input.value = ''; }
|
|
3364
|
-
if (source
|
|
3564
|
+
if (source !== 'found') draw();
|
|
3365
3565
|
}
|
|
3366
3566
|
async function add() {
|
|
3367
3567
|
error.textContent = '';
|
|
@@ -3417,6 +3617,30 @@ function openAddSheet() {
|
|
|
3417
3617
|
}
|
|
3418
3618
|
ui.add.addEventListener('click', openAddSheet);
|
|
3419
3619
|
|
|
3620
|
+
// THE MARKET and MODULES (2026-10-18), full screen (market-view.js): the store, and what is on in
|
|
3621
|
+
// which project. From Home a module is turned on for every project or only in one; one that writes
|
|
3622
|
+
// into a project's folder opens that project, on its card, where its commands are read first.
|
|
3623
|
+
const openHomeMarket = (tab = 'shop', focus = null) => openMarket({
|
|
3624
|
+
where: 'home',
|
|
3625
|
+
tab,
|
|
3626
|
+
focus,
|
|
3627
|
+
host: {
|
|
3628
|
+
localAi: () => openSettings('local'),
|
|
3629
|
+
toast: (words) => toast(words),
|
|
3630
|
+
changed: () => drawColophonKeys(ui.colophon),
|
|
3631
|
+
openProject: async (projectId, item) => {
|
|
3632
|
+
const answer = await post('/api/salas/start', { room: projectId });
|
|
3633
|
+
if (!answer.ok || !answer.body.port) return false;
|
|
3634
|
+
window.open(`http://127.0.0.1:${answer.body.port}/#modulo=${encodeURIComponent(item.id)}`, '_blank', 'noopener');
|
|
3635
|
+
return true;
|
|
3636
|
+
},
|
|
3637
|
+
},
|
|
3638
|
+
});
|
|
3639
|
+
document.querySelector('#sm-tb-market')?.addEventListener('click', () => void openHomeMarket('shop'));
|
|
3640
|
+
document.querySelector('#sm-tb-modules')?.addEventListener('click', () => void openHomeMarket('mine'));
|
|
3641
|
+
// The Market's catalog from madre.run, and what it updated on its own since, said once.
|
|
3642
|
+
void loadMarket({ toast: (words, action) => toast(words, action ? 'news' : 'done', action), open: (id) => openHomeMarket('shop', id) });
|
|
3643
|
+
|
|
3420
3644
|
// The agents of this computer, with everything a connection needs in one place: which account
|
|
3421
3645
|
// each one signs in with, the doors it has, the box where a pasted key goes, the address a
|
|
3422
3646
|
// sign-in opened and the code that page gives back. Home used to show a name, a lamp and one
|
|
@@ -3425,7 +3649,7 @@ ui.add.addEventListener('click', openAddSheet);
|
|
|
3425
3649
|
// `compact` is the welcome's first step: there, a connected agent is done, and a button to change
|
|
3426
3650
|
// the account it signs in with is a question nobody asked on their first minute.
|
|
3427
3651
|
// Who makes each agent, said under its name as the project's AJUSTES says it.
|
|
3428
|
-
const VENDOR_WORDS = { codex: 'OpenAI', claude: 'Anthropic', gemini: 'Google', opencode: 'OpenCode' };
|
|
3652
|
+
const VENDOR_WORDS = { codex: 'OpenAI', claude: 'Anthropic', gemini: 'Google', opencode: 'OpenCode', cursor: 'Cursor' };
|
|
3429
3653
|
|
|
3430
3654
|
// OpenCode's providers in Home, the same block as the project's AJUSTES › Agents › OpenCode: a line
|
|
3431
3655
|
// per provider with a key or an account and its switch for the chat, one row to add another, and
|
|
@@ -3550,6 +3774,16 @@ function agentsPanel({ onPaint = null, compact = false } = {}) {
|
|
|
3550
3774
|
open.rel = 'noopener';
|
|
3551
3775
|
box.append(open);
|
|
3552
3776
|
}
|
|
3777
|
+
// A device sign-in (GitHub Copilot through OpenCode) shows its code instead of asking for one.
|
|
3778
|
+
if (live.deviceCode) {
|
|
3779
|
+
const said = el('div', 'sm-agent-device');
|
|
3780
|
+
said.append(el('span', null, t('Type this code on that page:')), el('code', null, live.deviceCode));
|
|
3781
|
+
const copy = button('sm-btn small', t('Copy'), async () => {
|
|
3782
|
+
try { await navigator.clipboard.writeText(live?.deviceCode ?? ''); copy.textContent = t('Copied'); } catch { copy.textContent = t('Select it'); }
|
|
3783
|
+
});
|
|
3784
|
+
said.append(copy);
|
|
3785
|
+
box.append(said);
|
|
3786
|
+
}
|
|
3553
3787
|
if (live.asksForCode || live.awaitingCode) {
|
|
3554
3788
|
const form = el('form', 'sm-agent-code');
|
|
3555
3789
|
const input = el('input');
|
|
@@ -3661,10 +3895,19 @@ function agentsPanel({ onPaint = null, compact = false } = {}) {
|
|
|
3661
3895
|
// One button per account the CLI can sign in with: Claude has two, and only the human can
|
|
3662
3896
|
// say which one is paying.
|
|
3663
3897
|
const doorKeys = () => {
|
|
3664
|
-
|
|
3898
|
+
// With a session, every agent offers the same «sign in again» as Codex. A single door
|
|
3899
|
+
// (Gemini's Google account) is that door; two doors (Claude's subscription and Console)
|
|
3900
|
+
// are chosen after the click.
|
|
3901
|
+
const methods = agent.login?.methods ?? [];
|
|
3902
|
+
const choosing = choosingAccount.has(agent.id);
|
|
3903
|
+
const doors = !state.ok ? (methods.length ? methods : [{ id: null, label: null, note: agent.login?.note }])
|
|
3904
|
+
: methods.length > 1 && choosing ? methods
|
|
3905
|
+
: [{ id: methods.length === 1 ? methods[0].id : null, label: null, note: methods.length === 1 ? methods[0].note : agent.login?.note, choose: methods.length > 1 }];
|
|
3665
3906
|
return doors.map((door) => {
|
|
3666
3907
|
const name = door.label ? t(door.label) : state.ok ? t('Sign in again') : t('Sign in');
|
|
3667
|
-
const key = button(`sm-btn ${state.ok ? '' : 'primary'} small`, name, async (event) => {
|
|
3908
|
+
const key = button(`sm-btn ${(state.ok && !choosing) || agent.key?.first ? '' : 'primary'} small`, name, async (event) => {
|
|
3909
|
+
if (door.choose) { choosingAccount.add(agent.id); void paint(); return; }
|
|
3910
|
+
choosingAccount.delete(agent.id);
|
|
3668
3911
|
event.currentTarget.disabled = true;
|
|
3669
3912
|
await post(`/api/agents/${agent.id}/login`, { method: door.id });
|
|
3670
3913
|
void paint();
|
|
@@ -3673,6 +3916,9 @@ function agentsPanel({ onPaint = null, compact = false } = {}) {
|
|
|
3673
3916
|
return key;
|
|
3674
3917
|
});
|
|
3675
3918
|
};
|
|
3919
|
+
// Gemini's key is its way in (agent.key.first): the field before the Google door.
|
|
3920
|
+
const keyFirst = Boolean(agent.detected && agent.key?.first);
|
|
3921
|
+
if (keyFirst) body.append(keyForm(agent));
|
|
3676
3922
|
const acts = el('div', 'sm-agent-acts');
|
|
3677
3923
|
if (!state.busyNow) {
|
|
3678
3924
|
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(); }));
|
|
@@ -3682,9 +3928,12 @@ function agentsPanel({ onPaint = null, compact = false } = {}) {
|
|
|
3682
3928
|
const ask = askBlock(agent);
|
|
3683
3929
|
if (ask) body.append(ask);
|
|
3684
3930
|
// A key, pasted here; OpenCode's are its providers', in the block below.
|
|
3685
|
-
if (agent.detected && agent.key && agent.id !== 'opencode') body.append(keyForm(agent));
|
|
3686
|
-
//
|
|
3687
|
-
|
|
3931
|
+
if (agent.detected && agent.key && agent.id !== 'opencode' && !keyFirst) body.append(keyForm(agent));
|
|
3932
|
+
// The same install or sign-in typed in a terminal, folded for whoever prefers it. Signed in,
|
|
3933
|
+
// OpenCode's providers are added in the block below; its terminal is for a first sign-in.
|
|
3934
|
+
const way = !agent.detected ? agent.install && [agent.install.display, ...(agent.install.alternatives ?? []).map((line) => `# ${t(line)}`)]
|
|
3935
|
+
: agent.login?.terminal && !(agent.id === 'opencode' && state.ok) ? [agent.login.terminal.command, ...(agent.login.terminal.note ? [`# ${t(agent.login.terminal.note)}`] : [])] : null;
|
|
3936
|
+
if (way && !state.busyNow) body.append(terminalWay(way, agent.id));
|
|
3688
3937
|
if (agent.id === 'opencode' && agent.detected) body.append(openCodeProviders.node());
|
|
3689
3938
|
item.append(body);
|
|
3690
3939
|
list.append(item);
|
|
@@ -3967,7 +4216,7 @@ function setStatus(id) {
|
|
|
3967
4216
|
if (id === 'local') {
|
|
3968
4217
|
if (!localSeen) return { line: localFailed ? t('Home did not answer.') : t('Reading…') };
|
|
3969
4218
|
const on = localSeen.projects.filter((one) => one.on).length;
|
|
3970
|
-
return { line: `${LOCAL_MODE_WORDS[localSeen.mode]()} · ${t('{n} of {total} projects', { n: on, total: localSeen.projects.length })}`, warn: !localSeen.master ? t('Local AI is
|
|
4219
|
+
return { line: `${LOCAL_MODE_WORDS[localSeen.mode]()} · ${t('{n} of {total} projects', { n: on, total: localSeen.projects.length })}`, warn: !localSeen.master ? t('Local AI is off for every project.') : !localSeen.ollama.running ? t('Ollama is not running') : null };
|
|
3971
4220
|
}
|
|
3972
4221
|
if (id === 'projects') {
|
|
3973
4222
|
if (!data) return { line: t('Reading…') };
|
|
@@ -3976,7 +4225,7 @@ function setStatus(id) {
|
|
|
3976
4225
|
}
|
|
3977
4226
|
if (id === 'release') {
|
|
3978
4227
|
if (!versionSeen) return { line: t('Reading…') };
|
|
3979
|
-
return { line: `MADRE ${versionSeen.current} · ${versionSeen.available ? t('{version} available', { version: versionSeen.latest }) : versionSeen.enabled === false ? t('not checked') : t('up to date')}`, warn: versionSeen.available ? t('{version} is
|
|
4228
|
+
return { line: `MADRE ${versionSeen.current} · ${versionSeen.available ? t('{version} available', { version: versionSeen.latest }) : versionSeen.enabled === false ? t('not checked') : t('up to date')}`, warn: versionSeen.available ? t('{version} is out', { version: versionSeen.latest }) : null };
|
|
3980
4229
|
}
|
|
3981
4230
|
return { line: '' };
|
|
3982
4231
|
}
|
|
@@ -4089,6 +4338,15 @@ function fillLook(host) {
|
|
|
4089
4338
|
const papers = el('div', 'custom-papers');
|
|
4090
4339
|
papers.setAttribute('role', 'radiogroup');
|
|
4091
4340
|
for (const value of WALLPAPERS) {
|
|
4341
|
+
// The person's own picture: chosen like any other once it is here; until then, the way to add it.
|
|
4342
|
+
if (value === CUSTOM_WALL) {
|
|
4343
|
+
const mine = customWall();
|
|
4344
|
+
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') }));
|
|
4345
|
+
if (mine) { paper.setAttribute('role', 'radio'); paper.setAttribute('aria-checked', String(looks.wallpaper === CUSTOM_WALL)); }
|
|
4346
|
+
customTile(paper, mine);
|
|
4347
|
+
papers.append(paper);
|
|
4348
|
+
continue;
|
|
4349
|
+
}
|
|
4092
4350
|
const paper = button('paper', null, () => saveLooks({ wallpaper: value }));
|
|
4093
4351
|
paper.dataset.wp = value;
|
|
4094
4352
|
paper.setAttribute('role', 'radio');
|
|
@@ -4125,10 +4383,19 @@ function fillLook(host) {
|
|
|
4125
4383
|
return line;
|
|
4126
4384
|
};
|
|
4127
4385
|
tune.append(dial('veil', t('Intensity')), dial('blur', t('Blur')));
|
|
4386
|
+
// Your picture: where the screen falls on it, and changing it or taking it away.
|
|
4387
|
+
if (looks.wallpaper === CUSTOM_WALL && customWall()) {
|
|
4388
|
+
tune.append(customTools(customWall(), {
|
|
4389
|
+
button: (words, click) => sxButton(words, click),
|
|
4390
|
+
changed: (kept, { reframed }) => { if (!reframed) settingsSheet?.paintLooks(); },
|
|
4391
|
+
removed: () => saveLooks({ wallpaper: 'default' }),
|
|
4392
|
+
failed: (message) => toast(message, 'look'),
|
|
4393
|
+
}));
|
|
4394
|
+
}
|
|
4128
4395
|
paperBox.append(tune);
|
|
4129
4396
|
}
|
|
4130
4397
|
sxBlock(page, t('Wallpaper'), photo
|
|
4131
|
-
? t('
|
|
4398
|
+
? t('The project\'s background can be a photograph, shown as a layer or as a mesh of its colors: choose how strong it looks and how much the photograph blurs.')
|
|
4132
4399
|
: t('Behind Home, and behind the conversation of the project served at this address. Light and dark each have their own.')).rows.append(paperBox);
|
|
4133
4400
|
|
|
4134
4401
|
// Home's own: its cards, its text, the order of its projects. The conversation's bubbles and text
|
|
@@ -4191,13 +4458,16 @@ function fillBishop(host) {
|
|
|
4191
4458
|
// show; Ollama itself and what it does are set in a project's Modules › Local AI.
|
|
4192
4459
|
const LOCAL_MODE_WORDS = { auto: () => t('Automatic'), pick: () => t('One by one'), manual: () => t('Manual') };
|
|
4193
4460
|
const LOCAL_MODE_SAYS = {
|
|
4194
|
-
auto: () => t('Every project uses
|
|
4461
|
+
auto: () => t('Every project uses it, new ones too.'),
|
|
4195
4462
|
pick: () => t('Only the projects you switch on below. New projects start without it.'),
|
|
4196
|
-
manual: () => t('Each project decides in its own
|
|
4463
|
+
manual: () => t('Each project decides in its own Modules › Local AI. New ones start with it.'),
|
|
4197
4464
|
};
|
|
4198
4465
|
let localFailed = false; // the last read got no answer: the sheet says so instead of «Reading…» forever
|
|
4466
|
+
let localHardware = null; // what this computer can run and train (src/modules/local-models.mjs)
|
|
4199
4467
|
async function loadLocal() {
|
|
4200
4468
|
const answer = await fetch('/api/salas/local-ai', { cache: 'no-store' }).then((response) => (response.ok ? response.json() : null)).catch(() => null);
|
|
4469
|
+
const ollama = await fetch('/api/ollama', { cache: 'no-store' }).then((response) => (response.ok ? response.json() : null)).catch(() => null);
|
|
4470
|
+
if (ollama?.recommended?.hardware) localHardware = ollama.recommended.hardware;
|
|
4201
4471
|
if (answer) localSeen = answer;
|
|
4202
4472
|
localFailed = !answer;
|
|
4203
4473
|
settingsSheet?.sync({ detail: true });
|
|
@@ -4240,11 +4510,13 @@ function fillLocal(host) {
|
|
|
4240
4510
|
return;
|
|
4241
4511
|
}
|
|
4242
4512
|
if (!localSeen) { host.append(el('p', 'settings-reading', t('Reading…'))); return; }
|
|
4243
|
-
host.append(el('p', 'sx-intro', t('
|
|
4513
|
+
host.append(el('p', 'sx-intro', t('Ollama runs local AI: it searches and sums up each project\'s memory and answers as @madre, free and private. Choose which projects use it.')));
|
|
4244
4514
|
const mode = sxBlock(host, t('Which projects'), LOCAL_MODE_SAYS[localSeen.mode]());
|
|
4245
4515
|
sxRow(mode, t('Mode'), null, localModeSeg());
|
|
4246
|
-
if (!localSeen.master) sxRow(mode, t('Local AI is
|
|
4247
|
-
else if (!localSeen.ollama.running) sxRow(mode, t('Ollama is not running'), t('
|
|
4516
|
+
if (!localSeen.master) sxRow(mode, t('Local AI is off for every project.'), t('Turn it on in a project, in Modules › Local AI.'));
|
|
4517
|
+
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.'));
|
|
4518
|
+
// What this computer can run and train, as a project shows it in Modules › Local AI.
|
|
4519
|
+
if (localHardware) drawHardware(host, localHardware, { el, sxBlock, sxRow, sxChip: (text, tone = '') => el('span', `sx-chip${tone ? ` ${tone}` : ''}`, text) });
|
|
4248
4520
|
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.'));
|
|
4249
4521
|
if (!localSeen.projects.length) { sxRow(list, t('No projects yet'), null); return; }
|
|
4250
4522
|
const pages = Math.ceil(localSeen.projects.length / LOCAL_PER_PAGE);
|
|
@@ -4328,6 +4600,7 @@ function fillRelease(host) {
|
|
|
4328
4600
|
const late = sxBlock(host, t('Projects on an older MADRE'), t('They keep running the version they started with until you restart them. What is running in them stops.'), { aside: behind.length > 1 ? sxButton(t('Restart all'), () => void restartAll(), { tone: 'primary' }) : null });
|
|
4329
4601
|
for (const room of behind) sxRow(late, roomLabel(room), `MADRE ${room.open.version}`, sxButton(t('Restart'), () => { settingsSheet?.close(); void restartRoom(room); }));
|
|
4330
4602
|
}
|
|
4603
|
+
drawLegal(host);
|
|
4331
4604
|
}
|
|
4332
4605
|
async function updateHome(info, key, said) {
|
|
4333
4606
|
key.disabled = true;
|
|
@@ -4518,6 +4791,10 @@ function openSettings(section = null) {
|
|
|
4518
4791
|
show(section);
|
|
4519
4792
|
}
|
|
4520
4793
|
ui.gear.addEventListener('click', () => openSettings());
|
|
4794
|
+
// Turning MADRE off (public/power.js), last in the bar, after the settings.
|
|
4795
|
+
const powerButton = powerKey('sm-icon-key bar');
|
|
4796
|
+
powerButton.id = 'sm-power';
|
|
4797
|
+
ui.gear.after(powerButton);
|
|
4521
4798
|
|
|
4522
4799
|
function translateStatic() {
|
|
4523
4800
|
document.title = `MADRE · ${t('Home')}`;
|
|
@@ -4527,12 +4804,18 @@ function translateStatic() {
|
|
|
4527
4804
|
ui.bar.open.setAttribute('aria-label', t('Projects on the bar'));
|
|
4528
4805
|
ui.bar.agents.setAttribute('aria-label', t('Agents'));
|
|
4529
4806
|
ui.bar.links.querySelector('span').textContent = t('Connections');
|
|
4807
|
+
document.querySelector('#sm-tb-market span')?.replaceChildren(t('Market'));
|
|
4808
|
+
document.querySelector('#sm-tb-modules span')?.replaceChildren(t('Modules'));
|
|
4530
4809
|
ui.bar.reload.title = t('Reload the page');
|
|
4531
4810
|
ui.bar.reload.setAttribute('aria-label', t('Reload the page'));
|
|
4811
|
+
const addKey = document.getElementById('sm-add-widgets');
|
|
4812
|
+
addKey?.setAttribute('title', t('Add widgets to Home'));
|
|
4813
|
+
addKey?.setAttribute('aria-label', t('Add widgets to Home'));
|
|
4532
4814
|
ui.stop.querySelector('span').textContent = t('Stop all');
|
|
4533
4815
|
ui.macro.querySelector('span').textContent = t('Project map');
|
|
4534
4816
|
ui.gear.title = t('Home settings');
|
|
4535
4817
|
ui.gear.setAttribute('aria-label', t('Home settings'));
|
|
4818
|
+
labelPowerKey(powerButton);
|
|
4536
4819
|
ui.search.placeholder = t('Search a project…');
|
|
4537
4820
|
ui.search.setAttribute('aria-label', t('Search a project…'));
|
|
4538
4821
|
ui.filter.setAttribute('aria-label', t('Show'));
|
|
@@ -4544,9 +4827,9 @@ function translateStatic() {
|
|
|
4544
4827
|
ui.bishop.go.textContent = t('Talk to the Coordinator');
|
|
4545
4828
|
ui.bishop.tab.title = t('Open the Coordinator in a new tab');
|
|
4546
4829
|
ui.bishop.tab.setAttribute('aria-label', t('Open the Coordinator in a new tab'));
|
|
4547
|
-
ui.bishop.about.textContent = t('Your
|
|
4830
|
+
ui.bishop.about.textContent = t('Your Coordinator. It reads across your projects, signs the connections between them and carries an order from one to another.');
|
|
4548
4831
|
ui.work.title.textContent = t('Recent summary');
|
|
4549
|
-
ui.work.again.textContent = t('
|
|
4832
|
+
ui.work.again.textContent = t('Summarize again');
|
|
4550
4833
|
ui.waiting.title.textContent = t('Waiting on you');
|
|
4551
4834
|
ui.linksTileTitle.textContent = t('Connections');
|
|
4552
4835
|
ui.link.textContent = t('Connect projects');
|
|
@@ -4563,6 +4846,20 @@ function translateStatic() {
|
|
|
4563
4846
|
translateStatic();
|
|
4564
4847
|
drawWork();
|
|
4565
4848
|
drawDecisions();
|
|
4849
|
+
// The arrangement kept for every port, before the first full drawing, and again whenever Home
|
|
4850
|
+
// comes back into view (another page may have changed it).
|
|
4851
|
+
const homeArranged = async () => {
|
|
4852
|
+
if (!(await pullHomeLayout())) return;
|
|
4853
|
+
layout = readLayout();
|
|
4854
|
+
pins.saved = null;
|
|
4855
|
+
applyLooks();
|
|
4856
|
+
applyLayout();
|
|
4857
|
+
if (data) { draw(); drawTopbar(); }
|
|
4858
|
+
paintOrderKey();
|
|
4859
|
+
settingsSheet?.paintLooks();
|
|
4860
|
+
};
|
|
4861
|
+
await homeArranged();
|
|
4862
|
+
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') void homeArranged(); });
|
|
4566
4863
|
await refresh();
|
|
4567
4864
|
// Arriving from RELOAD (salas-mac.html put the curtain back): Home has read its projects, so the
|
|
4568
4865
|
// bar fills and the curtain clears.
|
|
@@ -4583,6 +4880,11 @@ void loadDecisions();
|
|
|
4583
4880
|
if (location.hash === '#welcome') { history.replaceState(null, '', location.pathname); welcomed = true; openWizard(); }
|
|
4584
4881
|
// Sent here for the Macroverso (the key in BISHOP's room): straight into it, on a large screen.
|
|
4585
4882
|
if (location.hash === '#macroverso' && data && !ui.macro.hidden) { history.replaceState(null, '', location.pathname); ui.macro.click(); }
|
|
4883
|
+
// Sent here for the Market (a project's MODULES, or a module offered in the conversation): straight in,
|
|
4884
|
+
// on that module's page when one is named.
|
|
4885
|
+
// The Market's addresses: #mercado, #mercado=<module>, #modulos.
|
|
4886
|
+
if (/^#mercado(=|$)/.test(location.hash)) openHomeMarket('shop', decodeURIComponent(location.hash.split('=')[1] ?? '') || null);
|
|
4887
|
+
else if (location.hash === '#modulos') openHomeMarket('mine');
|
|
4586
4888
|
timer = setInterval(() => { if (!document.hidden && !busy.size && !pins.dropping && !pins.drag) void refresh(); }, 4000);
|
|
4587
4889
|
document.addEventListener('visibilitychange', () => { if (!document.hidden) { void refresh(); void loadAgents(); void loadWork(); void loadDecisions(); } });
|
|
4588
4890
|
// The decisions are asked again every few minutes (the route itself asks the rooms no oftener).
|