lampson 0.2.0 → 0.2.2
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/lib/workspaces.syn +3 -1
- package/package.json +1 -1
- package/public/css/panel.css +27 -0
- package/public/hub.html +1 -0
- package/public/js/hub.js +1 -2
- package/public/js/procs.js +2 -1
- package/public/js/workspaces.js +35 -21
package/lib/workspaces.syn
CHANGED
|
@@ -526,7 +526,9 @@ export task pick_folder()
|
|
|
526
526
|
require env("OS")
|
|
527
527
|
try
|
|
528
528
|
when is_win()
|
|
529
|
-
|
|
529
|
+
-- Shell.Application.BrowseForFolder: el diálogo del Explorador; sale al frente aunque este proceso no tenga
|
|
530
|
+
-- ventana (FolderBrowserDialog de WinForms quedaba detrás desde un proceso oculto). 0x40 = con campo de ruta, 0x1 = solo carpetas
|
|
531
|
+
let ps be "$s = New-Object -ComObject Shell.Application; $f = $s.BrowseForFolder(0, 'Elegí la carpeta del proyecto (workspace de Lampson)', 0x41, 0); if ($f) { Write-Output $f.Self.Path }"
|
|
530
532
|
let r be run("powershell", ["-NoProfile", "-STA", "-Command", ps], 300)
|
|
531
533
|
give trim(text(r["stdout"]))
|
|
532
534
|
let r be run("sh", ["-c", "if command -v osascript >/dev/null; then osascript -e 'POSIX path of (choose folder with prompt \"Carpeta del proyecto\")'; elif command -v zenity >/dev/null; then zenity --file-selection --directory --title='Carpeta del proyecto'; else exit 3; fi"], 300)
|
package/package.json
CHANGED
package/public/css/panel.css
CHANGED
|
@@ -123,3 +123,30 @@
|
|
|
123
123
|
.panel .phelp p { margin:0 0 12px; max-width:78ch; }
|
|
124
124
|
.panel .phelp ul { margin:0 0 16px 20px; padding:0; display:grid; gap:4px; max-width:78ch; }
|
|
125
125
|
.panel .phelp button { text-transform:none; letter-spacing:0; font:400 12px/1 var(--mono); padding:6px 10px; }
|
|
126
|
+
|
|
127
|
+
/* detalle: la prosa y los formularios de las vistas no heredan la mono del contenedor */
|
|
128
|
+
.panel .bdetail .lead, .panel .bdetail .ddesc, .panel .bdetail .ds, .panel .bdetail .none { font-family:var(--serif); }
|
|
129
|
+
.panel .bdetail .ds { display:block; color:var(--ink-3); font:400 11.5px/1.45 var(--serif); }
|
|
130
|
+
.panel .bdetail .dform input, .panel .bdetail .dform select, .panel .bdetail .dform textarea { max-width:560px; }
|
|
131
|
+
.panel .bdetail .dform label { color:var(--ink-3); }
|
|
132
|
+
.panel .bdetail .dform label.col { max-width:560px; }
|
|
133
|
+
.panel .bdetail .dacts .ds { display:inline; color:var(--ink-3); }
|
|
134
|
+
.panel .bdetail .dfoot { font-family:var(--mono); }
|
|
135
|
+
|
|
136
|
+
/* nuevo workspace: elegir carpeta → carpeta elegida → crear */
|
|
137
|
+
.panel .pickrow { display:flex; align-items:center; gap:12px; margin:6px 0 12px; }
|
|
138
|
+
.panel .pickrow .ds { display:inline; margin:0; }
|
|
139
|
+
.panel .pickrow a { color:var(--accent); text-decoration:none; }
|
|
140
|
+
.panel .pickrow a:hover { text-decoration:underline; text-underline-offset:.18em; }
|
|
141
|
+
.panel .chosen { display:flex; align-items:center; gap:10px; padding:10px 12px; border:1px solid var(--rule-2); border-radius:var(--r); background:var(--paper-2); margin:0 0 6px; max-width:640px; }
|
|
142
|
+
.panel .chosen .lk { font:400 10.5px/1 var(--mono); text-transform:uppercase; letter-spacing:.1em; color:var(--ink-3); }
|
|
143
|
+
.panel .chosen code { font:400 12.5px/1.4 var(--mono); color:var(--ink); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1; }
|
|
144
|
+
.panel .chosen .del { color:var(--ink-3); cursor:pointer; font-size:11px; }
|
|
145
|
+
.panel .chosen .del:hover { color:var(--rubric); }
|
|
146
|
+
.panel .wsbrowser { border:1px solid var(--rule); border-radius:var(--r); max-width:640px; margin:0 0 10px; display:flex; flex-direction:column; }
|
|
147
|
+
.panel .wsbrowser .bhead { display:flex; align-items:center; gap:10px; padding:8px 10px; border-bottom:1px solid var(--rule); }
|
|
148
|
+
.panel .wsbrowser .bhead .ds { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-family:var(--mono); color:var(--ink); display:inline; }
|
|
149
|
+
.panel .wsbrowser .bhead input { flex:0 0 240px; height:28px; padding:0 8px; font-size:12px; background:transparent; }
|
|
150
|
+
.panel .wsbrowser .bitems { max-height:30vh; overflow-y:auto; padding:4px; }
|
|
151
|
+
.panel .wsbrowser .bfoot { padding:8px 10px; border-top:1px solid var(--rule); display:flex; justify-content:flex-end; }
|
|
152
|
+
.panel .wsbrowser .bfoot button { font:400 12px/1 var(--mono); padding:6px 12px; }
|
package/public/hub.html
CHANGED
package/public/js/hub.js
CHANGED
|
@@ -22,9 +22,8 @@ paintTheme();
|
|
|
22
22
|
// tarjetas
|
|
23
23
|
async function paintGrid() {
|
|
24
24
|
const grid = $('#wsGrid'); let r; try { r = await (await fetch(BASE + '/api/workspaces')).json(); } catch (e) { grid.innerHTML = '<div class="card"><p>el hub no responde</p></div>'; return; }
|
|
25
|
-
wsList = r.workspaces || []; wsIdle = r.idle_hours;
|
|
26
25
|
grid.innerHTML = '';
|
|
27
|
-
for (const w of
|
|
26
|
+
for (const w of (r.workspaces || [])) {
|
|
28
27
|
const c = document.createElement('a'); c.className = 'card ws' + (w.alive ? ' on' : ''); c.href = w.url;
|
|
29
28
|
c.innerHTML = `<h3>${w.alive ? '● vivo' : '○ apagado'}${w.schedules_on ? ' · ⏰ ' + w.schedules_on : ''}</h3><p class="nm">${esc(w.name)}</p><p class="pth">${esc(w.path)}</p><p class="meta">último uso ${esc(fmtWhen(w.last_used))}</p>`;
|
|
30
29
|
grid.appendChild(c);
|
package/public/js/procs.js
CHANGED
|
@@ -3,7 +3,8 @@ let procOpen = null, procTimer = null;
|
|
|
3
3
|
async function loadPorts() {
|
|
4
4
|
let r; try { r = await (await fetch(BASE + '/api/ports')).json(); } catch (e) { return; }
|
|
5
5
|
const box = $('#ports'); box.innerHTML = '';
|
|
6
|
-
|
|
6
|
+
// los procesos de lampson (hub :8080 y los workspaces :808N) no son «servidores del proyecto»
|
|
7
|
+
const list = (r.ports || []).filter(p => !(p.name === 'synsema.exe' || p.name === 'synsema') || !/\b(web|hub)\.syn\b/.test(p.command || '') && p.port !== 8080);
|
|
7
8
|
$('#portCount').textContent = list.length || ''; autoSec('ports', list.length > 0);
|
|
8
9
|
if (!list.length) { box.innerHTML = '<div class="p none">ninguno</div>'; return; }
|
|
9
10
|
for (const p of list) {
|
package/public/js/workspaces.js
CHANGED
|
@@ -27,7 +27,7 @@ function openWorkspaces(selectSlug) {
|
|
|
27
27
|
<div class="ddesc">${w.schedules_on ? `${w.schedules_on} tarea${w.schedules_on === 1 ? '' : 's'} programada${w.schedules_on === 1 ? '' : 's'} encendida${w.schedules_on === 1 ? '' : 's'} (lo mantienen vivo). ` : ''}Último uso: ${esc(fmtWhen(w.last_used))}.</div>
|
|
28
28
|
<div class="dacts"><button class="primary" data-open>${w.slug === WS_SLUG ? 'Ya estás acá' : 'Abrir'}</button>${w.alive ? '<button data-stop>■ apagar</button>' : '<button data-start>▶ encender</button>'}</div>
|
|
29
29
|
<div class="dform" style="margin-top:14px"><label>vida <select name="policy"><option value="auto" ${w.policy === 'auto' ? 'selected' : ''}>auto — vivo mientras se use (${wsIdle === 0 ? 'nunca se apaga' : 'se apaga tras ' + wsIdle + ' h sin uso'}) o tenga tareas</option><option value="always" ${w.policy === 'always' ? 'selected' : ''}>siempre vivo</option><option value="off" ${w.policy === 'off' ? 'selected' : ''}>apagado — solo cuando lo abrís</option></select></label><span class="ds">las horas de inactividad se cambian en ⚙ → General</span></div>
|
|
30
|
-
<div class="dfoot"><span>/w/${esc(w.slug)}
|
|
30
|
+
<div class="dfoot"><span>/w/${esc(w.slug)} · :${w.port} (solo loopback)</span><span class="del">quitar del registro</span></div><div class="derr"></div>`,
|
|
31
31
|
wire: (w, box) => {
|
|
32
32
|
const err = (m) => { const e = box.querySelector('.derr'); if (e) e.textContent = m || ''; };
|
|
33
33
|
if (w === WS_NEW) { wsNewWire(box, err); return; }
|
|
@@ -44,40 +44,54 @@ function openWorkspaces(selectSlug) {
|
|
|
44
44
|
function wsNewForm() {
|
|
45
45
|
return `<div class="dhead"><span class="nm serif">Nuevo workspace</span></div><div class="dform">
|
|
46
46
|
<p class="lead">Un workspace es una carpeta de tu disco: el agente solo puede tocar lo que hay adentro. Corre en su propio proceso, con sus sesiones, tareas programadas, MCP y lámparas.</p>
|
|
47
|
-
<div class="
|
|
48
|
-
<
|
|
49
|
-
<div data-browser
|
|
50
|
-
|
|
47
|
+
<div class="pickrow"><button class="primary" data-pick>Elegir carpeta…</button><span class="ds">se abre el diálogo de carpetas de tu sistema · <a href="#" data-browse>o explorá desde acá</a></span></div>
|
|
48
|
+
<div class="chosen" data-chosen style="display:none"><span class="lk">carpeta</span><code data-chosen-path></code><span class="del" data-clear title="elegir otra">✕</span></div>
|
|
49
|
+
<div data-browser class="wsbrowser" style="display:none">
|
|
50
|
+
<div class="bhead"><span class="ds" data-here></span><input name="path" spellcheck="false" autocomplete="off" placeholder="o pegá una ruta y Enter"></div>
|
|
51
|
+
<div class="bitems" data-dirs></div>
|
|
52
|
+
<div class="bfoot"><button data-use>Usar esta carpeta</button></div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="pfoot" data-go-row style="display:none;justify-content:flex-end"><span class="derr" style="margin:0 auto 0 0"></span><button class="primary" data-go>Crear y abrir</button></div>
|
|
55
|
+
<div class="derr" data-err2></div></div>`;
|
|
51
56
|
}
|
|
52
|
-
function wsNewWire(box,
|
|
57
|
+
function wsNewWire(box, err0) {
|
|
58
|
+
const chosenBox = box.querySelector('[data-chosen]'), goRow = box.querySelector('[data-go-row]'), browser = box.querySelector('[data-browser]');
|
|
53
59
|
const f = n => box.querySelector(`[name="${n}"]`);
|
|
54
|
-
const
|
|
60
|
+
const err = (m) => { const e = goRow.style.display === 'none' ? box.querySelector('[data-err2]') : goRow.querySelector('.derr'); box.querySelectorAll('.derr').forEach(x => x.textContent = ''); if (e) e.textContent = m || ''; };
|
|
61
|
+
let chosen = '';
|
|
62
|
+
const choose = (p) => { chosen = (p || '').trim(); chosenBox.style.display = chosen ? '' : 'none'; goRow.style.display = chosen ? 'flex' : 'none'; box.querySelector('[data-chosen-path]').textContent = chosen; if (chosen) { browser.style.display = 'none'; err(''); } };
|
|
63
|
+
let here = '';
|
|
55
64
|
const showDir = async (p) => {
|
|
65
|
+
err('');
|
|
56
66
|
const r = await (await fetch('/api/workspaces/browse?path=' + encodeURIComponent(p || ''))).json();
|
|
57
67
|
browser.style.display = '';
|
|
58
|
-
box.querySelector('[data-here]').textContent =
|
|
59
|
-
f('path').value = r.path || '';
|
|
68
|
+
here = r.path || ''; box.querySelector('[data-here]').textContent = here + (r.error ? ' — ' + r.error : ''); f('path').value = '';
|
|
60
69
|
const list = box.querySelector('[data-dirs]'); list.innerHTML = '';
|
|
61
|
-
const up = document.createElement('div'); up.className = 'li nodot'; up.innerHTML = '<div><div class="nm">..</div></div>'; up.onclick = () => showDir(r.parent); list.appendChild(up);
|
|
62
|
-
for (const d of (r.dirs || [])) { const it = document.createElement('div'); it.className = 'li nodot'; it.innerHTML = `<div><div class="nm" style="font-weight:400">${esc(d)}/</div></div>`; it.onclick = () => showDir(
|
|
70
|
+
const up = document.createElement('div'); up.className = 'li nodot'; up.innerHTML = '<div><div class="nm">..</div><div class="meta">subir un nivel</div></div>'; up.onclick = () => showDir(r.parent); list.appendChild(up);
|
|
71
|
+
for (const d of (r.dirs || [])) { const it = document.createElement('div'); it.className = 'li nodot'; it.innerHTML = `<div><div class="nm" style="font-weight:400">${esc(d)}/</div></div>`; it.onclick = () => showDir(here.replace(/[\\/]$/, '') + (here.includes('\\') ? '\\' : '/') + d); list.appendChild(it); }
|
|
63
72
|
};
|
|
64
|
-
box.querySelector('[data-browse]').onclick = () => showDir(
|
|
73
|
+
box.querySelector('[data-browse]').onclick = (e) => { e.preventDefault(); showDir(chosen || ''); };
|
|
74
|
+
box.querySelector('[data-use]').onclick = () => choose(here);
|
|
75
|
+
f('path').addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); const v = f('path').value.trim(); if (v) showDir(v); } });
|
|
76
|
+
box.querySelector('[data-clear]').onclick = () => choose('');
|
|
65
77
|
box.querySelector('[data-pick]').onclick = async () => {
|
|
66
|
-
err('esperando el diálogo… (mirá la barra de tareas)');
|
|
78
|
+
err('esperando el diálogo de carpetas… (si no lo ves, mirá la barra de tareas)');
|
|
67
79
|
const r = await api('/api/workspaces/pick', {});
|
|
68
|
-
if (!r.
|
|
69
|
-
|
|
80
|
+
if (!r.ok) { err('esta página no es el hub: abrí ' + location.protocol + '//' + location.hostname + ':8080/'); return; }
|
|
81
|
+
if (!r.data.native) { err('no hay diálogo en este equipo (VPS): explorá desde acá'); showDir(''); return; }
|
|
82
|
+
err(''); if (r.data.path) choose(r.data.path);
|
|
70
83
|
};
|
|
71
84
|
box.querySelector('[data-go]').onclick = async () => {
|
|
72
|
-
|
|
85
|
+
if (!chosen) { err('elegí una carpeta'); return; }
|
|
73
86
|
err('creando y arrancando… (el hub se reinicia un instante)');
|
|
74
|
-
const r = await api('/api/workspaces', { path });
|
|
87
|
+
const r = await api('/api/workspaces', { path: chosen });
|
|
75
88
|
if (!r.ok) { err(r.data.error || ('error ' + r.status)); return; }
|
|
76
|
-
// el hub se regenera con la ruta nueva y se reinicia
|
|
89
|
+
// el hub se regenera con la ruta nueva y se reinicia: esperar a que vuelva y navegar
|
|
77
90
|
const url = r.data.url; let tries = 0;
|
|
78
|
-
const poll = async () => { tries++; try { const h = await fetch(
|
|
91
|
+
const poll = async () => { tries++; try { const h = await fetch('/api/hub'); if (h.ok && tries > 2) { location.href = url; return; } } catch (e) {} if (tries < 40) setTimeout(poll, 500); else err('el hub no volvió: abrí ' + url + ' a mano'); };
|
|
79
92
|
setTimeout(poll, 1200);
|
|
80
93
|
};
|
|
81
|
-
f('path').focus();
|
|
82
94
|
}
|
|
83
|
-
|
|
95
|
+
// standalone (abierto por el puerto del workspace, no por el hub): la pill lleva al hub
|
|
96
|
+
const HUB_URL = (location.port && location.port !== '8080') ? location.protocol + '//' + location.hostname + ':8080/' : '';
|
|
97
|
+
if ($('#wsPill')) { if (HUB_URL) { const pill = $('#wsPill'); pill.style.display = ''; pill.textContent = 'workspaces ↗'; pill.title = 'esta pestaña es el proceso del workspace; los workspaces se gestionan en el hub'; pill.onclick = () => window.open(HUB_URL, '_blank'); } else { $('#wsPill').onclick = () => openWorkspaces(); paintWorkspacePill(); } }
|