nio-cat 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/nio.js +15 -5
- package/package.json +1 -1
- package/src/pair.js +20 -9
- package/src/server.js +3 -2
- package/web/app/app.js +50 -9
- package/web/app/style.css +8 -0
package/bin/nio.js
CHANGED
|
@@ -11,7 +11,7 @@ const { Hub } = require('../src/hub');
|
|
|
11
11
|
const { createServer } = require('../src/server');
|
|
12
12
|
const { detectAll } = require('../src/agents');
|
|
13
13
|
const { startTunnel } = require('../src/tunnel');
|
|
14
|
-
const { pairLink, terminalQr, writePairPage, makeCode } = require('../src/pair');
|
|
14
|
+
const { pairLink, terminalQr, writePairPage, makeCode, codeLink, terminalQrSize } = require('../src/pair');
|
|
15
15
|
const { which } = require('../src/proc');
|
|
16
16
|
|
|
17
17
|
const argv = process.argv.slice(2);
|
|
@@ -82,10 +82,20 @@ function openDashboard() {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
async function showPair(publicUrl) {
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
const app = flags.app || cfg.app;
|
|
86
|
+
const link = pairLink(app, publicUrl, cfg.token), code = makeCode(publicUrl, cfg.token), qrLink = codeLink(app, code);
|
|
87
|
+
const file = writePairPage(link, code, qrLink);
|
|
88
|
+
// El QR ha de cabre al terminal: marge normal, marge d'1 mòdul i, si no hi cap, la pàgina del navegador.
|
|
89
|
+
const rows = process.stdout.rows || 999, cols = process.stdout.columns || 999;
|
|
90
|
+
const fits = (m) => { const z = terminalQrSize(qrLink, m); return z.rows + 9 <= rows && z.cols + 2 <= cols; };
|
|
91
|
+
const margin = fits(2) ? 2 : fits(1) ? 1 : 0;
|
|
92
|
+
if (margin) {
|
|
93
|
+
console.log(`\n ${c.b('Escaneja aquest codi amb el mòbil')}\n`);
|
|
94
|
+
console.log(terminalQr(qrLink, { margin }));
|
|
95
|
+
} else {
|
|
96
|
+
console.log(`\n ${c.warn('El terminal és massa petit per al QR.')} Fes-lo més gran o obre ${file}`);
|
|
97
|
+
if (canOpenBrowser()) openUrl(file);
|
|
98
|
+
}
|
|
89
99
|
console.log(`\n ${c.b('Codi:')} ${code}`);
|
|
90
100
|
console.log(c.dim(' Copia\'l i enganxa\'l a Sessions → Tria un ordinador.'));
|
|
91
101
|
console.log(c.dim(' Aquest codi dona accés als teus agents. Guarda\'l com una contrasenya.\n'));
|
package/package.json
CHANGED
package/src/pair.js
CHANGED
|
@@ -16,13 +16,17 @@ function pairLink(app, url, token) {
|
|
|
16
16
|
// La web ja sap posar-hi l'enllaç de davant. Les adreces de Cloudflare es comprimeixen (≈ 60 caràcters).
|
|
17
17
|
const CF = '.trycloudflare.com';
|
|
18
18
|
function makeCode(url, token) {
|
|
19
|
-
const raw = Buffer.from(token, 'base64url');
|
|
19
|
+
const raw = Buffer.from(token, 'base64url'), tok24 = raw.length === 24 && raw.toString('base64url') === token;
|
|
20
20
|
const sub = url.startsWith('https://') && url.endsWith(CF) ? url.slice(8, -CF.length) : null;
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const lan = url.match(/^http:\/\/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3}):(\d{1,5})$/);
|
|
22
|
+
let bytes;
|
|
23
|
+
if (tok24 && sub && /^[\w-]+$/.test(sub)) bytes = Buffer.concat([Buffer.from([1]), raw, Buffer.from(sub)]);
|
|
24
|
+
else if (tok24 && lan && lan.slice(1, 5).every((n) => +n < 256) && +lan[5] < 65536) bytes = Buffer.concat([Buffer.from([2]), raw, Buffer.from(lan.slice(1, 5).map(Number)), Buffer.from([+lan[5] >> 8, +lan[5] & 255])]);
|
|
25
|
+
else bytes = Buffer.concat([Buffer.from([0]), Buffer.from(`${url}\n${token}`)]);
|
|
24
26
|
return `nio-${bytes.toString('base64url')}`;
|
|
25
27
|
}
|
|
28
|
+
// Enllaç curt per al QR: una adreça de l'app amb el codi al fragment. Un QR més petit cap millor al terminal.
|
|
29
|
+
const codeLink = (app, code) => `${app.replace(/\/$/, '')}/app#${code}`;
|
|
26
30
|
|
|
27
31
|
function qr(text) {
|
|
28
32
|
const q = qrcode(0, 'L');
|
|
@@ -33,10 +37,10 @@ function qr(text) {
|
|
|
33
37
|
|
|
34
38
|
// QR per al terminal: cada mòdul són dos espais amb color de fons (negre o blanc). No depèn de cap glif,
|
|
35
39
|
// així que no es talla amb tipus de lletra o interlineats estranys. Sempre negre sobre blanc, amb tema clar o fosc.
|
|
36
|
-
function terminalQr(text, { color = !process.env.NO_COLOR } = {}) {
|
|
40
|
+
function terminalQr(text, { color = !process.env.NO_COLOR, margin = 2 } = {}) {
|
|
37
41
|
const q = qr(text);
|
|
38
42
|
const n = q.getModuleCount();
|
|
39
|
-
const M =
|
|
43
|
+
const M = margin; // marge blanc al voltant, en mòduls
|
|
40
44
|
const dark = (r, c) => r >= 0 && c >= 0 && r < n && c < n && q.isDark(r, c);
|
|
41
45
|
const lines = [];
|
|
42
46
|
for (let r = -M; r < n + M; r++) {
|
|
@@ -72,8 +76,8 @@ function qrSvg(text) {
|
|
|
72
76
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" shape-rendering="crispEdges" role="img" aria-label="Codi QR"><rect width="${size}" height="${size}" fill="#fff"/><path d="${d}" fill="#000"/></svg>`;
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
function writePairPage(link, code) {
|
|
76
|
-
const svg = qrSvg(link);
|
|
79
|
+
function writePairPage(link, code, qrLink) {
|
|
80
|
+
const svg = qrSvg(qrLink || link);
|
|
77
81
|
const html = `<!doctype html><html lang="ca"><head><meta charset="utf-8">
|
|
78
82
|
<meta name="viewport" content="width=device-width,initial-scale=1"><title>nio // connecta el mòbil</title>
|
|
79
83
|
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600&display=swap" rel="stylesheet">
|
|
@@ -88,4 +92,11 @@ code{display:block;word-break:break-all;font:12px/1.5 ui-monospace,Menlo,monospa
|
|
|
88
92
|
return file;
|
|
89
93
|
}
|
|
90
94
|
|
|
91
|
-
module.exports = { pairLink, terminalQr, writePairPage, qrSvg, makeCode };
|
|
95
|
+
module.exports = { pairLink, terminalQr, writePairPage, qrSvg, makeCode, codeLink };
|
|
96
|
+
|
|
97
|
+
// Mida del QR al terminal: files i columnes que ocupa amb un marge donat.
|
|
98
|
+
function terminalQrSize(text, margin = 2) {
|
|
99
|
+
const n = qr(text).getModuleCount() + margin * 2;
|
|
100
|
+
return { rows: n, cols: n * 2 + 2 };
|
|
101
|
+
}
|
|
102
|
+
module.exports.terminalQrSize = terminalQrSize;
|
package/src/server.js
CHANGED
|
@@ -10,7 +10,7 @@ const ollama = require('./agents/ollama');
|
|
|
10
10
|
const { save } = require('./config');
|
|
11
11
|
const { httpErr } = require('./hub');
|
|
12
12
|
const github = require('./github');
|
|
13
|
-
const { pairLink, qrSvg, makeCode } = require('./pair');
|
|
13
|
+
const { pairLink, qrSvg, makeCode, codeLink } = require('./pair');
|
|
14
14
|
|
|
15
15
|
// L'app web: copiada dins del paquet en publicar-lo, o l'arrel del repositori en desenvolupament.
|
|
16
16
|
const WEB_DIRS = [path.join(__dirname, '..', 'web'), path.join(__dirname, '..', '..')];
|
|
@@ -232,7 +232,8 @@ function createServer({ cfg, hub, getPublicUrl, getApp }) {
|
|
|
232
232
|
const pub = getPublicUrl();
|
|
233
233
|
if (!pub || /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:|\/|$)/i.test(pub)) return { link: null };
|
|
234
234
|
const link = pairLink(getApp ? getApp() : cfg.app, pub, cfg.token);
|
|
235
|
-
|
|
235
|
+
const code = makeCode(pub, cfg.token);
|
|
236
|
+
return { link, code, svg: qrSvg(codeLink(getApp ? getApp() : cfg.app, code)), url: pub, lan: /^http:\/\//.test(pub) };
|
|
236
237
|
}
|
|
237
238
|
if (p === '/api/github' && method === 'GET') return github.status();
|
|
238
239
|
if (p === '/api/github/repos' && method === 'GET') return github.repos(url.searchParams.get('owner') || '');
|
package/web/app/app.js
CHANGED
|
@@ -28,6 +28,7 @@ const ICON = {
|
|
|
28
28
|
more: '<circle cx="5" cy="12" r="1.3" fill="currentColor"/><circle cx="12" cy="12" r="1.3" fill="currentColor"/><circle cx="19" cy="12" r="1.3" fill="currentColor"/>',
|
|
29
29
|
refresh: '<path d="M20 11a8 8 0 1 0-2.3 5.7M20 4v7h-7"/>', back: '<path d="M15 5l-7 7 7 7"/>',
|
|
30
30
|
bell: '<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>',
|
|
31
|
+
check: '<path class="tick" d="M5 12.5l4.5 4.5L19 7.5"/>',
|
|
31
32
|
copy: '<rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3"/>'
|
|
32
33
|
};
|
|
33
34
|
const AGENT_NAMES = { claude: 'Claude Code', codex: 'Codex', grok: 'Grok', antigravity: 'Antigravity', ollama: 'Ollama', demo: 'Demo', browser: 'Xat local' };
|
|
@@ -194,10 +195,12 @@ function parsePair(text) {
|
|
|
194
195
|
// Codi «nio-…»: porta l'adreça i la clau dins (vegeu makeCode al programa). Es descodifica aquí, sense servidor.
|
|
195
196
|
const CF = '.trycloudflare.com';
|
|
196
197
|
function parseCode(text) {
|
|
197
|
-
const m = String(text || '').replace(/\s+/g, '').match(
|
|
198
|
+
const m = String(text || '').replace(/\s+/g, '').match(/(?:^|#)nio-([\w-]+)$/i); if (!m) return null;
|
|
198
199
|
try {
|
|
199
200
|
const bin = atob(m[1].replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (m[1].length % 4)) % 4)), u = Uint8Array.from(bin, (c) => c.charCodeAt(0));
|
|
200
|
-
|
|
201
|
+
const tok = () => btoa(String.fromCharCode(...u.slice(1, 25))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
202
|
+
if (u[0] === 1) return { url: `https://${new TextDecoder().decode(u.slice(25))}${CF}`, token: tok() };
|
|
203
|
+
if (u[0] === 2) return { url: `http://${[...u.slice(25, 29)].join('.')}:${(u[29] << 8) | u[30]}`, token: tok() };
|
|
201
204
|
if (u[0] === 0) { const [url, token] = new TextDecoder().decode(u.slice(1)).split('\n'); if (url && token) return { url: url.replace(/\/$/, ''), token }; }
|
|
202
205
|
} catch { /* codi malmès */ }
|
|
203
206
|
return null;
|
|
@@ -212,7 +215,7 @@ function addMachine({ url, token, name }) {
|
|
|
212
215
|
connect();
|
|
213
216
|
}
|
|
214
217
|
function checkPairHash() {
|
|
215
|
-
const pair = parsePair(location.hash); if (!pair) return false;
|
|
218
|
+
const pair = parsePair(location.hash) || parseCode(location.hash); if (!pair) return false;
|
|
216
219
|
history.replaceState(null, '', location.pathname + '#/sessions');
|
|
217
220
|
state.route = { name: 'sessions' };
|
|
218
221
|
addMachine(pair); render(); toast('Ordinador connectat'); return true;
|
|
@@ -445,10 +448,17 @@ function renderSessionHead() {
|
|
|
445
448
|
bar.innerHTML = `<span class="chip"><span class="agent-tag" data-agent="${esc(s.agent)}">${esc(AGENT_NAMES[s.agent] || s.agent)}</span></span>
|
|
446
449
|
${ag?.noModes ? '' : `<button class="chip" data-act="mode" type="button">${esc(MODES[s.mode]?.label || s.mode)}</button>`}
|
|
447
450
|
<span class="chip mono">${esc(basename(s.cwd))}</span>
|
|
448
|
-
|
|
451
|
+
<button class="chip mono" data-act="model" type="button" title="Canvia de model">${esc(s.model || 'Model per defecte')}</button>
|
|
449
452
|
${s.cost ? `<span class="chip mono">${s.cost.toFixed(2)} $</span>` : ''}
|
|
450
453
|
${stateChip(s)}`;
|
|
451
454
|
}
|
|
455
|
+
// Alçada del camp de text segons el contingut, sense barra de desplaçament fins al límit.
|
|
456
|
+
function autogrow(ta, max = 180) {
|
|
457
|
+
ta.style.height = 'auto';
|
|
458
|
+
const h = ta.scrollHeight + (ta.offsetHeight - ta.clientHeight);
|
|
459
|
+
ta.style.height = `${Math.min(h, max)}px`;
|
|
460
|
+
ta.style.overflowY = h > max ? 'auto' : 'hidden';
|
|
461
|
+
}
|
|
452
462
|
const nearBottom = (f) => f.scrollHeight - f.scrollTop - f.clientHeight < 140;
|
|
453
463
|
function putEvent(e, scroll = true) {
|
|
454
464
|
const feed = $('#feed'); if (!feed) return;
|
|
@@ -516,7 +526,7 @@ function renderComposer() {
|
|
|
516
526
|
box.dataset.busy = String(busy);
|
|
517
527
|
box.innerHTML = `<div class="inner"><textarea rows="1" id="msg" placeholder="${busy ? 'L’agent està treballant…' : 'Escriu a l’agent…'}" enterkeyhint="send" aria-label="Missatge"></textarea><button class="send ${busy ? 'stop' : ''}" id="sendBtn" type="button" aria-label="${busy ? 'Atura l’agent' : 'Envia'}">${svg(busy ? ICON.stop : ICON.send)}</button></div>`;
|
|
518
528
|
const ta = $('#msg'); ta.value = prev;
|
|
519
|
-
const grow = () =>
|
|
529
|
+
const grow = () => autogrow(ta);
|
|
520
530
|
grow(); ta.addEventListener('input', grow);
|
|
521
531
|
const send = async () => {
|
|
522
532
|
const text = ta.value.trim(); if (!text || !s) return;
|
|
@@ -662,6 +672,28 @@ async function newSessionSheet(preset = {}) {
|
|
|
662
672
|
draw();
|
|
663
673
|
}, 'wiz');
|
|
664
674
|
}
|
|
675
|
+
// Canvia el model d'una sessió. Val a partir del pròxim missatge.
|
|
676
|
+
const COST_FROM = {
|
|
677
|
+
claude: 'El teu compte de Claude Code a l’ordinador (subscripció o clau d’API que hi tinguis)',
|
|
678
|
+
codex: 'El teu compte de Codex (ChatGPT o clau d’OpenAI) a l’ordinador',
|
|
679
|
+
grok: 'El teu compte de Grok a l’ordinador',
|
|
680
|
+
antigravity: 'El teu compte de Google a l’ordinador',
|
|
681
|
+
ollama: 'Res: el model funciona al teu ordinador'
|
|
682
|
+
};
|
|
683
|
+
const MODEL_PICKS = { claude: [['opus', 'Opus', 'El més capaç'], ['sonnet', 'Sonnet', 'Equilibrat'], ['haiku', 'Haiku', 'El més ràpid']] };
|
|
684
|
+
async function modelSheet(s) {
|
|
685
|
+
const picks = s.agent === 'ollama'
|
|
686
|
+
? (await api('/api/ollama/models').catch(() => [])).map((m) => [m.name, m.name, [m.params, m.quant, bytes(m.size)].filter(Boolean).join(' · ')])
|
|
687
|
+
: MODEL_PICKS[s.agent] || [];
|
|
688
|
+
const row = (v, name, note) => `<button class="card" type="button" data-model="${esc(v)}" aria-pressed="${(s.model || '') === v}"><b>${esc(name)}</b>${note ? `<div class="muted small" style="margin-top:4px">${esc(note)}</div>` : ''}</button>`;
|
|
689
|
+
openSheet(`<h2>Model</h2><p class="muted small">${esc(AGENT_NAMES[s.agent] || s.agent)}. El canvi s’aplica a partir del pròxim missatge.</p>
|
|
690
|
+
<div class="list">${row('', 'Per defecte', 'El que l’agent faci servir sol')}${picks.map((x) => row(...x)).join('')}</div>
|
|
691
|
+
<form class="pair-form" id="modelForm"><input type="text" id="modelFree" placeholder="Un altre model (nom exacte)" value="${picks.some((x) => x[0] === s.model) ? '' : esc(s.model || '')}" autocapitalize="off" spellcheck="false" aria-label="Nom del model"><button class="btn ink" type="submit">Desa</button></form>`, (sh) => {
|
|
692
|
+
const save = async (model) => { try { const sum = await api(`/api/sessions/${s.id}`, { method: 'PATCH', body: { model } }); state.sessions.set(s.id, sum); closeSheet(); renderSessionHead(); } catch (e) { toast(e.message); } };
|
|
693
|
+
$$('[data-model]', sh).forEach((b) => (b.onclick = () => save(b.dataset.model)));
|
|
694
|
+
$('#modelForm', sh).onsubmit = (e) => { e.preventDefault(); e.stopPropagation(); save($('#modelFree', sh).value.trim()); };
|
|
695
|
+
});
|
|
696
|
+
}
|
|
665
697
|
function modeSheet(s) {
|
|
666
698
|
openSheet(`<h2>Permisos</h2><div class="list">${Object.entries(MODES).map(([k, m]) => `<button class="card" type="button" data-m="${k}" style="${k === s.mode ? 'border-color:var(--fg);box-shadow:inset 0 0 0 1px var(--fg)' : ''}"><b>${m.label}</b><div class="muted small" style="margin-top:4px">${esc(modeHelp(s.agent, k))}</div></button>`).join('')}</div><p class="muted small">El canvi s’aplica a partir del pròxim missatge.</p>`, (sh) => {
|
|
667
699
|
$$('[data-m]', sh).forEach((b) => (b.onclick = async () => {
|
|
@@ -742,7 +774,7 @@ async function gitSheet(id) {
|
|
|
742
774
|
function sessionOptions(id) {
|
|
743
775
|
const s = state.sessions.get(id); if (!s) return;
|
|
744
776
|
openSheet(`<h2>${esc(s.title || 'Sessió')}</h2>
|
|
745
|
-
<dl class="kv"><dt>Agent</dt><dd>${esc(AGENT_NAMES[s.agent] || s.agent)}</dd><dt>Carpeta</dt><dd class="mono">${esc(s.cwd)}</dd><dt>Permisos</dt><dd>${esc(MODES[s.mode]?.label || '')}</dd><dt>Creada</dt><dd>${new Date(s.created).toLocaleString('ca')}</dd></dl>
|
|
777
|
+
<dl class="kv"><dt>Agent</dt><dd>${esc(AGENT_NAMES[s.agent] || s.agent)}</dd><dt>Carpeta</dt><dd class="mono">${esc(s.cwd)}</dd><dt>Permisos</dt><dd>${esc(MODES[s.mode]?.label || '')}</dd><dt>Model</dt><dd class="mono">${esc(s.model || 'per defecte')}</dd><dt>Es gasta de</dt><dd>${esc(COST_FROM[s.agent] || 'El compte de l’agent a l’ordinador')}. nio no fa cap crida pel seu compte.</dd><dt>Creada</dt><dd>${new Date(s.created).toLocaleString('ca')}</dd></dl>
|
|
746
778
|
<label class="f"><span>Nom de la sessió</span><input type="text" id="renameIn" value="${esc(s.title)}" maxlength="120"></label>
|
|
747
779
|
<button class="btn" data-act="git" type="button">Git i GitHub…</button>
|
|
748
780
|
<div class="row between"><button class="btn danger" id="delBtn" type="button">Esborra la sessió</button><button class="btn ink" id="renameBtn" type="button">Desa</button></div>`, (sh) => {
|
|
@@ -930,7 +962,7 @@ function bindLocal() {
|
|
|
930
962
|
for (const m of local.messages) feed.appendChild(m.role === 'user' ? el(`<div class="msg-user">${esc(m.content)}</div>`) : el(`<div class="msg-ai">${md(stripThink(m.content))}</div>`));
|
|
931
963
|
feed.scrollTop = feed.scrollHeight;
|
|
932
964
|
const ta = $('#lmsg');
|
|
933
|
-
ta.addEventListener('input', () =>
|
|
965
|
+
ta.addEventListener('input', () => autogrow(ta));
|
|
934
966
|
ta.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && !isTouch) { e.preventDefault(); sendLocal(); } });
|
|
935
967
|
$('#lsend').onclick = () => (local.busy ? stopLocal() : sendLocal());
|
|
936
968
|
setLocalBusy(local.busy);
|
|
@@ -964,7 +996,7 @@ async function loadLocal(id) {
|
|
|
964
996
|
async function sendLocal() {
|
|
965
997
|
const ta = $('#lmsg'), text = ta.value.trim();
|
|
966
998
|
if (!text || local.busy) return;
|
|
967
|
-
ta.value = ''; ta
|
|
999
|
+
ta.value = ''; autogrow(ta);
|
|
968
1000
|
const feed = $('#lfeed'); if (!local.messages.length) feed.innerHTML = '';
|
|
969
1001
|
local.messages.push({ role: 'user', content: text });
|
|
970
1002
|
feed.appendChild(el(`<div class="msg-user">${esc(text)}</div>`));
|
|
@@ -990,7 +1022,15 @@ async function sendLocal() {
|
|
|
990
1022
|
document.addEventListener('click', async (e) => {
|
|
991
1023
|
const a = e.target.closest('[data-act], [data-copy], [data-decide], [data-machine], [data-lm]');
|
|
992
1024
|
if (!a) return;
|
|
993
|
-
if (a.dataset.copy) {
|
|
1025
|
+
if (a.dataset.copy) {
|
|
1026
|
+
try { await navigator.clipboard.writeText(a.dataset.copy); } catch { return toast(a.dataset.copy); }
|
|
1027
|
+
if (a.dataset.copied) return;
|
|
1028
|
+
// El botó mostra una marca animada i «Copiat» un moment.
|
|
1029
|
+
const html = a.innerHTML; a.dataset.copied = '1'; a.classList.add('copied');
|
|
1030
|
+
a.innerHTML = `${svg(ICON.check)}${/\S/.test(a.textContent) ? 'Copiat' : ''}`;
|
|
1031
|
+
setTimeout(() => { a.innerHTML = html; a.classList.remove('copied'); delete a.dataset.copied; }, 1700);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
994
1034
|
if (a.dataset.decide) return decide(a.closest('[data-approval]'), a.dataset.decide);
|
|
995
1035
|
if (a.dataset.machine != null) { closeSheet(); store.active = Number(a.dataset.machine); state.ollama = null; state.sessions.clear(); state.approvals.clear(); state.events.clear(); state.loaded.clear(); state.connected = false; state.info = null; connect(); return render(); }
|
|
996
1036
|
if (a.dataset.lm) { if (local.loading) return; LS.set('nio.localModel', a.dataset.lm); return render(); }
|
|
@@ -1002,6 +1042,7 @@ document.addEventListener('click', async (e) => {
|
|
|
1002
1042
|
case 'ollama-del': return deleteModel(a.dataset.name);
|
|
1003
1043
|
case 'ollama-chat': try { state.info = await api('/api/info?refresh=1'); } catch { /* res */ } return newSessionSheet({ agent: 'ollama', model: a.dataset.name });
|
|
1004
1044
|
case 'retry': return connect();
|
|
1045
|
+
case 'model': { const s = state.sessions.get(state.route.id); if (s) modelSheet(s); return; }
|
|
1005
1046
|
case 'mode': { const s = state.sessions.get(state.route.id); if (s) modeSheet(s); return; }
|
|
1006
1047
|
case 'session-more': return sessionOptions(state.route.id);
|
|
1007
1048
|
case 'refresh-agents': try { state.info = await api('/api/info?refresh=1'); render(); toast('Agents actualitzats'); } catch (err) { toast(err.message); } return;
|
package/web/app/style.css
CHANGED
|
@@ -419,3 +419,11 @@ dialog.sheet.wiz .sheet-in { padding: 0; gap: 0; flex: 1; min-height: 0; display
|
|
|
419
419
|
#fsForm input { flex: 1; min-width: 0; }
|
|
420
420
|
.pair-code { display: grid; gap: 4px; text-align: center; }
|
|
421
421
|
.pair-code b { font-size: 30px; letter-spacing: .14em; font-weight: 600; cursor: copy; }
|
|
422
|
+
|
|
423
|
+
/* Enllaços-targeta sense subratllat, camps de text sense barra mínima, animació de «Copiat» */
|
|
424
|
+
a.card, a.card:hover { text-decoration: none; color: inherit; }
|
|
425
|
+
.composer textarea { overflow-y: hidden; }
|
|
426
|
+
.copied { color: var(--moss); }
|
|
427
|
+
.copied .tick { stroke-dasharray: 24; stroke-dashoffset: 24; animation: tick .38s var(--ease) forwards; }
|
|
428
|
+
@keyframes tick { to { stroke-dashoffset: 0; } }
|
|
429
|
+
@media (prefers-reduced-motion: reduce) { .copied .tick { animation: none; stroke-dashoffset: 0; } }
|