nio-cat 0.9.9 → 0.9.10
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/package.json +1 -1
- package/src/agents/claude.js +2 -0
- package/src/claude-hook.js +1 -0
- package/src/hub.js +8 -1
- package/src/mcp.js +9 -1
- package/web/app/app.js +27 -7
- package/web/app/style.css +18 -5
- package/web/sw.js +1 -1
package/package.json
CHANGED
package/src/agents/claude.js
CHANGED
|
@@ -20,6 +20,8 @@ module.exports = {
|
|
|
20
20
|
// Conversa del terminal: el primer missatge en fa una branca; després ja continua la pròpia.
|
|
21
21
|
if (session.nativeId && session.forkNative) { args.push('--fork-session'); delete session.forkNative; }
|
|
22
22
|
if (session.model) args.push('--model', session.model);
|
|
23
|
+
// Que l'agent posi un títol que resumeixi la feina (eina MCP de nio; el hub el desa).
|
|
24
|
+
args.push('--append-system-prompt', 'Treballes dins una sessió de nio. Quan entenguis la tasca, crida l’eina mcp__nio__sessio_titol amb un títol de 3-7 paraules que la resumeixi, en l’idioma de l’usuari; torna-la a cridar només si la feina canvia de tema.');
|
|
23
25
|
|
|
24
26
|
if (session.mode === 'tot' && process.getuid?.() === 0) {
|
|
25
27
|
ctx.fail('Claude Code no deixa el mode «Via lliure» quan s’executa com a root. Canvia els permisos a «Demana permís» o engega nio amb un usuari normal.');
|
package/src/claude-hook.js
CHANGED
package/src/hub.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
const { EventEmitter } = require('events');
|
|
7
7
|
const { byId, binPath } = require('./agents');
|
|
8
|
+
const TITLE_TOOL = /(^|[._])sessio_titol$/;
|
|
8
9
|
const { HOME, save } = require('./config');
|
|
9
10
|
const push = require('./push');
|
|
10
11
|
const usage = require('./usage');
|
|
@@ -206,7 +207,7 @@ class Hub extends EventEmitter {
|
|
|
206
207
|
const s = this.get(sid);
|
|
207
208
|
if (!s) throw httpErr(404, 'Aquesta sessió no existeix');
|
|
208
209
|
if (patch.mode && ['llegir', 'preguntar', 'tot'].includes(patch.mode)) s.mode = patch.mode;
|
|
209
|
-
if (typeof patch.title === 'string') s.title = patch.title.slice(0, 120);
|
|
210
|
+
if (typeof patch.title === 'string') { s.title = patch.title.slice(0, 120); s.titleBy = 'user'; } // un nom posat a mà no el canvia l'agent
|
|
210
211
|
if (typeof patch.model === 'string') s.model = patch.model.slice(0, 80);
|
|
211
212
|
if (typeof patch.cwd === 'string' && path.resolve(patch.cwd) !== s.cwd) {
|
|
212
213
|
const cwd = path.resolve(patch.cwd);
|
|
@@ -285,6 +286,12 @@ class Hub extends EventEmitter {
|
|
|
285
286
|
stream: (t) => this.chunk(s, String(t)),
|
|
286
287
|
tool: ({ id: tid, name, input }) => {
|
|
287
288
|
toolNames.set(tid, name);
|
|
289
|
+
// L'agent posa títol a la sessió amb l'eina MCP sessio_titol (Claude: mcp__nio__sessio_titol, Codex: nio.sessio_titol).
|
|
290
|
+
if (TITLE_TOOL.test(name || '') && typeof input === 'string') try { input = JSON.parse(input); } catch { input = {}; }
|
|
291
|
+
if (TITLE_TOOL.test(name || '') && s.titleBy !== 'user') {
|
|
292
|
+
const t = String(input?.titol || '').replace(/\s+/g, ' ').trim().replace(/[.。]$/, '').slice(0, 80);
|
|
293
|
+
if (t && t !== s.title) { s.title = t; s.titleBy = 'agent'; this.touch(s); }
|
|
294
|
+
}
|
|
288
295
|
const inp = summarizeInput(input);
|
|
289
296
|
this.push(s, 'tool', { id: tid, name, input: inp });
|
|
290
297
|
this.audit.add({ type: 'tool', session: s.id, agent: s.agent, title: s.title, cwd: s.cwd, tool: name, what: describe(name, inp), mode: s.mode });
|
package/src/mcp.js
CHANGED
|
@@ -50,6 +50,14 @@ const TOOLS = [
|
|
|
50
50
|
return `Desat a ${r.path} (${r.size} B).`;
|
|
51
51
|
},
|
|
52
52
|
},
|
|
53
|
+
{
|
|
54
|
+
name: 'sessio_titol',
|
|
55
|
+
description: 'Posa títol a la sessió de nio on treballes: 3-7 paraules que resumeixin la feina (p. ex. «Mode fosc a la botiga»), perquè l’usuari la reconegui a la llista. Crida-la quan entenguis la tasca i torna-la a cridar si la feina canvia de tema. Fora de nio no fa res.',
|
|
56
|
+
inputSchema: { type: 'object', properties: { titol: { type: 'string', description: 'Títol curt, sense punt final' } }, required: ['titol'], additionalProperties: false },
|
|
57
|
+
annotations: { readOnlyHint: true },
|
|
58
|
+
// El títol el posa nio quan veu passar aquesta crida (hub.js); aquí només es confirma.
|
|
59
|
+
run: ({ titol }) => (String(titol || '').trim() ? `Títol: ${String(titol).trim().slice(0, 80)}` : 'Falta el títol.'),
|
|
60
|
+
},
|
|
53
61
|
];
|
|
54
62
|
|
|
55
63
|
function serve({ input = process.stdin, output = process.stdout } = {}) {
|
|
@@ -64,7 +72,7 @@ function serve({ input = process.stdin, output = process.stdout } = {}) {
|
|
|
64
72
|
protocolVersion: params?.protocolVersion || '2025-06-18',
|
|
65
73
|
capabilities: { tools: {} },
|
|
66
74
|
serverInfo: { name: 'nio', version: require('../package.json').version },
|
|
67
|
-
instructions: 'Memòria compartida de nio (fitxers markdown en aquest ordinador). Consulta-la a l’inici d’una tasca (memoria_cerca o memoria_llegeix preferencies.md) i desa-hi el que valgui la pena recordar.',
|
|
75
|
+
instructions: 'Memòria compartida de nio (fitxers markdown en aquest ordinador). Consulta-la a l’inici d’una tasca (memoria_cerca o memoria_llegeix preferencies.md) i desa-hi el que valgui la pena recordar. Si treballes dins una sessió de nio, posa-li un títol curt amb sessio_titol.',
|
|
68
76
|
});
|
|
69
77
|
case 'ping':
|
|
70
78
|
return reply({});
|
package/web/app/app.js
CHANGED
|
@@ -213,14 +213,14 @@ async function api(path, opts = {}) {
|
|
|
213
213
|
return data;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
let streamCtrl = null, retry = 0, gotHello = false;
|
|
216
|
+
let streamCtrl = null, retry = 0, gotHello = false, alive = 0; // alive: última vegada que ha arribat alguna cosa de l'ordinador
|
|
217
217
|
async function pollLoop(ctrl) {
|
|
218
218
|
let first = true;
|
|
219
219
|
while (!ctrl.signal.aborted) {
|
|
220
220
|
try {
|
|
221
221
|
const [list, aps] = await Promise.all([api('/api/sessions'), api('/api/approvals')]);
|
|
222
222
|
if (ctrl.signal.aborted) return;
|
|
223
|
-
state.connected = true; state.lastError = null;
|
|
223
|
+
state.connected = true; state.lastError = null; alive = Date.now();
|
|
224
224
|
if (first) { first = false; state.synced = true; const r = state.route; state.sessions = new Map(list.map((x) => [x.id, x])); state.approvals = new Map(aps.map((a) => [a.id, a])); if (r.name === 'session') openSession(r.id); else render(); }
|
|
225
225
|
else {
|
|
226
226
|
const r = state.route, seen = new Set();
|
|
@@ -297,7 +297,7 @@ async function connect() {
|
|
|
297
297
|
const res = await fetch(`${m.url}/api/stream?d=${encodeURIComponent(deviceId())}&k=${isPhone() ? 'phone' : 'desktop'}`, { headers: { Authorization: `Bearer ${m.token}` }, signal: ctrl.signal, cache: 'no-store' });
|
|
298
298
|
if (res.status === 401) throw Object.assign(new Error('La clau no és vàlida. Torna a connectar amb el codi QR.'), { fatal: true });
|
|
299
299
|
if (!res.ok || !res.body) throw new Error(`Error ${res.status}`);
|
|
300
|
-
state.connected = true; state.lastError = null; retry = 0;
|
|
300
|
+
state.connected = true; state.lastError = null; retry = 0; alive = Date.now();
|
|
301
301
|
rememberIdentity(m.url, state.info.identity);
|
|
302
302
|
renameMachine(state.info.host, state.info.id); syncServiceWorker(); render(); brainsForBoard(); loadVoice().then(() => state.route.name === 'session' && renderComposer?.()); flushQueue();
|
|
303
303
|
const reader = res.body.getReader(), dec = new TextDecoder(); let buf = '';
|
|
@@ -306,6 +306,7 @@ async function connect() {
|
|
|
306
306
|
const hw = setTimeout(() => { if (!gotHello) { poll = true; reader.cancel().catch(() => {}); } }, 5000);
|
|
307
307
|
for (;;) {
|
|
308
308
|
const { value, done } = await reader.read().catch(() => ({ done: true })); if (done) break;
|
|
309
|
+
alive = Date.now();
|
|
309
310
|
buf += dec.decode(value, { stream: true }); let i;
|
|
310
311
|
while ((i = buf.indexOf('\n\n')) >= 0) {
|
|
311
312
|
const block = buf.slice(0, i); buf = buf.slice(i + 2);
|
|
@@ -922,7 +923,7 @@ async function pantallaTab() {
|
|
|
922
923
|
});
|
|
923
924
|
}
|
|
924
925
|
function moreSheet() {
|
|
925
|
-
const item = (k) => `<a class="more-link" href="${HASH[k]}" ${state.route.name === k ? 'aria-current="page"' : ''}
|
|
926
|
+
const item = (k) => `<a class="more-link" href="${HASH[k]}" ${state.route.name === k ? 'aria-current="page"' : ''}>${svg(VIEWS[k].icon)}<span>${VIEWS[k].label}</span>${k === 'approvals' && state.approvals.size ? `<span class="badge">${state.approvals.size}</span>` : ''}</a>`;
|
|
926
927
|
const groups = [...new Set(Object.values(VIEWS).map((v) => v.group))].map((group) => [group, Object.keys(VIEWS).filter((k) => VIEWS[k].group === group)]);
|
|
927
928
|
openSheet(`<h2>Més</h2><nav class="more-sections" aria-label="Totes les seccions">${groups.map(([title, keys]) => `<section><h3>${title}</h3><div>${keys.map(item).join('')}</div></section>`).join('')}</nav>`, (sh) => { sh.onclick = (e) => { if (e.target.closest('a, [data-act]')) closeSheet(); }; });
|
|
928
929
|
}
|
|
@@ -1310,7 +1311,10 @@ const hhmm = (t) => new Date(t * 1000).toLocaleString('ca', { weekday: t * 1000
|
|
|
1310
1311
|
function usageChip(agent) {
|
|
1311
1312
|
const u = state.usage?.[agent]; if (!u?.windows?.length) return '';
|
|
1312
1313
|
const tip = u.windows.map((w) => `${w.label}: ${Math.round(w.used * 100)} %${w.resetsAt ? ` · es reinicia ${hhmm(w.resetsAt)}` : ''}`).join('\n');
|
|
1313
|
-
|
|
1314
|
+
// Etiquetes curtes: «5h», «7d». La icona és un anell que s'omple amb la finestra més plena.
|
|
1315
|
+
const short = (l) => (l === 'setmana' ? '7d' : l.replace(/\s+/g, '')), top = Math.max(...u.windows.map((w) => w.used)), pct = Math.round(top * 100);
|
|
1316
|
+
const ring = `<svg class="ico ring" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8.5" class="rt"/><circle cx="12" cy="12" r="8.5" pathLength="100" stroke-dasharray="${Math.max(pct, 2)} 100" transform="rotate(-90 12 12)" class="rv"/></svg>`;
|
|
1317
|
+
return `<span class="chip mono usage" data-hi="${top >= 0.8 ? 1 : 0}" title="${esc(`Ús de la subscripció\n${tip}`)}">${ring}${u.windows.map((w) => `<span class="uw" data-hi="${w.used >= 0.8 ? 1 : 0}">${esc(short(w.label))} (${Math.round(w.used * 100)}%)</span>`).join('')}</span>`;
|
|
1314
1318
|
}
|
|
1315
1319
|
function renderSessionHead() {
|
|
1316
1320
|
renderChrome();
|
|
@@ -1423,6 +1427,7 @@ function putEvent(e, scroll = true) {
|
|
|
1423
1427
|
const feed = $('#feed'); if (!feed) return;
|
|
1424
1428
|
if (e.kind === 'text' && scroll && (e.open || typers.has(e.seq))) return streamText(state.route.id, e);
|
|
1425
1429
|
const stick = nearBottom(feed);
|
|
1430
|
+
if (e.kind === 'tool_result' && TITLE_TOOL.test(e.name || '')) return; // el títol ja es veu a dalt i a la línia «Títol»
|
|
1426
1431
|
if (e.kind === 'tool_result') {
|
|
1427
1432
|
const t = toolEls.get(e.id);
|
|
1428
1433
|
if (t) {
|
|
@@ -1448,12 +1453,15 @@ function putEvent(e, scroll = true) {
|
|
|
1448
1453
|
if (scroll && e.kind === 'user' && pinNext && !old) { pinNext = false; return pinMessage(feed, node); }
|
|
1449
1454
|
if (scroll) follow(feed, stick);
|
|
1450
1455
|
}
|
|
1456
|
+
// L'agent posa títol a la sessió amb l'eina sessio_titol de nio (Claude: mcp__nio__sessio_titol, Codex: nio.sessio_titol).
|
|
1457
|
+
const TITLE_TOOL = /(^|[._])sessio_titol$/;
|
|
1451
1458
|
function eventNode(e) {
|
|
1452
1459
|
const s = state.sessions.get(state.route.id);
|
|
1453
1460
|
switch (e.kind) {
|
|
1454
1461
|
case 'user': return el(`<div class="msg-user">${esc(e.text)}</div>`);
|
|
1455
1462
|
case 'text': return el(`<div class="msg-ai ${e.open && s?.state === 'running' ? 'cursor' : ''}" data-seq="${e.seq}">${md(e.text)}${!e.open && ttsOn() ? `<button class="speak" type="button" data-speak aria-label="Escolta la resposta">${svg(ICON.speaker)}<span>Escolta</span></button>` : ''}</div>`);
|
|
1456
|
-
case 'tool':
|
|
1463
|
+
case 'tool': if (TITLE_TOOL.test(e.name || '')) return el(`<div class="ev-info">Títol: «${esc(e.input?.titol || '')}»</div>`);
|
|
1464
|
+
return el(`<details class="tool"><summary><span class="name">${esc(e.name)}</span><span class="arg">${esc(toolArg(e.input))}</span><span class="st">…</span></summary><pre>${esc(prettyInput(e.input))}</pre></details>`);
|
|
1457
1465
|
case 'tool_result': return el(`<details class="tool"><summary><span class="name">${esc(e.name || 'Eina')}</span><span class="arg"></span><span class="st ${e.error ? 'err' : 'ok'}">${e.error ? '✕' : '✓'}</span></summary><pre class="out">${esc(e.output)}</pre></details>`);
|
|
1458
1466
|
case 'approval': { const a = state.approvals.get(e.approval) || {}; return el(approvalCard({ ...a, approval: e.approval, tool: e.tool, input: e.input, state: e.state && e.state !== 'pending' ? e.state : a.state || e.state }, false)); }
|
|
1459
1467
|
case 'error': return el(`<div class="ev-error" role="alert"><b>Error</b><span>${esc(e.text)}</span></div>`);
|
|
@@ -3014,7 +3022,19 @@ if ('serviceWorker' in navigator) {
|
|
|
3014
3022
|
navigator.serviceWorker.addEventListener('message', (e) => { if (e.data?.type === 'open-session' && e.data.id) go(`#/s/${e.data.id}`); });
|
|
3015
3023
|
}
|
|
3016
3024
|
addEventListener('hashchange', route);
|
|
3017
|
-
|
|
3025
|
+
// El mòbil congela la pàgina en segon pla i el flux queda mort sense cap error: l'ordinador envia un ping cada 15 s,
|
|
3026
|
+
// així que si fa més de 40 s que no arriba res (o en tornar a l'app, més de 20 s), es torna a connectar sol.
|
|
3027
|
+
function revive(maxIdle) {
|
|
3028
|
+
if (!machine() || isDemo(machine()) || document.hidden) return;
|
|
3029
|
+
// Desconnectat, el vigilant no talla els reintents en curs; en tornar a l'app sí que es torna a provar de seguida.
|
|
3030
|
+
if (state.connected ? Date.now() - alive < maxIdle : maxIdle >= 40000) return;
|
|
3031
|
+
retry = 0; reconTry.clear(); connect();
|
|
3032
|
+
}
|
|
3033
|
+
document.addEventListener('visibilitychange', () => revive(20000));
|
|
3034
|
+
addEventListener('pageshow', (e) => { if (e.persisted) revive(0); });
|
|
3035
|
+
addEventListener('online', () => revive(0));
|
|
3036
|
+
addEventListener('focus', () => revive(20000));
|
|
3037
|
+
setInterval(() => revive(40000), 10000);
|
|
3018
3038
|
setInterval(() => { if (!document.hidden && state.route.name === 'sessions') render(); }, 60000);
|
|
3019
3039
|
const qs = new URLSearchParams(location.search).get('s');
|
|
3020
3040
|
if (qs) history.replaceState(null, '', location.pathname + `#/s/${qs}`);
|
package/web/app/style.css
CHANGED
|
@@ -85,8 +85,8 @@ kbd { font: 600 11px var(--font); padding: 1px 6px; border: 1px solid var(--bord
|
|
|
85
85
|
body.side-collapsed .brand .word { visibility: hidden; }
|
|
86
86
|
body.side-collapsed .side nav a .l, body.side-collapsed .kbd .l, body.side-collapsed .kbd kbd, body.side-collapsed .conn span, body.side-collapsed .conn svg { display: none; }
|
|
87
87
|
body.side-collapsed .conn { padding-left: 0; justify-content: center; }
|
|
88
|
-
body.side-collapsed .side nav .grp { font-size: 0; padding:
|
|
89
|
-
body.side-collapsed .side nav .grp::before {
|
|
88
|
+
body.side-collapsed .side nav .grp { font-size: 0; margin-top: 8px; padding: 8px 0 0; }
|
|
89
|
+
body.side-collapsed .side nav .grp::before { left: 10px; right: 10px; }
|
|
90
90
|
body.side-collapsed .side nav .grp:first-child { display: none; }
|
|
91
91
|
body.side-collapsed .side nav a .badge { position: absolute; top: 2px; right: 2px; min-width: 16px; height: 16px; font-size: 9px; padding: 0 4px; }
|
|
92
92
|
}
|
|
@@ -135,7 +135,11 @@ button.chip:hover { border-color: var(--fg); }
|
|
|
135
135
|
.chip.waiting { background: var(--lemon); border-color: var(--lemon); }
|
|
136
136
|
.chip.error { color: var(--red); border-color: color-mix(in srgb, var(--red) 40%, transparent); }
|
|
137
137
|
.chip.usage { gap: 6px; }
|
|
138
|
-
.chip.usage .uw + .uw::before { content: '
|
|
138
|
+
.chip.usage .uw + .uw::before { content: '//'; margin-right: 6px; color: var(--fg-muted); font-weight: 400; opacity: .7; }
|
|
139
|
+
.chip.usage .ring { width: 15px; height: 15px; stroke-width: 3; }
|
|
140
|
+
.chip.usage .ring .rt { stroke: color-mix(in srgb, currentColor 18%, transparent); }
|
|
141
|
+
.chip.usage .ring .rv { stroke: var(--moss); }
|
|
142
|
+
.chip.usage[data-hi="1"] .ring .rv { stroke: var(--red); }
|
|
139
143
|
.chip.usage .uw[data-hi="1"] { color: var(--red); font-weight: 600; }
|
|
140
144
|
.chip.beta { height: 20px; padding: 0 7px; font-size: 10px; color: var(--fg-muted); }
|
|
141
145
|
.pulse { width: 6px; height: 6px; border-radius: 50%; background: currentColor; animation: pulse 1.2s ease-in-out infinite; }
|
|
@@ -185,6 +189,9 @@ button.chip:hover { border-color: var(--fg); }
|
|
|
185
189
|
.try h2 { font-size: 18px; font-weight: 600; letter-spacing: -.03em; }
|
|
186
190
|
.side nav .grp { display: block; padding: 14px 13px 4px; font-size: 11px; font-weight: 600; color: var(--fg-muted); letter-spacing: .02em; }
|
|
187
191
|
.side nav .grp:first-child { padding-top: 0; }
|
|
192
|
+
/* Cada apartat comença amb una línia fina, alineada amb el text dels enllaços. */
|
|
193
|
+
.side nav .grp:not(:first-child) { position: relative; margin-top: 10px; padding-top: 18px; }
|
|
194
|
+
.side nav .grp:not(:first-child)::before { content: ''; position: absolute; top: 0; left: 13px; right: 13px; height: 1px; background: color-mix(in srgb, var(--border) 85%, transparent); }
|
|
188
195
|
|
|
189
196
|
/* ---------- Sessions ---------- */
|
|
190
197
|
.session-card .title { font-size: 16px; font-weight: 500; letter-spacing: -.02em; margin-top: 8px; }
|
|
@@ -360,6 +367,7 @@ body.board-mode .view-enter { animation: none; }
|
|
|
360
367
|
body.nav-open .side { transform: none; box-shadow: 24px 0 60px -30px color-mix(in srgb, var(--fg) 45%, transparent); }
|
|
361
368
|
.side.dragging { transition: none; }
|
|
362
369
|
.side nav a { padding: 12px; font-size: 16px; }
|
|
370
|
+
.side nav, .side .foot { flex-shrink: 0; } /* el calaix fa scroll; la llista no s'encongeix sota el peu */
|
|
363
371
|
.side .foot .kbd kbd { display: none; }
|
|
364
372
|
.scrim { display: block; position: fixed; inset: 0; z-index: 45; background: color-mix(in srgb, var(--fg) 30%, transparent); opacity: 0; pointer-events: none; transition: opacity .3s ease-out; }
|
|
365
373
|
body.nav-open .scrim { opacity: 1; pointer-events: auto; }
|
|
@@ -372,6 +380,8 @@ body.board-mode .view-enter { animation: none; }
|
|
|
372
380
|
.cmd.big .btn { height: 38px; }
|
|
373
381
|
.paste .pair-form { flex-direction: column; }
|
|
374
382
|
.side nav .grp { padding: 16px 12px 4px; }
|
|
383
|
+
.side nav .grp:not(:first-child) { margin-top: 12px; padding-top: 20px; }
|
|
384
|
+
.side nav .grp:not(:first-child)::before { left: 12px; right: 12px; }
|
|
375
385
|
.top h1 { font-size: 26px; }
|
|
376
386
|
/* Al mòbil només fa scroll el document: res de contenidors amb scroll propi. */
|
|
377
387
|
.chat { height: auto; min-height: calc(100dvh - 56px - var(--safe-t)); }
|
|
@@ -751,11 +761,14 @@ body.chat-mode .demo-bar, body.board-mode .demo-bar { display: none; }
|
|
|
751
761
|
.feed { position: relative; }
|
|
752
762
|
|
|
753
763
|
/* Totes les seccions, amb controls còmodes de tocar. */
|
|
754
|
-
.more-sections { display: grid; gap:
|
|
755
|
-
.more-sections
|
|
764
|
+
.more-sections { display: grid; gap: 0; margin-top: 16px; }
|
|
765
|
+
.more-sections section + section { margin-top: 14px; padding-top: 16px; border-top: 1px solid color-mix(in srgb, var(--border) 85%, transparent); }
|
|
766
|
+
.more-sections h3 { margin: 0 0 4px; padding-left: 12px; font-size: 12px; font-weight: 600; color: var(--fg-muted); }
|
|
756
767
|
.more-sections section > div { display: grid; }
|
|
757
768
|
.more-link { display: flex; align-items: center; gap: 12px; min-height: 48px; padding: 10px 12px; border-radius: 10px; text-decoration: none; }
|
|
758
769
|
.more-link > span:first-of-type { flex: 1; }
|
|
770
|
+
.more-link > .ico:first-child { width: 20px; height: 20px; color: var(--fg-muted); }
|
|
771
|
+
.more-link[aria-current="page"] > .ico:first-child { color: var(--fg); }
|
|
759
772
|
.more-link > .ico:last-child { width: 14px; color: var(--fg-muted); }
|
|
760
773
|
.more-link:hover, .more-link[aria-current="page"] { background: var(--bg-2); }
|
|
761
774
|
.voice-extras { border: 1px solid var(--border); border-radius: 12px; padding: 0 14px; }
|
package/web/sw.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// nio · service worker: obre l'app encara que no hi hagi connexió i mostra els avisos.
|
|
2
|
-
const SHELL = 'nio-shell-
|
|
2
|
+
const SHELL = 'nio-shell-v51';
|
|
3
3
|
const FILES = ['/app', '/app/style.css', '/app/notes.css', '/app/call.css', '/app/app.js', '/app/notes.js', '/app/call.js', '/app/demo.js', '/bot.css', '/bot.js', '/board.css', '/board.js', '/favicon.svg', '/manifest.webmanifest'];
|
|
4
4
|
|
|
5
5
|
self.addEventListener('install', (e) => {
|