clavarium 0.2.375-next.1 → 0.2.377-next.1
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/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/server/main.mjs +2 -0
- package/server/rooms.mjs +49 -0
- package/server/settingswords.mjs +9 -6
- package/server/tethers.mjs +10 -2
- package/web/app.js +154 -150
- package/web/index.html +4 -19
- package/web/kit.js +61 -5
- package/web/links.js +2 -2
- package/web/style.css +16 -5
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clavarium",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.377-next.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "clavarium",
|
|
9
|
-
"version": "0.2.
|
|
9
|
+
"version": "0.2.377-next.1",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/package.json
CHANGED
package/server/main.mjs
CHANGED
|
@@ -718,6 +718,8 @@ export async function startHome({ port, host, dataDir, mntDir = dataDir, gitRese
|
|
|
718
718
|
} });
|
|
719
719
|
rooms.boxesBound(boxes.all());
|
|
720
720
|
rooms.refresh();
|
|
721
|
+
/* A Tether call held for an AI at work for a room: said in that room too, closed there once decided (tethers.mjs). */
|
|
722
|
+
tethers.rooms = { held: (name, event) => rooms.held(name, event), closed: (ids, event) => rooms.closed(ids, event) };
|
|
721
723
|
/* FEATURES (features.mjs): read from what the modules say, never a secret's value. The remote screen and the
|
|
722
724
|
catalogue are slow to ask: kept a while, and never waited for long. */
|
|
723
725
|
const screenNow = cached(() => meshcentral.status().catch(() => null), 60_000);
|
package/server/rooms.mjs
CHANGED
|
@@ -30,6 +30,11 @@ export const HOPS_MAX = 8;
|
|
|
30
30
|
/* How long an AI given a message is said to be thinking, at most (it may never answer). */
|
|
31
31
|
export const THINKING_MS = 10 * 60_000;
|
|
32
32
|
const KINDS = ['person', 'ai', 'box'];
|
|
33
|
+
/* A ROOM'S OWN LINES (a Tether call held for one of its AIs, and what closes it): the kinds, their level, at most kept. */
|
|
34
|
+
const LINE_LEVELS = Object.freeze({ 'tether-demande': 'needs-you', 'tether-done': 'trace' });
|
|
35
|
+
const LEVELS = ['needs-you', 'news', 'trace'];
|
|
36
|
+
const LINE_ACTION = /^[a-z]{1,20}(:[a-z0-9][a-z0-9._-]{0,63})?$/;
|
|
37
|
+
export const ROOM_LINES_MAX = 50;
|
|
33
38
|
/* A FILE THE PERSON ATTACHES IN A ROOM (06-11: a room carries what the chat carries): kept in the room's own folder
|
|
34
39
|
(rooms/<id>/attached/<uuid>/<name>), named in its thread by ROOM_FILE's path; a copy goes to each AI the message
|
|
35
40
|
goes to, attached in its own chat as the person attaches one there (talk.attach) -- never to the others. */
|
|
@@ -56,6 +61,11 @@ const nameOf = (v) => String(v ?? '').replace(/\s+/g, ' ').trim().slice(0, ROOM_
|
|
|
56
61
|
a secret never passes from one AI to another through a room. */
|
|
57
62
|
export const masked = (text) => { const str = String(text ?? ''); const hits = scan(str); if (!hits.length) return str; let out = ''; let last = 0; for (const h of hits) { out += str.slice(last, h.start) + '••••••'; last = h.end; } return out + str.slice(last); };
|
|
58
63
|
|
|
64
|
+
/* Every text of a line masked (a card's title, facts, quote...: plain data, words of the screen and their values) --
|
|
65
|
+
never a button's id nor a word's key. */
|
|
66
|
+
const maskedAll = (v, depth = 0) => (typeof v === 'string' ? masked(v) : depth > 8 || v === null || typeof v !== 'object' ? v
|
|
67
|
+
: Array.isArray(v) ? v.map((x) => maskedAll(x, depth + 1)) : Object.fromEntries(Object.entries(v).map(([k, x]) => [k, k === 'id' || k === 'k' ? x : maskedAll(x, depth + 1)])));
|
|
68
|
+
|
|
59
69
|
/* A sign as the cards draw it (web/kit.js hueOf, initialOf): the same name, the same letter and colour. */
|
|
60
70
|
export const lookOf = (name) => { const text = String(name ?? ''); let h = 0; for (const ch of text) h = (h * 31 + ch.codePointAt(0)) >>> 0; return { initial: ([...text.trim()][0] ?? '?').toUpperCase(), hue: h % 360 }; };
|
|
61
71
|
|
|
@@ -171,6 +181,10 @@ export class Rooms {
|
|
|
171
181
|
if (options?.seen !== false) { const list = this.#all(); const r = list.find((x) => x.id === id); if (r) { r.seen = this.#now().toISOString(); this.#file.set(list); } }
|
|
172
182
|
const thinking = this.#thinkingOf(id, members); const doing = this.#doingOf(id, members);
|
|
173
183
|
const messages = (read.messages ?? []).map((m) => this.#opened(m, members)).map((m) => (typeof m.text === 'string' && m.text.includes('@{') ? { ...m, text: named(m.text) } : m));
|
|
184
|
+
/* Its lines (a Tether call of one of its AIs, held): among its messages, where they happened -- those of the time read. */
|
|
185
|
+
const from = read.window?.more > 0 ? String(messages[0]?.at ?? '') : '';
|
|
186
|
+
const lines = (Array.isArray(room.lines) ? room.lines : []).filter((l) => String(l.at) >= from).map((l) => ({ from: 'system', kind: 'system', choices: [], ...l }));
|
|
187
|
+
if (lines.length) { messages.push(...lines); messages.sort((a, b) => String(a.at).localeCompare(String(b.at))); }
|
|
174
188
|
/* The files of the thread (shown, cited): each as the message that has it opens it; one nobody opens here, not listed. */
|
|
175
189
|
const opens = new Map(); for (const m of messages) for (const f of [...(m.refs ?? []), ...(m.files ?? []), ...(m.open ? [m] : [])]) if (f.path && f.open) opens.set(f.path, f.open);
|
|
176
190
|
const files = Array.isArray(read.files) ? read.files.map((f) => (String(f.open ?? '').startsWith(CITE) ? (opens.has(f.path) ? { ...f, open: opens.get(f.path) } : null) : f)).filter(Boolean) : read.files;
|
|
@@ -387,6 +401,41 @@ export class Rooms {
|
|
|
387
401
|
return copies;
|
|
388
402
|
}
|
|
389
403
|
|
|
404
|
+
/* A TETHER CALL HELD FOR AN AI AT WORK FOR A ROOM (07-11): an AI given a message of the room and not done with it
|
|
405
|
+
asks the person to allow a call -- its card goes in that room's thread too (and each room it works for), the
|
|
406
|
+
same line its own chat has (talk.event's shape: Accept / Refuse, Parley's request card), named by the AI.
|
|
407
|
+
Kept apart from the thread (the room's `lines`, ROOM_LINES_MAX), read among its messages; masked as any line
|
|
408
|
+
of a room. Gives the rooms it went to: what is said once it is decided goes there (closed). */
|
|
409
|
+
held(conversation, event) {
|
|
410
|
+
const list = this.#all(); const now = Date.now(); const ids = [];
|
|
411
|
+
for (const r of list) {
|
|
412
|
+
const on = this.#thinking.get(r.id);
|
|
413
|
+
const me = on && this.#shown(r).find((m) => m.kind === 'ai' && m.ref === conversation && on.has(m.id) && now - on.get(m.id).at < THINKING_MS);
|
|
414
|
+
if (!me) continue;
|
|
415
|
+
const request = event?.request && typeof event.request === 'object' ? { ...event.request, sub: [me.name, ...(Array.isArray(event.request.sub) ? event.request.sub : event.request.sub ? [event.request.sub] : [])].slice(0, 4) } : undefined;
|
|
416
|
+
const text = event?.text && typeof event.text === 'object' ? event.text : `${me.name}: ${String(event?.text ?? '')}`;
|
|
417
|
+
if (this.#line(r, { ...event, text, ...(request ? { request } : {}) })) ids.push(r.id);
|
|
418
|
+
}
|
|
419
|
+
if (ids.length) this.#file.set(list);
|
|
420
|
+
return ids;
|
|
421
|
+
}
|
|
422
|
+
/* What closes it (a 'tether-done' carrying its id), in the rooms its card went to. */
|
|
423
|
+
closed(ids, event) {
|
|
424
|
+
const list = this.#all(); let any = false;
|
|
425
|
+
for (const r of list) if ((Array.isArray(ids) ? ids : []).includes(r.id) && this.#line(r, event)) any = true;
|
|
426
|
+
if (any) this.#file.set(list);
|
|
427
|
+
}
|
|
428
|
+
#line(room, { kind, text, level, tone, topic, brief, actions, request, outcome } = {}) {
|
|
429
|
+
if (!Object.hasOwn(LINE_LEVELS, kind)) return null;
|
|
430
|
+
const kept = { id: randomUUID(), at: this.#now().toISOString(), event: kind, text: maskedAll(text), level: LEVELS.includes(level) ? level : LINE_LEVELS[kind],
|
|
431
|
+
...(tone === 'warn' || tone === 'bad' ? { tone } : {}), ...(topic ? { topic: maskedAll(topic) } : {}), ...(typeof brief === 'string' && brief ? { brief: brief.slice(0, 60) } : {}),
|
|
432
|
+
actions: (Array.isArray(actions) ? actions : []).filter((a) => typeof a === 'string' && LINE_ACTION.test(a)).slice(0, 4),
|
|
433
|
+
...(request && typeof request === 'object' && JSON.stringify(request).length <= 8000 ? { request: maskedAll(request) } : {}),
|
|
434
|
+
...(typeof outcome === 'string' && outcome ? { outcome: outcome.slice(0, 20) } : {}) };
|
|
435
|
+
room.lines = [...(Array.isArray(room.lines) ? room.lines : []), kept].slice(-ROOM_LINES_MAX);
|
|
436
|
+
return kept;
|
|
437
|
+
}
|
|
438
|
+
|
|
390
439
|
/* Each conversation told the rooms it is in, and who is there. */
|
|
391
440
|
#tellConversations(list) {
|
|
392
441
|
const shown = list.map((r) => ({ ...r, members: this.#shown(r) }));
|
package/server/settingswords.mjs
CHANGED
|
@@ -40,15 +40,15 @@ export const SETTINGS_WORDS_FR = {
|
|
|
40
40
|
'set-backup-now': 'Exporter · importer', 'set-git-key': '1 clé git', 'set-git-keys': '{n} clés git', 'set-restart': 'redémarrer',
|
|
41
41
|
'set-logs-now': 'Clavarium, machines, système',
|
|
42
42
|
'set-dict-backup': 'Secours en service', 'set-dict-expired': 'Clé expirée', 'set-dict-with-backup': 'avec secours', 'set-dict-server': 'réglée sur le serveur',
|
|
43
|
-
'set-
|
|
43
|
+
'set-offline': 'hors ligne',
|
|
44
44
|
'set-device': '1 appareil', 'set-devices': '{n} appareils', 'set-no-device': 'Aucun appareil', 'set-chat': 'Chat',
|
|
45
45
|
'set-wakeups-on': '{on} sur {n} actifs', 'set-asleep': 'En veille', 'set-not-looked': 'Pas encore regardée',
|
|
46
46
|
'set-running-well': 'En marche · rien d\'anormal', 'set-conv-advanced': 'Archiver · remettre à neuf · supprimer',
|
|
47
47
|
/* New asset, one box (05-11, step 3): one verb per gesture -- Créer, Attacher, Terminé. */
|
|
48
|
-
'set-na-new': 'Nouvel asset', 'set-na-
|
|
48
|
+
'set-na-new': 'Nouvel asset', 'set-na-there': 'Déjà là', 'set-na-or-new': 'ou un nouveau',
|
|
49
49
|
'set-na-step-kind': 'Type', 'set-na-step-set': 'Réglage', 'set-na-step-made': 'Créé', 'set-na-back': 'Retour',
|
|
50
|
-
'set-na-will': 'Sera
|
|
51
|
-
'set-na-made': '{name} créé', 'set-na-made-to': '{name} créé et
|
|
50
|
+
'set-na-will': 'Sera donné à {name}', 'set-na-level': 'Ce qu\'il pourra faire',
|
|
51
|
+
'set-na-made': '{name} créé', 'set-na-made-to': '{name} créé et donné à {to}', 'set-na-another': 'En créer un autre', 'set-na-done': 'Terminé',
|
|
52
52
|
'set-na-create': 'Créer', 'set-na-new-key': '+ Nouvelle clé…',
|
|
53
53
|
/* The top bar's way back (06-11, kit.js TopBar). */
|
|
54
54
|
'set-back': 'Retour', 'set-back-convs': 'Conversations',
|
|
@@ -160,9 +160,12 @@ export const SETTINGS_WORDS_FR = {
|
|
|
160
160
|
'set-rt-mcp-hint-propose': 'Tous les outils, chaque modification approuvée', 'set-rt-mcp-hint-free': 'Tous les outils ; suppressions demandées',
|
|
161
161
|
'set-rt-mcp-hint-frame': 'Votre choix pour chaque sorte d\'outil',
|
|
162
162
|
'set-cx-ssh': 'machine', 'set-cx-computer': 'ordinateur', 'set-cx-browser': 'navigateur', 'set-cx-screen': 'écran', 'set-cx-device': 'appareil',
|
|
163
|
-
'set-acc-share': 'Partager avec un appareil…', 'set-acc-in-assets': 'Ouvrir dans Assets',
|
|
163
|
+
'set-acc-share': 'Partager avec un appareil…', 'set-acc-in-assets': 'Ouvrir dans Assets',
|
|
164
164
|
'set-acc-used-1': 'Utilisé par 1 conversation : mettez « Aucun »', 'set-acc-used-n': 'Utilisé par {n} conversations : mettez « Aucun »',
|
|
165
|
-
|
|
165
|
+
/* Give… (07-11, assets step 4): in a conversation's tab; Settings › Access one line; a secret's next start. */
|
|
166
|
+
'set-give-go': 'Donner', 'set-give-all': 'Elle a déjà chaque asset.', 'set-group-none-all': 'Tout à Aucun',
|
|
167
|
+
'set-acc-assets-1': '1 asset · voir', 'set-acc-assets-n': '{n} assets · voir', 'set-acc-asset-1': '1 asset', 'set-acc-asset-n': '{n} assets',
|
|
168
|
+
'set-secret-next-on': '{name} : remis à son outil au prochain démarrage', 'set-secret-next-off': '{name} : plus remis dès son prochain démarrage',
|
|
166
169
|
'set-files-list': 'Liste', 'set-files-icons': 'Icônes', 'set-files-empty': 'Ce dossier est vide.',
|
|
167
170
|
'set-files-read-only': 'Lecture seule : rien ne peut y être changé.',
|
|
168
171
|
'set-this-conv': 'Cette conversation · {name}', 'set-ask-first': 'Demander avant d\'agir',
|
package/server/tethers.mjs
CHANGED
|
@@ -103,6 +103,9 @@ export class Tethers {
|
|
|
103
103
|
/* Set by the server once the thread is ready (main.mjs): (name, event) => talk.event(...).
|
|
104
104
|
A call waiting for the owner is said in its conversation's thread when there is one. */
|
|
105
105
|
announce = null;
|
|
106
|
+
/* Set by the server too (main.mjs): the rooms (rooms.mjs) -- a call held for an AI at work for a room is said in
|
|
107
|
+
that room as well (held: the rooms it went to), and closed there (closed) when it is decided. */
|
|
108
|
+
rooms = null;
|
|
106
109
|
|
|
107
110
|
/* `resources`: Resources (workFolder). `conversations()`: the names of the conversations.
|
|
108
111
|
`secrets`: the vault, read by name for a connector only (reveal). `dockets`: { on(name), post(name,
|
|
@@ -152,7 +155,10 @@ export class Tethers {
|
|
|
152
155
|
/* `request`: the card Parley draws (demandes.mjs); the text and its buttons stay for an older screen. */
|
|
153
156
|
let request;
|
|
154
157
|
try { request = demandeView(r); } catch { request = undefined; }
|
|
155
|
-
|
|
158
|
+
const event = { kind: 'tether-demande', text: text || (r.verb ?? 'A Tether call'), topic: TOPIC_REQUEST, tone: 'warn', actions: [`allow:${r.id}`, `deny:${r.id}`], ...(request ? { request } : {}) };
|
|
159
|
+
try { this.announce(name, event); } catch { /* its chat is off */ }
|
|
160
|
+
/* At work for a room (a message of it not yet answered): the same card there, decided there as here. */
|
|
161
|
+
try { const ids = this.rooms?.held(name, event); if (Array.isArray(ids) && ids.length) this.#seen.get(r.id).rooms = ids; } catch { /* its rooms' */ }
|
|
156
162
|
}
|
|
157
163
|
/* An approved call only the owner's browser can do (a device to flash): said once, with where to do it. */
|
|
158
164
|
let jobs = [];
|
|
@@ -178,7 +184,9 @@ export class Tethers {
|
|
|
178
184
|
const accepted = outcome === 'yes' || outcome === 'always';
|
|
179
185
|
if (accepted) { this.#acting.push({ id, ...info }); if (this.#acting.length > ACTING_MAX) this.#acting.shift(); }
|
|
180
186
|
const text = endedLine(info.verb, outcome);
|
|
181
|
-
|
|
187
|
+
const done = { kind: 'tether-done', text, topic: TOPIC_REQUEST, brief: id, ...(outcome ? { outcome } : {}) };
|
|
188
|
+
try { this.announce(info.name, done); } catch { /* its chat is off */ }
|
|
189
|
+
if (info.rooms) try { this.rooms?.closed(info.rooms, done); } catch { /* its rooms' */ }
|
|
182
190
|
}
|
|
183
191
|
for (const id of [...this.#ended.keys()]) if (!this.#seen.has(id)) this.#ended.delete(id);
|
|
184
192
|
}
|
package/web/app.js
CHANGED
|
@@ -2137,7 +2137,6 @@
|
|
|
2137
2137
|
void offerVoice();
|
|
2138
2138
|
|
|
2139
2139
|
/* ---------- folders ---------- */
|
|
2140
|
-
const LEVELS = [['none', 'Off'], ['read', 'Read'], ['write', 'Write']];
|
|
2141
2140
|
/* THE BAR (G): Chat | Terminal | Assets N, then its settings. What was
|
|
2142
2141
|
given to it is counted on "Assets", and shown there. */
|
|
2143
2142
|
let assetsShown = false; let roomPick = '';
|
|
@@ -2196,7 +2195,7 @@
|
|
|
2196
2195
|
const tabNow = () => (assetsShown ? 'assets' : listsShown ? 'lists' : panelShown ? 'settings' : simple ? 'chat' : 'terminal');
|
|
2197
2196
|
const TAB_WORD = { chat: 'Chat', terminal: 'Terminal', assets: 'Assets', get lists() { return LISTS_WORD.many; }, settings: 'Settings' };
|
|
2198
2197
|
function openPanelZone() { closeRoomLists(); if (panelShown) return; panelShown = true; $('panel').hidden = false; paintViews(); }
|
|
2199
|
-
function closePanelZone() { if (!panelShown) return; panelShown = false; $('panel').hidden = true;
|
|
2198
|
+
function closePanelZone() { if (!panelShown) return; panelShown = false; $('panel').hidden = true; paintViews(); }
|
|
2200
2199
|
function showTab(which, pick) {
|
|
2201
2200
|
if (which === 'assets') { closePanelZone(); void openRoomAssets(pick); return; }
|
|
2202
2201
|
if (which === 'settings') { closeRoomAssets(); openPanelZone(); return; }
|
|
@@ -2391,65 +2390,11 @@
|
|
|
2391
2390
|
async function paintPanel() {
|
|
2392
2391
|
const here = conversations.find((x) => x.name === current);
|
|
2393
2392
|
$('panel-title').textContent = uiSaid('set-this-conv', 'This conversation - {name}', { name: current });
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
const
|
|
2397
|
-
|
|
2398
|
-
for (const resource of all.filter((r) => mine.has(r.name))) {
|
|
2399
|
-
const kind = kindOf(resource);
|
|
2400
|
-
const row = document.createElement('div'); row.className = 'folder'; row.dataset.name = resource.name;
|
|
2401
|
-
const label = document.createElement('b'); label.textContent = resource.name;
|
|
2402
|
-
const about = document.createElement('small'); about.className = 'hint'; about.textContent = ` ${uiSaid(`set-as-kind-${kind}`, KIND_WORD[kind] ?? kind)}`; label.append(about);
|
|
2403
|
-
const levels = kind === 'mailbox' ? fixedRight('mailbox')
|
|
2404
|
-
: modes([['read', uiSaid('set-rt-pick-read', 'Read')], ['write', uiSaid('set-rt-pick-write', 'Write')]], mine.get(resource.name), async (access) => { await attachTo(current, resource.name, access); await paintPanel(); }, `Access of this conversation to ${resource.name}`);
|
|
2405
|
-
row.append(label, levels);
|
|
2406
|
-
if (kind === 'git') {
|
|
2407
|
-
const review = document.createElement('button'); review.type = 'button'; review.textContent = 'Review & send';
|
|
2408
|
-
review.addEventListener('click', () => { void openGit(resource.name); });
|
|
2409
|
-
row.append(review);
|
|
2410
|
-
}
|
|
2411
|
-
/* Detached, not removed: it stays in Assets. */
|
|
2412
|
-
const detach = document.createElement('button'); detach.type = 'button'; detach.textContent = uiSaid('set-acc-detach', 'Detach'); detach.setAttribute('aria-label', `Detach ${resource.name} from this conversation`);
|
|
2413
|
-
detach.addEventListener('click', async () => { detach.disabled = true; detach.textContent = 'Detaching...'; await attachTo(current, resource.name, 'none'); await paintPanel(); });
|
|
2414
|
-
row.append(detach);
|
|
2415
|
-
/* Right under the access it depends on: read-only, the agent has nothing of its own to send. */
|
|
2416
|
-
if (kind === 'git' && mine.get(resource.name) === 'write' && !DEVICE) row.append(autoSending(current, resource.name));
|
|
2417
|
-
rows.append(row);
|
|
2418
|
-
}
|
|
2419
|
-
$('panel-empty').hidden = mine.size > 0;
|
|
2420
|
-
/* The secrets its tool is handed. */
|
|
2421
|
-
const secrets = DEVICE ? [] : await (await fetch(`${API}/secrets`)).json().catch(() => []);
|
|
2422
|
-
const handed = $('access-secrets'); handed.replaceChildren();
|
|
2423
|
-
for (const secret of secrets.filter((x) => x.conversations.includes(current))) {
|
|
2424
|
-
const row = document.createElement('div'); row.className = 'folder'; row.dataset.name = secret.name;
|
|
2425
|
-
const label = document.createElement('b'); label.textContent = secret.name;
|
|
2426
|
-
const off = document.createElement('button'); off.type = 'button'; off.textContent = uiSaid('set-acc-detach', 'Detach'); off.setAttribute('aria-label', `Stop handing ${secret.name} to this conversation`);
|
|
2427
|
-
off.addEventListener('click', async () => {
|
|
2428
|
-
await handTo(current, secret.name, false); await paintPanel();
|
|
2429
|
-
$('secret-note').textContent = `${secret.name} will no longer be handed to the tool the next time it starts (leave the tool, then "Start it again").`;
|
|
2430
|
-
});
|
|
2431
|
-
row.append(label, fixedUse(), off); handed.append(row);
|
|
2432
|
-
}
|
|
2433
|
-
$('access-secrets-empty').hidden = handed.children.length > 0;
|
|
2393
|
+
/* ITS ASSETS (07-11, assets step 4): one line, « N assets · see », that opens its Assets tab -- what it was given
|
|
2394
|
+
lives there only (given, its right set, taken back: « None »). Its sign-in mailbox (not an asset) stays below. */
|
|
2395
|
+
const n = givenCount(here);
|
|
2396
|
+
$('access-assets').textContent = uiSaid(n === 1 ? 'set-acc-assets-1' : 'set-acc-assets-n', n === 1 ? '1 asset · see' : '{n} assets · see', { n });
|
|
2434
2397
|
paintConvStates(here);
|
|
2435
|
-
/* What can still be attached, for the picker. */
|
|
2436
|
-
const picker = $('attach-rows'); picker.replaceChildren();
|
|
2437
|
-
const offer = (name, kind, buttons) => {
|
|
2438
|
-
const row = document.createElement('div'); row.className = 'folder'; row.dataset.name = name;
|
|
2439
|
-
const label = document.createElement('b'); label.textContent = name;
|
|
2440
|
-
const about = document.createElement('small'); about.className = 'hint'; about.textContent = ` ${kind}`; label.append(about);
|
|
2441
|
-
row.append(label, ...buttons); picker.append(row);
|
|
2442
|
-
};
|
|
2443
|
-
const act = (text, run) => { const b = document.createElement('button'); b.type = 'button'; b.textContent = text; b.addEventListener('click', async () => { b.disabled = true; b.textContent = '...'; await run(); await paintPanel(); }); return b; };
|
|
2444
|
-
for (const resource of all.filter((r) => !mine.has(r.name))) {
|
|
2445
|
-
const kind = kindOf(resource);
|
|
2446
|
-
offer(resource.name, KIND_WORD[kind], kind === 'mailbox' ? [act('Read', () => attachTo(current, resource.name, 'read'))]
|
|
2447
|
-
: [act('Read', () => attachTo(current, resource.name, 'read')), act('Write', () => attachTo(current, resource.name, 'write'))]);
|
|
2448
|
-
}
|
|
2449
|
-
for (const secret of secrets.filter((x) => !x.conversations.includes(current))) {
|
|
2450
|
-
offer(secret.name, 'secret', [act('Use', async () => { await handTo(current, secret.name, true); $('secret-note').textContent = `${secret.name} will be handed to the tool the next time it starts (leave the tool, then "Start it again").`; })]);
|
|
2451
|
-
}
|
|
2452
|
-
$('attach-empty').hidden = picker.children.length > 0;
|
|
2453
2398
|
/* Where the tool works, and the way to the other place. */
|
|
2454
2399
|
const kept = here?.work === 'kept';
|
|
2455
2400
|
$('work-box').hidden = DEVICE || !here;
|
|
@@ -2860,8 +2805,8 @@
|
|
|
2860
2805
|
const join = (...parts) => parts.filter(Boolean).join(' · ');
|
|
2861
2806
|
chapterNow('general', join(here.title || here.name, here.group, here.toolLabel ?? here.tool));
|
|
2862
2807
|
chapterNow('machine', join(sizeWords(here.memoryMb), uiSaid(here.cpus === 1 ? 'set-core' : 'set-cores', here.cpus === 1 ? '1 core' : '{n} cores', { n: here.cpus ?? 1 }), here.diskGb ? `${here.diskGb} GB` : ''));
|
|
2863
|
-
const
|
|
2864
|
-
chapterNow('access', join(uiSaid(
|
|
2808
|
+
const given = givenCount(here);
|
|
2809
|
+
chapterNow('access', join(uiSaid(given === 1 ? 'set-acc-asset-1' : 'set-acc-asset-n', given === 1 ? '1 asset' : '{n} assets', { n: given }), here.offline ? uiSaid('set-offline', 'offline') : ''));
|
|
2865
2810
|
const shared = $('share-here').children.length;
|
|
2866
2811
|
chapterNow('sharing', shared ? uiSaid(shared === 1 ? 'set-device' : 'set-devices', shared === 1 ? '1 device' : '{n} devices', { n: shared }) : uiSaid('set-no-device', 'No device'));
|
|
2867
2812
|
const chatOn = levelsWord('talk-on');
|
|
@@ -3172,7 +3117,7 @@
|
|
|
3172
3117
|
/* A message attached to a card (#180), from its menu: its id, as the card opens it again. */
|
|
3173
3118
|
...(!DEVICE ? { card: (m) => attachToCard({ kind: 'message', ref: m.id, title: String(m.short || m.text || '').replace(/\s+/g, ' ').trim().slice(0, 80) }) } : {}),
|
|
3174
3119
|
/* The buttons of a line of the machine: what this screen may do. */
|
|
3175
|
-
act: (id) => { if (id === 'seen-done') return; if (id.startsWith('salon:')) { void openSalon(id.slice('salon:'.length)); return; } if (id === 'seen') void markSeen(); else if (id === 'memory') void openMemory(); else if (id === 'disk') void openDisk(); else if (id === 'room') seeTerminal(); else if (id === 'restart') void restartMachine(false); else if (id === 'terminal') seeTerminal(); else if (
|
|
3120
|
+
act: (id) => { if (id === 'seen-done') return; if (id.startsWith('salon:')) { void openSalon(id.slice('salon:'.length)); return; } if (id === 'seen') void markSeen(); else if (id === 'memory') void openMemory(); else if (id === 'disk') void openDisk(); else if (id === 'room') seeTerminal(); else if (id === 'restart') void restartMachine(false); else if (id === 'terminal') seeTerminal(); else if (tetherAct(id, () => thread?.refresh?.())) return; else if (id.startsWith('flash:')) void flashFromThread(id.slice('flash:'.length)); else if (id.startsWith('review:')) void openGit(id.slice('review:'.length)); else openCommits(id); },
|
|
3176
3121
|
/* What it opens over the thread (a picture, its media, a search...) is a step of this page's: one Back closes it. */
|
|
3177
3122
|
back: { open: (name, close) => nav.over(`chat-${name}`, () => close(), { stack: true }), closed: (step) => { if (step) nav.drop(step); } },
|
|
3178
3123
|
/* Files attached to a message: wherever the chat may be written in. */
|
|
@@ -3311,6 +3256,8 @@
|
|
|
3311
3256
|
/* A Tether call an agent asked the owner to allow, offered right in the thread: allow:<id> / deny:<id>. */
|
|
3312
3257
|
const DEMANDE = /^(allow|deny|always|flash):[0-9a-f-]{36}$/;
|
|
3313
3258
|
const DECISION = { allow: 'yes', deny: 'no', always: 'always' };
|
|
3259
|
+
/* Accept / Refuse a Tether call from a line that asks (a conversation's chat, a room): decided as the owner, then `after`. */
|
|
3260
|
+
const tetherAct = (id, after) => { const m = /^(allow|deny|always):(.+)$/.exec(String(id)); if (!m) return false; void decideTether(m[2], DECISION[m[1]], after); return true; };
|
|
3314
3261
|
const COMMIT = /^commit:([a-z0-9][a-z0-9-]*)\.([0-9a-f]{7,40})$/;
|
|
3315
3262
|
function openCommits(id) {
|
|
3316
3263
|
if (DEVICE) return;
|
|
@@ -3822,7 +3769,6 @@
|
|
|
3822
3769
|
try { await post(`${API}/conversations/${encodeURIComponent(current)}/answerer/resume`, {}); await refresh(); await paintAnswerer(); }
|
|
3823
3770
|
catch (error) { $('panel-error').textContent = error.message; $('panel-error').hidden = false; }
|
|
3824
3771
|
});
|
|
3825
|
-
const fixedUse = () => { const s = document.createElement('span'); s.className = 'hint'; s.textContent = uiSaid('set-rt-pick-use', 'Use'); return s; };
|
|
3826
3772
|
for (const b of $('network').children) {
|
|
3827
3773
|
b.addEventListener('click', async () => {
|
|
3828
3774
|
$('panel-error').hidden = true;
|
|
@@ -3832,64 +3778,21 @@
|
|
|
3832
3778
|
await paintPanel();
|
|
3833
3779
|
});
|
|
3834
3780
|
}
|
|
3835
|
-
/* `
|
|
3836
|
-
async function openAccess(
|
|
3781
|
+
/* Its settings, on the Access chapter. `giving`: « Give… » instead, in its Assets tab (07-11, assets step 4). */
|
|
3782
|
+
async function openAccess(giving = false) {
|
|
3837
3783
|
if (DEVICE || !current) return;
|
|
3838
3784
|
if (place !== 'conversations') leavePlace();
|
|
3785
|
+
if (giving) { openGive(); return; }
|
|
3839
3786
|
goTab('settings');
|
|
3840
|
-
/* What
|
|
3787
|
+
/* What it has may have changed from another screen: asked again before it is shown. */
|
|
3841
3788
|
await refresh();
|
|
3842
3789
|
await paintPanel();
|
|
3843
|
-
$('attach-picker').hidden = !picking;
|
|
3844
|
-
if (picking) openAttach();
|
|
3845
3790
|
/* The folders button shows the folders: their chapter opened, in its tab. */
|
|
3846
|
-
openChapter('access'
|
|
3847
|
-
if (picking) $('attach-picker').scrollIntoView({ block: 'nearest' });
|
|
3791
|
+
openChapter('access');
|
|
3848
3792
|
}
|
|
3849
3793
|
$('folders').addEventListener('click', () => { void openAccess(false); });
|
|
3850
|
-
/*
|
|
3851
|
-
|
|
3852
|
-
let attachNew = { kind: '', level: 'write' };
|
|
3853
|
-
function openAttach() {
|
|
3854
|
-
attachNew = { kind: '', level: 'write' };
|
|
3855
|
-
if (justMade?.to) justMade = null;
|
|
3856
|
-
$('attach-error').hidden = true;
|
|
3857
|
-
paintAttach();
|
|
3858
|
-
}
|
|
3859
|
-
function closeAttach() {
|
|
3860
|
-
$('attach-picker').hidden = true; $('attach-close').hidden = false;
|
|
3861
|
-
attachNew.kind = ''; if (justMade?.to) justMade = null;
|
|
3862
|
-
for (const f of $('attach-box').querySelectorAll('.as-form')) $('assets-forms').append(f);
|
|
3863
|
-
}
|
|
3864
|
-
function paintAttach() {
|
|
3865
|
-
const box = $('attach-box');
|
|
3866
|
-
for (const f of box.querySelectorAll('.as-form')) $('assets-forms').append(f);
|
|
3867
|
-
$('attach-park').append($('attach-there'));
|
|
3868
|
-
const who = shownName(conversations.find((c) => c.name === current)) || current;
|
|
3869
|
-
const made = justMade?.to === current ? justMade : null;
|
|
3870
|
-
const kind = made ? '' : attachNew.kind;
|
|
3871
|
-
/* At its ending, one Done: the box's own. */
|
|
3872
|
-
$('attach-close').hidden = Boolean(made) || (kind === 'box' && Boolean(boxFlow?.made));
|
|
3873
|
-
/* A secret is used or not, a voice box speaks there: no level to choose. */
|
|
3874
|
-
const level = kind && !['secret', 'box'].includes(kind) ? { options: [{ value: 'read', label: uiSaid('set-na-read', 'Read') }, { value: 'write', label: uiSaid('set-na-write', 'Write') }], value: attachNew.level, label: uiSaid('set-na-level', 'What it may do'), onChange: (v) => { attachNew.level = v; } } : null;
|
|
3875
|
-
box.replaceChildren(newAssetBox({
|
|
3876
|
-
kind, existing: $('attach-there'), title: '',
|
|
3877
|
-
context: made ? null : { line: uiSaid('set-na-will', 'Will be attached to {name}', { name: who }), level },
|
|
3878
|
-
made: made ? { said: uiSaid('set-na-made-to', '{name} created and attached to {to}', { name: made.name, to: who }), note: made.note } : null,
|
|
3879
|
-
onKind: (k) => {
|
|
3880
|
-
attachNew.kind = k; justMade = null; $('attach-error').hidden = true; paintAttach(); focusFirst(box);
|
|
3881
|
-
/* Its keys, read here too: Assets may not have been opened yet. */
|
|
3882
|
-
if (k === 'git') void getJson('/git-keys', []).then((keys) => paintKeys(keys));
|
|
3883
|
-
},
|
|
3884
|
-
onBack: () => { attachNew.kind = ''; $('attach-error').hidden = true; paintAttach(); },
|
|
3885
|
-
onAnother: () => { attachNew.kind = made.kind; justMade = null; paintAttach(); focusFirst(box); },
|
|
3886
|
-
onDone: () => { closeAttach(); },
|
|
3887
|
-
boxHost: { where: current, repaint: () => paintAttach(), close: () => closeAttach() },
|
|
3888
|
-
}));
|
|
3889
|
-
}
|
|
3890
|
-
$('attach-open').addEventListener('click', () => { openChapter('access'); $('attach-picker').hidden = false; openAttach(); $('attach-picker').scrollIntoView({ block: 'nearest' }); });
|
|
3891
|
-
$('attach-close').addEventListener('click', () => { closeAttach(); });
|
|
3892
|
-
$('attach-manage').addEventListener('click', () => { void openAssets(); });
|
|
3794
|
+
/* « N assets · see »: its Assets tab. */
|
|
3795
|
+
$('access-assets').addEventListener('click', () => { goTab('assets'); });
|
|
3893
3796
|
|
|
3894
3797
|
/* ---------- ASSETS: one design, two pages (G and H) ----------
|
|
3895
3798
|
The server's (a place, beside Access and Settings): everything kept
|
|
@@ -4043,11 +3946,6 @@
|
|
|
4043
3946
|
/* Once a finger's drag has begun, the page must not scroll under it. */
|
|
4044
3947
|
holder.addEventListener('touchmove', (e) => { if (drag?.on) e.preventDefault(); }, { passive: false });
|
|
4045
3948
|
}
|
|
4046
|
-
/* The level buttons an asset takes, by its type (same sets as its own detail). */
|
|
4047
|
-
const levelOptionsFor = (item) => (item.target ? [['none', 'Off'], ['watch', 'Watch'], ['propose', 'Propose'], ['frame', 'Frame'], ['free', 'Free']]
|
|
4048
|
-
: item.kind === 'secret' ? [['off', 'Off'], ['use', 'Use']]
|
|
4049
|
-
: item.kind === 'mailbox' ? [['none', 'Off'], ['read', 'Read']]
|
|
4050
|
-
: [['none', 'Off'], ['read', 'Read'], ['write', 'Write']]);
|
|
4051
3949
|
/* The shortcut "give all": each member at the lightest real level for its type. */
|
|
4052
3950
|
const giveValue = (item, on) => (!on ? (item.target || item.kind !== 'secret' ? 'none' : 'off')
|
|
4053
3951
|
: item.target ? 'watch' : item.kind === 'secret' ? 'use' : 'read');
|
|
@@ -4132,14 +4030,17 @@
|
|
|
4132
4030
|
const block = el('div', 'as-give-conv');
|
|
4133
4031
|
const head = el('div', 'as-give-head');
|
|
4134
4032
|
const giveAll = el('button', 'as-mini', 'Give all'); giveAll.type = 'button'; giveAll.addEventListener('click', () => setAll(c, true));
|
|
4135
|
-
const clearAll = el('button', 'as-mini', '
|
|
4033
|
+
const clearAll = el('button', 'as-mini', uiSaid('set-group-none-all', 'All to None')); clearAll.type = 'button'; clearAll.addEventListener('click', () => setAll(c, false));
|
|
4136
4034
|
head.append(el('b', '', shownName(c)), el('span', 'as-give-acts', ''));
|
|
4137
4035
|
head.lastChild.append(giveAll, clearAll);
|
|
4138
4036
|
block.append(head);
|
|
4139
4037
|
for (const m of members) {
|
|
4140
4038
|
const line = el('div', 'as-give-row');
|
|
4141
4039
|
const nm = el('span', 'as-give-name'); nm.append(kindIcon(m.kind), el('span', '', m.name));
|
|
4142
|
-
|
|
4040
|
+
/* The one picker (kit RightsPicker, « None » first): the same levels as the asset's own panel. */
|
|
4041
|
+
const pick = KIT.RightsPicker({ key: KIT.rightsKey(m), kind: m.kind, value: memberCurrent(m, c.name), label: `${m.name} for ${shownName(c)}`,
|
|
4042
|
+
onChange: async (level, stored) => { await applyMemberGrant(m, c.name, stored ?? level); repaint(); } });
|
|
4043
|
+
if (pick) line.append(nm, pick); else line.append(nm);
|
|
4143
4044
|
block.append(line);
|
|
4144
4045
|
}
|
|
4145
4046
|
if (members.length === 0) block.append(el('p', 'hint', 'The group is empty.'));
|
|
@@ -4708,7 +4609,80 @@
|
|
|
4708
4609
|
/* A conversation's tab, searched: its words, and whose they are. */
|
|
4709
4610
|
let cassetsQuery = ''; let cassetsFor = '';
|
|
4710
4611
|
$('cassets-search').addEventListener('input', () => { cassetsQuery = $('cassets-search').value.trim(); void paintRoomAssets(); });
|
|
4711
|
-
$('cassets-attach').addEventListener('click', () => {
|
|
4612
|
+
$('cassets-attach').addEventListener('click', () => { openGive(); });
|
|
4613
|
+
/* GIVE… (07-11, assets step 4): in its tab, in place of an asset opened -- the one list of what it has not yet
|
|
4614
|
+
(kit AssetPicker), its right, then « Give »; a new asset below (the New asset box, given at its end). Its state:
|
|
4615
|
+
the search typed, the one chosen, the kind of a new one being made. */
|
|
4616
|
+
const GIVE = 'give:';
|
|
4617
|
+
let giveQuery = ''; let giveChosen = ''; let giveNew = { kind: '', level: '' };
|
|
4618
|
+
function openGive() {
|
|
4619
|
+
if (DEVICE || !current) return;
|
|
4620
|
+
giveQuery = ''; giveChosen = ''; giveNew = { kind: '', level: '' };
|
|
4621
|
+
if (justMade?.to) justMade = null;
|
|
4622
|
+
$('cassets-error').hidden = true;
|
|
4623
|
+
if (tabNow() !== 'assets') goTab('assets');
|
|
4624
|
+
pickRoomAsset(GIVE);
|
|
4625
|
+
}
|
|
4626
|
+
const closeGive = () => { giveNew = { kind: '', level: '' }; if (justMade?.to) justMade = null; if (roomPick === GIVE && !nav.close('asset')) { roomPick = ''; void paintRoomAssets(); } };
|
|
4627
|
+
/* A secret given or taken back: its tool has it, or not, at its next start -- said under the tab a moment. */
|
|
4628
|
+
let noteTimer = 0;
|
|
4629
|
+
function secretNote(name, on) {
|
|
4630
|
+
const p = $('cassets-note');
|
|
4631
|
+
p.textContent = uiSaid(on ? 'set-secret-next-on' : 'set-secret-next-off', on ? '{name}: handed to its tool at its next start' : '{name}: no longer handed from its next start', { name });
|
|
4632
|
+
p.hidden = false;
|
|
4633
|
+
clearTimeout(noteTimer); noteTimer = setTimeout(() => { p.hidden = true; }, 12_000);
|
|
4634
|
+
}
|
|
4635
|
+
/* Every asset as a card, for Give…: what the Assets menu lists, without who has it. */
|
|
4636
|
+
function everyCard({ all = [], boxes = [], secrets = [], tether = {}, lists = {} } = {}) {
|
|
4637
|
+
const out = [];
|
|
4638
|
+
for (const r of Array.isArray(all) ? all : []) {
|
|
4639
|
+
const kind = kindOf(r); const box = kind === 'mailbox' ? (Array.isArray(boxes) ? boxes : []).find((m) => m.name === r.name) : undefined;
|
|
4640
|
+
out.push({ key: `${kind}:${r.name}`, kind, name: r.name, resource: r, box, sub: kind === 'git' ? gitSub(r) : kind === 'mailbox' ? boxSub(box) : '', tone: box?.failure ? 'bad' : '' });
|
|
4641
|
+
}
|
|
4642
|
+
for (const s2 of Array.isArray(secrets) ? secrets : []) out.push({ key: `secret:${s2.name}`, kind: 'secret', name: s2.name, secret: s2, ...secretItem(s2, uiSaid('set-as-name-only', 'name only')) });
|
|
4643
|
+
for (const tgt of Array.isArray(tether?.targets) ? tether.targets : []) out.push({ key: `connexion:${tgt.id}`, kind: tetherCat(tgt.kind), name: tgt.title, target: tgt, live: true, sub: connexionOwnSub(tgt.kind) });
|
|
4644
|
+
for (const l of lists?.ok && Array.isArray(lists.data) ? lists.data.filter((x) => !x.archived) : []) out.push({ key: `list:${l.id}`, kind: 'list', name: l.title, list: l, sub: cardsSub(l.count) });
|
|
4645
|
+
return out;
|
|
4646
|
+
}
|
|
4647
|
+
/* Given from the picker: the same grant as « None » takes back (applyMemberGrant), then the list again. */
|
|
4648
|
+
async function giveOne(item, level, stored) {
|
|
4649
|
+
$('cassets-error').hidden = true;
|
|
4650
|
+
await applyMemberGrant(item, current, stored ?? level);
|
|
4651
|
+
if (!$('cassets-error').hidden) return false;
|
|
4652
|
+
if (item.kind === 'secret') secretNote(item.name, true);
|
|
4653
|
+
giveChosen = '';
|
|
4654
|
+
await paintRoomAssets();
|
|
4655
|
+
return true;
|
|
4656
|
+
}
|
|
4657
|
+
/* The levels a new asset of that kind is given at (a live link made: Read watches, Write proposes -- targetLevel). */
|
|
4658
|
+
const newLevels = (kind) => (['secret', 'box', 'device'].includes(kind) ? [] : kind === 'mailbox' ? ['read'] : ['read', 'write']);
|
|
4659
|
+
function paintGive(detail, data, items) {
|
|
4660
|
+
const c = conversations.find((x) => x.name === current);
|
|
4661
|
+
const who = shownName(c) || current;
|
|
4662
|
+
const made = justMade?.to === current ? justMade : null;
|
|
4663
|
+
const kind = made ? '' : giveNew.kind;
|
|
4664
|
+
const levels = newLevels(kind);
|
|
4665
|
+
if (kind && !levels.includes(giveNew.level)) giveNew.level = levels[0] ?? '';
|
|
4666
|
+
const level = levels.length ? { options: levels.map((l) => ({ value: l, label: KIT.rightWord(l, 'pick') })), value: giveNew.level, label: uiSaid('set-na-level', 'What it may do'), onChange: (v) => { giveNew.level = v; } } : null;
|
|
4667
|
+
const box = newAssetBox({
|
|
4668
|
+
kind, title: '',
|
|
4669
|
+
context: kind && !made ? { line: uiSaid('set-na-will', 'Will be given to {name}', { name: who }), level } : null,
|
|
4670
|
+
made: made ? { said: uiSaid('set-na-made-to', '{name} created and given to {to}', { name: made.name, to: who }), note: made.note } : null,
|
|
4671
|
+
onKind: (k) => {
|
|
4672
|
+
giveNew.kind = k; justMade = null; $('cassets-error').hidden = true; void paintRoomAssets();
|
|
4673
|
+
/* Its keys, read here too: Assets may not have been opened yet. */
|
|
4674
|
+
if (k === 'git') void getJson('/git-keys', []).then((keys) => paintKeys(keys));
|
|
4675
|
+
},
|
|
4676
|
+
onBack: () => { giveNew.kind = ''; $('cassets-error').hidden = true; void paintRoomAssets(); },
|
|
4677
|
+
onAnother: () => { giveNew.kind = made.kind; justMade = null; void paintRoomAssets(); },
|
|
4678
|
+
onDone: () => { closeGive(); },
|
|
4679
|
+
boxHost: { where: current, repaint: () => { void paintRoomAssets(); }, close: () => closeGive() },
|
|
4680
|
+
});
|
|
4681
|
+
const body = kind || made ? box : KIT.AssetPicker({ items: everyCard(data).map((i) => ({ ...cardOf(i), item: i })), given: items.map((i) => i.key), kinds: assetKinds(), query: giveQuery, chosen: giveChosen,
|
|
4682
|
+
newAsset: box, words: { who }, onPick: (key) => { giveChosen = key; }, onQuery: (q) => { giveQuery = q; }, onGive: (card, l, stored) => giveOne(card.item, l, stored) });
|
|
4683
|
+
detail.append(KIT.AssetPanel({ key: GIVE, kind: 'give', name: uiSaid('set-as-give', 'Give…'), sub: who }, { back: { onBack: () => { if (!nav.close('asset')) { roomPick = ''; void paintRoomAssets(); } } }, parts: [body] }));
|
|
4684
|
+
if (kind) focusFirst(detail);
|
|
4685
|
+
}
|
|
4712
4686
|
/* WHAT A CONVERSATION WAS GIVEN, the rows of its Assets tab (each with its right): its work folder, its folders,
|
|
4713
4687
|
repositories and mailboxes, its secrets, what it was given live (Tether) and its binders. The tab draws them,
|
|
4714
4688
|
its bar counts them, a room's settings show them for each of its AIs: the same rows everywhere. */
|
|
@@ -4755,13 +4729,17 @@
|
|
|
4755
4729
|
const secrets = secretsKnown.length ? secretsKnown : (c.secrets ?? []).map((n) => ({ name: n, conversations: [c.name] }));
|
|
4756
4730
|
return givenItems(c.name, { all, secrets, tether: givenKnown.tether, lists: givenKnown.lists }).length;
|
|
4757
4731
|
}
|
|
4732
|
+
/* Painted again while a paint reads (a right changed, then the list refreshed): only the last one draws. */
|
|
4733
|
+
let roomPainting = 0;
|
|
4758
4734
|
async function paintRoomAssets() {
|
|
4759
4735
|
if (!assetsShown) return;
|
|
4736
|
+
const mine = ++roomPainting; const stale = () => mine !== roomPainting || !assetsShown;
|
|
4760
4737
|
watchTether();
|
|
4761
4738
|
const c = conversations.find((x) => x.name === current);
|
|
4762
4739
|
roomSig = JSON.stringify([current, c?.resources, c?.secrets, c?.work]);
|
|
4763
4740
|
const [all, secrets, boxes, tether, lists] = await Promise.all([getJson('/resources', []), getJson('/secrets', []), getJson('/mailboxes', []), getJson('/tether', {}), binderCall('lists', {}), readDevices()]);
|
|
4764
4741
|
keepGiven(tether, lists); paintAssetsCount();
|
|
4742
|
+
if (stale()) return;
|
|
4765
4743
|
/* The calls this conversation's agent asked the owner to allow: validated here,
|
|
4766
4744
|
in the parloir, without leaving for the Assets page. */
|
|
4767
4745
|
const mineDemandes = (Array.isArray(tether.approvals) ? tether.approvals : []).filter((q) => (q.agent?.id ?? '') === `conversation:${current}`);
|
|
@@ -4774,9 +4752,11 @@
|
|
|
4774
4752
|
const outbox = gits.length ? { key: 'send:all', kind: 'send', name: uiSaid('set-as-to-send', 'To send'), sub: uiSaid('set-as-to-send-sub', 'every repository, reviewed and sent at once') } : null;
|
|
4775
4753
|
const waitingCount = mineDemandes.length + mineJobs.length;
|
|
4776
4754
|
const demandes = waitingCount ? { key: 'demandes:mine', kind: 'demandes', name: 'Demandes', sub: uiSaid('set-as-waiting', '{n} waiting for you', { n: waitingCount }) } : null;
|
|
4777
|
-
const
|
|
4755
|
+
const giving = roomPick === GIVE;
|
|
4756
|
+
const chosen = giving ? null : (outbox && roomPick === outbox.key ? outbox : null) ?? (demandes && roomPick === demandes.key ? demandes : null) ?? items.find((i) => i.key === roomPick) ?? null;
|
|
4778
4757
|
const as = $('cassets-as');
|
|
4779
|
-
as.dataset.open = chosen ? 'detail' : 'list';
|
|
4758
|
+
as.dataset.open = chosen || giving ? 'detail' : 'list';
|
|
4759
|
+
$('cassets-attach').setAttribute('aria-pressed', String(giving));
|
|
4780
4760
|
/* THE ONE LIST (kit AssetList), scope this conversation: by type, its right on each row; To send and
|
|
4781
4761
|
Demandes pinned above. Its search is its own, emptied when another conversation is shown. */
|
|
4782
4762
|
if (cassetsFor !== current) { cassetsFor = current; cassetsQuery = ''; $('cassets-search').value = ''; }
|
|
@@ -4788,11 +4768,22 @@
|
|
|
4788
4768
|
paintAssetList($('cassets-rows'), model, { chosen: chosen?.key, onOpen: pickRoomAsset, pinned, empty: uiSaid('set-as-none-given', 'Nothing is given to it yet.'), noMatch: uiSaid('set-as-no-match', 'Nothing by that name.') });
|
|
4789
4769
|
if (c?.work === 'disk') $('cassets-rows').append(el('p', 'hint as-disk', 'Its work is on the disk of its machine: nothing of it is kept here.'));
|
|
4790
4770
|
const detail = $('cassets-detail');
|
|
4791
|
-
|
|
4771
|
+
/* The New asset forms wait in Assets: put back before the detail is drawn again (Give… may hold one). */
|
|
4772
|
+
const parkForms = () => { for (const f of detail.querySelectorAll('.as-form')) $('assets-forms').append(f); };
|
|
4773
|
+
if (giving) {
|
|
4774
|
+
/* Drawn again only when what it shows changed: a form being filled, a search typed, keep their place. */
|
|
4775
|
+
const sig = JSON.stringify([GIVE, giveNew.kind, justMade?.to === current ? justMade.key : '', items.map((i) => i.key), [all, secrets, tether?.targets, lists?.data].map((x) => (Array.isArray(x) ? x.length : 0))]);
|
|
4776
|
+
if (detail.dataset.asset === GIVE && detail.dataset.sig === sig) return;
|
|
4777
|
+
unembedFiles(); parkForms(); detail.replaceChildren();
|
|
4778
|
+
detail.dataset.asset = GIVE; detail.dataset.sig = sig;
|
|
4779
|
+
paintGive(detail, { all, boxes, secrets, tether, lists }, items);
|
|
4780
|
+
return;
|
|
4781
|
+
}
|
|
4782
|
+
if (!chosen) { unembedFiles(); parkForms(); detail.replaceChildren(); delete detail.dataset.asset; if (items.length) detail.append(noAssetChosen()); return; }
|
|
4792
4783
|
/* The same folder still shown, nothing changed in who has it: only its files are read again (no blink). */
|
|
4793
4784
|
const sig = JSON.stringify([chosen.key, chosen.own ? '' : chosen.kind === 'send' || chosen.kind === 'demandes' ? waitingCount : accessHolders(chosen)]);
|
|
4794
4785
|
const keepFiles = (chosen.kind === 'folder' || chosen.kind === 'mailbox') && !chosen.target && $('files').classList.contains('embedded') && detail.contains($('files')) && detail.dataset.asset === chosen.key && detail.dataset.sig === sig;
|
|
4795
|
-
if (!keepFiles) { unembedFiles(); detail.replaceChildren(); }
|
|
4786
|
+
if (!keepFiles) { unembedFiles(); parkForms(); detail.replaceChildren(); }
|
|
4796
4787
|
detail.dataset.asset = chosen.key; detail.dataset.sig = sig;
|
|
4797
4788
|
const back = () => { roomPick = ''; void paintRoomAssets(); };
|
|
4798
4789
|
if (keepFiles) { void paintFiles(); return; }
|
|
@@ -4809,7 +4800,7 @@
|
|
|
4809
4800
|
} else await sendDetail(body, gits);
|
|
4810
4801
|
return;
|
|
4811
4802
|
}
|
|
4812
|
-
await assetPanel(detail, chosen, { from: current, back, tether, gits });
|
|
4803
|
+
await assetPanel(detail, chosen, { from: current, back, tether, gits, stale });
|
|
4813
4804
|
}
|
|
4814
4805
|
/* One asset opened: a step (another one opened takes its place); Back, the list. */
|
|
4815
4806
|
const assetStep = () => { if (nav.last(['asset']) < 0) nav.push('asset', ({ quiet }) => { roomPick = ''; if (!quiet && assetsShown) void paintRoomAssets(); }); };
|
|
@@ -4838,6 +4829,8 @@
|
|
|
4838
4829
|
function closeRoomAssets() {
|
|
4839
4830
|
if (!assetsShown) return;
|
|
4840
4831
|
assetsShown = false; $('cassets').hidden = true; unembedFiles(); paintViews();
|
|
4832
|
+
for (const f of $('cassets-detail').querySelectorAll('.as-form')) $('assets-forms').append(f);
|
|
4833
|
+
if (roomPick === GIVE) { roomPick = ''; giveNew = { kind: '', level: '' }; delete $('cassets-detail').dataset.asset; }
|
|
4841
4834
|
if ($('cassets').contains($('gitview'))) shutGit();
|
|
4842
4835
|
}
|
|
4843
4836
|
|
|
@@ -5073,19 +5066,19 @@
|
|
|
5073
5066
|
const focusFirst = (holder) => { const f = holder.querySelector('.as-form input:not([type=radio])'); if (f) focusField(f); };
|
|
5074
5067
|
/* What was just made: the box's ending ("✓ <name> created"), until Done. `to`: the conversation it was attached to. */
|
|
5075
5068
|
let justMade = null;
|
|
5076
|
-
/* A form is in
|
|
5077
|
-
const
|
|
5078
|
-
const newAssetError = (form, message) => { const e =
|
|
5069
|
+
/* A form is in a conversation's Give… (its context), or in Assets. */
|
|
5070
|
+
const inGive = (form) => $('cassets-detail').contains(form);
|
|
5071
|
+
const newAssetError = (form, message) => { const e = inGive(form) ? $('cassets-error') : $('assets-error'); e.textContent = message; e.hidden = false; };
|
|
5079
5072
|
/* THE ONE ENDING. `give(conversation, level)` attaches it, when the box was opened from a conversation; in
|
|
5080
5073
|
Assets it is opened, the ending above it. */
|
|
5081
5074
|
async function assetMade(form, { key, name, give = null, note = '' }) {
|
|
5082
|
-
if (
|
|
5083
|
-
const kind =
|
|
5084
|
-
if (give) await give(current,
|
|
5075
|
+
if (inGive(form) && current) {
|
|
5076
|
+
const kind = giveNew.kind;
|
|
5077
|
+
if (give) await give(current, giveNew.level || 'read');
|
|
5085
5078
|
justMade = { kind, key, name, note, to: current };
|
|
5086
|
-
|
|
5087
|
-
await
|
|
5088
|
-
|
|
5079
|
+
giveNew.kind = '';
|
|
5080
|
+
await refresh();
|
|
5081
|
+
await paintRoomAssets();
|
|
5089
5082
|
return;
|
|
5090
5083
|
}
|
|
5091
5084
|
justMade = { kind: assetsAdding, key, name, note };
|
|
@@ -5093,10 +5086,10 @@
|
|
|
5093
5086
|
}
|
|
5094
5087
|
/* Another kind asked from inside a form (New mailbox's Google, Microsoft: the account's form). */
|
|
5095
5088
|
async function newAssetKind(form, kind) {
|
|
5096
|
-
if (
|
|
5089
|
+
if (inGive(form)) { giveNew.kind = kind; await paintRoomAssets(); return; }
|
|
5097
5090
|
assetsAdding = kind; await paintAssets();
|
|
5098
5091
|
}
|
|
5099
|
-
/* A Tether target
|
|
5092
|
+
/* A Tether target made in Give…: Read watches, Write proposes (asks before each action). */
|
|
5100
5093
|
const targetLevel = (level) => (level === 'write' ? 'propose' : 'watch');
|
|
5101
5094
|
/* ONE ASSET OPENED (06-11, assets step 3): the same panel (kit AssetPanel, AccessList) in the Assets menu and in a
|
|
5102
5095
|
conversation's tab. Seen from the menu (`from` ''), the asset itself is changed there too: its group, its name, its
|
|
@@ -5126,7 +5119,11 @@
|
|
|
5126
5119
|
if (stored) await post(`${API}/devices/${id}/give`, { asset: chosen.key, rights: stored });
|
|
5127
5120
|
else await post(`${API}/devices/${id}/take`, { asset: chosen.key });
|
|
5128
5121
|
await readDevices();
|
|
5129
|
-
} else
|
|
5122
|
+
} else {
|
|
5123
|
+
await applyMemberGrant(chosen, String(row.id).slice('conversation:'.length), stored ?? level);
|
|
5124
|
+
/* A secret: its tool has it, or no longer, at its next start (said where it was changed, its tab). */
|
|
5125
|
+
if (chosen.kind === 'secret' && place !== 'assets' && row.id === `conversation:${current}`) secretNote(chosen.name, level !== 'none');
|
|
5126
|
+
}
|
|
5130
5127
|
} catch (error) { sayAsErr(error); return false; }
|
|
5131
5128
|
await repaintAsset();
|
|
5132
5129
|
return true;
|
|
@@ -5197,7 +5194,7 @@
|
|
|
5197
5194
|
}
|
|
5198
5195
|
/* `detail`: where it is drawn. `from`: the conversation it is seen from ('' : the menu). `back`: the way back (a phone).
|
|
5199
5196
|
`tether`, `groups`: as the page read them. `gits`: the conversation's repositories (To send, beside). */
|
|
5200
|
-
async function assetPanel(detail, chosen, { from = '', back, tether = {}, groups = [], gits = [] } = {}) {
|
|
5197
|
+
async function assetPanel(detail, chosen, { from = '', back, tether = {}, groups = [], gits = [], stale = () => false } = {}) {
|
|
5201
5198
|
const inMenu = !from;
|
|
5202
5199
|
const r = chosen.resource;
|
|
5203
5200
|
const parts = []; const foot = [];
|
|
@@ -5377,7 +5374,10 @@
|
|
|
5377
5374
|
edit.addEventListener('click', () => { remoteEditing = !remoteEditing; box.hidden = !remoteEditing; if (remoteEditing) focusField($('gitview-url')); });
|
|
5378
5375
|
} else edit.addEventListener('click', () => { void openAssetsPlace(chosen.key, { remote: true }); });
|
|
5379
5376
|
gitHolder = el('div', 'as-git');
|
|
5380
|
-
parts.push({ title: 'Settings', extra: [edit], body }
|
|
5377
|
+
parts.push({ title: 'Settings', extra: [edit], body });
|
|
5378
|
+
/* What THIS conversation's agent may send by itself: under the right it depends on (it writes), seen from it. */
|
|
5379
|
+
if (!inMenu && !DEVICE && model?.here?.level === 'write') parts.push(autoSending(from, chosen.name));
|
|
5380
|
+
parts.push(gitHolder);
|
|
5381
5381
|
}
|
|
5382
5382
|
if (t) {
|
|
5383
5383
|
/* An MCP server's rights say themselves (Off / Read / All with approval / All / Custom): no levels' words under them. */
|
|
@@ -5423,6 +5423,8 @@
|
|
|
5423
5423
|
}
|
|
5424
5424
|
/* Its right said once: by its own row's picker when it has one (« Lire » pressed), else by the head's badge. */
|
|
5425
5425
|
const card = { ...cardOf(chosen), holders: undefined, right: inMenu || model?.here ? undefined : chosen.right, chips: chipsOf(chosen) };
|
|
5426
|
+
/* A later paint began while this one read: it draws, not this one. */
|
|
5427
|
+
if (stale()) return;
|
|
5426
5428
|
detail.append(KIT.AssetPanel(card, { back: back ? { onBack: () => { if (!nav.close('asset')) back(); } } : null, parts, foot }));
|
|
5427
5429
|
if (gitHolder) await gitDetail(gitHolder, chosen.name, { place: inMenu, others: gits.length > 1 });
|
|
5428
5430
|
}
|
|
@@ -5655,7 +5657,8 @@
|
|
|
5655
5657
|
words, markdown: true, agentName: '',
|
|
5656
5658
|
/* A mail an AI of the room cites: the same chip and viewer as in a conversation's chat, read for the conversation of the AI that cited it. */
|
|
5657
5659
|
schemes: { mail: { icon: 'mail', open: (ref, label) => { void openMailRef(ref, { label, of: '', room: id }); } } },
|
|
5658
|
-
|
|
5660
|
+
/* Its lines (a Tether call held for one of its AIs): drawn and decided as in a conversation's chat (machineLines, tetherAct). */
|
|
5661
|
+
load: (asked = {}) => fetch(`${API}/rooms/${id}/talk${asked.last ? `?last=${asked.last}` : ''}`).then((x) => x.json()).then(machineLines).then((d) => {
|
|
5659
5662
|
/* Read for a room no longer shown: its words go nowhere (Parley drops them with its screen). */
|
|
5660
5663
|
if (salonId !== id || place !== 'salon') return d;
|
|
5661
5664
|
/* Read, it is seen (the server says so): its count in the list goes at once, as a conversation's. */
|
|
@@ -5698,6 +5701,7 @@
|
|
|
5698
5701
|
diagram: drawDiagram, highlight: colourCode, touch: touchScreen,
|
|
5699
5702
|
voice: chatVoice(), autoSend: chatAuto, autoSendMs: chatAutoMs, onAutoSendMs: keepAutoMs, onAutoSend: (on) => { chatAuto = on; try { localStorage.setItem('clavarium-chat-auto', on ? '1' : '0'); } catch { /* as above */ } },
|
|
5700
5703
|
draft: `room:${id}`,
|
|
5704
|
+
act: (action) => { tetherAct(action, () => salonThread?.refresh?.()); },
|
|
5701
5705
|
});
|
|
5702
5706
|
salonTimer = setInterval(() => { if (salonThread && salonId === id) void salonThread.refresh(); }, 3000);
|
|
5703
5707
|
}
|
|
@@ -6117,7 +6121,7 @@
|
|
|
6117
6121
|
});
|
|
6118
6122
|
$('secret-new').addEventListener('submit', async (event) => {
|
|
6119
6123
|
event.preventDefault();
|
|
6120
|
-
$('assets-error').hidden = true; $('
|
|
6124
|
+
$('assets-error').hidden = true; $('cassets-error').hidden = true;
|
|
6121
6125
|
const lines = !$('secret-lines').hidden;
|
|
6122
6126
|
/* A key's last line ends with a line break: kept as it was pasted. */
|
|
6123
6127
|
const value = lines ? $('secret-lines').value.replace(/\r\n?/g, '\n') : $('secret-value').value;
|
|
@@ -6127,8 +6131,8 @@
|
|
|
6127
6131
|
await post(`${API}/secrets`, { name, value, ...(expires ? { expires } : {}) });
|
|
6128
6132
|
$('secret-name').value = ''; $('secret-expires').value = ''; secretLines(false, '');
|
|
6129
6133
|
await assetMade($('secret-box'), { key: `secret:${name}`, name, give: async (to) => {
|
|
6130
|
-
await handTo(to, name, true, '
|
|
6131
|
-
|
|
6134
|
+
await handTo(to, name, true, 'cassets-error');
|
|
6135
|
+
secretNote(name, true);
|
|
6132
6136
|
} });
|
|
6133
6137
|
} catch (error) { newAssetError($('secret-box'), error.message); }
|
|
6134
6138
|
await paintAssets();
|
|
@@ -8383,7 +8387,7 @@
|
|
|
8383
8387
|
$('git-said').hidden = false;
|
|
8384
8388
|
}
|
|
8385
8389
|
/* A new repository opens on its card, where it is attached: the next thing to do. */
|
|
8386
|
-
await assetMade($('git-box'), { key: `git:${made.name}`, name: made.name, note: made.firstSeen ? $('git-said').textContent : '', give: (to, level) => attachTo(to, made.name, level, '
|
|
8390
|
+
await assetMade($('git-box'), { key: `git:${made.name}`, name: made.name, note: made.firstSeen ? $('git-said').textContent : '', give: (to, level) => attachTo(to, made.name, level, 'cassets-error') });
|
|
8387
8391
|
} catch (error) { $('git-error').textContent = error.message; $('git-error').hidden = false; }
|
|
8388
8392
|
button.disabled = false; button.textContent = 'Create';
|
|
8389
8393
|
await paintAssets();
|
|
@@ -8927,12 +8931,12 @@
|
|
|
8927
8931
|
getJson('/accounts', null).then((d) => { if (d?.supported) { accountSupported = { ...d.supported, classic: ['mail', 'calendar', 'contacts'] }; setConnexionProvider($('connexion-new').dataset.provider || 'google'); } });
|
|
8928
8932
|
$('list-new').addEventListener('submit', async (event) => {
|
|
8929
8933
|
event.preventDefault();
|
|
8930
|
-
$('assets-error').hidden = true; $('
|
|
8934
|
+
$('assets-error').hidden = true; $('cassets-error').hidden = true;
|
|
8931
8935
|
const made = await binderCall('create-list', { template: $('listnew-template').value, title: $('listnew-title').value.trim(), lang: binderLang() });
|
|
8932
8936
|
if (made.ok) {
|
|
8933
8937
|
$('listnew-title').value = '';
|
|
8934
8938
|
const key = `list:${made.data.id}`;
|
|
8935
|
-
const attaching =
|
|
8939
|
+
const attaching = inGive($('list-box'));
|
|
8936
8940
|
await assetMade($('list-box'), { key, name: made.data.title ?? $('listnew-template').selectedOptions[0]?.textContent ?? '', give: (to, level) => grantList(made.data.id, to, level) });
|
|
8937
8941
|
if (!attaching) { listOpen = key; remember(); }
|
|
8938
8942
|
} else newAssetError($('list-box'), made.text);
|
|
@@ -8941,11 +8945,11 @@
|
|
|
8941
8945
|
$('folder-new').addEventListener('submit', async (event) => {
|
|
8942
8946
|
event.preventDefault();
|
|
8943
8947
|
$('assets-error').hidden = true;
|
|
8944
|
-
$('
|
|
8948
|
+
$('cassets-error').hidden = true;
|
|
8945
8949
|
try {
|
|
8946
8950
|
const made = await post(`${API}/resources`, { name: $('folder-name').value.trim() }); $('folder-name').value = '';
|
|
8947
8951
|
const name = made?.name ?? '';
|
|
8948
|
-
await assetMade($('folder-box'), { key: name ? `folder:${name}` : '', name, give: name ? (to, level) => attachTo(to, name, level, '
|
|
8952
|
+
await assetMade($('folder-box'), { key: name ? `folder:${name}` : '', name, give: name ? (to, level) => attachTo(to, name, level, 'cassets-error') : null });
|
|
8949
8953
|
} catch (error) { newAssetError($('folder-box'), error.message); }
|
|
8950
8954
|
await paintAssets();
|
|
8951
8955
|
});
|
package/web/index.html
CHANGED
|
@@ -105,6 +105,7 @@
|
|
|
105
105
|
each opened on the right (a folder: its files). -->
|
|
106
106
|
<div id="cassets" class="sheet as-room" hidden>
|
|
107
107
|
<p id="cassets-error" class="error" role="alert" hidden></p>
|
|
108
|
+
<p id="cassets-note" class="hint" role="status" hidden></p>
|
|
108
109
|
<div class="as" id="cassets-as">
|
|
109
110
|
<div class="as-list"><div class="as-tools"><input type="search" id="cassets-search" placeholder="Search" aria-label="Search its assets" spellcheck="false" autocomplete="off"><button type="button" id="cassets-attach" class="as-new"><span aria-hidden="true">+ </span><span data-said="set-as-give">Give…</span></button></div><div id="cassets-rows"></div></div>
|
|
110
111
|
<div class="as-detail" id="cassets-detail"></div>
|
|
@@ -226,26 +227,10 @@
|
|
|
226
227
|
</section>
|
|
227
228
|
<section class="settings-group" id="panel-group-access" data-group="access" role="tabpanel" aria-label="Access">
|
|
228
229
|
<details class="chapter" id="chapter-access" data-chapter="access">
|
|
229
|
-
<summary><span class="chapter-title" data-said="set-ch-access">Access</span><span class="chapter-about">
|
|
230
|
+
<summary><span class="chapter-title" data-said="set-ch-access">Access</span><span class="chapter-about">Its assets, its sign-in mailbox.</span></summary>
|
|
230
231
|
<div class="chapter-body">
|
|
231
|
-
|
|
232
|
-
<
|
|
233
|
-
<div id="panel-rows"></div>
|
|
234
|
-
<p id="panel-empty" class="hint" hidden>Nothing is attached to this conversation.</p>
|
|
235
|
-
<div class="panel-top"><b data-said="set-acc-secrets">Secrets handed to its tool</b></div>
|
|
236
|
-
<div id="access-secrets"></div>
|
|
237
|
-
<p id="access-secrets-empty" class="hint" hidden>No secret is handed to it.</p>
|
|
238
|
-
<p id="secret-note" class="hint" role="status"></p>
|
|
239
|
-
<!-- ATTACH: the New asset box (kit.js NewAsset) with this conversation as its context: what is
|
|
240
|
-
there already (#attach-there, moved into it), or a new one, attached at the end. -->
|
|
241
|
-
<div id="attach-picker" hidden>
|
|
242
|
-
<div class="panel-top"><b id="attach-title" data-said="set-na-attach">Attach</b><button type="button" id="attach-close" data-said="set-na-done">Done</button></div>
|
|
243
|
-
<div id="attach-box"></div>
|
|
244
|
-
<p id="attach-error" class="error" role="alert" hidden></p>
|
|
245
|
-
<div id="attach-park" hidden><div id="attach-there"><div id="attach-rows"></div>
|
|
246
|
-
<p id="attach-empty" class="hint" hidden>Everything there is is attached already.</p></div></div>
|
|
247
|
-
<button type="button" id="attach-manage" class="as-link">Manage all assets</button>
|
|
248
|
-
</div>
|
|
232
|
+
<!-- ITS ASSETS (07-11, assets step 4): one line; what it was given lives in its Assets tab (Give…, its rights). -->
|
|
233
|
+
<div class="row access-assets"><button type="button" id="access-assets" class="as-link"></button></div>
|
|
249
234
|
<div id="signin-setup" hidden>
|
|
250
235
|
<div class="panel-top"><b>Sign-in mailbox</b></div>
|
|
251
236
|
<p class="hint">Its sign-in links, beside the terminal</p><p class="hint tip" hidden>A mailbox for this tool's account, read only; its password never enters the machine.</p>
|
package/web/kit.js
CHANGED
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
* AccessList WHO HAS IT (06-11, assets step 3): a row per conversation or device, its RightsPicker; seen from one
|
|
31
31
|
* conversation, its row first, then "Also with"; accessModel places the rows.
|
|
32
32
|
* AssetPanel ONE ASSET OPENED, the same in the Assets menu and a conversation's tab: head, parts, foot (AssetDelete).
|
|
33
|
+
* AssetPicker GIVE… (07-11, assets step 4): the one list of what a conversation has not yet, one chosen, its right,
|
|
34
|
+
* then « Give »; a new asset below it (NewAsset). assetPickModel places the rows.
|
|
33
35
|
* RIGHTS THE ONE TABLE of what an asset can be given at, by its kind and by who gets it (a
|
|
34
36
|
* conversation, a device); "none" first. RightsBadge reads a right, RightsPicker sets it.
|
|
35
37
|
*
|
|
@@ -427,7 +429,7 @@
|
|
|
427
429
|
|
|
428
430
|
/* AN ASSET'S SIGN, by its kind: the icon set's name (Lucide, icons.js), or '' for one drawn here (a binder).
|
|
429
431
|
A live link (a connexion) takes the sign of what it links (a mailbox, a calendar); `acces`, any other. */
|
|
430
|
-
const ASSET_ICONS = { folder: 'folder', git: 'git-branch', mailbox: 'mail', agenda: 'calendar', contacts: 'user', secret: 'key-round', list: '', acces: 'link', send: 'arrow-up', demandes: 'arrow-up', box: 'mic', device: 'smartphone', conversation: 'message-square' };
|
|
432
|
+
const ASSET_ICONS = { folder: 'folder', git: 'git-branch', mailbox: 'mail', agenda: 'calendar', contacts: 'user', secret: 'key-round', list: '', acces: 'link', give: 'plus', send: 'arrow-up', demandes: 'arrow-up', box: 'mic', device: 'smartphone', conversation: 'message-square' };
|
|
431
433
|
const ASSET_KINDS = Object.keys(ASSET_ICONS);
|
|
432
434
|
/* A binder: the set has no such sign, so it is drawn here, in its style (was app.js listIcon). */
|
|
433
435
|
function listSign() {
|
|
@@ -714,8 +716,8 @@
|
|
|
714
716
|
/* The list drawn from its model. A row is an AssetCard (`rowCls` added: the page's handle on it); `pinned`,
|
|
715
717
|
rows above every section (what waits: To send, Demandes). `onOpen(key)`; a group's head folds it
|
|
716
718
|
(`onFold(id)`), its ⚙ opens it (`onGroup(id)`). `empty`: said when there is nothing at all; `noMatch`,
|
|
717
|
-
when a search finds nothing. */
|
|
718
|
-
function AssetList(model, { chosen = '', onOpen = () => {}, onFold = () => {}, onGroup = () => {}, rowCls = '', pinned = [], empty = '', noMatch = '' } = {}) {
|
|
719
|
+
when a search finds nothing. `after(a)`: a node drawn under a row (Give…: the chosen one's right). */
|
|
720
|
+
function AssetList(model, { chosen = '', onOpen = () => {}, onFold = () => {}, onGroup = () => {}, rowCls = '', pinned = [], empty = '', noMatch = '', after = null } = {}) {
|
|
719
721
|
const box = node('div', 'kit-aslist'); box.dataset.view = model.view;
|
|
720
722
|
box.append(...pinned.filter(Boolean));
|
|
721
723
|
const hints = {
|
|
@@ -746,7 +748,7 @@
|
|
|
746
748
|
} else head.append(node('h3', '', s.title));
|
|
747
749
|
sec.append(head);
|
|
748
750
|
if (!s.folded) {
|
|
749
|
-
for (const a of s.items) sec.append(AssetCard(a, { size: 'row', cls: rowCls, current: a.key === chosen, onOpen }));
|
|
751
|
+
for (const a of s.items) { sec.append(AssetCard(a, { size: 'row', cls: rowCls, current: a.key === chosen, onOpen })); const more = after?.(a); if (more) sec.append(more); }
|
|
750
752
|
if (s.empty) sec.append(node('p', 'hint kit-aslist-none', hints[s.empty] ?? ''));
|
|
751
753
|
}
|
|
752
754
|
box.append(sec);
|
|
@@ -756,6 +758,60 @@
|
|
|
756
758
|
return box;
|
|
757
759
|
}
|
|
758
760
|
|
|
761
|
+
/* GIVE… (07-11, assets step 4): ONE LIST of what a conversation has not been given yet (the same AssetList, by type),
|
|
762
|
+
searched; one chosen, its right below it (the kind's levels, without « None »: the lightest first pressed), then
|
|
763
|
+
« Give ». A new asset below the list (`newAsset`, the page's NewAsset): made there, it is given at the end.
|
|
764
|
+
Taking a right back is « None » in its own panel (AccessList): the asset stays in Assets.
|
|
765
|
+
|
|
766
|
+
assetPickModel: the rows placed, no page needed (tested alone).
|
|
767
|
+
items [cards] every asset given [keys] what the conversation has query, kinds as AssetList's */
|
|
768
|
+
function assetPickModel({ items = [], given = [], query = '', kinds = [] } = {}) {
|
|
769
|
+
const have = new Set(given);
|
|
770
|
+
return assetListModel({ items: (items ?? []).filter((i) => i && i.key && !have.has(i.key)), scope: 'conversation:', by: 'type', query, kinds });
|
|
771
|
+
}
|
|
772
|
+
/* Drawn, its state its own (the search typed, the one chosen, its level): the page repaints it only once given.
|
|
773
|
+
`onGive(card, level, stored)` gives it (the button waits; false or a throw: nothing changes); `onPick(key)`,
|
|
774
|
+
`onQuery(text)` keep the page's memory of them for its next paint. */
|
|
775
|
+
function AssetPicker({ items = [], given = [], kinds = [], query = '', chosen = '', newAsset = null, words: w = {}, onPick = () => {}, onQuery = () => {}, onGive = () => {} } = {}) {
|
|
776
|
+
const root = node('div', 'kit-aspick');
|
|
777
|
+
const search = node('input', 'kit-aspick-search'); search.type = 'search'; search.value = query;
|
|
778
|
+
search.placeholder = say('set-search', 'Search'); search.setAttribute('aria-label', search.placeholder);
|
|
779
|
+
search.spellcheck = false; search.autocomplete = 'off';
|
|
780
|
+
const list = node('div', 'kit-aspick-list');
|
|
781
|
+
const byKey = new Map((items ?? []).map((i) => [i.key, i]));
|
|
782
|
+
let q = query; let at = byKey.has(chosen) && !given.includes(chosen) ? chosen : '';
|
|
783
|
+
const give = (a) => {
|
|
784
|
+
const key = rightsKey(a);
|
|
785
|
+
const levels = rightsFor(key).filter((l) => l !== 'none');
|
|
786
|
+
if (!levels.length) return null;
|
|
787
|
+
let level = levels[0];
|
|
788
|
+
const bar = node('div', 'kit-aspick-give');
|
|
789
|
+
const pick = Segmented({ options: levels.map((l) => ({ value: l, label: wordOf(key, l, 'pick') })), value: level, label: say('set-acc-what', 'What {who} may do with {name}').replace('{who}', w.who ?? '').replace('{name}', a.name ?? ''), onChange: (l) => { level = l; } });
|
|
790
|
+
addClass(pick, 'kit-rights-pick');
|
|
791
|
+
for (const b of pick.children) b.title = wordOf(key, b.dataset.value, 'hint');
|
|
792
|
+
const go = node('button', 'primary kit-aspick-go', say('set-give-go', 'Give')); go.type = 'button';
|
|
793
|
+
go.addEventListener('click', async () => {
|
|
794
|
+
go.disabled = true;
|
|
795
|
+
try { if ((await onGive(a, level, storedOf(key, level))) === false) go.disabled = false; }
|
|
796
|
+
catch { go.disabled = false; }
|
|
797
|
+
});
|
|
798
|
+
bar.append(pick, go);
|
|
799
|
+
return bar;
|
|
800
|
+
};
|
|
801
|
+
const draw = () => {
|
|
802
|
+
const model = assetPickModel({ items, given, query: q, kinds });
|
|
803
|
+
if (at && !model.sections.some((s) => s.items.some((i) => i.key === at))) at = '';
|
|
804
|
+
list.replaceChildren(AssetList(model, { chosen: at, rowCls: 'as-row kit-aspick-row', after: (a) => (a.key === at ? give(a) : null),
|
|
805
|
+
onOpen: (key) => { at = at === key ? '' : key; onPick(at); draw(); },
|
|
806
|
+
empty: w.empty ?? say('set-give-all', 'It has every asset already.'), noMatch: say('set-as-no-match', 'Nothing by that name.') }));
|
|
807
|
+
};
|
|
808
|
+
search.addEventListener('input', () => { q = search.value.trim(); onQuery(q); draw(); });
|
|
809
|
+
draw();
|
|
810
|
+
root.append(search, list);
|
|
811
|
+
if (newAsset) root.append(node('h4', 'kit-na-sub kit-aspick-or', w.orNew ?? say('set-na-or-new', 'or a new one')), newAsset);
|
|
812
|
+
return root;
|
|
813
|
+
}
|
|
814
|
+
|
|
759
815
|
/* WHO HAS IT (06-11, assets step 3): ONE LIST of who was given an asset -- each conversation and each device on a
|
|
760
816
|
row, its right set in place (RightsPicker, "None" first). Seen from a conversation, its own row comes first, then
|
|
761
817
|
"Also with" the others. "+ Give to…" opens the conversations that do not have it yet, each at None.
|
|
@@ -887,7 +943,7 @@
|
|
|
887
943
|
|
|
888
944
|
window.clavariumKit = {
|
|
889
945
|
collectModel,
|
|
890
|
-
AssetIcon, AssetCard, Holders, MemberStack, memberStackModel, roomPatch, RightsBadge, RightsPicker, AssetList, assetListModel, AccessList, accessModel, AssetPanel, AssetDelete, RIGHT_SAME,
|
|
946
|
+
AssetIcon, AssetCard, Holders, MemberStack, memberStackModel, roomPatch, RightsBadge, RightsPicker, AssetList, assetListModel, AccessList, accessModel, AssetPanel, AssetDelete, AssetPicker, assetPickModel, RIGHT_SAME,
|
|
891
947
|
ASSET_ICONS, ASSET_KINDS, RIGHTS, RIGHT_LEVELS, MCP_WORDS, rightsKey, rightsFor, rightOf, storedOf, lightest, rightWord,
|
|
892
948
|
shareSlices, ShareDonut, ShareKey,
|
|
893
949
|
STATES, STATE_WORDS, stateOf, hueOf, initialOf, IDLE_MS, idleFor, idleWords, tidyList,
|
package/web/links.js
CHANGED
|
@@ -671,7 +671,7 @@
|
|
|
671
671
|
rm.addEventListener('click', () => {
|
|
672
672
|
const last = a.kind === 'device' && graph.links.filter((x) => x.from === a.id).length === 1;
|
|
673
673
|
/* The question in words above, two short buttons side by side below. */
|
|
674
|
-
const question = a.kind === 'device' ? `Take ${b.name} back from ${a.name}?${last ? ' It has nothing else: the device is removed too.' : ''}` : b.kind === 'secret' ? `Stop handing ${b.name} to ${a.name}?` : `
|
|
674
|
+
const question = a.kind === 'device' ? `Take ${b.name} back from ${a.name}?${last ? ' It has nothing else: the device is removed too.' : ''}` : b.kind === 'secret' ? `Stop handing ${b.name} to ${a.name}?` : `Take ${b.name} from ${a.name}? It stays in Assets.`;
|
|
675
675
|
const ask = h('p', { class: 'lk-ask', text: question });
|
|
676
676
|
const sure = h('button', { type: 'button', class: 'danger', text: 'Remove' });
|
|
677
677
|
const keep = h('button', { type: 'button', text: 'Cancel', onclick: () => openPop(l) });
|
|
@@ -772,7 +772,7 @@
|
|
|
772
772
|
h('div', { class: 'lk-top' }, h('i', { style: `background:${css(KIND(l))}` }), h('b', { text: other.name }), h('span', { class: 'lk-sb', text: what }),
|
|
773
773
|
l.pending ? null : h('button', { type: 'button', class: 'lk-x', 'aria-label': `Remove the link to ${other.name}`, text: '×', onclick: (ev) => {
|
|
774
774
|
const btn = ev.currentTarget;
|
|
775
|
-
btn.replaceWith(h('button', { type: 'button', class: 'danger', text: n.kind === 'device' || node(l.from).kind === 'device' ? 'Take back' : '
|
|
775
|
+
btn.replaceWith(h('button', { type: 'button', class: 'danger', text: n.kind === 'device' || node(l.from).kind === 'device' ? 'Take back' : 'Remove', onclick: async () => { try { await takeAway(l); await changed(); } catch (e) { err(e.message); } } }));
|
|
776
776
|
} })),
|
|
777
777
|
seg(l));
|
|
778
778
|
const out = graph.links.filter((l) => l.from === n.id); const inn = graph.links.filter((l) => l.to === n.id);
|
package/web/style.css
CHANGED
|
@@ -148,7 +148,6 @@ footer button.wide { flex: 1 0 auto; background: transparent; border-color: var(
|
|
|
148
148
|
.sheet { position: absolute; inset: 0; z-index: 3; display: flex; flex-direction: column; gap: 10px; padding: 14px; background: var(--panel); overflow-y: auto; }
|
|
149
149
|
.panel-top { display: flex; align-items: center; justify-content: space-between; }
|
|
150
150
|
.panel-actions { margin: 2px 0 12px; }
|
|
151
|
-
#panel-rows { display: flex; flex-direction: column; }
|
|
152
151
|
.folder { display: flex; align-items: center; gap: 8px; padding: 8px 0; border-top: 1px solid var(--line); flex-wrap: wrap; }
|
|
153
152
|
.folder b { flex: 1; min-width: 100px; overflow: hidden; text-overflow: ellipsis; }
|
|
154
153
|
.levels { display: flex; }
|
|
@@ -365,11 +364,10 @@ footer button.careful { border-style: dashed; color: var(--ink2); }
|
|
|
365
364
|
@media (prefers-reduced-motion: reduce) { .asset-head b::before { transition: none; } }
|
|
366
365
|
.asset-inside { flex-basis: 100%; padding: 0 0 8px 12px; display: flex; flex-direction: column; gap: 4px; }
|
|
367
366
|
@media (max-width: 700px) { .asset-head { grid-template-columns: 1fr; gap: 2px; } }
|
|
368
|
-
#assets details > summary, #access-advanced > summary { cursor: pointer; padding: 8px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--button); font-weight: 600; list-style: none; align-self: flex-start; }
|
|
369
|
-
#assets details > summary::before, #access-advanced > summary::before { content: '+ '; }
|
|
370
|
-
#assets details[open] > summary::before, #access-advanced[open] > summary::before { content: '- '; }
|
|
367
|
+
#assets details > summary, #access-advanced > summary, #cassets .auto-send > summary { cursor: pointer; padding: 8px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--button); font-weight: 600; list-style: none; align-self: flex-start; }
|
|
368
|
+
#assets details > summary::before, #access-advanced > summary::before, #cassets .auto-send > summary::before { content: '+ '; }
|
|
369
|
+
#assets details[open] > summary::before, #access-advanced[open] > summary::before, #cassets .auto-send[open] > summary::before { content: '- '; }
|
|
371
370
|
#assets details form, #access-advanced > div { margin-top: 8px; }
|
|
372
|
-
#attach-picker { border: 1px solid var(--brass); border-radius: 10px; padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; }
|
|
373
371
|
#chips { cursor: pointer; }
|
|
374
372
|
#settings .row { align-items: center; gap: 10px; }
|
|
375
373
|
/* One kind of event a line: its name, then its level, wrapped under it on a narrow screen. */
|
|
@@ -2911,6 +2909,19 @@ button.kit-switch[aria-checked="true"]::after { left: 18px; background: var(--ok
|
|
|
2911
2909
|
.kit-access-acts { display: flex; gap: 14px; flex-wrap: wrap; }
|
|
2912
2910
|
.kit-rights-pick > button > .kit-rights-same { display: block; font-size: .6875rem; font-weight: 500; opacity: .8; }
|
|
2913
2911
|
.kit-asdelete { display: inline; }
|
|
2912
|
+
/* GIVE… (07-11, assets step 4: kit AssetPicker): what it has not yet, searched; the one chosen opens its right and
|
|
2913
|
+
« Give » under it; a new asset below. Settings › Access: one line to its tab. */
|
|
2914
|
+
.kit-aspick { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
|
|
2915
|
+
.kit-aspick-search { width: 100%; padding: 9px 12px; border-radius: 9px; }
|
|
2916
|
+
.kit-aspick-list .kit-aslist-sec:first-child { margin-top: 0; }
|
|
2917
|
+
.kit-aspick-give { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 8px; margin: 0 0 6px 40px; padding: 8px 10px; border: 1px solid color-mix(in srgb, var(--brass) 45%, var(--line)); border-radius: 10px; background: color-mix(in srgb, var(--brass) 7%, var(--bar)); }
|
|
2918
|
+
.kit-aspick-give > .kit-rights-pick { margin-right: auto; }
|
|
2919
|
+
.kit-aspick-go { min-height: 30px; padding: 0 16px; }
|
|
2920
|
+
.kit-aspick-or { margin-top: 10px; }
|
|
2921
|
+
.kit-asset-ic[data-kind="give"] { color: var(--brass-ink); }
|
|
2922
|
+
.as-tools .as-new[aria-pressed="true"] { background: var(--brass); color: var(--on-brass, #fff); border-color: var(--brass); }
|
|
2923
|
+
.access-assets { margin: 2px 0 8px; }
|
|
2924
|
+
.access-assets > .as-link { font-size: .9375rem; font-weight: 600; }
|
|
2914
2925
|
.as-files-menu #files.embedded > .panel-top { display: none; }
|
|
2915
2926
|
/* IDLE 12 H OR MORE (07-11): « rien depuis 14 h », in grey, never bold. */
|
|
2916
2927
|
.item small.kit-conv-idle { color: var(--ink2); opacity: .8; }
|