lampson 0.1.2 → 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/.env.example +9 -0
- package/README.md +60 -2
- package/chat.syn +264 -9
- package/lampson.ps1 +17 -3
- package/lampson.sh +17 -3
- package/lib/agents.syn +1 -1
- package/lib/approvals.syn +176 -0
- package/lib/diff.syn +37 -11
- package/lib/line.syn +13 -8
- package/lib/loop.syn +25 -11
- package/lib/md.syn +76 -18
- package/lib/permission.syn +18 -0
- package/lib/prompt.syn +1 -1
- package/lib/sched_run.syn +161 -0
- package/lib/schedule.syn +660 -0
- package/lib/session.syn +38 -1
- package/lib/settings.syn +55 -0
- package/lib/tools.syn +94 -2
- package/lib/ui.syn +161 -0
- package/package.json +1 -1
- package/public/css/chat.css +56 -0
- package/public/css/layout.css +97 -0
- package/public/css/panel.css +125 -0
- package/public/css/sidebar.css +80 -0
- package/public/css/tokens.css +57 -0
- package/public/index.html +49 -1187
- package/public/js/agents.js +31 -0
- package/public/js/app.js +21 -0
- package/public/js/approvals.js +26 -0
- package/public/js/chat.js +92 -0
- package/public/js/config.js +98 -0
- package/public/js/core.js +88 -0
- package/public/js/events.js +31 -0
- package/public/js/lamps.js +112 -0
- package/public/js/lsp.js +81 -0
- package/public/js/mcp.js +74 -0
- package/public/js/memory.js +17 -0
- package/public/js/panel.js +101 -0
- package/public/js/procs.js +45 -0
- package/public/js/schedules.js +118 -0
- package/public/js/sessions.js +69 -0
- package/public/js/sidebar.js +33 -0
- package/public/js/terminal.js +49 -0
- package/public/js/theme.js +6 -0
- package/public/js/todo.js +10 -0
- package/public/js/tree.js +53 -0
- package/public/js/update.js +14 -0
- package/skills/lampson/SKILL.md +16 -0
- package/web.syn +106 -15
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
|
-
|
|
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/lib/ui.syn
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
-- lib/ui.syn — una sola paleta y medidas de texto para la terminal (md.syn, diff.syn, line.syn, chat.syn)
|
|
2
|
+
-- Colores: dim/blue/green/red/yellow/cyan/bold/inv (+ fondos bg_add/bg_del para diffs). color=false → texto plano.
|
|
3
|
+
-- width(s) ancho visible (sin escapes; emoji/CJK cuentan 2)
|
|
4
|
+
-- cut(s, max) corta a `max` columnas respetando escapes (y cierra el color)
|
|
5
|
+
-- wrap(s, max, indent) envuelve texto PLANO por palabras; las líneas siguientes llevan `indent`
|
|
6
|
+
-- cols() columnas de la terminal (el editor las publica en el blackboard "lampson:ui:cols"; 100 si no hay)
|
|
7
|
+
-- fmt_duration(secs) → "12s" · "2m 27s" · "1h 03m"
|
|
8
|
+
|
|
9
|
+
let ESC be decode(bytes("1b", "hex"))
|
|
10
|
+
|
|
11
|
+
export task sgr(color, code, s)
|
|
12
|
+
when not color or s == ""
|
|
13
|
+
give s
|
|
14
|
+
give ESC + "[" + code + "m" + s + ESC + "[0m"
|
|
15
|
+
|
|
16
|
+
export task dim(color, s)
|
|
17
|
+
give sgr(color, "2", s)
|
|
18
|
+
export task bold(color, s)
|
|
19
|
+
give sgr(color, "1", s)
|
|
20
|
+
export task inv(color, s)
|
|
21
|
+
give sgr(color, "7", s)
|
|
22
|
+
export task blue(color, s)
|
|
23
|
+
give sgr(color, "34", s)
|
|
24
|
+
export task cyan(color, s)
|
|
25
|
+
give sgr(color, "36", s)
|
|
26
|
+
export task green(color, s)
|
|
27
|
+
give sgr(color, "32", s)
|
|
28
|
+
export task red(color, s)
|
|
29
|
+
give sgr(color, "31", s)
|
|
30
|
+
export task yellow(color, s)
|
|
31
|
+
give sgr(color, "33", s)
|
|
32
|
+
-- fondos para diffs (256 colores: verde y rojo oscuros, legibles en tema claro y oscuro)
|
|
33
|
+
export task bg_add(color, s)
|
|
34
|
+
give sgr(color, "48;5;22", s)
|
|
35
|
+
export task bg_del(color, s)
|
|
36
|
+
give sgr(color, "48;5;52", s)
|
|
37
|
+
|
|
38
|
+
-- ---------- medidas ----------
|
|
39
|
+
|
|
40
|
+
-- punto de código de UN carácter a partir de sus bytes UTF-8
|
|
41
|
+
task codepoint(ch)
|
|
42
|
+
let b be bytes(ch)
|
|
43
|
+
let n be length(b)
|
|
44
|
+
when n == 1
|
|
45
|
+
give b[0]
|
|
46
|
+
when n == 2
|
|
47
|
+
give (b[0] - 192) * 64 + (b[1] - 128)
|
|
48
|
+
when n == 3
|
|
49
|
+
give (b[0] - 224) * 4096 + (b[1] - 128) * 64 + (b[2] - 128)
|
|
50
|
+
give (b[0] - 240) * 262144 + (b[1] - 128) * 4096 + (b[2] - 128) * 64 + (b[3] - 128)
|
|
51
|
+
|
|
52
|
+
-- ancho de un carácter en celdas: emoji, CJK y símbolos "wide" ocupan 2
|
|
53
|
+
task char_width(ch)
|
|
54
|
+
let cp be codepoint(ch)
|
|
55
|
+
when cp < 4352
|
|
56
|
+
give 1
|
|
57
|
+
when cp >= 127744 and cp <= 129791
|
|
58
|
+
give 2
|
|
59
|
+
when cp >= 4352 and cp <= 4447
|
|
60
|
+
give 2
|
|
61
|
+
when cp >= 11904 and cp <= 42191
|
|
62
|
+
give 2
|
|
63
|
+
when cp >= 44032 and cp <= 55203
|
|
64
|
+
give 2
|
|
65
|
+
when cp >= 63744 and cp <= 64255
|
|
66
|
+
give 2
|
|
67
|
+
when cp >= 65040 and cp <= 65135
|
|
68
|
+
give 2
|
|
69
|
+
when cp >= 65280 and cp <= 65376
|
|
70
|
+
give 2
|
|
71
|
+
when cp >= 9800 and cp <= 9811
|
|
72
|
+
give 2
|
|
73
|
+
when cp == 9888 or cp == 9889 or cp == 9989 or cp == 10060 or cp == 11093
|
|
74
|
+
give 2
|
|
75
|
+
give 1
|
|
76
|
+
|
|
77
|
+
export task width(s)
|
|
78
|
+
let t be strip_ansi(s)
|
|
79
|
+
let w be 0
|
|
80
|
+
let i be 0
|
|
81
|
+
let n be length(t)
|
|
82
|
+
while i < n
|
|
83
|
+
set w to w + char_width(slice(t, i, i + 1))
|
|
84
|
+
set i to i + 1
|
|
85
|
+
give w
|
|
86
|
+
|
|
87
|
+
-- corta a `max` celdas visibles, saltando secuencias ANSI enteras; añade "…" si cortó y cierra el color
|
|
88
|
+
export task cut(s, max)
|
|
89
|
+
when width(s) <= max
|
|
90
|
+
give s
|
|
91
|
+
let out be ""
|
|
92
|
+
let w be 0
|
|
93
|
+
let i be 0
|
|
94
|
+
let n be length(s)
|
|
95
|
+
let limit be max - 1
|
|
96
|
+
while i < n and w < limit
|
|
97
|
+
let ch be slice(s, i, i + 1)
|
|
98
|
+
when ch == ESC
|
|
99
|
+
-- copiar la secuencia CSI completa: ESC [ … letra
|
|
100
|
+
let j be i + 1
|
|
101
|
+
while j < n and not matches(slice(s, j, j + 1), "[A-Za-z]")
|
|
102
|
+
set j to j + 1
|
|
103
|
+
set out to out + slice(s, i, j + 1)
|
|
104
|
+
set i to j + 1
|
|
105
|
+
otherwise
|
|
106
|
+
let cw be char_width(ch)
|
|
107
|
+
when w + cw <= limit
|
|
108
|
+
set out to out + ch
|
|
109
|
+
set w to w + cw
|
|
110
|
+
set i to i + 1
|
|
111
|
+
otherwise
|
|
112
|
+
set i to n
|
|
113
|
+
give out + ESC + "[0m" + "…"
|
|
114
|
+
|
|
115
|
+
-- envuelve texto plano (sin escapes) por palabras; give lista de líneas (sin indent en la primera)
|
|
116
|
+
export task wrap(s, max, indent)
|
|
117
|
+
when max <= 10 or length(s) <= max
|
|
118
|
+
give [s]
|
|
119
|
+
let words be split(s, " ")
|
|
120
|
+
let lines be []
|
|
121
|
+
let cur be ""
|
|
122
|
+
let curw be 0
|
|
123
|
+
each w in words
|
|
124
|
+
let ww be width(w)
|
|
125
|
+
let room be when length(lines) == 0 then max otherwise max - length(indent)
|
|
126
|
+
when curw == 0
|
|
127
|
+
set cur to w
|
|
128
|
+
set curw to ww
|
|
129
|
+
otherwise when curw + 1 + ww <= room
|
|
130
|
+
set cur to cur + " " + w
|
|
131
|
+
set curw to curw + 1 + ww
|
|
132
|
+
otherwise
|
|
133
|
+
set lines to append(lines, (when length(lines) == 0 then "" otherwise indent) + cur)
|
|
134
|
+
set cur to w
|
|
135
|
+
set curw to ww
|
|
136
|
+
set lines to append(lines, (when length(lines) == 0 then "" otherwise indent) + cur)
|
|
137
|
+
give lines
|
|
138
|
+
|
|
139
|
+
-- ---------- terminal ----------
|
|
140
|
+
|
|
141
|
+
export task cols()
|
|
142
|
+
observe "lampson:ui:cols" as c
|
|
143
|
+
when c == nothing
|
|
144
|
+
give 100
|
|
145
|
+
give c
|
|
146
|
+
|
|
147
|
+
export task set_cols(n)
|
|
148
|
+
share n as "lampson:ui:cols"
|
|
149
|
+
give n
|
|
150
|
+
|
|
151
|
+
export task fmt_duration(secs)
|
|
152
|
+
let s be floor(secs)
|
|
153
|
+
when s < 60
|
|
154
|
+
give text(s) + "s"
|
|
155
|
+
when s < 3600
|
|
156
|
+
let m be floor(s / 60)
|
|
157
|
+
let r be s - m * 60
|
|
158
|
+
give text(m) + "m " + (when r < 10 then "0" otherwise "") + text(r) + "s"
|
|
159
|
+
let h be floor(s / 3600)
|
|
160
|
+
let m2 be floor((s - h * 3600) / 60)
|
|
161
|
+
give text(h) + "h " + (when m2 < 10 then "0" otherwise "") + text(m2) + "m"
|
package/package.json
CHANGED
|
@@ -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; } }
|