@danieltmn/tamnora 0.7.0 → 0.8.0
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 +38 -0
- package/README.md +9 -0
- package/SECURITY.md +11 -6
- package/harness/packages/cli/src/commands/run.js +1 -1
- package/harness/packages/cli/src/commands/tui.js +1 -1
- package/harness/packages/core/src/agent/runAgent.js +3 -0
- package/harness/packages/core/src/git/index.js +177 -0
- package/harness/packages/core/src/index.js +2 -0
- package/harness/packages/core/src/procs/kill.js +95 -0
- package/harness/packages/core/test/procs.test.js +50 -0
- package/harness/packages/server/public/app.js +509 -75
- package/harness/packages/server/public/index.html +45 -1
- package/harness/packages/server/public/landing/index.html +5 -5
- package/harness/packages/server/public/markdown.js +43 -0
- package/harness/packages/server/public/styles.css +148 -22
- package/harness/packages/server/src/routes/chat.js +3 -0
- package/harness/packages/server/src/routes/git.js +43 -0
- package/harness/packages/server/src/routes/procs.js +14 -2
- package/harness/packages/server/src/routes/shell.js +23 -0
- package/harness/packages/server/src/server.js +4 -0
- package/harness/packages/server/test/markdown.test.js +33 -0
- package/harness/packages/server/test/server.test.js +107 -0
- 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.
|
|
36
|
+
<div class="brand"><span class="logo"><span class="logo-t">T</span><span class="logo-h">H</span></span> <span class="ver">v0.8.0</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,10 +116,23 @@
|
|
|
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">
|
|
121
133
|
<div id="slash-menu" class="slash-menu" hidden></div>
|
|
122
134
|
<div id="attach-preview" class="attach-preview" hidden></div>
|
|
135
|
+
<div id="chat-queue" class="chat-queue" hidden></div>
|
|
123
136
|
<div class="composer-row">
|
|
124
137
|
<span class="prompt-mark">❯</span>
|
|
125
138
|
<button type="button" id="attach-image" class="btn icon" title="Adjuntar imagen (o pegala / arrastrala)" aria-label="adjuntar imagen">
|
|
@@ -142,6 +155,7 @@
|
|
|
142
155
|
<span class="dot">·</span>
|
|
143
156
|
<span id="sb-model">—</span>
|
|
144
157
|
<span class="dot">·</span>
|
|
158
|
+
<span id="sb-activity" class="sb-activity" hidden></span>
|
|
145
159
|
<span id="sb-rounds">listo</span>
|
|
146
160
|
<span class="spacer"></span>
|
|
147
161
|
<span id="sb-context" class="ctx muted" title="uso del contexto del modelo">
|
|
@@ -159,6 +173,7 @@
|
|
|
159
173
|
<button type="button" class="xtab is-active" data-xp="files">estructura</button>
|
|
160
174
|
<button type="button" class="xtab" data-xp="preview">preview</button>
|
|
161
175
|
<button type="button" class="xtab" data-xp="procs">procs</button>
|
|
176
|
+
<button type="button" class="xtab" data-xp="git">git</button>
|
|
162
177
|
</div>
|
|
163
178
|
<span class="spacer"></span>
|
|
164
179
|
<button id="explorer-refresh" type="button" class="btn icon" title="refrescar" aria-label="refrescar">
|
|
@@ -221,6 +236,18 @@
|
|
|
221
236
|
<ul id="procs-list" class="providers"></ul>
|
|
222
237
|
<pre id="procs-log" class="procs-log"></pre>
|
|
223
238
|
</div>
|
|
239
|
+
|
|
240
|
+
<div id="xp-git" class="xp-pane" hidden>
|
|
241
|
+
<div class="xp-bar">
|
|
242
|
+
<span id="git-branch" class="git-branch">…</span>
|
|
243
|
+
<span id="git-sync" class="muted grow"></span>
|
|
244
|
+
</div>
|
|
245
|
+
<div class="explorer-filter">
|
|
246
|
+
<input id="git-filter" placeholder="filtrar por nombre…" spellcheck="false" />
|
|
247
|
+
</div>
|
|
248
|
+
<ul id="git-list" class="git-list"></ul>
|
|
249
|
+
<div id="git-status" class="explorer-status"></div>
|
|
250
|
+
</div>
|
|
224
251
|
</aside>
|
|
225
252
|
|
|
226
253
|
<button id="sidebar-handle" type="button" class="panel-handle panel-handle-left" title="mostrar sesiones" aria-label="mostrar panel de sesiones" aria-pressed="false">
|
|
@@ -267,6 +294,23 @@
|
|
|
267
294
|
</div>
|
|
268
295
|
</div>
|
|
269
296
|
</dialog>
|
|
297
|
+
|
|
298
|
+
<dialog id="git-modal" class="modal file-modal">
|
|
299
|
+
<div class="modal-card">
|
|
300
|
+
<div class="row between">
|
|
301
|
+
<h3 id="git-title">cambios</h3>
|
|
302
|
+
<div class="file-actions">
|
|
303
|
+
<span id="git-meta" class="muted"></span>
|
|
304
|
+
<button id="git-close" type="button" class="btn icon" title="cerrar" aria-label="cerrar">
|
|
305
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
306
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
307
|
+
</svg>
|
|
308
|
+
</button>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
<pre id="git-diff" class="file-preview git-diff"></pre>
|
|
312
|
+
</div>
|
|
313
|
+
</dialog>
|
|
270
314
|
</section>
|
|
271
315
|
|
|
272
316
|
<section id="view-providers" class="view">
|
|
@@ -450,8 +450,8 @@
|
|
|
450
450
|
<h1>Tamnora<br /><span>Harness</span></h1>
|
|
451
451
|
|
|
452
452
|
<p class="hero-copy">
|
|
453
|
-
Un harness
|
|
454
|
-
|
|
453
|
+
Un harness local para orquestar agentes de IA, ejecutar tools en tu workspace y mantener el
|
|
454
|
+
control desde tu máquina o el celular (túnel + QR o Telegram).
|
|
455
455
|
</p>
|
|
456
456
|
|
|
457
457
|
<div class="actions">
|
|
@@ -462,15 +462,15 @@
|
|
|
462
462
|
<div class="features" id="caracteristicas">
|
|
463
463
|
<div class="feature">
|
|
464
464
|
<div class="feature-icon">◈</div>
|
|
465
|
-
<div><b>
|
|
465
|
+
<div><b>Local</b><span>Corre en tu máquina; el estado vive en tu casa.</span></div>
|
|
466
466
|
</div>
|
|
467
467
|
<div class="feature">
|
|
468
468
|
<div class="feature-icon">↗</div>
|
|
469
|
-
<div><b>Accesible</b><span>
|
|
469
|
+
<div><b>Accesible</b><span>Consola web, TUI y Telegram; túnel con QR.</span></div>
|
|
470
470
|
</div>
|
|
471
471
|
<div class="feature">
|
|
472
472
|
<div class="feature-icon">✓</div>
|
|
473
|
-
<div><b>Control</b><span>
|
|
473
|
+
<div><b>Control</b><span>Permisos allow/ask/deny y runs trazables.</span></div>
|
|
474
474
|
</div>
|
|
475
475
|
</div>
|
|
476
476
|
</section>
|
|
@@ -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: #
|
|
4
|
-
--panel: rgba(
|
|
5
|
-
--rail: rgba(
|
|
6
|
-
--theirs: #
|
|
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.
|
|
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: #
|
|
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
|
-
--
|
|
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,66 @@ 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;
|
|
415
|
-
.msg
|
|
416
|
-
|
|
417
|
-
|
|
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: 26px;
|
|
449
|
+
height: 24px;
|
|
450
|
+
padding: 0;
|
|
418
451
|
background: transparent;
|
|
419
|
-
border: 1px solid
|
|
452
|
+
border: 1px solid transparent;
|
|
420
453
|
border-radius: 7px;
|
|
421
454
|
color: var(--muted);
|
|
422
|
-
|
|
423
|
-
|
|
455
|
+
cursor: pointer;
|
|
456
|
+
opacity: 0.4;
|
|
457
|
+
transition: opacity 0.12s ease, background 0.12s ease, color 0.12s ease;
|
|
458
|
+
}
|
|
459
|
+
.msg:hover .msg-menu-btn,
|
|
460
|
+
.msg:focus-within .msg-menu-btn,
|
|
461
|
+
.msg-menu-btn[aria-expanded='true'] { opacity: 1; }
|
|
462
|
+
.msg-menu-btn:hover,
|
|
463
|
+
.msg-menu-btn[aria-expanded='true'] { color: var(--text); background: var(--theirs); }
|
|
464
|
+
.msg-menu {
|
|
465
|
+
position: fixed;
|
|
466
|
+
top: 0;
|
|
467
|
+
left: 0;
|
|
468
|
+
z-index: 60;
|
|
469
|
+
min-width: 178px;
|
|
470
|
+
display: flex;
|
|
471
|
+
flex-direction: column;
|
|
472
|
+
gap: 1px;
|
|
473
|
+
padding: 5px;
|
|
474
|
+
border: 1px solid var(--border);
|
|
475
|
+
border-radius: 11px;
|
|
476
|
+
background: var(--solid, var(--panel));
|
|
477
|
+
box-shadow: 0 14px 34px rgb(0 0 0 / 0.24);
|
|
478
|
+
backdrop-filter: blur(18px);
|
|
479
|
+
}
|
|
480
|
+
.msg-menu[hidden] { display: none; }
|
|
481
|
+
.msg-menu-sep { height: 1px; margin: 4px 6px; background: var(--border); }
|
|
482
|
+
.msg-actions button.msg-act {
|
|
483
|
+
width: 100%;
|
|
484
|
+
display: flex;
|
|
485
|
+
align-items: center;
|
|
486
|
+
gap: 8px;
|
|
487
|
+
text-align: left;
|
|
488
|
+
background: transparent;
|
|
489
|
+
border: 0;
|
|
490
|
+
border-radius: 7px;
|
|
491
|
+
color: var(--text);
|
|
492
|
+
font: 500 12.5px var(--sans);
|
|
493
|
+
padding: 6px 8px;
|
|
424
494
|
cursor: pointer;
|
|
425
495
|
}
|
|
426
|
-
.msg-
|
|
427
|
-
.msg-
|
|
428
|
-
.
|
|
496
|
+
.msg-act .mi { flex: 0 0 auto; color: var(--muted); }
|
|
497
|
+
.msg-act .mlabel { min-width: 0; }
|
|
498
|
+
.msg-actions button.msg-act:hover { background: var(--theirs); }
|
|
499
|
+
.msg-actions button.msg-act:hover .mi { color: var(--accent); }
|
|
500
|
+
.msg-actions button.msg-act.danger { color: var(--danger); }
|
|
501
|
+
.msg-actions button.msg-act.danger:hover { background: var(--theirs); }
|
|
502
|
+
.msg-actions button.msg-act.danger:hover .mi { color: var(--danger); }
|
|
503
|
+
.bubble .tools { margin-top: 10px; display: flex; flex-direction: column; gap: 6px; font-weight: 700; }
|
|
429
504
|
|
|
430
505
|
.tools-actions { display: flex; align-items: center; gap: 8px; }
|
|
431
506
|
.tools-actions[hidden] { display: none; }
|
|
@@ -573,6 +648,11 @@ body.resizing * { cursor: col-resize !important; }
|
|
|
573
648
|
.activity .loader { color: var(--accent); }
|
|
574
649
|
.activity-label { color: var(--muted); }
|
|
575
650
|
#sb-rounds.running { color: var(--accent); }
|
|
651
|
+
.sb-activity { display: inline-flex; align-items: center; gap: 6px; }
|
|
652
|
+
.sb-activity .loader { color: var(--accent); }
|
|
653
|
+
.sb-activity .loader-thinking { width: 24px; height: 6px; }
|
|
654
|
+
.sb-activity .loader-working { width: 13px; height: 13px; }
|
|
655
|
+
.sb-activity .activity-label { color: var(--muted); }
|
|
576
656
|
|
|
577
657
|
.loader-thinking {
|
|
578
658
|
width: 32px;
|
|
@@ -638,6 +718,14 @@ details.reasoning .body { color: var(--muted); white-space: pre-wrap; margin-top
|
|
|
638
718
|
.btn.icon svg { width: 18px; height: 18px; display: block; }
|
|
639
719
|
.btn.icon:hover { color: var(--accent); }
|
|
640
720
|
|
|
721
|
+
.chat-queue { display: flex; flex-direction: column; gap: 4px; padding: 8px 10px; border: 1px dashed var(--border); border-radius: 9px; background: var(--theirs); }
|
|
722
|
+
.chat-queue[hidden] { display: none; }
|
|
723
|
+
.queue-label { color: var(--muted); font: 600 10.5px var(--mono); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
724
|
+
.queue-item { display: flex; align-items: center; gap: 8px; }
|
|
725
|
+
.queue-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 500 12.5px var(--sans); }
|
|
726
|
+
.queue-remove { flex: 0 0 auto; width: 20px; height: 20px; padding: 0; line-height: 1; border-radius: 50%; border: 0; background: transparent; color: var(--muted); cursor: pointer; }
|
|
727
|
+
.queue-remove:hover { color: var(--danger); background: var(--theirs); }
|
|
728
|
+
.shell-out { margin: 0; padding: 8px 10px; max-height: 320px; overflow: auto; background: var(--theirs); border: 1px solid var(--border); border-radius: 8px; font: 11.5px/1.5 var(--mono); white-space: pre-wrap; word-break: break-word; }
|
|
641
729
|
.attach-preview { display: flex; gap: 8px; flex-wrap: wrap; padding-bottom: 2px; }
|
|
642
730
|
.attach-thumb { position: relative; }
|
|
643
731
|
.attach-thumb img { height: 64px; border-radius: 6px; border: 1px solid var(--border); display: block; }
|
|
@@ -671,6 +759,16 @@ details.reasoning .body { color: var(--muted); white-space: pre-wrap; margin-top
|
|
|
671
759
|
.text blockquote { margin: 0 0 10px; padding-left: 12px; border-left: 2px solid var(--border); color: var(--muted); }
|
|
672
760
|
.text a { color: var(--accent); }
|
|
673
761
|
.text hr { border: 0; border-top: 1px solid var(--border); margin: 14px 0; }
|
|
762
|
+
.text .table-wrap, .agent-text .table-wrap { overflow-x: auto; margin: 0 0 10px; }
|
|
763
|
+
.text table, .agent-text table { border-collapse: collapse; width: 100%; font-size: 0.95em; }
|
|
764
|
+
.text th, .text td, .agent-text th, .agent-text td {
|
|
765
|
+
border: 1px solid var(--border);
|
|
766
|
+
padding: 5px 10px;
|
|
767
|
+
text-align: left;
|
|
768
|
+
vertical-align: top;
|
|
769
|
+
}
|
|
770
|
+
.text th, .agent-text th { background: var(--theirs); font-weight: 600; }
|
|
771
|
+
.text .table-wrap code, .agent-text .table-wrap code { white-space: nowrap; }
|
|
674
772
|
.text code {
|
|
675
773
|
background: var(--theirs);
|
|
676
774
|
border: 1px solid var(--border);
|
|
@@ -687,7 +785,7 @@ details.reasoning .body { color: var(--muted); white-space: pre-wrap; margin-top
|
|
|
687
785
|
.code-copy { background: transparent; border: 0; color: var(--muted); cursor: pointer; font: inherit; font-size: 11px; }
|
|
688
786
|
.code-copy:hover { color: var(--text); }
|
|
689
787
|
pre.code { margin: 0; padding: 10px 12px; overflow: auto; max-height: 420px; }
|
|
690
|
-
pre.code code { background: none; border: 0; padding: 0; font-size:
|
|
788
|
+
pre.code code { background: none; border: 0; padding: 0; font-size: 0.92em; }
|
|
691
789
|
.tok-comment { color: #8c8779; font-style: italic; }
|
|
692
790
|
.tok-string { color: #c2a878; }
|
|
693
791
|
.tok-number { color: #e0a05c; }
|
|
@@ -967,6 +1065,26 @@ pre.code code { background: none; border: 0; padding: 0; font-size: 12.5px; }
|
|
|
967
1065
|
#xp-preview.preview-maximized .preview-stage { border-radius: 10px; }
|
|
968
1066
|
#procs-list { flex: 1; min-height: 0; overflow-y: auto; }
|
|
969
1067
|
.procs-log { margin: 0; padding: 8px 12px; height: 150px; overflow: auto; background: var(--theirs); border-top: 1px solid var(--border); font: 500 11px var(--mono); white-space: pre-wrap; word-break: break-word; }
|
|
1068
|
+
.git-branch { font: 700 12px var(--mono); color: var(--accent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1069
|
+
.git-list { list-style: none; margin: 0; padding: 0 8px 10px; overflow-y: auto; flex: 1; min-height: 0; }
|
|
1070
|
+
.git-group { padding: 8px 6px 3px; color: var(--muted); font: 600 11px var(--sans); text-transform: uppercase; letter-spacing: 0.04em; }
|
|
1071
|
+
.git-item { border-radius: 7px; }
|
|
1072
|
+
.git-row { display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: 7px; cursor: pointer; }
|
|
1073
|
+
.git-row:hover { background: var(--theirs); }
|
|
1074
|
+
.git-badge { flex: 0 0 auto; width: 16px; text-align: center; font: 700 11px var(--mono); }
|
|
1075
|
+
.git-badge.mod, .git-badge.ren { color: var(--accent); }
|
|
1076
|
+
.git-badge.add, .git-badge.new { color: var(--ok); }
|
|
1077
|
+
.git-badge.del, .git-badge.conf { color: var(--danger); }
|
|
1078
|
+
.git-badge.ign { color: var(--muted); }
|
|
1079
|
+
.git-path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; font: 500 12px var(--sans); }
|
|
1080
|
+
.git-diff { margin: 2px 0 8px; padding: 8px 10px; max-height: 320px; overflow: auto; background: var(--theirs); border-radius: 8px; font: 11px/1.5 var(--mono); white-space: pre; }
|
|
1081
|
+
.git-diff.err { color: var(--danger); }
|
|
1082
|
+
.git-diff .diff-line { display: block; }
|
|
1083
|
+
.git-diff .diff-add { color: var(--ok); }
|
|
1084
|
+
.git-diff .diff-del { color: var(--danger); }
|
|
1085
|
+
.git-diff .diff-eq { color: var(--muted); }
|
|
1086
|
+
.git-diff .diff-sep { color: var(--accent); opacity: 0.8; }
|
|
1087
|
+
.git-diff .diff-meta { color: var(--muted); opacity: 0.7; }
|
|
970
1088
|
.import-bar { display: flex; gap: 6px; margin: 4px 0 10px; }
|
|
971
1089
|
.import-bar input { flex: 1; min-width: 0; }
|
|
972
1090
|
|
|
@@ -1059,6 +1177,14 @@ dialog.file-modal.preview-maximized {
|
|
|
1059
1177
|
}
|
|
1060
1178
|
|
|
1061
1179
|
|
|
1180
|
+
/* desktop: los laterales conservan su ancho mínimo (igual que el resizer) */
|
|
1181
|
+
@media (min-width: 821px) {
|
|
1182
|
+
.sidebar { min-width: 180px; }
|
|
1183
|
+
}
|
|
1184
|
+
@media (min-width: 901px) {
|
|
1185
|
+
.explorer { min-width: 200px; }
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1062
1188
|
@media (max-width: 900px) {
|
|
1063
1189
|
.explorer {
|
|
1064
1190
|
position: fixed;
|
|
@@ -264,6 +264,9 @@ export function registerChat(route, ctx) {
|
|
|
264
264
|
} else if (event.type === 'retry') {
|
|
265
265
|
partials.flush();
|
|
266
266
|
ctx.runs.emit(sessionId, 'retry', { attempt: event.attempt, delayMs: event.delayMs, message: event.message });
|
|
267
|
+
} else if (event.type === 'usage') {
|
|
268
|
+
partials.flush();
|
|
269
|
+
ctx.runs.emit(sessionId, 'usage', { tokens: event.usage });
|
|
267
270
|
} else if (event.type === 'trim') {
|
|
268
271
|
partials.flush();
|
|
269
272
|
ctx.runs.emit(sessionId, 'trim', { trimmed: event.trimmed, dropped: event.dropped, tokens: event.tokens });
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { gitStatus, gitDiff } from '../lib/core.js';
|
|
3
|
+
import { sendJson, sendError } from '../lib/http.js';
|
|
4
|
+
import { isLoopback, resolveExistingDir } from '../lib/workspace.js';
|
|
5
|
+
|
|
6
|
+
function isInside(root, target) {
|
|
7
|
+
const rel = path.relative(root, target);
|
|
8
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function registerGit(route, ctx) {
|
|
12
|
+
const resolveRoot = (req, res, raw) => {
|
|
13
|
+
if (!isLoopback(req.socket.localAddress)) {
|
|
14
|
+
sendError(res, 403, 'El estado de git solo está disponible en conexiones locales.');
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
return resolveExistingDir(raw || ctx.cwd);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
sendError(res, 400, error.message);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
route('GET', '/api/git/status', async ({ req, res, query }) => {
|
|
26
|
+
const root = resolveRoot(req, res, query.get('cwd'));
|
|
27
|
+
if (!root) return;
|
|
28
|
+
const status = await gitStatus(root);
|
|
29
|
+
sendJson(res, 200, { root, ...status });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
route('GET', '/api/git/diff', async ({ req, res, query }) => {
|
|
33
|
+
const root = resolveRoot(req, res, query.get('cwd'));
|
|
34
|
+
if (!root) return;
|
|
35
|
+
const rel = String(query.get('path') ?? '').replace(/^[/\\]+/, '');
|
|
36
|
+
if (rel && !isInside(root, path.resolve(root, rel))) {
|
|
37
|
+
return sendError(res, 400, 'La ruta queda fuera del workspace.');
|
|
38
|
+
}
|
|
39
|
+
const result = await gitDiff(root, { path: rel, staged: query.get('staged') === '1' });
|
|
40
|
+
if (!result.ok) return sendError(res, 400, result.error ?? 'No se pudo obtener el diff.');
|
|
41
|
+
sendJson(res, 200, { root, path: rel || null, ...result });
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { detectProcs } from '../lib/core.js';
|
|
1
|
+
import { detectProcs, killPort } from '../lib/core.js';
|
|
2
2
|
import { readJson, sendJson, sendError } from '../lib/http.js';
|
|
3
|
-
import { resolveExistingDir } from '../lib/workspace.js';
|
|
3
|
+
import { isLoopback, resolveExistingDir } from '../lib/workspace.js';
|
|
4
4
|
|
|
5
5
|
export function registerProcs(route, ctx) {
|
|
6
6
|
route('GET', '/api/procs', ({ res }) => {
|
|
@@ -44,4 +44,16 @@ export function registerProcs(route, ctx) {
|
|
|
44
44
|
if (!ctx.procs.stop(params.id)) return sendError(res, 404, 'Proceso no encontrado.');
|
|
45
45
|
sendJson(res, 200, { ok: true });
|
|
46
46
|
});
|
|
47
|
+
|
|
48
|
+
route('POST', '/api/procs/kill', async ({ req, res }) => {
|
|
49
|
+
if (!isLoopback(req.socket.localAddress)) {
|
|
50
|
+
return sendError(res, 403, 'Matar procesos solo está disponible en conexiones locales.');
|
|
51
|
+
}
|
|
52
|
+
const body = await readJson(req);
|
|
53
|
+
try {
|
|
54
|
+
sendJson(res, 200, await killPort(body.port));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
sendError(res, 400, error.message);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
47
59
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createTools } from '../lib/core.js';
|
|
2
|
+
import { readJson, sendJson, sendError } from '../lib/http.js';
|
|
3
|
+
import { isLoopback, resolveExistingDir } from '../lib/workspace.js';
|
|
4
|
+
|
|
5
|
+
export function registerShell(route, ctx) {
|
|
6
|
+
route('POST', '/api/shell', async ({ req, res }) => {
|
|
7
|
+
if (!isLoopback(req.socket.localAddress)) {
|
|
8
|
+
return sendError(res, 403, 'El shell solo está disponible en conexiones locales.');
|
|
9
|
+
}
|
|
10
|
+
const body = await readJson(req);
|
|
11
|
+
const command = String(body.command ?? '').trim();
|
|
12
|
+
if (!command) return sendError(res, 400, 'Falta "command".');
|
|
13
|
+
let root;
|
|
14
|
+
try {
|
|
15
|
+
root = resolveExistingDir(body.cwd || ctx.cwd);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return sendError(res, 400, error.message);
|
|
18
|
+
}
|
|
19
|
+
const tools = createTools({ root, enabled: ['bash'], yes: true });
|
|
20
|
+
const output = await tools.execute('bash', { command });
|
|
21
|
+
sendJson(res, 200, { command, cwd: root, output: String(output ?? '') });
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -28,12 +28,14 @@ import { registerAuth } from './routes/auth.js';
|
|
|
28
28
|
import { registerSessions } from './routes/sessions.js';
|
|
29
29
|
import { registerFolders } from './routes/folders.js';
|
|
30
30
|
import { registerFiles } from './routes/files.js';
|
|
31
|
+
import { registerGit } from './routes/git.js';
|
|
31
32
|
import { registerModels } from './routes/models.js';
|
|
32
33
|
import { registerProviders } from './routes/providers.js';
|
|
33
34
|
import { registerMcp } from './routes/mcp.js';
|
|
34
35
|
import { registerSkills } from './routes/skills.js';
|
|
35
36
|
import { registerChannels } from './routes/channels.js';
|
|
36
37
|
import { registerProcs } from './routes/procs.js';
|
|
38
|
+
import { registerShell } from './routes/shell.js';
|
|
37
39
|
import { registerPreview } from './routes/preview.js';
|
|
38
40
|
import { registerChat } from './routes/chat.js';
|
|
39
41
|
import { registerEval } from './routes/eval.js';
|
|
@@ -212,12 +214,14 @@ export async function createTamnoraServer({
|
|
|
212
214
|
registerSessions(route, ctx);
|
|
213
215
|
registerFolders(route, ctx);
|
|
214
216
|
registerFiles(route, ctx);
|
|
217
|
+
registerGit(route, ctx);
|
|
215
218
|
registerModels(route, ctx);
|
|
216
219
|
registerProviders(route, ctx);
|
|
217
220
|
registerMcp(route, ctx);
|
|
218
221
|
registerSkills(route, ctx);
|
|
219
222
|
registerChannels(route, ctx);
|
|
220
223
|
registerProcs(route, ctx);
|
|
224
|
+
registerShell(route, ctx);
|
|
221
225
|
const preview = registerPreview(route, ctx);
|
|
222
226
|
registerChat(route, ctx);
|
|
223
227
|
registerEval(route, ctx);
|
|
@@ -31,6 +31,39 @@ test('only allows http(s) links', () => {
|
|
|
31
31
|
assert.ok(!renderMarkdown('[x](javascript:alert(1))').includes('<a '));
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
+
test('renders pipe tables with alignment and stops at the next block', () => {
|
|
35
|
+
const md = [
|
|
36
|
+
'| Archivo | Qué es | Centro | Derecha |',
|
|
37
|
+
'| --- | :-- | :-: | --: |',
|
|
38
|
+
'| index.html | Landing | x | y |',
|
|
39
|
+
'| `a.js` | **script** | z | w |',
|
|
40
|
+
'',
|
|
41
|
+
'sigue el texto',
|
|
42
|
+
].join('\n');
|
|
43
|
+
const html = renderMarkdown(md);
|
|
44
|
+
assert.match(html, /<div class="table-wrap"><table>/);
|
|
45
|
+
assert.match(html, /<th>Archivo<\/th>/);
|
|
46
|
+
assert.match(html, /<th style="text-align:center">Centro<\/th>/);
|
|
47
|
+
assert.match(html, /<th style="text-align:right">Derecha<\/th>/);
|
|
48
|
+
assert.match(html, /<td style="text-align:left">Landing<\/td>/);
|
|
49
|
+
assert.match(html, /<td><code>a\.js<\/code><\/td>/);
|
|
50
|
+
assert.match(html, /<td style="text-align:left"><strong>script<\/strong><\/td>/);
|
|
51
|
+
assert.match(html, /<\/tbody><\/table><\/div>\n<p>sigue el texto<\/p>/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('table cells are escaped and escaped pipes stay in one cell', () => {
|
|
55
|
+
const html = renderMarkdown('| col | col2 |\n| --- | --- |\n| a \\| b | <script> |');
|
|
56
|
+
assert.match(html, /<td>a \| b<\/td>/);
|
|
57
|
+
assert.ok(!html.includes('<script>'));
|
|
58
|
+
assert.match(html, /<script>/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('a lone pipe line without a delimiter row stays a paragraph', () => {
|
|
62
|
+
const html = renderMarkdown('foo | bar');
|
|
63
|
+
assert.ok(!html.includes('<table'));
|
|
64
|
+
assert.equal(html, '<p>foo | bar</p>');
|
|
65
|
+
});
|
|
66
|
+
|
|
34
67
|
test('highlight escapes code content', () => {
|
|
35
68
|
assert.equal(highlight('a < b', 'bash'), 'a < b');
|
|
36
69
|
});
|