@danieltmn/tamnora 0.9.5 → 0.9.6

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/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ Todos los cambios relevantes de este proyecto se documentan en este archivo.
5
5
  El formato sigue [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/)
6
6
  y el proyecto usa [Versionado Semantico](https://semver.org/lang/es/).
7
7
 
8
+ ## [0.9.6] - 2026-10-08
9
+
10
+ ### Cambios
11
+
12
+ - fix(consola): borde inferior y padding inferior en la barra de pestañas
13
+ - feat(consola): iconos/logo (favicon, PWA, topbar) y composer estilo codex
14
+ - docs(website): voz, asesor y actualizacion con un click
15
+
8
16
  ## [0.9.5] - 2026-10-08
9
17
 
10
18
  ### Agregado
@@ -1,10 +1,13 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import { homeDir } from '../../../core/src/index.js';
6
7
  import { parse } from '../lib/util.js';
7
8
 
9
+ const ICON_PATH = fileURLToPath(new URL('../../../server/public/icons/tamnora.ico', import.meta.url));
10
+
8
11
  const OPTIONS = { home: { type: 'string' }, help: { type: 'boolean', short: 'h' } };
9
12
 
10
13
  function autostartPath() {
@@ -76,6 +79,7 @@ export async function autostartCommand(argv) {
76
79
  `$sc.TargetPath = ${psQuote(wscript)};`,
77
80
  `$sc.Arguments = ${psQuote(`"${vbsPath}"`)};`,
78
81
  `$sc.WorkingDirectory = ${psQuote(workdir)};`,
82
+ existsSync(ICON_PATH) ? `$sc.IconLocation = ${psQuote(ICON_PATH)};` : '',
79
83
  '$sc.WindowStyle = 1;',
80
84
  '$sc.Save();',
81
85
  ].join(' ');
@@ -39,6 +39,7 @@ function setCwd(value) {
39
39
  if (!value) return;
40
40
  state.cwd = value;
41
41
  $('#rail-cwd').textContent = value;
42
+ renderCwdPill();
42
43
  if (state.explorerOpen) refreshExplorerMode();
43
44
  }
44
45
 
@@ -478,11 +479,6 @@ async function loadModes() {
478
479
  updateStatus();
479
480
  }
480
481
 
481
- function modeLabel(id) {
482
- const option = [...modeSelect.options].find((o) => o.value === id);
483
- return option ? option.textContent : id;
484
- }
485
-
486
482
  async function setMode(id, { persist = true } = {}) {
487
483
  if (![...modeSelect.options].some((o) => o.value === id)) return;
488
484
  modeSelect.value = id;
@@ -504,11 +500,82 @@ document.addEventListener('keydown', (event) => {
504
500
  }
505
501
  });
506
502
 
503
+ /* --- pills del composer (carpeta, permisos, modelo) ----------------------- */
504
+
505
+ const cwdPill = $('#cwd-pill');
506
+ const cwdPillLabel = $('#cwd-pill-label');
507
+ const modePill = $('#mode-pill');
508
+ const modePillLabel = $('#mode-pill-label');
509
+ const modeMenu = $('#mode-menu');
510
+ const modelPill = $('#model-pill');
511
+ const modelPillLabel = $('#model-pill-label');
512
+
513
+ function renderCwdPill() {
514
+ if (!cwdPillLabel) return;
515
+ cwdPillLabel.textContent = state.cwd ? basename(state.cwd) : 'sin carpeta';
516
+ if (cwdPill) cwdPill.title = state.cwd || 'sin carpeta de trabajo';
517
+ }
518
+
519
+ // El pill de permisos resume modo + auto-aprobación en uno de tres niveles
520
+ // estilo Codex: Read Only, Workspace Write y Full access.
521
+ function renderModePill() {
522
+ const plan = modeSelect.value === 'plan';
523
+ const full = !plan && Boolean(yesInput?.checked);
524
+ if (modePillLabel) modePillLabel.textContent = plan ? 'Read Only' : full ? 'Full access' : 'Workspace Write';
525
+ if (!modeMenu) return;
526
+ for (const item of modeMenu.querySelectorAll('.ctrl-menu-item')) {
527
+ const kind = item.dataset.kind;
528
+ const active = kind === 'read' ? plan : kind === 'full' ? full : !plan && !full;
529
+ item.classList.toggle('active', active);
530
+ item.setAttribute('aria-checked', active ? 'true' : 'false');
531
+ }
532
+ }
533
+
534
+ function renderModelPill() {
535
+ if (!modelPillLabel) return;
536
+ modelPillLabel.textContent = state.active.model || 'elegí un modelo';
537
+ if (modelPill) modelPill.title = `${state.active.provider || '—'} / ${state.active.model || '—'}`;
538
+ }
539
+
540
+ function closeModeMenu() {
541
+ if (!modeMenu || modeMenu.hidden) return;
542
+ modeMenu.hidden = true;
543
+ modePill?.setAttribute('aria-expanded', 'false');
544
+ }
545
+
546
+ cwdPill?.addEventListener('click', () => openFolderPicker('session'));
547
+ modePill?.addEventListener('click', (event) => {
548
+ event.stopPropagation();
549
+ if (!modeMenu) return;
550
+ modeMenu.hidden = !modeMenu.hidden;
551
+ modePill.setAttribute('aria-expanded', modeMenu.hidden ? 'false' : 'true');
552
+ });
553
+ modeMenu?.addEventListener('click', async (event) => {
554
+ const item = event.target.closest('.ctrl-menu-item');
555
+ if (!item) return;
556
+ const kind = item.dataset.kind;
557
+ closeModeMenu();
558
+ if (kind === 'full') {
559
+ await setMode('agent');
560
+ if (yesInput) yesInput.checked = true;
561
+ } else {
562
+ if (yesInput) yesInput.checked = false;
563
+ await setMode(kind === 'read' ? 'plan' : 'agent');
564
+ }
565
+ renderModePill();
566
+ });
567
+ document.addEventListener('click', (event) => {
568
+ if (modeMenu && !modeMenu.hidden && !event.target.closest('.ctrl-wrap')) closeModeMenu();
569
+ });
570
+ document.addEventListener('keydown', (event) => {
571
+ if (event.key === 'Escape') closeModeMenu();
572
+ });
573
+ modelPill?.addEventListener('click', () => openSettingsModal());
574
+ yesInput?.addEventListener('change', renderModePill);
575
+
507
576
  function updateStatus(extra = {}) {
508
- const p = state.providers.find((x) => x.id === state.active.provider);
509
- $('#sb-mode').textContent = modeLabel(modeSelect.value) || 'Build';
510
- $('#sb-provider').textContent = p ? `${p.id}` : state.active.provider || '—';
511
- $('#sb-model').textContent = state.active.model || '—';
577
+ renderModePill();
578
+ renderModelPill();
512
579
  if (extra.rounds !== undefined) $('#sb-rounds').textContent = `${extra.rounds} rondas`;
513
580
  if (extra.stopReason) $('#sb-rounds').textContent = extra.stopReason;
514
581
  if (extra.running) $('#sb-rounds').textContent = 'pensando…';
@@ -1814,8 +1881,9 @@ function activateSession(id) {
1814
1881
 
1815
1882
  function updateSubmitLabel(working) {
1816
1883
  if (!submitBtn) return;
1817
- submitBtn.textContent = working ? 'encolar' : 'enviar';
1818
- submitBtn.title = working ? 'El agente está trabajando: se envía al terminar' : '';
1884
+ submitBtn.dataset.working = working ? '1' : '0';
1885
+ submitBtn.setAttribute('aria-label', working ? 'encolar' : 'enviar');
1886
+ submitBtn.title = working ? 'El agente está trabajando: se envía al terminar' : 'enviar';
1819
1887
  }
1820
1888
 
1821
1889
  function auditStatus(record) {
@@ -5419,10 +5487,45 @@ function applyState(diff) {
5419
5487
 
5420
5488
  /* --- señales de alerta: que se note aun mirando otra cosa ----------------- */
5421
5489
 
5422
- const FAVICON_IDLE =
5423
- "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%23231a17'/%3E%3Ctext x='16' y='23' font-family='Georgia,serif' font-size='18' font-weight='700' fill='%23e8dfd6' text-anchor='middle'%3ET%3C/text%3E%3C/svg%3E";
5424
- const FAVICON_ALERT =
5425
- "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%23231a17'/%3E%3Ctext x='16' y='23' font-family='Georgia,serif' font-size='18' font-weight='700' fill='%23e8dfd6' text-anchor='middle'%3ET%3C/text%3E%3Ccircle cx='24' cy='9' r='5' fill='%23d9704a'/%3E%3C/svg%3E";
5490
+ const FAVICON_IDLE = '/favicon.ico';
5491
+ const FAVICON_SOURCE = '/icons/icon-192x192.png';
5492
+ let faviconAlert = '';
5493
+ let faviconAlertPending = null;
5494
+
5495
+ /**
5496
+ * El favicon de alerta se dibuja sobre una copia del logo (mismo origen), para
5497
+ * que el punto rojo combine con el icono real y no con un SVG desincronizado.
5498
+ */
5499
+ function alertFavicon() {
5500
+ if (faviconAlert) return Promise.resolve(faviconAlert);
5501
+ if (faviconAlertPending) return faviconAlertPending;
5502
+ faviconAlertPending = new Promise((resolve) => {
5503
+ const img = new Image();
5504
+ img.onload = () => {
5505
+ try {
5506
+ const canvas = document.createElement('canvas');
5507
+ canvas.width = 64;
5508
+ canvas.height = 64;
5509
+ const ctx = canvas.getContext('2d');
5510
+ ctx.drawImage(img, 0, 0, 64, 64);
5511
+ ctx.beginPath();
5512
+ ctx.arc(64 - 16, 16, 13, 0, Math.PI * 2);
5513
+ ctx.fillStyle = '#d9704a';
5514
+ ctx.fill();
5515
+ ctx.lineWidth = 3;
5516
+ ctx.strokeStyle = 'rgba(35,26,23,0.9)';
5517
+ ctx.stroke();
5518
+ faviconAlert = canvas.toDataURL('image/png');
5519
+ resolve(faviconAlert);
5520
+ } catch {
5521
+ resolve(FAVICON_IDLE);
5522
+ }
5523
+ };
5524
+ img.onerror = () => resolve(FAVICON_IDLE);
5525
+ img.src = FAVICON_SOURCE;
5526
+ });
5527
+ return faviconAlertPending;
5528
+ }
5426
5529
 
5427
5530
  let baseTitle = '';
5428
5531
 
@@ -5437,7 +5540,10 @@ function renderAlertSignals() {
5437
5540
  const clean = baseTitle.replace(/^\(⚠ \d+\) /, '');
5438
5541
  document.title = pending > 0 ? `(⚠ ${pending}) ${clean}` : clean;
5439
5542
  const link = document.querySelector('link[rel="icon"]');
5440
- if (link) link.href = pending > 0 ? FAVICON_ALERT : FAVICON_IDLE;
5543
+ if (link) {
5544
+ if (pending > 0) alertFavicon().then((href) => { link.href = href; });
5545
+ else link.href = FAVICON_IDLE;
5546
+ }
5441
5547
  }
5442
5548
 
5443
5549
  /* --- canal global: solo latencia ------------------------------------------ */
@@ -4,7 +4,10 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>Tamnora Console</title>
7
- <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%23231a17'/%3E%3Ctext x='16' y='23' font-family='Georgia,serif' font-size='18' font-weight='700' fill='%23e8dfd6' text-anchor='middle'%3ET%3C/text%3E%3C/svg%3E" />
7
+ <link rel="icon" href="/favicon.ico" sizes="any" />
8
+ <link rel="apple-touch-icon" href="/icons/icon-192x192.png" />
9
+ <link rel="manifest" href="/manifest.webmanifest" />
10
+ <meta name="theme-color" content="#231a17" />
8
11
  <script>
9
12
  (function () {
10
13
  var theme = null;
@@ -34,7 +37,7 @@
34
37
  </head>
35
38
  <body>
36
39
  <header class="topbar">
37
- <div class="brand"><span class="logo"><span class="logo-t">T</span><span class="logo-h">H</span></span> <span class="ver">v0.9.5</span></div>
40
+ <div class="brand"><span class="logo"><img src="/icons/logo.png" alt="Tamnora" /></span> <span class="ver">v0.9.6</span></div>
38
41
  <nav class="tabs">
39
42
  <button class="tab active" data-tab="chat">chat</button>
40
43
  <button class="tab" data-tab="providers">proveedores</button>
@@ -151,16 +154,45 @@
151
154
  <div id="attach-preview" class="attach-preview" hidden></div>
152
155
  <div id="chat-queue" class="chat-queue" hidden></div>
153
156
  <div id="audio-player" class="audio-player" hidden></div>
154
- <div class="composer-row">
155
- <span class="prompt-mark">❯</span>
156
- <button type="button" id="attach-image" class="btn icon" title="Adjuntar imagen (o pegala / arrastrala)" aria-label="adjuntar imagen">
157
+ <div class="composer-top">
158
+ <button type="button" id="cwd-pill" class="ctrl-pill" title="carpeta de trabajo de la sesión">
157
159
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
158
- <rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
159
- <circle cx="9" cy="9" r="2" />
160
- <path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
160
+ <path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
161
161
  </svg>
162
+ <span id="cwd-pill-label">sin carpeta</span>
163
+ <svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
164
+ </button>
165
+ <div class="ctrl-wrap">
166
+ <button type="button" id="mode-pill" class="ctrl-pill" aria-haspopup="menu" aria-expanded="false" title="permisos del agente">
167
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></svg>
168
+ <span id="mode-pill-label">Workspace Write</span>
169
+ <svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
170
+ </button>
171
+ <div id="mode-menu" class="ctrl-menu" role="menu" hidden>
172
+ <button type="button" class="ctrl-menu-item" data-kind="read" role="menuitemradio" aria-checked="false">
173
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" /><circle cx="12" cy="12" r="3" /></svg>
174
+ <span>Read Only</span>
175
+ <svg class="ctrl-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5" /></svg>
176
+ </button>
177
+ <button type="button" class="ctrl-menu-item" data-kind="write" role="menuitemradio" aria-checked="false">
178
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" /><path d="m15 5 4 4" /></svg>
179
+ <span>Workspace Write</span>
180
+ <svg class="ctrl-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5" /></svg>
181
+ </button>
182
+ <button type="button" class="ctrl-menu-item" data-kind="full" role="menuitemradio" aria-checked="false">
183
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /><path d="m9 12 2 2 4-4" /></svg>
184
+ <span>Full access</span>
185
+ <svg class="ctrl-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5" /></svg>
186
+ </button>
187
+ </div>
188
+ </div>
189
+ </div>
190
+
191
+ <div class="composer-row">
192
+ <button type="button" id="attach-image" class="add-btn" title="Adjuntar imagen (o pegala / arrastrala)" aria-label="adjuntar imagen">
193
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14" /><path d="M12 5v14" /></svg>
162
194
  </button>
163
- <button type="button" id="mic-btn" class="btn icon" title="Dictar por voz (tocá para grabar y parar)" aria-label="dictar por voz">
195
+ <button type="button" id="mic-btn" class="add-btn" title="Dictar por voz (tocá para grabar y parar)" aria-label="dictar por voz">
164
196
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
165
197
  <path d="M12 19v3" />
166
198
  <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
@@ -170,27 +202,30 @@
170
202
  </button>
171
203
  <input id="image-input" type="file" accept="image/*" multiple hidden />
172
204
  <textarea id="chat-input" rows="1" placeholder="escribí un mensaje…" title="Enter envía · Shift+Enter salto" spellcheck="false"></textarea>
173
- <button class="btn primary" type="submit">enviar</button>
174
- <button class="btn" type="button" id="stop-btn" title="Detener la respuesta" hidden>parar</button>
205
+ <button type="button" id="model-pill" class="ctrl-pill model-pill" title="proveedor y modelo activos">
206
+ <span id="model-pill-label">—</span>
207
+ <svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
208
+ </button>
209
+ <button class="send-btn" type="submit" aria-label="enviar" title="enviar">
210
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 7-7 7 7" /><path d="M12 19V5" /></svg>
211
+ </button>
212
+ <button class="add-btn" type="button" id="stop-btn" title="Detener la respuesta" aria-label="detener" hidden>
213
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2" /></svg>
214
+ </button>
215
+ </div>
216
+
217
+ <div id="statusbar" class="composer-status" title="estado del run" role="button" tabindex="0">
218
+ <span id="sb-activity" class="sb-activity" hidden></span>
219
+ <span id="sb-sync" class="sb-channel retrying" hidden title="sin sincronía con el server"></span>
220
+ <span id="sb-channel" class="sb-channel" hidden title="canal de eventos entre dispositivos"></span>
221
+ <span id="sb-rounds">listo</span>
222
+ <span class="spacer"></span>
223
+ <span id="sb-context" class="ctx muted" title="uso del contexto del modelo">
224
+ <span id="sb-tokens">tokens —</span>
225
+ <span class="ctx-bar" aria-hidden="true"><span id="sb-context-fill" class="ctx-fill"></span></span>
226
+ </span>
175
227
  </div>
176
228
  </form>
177
- <div id="statusbar" class="statusbar" title="cambiar modo, proveedor y modelo" role="button" tabindex="0">
178
- <span id="sb-mode">agent</span>
179
- <span class="dot">·</span>
180
- <span id="sb-provider">—</span>
181
- <span class="dot">·</span>
182
- <span id="sb-model">—</span>
183
- <span class="dot">·</span>
184
- <span id="sb-activity" class="sb-activity" hidden></span>
185
- <span id="sb-sync" class="sb-channel retrying" hidden title="sin sincronía con el server"></span>
186
- <span id="sb-channel" class="sb-channel" hidden title="canal de eventos entre dispositivos"></span>
187
- <span id="sb-rounds">listo</span>
188
- <span class="spacer"></span>
189
- <span id="sb-context" class="ctx muted" title="uso del contexto del modelo">
190
- <span id="sb-tokens">tokens —</span>
191
- <span class="ctx-bar" aria-hidden="true"><span id="sb-context-fill" class="ctx-fill"></span></span>
192
- </span>
193
- </div>
194
229
  </div>
195
230
 
196
231
  <div id="resizer-explorer" class="resizer" title="ajustar" role="separator" aria-orientation="vertical"></div>
@@ -4,6 +4,10 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>Tamnora Harness</title>
7
+ <link rel="icon" href="/favicon.ico" sizes="any" />
8
+ <link rel="apple-touch-icon" href="/icons/icon-192x192.png" />
9
+ <link rel="manifest" href="/manifest.webmanifest" />
10
+ <meta name="theme-color" content="#231a17" />
7
11
  <script>
8
12
  (function () {
9
13
  var theme = null;
@@ -129,13 +133,13 @@
129
133
  border-radius: 11px;
130
134
  display: grid;
131
135
  place-items: center;
136
+ overflow: hidden;
132
137
  background: var(--surface);
133
138
  backdrop-filter: blur(10px);
134
- font-family: Georgia, serif;
135
- font-weight: 700;
136
- font-size: 19px;
137
139
  }
138
140
 
141
+ .logo-mark img { width: 100%; height: 100%; display: block; object-fit: cover; }
142
+
139
143
  .logo-text strong {
140
144
  display: block;
141
145
  font-family: var(--serif);
@@ -420,7 +424,7 @@
420
424
  <div class="page">
421
425
  <header>
422
426
  <a href="/landing" class="logo">
423
- <div class="logo-mark">T</div>
427
+ <div class="logo-mark"><img src="/icons/logo.png" alt="Tamnora" /></div>
424
428
  <div class="logo-text">
425
429
  <strong>Tamnora Harness</strong>
426
430
  <small>Harness de agentes IA</small>
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "Tamnora Harness",
3
+ "short_name": "Tamnora",
4
+ "description": "Consola web del harness local de agentes IA.",
5
+ "start_url": "/",
6
+ "scope": "/",
7
+ "display": "standalone",
8
+ "background_color": "#f5efe2",
9
+ "theme_color": "#231a17",
10
+ "icons": [
11
+ { "src": "/icons/icon-72x72.png", "sizes": "72x72", "type": "image/png" },
12
+ { "src": "/icons/icon-96x96.png", "sizes": "96x96", "type": "image/png" },
13
+ { "src": "/icons/icon-128x128.png", "sizes": "128x128", "type": "image/png" },
14
+ { "src": "/icons/icon-144x144.png", "sizes": "144x144", "type": "image/png" },
15
+ { "src": "/icons/icon-152x152.png", "sizes": "152x152", "type": "image/png" },
16
+ { "src": "/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
17
+ { "src": "/icons/icon-384x384.png", "sizes": "384x384", "type": "image/png" },
18
+ { "src": "/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }
19
+ ]
20
+ }