@danieltmn/tamnora 0.6.2 → 0.7.1

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/SECURITY.md +6 -0
  3. package/harness/packages/cli/src/commands/repl.js +2 -0
  4. package/harness/packages/cli/src/commands/run.js +2 -0
  5. package/harness/packages/cli/src/commands/tui.js +2 -0
  6. package/harness/packages/cli/src/lib/runtime.js +4 -0
  7. package/harness/packages/cli/src/tui/app.js +4 -0
  8. package/harness/packages/core/src/agent/runAgent.js +131 -47
  9. package/harness/packages/core/src/config/index.js +3 -1
  10. package/harness/packages/core/src/config/secrets.js +36 -0
  11. package/harness/packages/core/src/eval/runTask.js +2 -11
  12. package/harness/packages/core/src/index.js +13 -1
  13. package/harness/packages/core/src/mcp/common.js +15 -11
  14. package/harness/packages/core/src/mcp/index.js +1 -1
  15. package/harness/packages/core/src/mcp/registry.js +1 -0
  16. package/harness/packages/core/src/mcp/stdio.js +41 -2
  17. package/harness/packages/core/src/redact.js +104 -0
  18. package/harness/packages/core/test/agent.test.js +93 -0
  19. package/harness/packages/core/test/mcp.test.js +67 -0
  20. package/harness/packages/core/test/redact.test.js +45 -0
  21. package/harness/packages/server/public/app.js +368 -41
  22. package/harness/packages/server/public/index.html +102 -26
  23. package/harness/packages/server/public/markdown.js +43 -0
  24. package/harness/packages/server/public/styles.css +171 -21
  25. package/harness/packages/server/src/lib/agentTools.js +9 -3
  26. package/harness/packages/server/src/lib/channels/telegram.js +11 -5
  27. package/harness/packages/server/src/routes/chat.js +8 -1
  28. package/harness/packages/server/src/routes/mcp.js +29 -5
  29. package/harness/packages/server/src/routes/providers.js +16 -15
  30. package/harness/packages/server/src/server.js +13 -0
  31. package/harness/packages/server/test/markdown.test.js +33 -0
  32. package/harness/packages/server/test/server.test.js +77 -1
  33. package/harness/tamnora.example.json +17 -1
  34. package/package.json +1 -1
@@ -33,7 +33,7 @@
33
33
  </head>
34
34
  <body>
35
35
  <header class="topbar">
36
- <div class="brand"><span class="logo"><span class="logo-t">T</span><span class="logo-h">H</span></span> <span class="ver">v0.6.2</span></div>
36
+ <div class="brand"><span class="logo"><span class="logo-t">T</span><span class="logo-h">H</span></span> <span class="ver">v0.7.1</span></div>
37
37
  <nav class="tabs">
38
38
  <button class="tab active" data-tab="chat">chat</button>
39
39
  <button class="tab" data-tab="providers">proveedores</button>
@@ -116,8 +116,21 @@
116
116
  </div>
117
117
  </div>
118
118
  </div>
119
+ <div id="scroll-nav" class="scroll-nav" hidden>
120
+ <button id="scroll-top" type="button" class="scroll-btn" title="ir al inicio" aria-label="ir al inicio de la conversación">
121
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
122
+ <path d="m18 15-6-6-6 6" />
123
+ </svg>
124
+ </button>
125
+ <button id="scroll-down" type="button" class="scroll-btn" title="ir al final" aria-label="ir al final de la conversación">
126
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
127
+ <path d="m6 9 6 6 6-6" />
128
+ </svg>
129
+ </button>
130
+ </div>
119
131
  </div>
120
132
  <form id="chat-form" class="composer">
133
+ <div id="slash-menu" class="slash-menu" hidden></div>
121
134
  <div id="attach-preview" class="attach-preview" hidden></div>
122
135
  <div class="composer-row">
123
136
  <span class="prompt-mark">❯</span>
@@ -134,13 +147,14 @@
134
147
  <button class="btn" type="button" id="stop-btn" title="Detener la respuesta" hidden>parar</button>
135
148
  </div>
136
149
  </form>
137
- <div class="statusbar">
150
+ <div id="statusbar" class="statusbar" title="cambiar modo, proveedor y modelo" role="button" tabindex="0">
138
151
  <span id="sb-mode">agent</span>
139
152
  <span class="dot">·</span>
140
153
  <span id="sb-provider">—</span>
141
154
  <span class="dot">·</span>
142
155
  <span id="sb-model">—</span>
143
156
  <span class="dot">·</span>
157
+ <span id="sb-activity" class="sb-activity" hidden></span>
144
158
  <span id="sb-rounds">listo</span>
145
159
  <span class="spacer"></span>
146
160
  <span id="sb-context" class="ctx muted" title="uso del contexto del modelo">
@@ -343,7 +357,62 @@
343
357
  </section>
344
358
 
345
359
  <section id="view-ajustes" class="view">
346
- <div class="panel">
360
+ <div class="ajustes-wrap">
361
+ <nav class="ajustes-tabs" role="tablist" aria-label="secciones de ajustes">
362
+ <button type="button" class="ajustes-tab active" data-panel="agente" role="tab">Agente</button>
363
+ <button type="button" class="ajustes-tab" data-panel="sesiones" role="tab">Sesiones</button>
364
+ <button type="button" class="ajustes-tab" data-panel="mcp" role="tab">MCP</button>
365
+ <button type="button" class="ajustes-tab" data-panel="skills" role="tab">Skills</button>
366
+ <button type="button" class="ajustes-tab" data-panel="telegram" role="tab">Telegram</button>
367
+ <button type="button" class="ajustes-tab" data-panel="limites" role="tab">Límites</button>
368
+ </nav>
369
+
370
+ <div class="panel ajustes-panel active" data-panel="agente">
371
+ <div class="row between">
372
+ <h2>Agente</h2>
373
+ </div>
374
+ <p class="muted verify-line">
375
+ Modo, proveedor, modelo y aprobación automática del agente.
376
+ </p>
377
+ <div id="agent-controls-slot">
378
+ <div id="agent-settings-controls">
379
+ <label for="mode">Modo</label>
380
+ <select id="mode" title="modo de agente"></select>
381
+ <label for="provider">Proveedor</label>
382
+ <select id="provider" title="proveedor"></select>
383
+ <label for="model">Modelo</label>
384
+ <select id="model" title="modelo"></select>
385
+ <label class="check" title="aprobar automáticamente tools con permiso ask">
386
+ <input id="yes" type="checkbox" /> Aprobar automáticamente las tools (auto)
387
+ </label>
388
+ </div>
389
+ </div>
390
+ <div class="row">
391
+ <button type="button" id="agent-settings-open" class="btn">abrir en ventana</button>
392
+ <button type="button" id="agent-providers" class="btn">gestionar proveedores</button>
393
+ </div>
394
+ </div>
395
+
396
+ <div class="panel ajustes-panel" data-panel="sesiones">
397
+ <div class="row between">
398
+ <h2>Sesiones</h2>
399
+ </div>
400
+ <p class="muted verify-line">
401
+ Carpeta donde se abre el selector de carpetas y <code>/new</code> cuando no hay sesión.
402
+ </p>
403
+ <label for="default-cwd">Carpeta predeterminada para nuevas sesiones</label>
404
+ <div class="row default-cwd-row">
405
+ <input id="default-cwd" type="text" readonly placeholder="(la casa del usuario)" title="carpeta donde se abre el selector y /new sin sesión" />
406
+ <button type="button" id="default-cwd-pick" class="btn">elegir</button>
407
+ <button type="button" id="default-cwd-clear" class="btn icon" title="quitar la carpeta predeterminada" aria-label="quitar la carpeta predeterminada">
408
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
409
+ <path d="M18 6 6 18M6 6l12 12" />
410
+ </svg>
411
+ </button>
412
+ </div>
413
+ </div>
414
+
415
+ <div class="panel ajustes-panel" data-panel="mcp">
347
416
  <div class="row between">
348
417
  <h2>Servidores MCP</h2>
349
418
  <div class="row">
@@ -362,7 +431,7 @@
362
431
  <ul id="mcp-list" class="providers"></ul>
363
432
  </div>
364
433
 
365
- <div class="panel">
434
+ <div class="panel ajustes-panel" data-panel="skills">
366
435
  <div class="row between">
367
436
  <h2>Skills</h2>
368
437
  <button id="skill-new" class="btn">+ nueva</button>
@@ -378,7 +447,7 @@
378
447
  <ul id="skill-list" class="providers"></ul>
379
448
  </div>
380
449
 
381
- <div class="panel">
450
+ <div class="panel ajustes-panel" data-panel="telegram">
382
451
  <div class="row between">
383
452
  <h2>Telegram</h2>
384
453
  <span id="tg-status" class="summary">cargando…</span>
@@ -416,6 +485,30 @@
416
485
  <span id="tg-msg" class="summary"></span>
417
486
  </div>
418
487
  </div>
488
+
489
+ <div class="panel ajustes-panel" data-panel="limites">
490
+ <div class="row between">
491
+ <h2>Límites del agente</h2>
492
+ </div>
493
+ <p class="muted verify-line">
494
+ El agente trabaja hasta terminar solo (<code>end_turn</code>). Estos topes son
495
+ opcionales: <code>0</code> = sin límite. Al cortarse, cierra con un resumen.
496
+ </p>
497
+ <label class="field"><span>máximo de rondas de herramientas (0 = sin límite)</span>
498
+ <input id="limits-rounds" type="number" min="0" step="1" placeholder="0" />
499
+ </label>
500
+ <label class="field"><span>presupuesto de tokens (0 = sin límite)</span>
501
+ <input id="limits-tokens" type="number" min="0" step="1000" placeholder="0" />
502
+ </label>
503
+ <label class="field"><span>presupuesto de tiempo en segundos (0 = sin límite)</span>
504
+ <input id="limits-duration" type="number" min="0" step="1" placeholder="0" />
505
+ </label>
506
+ <div class="row">
507
+ <button id="limits-save" class="btn primary" type="button">guardar</button>
508
+ <span id="limits-msg" class="summary"></span>
509
+ </div>
510
+ </div>
511
+ </div>
419
512
  </section>
420
513
 
421
514
  <dialog id="folder-modal" class="modal folder-modal">
@@ -485,25 +578,7 @@
485
578
  </svg>
486
579
  </button>
487
580
  </div>
488
- <label for="mode">Modo</label>
489
- <select id="mode" title="modo de agente"></select>
490
- <label for="provider">Proveedor</label>
491
- <select id="provider" title="proveedor"></select>
492
- <label for="model">Modelo</label>
493
- <select id="model" title="modelo"></select>
494
- <label for="default-cwd">Carpeta predeterminada para nuevas sesiones</label>
495
- <div class="row default-cwd-row">
496
- <input id="default-cwd" type="text" readonly placeholder="(la casa del usuario)" title="carpeta donde se abre el selector y /new sin sesión" />
497
- <button type="button" id="default-cwd-pick" class="btn">elegir</button>
498
- <button type="button" id="default-cwd-clear" class="btn icon" title="quitar la carpeta predeterminada" aria-label="quitar la carpeta predeterminada">
499
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
500
- <path d="M18 6 6 18M6 6l12 12" />
501
- </svg>
502
- </button>
503
- </div>
504
- <label class="check" title="aprobar automáticamente tools con permiso ask">
505
- <input id="yes" type="checkbox" /> Aprobar automáticamente las tools (auto)
506
- </label>
581
+ <div id="settings-controls-slot"></div>
507
582
  <div class="row between settings-actions">
508
583
  <button type="button" id="settings-providers" class="btn">gestionar proveedores</button>
509
584
  <button type="button" id="settings-done" class="btn primary">listo</button>
@@ -538,9 +613,10 @@
538
613
  <div class="row">
539
614
  <label class="field grow" id="mf-args-field"><span>args (separados por espacio)</span><input id="mf-args" spellcheck="false" placeholder="harness/scripts/mcp-echo-server.mjs" /></label>
540
615
  </div>
541
- <label class="field"><span>env (KEY=valor por línea)</span>
542
- <textarea id="mf-env" rows="3" spellcheck="false" placeholder="API_KEY=..."></textarea>
616
+ <label class="field" id="mf-env-field"><span>env (KEY=valor por línea · usá <code>env:VAR</code> para no hardcodear secretos)</span>
617
+ <textarea id="mf-env" rows="3" spellcheck="false" placeholder="API_KEY=env:MY_API_KEY"></textarea>
543
618
  </label>
619
+ <label class="check" id="mf-inherit-field"><input id="mf-inherit-env" type="checkbox" /> heredar todo el entorno (<code>inheritEnv</code>, solo MCPs de confianza)</label>
544
620
  <label class="check"><input id="mf-enabled" type="checkbox" checked /> habilitado</label>
545
621
  <div class="row between settings-actions">
546
622
  <button type="button" id="mf-cancel" class="btn">cancelar</button>
@@ -73,6 +73,27 @@ function inline(text) {
73
73
  return out.replace(/\uE000(\d+)\uE000/g, (_, idx) => `<code>${codes[Number(idx)]}</code>`);
74
74
  }
75
75
 
76
+ const CELL_PIPE = '';
77
+
78
+ function splitRow(line) {
79
+ const masked = line.trim().replace(/\\\|/g, CELL_PIPE);
80
+ const inner = masked.replace(/^\s*\|/, '').replace(/\|\s*$/, '');
81
+ return inner.split('|').map((cell) => cell.trim().split(CELL_PIPE).join('|'));
82
+ }
83
+
84
+ function isDelimiterRow(line) {
85
+ if (!line.includes('|')) return false;
86
+ const cells = splitRow(line);
87
+ return cells.length > 0 && cells.every((cell) => /^:?-+:?$/.test(cell));
88
+ }
89
+
90
+ function cellAlign(cell) {
91
+ if (cell.startsWith(':') && cell.endsWith(':')) return 'center';
92
+ if (cell.endsWith(':')) return 'right';
93
+ if (cell.startsWith(':')) return 'left';
94
+ return '';
95
+ }
96
+
76
97
  /** Render a Markdown string to safe HTML. */
77
98
  export function renderMarkdown(text) {
78
99
  const lines = String(text ?? '').replace(/\r\n?/g, '\n').split('\n');
@@ -133,6 +154,28 @@ export function renderMarkdown(text) {
133
154
  continue;
134
155
  }
135
156
 
157
+ if (line.includes('|') && i + 1 < lines.length && isDelimiterRow(lines[i + 1])) {
158
+ flush();
159
+ const header = splitRow(line);
160
+ const aligns = splitRow(lines[i + 1]).map(cellAlign);
161
+ i += 2;
162
+ const rows = [];
163
+ while (i < lines.length && lines[i].trim() && lines[i].includes('|')) {
164
+ rows.push(splitRow(lines[i]));
165
+ i += 1;
166
+ }
167
+ const cols = Math.max(header.length, aligns.length);
168
+ const attr = (idx) => (aligns[idx] ? ` style="text-align:${aligns[idx]}"` : '');
169
+ const cells = (row, tag) =>
170
+ Array.from({ length: cols }, (_, idx) => `<${tag}${attr(idx)}>${inline(row[idx] ?? '')}</${tag}>`).join('');
171
+ out.push(
172
+ `<div class="table-wrap"><table><thead><tr>${cells(header, 'th')}</tr></thead><tbody>` +
173
+ rows.map((row) => `<tr>${cells(row, 'td')}</tr>`).join('') +
174
+ '</tbody></table></div>'
175
+ );
176
+ continue;
177
+ }
178
+
136
179
  if (/^\s*>/.test(line)) {
137
180
  flush();
138
181
  const quote = [];
@@ -1,27 +1,28 @@
1
1
  /* tamnora console — paper / aguada theme (opencode/codex-like) */
2
2
  :root {
3
- --bg: #161712;
4
- --panel: rgba(28, 29, 23, 0.82);
5
- --rail: rgba(24, 25, 19, 0.78);
6
- --theirs: #24251d;
3
+ --bg: #101107;
4
+ --panel: rgba(21, 22, 16, 0.86);
5
+ --rail: rgba(17, 18, 12, 0.82);
6
+ --theirs: #1c1d15;
7
7
  --mine: #d9704a;
8
8
  --mine-soft: rgba(217, 112, 74, 0.12);
9
9
  --text: #e9e4d6;
10
10
  --muted: #9c9789;
11
- --border: rgba(240, 234, 219, 0.14);
11
+ --border: rgba(240, 234, 219, 0.13);
12
12
  --danger: #e0705c;
13
13
  --accent: #d9704a;
14
14
  --accent-dark: #b8542f;
15
15
  --accent-soft: rgba(217, 112, 74, 0.18);
16
16
  --ok: #8fae7f;
17
17
  --warn: #d9a441;
18
- --solid: #1b1c16;
18
+ --solid: #12130b;
19
19
  --primary-fg: #fff7ef;
20
20
  --sans: Manrope, system-ui, "Segoe UI", Arial, sans-serif;
21
21
  --serif: "Libre Baskerville", Georgia, "Times New Roman", serif;
22
22
  --mono: "DM Mono", ui-monospace, "Cascadia Code", "JetBrains Mono", "SF Mono", Consolas, monospace;
23
23
  --font: var(--sans);
24
- --page-bg: linear-gradient(rgba(22, 23, 15, 0.94), rgba(22, 23, 15, 0.94)), url("/img/fondo-agentes.webp") center/cover fixed no-repeat;
24
+ --chat-font: 14.5px;
25
+ --page-bg: linear-gradient(rgba(11, 12, 8, 0.96), rgba(11, 12, 8, 0.96)), url("/img/fondo-agentes.webp") center/cover fixed no-repeat;
25
26
  color-scheme: dark;
26
27
  }
27
28
 
@@ -214,6 +215,7 @@ main { flex: 1; min-height: 0; min-width: 0; display: flex; }
214
215
  /* chat -------------------------------------------------------------------- */
215
216
  .sidebar {
216
217
  width: 260px;
218
+ flex: 0 0 auto;
217
219
  border-right: 1px solid var(--border);
218
220
  background: var(--rail);
219
221
  backdrop-filter: blur(14px);
@@ -300,10 +302,9 @@ main { flex: 1; min-height: 0; min-width: 0; display: flex; }
300
302
  }
301
303
  .rail-exit:hover { color: var(--accent); }
302
304
 
303
- .conversation { flex: 1; display: flex; flex-direction: column; min-width: 0; }
304
- .panes { flex: 1; min-height: 0; display: flex; min-width: 0; }
305
+ .conversation { flex: 1; display: flex; flex-direction: column; min-width: 0; }.panes { flex: 1; min-height: 0; display: flex; min-width: 0; position: relative; }
305
306
  .panes > .messages { flex: 1; min-height: 0; min-width: 0; }
306
- .messages { flex: 1; overflow-y: auto; overflow-x: hidden; min-width: 0; padding: 20px 24px 28px; display: flex; flex-direction: column; gap: 20px; }
307
+ .messages { flex: 1; overflow-y: auto; overflow-x: hidden; min-width: 0; padding: 20px 24px 28px; display: flex; flex-direction: column; gap: 20px; font-size: var(--chat-font); }
307
308
 
308
309
  /* resizable gutters -------------------------------------------------------- */
309
310
  .resizer {
@@ -317,6 +318,36 @@ main { flex: 1; min-height: 0; min-width: 0; display: flex; }
317
318
  body.resizing { cursor: col-resize; user-select: none; }
318
319
  body.resizing * { cursor: col-resize !important; }
319
320
 
321
+ /* scroll rápido de la conversación ---------------------------------------- */
322
+ .scroll-nav {
323
+ position: absolute;
324
+ right: 16px;
325
+ bottom: 16px;
326
+ z-index: 25;
327
+ display: flex;
328
+ flex-direction: column;
329
+ gap: 6px;
330
+ }
331
+ .scroll-nav[hidden] { display: none; }
332
+ .scroll-btn {
333
+ display: grid;
334
+ place-items: center;
335
+ width: 32px;
336
+ height: 32px;
337
+ padding: 0;
338
+ border: 1px solid var(--border);
339
+ border-radius: 999px;
340
+ background: var(--solid, var(--panel));
341
+ color: var(--muted);
342
+ cursor: pointer;
343
+ backdrop-filter: blur(14px);
344
+ box-shadow: 0 6px 18px rgb(20 18 12 / 0.22);
345
+ transition: color 0.12s ease, border-color 0.12s ease, opacity 0.12s ease;
346
+ }
347
+ .scroll-btn svg { width: 16px; height: 16px; display: block; }
348
+ .scroll-btn:hover { color: var(--accent); border-color: var(--accent); }
349
+ .scroll-btn:disabled { opacity: 0.35; cursor: default; border-color: var(--border); color: var(--muted); }
350
+
320
351
  /* session tabs ------------------------------------------------------------- */
321
352
  .tabbar {
322
353
  display: flex;
@@ -402,7 +433,6 @@ body.resizing * { cursor: col-resize !important; }
402
433
  }
403
434
  .msg-mode[data-mode='plan'] { color: var(--warn); }
404
435
  .bubble {
405
- max-width: 900px;
406
436
  min-width: 0;
407
437
  padding-left: 12px;
408
438
  border-left: 2px solid var(--border);
@@ -411,21 +441,58 @@ body.resizing * { cursor: col-resize !important; }
411
441
  overflow-wrap: anywhere;
412
442
  }
413
443
  .msg.user .bubble { border-left-color: var(--mine); color: var(--text); }
414
- .msg-actions { display: flex; gap: 6px; margin-top: 7px; opacity: 0.45; transition: opacity 0.12s ease; }
415
- .msg:hover .msg-actions,
416
- .msg:focus-within .msg-actions { opacity: 1; }
417
- .msg-actions button.msg-act {
444
+ .msg-actions { position: relative; display: flex; gap: 6px; margin-top: 7px; }
445
+ .msg-menu-btn {
446
+ display: inline-grid;
447
+ place-items: center;
448
+ width: auto;
449
+ min-height: 24px;
450
+ padding: 2px 9px;
418
451
  background: transparent;
419
452
  border: 1px solid var(--border);
420
453
  border-radius: 7px;
421
454
  color: var(--muted);
455
+ cursor: pointer;
456
+ font: 500 11px var(--mono);
457
+ opacity: 0.45;
458
+ transition: opacity 0.12s ease;
459
+ }
460
+ .msg:hover .msg-menu-btn,
461
+ .msg:focus-within .msg-menu-btn,
462
+ .msg-menu-btn[aria-expanded='true'] { opacity: 1; }
463
+ .msg-menu-btn:hover,
464
+ .msg-menu-btn[aria-expanded='true'] { color: var(--text); border-color: var(--accent); }
465
+ .msg-menu {
466
+ position: fixed;
467
+ top: 0;
468
+ left: 0;
469
+ z-index: 60;
470
+ min-width: 170px;
471
+ display: flex;
472
+ flex-direction: column;
473
+ gap: 2px;
474
+ padding: 5px;
475
+ border: 1px solid var(--border);
476
+ border-radius: 10px;
477
+ background: var(--panel);
478
+ box-shadow: 0 12px 30px rgb(0 0 0 / 0.22);
479
+ }
480
+ .msg-menu[hidden] { display: none; }
481
+ .msg-actions button.msg-act {
482
+ width: 100%;
483
+ text-align: left;
484
+ background: transparent;
485
+ border: 1px solid transparent;
486
+ border-radius: 7px;
487
+ color: var(--muted);
422
488
  font: 500 11px var(--mono);
423
- padding: 2px 8px;
489
+ padding: 5px 8px;
424
490
  cursor: pointer;
425
491
  }
426
- .msg-actions button.msg-act:hover { color: var(--text); border-color: var(--accent); }
427
- .msg-actions button.msg-act.danger:hover { color: var(--danger); border-color: var(--danger); }
428
- .bubble .tools { margin-top: 10px; display: flex; flex-direction: column; gap: 6px; }
492
+ .msg-actions button.msg-act:hover { color: var(--text); background: var(--theirs); }
493
+ .msg-actions button.msg-act.danger { color: var(--danger); }
494
+ .msg-actions button.msg-act.danger:hover { background: var(--theirs); }
495
+ .bubble .tools { margin-top: 10px; display: flex; flex-direction: column; gap: 6px; font-weight: 700; }
429
496
 
430
497
  .tools-actions { display: flex; align-items: center; gap: 8px; }
431
498
  .tools-actions[hidden] { display: none; }
@@ -573,6 +640,11 @@ body.resizing * { cursor: col-resize !important; }
573
640
  .activity .loader { color: var(--accent); }
574
641
  .activity-label { color: var(--muted); }
575
642
  #sb-rounds.running { color: var(--accent); }
643
+ .sb-activity { display: inline-flex; align-items: center; gap: 6px; }
644
+ .sb-activity .loader { color: var(--accent); }
645
+ .sb-activity .loader-thinking { width: 24px; height: 6px; }
646
+ .sb-activity .loader-working { width: 13px; height: 13px; }
647
+ .sb-activity .activity-label { color: var(--muted); }
576
648
 
577
649
  .loader-thinking {
578
650
  width: 32px;
@@ -621,7 +693,7 @@ details.reasoning .body { color: var(--muted); white-space: pre-wrap; margin-top
621
693
 
622
694
  @keyframes pulse { 50% { opacity: 0.35; } }
623
695
 
624
- .composer { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; border-top: 1px solid var(--border); background: var(--panel); backdrop-filter: blur(14px); }
696
+ .composer { position: relative; display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; border-top: 1px solid var(--border); background: var(--panel); backdrop-filter: blur(14px); }
625
697
  .composer.dragging { outline: 1px dashed var(--accent); outline-offset: -4px; }
626
698
  .composer-row { display: flex; align-items: flex-end; gap: 8px; }
627
699
  .prompt-mark { color: var(--mine); font-weight: 700; padding-bottom: 8px; }
@@ -671,6 +743,16 @@ details.reasoning .body { color: var(--muted); white-space: pre-wrap; margin-top
671
743
  .text blockquote { margin: 0 0 10px; padding-left: 12px; border-left: 2px solid var(--border); color: var(--muted); }
672
744
  .text a { color: var(--accent); }
673
745
  .text hr { border: 0; border-top: 1px solid var(--border); margin: 14px 0; }
746
+ .text .table-wrap, .agent-text .table-wrap { overflow-x: auto; margin: 0 0 10px; }
747
+ .text table, .agent-text table { border-collapse: collapse; width: 100%; font-size: 0.95em; }
748
+ .text th, .text td, .agent-text th, .agent-text td {
749
+ border: 1px solid var(--border);
750
+ padding: 5px 10px;
751
+ text-align: left;
752
+ vertical-align: top;
753
+ }
754
+ .text th, .agent-text th { background: var(--theirs); font-weight: 600; }
755
+ .text .table-wrap code, .agent-text .table-wrap code { white-space: nowrap; }
674
756
  .text code {
675
757
  background: var(--theirs);
676
758
  border: 1px solid var(--border);
@@ -687,7 +769,7 @@ details.reasoning .body { color: var(--muted); white-space: pre-wrap; margin-top
687
769
  .code-copy { background: transparent; border: 0; color: var(--muted); cursor: pointer; font: inherit; font-size: 11px; }
688
770
  .code-copy:hover { color: var(--text); }
689
771
  pre.code { margin: 0; padding: 10px 12px; overflow: auto; max-height: 420px; }
690
- pre.code code { background: none; border: 0; padding: 0; font-size: 12.5px; }
772
+ pre.code code { background: none; border: 0; padding: 0; font-size: 0.92em; }
691
773
  .tok-comment { color: #8c8779; font-style: italic; }
692
774
  .tok-string { color: #c2a878; }
693
775
  .tok-number { color: #e0a05c; }
@@ -707,6 +789,46 @@ pre.code code { background: none; border: 0; padding: 0; font-size: 12.5px; }
707
789
  }
708
790
  .statusbar .dot { opacity: 0.5; }
709
791
  .statusbar .spacer { flex: 1; }
792
+ .statusbar[role="button"] { cursor: pointer; }
793
+ .statusbar[role="button"]:hover { color: var(--fg); }
794
+
795
+ /* menú de comandos "/" en el composer */
796
+ .slash-menu {
797
+ position: absolute;
798
+ left: 14px;
799
+ right: 14px;
800
+ bottom: calc(100% - 6px);
801
+ max-height: 260px;
802
+ overflow-y: auto;
803
+ display: flex;
804
+ flex-direction: column;
805
+ gap: 2px;
806
+ padding: 6px;
807
+ border: 1px solid var(--border);
808
+ border-radius: 12px;
809
+ background: var(--panel);
810
+ box-shadow: 0 12px 32px rgb(0 0 0 / 0.22);
811
+ z-index: 30;
812
+ }
813
+ .slash-menu[hidden] { display: none; }
814
+ .slash-item {
815
+ display: flex;
816
+ align-items: baseline;
817
+ gap: 10px;
818
+ width: 100%;
819
+ padding: 7px 9px;
820
+ border: 0;
821
+ border-radius: 8px;
822
+ background: none;
823
+ color: var(--fg);
824
+ text-align: left;
825
+ cursor: pointer;
826
+ font: 500 13px var(--mono);
827
+ }
828
+ .slash-item.active,
829
+ .slash-item:hover { background: var(--theirs); }
830
+ .slash-cmd { color: var(--accent); font-weight: 700; }
831
+ .slash-desc { color: var(--muted); font: 400 12px var(--serif); }
710
832
  .ctx { display: inline-flex; align-items: center; gap: 8px; }
711
833
  .ctx-bar { width: 76px; height: 6px; border-radius: 999px; background: var(--border); overflow: hidden; }
712
834
  .ctx-fill { display: block; height: 100%; width: 0; border-radius: 999px; background: currentColor; transition: width 0.35s ease; }
@@ -724,6 +846,26 @@ pre.code code { background: none; border: 0; padding: 0; font-size: 12.5px; }
724
846
 
725
847
  .card { border: 1px solid var(--border); background: var(--panel); backdrop-filter: blur(14px); border-radius: 14px; padding: 14px; display: flex; flex-direction: column; gap: 10px; }
726
848
 
849
+ /* ajustes: pestañas internas ---------------------------------------------- */
850
+ #view-ajustes.active { display: block; overflow-y: auto; }
851
+ .ajustes-wrap { width: 100%; max-width: 780px; margin: 0 auto; padding: 18px; display: flex; flex-direction: column; gap: 12px; min-height: 100%; }
852
+ .ajustes-tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
853
+ .ajustes-tab {
854
+ appearance: none; background: none; border: 0; border-bottom: 2px solid transparent;
855
+ padding: 8px 14px; margin-bottom: -1px; color: var(--muted); cursor: pointer;
856
+ font: 600 13px var(--serif); letter-spacing: 0.01em;
857
+ }
858
+ .ajustes-tab:hover { color: var(--fg); }
859
+ .ajustes-tab.active { color: var(--fg); border-bottom-color: var(--accent); }
860
+ .ajustes-panel { display: none; }
861
+ .ajustes-panel.active { display: flex; }
862
+ .ajustes-panel.panel { flex: none; max-width: none; overflow: visible; padding: 6px 2px 18px; }
863
+ .ajustes-panel .verify-line code { font-size: 11px; }
864
+ #agent-settings-controls { display: flex; flex-direction: column; gap: 7px; }
865
+ #agent-settings-controls label { color: var(--muted); font-size: 12px; }
866
+ #agent-settings-controls select { width: 100%; }
867
+ #agent-controls-slot:empty { display: none; }
868
+
727
869
  .providers { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
728
870
  .providers li {
729
871
  display: flex;
@@ -999,6 +1141,14 @@ dialog.file-modal.preview-maximized {
999
1141
  }
1000
1142
 
1001
1143
 
1144
+ /* desktop: los laterales conservan su ancho mínimo (igual que el resizer) */
1145
+ @media (min-width: 821px) {
1146
+ .sidebar { min-width: 180px; }
1147
+ }
1148
+ @media (min-width: 901px) {
1149
+ .explorer { min-width: 200px; }
1150
+ }
1151
+
1002
1152
  @media (max-width: 900px) {
1003
1153
  .explorer {
1004
1154
  position: fixed;
@@ -42,6 +42,8 @@ const DEFINITIONS = [
42
42
  transport: { type: 'string', enum: ['stdio', 'http'] },
43
43
  command: { type: 'string', description: 'Para stdio, ej. "node" o "npx"' },
44
44
  args: { type: 'array', items: { type: 'string' } },
45
+ env: { type: 'object', description: 'Variables para el MCP (usá "env:VAR" para no hardcodear secretos)' },
46
+ inheritEnv: { type: 'boolean', description: 'Heredar process.env (solo MCPs de confianza; default false)' },
45
47
  url: { type: 'string', description: 'Para http' },
46
48
  enabled: { type: 'boolean' },
47
49
  },
@@ -133,7 +135,7 @@ export function createAgentTools(ctx) {
133
135
  command: cfg.command ?? null,
134
136
  url: cfg.url ?? null,
135
137
  enabled: true,
136
- config: { args: cfg.args ?? [], env: cfg.env ?? null },
138
+ config: { args: cfg.args ?? [], env: cfg.env ?? null, inheritEnv: cfg.inheritEnv === true },
137
139
  });
138
140
  await ctx.reloadMcp();
139
141
  const required = (cfg.requiredEnv ?? []).length ? ` Requiere env: ${cfg.requiredEnv.join(', ')}.` : '';
@@ -154,7 +156,7 @@ export function createAgentTools(ctx) {
154
156
  command: cfg.command ?? null,
155
157
  url: cfg.url ?? null,
156
158
  enabled: cfg.enabled !== false,
157
- config: { args: cfg.args ?? [], env: cfg.env ?? null },
159
+ config: { args: cfg.args ?? [], env: cfg.env ?? null, inheritEnv: cfg.inheritEnv === true },
158
160
  });
159
161
  await ctx.reloadMcp();
160
162
  return `MCP "${cfg.id}" importado.`;
@@ -174,7 +176,11 @@ export function createAgentTools(ctx) {
174
176
  command: transport === 'stdio' ? command : null,
175
177
  url: transport === 'http' ? url : null,
176
178
  enabled: args.enabled !== false,
177
- config: { args: Array.isArray(args.args) ? args.args : [], env: args.env ?? null },
179
+ config: {
180
+ args: Array.isArray(args.args) ? args.args : [],
181
+ env: args.env ?? null,
182
+ inheritEnv: args.inheritEnv === true,
183
+ },
178
184
  });
179
185
  await ctx.reloadMcp();
180
186
  return `MCP "${id}" registrado (${transport}).`;
@@ -75,8 +75,8 @@ export function completionFooter(stopReason, rounds, toolCalls, durationMs) {
75
75
  const parts = [`${rounds ?? '?'} ronda${rounds === 1 ? '' : 's'}`];
76
76
  if (toolCalls) parts.push(`${toolCalls} herramienta${toolCalls === 1 ? '' : 's'}`);
77
77
  if (durationMs > 0) parts.push(durationMs >= 1000 ? `${(durationMs / 1000).toFixed(1)}s` : `${durationMs}ms`);
78
- const maxed = stopReason === 'max_rounds';
79
- return `${maxed ? '⚠ máximo de rondas' : '✓ terminado'} · ${parts.join(' · ')}`;
78
+ const maxed = ['max_rounds', 'no_progress', 'budget'].includes(stopReason);
79
+ return `${maxed ? '⚠ corte de seguridad' : '✓ terminado'} · ${parts.join(' · ')}`;
80
80
  }
81
81
 
82
82
  /**
@@ -249,7 +249,9 @@ export function startTelegramChannel(
249
249
  tools: definitions,
250
250
  execute,
251
251
  signal: controller.signal,
252
- maxRounds: ctx.config.maxRounds,
252
+ maxRounds: ctx.effectiveMaxRounds?.() ?? ctx.config.maxRounds,
253
+ maxTokens: ctx.effectiveMaxTokens?.() ?? 0,
254
+ maxDurationMs: ctx.effectiveMaxDurationMs?.() ?? 0,
253
255
  onEvent(event) {
254
256
  if (event.type === 'text') {
255
257
  full = event.full;
@@ -272,10 +274,14 @@ export function startTelegramChannel(
272
274
  const durationMs = Date.now() - startedAt;
273
275
  let body = result.content?.trim() || full.trim();
274
276
  if (!body) {
275
- if (result.stopReason === 'max_rounds') {
277
+ if (result.stopReason === 'no_progress') {
278
+ body = '⚠ El agente se quedó repitiendo las mismas herramientas sin avanzar.';
279
+ } else if (result.stopReason === 'budget') {
280
+ body = '⚠ El agente alcanzó el presupuesto de tokens/tiempo sin respuesta final.';
281
+ } else if (result.stopReason === 'max_rounds') {
276
282
  body = `⚠ El agente alcanzó el máximo de rondas (${result.rounds}) sin respuesta final.${
277
283
  toolCalls ? ` Hizo ${toolCalls} herramientas.` : ''
278
- } Pedile que continúe o subí maxRounds.`;
284
+ }`;
279
285
  } else if (toolCalls) {
280
286
  body = '✓ El agente terminó sus herramientas, pero no devolvió un mensaje final.';
281
287
  } else {