lampson 0.2.1 → 0.2.3

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.
@@ -24,7 +24,8 @@ export let REGISTRY be ".lampson/workspaces.json"
24
24
  export let HUB_PORT be 8080
25
25
  export let HUB_FILE be "hub.syn"
26
26
  export let HUB_TEMPLATE be "hub.tpl.syn"
27
- let FIRST_PORT be 8081
27
+ -- rango alto para no chocar con lo que usan las apps (3000, 5173, 8000, 8080-8090…): solo el hub queda en 8080
28
+ let FIRST_PORT be 47101
28
29
  let LINKS be ["lib", "public", "skills", "lamps", "memory"]
29
30
  let COPIES be ["web.syn", "chat.syn"]
30
31
 
@@ -82,6 +83,38 @@ task load_reg()
82
83
  task save_reg(doc)
83
84
  write_file(REGISTRY, json_encode(doc))
84
85
 
86
+ -- registros hechos con puertos 808N (antes de 0.2.3): reasignar al rango alto; los procesos viejos se paran en
87
+ -- ensure/tick (por puerto) y el supervisor los levanta en el nuevo
88
+ export task migrate_ports()
89
+ require exec
90
+ require time
91
+ require env("LAMPSON_*")
92
+ require env("OS")
93
+ require file(".lampson")
94
+ require file(".lampson/*")
95
+ require file.read("hub.tpl.syn")
96
+ require file("hub.syn")
97
+ let doc be load_reg()
98
+ let changed be false
99
+ when doc["next_port"] < FIRST_PORT
100
+ set doc["next_port"] to FIRST_PORT
101
+ set changed to true
102
+ each k in keys(doc["workspaces"])
103
+ let w be doc["workspaces"][k]
104
+ when w["port"] < FIRST_PORT
105
+ let old be w["port"]
106
+ each l in proc.listeners()
107
+ when l["port"] == old
108
+ proc.kill_pid(l["pid"])
109
+ set w["port"] to doc["next_port"]
110
+ set doc["next_port"] to doc["next_port"] + 1
111
+ set doc["workspaces"][k] to w
112
+ set changed to true
113
+ when changed
114
+ save_reg(doc)
115
+ regen_hub()
116
+ give changed
117
+
85
118
  export task all()
86
119
  require file(".lampson")
87
120
  require file(".lampson/*")
@@ -241,6 +274,7 @@ export task ensure(path)
241
274
  require file.read("chat.syn")
242
275
  require file.read("hub.tpl.syn")
243
276
  require file("hub.syn")
277
+ migrate_ports()
244
278
  let existing be by_path(path)
245
279
  let w be existing
246
280
  when w == nothing
@@ -490,6 +524,8 @@ export task tick()
490
524
  require file(".lampson/*")
491
525
  require file.read("web.syn")
492
526
  require file.read("chat.syn")
527
+ when migrate_ports() and hub_alive()
528
+ restart_hub_later()
493
529
  let started be []
494
530
  let stopped be []
495
531
  each w in all()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lampson",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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": {
@@ -132,3 +132,21 @@
132
132
  .panel .bdetail .dform label.col { max-width:560px; }
133
133
  .panel .bdetail .dacts .ds { display:inline; color:var(--ink-3); }
134
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; }
@@ -78,3 +78,7 @@
78
78
  .del.ask .yes { color:var(--rubric); cursor:pointer; font-weight:600; }
79
79
  .del.ask .no { color:var(--ink-2); cursor:pointer; }
80
80
  .del.ask .yes:hover, .del.ask .no:hover { text-decoration:underline; text-underline-offset:.18em; }
81
+
82
+ /* puertos ocupados por lampson: visibles pero sin link ni ✕ */
83
+ .p .port.own { color:var(--ink-3); font-weight:400; }
84
+ .p.own .cm { color:var(--ink-3); }
package/public/js/app.js CHANGED
@@ -14,7 +14,7 @@ async function loadCfg() {
14
14
  sel.onchange = () => { localStorage.setItem('lampson.agent', sel.value); hint(); }; hint();
15
15
  if (!log.children.length) empty();
16
16
  }
17
- loadCfg().then(() => { loadTree(); loadSessions(); loadProcs(); loadMemory(); loadAgents(); loadMcp(); loadLsp(); loadLamps(); loadTodo(); loadSched(); loadApprovals(); checkUpdate(); const pq = new URLSearchParams(location.search).get('proc'); if (pq) openProc(pq); else if (session) open(session); });
17
+ loadCfg().then(() => { loadTree(); loadSessions(); loadProcs(); loadMemory(); loadAgents(); loadMcp(); loadLsp(); loadLamps(); loadTodo(); loadSched(); loadApprovals(); checkUpdate(); const pq = new URLSearchParams(location.search).get('proc'); if (pq) openProc(pq); else if (session) openSession(session); });
18
18
  // respaldos lentos por si el stream de eventos se cae
19
19
  setInterval(loadProcs, events ? 60000 : 10000);
20
20
  setInterval(() => { loadSched(); if (!events) loadApprovals(); }, events ? 60000 : 15000);
@@ -3,12 +3,16 @@ 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, un workspace por puerto alto) OCUPAN el puerto: se ven, identificados,
7
+ // sin link (la UI se usa por el hub) y sin ✕ (un workspace se apaga desde su Panel; matarlo lo relanza el supervisor)
8
+ let wsByPort = {}; try { const ws = (await (await fetch('/api/workspaces')).json()).workspaces || []; for (const w of ws) wsByPort[w.port] = w; } catch (e) {}
9
+ const list = (r.ports || []).map(p => { const own = /\b(web|hub)\.syn\b/.test(p.command || '') || (p.port === 8080 && /synsema/.test(p.name || '')); return Object.assign({}, p, { lampson: own, wsName: own && wsByPort[p.port] ? wsByPort[p.port].name : (own && p.port === 8080 ? 'hub' : '') }); });
7
10
  $('#portCount').textContent = list.length || ''; autoSec('ports', list.length > 0);
8
11
  if (!list.length) { box.innerHTML = '<div class="p none">ninguno</div>'; return; }
9
12
  for (const p of list) {
10
- const d = document.createElement('div'); d.className = 'p'; d.style.cursor = 'default'; d.title = `pid ${p.pid}\n${p.command || '(línea de comando no disponible)'}`;
13
+ const d = document.createElement('div'); d.className = 'p' + (p.lampson ? ' own' : ''); d.style.cursor = 'default'; d.title = `pid ${p.pid}\n${p.command || '(línea de comando no disponible)'}`;
11
14
  const what = p.command ? p.command.replace(/^"?[A-Za-z]:\\[^"]*\\([^"\\]+)"?/, '$1') : p.name;
15
+ if (p.lampson) { d.innerHTML = `<span class="port own" title="puerto ocupado por lampson">:${p.port}</span><span class="cm">lampson · ${esc(p.wsName ? (p.port === 8080 ? 'hub' : 'workspace ' + p.wsName) : 'proceso')}</span>`; box.appendChild(d); continue; }
12
16
  d.innerHTML = `<a class="port" href="http://127.0.0.1:${p.port}" target="_blank" rel="noopener" title="abrir http://127.0.0.1:${p.port} en otra pestaña">:${p.port}</a><span class="cm">${esc(what)}</span><button title="matar este proceso (pid ${p.pid}; no lo gestiona lampson)">✕</button>`;
13
17
  const kb = d.querySelector('button'); kb.onclick = () => { const holder = document.createElement('span'); holder.className = 'del'; kb.replaceWith(holder); inlineConfirm(holder, `¿matar pid ${p.pid}?`, async () => { await api(BASE + '/api/ports/kill', { pid: p.pid }); loadPorts(); }); setTimeout(() => { if (!holder.classList.contains('ask')) loadPorts(); }, 6100); };
14
18
  box.appendChild(d);
@@ -68,7 +68,7 @@ function openSched(selectId) {
68
68
  if (t === SCHED_NEW) { schedFormWire(box, err); return; }
69
69
  box.querySelector('[data-run]').onclick = async () => { const r = await api(BASE + '/api/schedules/run', { id: t.id }); add('meta', '⏰ ' + esc(r.data.result || r.data.error || '')); setTimeout(loadSched, 1500); setTimeout(() => Panel.is('schedules') && Panel.detail(), 2500); };
70
70
  box.querySelector('[data-toggle]').onclick = async () => { const r = await api(BASE + '/api/schedules/toggle', { id: t.id, enabled: !t.enabled }); if (!r.ok) { err(r.data.error || 'error'); return; } loadSched(); };
71
- box.querySelectorAll('[data-session]').forEach(a => a.onclick = (e) => { e.preventDefault(); Panel.close(); open(a.dataset.session); });
71
+ box.querySelectorAll('[data-session]').forEach(a => a.onclick = (e) => { e.preventDefault(); Panel.close(); openSession(a.dataset.session); });
72
72
  const del = box.querySelector('.dfoot .del');
73
73
  del.onclick = () => inlineConfirm(del, `¿quitar ${t.name}?`, async () => { await api(BASE + '/api/schedules/remove', { id: t.id }); loadSched(); });
74
74
  const live = box.querySelector('[data-live]'); if (live) live.scrollTop = live.scrollHeight;
@@ -9,7 +9,7 @@ async function loadSessions() {
9
9
  if (!r.sessions.length) { box.innerHTML = '<div class="s none">todavía ninguna</div>'; return; }
10
10
  const shown = r.sessions.slice(0, 4);
11
11
  for (const s of shown) {
12
- const d = document.createElement('div'); d.className = 's' + (s.id === session ? ' active' : ''); d.innerHTML = '<span class="id">' + esc(s.id.slice(-6)) + '</span><span class="title">' + esc(s.title || '') + '</span><span class="del tr" title="traza: qué hizo el agente en esta sesión (pasos, tools, tiempos, tokens)">≡</span><span class="del" title="borrar esta sesión">✕</span>'; d.title = s.id; d.onclick = () => open(s.id);
12
+ const d = document.createElement('div'); d.className = 's' + (s.id === session ? ' active' : ''); d.innerHTML = '<span class="id">' + esc(s.id.slice(-6)) + '</span><span class="title">' + esc(s.title || '') + '</span><span class="del tr" title="traza: qué hizo el agente en esta sesión (pasos, tools, tiempos, tokens)">≡</span><span class="del" title="borrar esta sesión">✕</span>'; d.title = s.id; d.onclick = () => openSession(s.id);
13
13
  d.querySelector('.tr').onclick = (ev) => { ev.stopPropagation(); openTrace(s.id); };
14
14
  d.querySelector('.del:not(.tr)').onclick = (ev) => { ev.stopPropagation(); inlineConfirm(d.querySelector('.del:not(.tr)'), '¿borrar?', () => deleteSession(s.id)); };
15
15
  box.appendChild(d);
@@ -35,18 +35,18 @@ function openSessionsPanel() {
35
35
  + `<div class="dfoot"><span>.lampson/sessions/${esc(s.id)}.json</span><span class="del">borrar</span></div>`;
36
36
  },
37
37
  wire: (s, box) => {
38
- box.querySelector('[data-act="open"]').onclick = () => { Panel.close(); open(s.id); };
38
+ box.querySelector('[data-act="open"]').onclick = () => { Panel.close(); openSession(s.id); };
39
39
  box.querySelector('[data-act="trace"]').onclick = () => { Panel.close(); openTrace(s.id); };
40
40
  const del = box.querySelector('.dfoot .del');
41
41
  del.onclick = () => inlineConfirm(del, `¿borrar ${s.id}?`, async () => { await deleteSession(s.id); Panel.refresh(); });
42
42
  },
43
- onPick: (s) => open(s.id),
43
+ onPick: (s) => openSession(s.id),
44
44
  count: (rows, q) => q ? `${rows.length} coincidencia${rows.length === 1 ? '' : 's'}` : `${rows.length} sesión${rows.length === 1 ? '' : 'es'}`,
45
45
  emptyHtml: 'todavía no hay sesiones'
46
46
  }
47
47
  });
48
48
  }
49
- async function open(id) {
49
+ async function openSession(id) {
50
50
  session = id; localStorage.setItem('lampson.session', id); log.innerHTML = ''; log.classList.remove('hero'); showPane('log'); loadTodo();
51
51
  const r = await fetch(BASE + '/api/sessions/' + id); if (!r.ok) { session = ''; empty(); return loadSessions(); }
52
52
  const d = await r.json();
@@ -1,6 +1,9 @@
1
1
  // workspaces.js — el selector de workspace de la cabecera (dentro de un workspace) y el Panel de workspaces.
2
2
  // La API de workspaces es del HUB (raíz, sin BASE): /api/workspaces… Sin hub (standalone) el selector se oculta.
3
3
  let wsList = [], wsIdle = 4;
4
+ // abierto por el puerto de un proceso (:808N) las URLs de otros workspaces tienen que ir al hub (:8080), no a este proceso
5
+ const HUB_BASE = (location.port && location.port !== '8080') ? location.protocol + '//' + location.hostname + ':8080' : '';
6
+ function wsUrl(w) { return HUB_BASE + w.url; }
4
7
  async function fetchWorkspaces() {
5
8
  try { const r = await (await fetch('/api/workspaces')).json(); wsList = r.workspaces || []; wsIdle = r.idle_hours; return true; } catch (e) { wsList = []; return false; }
6
9
  }
@@ -31,12 +34,12 @@ function openWorkspaces(selectSlug) {
31
34
  wire: (w, box) => {
32
35
  const err = (m) => { const e = box.querySelector('.derr'); if (e) e.textContent = m || ''; };
33
36
  if (w === WS_NEW) { wsNewWire(box, err); return; }
34
- box.querySelector('[data-open]').onclick = () => { if (w.slug !== WS_SLUG) location.href = w.url; else Panel.close(); };
37
+ box.querySelector('[data-open]').onclick = () => { if (w.slug !== WS_SLUG) location.href = wsUrl(w); else Panel.close(); };
35
38
  const st = box.querySelector('[data-start]'); if (st) st.onclick = async () => { err('arrancando…'); const r = await api('/api/workspaces/start', { slug: w.slug }); err(r.data.ok ? '' : 'no respondió a tiempo'); Panel.refresh(); };
36
39
  const sp = box.querySelector('[data-stop]'); if (sp) sp.onclick = async () => { await api('/api/workspaces/stop', { slug: w.slug }); setTimeout(() => Panel.refresh(), 800); };
37
40
  box.querySelector('[name="policy"]').onchange = async (ev) => { const r = await api('/api/workspaces/policy', { slug: w.slug, policy: ev.target.value }); if (!r.ok) err(r.data.error || 'error'); };
38
41
  const del = box.querySelector('.dfoot .del');
39
- del.onclick = () => inlineConfirm(del, `¿quitar ${w.name} del registro?`, async () => { const r = await api('/api/workspaces/remove', { slug: w.slug }); if (!r.ok) { err(r.data.error || 'error'); return; } if (w.slug === WS_SLUG) location.href = '/'; else Panel.refresh(); });
42
+ del.onclick = () => inlineConfirm(del, `¿quitar ${w.name} del registro?`, async () => { const r = await api('/api/workspaces/remove', { slug: w.slug }); if (!r.ok) { err(r.data.error || 'error'); return; } if (w.slug === WS_SLUG) location.href = HUB_BASE + '/'; else Panel.refresh(); });
40
43
  }
41
44
  }
42
45
  });
@@ -44,40 +47,52 @@ function openWorkspaces(selectSlug) {
44
47
  function wsNewForm() {
45
48
  return `<div class="dhead"><span class="nm serif">Nuevo workspace</span></div><div class="dform">
46
49
  <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;align-items:center"><button class="primary" data-pick>Elegir carpeta…</button><span class="ds" data-pickhint style="margin:0">abre el diálogo de carpetas 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>`;
50
+ <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>
51
+ <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>
52
+ <div data-browser class="wsbrowser" style="display:none">
53
+ <div class="bhead"><span class="ds" data-here></span><input name="path" spellcheck="false" autocomplete="off" placeholder="o pegá una ruta y Enter"></div>
54
+ <div class="bitems" data-dirs></div>
55
+ <div class="bfoot"><button data-use>Usar esta carpeta</button></div>
56
+ </div>
57
+ <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>
58
+ <div class="derr" data-err2></div></div>`;
51
59
  }
52
- function wsNewWire(box, err) {
60
+ function wsNewWire(box, err0) {
61
+ const chosenBox = box.querySelector('[data-chosen]'), goRow = box.querySelector('[data-go-row]'), browser = box.querySelector('[data-browser]');
53
62
  const f = n => box.querySelector(`[name="${n}"]`);
54
- const browser = box.querySelector('[data-browser]');
63
+ 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 || ''; };
64
+ let chosen = '';
65
+ 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(''); } };
66
+ let here = '';
55
67
  const showDir = async (p) => {
68
+ err('');
56
69
  const r = await (await fetch('/api/workspaces/browse?path=' + encodeURIComponent(p || ''))).json();
57
70
  browser.style.display = '';
58
- box.querySelector('[data-here]').textContent = r.path + (r.error ? ' — ' + r.error : '');
59
- f('path').value = r.path || '';
71
+ here = r.path || ''; box.querySelector('[data-here]').textContent = here + (r.error ? ' — ' + r.error : ''); f('path').value = '';
60
72
  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); }
73
+ 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);
74
+ 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
75
  };
64
- box.querySelector('[data-browse]').onclick = () => showDir(f('path').value.trim());
76
+ box.querySelector('[data-browse]').onclick = (e) => { e.preventDefault(); showDir(chosen || ''); };
77
+ box.querySelector('[data-use]').onclick = () => choose(here);
78
+ f('path').addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); const v = f('path').value.trim(); if (v) showDir(v); } });
79
+ box.querySelector('[data-clear]').onclick = () => choose('');
65
80
  box.querySelector('[data-pick]').onclick = async () => {
66
- err('esperando el diálogo de carpetas… (si no aparece, mirá la barra de tareas o usá «explorar en el servidor»)');
81
+ err('esperando el diálogo de carpetas… (si no lo ves, mirá la barra de tareas)');
67
82
  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;
83
+ if (!r.ok) { err('esta página no es el hub: abrí ' + location.protocol + '//' + location.hostname + ':8080/'); return; }
84
+ if (!r.data.native) { err('no hay diálogo en este equipo (VPS): explorá desde acá'); showDir(''); return; }
85
+ err(''); if (r.data.path) choose(r.data.path);
70
86
  };
71
87
  box.querySelector('[data-go]').onclick = async () => {
72
- const path = f('path').value.trim(); if (!path) { err('elegí una carpeta'); return; }
88
+ if (!chosen) { err('elegí una carpeta'); return; }
73
89
  err('creando y arrancando… (el hub se reinicia un instante)');
74
- const r = await api('/api/workspaces', { path });
90
+ const r = await api('/api/workspaces', { path: chosen });
75
91
  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
77
- 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'); };
92
+ // el hub se regenera con la ruta nueva y se reinicia: esperar a que vuelva y navegar
93
+ const url = HUB_BASE + r.data.url; let tries = 0;
94
+ 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
95
  setTimeout(poll, 1200);
80
96
  };
81
- f('path').focus();
82
97
  }
83
98
  if ($('#wsPill')) { $('#wsPill').onclick = () => openWorkspaces(); paintWorkspacePill(); }
package/web.syn CHANGED
@@ -138,11 +138,29 @@ serve on 8080
138
138
  auth with check_client
139
139
  static "./public"
140
140
 
141
- -- standalone (sin hub: docker, `synsema serve web.syn`): la UI vive igual bajo /w/<slug> (BASE en js/core.js)
142
- route "GET /"
143
- give redirect("/w/" + (when env("LAMPSON_WS", "") == "" then "local" otherwise env("LAMPSON_WS", "")))
141
+ -- la UI vive bajo /w/<slug> (BASE en js/core.js); si alguien entra por el puerto del proceso, es lo mismo
142
+ -- Acceso DIRECTO por el puerto del proceso (sin X-Forwarded-Host, que solo agrega el hub al proxyear): la UI se
143
+ -- usa siempre por el hub redirigir a http://127.0.0.1:8080/w/<slug>. Standalone sin hub (docker) no hay
144
+ -- LAMPSON_WS y se sirve acá mismo.
144
145
  route "GET /w/:slug"
146
+ let h be headers of request
147
+ when env("LAMPSON_WS", "") != "" and not contains(h, "x-forwarded-host")
148
+ give redirect("http://127.0.0.1:8080/w/" + params.slug)
145
149
  give html(read_file("public/index.html"))
150
+ route "GET /"
151
+ when env("LAMPSON_WS", "") == ""
152
+ give redirect("/w/local")
153
+ give redirect("http://127.0.0.1:8080/w/" + env("LAMPSON_WS", ""))
154
+ route "GET /api/hub"
155
+ proxy to "http://127.0.0.1:8080"
156
+ route "GET /api/workspaces"
157
+ proxy to "http://127.0.0.1:8080"
158
+ route "GET /api/workspaces/*path"
159
+ proxy to "http://127.0.0.1:8080"
160
+ route "POST /api/workspaces"
161
+ proxy to "http://127.0.0.1:8080"
162
+ route "POST /api/workspaces/*path"
163
+ proxy to "http://127.0.0.1:8080"
146
164
 
147
165
  -- salud para el hub/supervisor y la terminal (último uso = chat, terminal, eventos o abrir la UI)
148
166
  route "GET /w/:slug/api/health"