nio-cat 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/nio.js +5 -2
- package/package.json +1 -1
- package/web/app/app.js +46 -7
- package/web/app/style.css +8 -0
package/bin/nio.js
CHANGED
|
@@ -76,8 +76,10 @@ function canOpenBrowser() {
|
|
|
76
76
|
|
|
77
77
|
function openDashboard() {
|
|
78
78
|
const local = `http://127.0.0.1:${cfg.port}`;
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
// Per defecte s'obre l'app en línia (sempre al dia), ja connectada a aquest ordinador. Amb --local, la còpia que porta nio.
|
|
80
|
+
const online = !flags.local;
|
|
81
|
+
const link = pairLink(online ? flags.app || cfg.app : local, local, cfg.token);
|
|
82
|
+
if (canOpenBrowser() && openUrl(link)) console.log(` ${c.dim('Panell:')} ${online ? (flags.app || cfg.app) + '/app' : local} ${c.dim('(obert al navegador; --no-open per evitar-ho' + (online ? '; --local per fer servir la còpia d\'aquest ordinador' : '') + ')')}`);
|
|
81
83
|
else console.log(` ${c.dim('Panell:')} ${link}`);
|
|
82
84
|
}
|
|
83
85
|
|
|
@@ -288,6 +290,7 @@ async function start() {
|
|
|
288
290
|
--no-tunnel Només des d'aquest ordinador
|
|
289
291
|
--port <n> Port local (7717 si no dius res)
|
|
290
292
|
--no-open No obris el panell al navegador d'aquest ordinador
|
|
293
|
+
--local Obre la còpia del panell d'aquest ordinador (si el navegador bloqueja nio.cat, p. ex. Safari)
|
|
291
294
|
--open Obre al navegador la pàgina del codi QR
|
|
292
295
|
nio pair Mostra el codi QR una altra vegada
|
|
293
296
|
|
package/package.json
CHANGED
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' };
|
|
@@ -139,7 +140,7 @@ async function connect() {
|
|
|
139
140
|
} catch (e) {
|
|
140
141
|
if (ctrl.signal.aborted) return;
|
|
141
142
|
state.connected = false;
|
|
142
|
-
state.lastError = e.message === 'Failed to fetch' || e.name === 'TypeError' ? 'No arribo a l’ordinador' : e.message;
|
|
143
|
+
state.lastError = e.message === 'Failed to fetch' || e.name === 'TypeError' ? (location.protocol === 'https:' && /^http:\/\/(127\.0\.0\.1|localhost)/.test(m.url) ? 'El navegador no deixa que aquesta web parli amb aquest ordinador. Prova Chrome, o engega nio amb --local.' : 'No arribo a l’ordinador') : e.message;
|
|
143
144
|
render();
|
|
144
145
|
if (e.fatal) return;
|
|
145
146
|
setTimeout(() => machine() && !ctrl.signal.aborted && connect(), Math.min(15000, 1000 * 2 ** retry++));
|
|
@@ -447,10 +448,17 @@ function renderSessionHead() {
|
|
|
447
448
|
bar.innerHTML = `<span class="chip"><span class="agent-tag" data-agent="${esc(s.agent)}">${esc(AGENT_NAMES[s.agent] || s.agent)}</span></span>
|
|
448
449
|
${ag?.noModes ? '' : `<button class="chip" data-act="mode" type="button">${esc(MODES[s.mode]?.label || s.mode)}</button>`}
|
|
449
450
|
<span class="chip mono">${esc(basename(s.cwd))}</span>
|
|
450
|
-
|
|
451
|
+
<button class="chip mono" data-act="model" type="button" title="Canvia de model">${esc(s.model || 'Model per defecte')}</button>
|
|
451
452
|
${s.cost ? `<span class="chip mono">${s.cost.toFixed(2)} $</span>` : ''}
|
|
452
453
|
${stateChip(s)}`;
|
|
453
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
|
+
}
|
|
454
462
|
const nearBottom = (f) => f.scrollHeight - f.scrollTop - f.clientHeight < 140;
|
|
455
463
|
function putEvent(e, scroll = true) {
|
|
456
464
|
const feed = $('#feed'); if (!feed) return;
|
|
@@ -518,7 +526,7 @@ function renderComposer() {
|
|
|
518
526
|
box.dataset.busy = String(busy);
|
|
519
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>`;
|
|
520
528
|
const ta = $('#msg'); ta.value = prev;
|
|
521
|
-
const grow = () =>
|
|
529
|
+
const grow = () => autogrow(ta);
|
|
522
530
|
grow(); ta.addEventListener('input', grow);
|
|
523
531
|
const send = async () => {
|
|
524
532
|
const text = ta.value.trim(); if (!text || !s) return;
|
|
@@ -664,6 +672,28 @@ async function newSessionSheet(preset = {}) {
|
|
|
664
672
|
draw();
|
|
665
673
|
}, 'wiz');
|
|
666
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
|
+
}
|
|
667
697
|
function modeSheet(s) {
|
|
668
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) => {
|
|
669
699
|
$$('[data-m]', sh).forEach((b) => (b.onclick = async () => {
|
|
@@ -744,7 +774,7 @@ async function gitSheet(id) {
|
|
|
744
774
|
function sessionOptions(id) {
|
|
745
775
|
const s = state.sessions.get(id); if (!s) return;
|
|
746
776
|
openSheet(`<h2>${esc(s.title || 'Sessió')}</h2>
|
|
747
|
-
<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>
|
|
748
778
|
<label class="f"><span>Nom de la sessió</span><input type="text" id="renameIn" value="${esc(s.title)}" maxlength="120"></label>
|
|
749
779
|
<button class="btn" data-act="git" type="button">Git i GitHub…</button>
|
|
750
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) => {
|
|
@@ -932,7 +962,7 @@ function bindLocal() {
|
|
|
932
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>`));
|
|
933
963
|
feed.scrollTop = feed.scrollHeight;
|
|
934
964
|
const ta = $('#lmsg');
|
|
935
|
-
ta.addEventListener('input', () =>
|
|
965
|
+
ta.addEventListener('input', () => autogrow(ta));
|
|
936
966
|
ta.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && !isTouch) { e.preventDefault(); sendLocal(); } });
|
|
937
967
|
$('#lsend').onclick = () => (local.busy ? stopLocal() : sendLocal());
|
|
938
968
|
setLocalBusy(local.busy);
|
|
@@ -966,7 +996,7 @@ async function loadLocal(id) {
|
|
|
966
996
|
async function sendLocal() {
|
|
967
997
|
const ta = $('#lmsg'), text = ta.value.trim();
|
|
968
998
|
if (!text || local.busy) return;
|
|
969
|
-
ta.value = ''; ta
|
|
999
|
+
ta.value = ''; autogrow(ta);
|
|
970
1000
|
const feed = $('#lfeed'); if (!local.messages.length) feed.innerHTML = '';
|
|
971
1001
|
local.messages.push({ role: 'user', content: text });
|
|
972
1002
|
feed.appendChild(el(`<div class="msg-user">${esc(text)}</div>`));
|
|
@@ -992,7 +1022,15 @@ async function sendLocal() {
|
|
|
992
1022
|
document.addEventListener('click', async (e) => {
|
|
993
1023
|
const a = e.target.closest('[data-act], [data-copy], [data-decide], [data-machine], [data-lm]');
|
|
994
1024
|
if (!a) return;
|
|
995
|
-
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
|
+
}
|
|
996
1034
|
if (a.dataset.decide) return decide(a.closest('[data-approval]'), a.dataset.decide);
|
|
997
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(); }
|
|
998
1036
|
if (a.dataset.lm) { if (local.loading) return; LS.set('nio.localModel', a.dataset.lm); return render(); }
|
|
@@ -1004,6 +1042,7 @@ document.addEventListener('click', async (e) => {
|
|
|
1004
1042
|
case 'ollama-del': return deleteModel(a.dataset.name);
|
|
1005
1043
|
case 'ollama-chat': try { state.info = await api('/api/info?refresh=1'); } catch { /* res */ } return newSessionSheet({ agent: 'ollama', model: a.dataset.name });
|
|
1006
1044
|
case 'retry': return connect();
|
|
1045
|
+
case 'model': { const s = state.sessions.get(state.route.id); if (s) modelSheet(s); return; }
|
|
1007
1046
|
case 'mode': { const s = state.sessions.get(state.route.id); if (s) modeSheet(s); return; }
|
|
1008
1047
|
case 'session-more': return sessionOptions(state.route.id);
|
|
1009
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; } }
|