lampson 0.1.3 → 0.1.4

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/session.syn CHANGED
@@ -72,7 +72,8 @@ export task list()
72
72
  try
73
73
  let d be load(id)
74
74
  let title be when contains(d["meta"], "title") then d["meta"]["title"] otherwise ""
75
- let project be when contains(d["meta"], "project") then d["meta"]["project"] otherwise mine
75
+ -- sin marca de proyecto (anteriores al 2026-08-27) no se listan en ningún workspace (aparecían en todos)
76
+ let project be when contains(d["meta"], "project") then d["meta"]["project"] otherwise ""
76
77
  when project == mine
77
78
  set out to append(out, {"id": id, "updated": d["updated"], "title": title})
78
79
  recover err
@@ -81,6 +82,42 @@ export task list()
81
82
  give []
82
83
  give sort_by(out, (s) => 0 - number(replace_text(replace_text(s["id"], "-", ""), "test", "")))
83
84
 
85
+ -- Búsqueda de texto en las sesiones de ESTE proyecto (título y contenido de los mensajes user/assistant):
86
+ -- [{id, updated, title, snippet}] — snippet = el primer trozo donde aparece `q`. Vacío = todas (sin snippet).
87
+ -- Lineal sobre los archivos (local, decenas o cientos de sesiones): suficiente; migrar a db si crece.
88
+ export task search(q, limit)
89
+ require file.read(".lampson")
90
+ require file.read(".lampson/*")
91
+ require env("LAMPSON_*")
92
+ let needle be lower(trim(text(q)))
93
+ let out be []
94
+ each s in list()
95
+ when length(out) >= limit
96
+ give out
97
+ when needle == ""
98
+ set out to append(out, {"id": s["id"], "updated": s["updated"], "title": s["title"], "snippet": ""})
99
+ otherwise
100
+ let hit be nothing
101
+ when contains(lower(s["title"]), needle)
102
+ set hit to ""
103
+ otherwise
104
+ try
105
+ let d be load(s["id"])
106
+ each m in d["messages"]
107
+ when hit == nothing and (m["role"] == "user" or m["role"] == "assistant") and text(m["content"]) == m["content"]
108
+ let c be text(m["content"])
109
+ let lc be lower(c)
110
+ when contains(lc, needle)
111
+ let pos be length(split(lc, needle)[0])
112
+ let a be when pos > 60 then pos - 60 otherwise 0
113
+ let b be when pos + length(needle) + 90 < length(c) then pos + length(needle) + 90 otherwise length(c)
114
+ set hit to (when a > 0 then "…" otherwise "") + replace_text(replace_text(slice(c, a, b), "\r", ""), "\n", " ") + (when b < length(c) then "…" otherwise "")
115
+ recover err
116
+ set hit to nothing
117
+ when hit != nothing
118
+ set out to append(out, {"id": s["id"], "updated": s["updated"], "title": s["title"], "snippet": hit})
119
+ give out
120
+
84
121
  -- Borrar una sesión (no hay delete_file en el runtime: se usa el shell del sistema sobre la ruta fija).
85
122
  export task delete(id)
86
123
  require exec
package/lib/settings.syn CHANGED
@@ -68,3 +68,58 @@ export task set_default(name, model)
68
68
  set doc["model"] to model
69
69
  save(doc)
70
70
  give true
71
+
72
+ -- valores de configuración generales (rueda de la UI): tz, public_url, webhook_url, webhook_secret…
73
+ -- Vacío = borrar la clave. Nunca se devuelven secretos al navegador: `values()` marca webhook_secret como has_*.
74
+ export let KEYS be ["tz", "public_url", "webhook_url", "webhook_secret"]
75
+
76
+ -- el archivo REAL, ignorando LAMPSON_NO_CONFIG: escribir con el doc en blanco de los tests pisaría provider/keys
77
+ -- (pasó el 2026-08-29 probando la rueda con un web de test)
78
+ task load_raw()
79
+ try
80
+ let doc be json_decode(read_file(PATH))
81
+ when not contains(doc, "keys")
82
+ set doc["keys"] to {}
83
+ when not contains(doc, "provider")
84
+ set doc["provider"] to ""
85
+ when not contains(doc, "model")
86
+ set doc["model"] to ""
87
+ give doc
88
+ recover err
89
+ give {"provider": "", "model": "", "keys": {}}
90
+
91
+ export task set_value(key, value)
92
+ require file(".lampson")
93
+ require file(".lampson/*")
94
+ require env("LAMPSON_*")
95
+ when not contains(KEYS, key)
96
+ raise("unknown setting '" + text(key) + "'")
97
+ let doc be load_raw()
98
+ when trim(text(value)) == ""
99
+ let clean be {}
100
+ each k in keys(doc)
101
+ when k != key
102
+ set clean[k] to doc[k]
103
+ set doc to clean
104
+ otherwise
105
+ set doc[key] to trim(text(value))
106
+ save(doc)
107
+ give true
108
+
109
+ export task values()
110
+ require file.read(".lampson")
111
+ require file.read(".lampson/*")
112
+ require env("LAMPSON_*")
113
+ let doc be load_raw()
114
+ let out be {}
115
+ each k in KEYS
116
+ when k == "webhook_secret"
117
+ set out["has_webhook_secret"] to (contains(doc, k) and text(doc[k]) != "") or env("LAMPSON_WEBHOOK_SECRET", "") != ""
118
+ otherwise
119
+ set out[k] to when contains(doc, k) then text(doc[k]) otherwise ""
120
+ -- lo fijado en .env gana y se muestra como tal
121
+ let e be env("LAMPSON_" + upper(k), "")
122
+ when e != ""
123
+ set out[k] to e
124
+ set out[k + "_from_env"] to true
125
+ give out
package/lib/tools.syn CHANGED
@@ -27,6 +27,7 @@ use "./skills.syn" as skills
27
27
  use "./mcp.syn" as mcp
28
28
  use "./lamps.syn" as lamps
29
29
  use "./lsp.syn" as lsp
30
+ use "./schedule.syn" as schedule
30
31
 
31
32
  -- la task de la tool skill (su SPEC está en tools/skill.syn)
32
33
  task skill_tool(name, action, source, scope)
@@ -155,6 +156,96 @@ let LSP_SPEC be {
155
156
  }, "required": ["op"]}
156
157
  }
157
158
 
159
+ -- la tool schedule (lib/schedule.syn): tareas programadas. add/remove/enable/run piden humano SIEMPRE (permission.syn):
160
+ -- crear una tarea = autorizar de una vez todo lo que va a hacer sin nadie mirando. Las corridas las hace el daemon
161
+ -- (web.syn); desde acá solo se corren en el acto las lamp/bash (una prompt anidaría un loop dentro del turno).
162
+ task schedule_tool(action, id, name, at, kind, lamp, tool, args, command, prompt, agent, permission, approval_timeout, notify)
163
+ require exec
164
+ require time
165
+ require net
166
+ require env("LAMPSON_*")
167
+ require env("OS")
168
+ require file(".lampson")
169
+ require file(".lampson/*")
170
+ require file.read("lamps")
171
+ require file.read("lamps/*")
172
+ require file("workspace")
173
+ require file("workspace/*")
174
+ let act be when action == nothing then "list" otherwise action
175
+ when act == "add"
176
+ let a be {"type": kind}
177
+ when kind == "lamp"
178
+ set a to {"type": "lamp", "lamp": lamp, "tool": tool, "args": when args == nothing then {} otherwise args}
179
+ otherwise when kind == "bash"
180
+ set a to {"type": "bash", "command": command}
181
+ otherwise when kind == "prompt"
182
+ set a to {"type": "prompt", "prompt": prompt, "agent": when agent == nothing then "build" otherwise agent}
183
+ let spec be {"name": name, "when": at, "action": a, "permission": when permission == nothing then "ask" otherwise permission}
184
+ when approval_timeout != nothing
185
+ set spec["approval_timeout"] to approval_timeout
186
+ when notify != nothing
187
+ set spec["notify"] to notify
188
+ let t be schedule.add(spec)
189
+ give "scheduled '" + t["name"] + "' (id " + t["id"] + "): " + schedule.describe_plan(t["plan"]) + " (local time) · " + schedule.describe_action(t["action"]) + " · permission " + t["permission"] + " · next run " + schedule.fmt_local(t["next_run"]) + " — tell the user this next-run time as is. The run's report lands in a NEW session named ⏰ <name> (sidebar «Sesiones» / /sessions) and, if notify is set, in that webhook — NOT in this chat: say so. " + daemon_note()
190
+ when act == "remove"
191
+ give schedule.remove(id)
192
+ when act == "enable"
193
+ give schedule.set_enabled(id, true)
194
+ when act == "disable"
195
+ give schedule.set_enabled(id, false)
196
+ when act == "run"
197
+ let t be schedule.get(id)
198
+ when t == nothing
199
+ raise("no scheduled task '" + text(id) + "'")
200
+ when t["action"]["type"] == "prompt"
201
+ schedule.request_run(id)
202
+ give "run requested: it runs within " + text(schedule.TICK_SECONDS) + " s in the background and its report lands in a NEW session (⏰ " + t["name"] + "), not in this chat. " + daemon_note()
203
+ schedule.mark_started(t)
204
+ let started be now()
205
+ let out be schedule.run_simple(t)
206
+ schedule.record(t, started, when starts_with(out, "ERROR") or starts_with(out, "DENIED") then "error" otherwise "ok", out, {})
207
+ give out
208
+ when act == "log"
209
+ let tail be schedule.log_tail(id, 80)
210
+ give when tail == "" then "no runs yet for " + text(id) otherwise tail
211
+ let sm be schedule.summary()
212
+ when length(sm) == 0
213
+ give "no scheduled tasks. " + daemon_note()
214
+ let lines be []
215
+ each s in sm
216
+ set lines to append(lines, s["id"] + (when s["enabled"] then " ON " otherwise " off") + " · " + s["plan"] + " · " + s["action_text"] + " · permission " + s["permission"] + " · next " + s["next_local"] + (when s["last_run"] != nothing then " · last " + s["last_status"] + ": " + s["last_summary"] otherwise ""))
217
+ give join(lines, "\n") + "\n" + daemon_note()
218
+
219
+ task daemon_note()
220
+ require time
221
+ require file(".lampson")
222
+ require file(".lampson/*")
223
+ let age be schedule.daemon_age()
224
+ when age == nothing or age > schedule.TICK_SECONDS * 3
225
+ give "NOTE: no scheduler is running right now (scheduled tasks run inside the web/daemon process): the user starts it with `lampson --daemon start` (or keeps `lampson --web` open)."
226
+ give "Scheduler alive (last tick " + text(floor(age)) + " s ago)."
227
+
228
+ let SCHEDULE_SPEC be {
229
+ "name": "schedule",
230
+ "description": "Scheduled tasks: run something on a schedule with nobody watching — a lamp tool, a fixed shell command, or a full agent run from a prompt (kind=prompt: the agent works unattended with the chosen profile and writes a report; its session appears as ⏰ <name>). `at` formats — recurring: 'every 6h' | 'every 30m' | 'daily 09:00' | 'mon,wed 08:30' | 'weekdays 09:00'; ONE-TIME (runs once, then turns itself off): 'today 15:14' | 'tomorrow 09:00' | 'once 2026-08-29 15:14' | 'in 2h'. Times are the USER'S LOCAL time (the machine's timezone): write the hour exactly as the user says it — NEVER convert to UTC. Tasks belong to the current workspace. permission = what a prompt run may do without asking: strict (dangerous actions denied), ask (default: a dangerous action sends the user an approval link/notification and waits up to approval_timeout seconds, denied if unanswered), yolo (allowed). notify = optional webhook URL that receives the result as JSON (for 'search and send me' tasks). action=add ALWAYS asks the user (it authorizes future unattended runs); remove/enable/disable/run also ask; list and log do not. The tasks are executed by Lampson's resident process (`lampson --daemon start` or the open web UI) — say so if the list shows no scheduler running. Use it when the user says 'every day at', 'each N hours', 'on Mondays', 'periodically', 'remind me', 'send me'.",
231
+ "parameters": {"type": "object", "properties": {
232
+ "action": {"type": "string", "enum": ["list", "add", "remove", "enable", "disable", "run", "log"], "description": "Default: list"},
233
+ "id": {"type": "string", "description": "remove/enable/disable/run/log: the task id (from list)"},
234
+ "name": {"type": "string", "description": "add: short human name"},
235
+ "at": {"type": "string", "description": "add: the schedule — 'every 6h' | 'daily 09:00' | 'mon,wed 08:30' | 'weekdays 09:00'"},
236
+ "kind": {"type": "string", "enum": ["lamp", "bash", "prompt"], "description": "add: what runs"},
237
+ "lamp": {"type": "string", "description": "add kind=lamp: lamp name (must be ON)"},
238
+ "tool": {"type": "string", "description": "add kind=lamp: tool of that lamp"},
239
+ "args": {"type": "object", "description": "add kind=lamp: arguments for the tool"},
240
+ "command": {"type": "string", "description": "add kind=bash: the shell command (must finish on its own; no servers)"},
241
+ "prompt": {"type": "string", "description": "add kind=prompt: self-contained instructions for the unattended agent run (what to do, how to verify, what to report)"},
242
+ "agent": {"type": "string", "enum": ["build", "plan", "review", "explore"], "description": "add kind=prompt: profile (default build)"},
243
+ "permission": {"type": "string", "enum": ["strict", "ask", "yolo"], "description": "add: permission envelope for unattended runs (default ask)"},
244
+ "approval_timeout": {"type": "integer", "description": "add: seconds to wait for a remote approval (default 7200)"},
245
+ "notify": {"type": "string", "description": "add: webhook URL that receives each result as JSON"}
246
+ }, "required": ["action"]}
247
+ }
248
+
158
249
  -- Allow-list nombre → task. El modelo solo devuelve NOMBRES; el loop decide si se ejecuta.
159
250
  export task registry()
160
251
  give {
@@ -171,10 +262,11 @@ export task registry()
171
262
  "skill": skill_tool,
172
263
  "mcp": mcp_tool,
173
264
  "lamp": lamp_tool,
174
- "lsp": lsp_tool
265
+ "lsp": lsp_tool,
266
+ "schedule": schedule_tool
175
267
  }
176
268
 
177
- export let CATALOG be [t_read.SPEC, t_write.SPEC, t_edit.SPEC, t_ls.SPEC, t_find.SPEC, t_grep.SPEC, LSP_SPEC, t_bash.SPEC, t_process.SPEC, t_memo.SPEC, t_todo.SPEC, t_skill.SPEC, MCP_SPEC, LAMP_SPEC]
269
+ export let CATALOG be [t_read.SPEC, t_write.SPEC, t_edit.SPEC, t_ls.SPEC, t_find.SPEC, t_grep.SPEC, LSP_SPEC, t_bash.SPEC, t_process.SPEC, t_memo.SPEC, t_todo.SPEC, t_skill.SPEC, MCP_SPEC, LAMP_SPEC, SCHEDULE_SPEC]
178
270
 
179
271
  -- Subconjuntos (para perfiles de agente): registry/catálogo filtrados por nombre.
180
272
  export task registry_subset(names)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lampson",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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": {
@@ -0,0 +1,56 @@
1
+ /* chat: mensajes, pasos del agente, tarjeta de permiso, portada (hero) */
2
+ /* mensajes: el del usuario es una nota al margen (como el blockquote de la doc), el del agente es prosa */
3
+ .msg { margin:0 0 16px; }
4
+ .user { margin:26px 0 16px; padding:.85em 1.1em; background:var(--paper-2); border-left:2px solid var(--accent); white-space:pre-wrap; font-size:15px; }
5
+ .user::before { content:"vos"; display:block; font:400 10px/1 var(--mono); letter-spacing:.12em; text-transform:uppercase; color:var(--accent); margin-bottom:6px; }
6
+ .assistant { font-size:15px; line-height:1.62; }
7
+ .assistant p { margin:0 0 .9em; }
8
+ .assistant code { font:400 .86em/1.5 var(--mono); background:var(--paper-2); border-radius:2px; padding:.08em .3em; color:var(--ink); }
9
+ .assistant pre { background:var(--paper-2); border:1px solid var(--rule); border-radius:var(--r); padding:14px 16px; overflow-x:auto; margin:1em 0; }
10
+ .assistant pre code { background:none; padding:0; font-size:13px; line-height:1.62; }
11
+ .assistant ul, .assistant ol { margin:0 0 .9em; padding-left:1.25em; }
12
+ .assistant li { margin:.3em 0; }
13
+ .assistant li::marker { color:var(--ink-3); }
14
+ .assistant h1, .assistant h2, .assistant h3 { font:600 1.06rem/1.4 var(--serif); margin:1.4em 0 .5em; }
15
+ .assistant b, .assistant strong { font-weight:600; }
16
+
17
+ /* pasos del agente: maquinaria, en mono */
18
+ .step { display:grid; grid-template-columns:16px 1fr; column-gap:8px; margin:0 0 7px 2px; font:400 12.5px/1.5 var(--mono); color:var(--ink-2); }
19
+ .step .ic { text-align:center; color:var(--ink-3); }
20
+ .step .ok { color:var(--str); }
21
+ .step .bad { color:var(--rubric); }
22
+ .step .name { color:var(--ink); }
23
+ .step .cmd { white-space:pre-wrap; word-break:break-word; max-height:4.6em; overflow:hidden; }
24
+ .step .cmd.open { max-height:none; }
25
+ .step .more { color:var(--accent); cursor:pointer; font-size:11px; }
26
+ .step details { grid-column:2; min-width:0; }
27
+ .step summary { cursor:pointer; list-style:none; color:var(--ink-3); white-space:pre-wrap; word-break:break-word; max-height:3.2em; overflow:hidden; }
28
+ .step summary::before { content:"→ "; }
29
+ .step summary.bad { color:var(--rubric); }
30
+ .step pre { margin:6px 0 4px; padding:10px 12px; background:var(--paper-2); border:1px solid var(--rule); border-radius:var(--r); white-space:pre-wrap; max-height:340px; overflow:auto; font-size:12px; color:var(--ink-2); }
31
+ .denied { color:var(--rubric); font:400 12.5px/1.5 var(--mono); }
32
+ .meta { color:var(--ink-3); font:400 11px/1 var(--mono); letter-spacing:.03em; margin:4px 0 20px 2px; }
33
+ .working { display:flex; align-items:center; gap:8px; color:var(--ink-3); font:400 11.5px/1 var(--mono); margin:4px 0 14px 2px; }
34
+ .working .dot { width:7px; height:7px; border-radius:50%; background:var(--accent); animation:pulse 1s infinite; }
35
+ @keyframes pulse { 0%,100% { opacity:.25 } 50% { opacity:1 } }
36
+
37
+ /* permisos: lo único en rubric */
38
+ .approval .card { border:1px solid var(--rubric); border-radius:var(--r); padding:12px 14px; max-width:720px; }
39
+ .approval .why { color:var(--rubric); font:600 12px/1.5 var(--mono); margin-bottom:6px; }
40
+ .approval code { font:400 12.5px/1.5 var(--mono); display:block; background:var(--paper-2); padding:8px 10px; border-radius:2px; margin-bottom:10px; white-space:pre-wrap; }
41
+ .approval .btns { display:flex; gap:8px; }
42
+
43
+ /* ---- hero: el primer golpe de vista ---- */
44
+ .empty { width:100%; max-width:1180px; margin:0 auto; color:var(--ink-2); }
45
+ .empty .eyebrow { font:400 10.5px/1 var(--mono); text-transform:uppercase; letter-spacing:.12em; color:var(--rubric); margin:0 0 12px; }
46
+ .empty h1 { font:600 2.1rem/1.15 var(--serif); letter-spacing:-.022em; color:var(--ink); margin:0 0 .4em; }
47
+ .empty .lead { font-size:1.06rem; margin:0 0 26px; max-width:720px; }
48
+ .empty .grid { display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); gap:14px; }
49
+ .empty .card { border:1px solid var(--rule); border-radius:var(--r); padding:14px 16px 12px; min-width:0; }
50
+ .empty .card h3 { font:400 10.5px/1 var(--mono); text-transform:uppercase; letter-spacing:.1em; color:var(--ink-3); margin:0 0 12px; }
51
+ .empty .card p { margin:0 0 8px; font-size:13.5px; line-height:1.5; }
52
+ .empty code { font:400 .88em/1.5 var(--mono); background:var(--paper-2); padding:.08em .3em; border-radius:2px; color:var(--ink); white-space:nowrap; }
53
+ .empty .k { font:600 12px/1 var(--mono); color:var(--ink); margin-right:.3em; }
54
+ .empty .try { cursor:pointer; color:var(--accent); }
55
+ .empty .try:hover { text-decoration:underline; text-underline-offset:.18em; }
56
+ @media (max-width:1400px) { .empty .grid { grid-template-columns:repeat(2, minmax(0,1fr)); } }
@@ -0,0 +1,97 @@
1
+ /* layout: grid de tres columnas, cabecera, paneles laterales plegables, zona central (chat / visor / terminal), composer */
2
+ /* ---- grid: cabecera arriba, panel izquierdo (todo lo operativo), chat, panel derecho (árbol) ---- */
3
+ body { display:grid; grid-template-rows:auto 1fr; grid-template-columns:var(--side-l) 1fr var(--side-r); grid-template-areas:"top top top" "side main tree"; --side-l:var(--side); --side-r:var(--side); }
4
+ body.l-closed { --side-l:40px; }
5
+ body.r-closed { --side-r:40px; }
6
+
7
+ /* ---- header: fila 1 quién/dónde, fila 2 contexto y cómo corre el agente ---- */
8
+ header { grid-area:top; border-bottom:1px solid var(--rule); background:var(--paper); }
9
+ .hrow { display:flex; align-items:center; gap:14px; padding:0 18px; height:44px; min-width:0; }
10
+ .hrow.sub { height:32px; border-top:1px solid var(--rule); gap:12px; }
11
+ .brand { font:600 14px/1 var(--mono); color:var(--ink); letter-spacing:-.01em; white-space:nowrap; }
12
+ .brand .proj { color:var(--ink-3); font-weight:400; margin-left:.6em; }
13
+ .hbtn { display:flex; align-items:center; gap:6px; font:600 10.5px/1 var(--mono); text-transform:uppercase; letter-spacing:.09em; color:var(--ink-2); background:none; border:1px solid var(--rule-2); border-radius:2px; padding:7px 12px; cursor:pointer; }
14
+ .hbtn:hover { color:var(--ink); border-color:var(--ink-3); }
15
+ .hbtn.upd { color:var(--amber); border-color:var(--amber); }
16
+ .hbtn.upd:hover { color:var(--ink); border-color:var(--ink); }
17
+ .theme-toggle { display:flex; align-items:center; justify-content:center; background:none; border:0; color:var(--ink-3); cursor:pointer; padding:6px; border-radius:var(--r); line-height:0; }
18
+ .theme-toggle:hover { color:var(--ink); background:var(--paper-2); }
19
+ header select { font:400 11.5px/1 var(--mono); color:var(--ink-2); background:var(--paper); border:1px solid var(--rule); border-radius:var(--r); padding:5px 6px; cursor:pointer; }
20
+ header select:hover { border-color:var(--rule-2); color:var(--ink); }
21
+ .pill { font:400 11.5px/1 var(--mono); color:var(--ink-3); letter-spacing:.02em; white-space:nowrap; max-width:200px; overflow:hidden; text-overflow:ellipsis; flex:none; cursor:pointer; border-bottom:1px dashed transparent; }
22
+ .pill:hover { color:var(--ink); border-bottom-color:var(--rule-2); }
23
+ .pill.nokey { color:var(--rubric); }
24
+ .pill.on { color:var(--accent); }
25
+
26
+ .gitchip { display:none; align-items:center; gap:8px; font:400 11.5px/1 var(--mono); color:var(--ink-2); max-width:45%; flex:none; }
27
+ .gitchip .br { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
28
+ .gitchip b { font-weight:600; }
29
+ .gitchip .w { color:var(--amber); } .gitchip .g { color:var(--str); } .gitchip .r { color:var(--rubric); } .gitchip .c { color:var(--ink-3); font-weight:400; }
30
+ .hpath { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--ink-3); font:400 11.5px/1 var(--mono); }
31
+
32
+ /* ---- paneles laterales: iguales, plegables (cerrado = barra angosta con su icono) ---- */
33
+ aside { grid-area:side; position:relative; border-right:1px solid var(--rule); background:var(--paper); display:flex; flex-direction:column; min-height:0; overflow:hidden; }
34
+ aside.right { grid-area:tree; border-right:0; border-left:1px solid var(--rule); }
35
+ aside .rail { display:none; flex-direction:column; align-items:center; padding-top:10px; gap:8px; }
36
+ aside .rail button, aside .fold { background:none; border:0; color:var(--ink-3); cursor:pointer; padding:5px; border-radius:var(--r); line-height:0; }
37
+ aside .rail button:hover, aside .fold:hover { color:var(--ink); background:var(--paper-2); }
38
+ aside .fold { position:absolute; top:7px; right:6px; z-index:2; }
39
+ aside.right h2 { padding-right:56px; }
40
+ aside:not(.right) > .sec:first-of-type h2 { padding-right:60px; }
41
+ aside.closed .sec, aside.closed .fold { display:none; }
42
+ aside.closed .rail { display:flex; }
43
+
44
+ /* ---- main ---- */
45
+ main { grid-area:main; display:grid; grid-template-rows:1fr auto; min-height:0; min-width:0; background:var(--paper); }
46
+ #stage { overflow:auto; min-height:0; }
47
+ #log { padding:26px 40px 30px; max-width:920px; }
48
+ #log.hero { max-width:none; min-height:100%; display:flex; align-items:center; padding:20px 40px; }
49
+ #viewer { display:none; padding:0; }
50
+ .vh { position:sticky; top:0; display:flex; gap:12px; align-items:center; padding:9px 16px; background:var(--paper-3); border-bottom:1px solid var(--rule); font:400 11.5px/1 var(--mono); color:var(--ink-3); letter-spacing:.03em; }
51
+ .vh b { color:var(--ink); font-weight:400; }
52
+ .vh .x { cursor:pointer; color:var(--ink-3); }
53
+ .vh .x:hover { color:var(--ink); }
54
+ #viewer pre { margin:0; padding:14px 18px; font:400 12.5px/1.62 var(--mono); tab-size:4; }
55
+ #viewer pre .ln { display:inline-block; width:4ch; margin-right:16px; color:var(--ink-3); text-align:right; user-select:none; }
56
+ #viewer pre.log { color:var(--ink-2); white-space:pre-wrap; }
57
+ /* terminal real (xterm.js sobre un pty del servidor) — ocupa la zona central como un archivo abierto */
58
+ #termpane { display:none; height:100%; flex-direction:column; }
59
+ #termpane .vh { position:static; }
60
+ #termpane .vh .st { width:7px; height:7px; border-radius:50%; background:var(--ink-3); }
61
+ #termpane.live .vh .st { background:var(--str); }
62
+ #xterm { flex:1; min-height:0; padding:10px 14px; background:var(--paper); }
63
+ #xterm .xterm { height:100%; }
64
+ #xterm .xterm-viewport { background:transparent !important; }
65
+
66
+ /* ---- composer ---- */
67
+ form { padding:12px 40px 12px; border-top:1px solid var(--rule); background:var(--paper); box-sizing:border-box; width:100%; max-width:100%; min-width:0; }
68
+ main > div { min-width:0; }
69
+ .composer { background:var(--paper-2); border:1px solid var(--rule); border-radius:var(--r); padding:10px 10px 8px 14px; transition:border-color .12s, box-shadow .12s; }
70
+ .composer:focus-within { border-color:var(--rule-2); box-shadow:0 0 0 3px var(--accent-bg); }
71
+ textarea { display:block; width:100%; resize:none; min-height:40px; max-height:220px; background:transparent; color:var(--ink); border:0; padding:2px 0; font:400 15px/1.55 var(--serif); }
72
+ textarea::placeholder { color:var(--ink-3); }
73
+ textarea:focus { outline:none; }
74
+ .crow { display:flex; align-items:center; gap:10px; margin-top:6px; min-width:0; }
75
+ .crow .tools { flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
76
+ .crow .keys, .crow button { flex:none; }
77
+ /* imágenes pegadas: miniaturas en el composer y en el mensaje */
78
+ .attach { display:flex; gap:8px; flex-wrap:wrap; }
79
+ .attach:not(:empty) { margin:2px 0 8px; }
80
+ .attach .th, .user .th { position:relative; width:72px; height:72px; border:1px solid var(--rule); border-radius:var(--r); overflow:hidden; background:var(--paper-3); }
81
+ .attach .th img, .user .th img { width:100%; height:100%; object-fit:cover; display:block; }
82
+ .attach .th .rm { position:absolute; top:2px; right:2px; width:18px; height:18px; border-radius:50%; background:var(--paper); color:var(--ink-2); font:600 11px/18px var(--mono); text-align:center; cursor:pointer; border:1px solid var(--rule-2); }
83
+ .attach .th .rm:hover { color:var(--rubric); }
84
+ .attach .th .sz { position:absolute; left:0; right:0; bottom:0; background:rgba(0,0,0,.55); color:#fff; font:400 9.5px/1.4 var(--mono); text-align:center; }
85
+ .user .ims { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
86
+ .user .th { width:120px; height:90px; cursor:zoom-in; }
87
+ .composer.drop { border-color:var(--accent); }
88
+ .attach .novision { flex-basis:100%; color:var(--amber); font:400 11.5px/1.5 var(--mono); }
89
+ .crow .tools { color:var(--ink-3); font:400 11px/1 var(--mono); letter-spacing:.03em; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
90
+ .crow .keys { color:var(--ink-3); font:400 10.5px/1 var(--mono); letter-spacing:.03em; }
91
+ #send { width:34px; height:30px; padding:0; font-size:15px; display:inline-flex; align-items:center; justify-content:center; }
92
+ #stop { color:var(--rubric); border-color:var(--rubric); display:none; height:30px; }
93
+ form.busy #send { display:none; } form.busy #stop { display:inline-flex; align-items:center; gap:6px; }
94
+ .hint { display:none; }
95
+
96
+ @media (max-width:900px) { body { grid-template-columns:1fr; grid-template-areas:"top" "main"; } aside { display:none; } #log, form { padding-left:20px; padding-right:20px; } }
97
+ @media (prefers-reduced-motion:reduce) { *, *::before, *::after { transition:none !important; animation:none !important; } }
@@ -0,0 +1,125 @@
1
+ /* panel: EL componente modal de la app (js/panel.js). Un cascarón (overlay, cabecera, ✕/Esc/clic afuera) y
2
+ tres layouts: browse (buscador + lista + detalle), tabs (pestañas con formularios) y form (un formulario).
3
+ Cada vista (sesiones, lámparas, configuración, programar…) solo aporta su contenido; nada de esto se repite. */
4
+ .modal { position:fixed; inset:0; background:rgba(0,0,0,.45); display:flex; align-items:center; justify-content:center; z-index:50; }
5
+ .panel { background:var(--paper); border:1px solid var(--rule-2); border-radius:var(--r); box-shadow:0 10px 30px rgba(0,0,0,.35); display:flex; flex-direction:column; min-height:0; max-height:92vh; }
6
+ .panel.lg { width:min(1100px, 94vw); height:min(760px, 90vh); padding:14px 18px 12px; }
7
+ .panel.md { width:min(680px, 94vw); padding:14px 18px 12px; }
8
+ .panel.sm { width:min(560px, 92vw); padding:14px 18px 12px; }
9
+
10
+ /* cabecera */
11
+ .panel .phead { display:flex; align-items:center; gap:12px; padding-bottom:10px; border-bottom:1px solid var(--rule); flex:none; }
12
+ .panel .phead .eyebrow { font:400 10.5px/1 var(--mono); text-transform:uppercase; letter-spacing:.12em; color:var(--rubric); margin:0; }
13
+ .panel .phead h3 { margin:0; font:600 16px/1.2 var(--serif); letter-spacing:-.01em; }
14
+ .panel .phead .sub { color:var(--ink-3); font:400 11.5px/1 var(--mono); }
15
+ .panel .pghost { display:inline-flex; align-items:center; justify-content:center; width:26px; height:26px; padding:0; margin:0; font:400 13px/1 var(--mono); letter-spacing:0; text-transform:none; border:1px solid var(--rule); border-radius:var(--r); background:transparent; color:var(--ink-3); cursor:pointer; }
16
+ .panel .pghost:hover { color:var(--ink); border-color:var(--rule-2); }
17
+ .panel .pghost + .pghost { margin-left:6px; }
18
+
19
+ /* pestañas */
20
+ .panel .ptabs { display:flex; gap:2px; border-bottom:1px solid var(--rule); margin:0 0 14px; flex:none; }
21
+ .panel .ptab { font:400 11px/1 var(--mono); text-transform:uppercase; letter-spacing:.08em; color:var(--ink-3); padding:10px 12px 8px; cursor:pointer; border-bottom:2px solid transparent; margin-bottom:-1px; }
22
+ .panel .ptab:hover { color:var(--ink); }
23
+ .panel .ptab.on { color:var(--accent); border-bottom-color:var(--accent); }
24
+
25
+ /* cuerpo genérico (form / tabs): prosa y campos con la gramática de la app */
26
+ .panel .pbody { flex:1; min-height:0; overflow:auto; padding:14px 2px 4px; }
27
+ .panel .lead { color:var(--ink-2); margin:0 0 16px; font-size:14px; }
28
+ .panel .lead code, .panel .phelp code { font:400 .88em/1.5 var(--mono); background:var(--paper-2); padding:.08em .3em; border-radius:2px; color:var(--ink); }
29
+ .panel label { display:flex; align-items:center; gap:12px; font:400 10.5px/1 var(--mono); text-transform:uppercase; letter-spacing:.1em; color:var(--ink-3); margin:0 0 10px; }
30
+ .panel label.col { flex-direction:column; align-items:stretch; gap:4px; }
31
+ .panel label .ds { display:block; color:var(--ink-3); font:400 11.5px/1.4 var(--serif); text-transform:none; letter-spacing:0; margin-top:2px; }
32
+ .panel input, .panel select, .panel textarea { flex:1; font:400 13px/1.4 var(--mono); color:var(--ink); background:var(--paper-2); border:1px solid var(--rule); border-radius:var(--r); padding:7px 10px; min-width:0; }
33
+ .panel input:focus, .panel select:focus, .panel textarea:focus { outline:none; border-color:var(--rule-2); box-shadow:0 0 0 3px var(--accent-bg); }
34
+ .panel input[disabled] { opacity:.55; }
35
+ .panel textarea { width:100%; resize:vertical; font:400 12.5px/1.45 var(--mono); }
36
+ .panel .pfoot { display:flex; gap:8px; align-items:center; margin-top:14px; flex:none; }
37
+ .panel .err { color:var(--rubric); font:400 12px/1.4 var(--mono); min-height:14px; }
38
+ .panel .plist { border-top:1px solid var(--rule); border-bottom:1px solid var(--rule); max-height:40vh; overflow:auto; margin:0 0 14px; }
39
+ .panel .plist .pr { display:flex; align-items:center; gap:10px; padding:7px 10px; cursor:pointer; font:400 12.5px/1.4 var(--mono); color:var(--ink-2); border-left:2px solid transparent; }
40
+ .panel .plist .pr:hover { color:var(--ink); }
41
+ .panel .plist .pr.on { color:var(--ink); border-left-color:var(--accent); background:var(--paper-2); }
42
+ .panel .plist .pr b { width:110px; font-weight:600; color:var(--ink); }
43
+ .panel .plist .pr .dm { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--ink-3); }
44
+ .panel .plist .pr .k { font-size:11px; color:var(--str); }
45
+ .panel .plist .pr .k.no { color:var(--ink-3); }
46
+
47
+ /* browse: lista a la izquierda (buscador, filas, contador), detalle a la derecha. Un scroll por columna, nunca horizontal. */
48
+ .panel .browse { flex:1; min-height:0; display:grid; grid-template-columns:var(--list-w, 300px) minmax(0,1fr); }
49
+ .panel .blist { display:flex; flex-direction:column; border-right:1px solid var(--rule); padding:10px 12px 8px 0; min-height:0; min-width:0; }
50
+ .panel .blist input { flex:none; height:30px; font:400 12px/1.4 var(--mono); background:transparent; padding:0 8px; margin:0 0 8px; width:100%; }
51
+ .panel .bitems { flex:1; overflow-y:auto; overflow-x:hidden; min-height:0; }
52
+ .panel .bcount { color:var(--ink-3); font:400 11px/1 var(--mono); padding-top:8px; border-top:1px solid var(--rule); flex:none; }
53
+ .panel .bdetail { overflow-y:auto; overflow-x:hidden; min-height:0; min-width:0; padding:12px 6px 8px 18px; font:400 12.5px/1.5 var(--mono); }
54
+ .panel .bempty { color:var(--ink-3); font:400 12.5px/1.5 var(--serif); padding:8px; }
55
+ .panel .li { display:grid; grid-template-columns:14px minmax(0,1fr); gap:8px; align-items:start; padding:7px 8px; border-radius:var(--r); cursor:pointer; font:400 12px/1.35 var(--mono); min-width:0; }
56
+ .panel .li.nodot { grid-template-columns:minmax(0,1fr); }
57
+ .panel .li > div { min-width:0; }
58
+ .panel .li:hover { background:var(--paper-2); }
59
+ .panel .li.sel { background:var(--paper-2); box-shadow:inset 2px 0 0 var(--accent); }
60
+ .panel .li .dot { color:var(--ink-3); margin-top:1px; }
61
+ .panel .li.on .dot { color:var(--accent); }
62
+ .panel .li.bad .dot { color:var(--rubric); }
63
+ .panel .li.new .nm { color:var(--accent); font-weight:400; }
64
+ .panel .dform label { margin-top:10px; }
65
+ .panel .dform .lead { margin:8px 0 12px; }
66
+ .panel .dform .pfoot { margin-top:16px; }
67
+ .panel .derr { color:var(--rubric); font:400 12px/1.4 var(--mono); min-height:14px; margin-top:6px; }
68
+ .panel .dlog { margin-top:12px; max-height:38vh; overflow:auto; font:400 11.5px/1.45 var(--mono); border:1px solid var(--rule); border-radius:var(--r); padding:8px; white-space:pre-wrap; color:var(--ink-2); }
69
+ .panel .li.broken .dot { color:var(--rubric); }
70
+ .panel .li .nm { color:var(--ink); font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
71
+ .panel .li .nm.serif { font:400 13px/1.35 var(--serif); }
72
+ .panel .li .meta { color:var(--ink-3); font-size:11px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
73
+ .panel .li .snip { color:var(--ink-2); font:400 12px/1.4 var(--serif); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
74
+ .panel mark { background:transparent; color:var(--accent); font-weight:600; }
75
+
76
+ /* piezas de detalle que comparten las vistas */
77
+ .panel .dhead { display:flex; align-items:center; gap:12px; flex-wrap:wrap; }
78
+ .panel .dhead .nm { font:600 18px/1.2 var(--mono); color:var(--ink); }
79
+ .panel .dhead .nm.serif { font:600 16px/1.3 var(--serif); }
80
+ .panel .dhead .meta { color:var(--ink-3); font-size:11.5px; }
81
+ .panel .ddesc { color:var(--ink-2); font:400 13px/1.5 var(--serif); margin:8px 0 4px; max-width:70ch; }
82
+ .panel .dcap { color:var(--ink-3); font-size:11.5px; margin-bottom:12px; word-break:break-all; }
83
+ .panel .dcap b { color:var(--rubric); font-weight:600; }
84
+ .panel .dcap.err { color:var(--rubric); }
85
+ .panel .dnote { color:var(--ink-3); font:400 12px/1.5 var(--serif); border:1px dashed var(--rule-2); border-radius:var(--r); padding:8px 10px; margin:8px 0 12px; }
86
+ .panel .dacts { display:flex; gap:8px; margin-top:12px; }
87
+ .panel .dacts button { font:400 12px/1 var(--mono); padding:6px 12px; }
88
+ .panel .dfoot { display:flex; align-items:center; gap:12px; margin-top:14px; padding-top:10px; border-top:1px solid var(--rule); color:var(--ink-3); font-size:11.5px; }
89
+ .panel .dfoot .del { margin-left:auto; cursor:pointer; color:var(--rubric); }
90
+ .panel .dfoot .del.ask .yes { color:var(--rubric); cursor:pointer; font-weight:600; }
91
+ .panel .dfoot .del.ask .no { cursor:pointer; }
92
+ .panel .none { color:var(--ink-3); font:400 13px/1.6 var(--serif); max-width:520px; }
93
+ /* switch encendido/apagado (lámparas) */
94
+ .panel label.sw { display:inline-flex; align-items:center; margin:0 0 0 auto; gap:6px; cursor:pointer; user-select:none; font:400 11.5px/1 var(--mono); text-transform:none; letter-spacing:0; color:var(--ink-3); }
95
+ .panel label.sw input { flex:none; width:16px; height:16px; padding:0; margin:0; accent-color:var(--accent); }
96
+ .panel label.sw.on { color:var(--accent); }
97
+ /* formulario generado desde un JSON Schema (tools de una lámpara) */
98
+ .panel .tool { border:1px solid var(--rule); border-radius:var(--r); padding:10px 12px; margin-bottom:10px; }
99
+ .panel .tool .th { display:grid; grid-template-columns:auto 1fr auto; gap:8px 10px; align-items:baseline; }
100
+ .panel .tool .th code { font-weight:600; color:var(--ink); }
101
+ .panel .tool .th .ds { color:var(--ink-3); font:400 12px/1.4 var(--serif); }
102
+ .panel label.lf { display:grid; grid-template-columns:120px minmax(0,1fr); gap:2px 10px; align-items:center; margin:8px 0 0; font:400 12px/1.4 var(--mono); text-transform:none; letter-spacing:0; color:var(--ink); }
103
+ .panel label.lf .lk { color:var(--ink); }
104
+ .panel label.lf .req { color:var(--rubric); font-size:10.5px; }
105
+ .panel label.lf .ds { grid-column:2; color:var(--ink-3); font:400 11.5px/1.4 var(--serif); }
106
+ .panel label.lf input, .panel label.lf select, .panel label.lf textarea { flex:none; width:100%; max-width:420px; padding:4px 6px; font:400 12px/1.4 var(--mono); background:transparent; }
107
+ .panel label.lf textarea { grid-column:2; min-height:56px; }
108
+ .panel .tool .go { margin-top:10px; display:flex; align-items:center; gap:10px; }
109
+ .panel .tool .go button { font:400 12px/1 var(--mono); padding:5px 10px; }
110
+ .panel .tool .go .st { color:var(--ink-3); font-size:11.5px; }
111
+ .panel .tool pre.out { margin-top:8px; max-height:38vh; overflow:auto; font:400 11.5px/1.45 var(--mono); border:1px solid var(--rule); border-radius:var(--r); padding:8px; white-space:pre-wrap; }
112
+ /* conversación resumida (sesiones) */
113
+ .panel .dmsgs { margin-top:12px; display:flex; flex-direction:column; gap:8px; }
114
+ .panel .dmsg { border-left:2px solid var(--rule-2); padding:2px 0 2px 10px; font:400 12.5px/1.5 var(--serif); color:var(--ink-2); white-space:pre-wrap; word-break:break-word; }
115
+ .panel .dmsg.dm-user { border-left-color:var(--accent); color:var(--ink); }
116
+ .panel .dmsg.dm-bad { border-left-color:var(--rubric); }
117
+ .panel .dmsg .who a { color:var(--accent); text-decoration:none; }
118
+ .panel .dmsg .who { font:400 10.5px/1 var(--mono); text-transform:uppercase; letter-spacing:.08em; color:var(--ink-3); display:block; margin-bottom:3px; }
119
+ /* ayuda («?»): prosa a lo ancho, reemplaza al cuerpo */
120
+ .panel .phelp { flex:1; min-height:0; overflow:auto; padding:16px 24px 8px 6px; font:400 13.5px/1.6 var(--serif); color:var(--ink-2); }
121
+ .panel .phelp h4 { margin:0 0 8px; font:600 15px/1.3 var(--serif); color:var(--ink); }
122
+ .panel .phelp b { color:var(--ink); font-weight:600; }
123
+ .panel .phelp p { margin:0 0 12px; max-width:78ch; }
124
+ .panel .phelp ul { margin:0 0 16px 20px; padding:0; display:grid; gap:4px; max-width:78ch; }
125
+ .panel .phelp button { text-transform:none; letter-spacing:0; font:400 12px/1 var(--mono); padding:6px 10px; }
@@ -0,0 +1,80 @@
1
+ /* sidebar: secciones acordeón y las filas de cada una */
2
+ /* secciones colapsables; el activo es una muesca en la regla, no una caja */
3
+ .sec { display:flex; flex-direction:column; min-height:0; flex:none; overflow:hidden; }
4
+ .sec + .sec { border-top:1px solid var(--rule); }
5
+ .sec h2 { font:400 10.5px/1.35 var(--mono); text-transform:uppercase; letter-spacing:.1em; color:var(--ink-3); margin:0; padding:12px 16px 8px; display:flex; align-items:center; gap:8px; cursor:pointer; user-select:none; flex:none; }
6
+ .sec h2:hover { color:var(--ink); }
7
+ .sec .caret { width:9px; font-size:9px; color:var(--ink-3); }
8
+ .sec h2 .h2act { margin-left:auto; border:0; background:none; color:var(--ink-3); font:inherit; font-size:12px; cursor:pointer; padding:0 2px; line-height:1; opacity:.6; }
9
+ .sec h2 .h2act:hover { opacity:1; color:var(--accent); }
10
+ .sec h2 .h2act.spin { animation: h2spin .6s linear; }
11
+ @keyframes h2spin { to { transform: rotate(360deg); } }
12
+ .sec .cnt { margin-left:auto; letter-spacing:0; text-transform:none; }
13
+ .sec .body { display:none; overflow:auto; min-height:0; padding:0 10px 10px 16px; }
14
+ .sec.open .body { display:block; }
15
+ .sec:not(.grow).open .body { max-height:22vh; }
16
+ .sec.grow.open { flex:1; }
17
+ .sec.grow.open .body { flex:1; }
18
+ #tree { font:400 12.5px/1.5 var(--mono); }
19
+ .node { user-select:none; }
20
+ .row { display:flex; align-items:center; gap:6px; padding:3px 6px 3px 8px; cursor:pointer; color:var(--ink-2); white-space:nowrap; margin-left:-1px; border-left:2px solid transparent; }
21
+ .row:hover { color:var(--ink); }
22
+ .row.dir { color:var(--ink); }
23
+ .row .tw { width:9px; color:var(--ink-3); font-size:9px; }
24
+ .row.active { color:var(--ink); font-weight:600; border-left-color:var(--accent); }
25
+ .row.gM > span:nth-child(2) { color:var(--amber); }
26
+ .row.gA > span:nth-child(2), .row.gU > span:nth-child(2) { color:var(--str); }
27
+ .row.gD > span:nth-child(2) { color:var(--rubric); text-decoration:line-through; }
28
+ .row .gs { margin-left:auto; font-size:10px; color:var(--ink-3); }
29
+ .kids { margin-left:12px; border-left:1px solid var(--rule); }
30
+ .kids.hidden { display:none; }
31
+ .s, .m, .p { display:flex; align-items:center; gap:8px; padding:4px 6px 4px 8px; cursor:pointer; color:var(--ink-2); font:400 12.5px/1.5 var(--mono); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-left:-1px; border-left:2px solid transparent; }
32
+ .s:hover, .m:hover, .p:hover { color:var(--ink); }
33
+ .s.active, .m.active, .p.active { color:var(--ink); border-left-color:var(--accent); }
34
+ .s .title { overflow:hidden; text-overflow:ellipsis; font-family:var(--serif); font-size:13px; }
35
+ .s .id { color:var(--ink-3); font-size:11px; flex:none; }
36
+ .m b { color:var(--ink); font-weight:600; flex:none; }
37
+ .m .t { overflow:hidden; text-overflow:ellipsis; font-family:var(--serif); font-size:13px; }
38
+ .p .st { width:7px; height:7px; border-radius:50%; background:var(--ink-3); flex:none; }
39
+ .p.run .st { background:var(--str); }
40
+ .p .nm { font-weight:600; color:var(--ink); }
41
+ .p .cm { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1; }
42
+ .p .port { color:var(--amber); font-weight:600; text-decoration:none; flex:none; }
43
+ .p .port:hover { text-decoration:underline; text-underline-offset:.18em; }
44
+ .p button { padding:1px 7px; font-size:10px; }
45
+ .none { color:var(--ink-3); cursor:default; }
46
+ .none:hover { color:var(--ink-3); }
47
+
48
+ /* filas de tareas programadas y aprobaciones */
49
+ .p.sched { flex-wrap:wrap; }
50
+ .p.sched .st.on { background:var(--accent); }
51
+ .p.sched .st.busy { background:var(--amber); animation: pulse 1s ease-in-out infinite; }
52
+ .p.sched .st.bad { background:var(--rubric); }
53
+ .p.sched .sub { flex-basis:100%; color:var(--ink-3); font-size:10.5px; padding-left:15px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
54
+ .p.sched .act { margin-left:auto; display:inline-flex; gap:2px; visibility:hidden; }
55
+ .p.sched:hover .act, .p.sched .act.ask { visibility:visible; }
56
+ .p.sched .act span { color:var(--ink-3); font-size:11px; padding:0 4px; cursor:pointer; }
57
+ .p.sched .act span:hover { color:var(--accent); }
58
+ .p.sched .act span.rm:hover { color:var(--rubric); }
59
+ .p.appr { flex-wrap:wrap; cursor:default; }
60
+ .p.appr .msg { flex-basis:100%; color:var(--ink); font-size:11.5px; white-space:pre-wrap; padding-left:15px; }
61
+ .p.appr .why { flex-basis:100%; color:var(--rubric); font-size:10.5px; padding-left:15px; }
62
+ .p.appr .btns { flex-basis:100%; display:flex; gap:6px; padding:4px 0 2px 15px; }
63
+ .p.appr .btns button { padding:2px 9px; font-size:11px; }
64
+ .sec .daemon { font:400 10.5px/1.5 var(--mono); color:var(--ink-3); padding:2px 0 4px; }
65
+ .sec .daemon.off { color:var(--rubric); }
66
+ @keyframes pulse { 0%,100% { opacity:1 } 50% { opacity:.35 } }
67
+
68
+ .s.more { color:var(--accent); }
69
+
70
+ /* ✕ → «¿borrar? sí · no» en línea */
71
+ .s .del { margin-left:auto; color:var(--ink-3); visibility:hidden; font-size:11px; padding:0 4px; }
72
+ .s:hover .del { visibility:visible; }
73
+ .s .del:hover { color:var(--rubric); }
74
+ .p .del { margin-left:auto; color:var(--ink-3); visibility:hidden; font-size:11px; padding:0 4px; cursor:pointer; }
75
+ .p:hover .del { visibility:visible; }
76
+ .p .del:hover { color:var(--rubric); }
77
+ .del.ask, .s:hover .del.ask { visibility:visible; color:var(--ink-2); font:400 11px/1.4 var(--mono); white-space:nowrap; }
78
+ .del.ask .yes { color:var(--rubric); cursor:pointer; font-weight:600; }
79
+ .del.ask .no { color:var(--ink-2); cursor:pointer; }
80
+ .del.ask .yes:hover, .del.ask .no:hover { text-decoration:underline; text-underline-offset:.18em; }