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 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
- const link = pairLink(local, local, cfg.token);
80
- if (canOpenBrowser() && openUrl(link)) console.log(` ${c.dim('Panell:')} ${local} ${c.dim('(obert al navegador; --no-open per evitar-ho)')}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nio-cat",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "nio: fes servir Claude Code, Codex, Grok, Antigravity i Ollama del teu ordinador des del mòbil.",
5
5
  "license": "MIT",
6
6
  "author": "f.cat",
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
- ${s.model ? `<span class="chip mono">${esc(s.model)}</span>` : ''}
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 = () => { ta.style.height = 'auto'; ta.style.height = `${Math.min(ta.scrollHeight, 180)}px`; };
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', () => { ta.style.height = 'auto'; ta.style.height = `${Math.min(ta.scrollHeight, 180)}px`; });
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.style.height = 'auto';
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) { try { await navigator.clipboard.writeText(a.dataset.copy); toast('Copiat'); } catch { toast(a.dataset.copy); } return; }
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; } }