lampson 0.2.4 → 0.2.6

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.
@@ -380,6 +380,10 @@ export task start_ws(w)
380
380
  require file.read("web.syn")
381
381
  require file.read("chat.syn")
382
382
  require file.read(".env")
383
+ -- encender levanta la pausa manual
384
+ when contains(w, "paused") and w["paused"] == true
385
+ set w["paused"] to false
386
+ put(w)
383
387
  let d be prepare(w)
384
388
  let h be home()
385
389
  let sep be when is_win() then "\\" otherwise "/"
@@ -396,13 +400,21 @@ export task health(w)
396
400
  require net("127.0.0.1")
397
401
  require time
398
402
  try
399
- let r be http_get("http://127.0.0.1:" + text(w["port"]) + "/w/" + w["slug"] + "/api/health", {"timeout": 2})
403
+ let r be http("GET", "http://127.0.0.1:" + text(w["port"]) + "/w/" + w["slug"] + "/api/health", {}, {}, "", 1.5)
400
404
  when r["ok"]
401
405
  give json_decode(body of r)
402
406
  give nothing
403
407
  recover err
404
408
  give nothing
405
409
 
410
+ -- salud de TODOS a la vez. En Windows con el firewall en modo stealth, conectar a un puerto cerrado de 127.0.0.1
411
+ -- no se rechaza al instante: el SYN queda sin respuesta ~2 s. Secuencial era 2 s × workspaces (6 s con tres
412
+ -- apagados); en paralelo es 2 s en total, y el resultado se comparte en el blackboard para que la lista sea inmediata.
413
+ export task health_all(list)
414
+ require net("127.0.0.1")
415
+ require time
416
+ give parallel_map(health, list, 8)
417
+
406
418
  -- esperar hasta que responda (arranque)
407
419
  export task wait_ready(w, secs)
408
420
  require net("127.0.0.1")
@@ -414,6 +426,25 @@ export task wait_ready(w, secs)
414
426
  sleep(0.4)
415
427
  give false
416
428
 
429
+ -- «■ apagar»: marca la pausa manual en el registro y para el proceso; el supervisor no lo vuelve a levantar
430
+ export task pause_ws(w)
431
+ require exec
432
+ require time
433
+ require env("LAMPSON_*")
434
+ require env("OS")
435
+ require net("127.0.0.1")
436
+ require file(".lampson")
437
+ require file(".lampson/*")
438
+ set w["paused"] to true
439
+ put(w)
440
+ stop_ws(w)
441
+ -- esperar a que deje de responder (matar tarda unos segundos) y dejar la caché al día
442
+ let deadline be now() + 6
443
+ while now() < deadline and health(w) != nothing
444
+ sleep(0.4)
445
+ summary()
446
+ give true
447
+
417
448
  export task stop_ws(w)
418
449
  require exec
419
450
  require time
@@ -490,6 +521,11 @@ export task should_live(w, hc)
490
521
  require file.read(".lampson/*")
491
522
  require env("LAMPSON_*")
492
523
  require time
524
+ -- apagado a mano («■ apagar» en el hub): gana sobre la política y sobre las tareas programadas hasta que
525
+ -- alguien lo encienda (botón ▶, o `lampson` en esa carpeta). Antes el tick lo revivía a los 15 s y el botón
526
+ -- parecía no hacer nada.
527
+ when contains(w, "paused") and w["paused"] == true
528
+ give false
493
529
  let pol be when contains(w, "policy") then w["policy"] otherwise "auto"
494
530
  when pol == "always"
495
531
  give true
@@ -528,8 +564,11 @@ export task tick()
528
564
  restart_hub_later()
529
565
  let started be []
530
566
  let stopped be []
531
- each w in all()
532
- let hc be health(w)
567
+ let list be all()
568
+ let hcs be health_all(list)
569
+ each e in enumerate(list)
570
+ let w be e["item"]
571
+ let hc be hcs[e["index"]]
533
572
  let want be should_live(w, hc)
534
573
  when want and hc == nothing
535
574
  start_ws(w)
@@ -537,6 +576,7 @@ export task tick()
537
576
  otherwise when not want and hc != nothing
538
577
  stop_ws(w)
539
578
  set stopped to append(stopped, w["slug"])
579
+ summary()
540
580
  give {"started": started, "stopped": stopped}
541
581
 
542
582
  -- resumen para la UI del hub
@@ -549,11 +589,35 @@ export task summary()
549
589
  require file(".lampson")
550
590
  require file(".lampson/*")
551
591
  let out be []
552
- each w in all()
553
- let hc be health(w)
554
- set out to append(out, {"slug": w["slug"], "name": w["name"], "path": w["path"], "port": w["port"], "policy": when contains(w, "policy") then w["policy"] otherwise "auto", "created": w["created"], "last_used": when hc != nothing and contains(hc, "last_used") and hc["last_used"] != nothing then hc["last_used"] otherwise w["last_used"], "alive": hc != nothing, "schedules_on": when hc != nothing and contains(hc, "schedules_on") then hc["schedules_on"] otherwise schedules_on_disk(w), "url": "/w/" + w["slug"]})
592
+ let list be all()
593
+ let hcs be health_all(list)
594
+ each e in enumerate(list)
595
+ let w be e["item"]
596
+ let hc be hcs[e["index"]]
597
+ set out to append(out, {"slug": w["slug"], "name": w["name"], "path": w["path"], "port": w["port"], "policy": when contains(w, "policy") then w["policy"] otherwise "auto", "created": w["created"], "last_used": when hc != nothing and contains(hc, "last_used") and hc["last_used"] != nothing then hc["last_used"] otherwise w["last_used"], "alive": hc != nothing, "paused": contains(w, "paused") and w["paused"] == true, "schedules_on": when hc != nothing and contains(hc, "schedules_on") then hc["schedules_on"] otherwise schedules_on_disk(w), "url": "/w/" + w["slug"]})
598
+ share out as "hub:summary"
599
+ share now() as "hub:summary_at"
555
600
  give out
556
601
 
602
+ -- la lista que ve la UI: la última calculada (tick cada 15 s, y cada start/stop la refresca) si tiene menos de
603
+ -- max_age segundos; si no, se calcula ahora. GET /api/workspaces?fresh=1 fuerza el cálculo.
604
+ export task summary_cached(max_age)
605
+ require exec
606
+ require time
607
+ require env("LAMPSON_*")
608
+ require env("OS")
609
+ require net("127.0.0.1")
610
+ require file(".lampson")
611
+ require file(".lampson/*")
612
+ try
613
+ observe "hub:summary_at" as at
614
+ observe "hub:summary" as cached
615
+ when at != nothing and cached != nothing and now() - at < max_age
616
+ give cached
617
+ recover err
618
+ give summary()
619
+ give summary()
620
+
557
621
  -- ---------- explorador de carpetas ----------
558
622
  -- diálogo nativo del SO (PC con escritorio). Devuelve la ruta elegida, "" si canceló, nothing si no hay diálogo.
559
623
  export task pick_folder()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lampson",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
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": {
@@ -54,3 +54,10 @@
54
54
  .empty .try { cursor:pointer; color:var(--accent); }
55
55
  .empty .try:hover { text-decoration:underline; text-underline-offset:.18em; }
56
56
  @media (max-width:1400px) { .empty .grid { grid-template-columns:repeat(2, minmax(0,1fr)); } }
57
+
58
+ /* workspace apagado (events.js): un aviso que se vea, no una línea más del log */
59
+ .msg.wsoff{border:1px solid var(--rubric);border-left-width:4px;background:var(--paper-2);padding:12px 16px;border-radius:var(--r);font:400 14.5px/1.55 var(--serif);color:var(--ink);opacity:1}
60
+ .msg.wsoff a{font-family:var(--mono);font-size:13px;margin-left:.2em;color:var(--accent);text-decoration:none}
61
+ .msg.wsoff a:hover{text-decoration:underline}
62
+ .msg.wsoff .wsst{color:var(--ink-3);font-family:var(--mono);font-size:12px;margin-left:.6em}
63
+ .msg.wson{border-left:4px solid var(--ok, #8fc28f);padding-left:12px}
@@ -6,6 +6,10 @@ body.hub #stage { display:flex; align-items:center; padding:20px 40px; }
6
6
  .card.ws { display:block; text-decoration:none; color:inherit; cursor:pointer; transition:border-color .12s; }
7
7
  .card.ws:hover { border-color:var(--ink-3); }
8
8
  .card.ws.on { border-color:var(--accent); }
9
+ /* estado a un vistazo: verde vivo · rojo apagado · ámbar mientras arranca */
10
+ .card.ws.on h3 { color:var(--ok); }
11
+ .card.ws.off h3 { color:var(--rubric); }
12
+ .card.ws.busy h3 { color:var(--amber); }
9
13
  .card.ws.new { border-style:dashed; }
10
14
  .card.ws .nm { font:600 16px/1.3 var(--serif); color:var(--ink); margin:0 0 4px; }
11
15
  .card.ws .pth { font:400 11px/1.4 var(--mono); color:var(--ink-3); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin:0 0 6px; }
@@ -58,6 +58,10 @@
58
58
  .panel .li:hover { background:var(--paper-2); }
59
59
  .panel .li.sel { background:var(--paper-2); box-shadow:inset 2px 0 0 var(--accent); }
60
60
  .panel .li .dot { color:var(--ink-3); margin-top:1px; }
61
+ .panel .li .dot.on { color:var(--ok); }
62
+ .panel .li .dot.off { color:var(--rubric); }
63
+ .panel .st.on { color:var(--ok); font-weight:600; }
64
+ .panel .st.off { color:var(--rubric); font-weight:600; }
61
65
  .panel .li.on .dot { color:var(--accent); }
62
66
  .panel .li.bad .dot { color:var(--rubric); }
63
67
  .panel .li.new .nm { color:var(--accent); font-weight:400; }
@@ -19,7 +19,7 @@
19
19
  --ink:#e9e8df; --ink-2:#9b9c90; --ink-3:#848678;
20
20
  --rule:#2b2c27; --rule-2:#3c3d36;
21
21
  --accent:#8fb4dc; --accent-bg:#1b2836;
22
- --rubric:#d98670; --str:#a8bd84; --amber:#d9b26f;
22
+ --rubric:#d98670; --str:#a8bd84; --amber:#d9b26f; --ok:#8fc28f;
23
23
  --sel:#33465b; --term-blue:#4f86d1;
24
24
  --serif:"Plex Serif",Georgia,"Times New Roman",serif;
25
25
  --mono:"Plex Mono",ui-monospace,SFMono-Regular,Consolas,"Liberation Mono",monospace;
@@ -30,7 +30,7 @@
30
30
  --ink:#1a1b18; --ink-2:#56584f; --ink-3:#6a6c61;
31
31
  --rule:#e4e3da; --rule-2:#cfcec2;
32
32
  --accent:#21578c; --accent-bg:#e9eef4;
33
- --rubric:#99372a; --str:#4a6b3f; --amber:#8a6a1f;
33
+ --rubric:#99372a; --str:#4a6b3f; --amber:#8a6a1f; --ok:#3d7a3d;
34
34
  --sel:#cfe0f0; --term-blue:#21578c;
35
35
  }
36
36
 
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <rect width="64" height="64" rx="10" fill="#131412"/>
3
+ <circle cx="32" cy="27" r="11" fill="#d9c26a"/>
4
+ <g stroke="#d9c26a" stroke-width="3" stroke-linecap="round">
5
+ <line x1="32" y1="6" x2="32" y2="10"/>
6
+ <line x1="14" y1="13" x2="17" y2="16"/>
7
+ <line x1="50" y1="13" x2="47" y2="16"/>
8
+ <line x1="9" y1="27" x2="13" y2="27"/>
9
+ <line x1="55" y1="27" x2="51" y2="27"/>
10
+ </g>
11
+ <rect x="26" y="40" width="12" height="7" rx="1.5" fill="#e9e8df"/>
12
+ <rect x="27.5" y="49" width="9" height="3" rx="1.5" fill="#9b9c90"/>
13
+ </svg>
package/public/hub.html CHANGED
@@ -2,6 +2,7 @@
2
2
  <html lang="es">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml">
5
6
  <title>Lampson · workspaces</title>
6
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
8
  <link rel="stylesheet" href="/css/tokens.css">
package/public/index.html CHANGED
@@ -2,6 +2,7 @@
2
2
  <html lang="es">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml">
5
6
  <title>Lampson</title>
6
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
8
  <!-- estilos: tokens (fuentes, paleta, base) → layout (grid, cabecera, paneles, chat, composer) → sidebar → chat → panel (el modal) -->
@@ -2,10 +2,32 @@
2
2
  // Los supervisores de procesos publican cada línea y cambio de estado; los subagentes, inicio/fin/pasos; las
3
3
  // tareas programadas y aprobaciones, sus cambios. La UI reacciona a eso (con un pequeño debounce) en vez de
4
4
  // recargar por timer; los timers quedan como respaldo lento por si el stream se cae (reconexión con backoff).
5
- let events = null, evBackoff = 1000;
5
+ let events = null, evBackoff = 1000, evOffline = false, evEverOpen = false;
6
+ // El hub responde aunque el workspace esté apagado: por ahí se pregunta "¿sigue vivo?" sin llenar la consola de 502.
7
+ const EV_HUB = (location.port && location.port !== '8080') ? location.protocol + '//' + location.hostname + ':8080' : '';
8
+ async function wsAlive() {
9
+ if (!WS_SLUG) return true;
10
+ try { const r = await fetch(EV_HUB + '/api/workspaces'); const d = await r.json(); const w = (d.workspaces || []).find(x => x.slug === WS_SLUG); return w ? { alive: w.alive, paused: w.paused } : { alive: true }; } catch (e) { return { alive: true }; }
11
+ }
12
+ function goOffline(state) {
13
+ if (evOffline) return;
14
+ evOffline = true;
15
+ // apagado a mano (desde el hub o desde este mismo panel): volver al inicio; el banner queda para caídas
16
+ if (state.paused) { location.href = EV_HUB + '/'; return; }
17
+ document.title = '○ ' + document.title.replace(/^○ /, '');
18
+ const why = state.paused ? 'Lo apagaste desde el hub.' : 'El proceso no responde.';
19
+ const m = add('meta wsoff', `○ <b>Este workspace está apagado.</b> ${why} <a href="#" class="wson">▶ encender</a> · <a href="${EV_HUB}/">ir al hub</a> <span class="wsst"></span>`);
20
+ m.querySelector('.wson').onclick = async (e) => { e.preventDefault(); m.querySelector('.wsst').textContent = 'encendiendo…'; try { await fetch(EV_HUB + '/api/workspaces/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug: WS_SLUG }) }); } catch (err) {} };
21
+ const poll = async () => {
22
+ const s = await wsAlive();
23
+ if (s.alive) { evOffline = false; document.title = document.title.replace(/^○ /, ''); add('meta wson', '● workspace encendido · reconectado'); evBackoff = 1000; connectEvents(); if (typeof loadProcs === 'function') loadProcs(); if (typeof loadSessions === 'function') loadSessions(); }
24
+ else setTimeout(poll, 3000);
25
+ };
26
+ setTimeout(poll, 3000);
27
+ }
6
28
  function connectEvents() {
7
29
  try { events = new EventSource(BASE + '/api/events'); } catch (e) { events = null; return; }
8
- events.onopen = () => { evBackoff = 1000; };
30
+ events.onopen = () => { evBackoff = 1000; evEverOpen = true; };
9
31
  events.addEventListener('event', e => {
10
32
  let ev; try { ev = JSON.parse(e.data); } catch { return; }
11
33
  const t = ev.topic || '';
@@ -26,6 +48,12 @@ function connectEvents() {
26
48
  if (id && agentOpen === id && agentPaint) debounce('agentview', agentPaint, 250);
27
49
  }
28
50
  });
29
- events.onerror = () => { events.close(); events = null; setTimeout(connectEvents, evBackoff); evBackoff = Math.min(evBackoff * 2, 30000); };
51
+ events.onerror = async () => {
52
+ events.close(); events = null;
53
+ // ¿se cayó el stream o se apagó el workspace? Si el hub dice que no está vivo, no insistimos contra el proxy.
54
+ const s = await wsAlive();
55
+ if (!s.alive) { goOffline(s); return; }
56
+ setTimeout(connectEvents, evBackoff); evBackoff = Math.min(evBackoff * 2, 30000);
57
+ };
30
58
  }
31
59
  connectEvents();
package/public/js/hub.js CHANGED
@@ -24,8 +24,16 @@ 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
25
  grid.innerHTML = '';
26
26
  for (const w of (r.workspaces || [])) {
27
- const c = document.createElement('a'); c.className = 'card ws' + (w.alive ? ' on' : ''); c.href = w.url;
27
+ const c = document.createElement('a'); c.className = 'card ws' + (w.alive ? ' on' : ' off'); c.href = w.url;
28
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>`;
29
+ // entrar = usarlo: si está apagado se enciende acá mismo (con aviso en la tarjeta) y después se abre. La ruta
30
+ // /w/<slug> del hub también lo enciende sola, así que un link directo funciona igual; esto solo da feedback.
31
+ if (!w.alive) c.onclick = async (e) => {
32
+ e.preventDefault(); if (c.dataset.busy) return; c.dataset.busy = '1';
33
+ const h = c.querySelector('h3'); h.textContent = '⟳ encendiendo…'; c.classList.add('busy');
34
+ try { const r = await api('/api/workspaces/start', { slug: w.slug }); if (!r.ok || !r.data.ok) { h.textContent = '○ no arrancó · mirá .lampson/ws/' + w.slug + '/.lampson/web.log'; c.classList.remove('busy'); delete c.dataset.busy; return; } } catch (err) {}
35
+ location.href = w.url;
36
+ };
29
37
  grid.appendChild(c);
30
38
  }
31
39
  const add = document.createElement('a'); add.className = 'card ws new'; add.href = '#'; add.innerHTML = '<h3>+</h3><p class="nm">nuevo workspace</p><p class="pth">elegí una carpeta de tu disco</p>';
@@ -71,6 +71,8 @@ const Panel = (() => {
71
71
  if (!cur || cur.layout !== 'browse') return;
72
72
  const spec = cur, q = query(), my = ++seq;
73
73
  const keep = pendingKey != null ? pendingKey : (rows[sel] && spec.browse.key ? spec.browse.key(rows[sel]) : null); pendingKey = null;
74
+ // mientras carga, decirlo (la lista de workspaces puede tardar si el hub tiene que sondear procesos)
75
+ if (!rows.length) { const it = box().querySelector('.bitems'); if (it) it.innerHTML = '<div class="bempty">cargando…</div>'; }
74
76
  let list; try { list = await spec.browse.load(q); } catch (e) { list = []; }
75
77
  if (cur !== spec || my !== seq) return;
76
78
  rows = list || [];
@@ -4,8 +4,9 @@ let wsList = [], wsIdle = 4;
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
5
  const HUB_BASE = (location.port && location.port !== '8080') ? location.protocol + '//' + location.hostname + ':8080' : '';
6
6
  function wsUrl(w) { return HUB_BASE + w.url; }
7
- async function fetchWorkspaces() {
8
- try { const r = await (await fetch('/api/workspaces')).json(); wsList = r.workspaces || []; wsIdle = r.idle_hours; return true; } catch (e) { wsList = []; return false; }
7
+ // fresh=true: saltear la caché del hub (después de encender/apagar)
8
+ async function fetchWorkspaces(fresh) {
9
+ try { const r = await (await fetch('/api/workspaces' + (fresh ? '?fresh=1' : ''))).json(); wsList = r.workspaces || []; wsIdle = r.idle_hours; return true; } catch (e) { wsList = []; return false; }
9
10
  }
10
11
  async function paintWorkspacePill() {
11
12
  const pill = $('#wsPill'); if (!pill) return;
@@ -22,12 +23,12 @@ function openWorkspaces(selectSlug) {
22
23
  browse: {
23
24
  placeholder: 'buscar workspace…', listWidth: '300px', key: w => w.slug,
24
25
  load: async (q) => { await fetchWorkspaces(); const list = wsList.filter(w => !q || (w.name + ' ' + w.path).toLowerCase().includes(q.toLowerCase())); return q ? list : [...list, WS_NEW]; },
25
- render: (w) => w === WS_NEW ? `<span class="dot">+</span><div><div class="nm" style="color:var(--accent);font-weight:400">nuevo workspace…</div><div class="meta">elegí una carpeta</div></div>` : `<span class="dot">${w.alive ? '●' : '○'}</span><div><div class="nm">${esc(w.name)}${w.slug === WS_SLUG ? ' <span class="meta">(este)</span>' : ''}</div><div class="meta">${esc(w.path)}</div></div>`,
26
+ render: (w) => w === WS_NEW ? `<span class="dot">+</span><div><div class="nm" style="color:var(--accent);font-weight:400">nuevo workspace…</div><div class="meta">elegí una carpeta</div></div>` : `<span class="dot ${w.alive ? 'on' : 'off'}">${w.alive ? '●' : '○'}</span><div><div class="nm">${esc(w.name)}${w.slug === WS_SLUG ? ' <span class="meta">(este)</span>' : ''}</div><div class="meta">${esc(w.path)}</div></div>`,
26
27
  count: (rows) => { const n = rows.filter(r => r !== WS_NEW).length, a = rows.filter(r => r !== WS_NEW && r.alive).length; return `${n} workspace${n === 1 ? '' : 's'} · ${a} vivo${a === 1 ? '' : 's'}`; },
27
28
  emptyHtml: 'nada coincide', emptyDetail: 'Todavía no hay workspaces. Creá uno con «nuevo workspace…» o abrí <code>lampson</code> en la carpeta de un proyecto.',
28
- detail: (w) => w === WS_NEW ? wsNewForm() : `<div class="dhead"><span class="nm">${esc(w.name)}</span><span class="meta">${w.alive ? '● vivo' : '○ apagado'} · ${esc(w.policy)}</span></div>
29
+ detail: (w) => w === WS_NEW ? wsNewForm() : `<div class="dhead"><span class="nm">${esc(w.name)}</span><span class="meta"><b class="st ${w.alive ? 'on' : 'off'}">${w.alive ? '● vivo' : (w.paused ? '○ apagado a mano' : '○ apagado')}</b> · ${esc(w.policy)}</span></div>
29
30
  <div class="dcap">${esc(w.path)}</div>
30
- <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>
31
+ <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'} ${w.alive ? '(lo mantienen vivo)' : (w.paused ? '— en pausa hasta que lo enciendas' : '')}. ` : ''}${!w.alive && w.paused ? 'Lo apagaste a mano: no se enciende solo hasta ▶ encender o abrir <code>lampson</code> en la carpeta. ' : ''}Último uso: ${esc(fmtWhen(w.last_used))}.</div>
31
32
  <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>
32
33
  <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>
33
34
  <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>`,
@@ -35,8 +36,11 @@ function openWorkspaces(selectSlug) {
35
36
  const err = (m) => { const e = box.querySelector('.derr'); if (e) e.textContent = m || ''; };
36
37
  if (w === WS_NEW) { wsNewWire(box, err); return; }
37
38
  box.querySelector('[data-open]').onclick = () => { if (w.slug !== WS_SLUG) location.href = wsUrl(w); else Panel.close(); };
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(); };
39
- const sp = box.querySelector('[data-stop]'); if (sp) sp.onclick = async () => { await api('/api/workspaces/stop', { slug: w.slug }); setTimeout(() => Panel.refresh(), 800); };
39
+ // esperar a que el hub vea el estado nuevo (matar/arrancar un proceso tarda unos segundos): sin esto el refresco
40
+ // a los 0,8 s todavía mostraba «vivo» y el botón parecía no hacer nada
41
+ const waitFor = async (alive, secs) => { const t0 = Date.now(); while (Date.now() - t0 < secs * 1000) { await fetchWorkspaces(true); const x = wsList.find(y => y.slug === w.slug); if (x && x.alive === alive) return true; await new Promise(r => setTimeout(r, 700)); } return false; };
42
+ const st = box.querySelector('[data-start]'); if (st) st.onclick = async () => { st.disabled = true; st.textContent = '⟳ encendiendo…'; err(''); const r = await api('/api/workspaces/start', { slug: w.slug }); const ok = r.data.ok || await waitFor(true, 6); err(ok ? '' : 'no respondió a tiempo · mirá .lampson/ws/' + w.slug + '/.lampson/web.log'); Panel.refresh(); };
43
+ const sp = box.querySelector('[data-stop]'); if (sp) sp.onclick = async () => { sp.disabled = true; sp.textContent = '⟳ apagando…'; err(''); await api('/api/workspaces/stop', { slug: w.slug }); const ok = await waitFor(false, 10); if (ok && w.slug === WS_SLUG) { location.href = HUB_BASE + '/'; return; } err(ok ? '' : 'sigue respondiendo · probá de nuevo'); Panel.refresh(); };
40
44
  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'); };
41
45
  const del = box.querySelector('.dfoot .del');
42
46
  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(); });
package/web.syn CHANGED
@@ -89,7 +89,8 @@ task as_text(x)
89
89
 
90
90
  task boot(profile, mode, ask_fn, pname, model)
91
91
  let cfg be provider.config_for(pname, model)
92
- let p be when contains(agents.PROFILES, profile) then profile otherwise "build"
92
+ -- worker es interno de delegate: por la API tampoco se elige como perfil de sesión
93
+ let p be when contains(agents.names(), profile) then profile otherwise "build"
93
94
  let opts be loop.default_opts(agents.registry_for(p), agents.catalog_for(p), ask_fn)
94
95
  set opts to loop.with_steps(opts, agents.steps_for(p))
95
96
  set opts to loop.with_inbox(opts, agents.parent_inbox, "")