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.
@@ -526,7 +526,9 @@ export task pick_folder()
526
526
  require env("OS")
527
527
  try
528
528
  when is_win()
529
- let ps be "Add-Type -AssemblyName System.Windows.Forms; $d = New-Object System.Windows.Forms.FolderBrowserDialog; $d.Description = 'Elegí la carpeta del proyecto (workspace de Lampson)'; $d.ShowNewFolderButton = $true; $f = New-Object System.Windows.Forms.Form; $f.TopMost = $true; if ($d.ShowDialog($f) -eq 'OK') { Write-Output $d.SelectedPath }"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lampson",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "A coding agent written in Synsema: terminal + web, tools confined to the mounted project, lamps (your own tool plugins), LSP, MCP, sub-agents.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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
@@ -32,5 +32,6 @@
32
32
  </main>
33
33
  <script src="/js/panel.js"></script>
34
34
  <script src="/js/hub.js"></script>
35
+ <script src="/js/workspaces.js"></script>
35
36
  </body>
36
37
  </html>
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 wsList) {
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);
@@ -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
- const list = (r.ports || []).filter(p => p.port !== 8080 || p.name !== 'synsema.exe');
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) {
@@ -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)}/ · :${w.port} (solo loopback)</span><span class="del">quitar del registro</span></div><div class="derr"></div>`,
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="dacts" style="margin-top:0"><button class="primary" data-pick>Elegir carpeta…</button><span class="ds" data-pickhint>abre el diálogo de tu sistema</span></div>
48
- <label style="margin-top:14px">ruta <input name="path" spellcheck="false" autocomplete="off" placeholder="C:\\proyectos\\mi-app · /home/yo/proyectos/mi-app"></label>
49
- <div data-browser style="display:none"><div class="dcap" data-here></div><div class="bitems" data-dirs style="max-height:32vh;border:1px solid var(--rule);border-radius:var(--r);padding:4px"></div></div>
50
- <div class="pfoot"><button class="primary" data-go>Crear y abrir</button><button data-browse>explorar en el servidor</button><span class="derr"></span></div></div>`;
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, err) {
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 browser = box.querySelector('[data-browser]');
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 = r.path + (r.error ? ' — ' + r.error : '');
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(r.path.replace(/[\\/]$/, '') + (r.path.includes('\\') ? '\\' : '/') + d); list.appendChild(it); }
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(f('path').value.trim());
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.data.native) { err(''); box.querySelector('[data-pickhint]').textContent = 'no hay diálogo en este equipo (VPS): usá el explorador'; showDir(''); return; }
69
- err(''); if (r.data.path) f('path').value = r.data.path;
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
- const path = f('path').value.trim(); if (!path) { err('elegí una carpeta'); return; }
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 (--watch): esperar a que vuelva y navegar
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(BASE + '/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'); };
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
- if ($('#wsPill')) { $('#wsPill').onclick = () => openWorkspaces(); paintWorkspacePill(); }
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(); } }