@danieltmn/openbridge 0.1.0 → 0.3.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 +21 -0
- package/README.md +146 -143
- package/package.json +9 -1
- package/src/bridge/bridge.js +85 -1
- package/src/cli.js +92 -2
- package/src/qr.js +224 -0
- package/src/web/assets/app.js +358 -9
- package/src/web/routes.js +3 -0
- package/src/web/templates/chat.html +57 -1
package/src/web/assets/app.js
CHANGED
|
@@ -27,6 +27,9 @@ var els = {
|
|
|
27
27
|
input: document.getElementById('input'),
|
|
28
28
|
sendBtn: document.getElementById('sendBtn'),
|
|
29
29
|
btnImg: document.getElementById('btnImg'),
|
|
30
|
+
btnMic: document.getElementById('btnMic'),
|
|
31
|
+
btnTpl: document.getElementById('btnTpl'),
|
|
32
|
+
tplPanel: document.getElementById('tplPanel'),
|
|
30
33
|
imgInput: document.getElementById('imgInput'),
|
|
31
34
|
imgPreview: document.getElementById('imgPreview'),
|
|
32
35
|
imgThumb: document.getElementById('imgThumb'),
|
|
@@ -44,6 +47,7 @@ var els = {
|
|
|
44
47
|
btnSkin: document.getElementById('btnSkin'),
|
|
45
48
|
skinPanel: document.getElementById('skinPanel'),
|
|
46
49
|
viewProcs: document.getElementById('viewProcs'),
|
|
50
|
+
viewChanges: document.getElementById('viewChanges'),
|
|
47
51
|
themeColor: document.getElementById('themeColor'),
|
|
48
52
|
themeLink: document.getElementById('themeStylesheet'),
|
|
49
53
|
statusbar: document.getElementById('statusbar'),
|
|
@@ -220,8 +224,17 @@ function updateStatusbar() {
|
|
|
220
224
|
if (state.currentId !== null && state.currentSession) {
|
|
221
225
|
extra = '· ' + extra;
|
|
222
226
|
var tokTxt = sessionTokensLabel(state.currentSession);
|
|
223
|
-
if (tokTxt)
|
|
227
|
+
if (tokTxt) {
|
|
228
|
+
var pct = sessionTokensPct(state.currentSession);
|
|
229
|
+
extra += ' · ' + tokTxt + ' tokens' + (pct ? ' (' + pct + ')' : '');
|
|
230
|
+
}
|
|
231
|
+
var cost = fmtCost(state.currentSession.cost);
|
|
232
|
+
if (cost) extra += ' · ' + cost;
|
|
224
233
|
}
|
|
234
|
+
var totalCost = 0;
|
|
235
|
+
for (var ci = 0; ci < state.sessions.length; ci++) totalCost += Number(state.sessions[ci].cost) || 0;
|
|
236
|
+
var totalTxt = fmtCost(totalCost);
|
|
237
|
+
if (totalTxt) extra += ' · ' + totalTxt + ' total';
|
|
225
238
|
els.sbExtra.textContent = extra;
|
|
226
239
|
}
|
|
227
240
|
|
|
@@ -761,6 +774,14 @@ function fmtTokens(n) {
|
|
|
761
774
|
return String(n);
|
|
762
775
|
}
|
|
763
776
|
|
|
777
|
+
// Costo acumulado (USD) con precision segun magnitud ('' si no hay dato).
|
|
778
|
+
function fmtCost(n) {
|
|
779
|
+
var v = Number(n) || 0;
|
|
780
|
+
if (v <= 0) return '';
|
|
781
|
+
var s = v >= 1 ? v.toFixed(2) : (v >= 0.01 ? v.toFixed(3) : v.toFixed(4));
|
|
782
|
+
return '$' + s.replace('.', ',');
|
|
783
|
+
}
|
|
784
|
+
|
|
764
785
|
// Ventana de contexto del modelo segun el catalogo (0 si no se conoce).
|
|
765
786
|
function modelContext(model) {
|
|
766
787
|
var c = state.catalog || {};
|
|
@@ -779,7 +800,16 @@ function sessionTokensLabel(s) {
|
|
|
779
800
|
function sessionTokensTitle(s) {
|
|
780
801
|
var tok = Math.round(Number(s.tokens) || 0);
|
|
781
802
|
var ctx = modelContext(s.model);
|
|
782
|
-
|
|
803
|
+
var cost = fmtCost(s.cost);
|
|
804
|
+
return tok + ' tokens' + (ctx ? ' de ' + ctx + ' de contexto' : '') + (cost ? ' · ' + cost + ' acumulado' : '');
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Porcentaje de contexto consumido ('' si no hay datos).
|
|
808
|
+
function sessionTokensPct(s) {
|
|
809
|
+
var tok = Math.round(Number(s.tokens) || 0);
|
|
810
|
+
var ctx = modelContext(s.model);
|
|
811
|
+
if (!tok || !ctx) return '';
|
|
812
|
+
return Math.min(100, Math.round((tok / ctx) * 100)) + '%';
|
|
783
813
|
}
|
|
784
814
|
|
|
785
815
|
// Etiqueta "consumidos / capacidad" de la sesion.
|
|
@@ -789,11 +819,23 @@ function sessionTokensHtml(s) {
|
|
|
789
819
|
return '<span class="stok" title="' + sessionTokensTitle(s) + '">' + label + '</span>';
|
|
790
820
|
}
|
|
791
821
|
|
|
792
|
-
//
|
|
822
|
+
// Costo acumulado de la sesion ('' si no hay dato).
|
|
823
|
+
function sessionCostHtml(s) {
|
|
824
|
+
var cost = fmtCost(s.cost);
|
|
825
|
+
if (!cost) return '';
|
|
826
|
+
return '<span class="scost" title="' + sessionTokensTitle(s) + '">' + cost + '</span>';
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Subtitulo del encabezado: proyecto · modelo · agente · tokens/contexto · costo.
|
|
793
830
|
function sessionSubtitle(s) {
|
|
794
831
|
var txt = projectLabel(s.folder) + ' · ' + (s.model || 'sin modelo') + ' · ' + (s.agent || 'build');
|
|
795
832
|
var label = sessionTokensLabel(s);
|
|
796
|
-
if (label)
|
|
833
|
+
if (label) {
|
|
834
|
+
var pct = sessionTokensPct(s);
|
|
835
|
+
txt += ' · ' + label + ' tokens' + (pct ? ' (' + pct + ')' : '');
|
|
836
|
+
}
|
|
837
|
+
var cost = fmtCost(s.cost);
|
|
838
|
+
if (cost) txt += ' · ' + cost;
|
|
797
839
|
return txt;
|
|
798
840
|
}
|
|
799
841
|
|
|
@@ -818,6 +860,7 @@ function sessionItemHtml(s, withFolder) {
|
|
|
818
860
|
+ (withFolder ? '<span class="sfolder">' + esc(projectLabel(s.folder)) + '</span>' : '')
|
|
819
861
|
+ pen
|
|
820
862
|
+ sessionTokensHtml(s)
|
|
863
|
+
+ sessionCostHtml(s)
|
|
821
864
|
+ '<span class="stime">' + esc(timeShort(s.last_ts)) + '</span>'
|
|
822
865
|
+ '</button>';
|
|
823
866
|
}
|
|
@@ -940,6 +983,7 @@ function showView(name) {
|
|
|
940
983
|
els.viewHistory.style.display = 'none';
|
|
941
984
|
els.viewPreview.style.display = 'none';
|
|
942
985
|
els.viewProcs.style.display = 'none';
|
|
986
|
+
els.viewChanges.style.display = 'none';
|
|
943
987
|
|
|
944
988
|
if (name !== 'procs') stopProcView();
|
|
945
989
|
|
|
@@ -1006,6 +1050,12 @@ function showView(name) {
|
|
|
1006
1050
|
var pf = sessionProcFolder();
|
|
1007
1051
|
els.hSub.textContent = pf ? projectLabel(pf) : 'de la sesión abierta';
|
|
1008
1052
|
enterProcsView();
|
|
1053
|
+
} else if (name === 'changes') {
|
|
1054
|
+
els.viewChanges.style.display = '';
|
|
1055
|
+
els.btnBack.style.display = 'none';
|
|
1056
|
+
els.sendForm.style.display = 'none';
|
|
1057
|
+
els.btnCmds.style.display = 'none';
|
|
1058
|
+
renderChangesView();
|
|
1009
1059
|
}
|
|
1010
1060
|
|
|
1011
1061
|
if (els.fabNew) els.fabNew.classList.toggle('show', name === 'home');
|
|
@@ -1057,6 +1107,8 @@ function composeOpenSheet() {
|
|
|
1057
1107
|
}
|
|
1058
1108
|
function composeCloseSheet() {
|
|
1059
1109
|
if (!els.sendForm) return;
|
|
1110
|
+
voiceStop();
|
|
1111
|
+
tplClose();
|
|
1060
1112
|
els.sendForm.classList.remove('open');
|
|
1061
1113
|
document.body.classList.remove('compose-open');
|
|
1062
1114
|
if (els.imgPreview && els.imgPreview.parentNode === els.sendForm && els.composeOpen) {
|
|
@@ -1216,9 +1268,12 @@ function renderHome(sessions) {
|
|
|
1216
1268
|
+ '</div>';
|
|
1217
1269
|
}
|
|
1218
1270
|
html += '</div>';
|
|
1219
|
-
html += '<
|
|
1271
|
+
html += '<div class="histrow">'
|
|
1272
|
+
+ '<button type="button" class="histbtn" data-folder="' + esc(key) + '">'
|
|
1220
1273
|
+ (hidden > 0 ? 'ver historial · ' + hidden + ' más' : 'ver historial')
|
|
1221
|
-
+ '</button>'
|
|
1274
|
+
+ '</button>'
|
|
1275
|
+
+ '<button type="button" class="histbtn filesbtn" data-folder="' + esc(key) + '" title="Archivos del proyecto">ver archivos</button>'
|
|
1276
|
+
+ '</div>';
|
|
1222
1277
|
html += '</div></div>';
|
|
1223
1278
|
}
|
|
1224
1279
|
}
|
|
@@ -1594,7 +1649,8 @@ function msgSig(m) {
|
|
|
1594
1649
|
function msgNodeHtml(m, chatQuery) {
|
|
1595
1650
|
var cls = m.role === 'user' ? 'mine' : 'theirs';
|
|
1596
1651
|
if (m.status === 'canceled') cls += ' canceled';
|
|
1597
|
-
var who = m.role === 'user' ? '❯ vos' : '●
|
|
1652
|
+
var who = m.role === 'user' ? '❯ vos' : '● Agente';
|
|
1653
|
+
if (m.agent === 'plan') cls += ' plan';
|
|
1598
1654
|
var agentBadge = m.agent ? ' <span class="abadge ' + esc(m.agent) + '">' + esc(m.agent) + '</span>' : '';
|
|
1599
1655
|
var stopBadge = m.canceled ? ' <span class="stopbadge">⏹ detenido</span>' : '';
|
|
1600
1656
|
var streaming = m.role === 'assistant' && m.status === 'streaming';
|
|
@@ -1855,6 +1911,19 @@ function focusProject(folder) {
|
|
|
1855
1911
|
}
|
|
1856
1912
|
}
|
|
1857
1913
|
|
|
1914
|
+
// Convierte la carpeta absoluta de un proyecto en ruta relativa al workspace
|
|
1915
|
+
// (para la vista de archivos). '' si no se puede derivar (usa la raiz).
|
|
1916
|
+
function folderToFilesPath(folder) {
|
|
1917
|
+
var ws = state.catalog && state.catalog.workspace ? state.catalog.workspace : '';
|
|
1918
|
+
if (!folder || !ws) return '';
|
|
1919
|
+
var norm = function (p) { return String(p).replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase(); };
|
|
1920
|
+
var nws = norm(ws), nf = norm(folder);
|
|
1921
|
+
if (nf === nws || nf.indexOf(nws + '/') !== 0) return '';
|
|
1922
|
+
var raw = String(folder).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
1923
|
+
var rawWs = String(ws).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
1924
|
+
return raw.slice(rawWs.length + 1);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1858
1927
|
// Historial de un proyecto: vista aparte con buscador dentro del proyecto.
|
|
1859
1928
|
function openHistory(folder) {
|
|
1860
1929
|
state.histFolder = folder;
|
|
@@ -2364,6 +2433,148 @@ if (els.btnImg) {
|
|
|
2364
2433
|
els.imgRemove.addEventListener('click', function () { setPendingImage(null); });
|
|
2365
2434
|
}
|
|
2366
2435
|
|
|
2436
|
+
// ---------------------------------------------------------------------------
|
|
2437
|
+
// Dictado por voz (Web Speech API nativa del navegador; sin dependencias).
|
|
2438
|
+
// ---------------------------------------------------------------------------
|
|
2439
|
+
var SpeechRec = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
2440
|
+
var voice = { rec: null, listening: false, base: '' };
|
|
2441
|
+
|
|
2442
|
+
function voiceSetListening(on) {
|
|
2443
|
+
voice.listening = on;
|
|
2444
|
+
if (!els.btnMic) return;
|
|
2445
|
+
els.btnMic.classList.toggle('listening', on);
|
|
2446
|
+
els.btnMic.title = on ? 'Detener dictado' : 'Dictar por voz';
|
|
2447
|
+
els.btnMic.setAttribute('aria-label', on ? 'Detener dictado' : 'Dictar por voz');
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
function voiceStop() {
|
|
2451
|
+
if (voice.rec && voice.listening) {
|
|
2452
|
+
try { voice.rec.stop(); } catch (e) {}
|
|
2453
|
+
}
|
|
2454
|
+
voiceSetListening(false);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
function voiceStart() {
|
|
2458
|
+
if (!voice.rec || state.currentId === null) return;
|
|
2459
|
+
voice.base = els.input.value ? els.input.value.replace(/\s+$/, ' ') : '';
|
|
2460
|
+
try { voice.rec.start(); } catch (e) { /* ya estaba iniciado */ }
|
|
2461
|
+
voiceSetListening(true);
|
|
2462
|
+
toast('dictado activo · hablá ahora', 'ok');
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
if (els.btnMic) {
|
|
2466
|
+
if (!SpeechRec) {
|
|
2467
|
+
els.btnMic.style.display = 'none';
|
|
2468
|
+
} else {
|
|
2469
|
+
voice.rec = new SpeechRec();
|
|
2470
|
+
voice.rec.lang = navigator.language || 'es-AR';
|
|
2471
|
+
voice.rec.continuous = true;
|
|
2472
|
+
voice.rec.interimResults = true;
|
|
2473
|
+
voice.rec.onresult = function (ev) {
|
|
2474
|
+
var fin = '', interim = '';
|
|
2475
|
+
for (var i = ev.resultIndex; i < ev.results.length; i++) {
|
|
2476
|
+
var r = ev.results[i];
|
|
2477
|
+
if (r.isFinal) fin += r[0].transcript;
|
|
2478
|
+
else interim += r[0].transcript;
|
|
2479
|
+
}
|
|
2480
|
+
if (fin) voice.base = (voice.base + fin).replace(/\s+/g, ' ');
|
|
2481
|
+
var tail = interim ? (voice.base ? ' ' : '') + interim : '';
|
|
2482
|
+
els.input.value = (voice.base + tail).replace(/^\s+/, '');
|
|
2483
|
+
autoGrow();
|
|
2484
|
+
};
|
|
2485
|
+
voice.rec.onerror = function (ev) {
|
|
2486
|
+
voiceSetListening(false);
|
|
2487
|
+
var m = (ev && ev.error) || '';
|
|
2488
|
+
if (m === 'not-allowed' || m === 'service-not-allowed') toast('permiso de micrófono denegado', 'error');
|
|
2489
|
+
else if (m !== 'aborted' && m !== 'no-speech') toast('dictado: ' + m, 'error');
|
|
2490
|
+
};
|
|
2491
|
+
voice.rec.onend = function () { voiceSetListening(false); };
|
|
2492
|
+
els.btnMic.addEventListener('click', function () {
|
|
2493
|
+
if (voice.listening) voiceStop();
|
|
2494
|
+
else voiceStart();
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
// ---------------------------------------------------------------------------
|
|
2500
|
+
// Plantillas de prompts (atajos guardados en el navegador).
|
|
2501
|
+
// ---------------------------------------------------------------------------
|
|
2502
|
+
var TPL_KEY = 'ob_tpl';
|
|
2503
|
+
|
|
2504
|
+
function tplLoad() {
|
|
2505
|
+
try {
|
|
2506
|
+
var a = JSON.parse(localStorage.getItem(TPL_KEY) || '[]');
|
|
2507
|
+
if (!Array.isArray(a)) return [];
|
|
2508
|
+
return a.filter(function (x) { return typeof x === 'string' && x.trim() !== ''; }).slice(0, 30);
|
|
2509
|
+
} catch (e) { return []; }
|
|
2510
|
+
}
|
|
2511
|
+
function tplStore(list) {
|
|
2512
|
+
try { localStorage.setItem(TPL_KEY, JSON.stringify(list)); } catch (e) {}
|
|
2513
|
+
}
|
|
2514
|
+
function tplClose() {
|
|
2515
|
+
if (els.tplPanel) els.tplPanel.style.display = 'none';
|
|
2516
|
+
}
|
|
2517
|
+
function tplRender() {
|
|
2518
|
+
var list = tplLoad();
|
|
2519
|
+
var html = '';
|
|
2520
|
+
for (var i = 0; i < list.length; i++) {
|
|
2521
|
+
html += '<div class="tpl-row" data-tpl="' + i + '"><span class="tpl-text">' + esc(list[i]) + '</span>'
|
|
2522
|
+
+ '<button type="button" class="tpl-del" data-del="' + i + '" title="Borrar plantilla" aria-label="Borrar plantilla">✕</button></div>';
|
|
2523
|
+
}
|
|
2524
|
+
if (!list.length) html += '<div class="tpl-empty">No hay plantillas. Escribí un prompt y tocá “guardar lo escrito”.</div>';
|
|
2525
|
+
html += '<button type="button" class="tpl-save" id="tplSave">+ guardar lo escrito</button>';
|
|
2526
|
+
els.tplPanel.innerHTML = html;
|
|
2527
|
+
}
|
|
2528
|
+
function tplOpen() {
|
|
2529
|
+
closeSlash();
|
|
2530
|
+
tplRender();
|
|
2531
|
+
els.tplPanel.style.display = 'block';
|
|
2532
|
+
}
|
|
2533
|
+
function tplInsert(text) {
|
|
2534
|
+
var cur = els.input.value;
|
|
2535
|
+
els.input.value = cur && cur.trim() !== '' ? (cur.replace(/\s+$/, '') + ' ' + text) : text;
|
|
2536
|
+
tplClose();
|
|
2537
|
+
autoGrow();
|
|
2538
|
+
els.input.focus();
|
|
2539
|
+
try { els.input.setSelectionRange(els.input.value.length, els.input.value.length); } catch (e) {}
|
|
2540
|
+
}
|
|
2541
|
+
function tplSaveCurrent() {
|
|
2542
|
+
var text = els.input.value.trim();
|
|
2543
|
+
if (!text) { toast('escribí un prompt primero', 'error'); return; }
|
|
2544
|
+
var list = tplLoad();
|
|
2545
|
+
if (list.indexOf(text) >= 0) { toast('esa plantilla ya existe', 'ok'); return; }
|
|
2546
|
+
list.unshift(text);
|
|
2547
|
+
tplStore(list.slice(0, 30));
|
|
2548
|
+
toast('plantilla guardada', 'ok');
|
|
2549
|
+
tplRender();
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
if (els.btnTpl && els.tplPanel) {
|
|
2553
|
+
els.btnTpl.addEventListener('click', function () {
|
|
2554
|
+
if (state.currentId === null) return;
|
|
2555
|
+
if (els.tplPanel.style.display === 'block') tplClose();
|
|
2556
|
+
else tplOpen();
|
|
2557
|
+
});
|
|
2558
|
+
els.tplPanel.addEventListener('click', function (e) {
|
|
2559
|
+
var del = e.target.closest('.tpl-del');
|
|
2560
|
+
if (del) {
|
|
2561
|
+
e.stopPropagation();
|
|
2562
|
+
var list = tplLoad();
|
|
2563
|
+
list.splice(parseInt(del.getAttribute('data-del'), 10), 1);
|
|
2564
|
+
tplStore(list);
|
|
2565
|
+
tplRender();
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
if (e.target.closest('.tpl-save')) { tplSaveCurrent(); return; }
|
|
2569
|
+
var row = e.target.closest('.tpl-row');
|
|
2570
|
+
if (row) {
|
|
2571
|
+
var list2 = tplLoad();
|
|
2572
|
+
var t = list2[parseInt(row.getAttribute('data-tpl'), 10)];
|
|
2573
|
+
if (typeof t === 'string') tplInsert(t);
|
|
2574
|
+
}
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2367
2578
|
// ---------------------------------------------------------------------------
|
|
2368
2579
|
// Composer: textarea multilínea (Enter envía, Shift+Enter salta de línea)
|
|
2369
2580
|
// y menú de comandos "/" con filtro, flechas y Enter.
|
|
@@ -2411,6 +2622,7 @@ els.input.addEventListener('input', function () {
|
|
|
2411
2622
|
autoGrow();
|
|
2412
2623
|
var v = els.input.value;
|
|
2413
2624
|
if (state.view === 'chat' && v.charAt(0) === '/') {
|
|
2625
|
+
tplClose();
|
|
2414
2626
|
slash.items = slashFilter(v);
|
|
2415
2627
|
slash.sel = 0;
|
|
2416
2628
|
renderSlash();
|
|
@@ -2443,7 +2655,8 @@ els.input.addEventListener('keydown', function (e) {
|
|
|
2443
2655
|
}
|
|
2444
2656
|
if (e.key === 'Escape') {
|
|
2445
2657
|
e.stopPropagation();
|
|
2446
|
-
|
|
2658
|
+
if (els.tplPanel && els.tplPanel.style.display === 'block') tplClose();
|
|
2659
|
+
else closeSlash();
|
|
2447
2660
|
return;
|
|
2448
2661
|
}
|
|
2449
2662
|
}
|
|
@@ -2472,6 +2685,7 @@ els.sendForm.addEventListener('submit', async function (ev) {
|
|
|
2472
2685
|
var text = els.input.value.trim();
|
|
2473
2686
|
if ((!text && !state.pendingImage) || state.isSending) return;
|
|
2474
2687
|
state.isSending = true;
|
|
2688
|
+
voiceStop();
|
|
2475
2689
|
try {
|
|
2476
2690
|
var body = { session: state.currentId, text: text };
|
|
2477
2691
|
if (state.pendingImage) body.image = state.pendingImage;
|
|
@@ -2480,6 +2694,7 @@ els.sendForm.addEventListener('submit', async function (ev) {
|
|
|
2480
2694
|
els.input.value = '';
|
|
2481
2695
|
autoGrow();
|
|
2482
2696
|
closeSlash();
|
|
2697
|
+
tplClose();
|
|
2483
2698
|
setPendingImage(null);
|
|
2484
2699
|
await loadHistory(state.currentId, true);
|
|
2485
2700
|
} else {
|
|
@@ -2644,9 +2859,17 @@ els.home.addEventListener('click', function (e) {
|
|
|
2644
2859
|
return;
|
|
2645
2860
|
}
|
|
2646
2861
|
var hist = e.target.closest('.histbtn');
|
|
2647
|
-
if (hist) {
|
|
2862
|
+
if (hist && !e.target.closest('.filesbtn')) {
|
|
2648
2863
|
e.stopPropagation();
|
|
2649
2864
|
openHistory(hist.getAttribute('data-folder'));
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
var fbtn = e.target.closest('.filesbtn');
|
|
2868
|
+
if (fbtn) {
|
|
2869
|
+
e.stopPropagation();
|
|
2870
|
+
state.filesPath = folderToFilesPath(fbtn.getAttribute('data-folder'));
|
|
2871
|
+
delete state.filesCache[state.filesPath];
|
|
2872
|
+
showView('files');
|
|
2650
2873
|
}
|
|
2651
2874
|
});
|
|
2652
2875
|
|
|
@@ -2751,6 +2974,132 @@ function filesJoin(name) {
|
|
|
2751
2974
|
return state.filesPath ? state.filesPath + '/' + name : name;
|
|
2752
2975
|
}
|
|
2753
2976
|
|
|
2977
|
+
// ---------------------------------------------------------------------------
|
|
2978
|
+
// Cambios del proyecto (git status/diff) vía la cola del puente.
|
|
2979
|
+
// ---------------------------------------------------------------------------
|
|
2980
|
+
function changesFolder() {
|
|
2981
|
+
return (state.currentSession && state.currentSession.folder) || '';
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
function renderChangesView() {
|
|
2985
|
+
els.hTitle.textContent = 'cambios';
|
|
2986
|
+
var folder = changesFolder();
|
|
2987
|
+
els.hSub.textContent = folder ? projectLabel(folder) : 'de la sesión abierta';
|
|
2988
|
+
if (!folder) {
|
|
2989
|
+
els.viewChanges.innerHTML = '<div class="placeholder">Abrí un chat para ver los cambios de su proyecto.</div>';
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
if (!state.online) {
|
|
2993
|
+
els.viewChanges.innerHTML = '<div class="placeholder">El puente está apagado. Iniciá “OpenBridge” en tu PC.</div>';
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
els.viewChanges.innerHTML = '<div class="view-head"><h2>cambios sin commitear</h2>'
|
|
2997
|
+
+ '<div class="view-sub">' + esc(projectLabel(folder)) + '</div></div>'
|
|
2998
|
+
+ '<div class="placeholder">consultando git…</div>';
|
|
2999
|
+
loadChanges(folder);
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
function chgStatusClass(st) {
|
|
3003
|
+
if (st === '??' || st === 'A') return 'add';
|
|
3004
|
+
if (st.indexOf('D') >= 0) return 'del';
|
|
3005
|
+
if (st.indexOf('M') >= 0 || st.indexOf('R') >= 0) return 'mod';
|
|
3006
|
+
return '';
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
function renderDiff(text) {
|
|
3010
|
+
var max = 200000;
|
|
3011
|
+
var trunc = false;
|
|
3012
|
+
if (text.length > max) { text = text.slice(0, max); trunc = true; }
|
|
3013
|
+
var lines = text.split('\n');
|
|
3014
|
+
var html = '';
|
|
3015
|
+
for (var i = 0; i < lines.length; i++) {
|
|
3016
|
+
var l = lines[i];
|
|
3017
|
+
var cls = 'diff-line';
|
|
3018
|
+
if (l.charAt(0) === '+') cls += ' add';
|
|
3019
|
+
else if (l.charAt(0) === '-') cls += ' del';
|
|
3020
|
+
else if (l.indexOf('@@') === 0) cls += ' hunk';
|
|
3021
|
+
else if (l.indexOf('diff --git') === 0 || l.indexOf('index ') === 0 || l.indexOf('--- ') === 0 || l.indexOf('+++ ') === 0) cls += ' head';
|
|
3022
|
+
html += '<div class="' + cls + '">' + esc(l || ' ') + '</div>';
|
|
3023
|
+
}
|
|
3024
|
+
if (trunc) html += '<div class="diff-line head">… (diff recortado)</div>';
|
|
3025
|
+
return '<div class="diff">' + html + '</div>';
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
function changesHead(folder, branch) {
|
|
3029
|
+
return '<div class="view-head"><h2>cambios sin commitear</h2><div class="view-sub">'
|
|
3030
|
+
+ esc(projectLabel(folder)) + (branch ? ' · ' + esc(branch) : '') + '</div></div>';
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
async function loadChanges(folder) {
|
|
3034
|
+
var st = await ocCommand('git_status', [folder], 15, 500);
|
|
3035
|
+
if (!st.ok) {
|
|
3036
|
+
els.viewChanges.innerHTML = changesHead(folder, '')
|
|
3037
|
+
+ '<div class="placeholder">' + esc(st.error || 'no se pudo consultar git') + '</div>';
|
|
3038
|
+
return;
|
|
3039
|
+
}
|
|
3040
|
+
var status = st.data || {};
|
|
3041
|
+
var files = status.files || [];
|
|
3042
|
+
if (!files.length) {
|
|
3043
|
+
els.viewChanges.innerHTML = changesHead(folder, status.branch)
|
|
3044
|
+
+ '<div class="placeholder">Sin cambios: el árbol está limpio.</div>';
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
var list = '<div class="chg-files">';
|
|
3048
|
+
for (var i = 0; i < files.length; i++) {
|
|
3049
|
+
var rev = files[i].status !== '??'
|
|
3050
|
+
? '<button type="button" class="chg-rev" data-path="' + esc(files[i].path) + '" title="Revertir este archivo" aria-label="Revertir este archivo">↩</button>'
|
|
3051
|
+
: '';
|
|
3052
|
+
list += '<div class="chg-row"><span class="chg-st ' + chgStatusClass(files[i].status) + '">' + esc(files[i].status) + '</span>'
|
|
3053
|
+
+ '<span class="chg-path">' + esc(files[i].path) + '</span>' + rev + '</div>';
|
|
3054
|
+
}
|
|
3055
|
+
list += '</div>';
|
|
3056
|
+
var tracked = files.some(function (f) { return f.status !== '??'; });
|
|
3057
|
+
var toolbar = tracked
|
|
3058
|
+
? '<div class="files-toolbar"><button type="button" class="linkbtn" id="chgRevert">↩ revertir cambios rastreados</button></div>'
|
|
3059
|
+
: '';
|
|
3060
|
+
els.viewChanges.innerHTML = changesHead(folder, status.branch) + toolbar + list + '<div class="placeholder">cargando diff…</div>';
|
|
3061
|
+
|
|
3062
|
+
var diff = await ocCommand('git_diff', [folder], 20, 700);
|
|
3063
|
+
var diffHtml;
|
|
3064
|
+
if (diff.ok) {
|
|
3065
|
+
var text = (diff.data && diff.data.diff) || '';
|
|
3066
|
+
diffHtml = text ? renderDiff(text) : '<div class="placeholder">Hay archivos nuevos o renombrados sin diff de contenido.</div>';
|
|
3067
|
+
} else {
|
|
3068
|
+
diffHtml = '<div class="placeholder">' + esc(diff.error || 'no se pudo obtener el diff') + '</div>';
|
|
3069
|
+
}
|
|
3070
|
+
els.viewChanges.innerHTML = changesHead(folder, status.branch) + toolbar + list + diffHtml;
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
// Revertir cambios rastreados (con confirmacion): borra las modificaciones que
|
|
3074
|
+
// el agente u otros hicieron en archivos ya versionados. No toca los nuevos.
|
|
3075
|
+
// Sirve para un archivo puntual (boton de la fila) o para todos.
|
|
3076
|
+
if (els.viewChanges) {
|
|
3077
|
+
els.viewChanges.addEventListener('click', function (e) {
|
|
3078
|
+
var folder = changesFolder();
|
|
3079
|
+
if (!folder) return;
|
|
3080
|
+
var one = e.target.closest('.chg-rev');
|
|
3081
|
+
if (one) {
|
|
3082
|
+
var rel = one.getAttribute('data-path');
|
|
3083
|
+
if (!window.confirm('¿Revertir “' + rel + '”?\n\nNo se puede deshacer.')) return;
|
|
3084
|
+
one.disabled = true;
|
|
3085
|
+
ocCommand('git_checkout', [folder, rel], 15, 500).then(function (r) {
|
|
3086
|
+
toast(r.ok ? 'revertido: ' + rel : (r.error || 'no se pudo revertir'), r.ok ? 'ok' : 'error');
|
|
3087
|
+
renderChangesView();
|
|
3088
|
+
});
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
var all = e.target.closest('#chgRevert');
|
|
3092
|
+
if (!all) return;
|
|
3093
|
+
if (!window.confirm('¿Revertir TODOS los cambios de archivos rastreados en “' + projectLabel(folder) + '”?\n\nNo se puede deshacer. Los archivos nuevos sin seguimiento no se tocan.')) return;
|
|
3094
|
+
all.disabled = true;
|
|
3095
|
+
all.textContent = 'revirtiendo…';
|
|
3096
|
+
ocCommand('git_checkout', [folder], 15, 500).then(function (r) {
|
|
3097
|
+
toast(r.ok ? 'cambios revertidos' : (r.error || 'no se pudo revertir'), r.ok ? 'ok' : 'error');
|
|
3098
|
+
renderChangesView();
|
|
3099
|
+
});
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
|
|
2754
3103
|
function renderFilesView() {
|
|
2755
3104
|
state.fileView = null;
|
|
2756
3105
|
var ws = (state.catalog && state.catalog.workspace) || (state.filesWorkspace || '');
|
package/src/web/routes.js
CHANGED
|
@@ -171,6 +171,9 @@ const OC_ALLOWED = {
|
|
|
171
171
|
opencode_version: [],
|
|
172
172
|
fs_list: ['path'],
|
|
173
173
|
fs_read: ['path'],
|
|
174
|
+
git_status: ['path'],
|
|
175
|
+
git_diff: ['path'],
|
|
176
|
+
git_checkout: ['path', 'path'],
|
|
174
177
|
tunnel_start: ['arg'],
|
|
175
178
|
tunnel_stop: ['arg'],
|
|
176
179
|
tunnel_list: [],
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
z-index: 30;
|
|
70
70
|
}
|
|
71
71
|
#sidebar .brand {
|
|
72
|
+
min-height: 54px;
|
|
72
73
|
padding: 12px 14px 10px;
|
|
73
74
|
display: flex;
|
|
74
75
|
align-items: center;
|
|
@@ -233,6 +234,7 @@
|
|
|
233
234
|
.sitem .sname { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; opacity: .78; }
|
|
234
235
|
.sitem.active .sname { opacity: 1; }
|
|
235
236
|
.sitem .stok { font-size: 10.5px; color: var(--muted); flex-shrink: 0; }
|
|
237
|
+
.sitem .scost { font-size: 10.5px; color: var(--warn-text, #d29922); flex-shrink: 0; }
|
|
236
238
|
.sitem .stime { font-size: 11px; color: var(--muted); flex-shrink: 0; }
|
|
237
239
|
.sitem .spen { width: 6px; height: 6px; border-radius: 50%; background: var(--mine); flex-shrink: 0; }
|
|
238
240
|
.sdot {
|
|
@@ -495,8 +497,9 @@
|
|
|
495
497
|
.projsec .pshead .pscount { font-size: 11px; color: var(--muted); flex-shrink: 0; }
|
|
496
498
|
.projsec .pbody { display: none; }
|
|
497
499
|
.projsec.open .pbody { display: block; }
|
|
500
|
+
.histrow { display: flex; gap: 6px; margin-top: 6px; }
|
|
498
501
|
.histbtn {
|
|
499
|
-
|
|
502
|
+
flex: 1; min-width: 0; padding: 8px; border: 1px dashed var(--border); border-radius: 8px;
|
|
500
503
|
background: transparent; color: var(--muted); font: inherit; font-size: 12px; cursor: pointer;
|
|
501
504
|
}
|
|
502
505
|
.histbtn:hover { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
|
|
@@ -598,6 +601,8 @@
|
|
|
598
601
|
.msg.mine .who { color: var(--mine); }
|
|
599
602
|
.msg.theirs { border-left-color: var(--accent); }
|
|
600
603
|
.msg.theirs .who { color: var(--accent); }
|
|
604
|
+
.msg.theirs.plan { border-left-color: var(--warn-text, #d29922); }
|
|
605
|
+
.msg.theirs.plan .who { color: var(--warn-text, #d29922); }
|
|
601
606
|
.msg.error { border-left-color: var(--danger); }
|
|
602
607
|
/* Aire extra en el cambio de turno: lo tuyo ↔ lo que responde el agente */
|
|
603
608
|
.msg.mine + .msg.theirs,
|
|
@@ -763,6 +768,17 @@
|
|
|
763
768
|
.slash-panel .item.sel, .slash-panel .item:hover { background: var(--accent-soft); }
|
|
764
769
|
.slash-panel .item b { color: var(--mine); font-weight: 600; }
|
|
765
770
|
.slash-panel .item span { display: block; color: var(--muted); font-size: 11px; margin-top: 1px; }
|
|
771
|
+
.tpl-panel .tpl-row { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: 8px; cursor: pointer; font-size: 13.5px; }
|
|
772
|
+
.tpl-panel .tpl-row:hover { background: var(--accent-soft); }
|
|
773
|
+
.tpl-panel .tpl-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
774
|
+
.tpl-panel .tpl-del { background: transparent; border: 0; color: var(--muted); cursor: pointer; font: inherit; padding: 2px 6px; border-radius: 6px; flex-shrink: 0; }
|
|
775
|
+
.tpl-panel .tpl-del:hover { color: var(--danger); }
|
|
776
|
+
.tpl-panel .tpl-empty { color: var(--muted); font-size: 12px; padding: 8px 10px; line-height: 1.4; }
|
|
777
|
+
.tpl-panel .tpl-save {
|
|
778
|
+
display: block; width: 100%; margin-top: 6px; padding: 8px; border: 1px dashed var(--border);
|
|
779
|
+
border-radius: 8px; background: transparent; color: var(--muted); font: inherit; font-size: 12.5px; cursor: pointer;
|
|
780
|
+
}
|
|
781
|
+
.tpl-panel .tpl-save:hover { color: var(--accent); border-color: var(--accent); }
|
|
766
782
|
form.sendbar button {
|
|
767
783
|
padding: 9px 16px; border: 1px solid var(--mine); border-radius: 8px; background: transparent; color: var(--mine);
|
|
768
784
|
font: inherit; font-size: 13.5px; font-weight: 600; cursor: pointer;
|
|
@@ -778,6 +794,14 @@
|
|
|
778
794
|
}
|
|
779
795
|
form.sendbar .imgbtn:hover { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
|
|
780
796
|
form.sendbar .imgbtn svg { flex-shrink: 0; display: block; }
|
|
797
|
+
form.sendbar .imgbtn.listening {
|
|
798
|
+
color: var(--danger); border-color: var(--danger); background: transparent;
|
|
799
|
+
animation: micpulse 1.1s ease-in-out infinite;
|
|
800
|
+
}
|
|
801
|
+
@keyframes micpulse {
|
|
802
|
+
0%, 100% { box-shadow: 0 0 0 0 transparent; }
|
|
803
|
+
50% { box-shadow: 0 0 0 4px var(--accent-soft); }
|
|
804
|
+
}
|
|
781
805
|
/* ----- Botón que abre la hoja de escritura (móvil) ----- */
|
|
782
806
|
.composeopen {
|
|
783
807
|
display: none; align-items: center; gap: 10px; width: 100%;
|
|
@@ -945,6 +969,33 @@
|
|
|
945
969
|
color: var(--muted); font-size: 13px; padding: 24px 0; text-align: center;
|
|
946
970
|
}
|
|
947
971
|
|
|
972
|
+
/* ----- Cambios (git) ----- */
|
|
973
|
+
.chg-files { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; margin: 0 0 14px; }
|
|
974
|
+
.chg-row { display: grid; grid-template-columns: 34px 1fr auto; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--border); font-size: 13px; align-items: center; }
|
|
975
|
+
.chg-row:last-child { border-bottom: 0; }
|
|
976
|
+
.chg-rev {
|
|
977
|
+
background: transparent; border: 1px solid var(--border); color: var(--muted);
|
|
978
|
+
border-radius: 6px; width: 26px; height: 24px; line-height: 1; cursor: pointer; font: inherit; font-size: 12px;
|
|
979
|
+
}
|
|
980
|
+
.chg-rev:hover { color: var(--danger); border-color: var(--danger); }
|
|
981
|
+
.chg-rev:disabled { opacity: .4; cursor: not-allowed; }
|
|
982
|
+
.chg-st { font-weight: 700; text-align: center; font-size: 11px; color: var(--muted); }
|
|
983
|
+
.chg-st.mod { color: var(--warn-text, #d29922); }
|
|
984
|
+
.chg-st.add { color: var(--accent); }
|
|
985
|
+
.chg-st.del { color: var(--danger); }
|
|
986
|
+
.chg-path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
987
|
+
.diff {
|
|
988
|
+
background: var(--code-bg, var(--bg)); border: 1px solid var(--border);
|
|
989
|
+
border-radius: 8px; overflow: auto; padding: 10px 0; margin: 0 0 20px;
|
|
990
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
991
|
+
font-size: 12px; line-height: 1.5;
|
|
992
|
+
}
|
|
993
|
+
.diff-line { white-space: pre; padding: 0 12px; }
|
|
994
|
+
.diff-line.add { background: rgba(63, 185, 80, .12); color: #3fb950; }
|
|
995
|
+
.diff-line.del { background: rgba(248, 81, 73, .12); color: #f85149; }
|
|
996
|
+
.diff-line.hunk { color: var(--accent); }
|
|
997
|
+
.diff-line.head { color: var(--muted); }
|
|
998
|
+
|
|
948
999
|
.view-head { margin-bottom: 12px; }
|
|
949
1000
|
.view-head h2 { margin: 0 0 4px; font-size: 16px; }
|
|
950
1001
|
.view-sub { font-size: 11.5px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
@@ -1146,6 +1197,7 @@
|
|
|
1146
1197
|
<button data-view="search" type="button" title="Buscar">buscar</button>
|
|
1147
1198
|
<button data-view="preview" type="button" title="Vista previa del proyecto (túnel)">preview</button>
|
|
1148
1199
|
<button data-view="procs" type="button" title="Procesos de la sesión">procs</button>
|
|
1200
|
+
<button data-view="changes" type="button" title="Cambios del proyecto (git)">cambios</button>
|
|
1149
1201
|
</div>
|
|
1150
1202
|
<div class="footer">
|
|
1151
1203
|
<div class="row" id="bridgeStatusRow">
|
|
@@ -1208,10 +1260,13 @@
|
|
|
1208
1260
|
</div>
|
|
1209
1261
|
<span class="prompt">❯</span>
|
|
1210
1262
|
<textarea id="input" rows="1" placeholder="escribí un mensaje…" autocomplete="off"></textarea>
|
|
1263
|
+
<button type="button" class="imgbtn" id="btnTpl" title="Plantillas de prompts" aria-label="Plantillas de prompts"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z"/></svg></button>
|
|
1211
1264
|
<button type="button" class="imgbtn" id="btnImg" title="Adjuntar imagen (modelos con visión)" aria-label="Adjuntar imagen"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"/></svg></button>
|
|
1265
|
+
<button type="button" class="imgbtn" id="btnMic" title="Dictar por voz" aria-label="Dictar por voz"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18.75a6 6 0 0 0 6-6v-1.5m-6 7.5a6 6 0 0 1-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 0 1-3-3V4.5a3 3 0 1 1 6 0v8.25a3 3 0 0 1-3 3Z"/></svg></button>
|
|
1212
1266
|
<input type="file" id="imgInput" accept="image/png,image/jpeg,image/webp,image/gif" style="display:none">
|
|
1213
1267
|
<button type="submit" id="sendBtn">enviar</button>
|
|
1214
1268
|
<div id="slashPanel" class="slash-panel"></div>
|
|
1269
|
+
<div id="tplPanel" class="slash-panel tpl-panel"></div>
|
|
1215
1270
|
</form>
|
|
1216
1271
|
</div>
|
|
1217
1272
|
|
|
@@ -1221,6 +1276,7 @@
|
|
|
1221
1276
|
<div id="viewHistory" class="view" style="display:none"></div>
|
|
1222
1277
|
<div id="viewPreview" class="view" style="display:none"></div>
|
|
1223
1278
|
<div id="viewProcs" class="view" style="display:none"></div>
|
|
1279
|
+
<div id="viewChanges" class="view" style="display:none"></div>
|
|
1224
1280
|
|
|
1225
1281
|
<footer id="statusbar">
|
|
1226
1282
|
<span class="dot" id="sbDot"></span>
|