lampson 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/chat.syn +141 -9
- 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/prompt.syn +1 -1
- package/lib/ui.syn +161 -0
- package/package.json +1 -1
- package/public/index.html +11 -1
package/README.md
CHANGED
|
@@ -139,6 +139,10 @@ servers, flags), `↑↓` browse history or the menu, `Alt+Enter` inserts a newl
|
|
|
139
139
|
last tool result in full, `Ctrl+U`/`Ctrl+W` clear the line/word, `Esc` closes the menu. Approvals are
|
|
140
140
|
an arrow-key menu (`permitir`/`denegar`, or `p`/`d`).
|
|
141
141
|
|
|
142
|
+
While the model thinks or a slow tool runs (a long `bash`, a sub-agent), a status line shows what is
|
|
143
|
+
running, the elapsed time and a bar that fills as you wait (`⠋ pensando ▰▰▱▱▱▱▱▱▱▱ 12s`); it appears
|
|
144
|
+
after 0.4 s so instant tools do not flicker, and anything you type meanwhile lands in the prompt.
|
|
145
|
+
|
|
142
146
|
The terminal renders the model's markdown (headings, lists, tables, code fences) and shows every tool
|
|
143
147
|
result: `edit`/`write` print a line diff (`- red / + green`, line numbers, 2 lines of context); other
|
|
144
148
|
tools are collapsed to 15 lines. `/out [n]` prints the n-th last result of the turn in full and
|
|
@@ -299,7 +303,7 @@ Borrowed from the harness that does each part best (see `notes/*.md`):
|
|
|
299
303
|
| Budget runs out silently | 80 %: a notice appended to the latest tool result (no new user message, cache stays warm); 95 %: last step without tools, summary required | hermes / opencode |
|
|
300
304
|
| Edits a file it never read, or one that changed | **Observation gate in code**: `read` records the file hash; `edit`/`write` on an existing file are rejected without it, or if the file changed since | deepseek |
|
|
301
305
|
| Loses the plan | `todo` tool (whole-list replacement, one `in_progress` at a time, scoped to the session like the three references); re-injected only after context compaction, active items only | hermes, opencode |
|
|
302
|
-
| Reads the whole project before touching anything | **Exploration cap** (ours): after
|
|
306
|
+
| Reads the whole project before touching anything | **Exploration cap** (ours): after 12 read-only calls in a row (read/ls/find/grep) without an edit/write/command the result carries a warning; after 24 they are refused until it acts (`LAMPSON_EXPLORE_CAP`) | — |
|
|
303
307
|
|
|
304
308
|
### Sub-agents
|
|
305
309
|
|
package/chat.syn
CHANGED
|
@@ -46,6 +46,7 @@ use "./lib/trace.syn" as trace
|
|
|
46
46
|
use "./lib/md.syn" as md
|
|
47
47
|
use "./lib/diff.syn" as diff
|
|
48
48
|
use "./lib/line.syn" as ed
|
|
49
|
+
use "./lib/ui.syn" as ui
|
|
49
50
|
use "./lib/mcp.syn" as mcp
|
|
50
51
|
use "./lib/lamps.syn" as lamps
|
|
51
52
|
use "./lib/lsp.syn" as lsp
|
|
@@ -107,6 +108,15 @@ task summarize(name, out)
|
|
|
107
108
|
set used to used + length(l) + 2
|
|
108
109
|
let rest be length(lines) - length(shown)
|
|
109
110
|
give text(length(lines)) + (when name == "ls" then " entradas: " otherwise " archivos: ") + join(shown, " ") + (when rest > 0 then dim(" +" + text(rest)) otherwise "")
|
|
111
|
+
when name == "delegate" and not VERBOSE
|
|
112
|
+
-- una línea por subagente: el encabezado [id · agente · estado · pasos · tokens · log]
|
|
113
|
+
let heads be []
|
|
114
|
+
each l in lines
|
|
115
|
+
when starts_with(trim(l), "[") and contains(l, " steps") or (starts_with(trim(l), "[") and contains(l, " · log:"))
|
|
116
|
+
set heads to append(heads, trim(l))
|
|
117
|
+
when length(heads) == 0
|
|
118
|
+
give first_line(out, 120)
|
|
119
|
+
give join(heads, "\n ")
|
|
110
120
|
when name == "grep" and not VERBOSE
|
|
111
121
|
let files be {}
|
|
112
122
|
each l in lines
|
|
@@ -136,11 +146,21 @@ task diff_of(name, out)
|
|
|
136
146
|
give diff.diff(d["old"], d["new"], 2, COLOR)
|
|
137
147
|
|
|
138
148
|
task print_diff(path, d)
|
|
139
|
-
|
|
140
|
-
|
|
149
|
+
print_diff_with(" ", path, d)
|
|
150
|
+
|
|
151
|
+
-- lead = lo que va antes del ✓ (la llamada, si se fusionó en la misma línea)
|
|
152
|
+
task print_diff_with(lead, path, d)
|
|
153
|
+
let head be green("✓") + " " + c("34", path) + " " + green("+" + text(d["added"])) + " " + red("−" + text(d["removed"]))
|
|
154
|
+
print(lead + head)
|
|
141
155
|
each l in d["lines"]
|
|
142
156
|
print(" " + l)
|
|
143
157
|
|
|
158
|
+
-- tools "instantáneas": la llamada se imprime junto con su resultado, en una sola línea
|
|
159
|
+
task is_instant(name)
|
|
160
|
+
give name == "read" or name == "ls" or name == "find" or name == "grep" or name == "memory" or name == "edit" or name == "write" or name == "todo" or name == "lsp"
|
|
161
|
+
|
|
162
|
+
let pending_call be ""
|
|
163
|
+
|
|
144
164
|
-- /out: resultado completo de una tool del turno
|
|
145
165
|
task print_full(o)
|
|
146
166
|
when o["diff"] != nothing
|
|
@@ -149,17 +169,110 @@ task print_full(o)
|
|
|
149
169
|
each l in split(o["output"], "\n")
|
|
150
170
|
print(" " + l)
|
|
151
171
|
|
|
172
|
+
-- ---------- indicador de espera ----------
|
|
173
|
+
-- El intérprete principal se bloquea en la llamada al modelo y en cada tool; el que dibuja es un agente
|
|
174
|
+
-- (hilo real) que mira el blackboard: `lampson:busy` = {label, kind, since} mientras hay algo en curso.
|
|
175
|
+
-- Arranca a dibujar recién a los 0,4 s (las tools instantáneas no parpadean) y suelta la terminal
|
|
176
|
+
-- (`lampson:spinner:holding` = false) antes de que el principal vuelva a imprimir.
|
|
177
|
+
agent Spinner
|
|
178
|
+
require time
|
|
179
|
+
let frames be ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
180
|
+
let i be 0
|
|
181
|
+
let drawn be false
|
|
182
|
+
let run be true
|
|
183
|
+
-- MIGA: un agente NO puede abrir la terminal (term_open → nothing) pero sí print+flush. Cada print de un
|
|
184
|
+
-- agente sale con el prefijo "[Spinner_0] " en la línea de abajo: cada frame limpia esa línea (\r ESC[K),
|
|
185
|
+
-- sube una (ESC[1A) y redibuja la suya. Al terminar deja el cursor en la línea del spinner, vacía.
|
|
186
|
+
let clear_here be "\r" + esc + "[K"
|
|
187
|
+
let up be esc + "[1A"
|
|
188
|
+
while run
|
|
189
|
+
observe "lampson:spinner:stop" as st
|
|
190
|
+
observe "lampson:busy" as b
|
|
191
|
+
when st == true
|
|
192
|
+
set run to false
|
|
193
|
+
otherwise when b == nothing
|
|
194
|
+
when drawn
|
|
195
|
+
print(clear_here + up + clear_here + up)
|
|
196
|
+
flush()
|
|
197
|
+
set drawn to false
|
|
198
|
+
share false as "lampson:spinner:holding"
|
|
199
|
+
sleep(0.05)
|
|
200
|
+
otherwise
|
|
201
|
+
let el be now() - b["since"]
|
|
202
|
+
when el >= 0.4
|
|
203
|
+
-- barra de "costo hundido": se llena rápido al principio y cada vez más despacio
|
|
204
|
+
let fill be floor(10 * (1 - exp(0 - el / 40)))
|
|
205
|
+
let bar be ""
|
|
206
|
+
let k be 0
|
|
207
|
+
while k < 10
|
|
208
|
+
set bar to bar + (when k < fill then "▰" otherwise "▱")
|
|
209
|
+
set k to k + 1
|
|
210
|
+
let hint be when el > 90 then " esto está tardando (Ctrl+C corta)" otherwise (when el > 30 then " ya casi" otherwise (when el > 8 then " sigue trabajando" otherwise ""))
|
|
211
|
+
let secs be text(floor(el)) + "s"
|
|
212
|
+
let label be b["label"]
|
|
213
|
+
when length(label) > 60
|
|
214
|
+
set label to slice(label, 0, 59) + "…"
|
|
215
|
+
let frame be frames[i % 10]
|
|
216
|
+
let body be when color then esc + "[36m" + frame + esc + "[0m " + label + " " + esc + "[2m" + bar + " " + secs + hint + esc + "[0m" otherwise frame + " " + label + " " + bar + " " + secs + hint
|
|
217
|
+
when drawn
|
|
218
|
+
print(clear_here + up + clear_here + " " + body)
|
|
219
|
+
otherwise
|
|
220
|
+
share true as "lampson:spinner:holding"
|
|
221
|
+
print(clear_here + " " + body)
|
|
222
|
+
set drawn to true
|
|
223
|
+
flush()
|
|
224
|
+
set i to i + 1
|
|
225
|
+
sleep(0.08)
|
|
226
|
+
when drawn
|
|
227
|
+
print(clear_here + up + clear_here + up)
|
|
228
|
+
flush()
|
|
229
|
+
share false as "lampson:spinner:holding"
|
|
230
|
+
|
|
231
|
+
task busy_on(label, kind)
|
|
232
|
+
share {"label": label, "kind": kind, "since": now()} as "lampson:busy"
|
|
233
|
+
|
|
234
|
+
-- apagar y esperar a que el agente suelte la terminal (máx. 1 s) antes de volver a imprimir
|
|
235
|
+
task busy_off()
|
|
236
|
+
share nothing as "lampson:busy"
|
|
237
|
+
let waited be 0
|
|
238
|
+
let holding be true
|
|
239
|
+
while holding and waited < 100
|
|
240
|
+
observe "lampson:spinner:holding" as hd
|
|
241
|
+
set holding to hd == true
|
|
242
|
+
when holding
|
|
243
|
+
sleep(0.01)
|
|
244
|
+
set waited to waited + 1
|
|
245
|
+
|
|
152
246
|
-- tag = "" para el agente principal; los subagentes en background NO pasan por acá (escriben su log)
|
|
153
247
|
task on_event(kind, data, tag)
|
|
154
248
|
trace.event(sid, kind, data, tag)
|
|
155
|
-
when kind == "
|
|
249
|
+
when kind == "busy"
|
|
250
|
+
busy_on(data["label"], data["kind"])
|
|
251
|
+
otherwise when kind == "idle"
|
|
252
|
+
busy_off()
|
|
253
|
+
otherwise when kind == "assistant"
|
|
254
|
+
-- ● marca el turno del asistente (primera línea con contenido)
|
|
255
|
+
let lines be split(md.render(data, COLOR), "\n")
|
|
256
|
+
let marked be false
|
|
257
|
+
let out_lines be []
|
|
258
|
+
each l in lines
|
|
259
|
+
when not marked and trim(l) != "" and starts_with(l, " ")
|
|
260
|
+
set out_lines to append(out_lines, c("36;1", "● ") + slice(l, 2, length(l)))
|
|
261
|
+
set marked to true
|
|
262
|
+
otherwise
|
|
263
|
+
set out_lines to append(out_lines, l)
|
|
156
264
|
print("")
|
|
157
|
-
print(
|
|
265
|
+
print(join(out_lines, "\n"))
|
|
158
266
|
print("")
|
|
159
267
|
otherwise when kind == "inbox"
|
|
160
268
|
print(" " + cyan("✉ " + first_line(data, 140)))
|
|
161
269
|
otherwise when kind == "tool_call"
|
|
162
|
-
|
|
270
|
+
let desc be permission.describe_call(data["name"], data["args"])
|
|
271
|
+
when is_instant(data["name"])
|
|
272
|
+
set pending_call to desc
|
|
273
|
+
otherwise
|
|
274
|
+
set pending_call to ""
|
|
275
|
+
print(" " + yellow("▸ " + desc))
|
|
163
276
|
otherwise when kind == "tool_result"
|
|
164
277
|
let out be data["output"]
|
|
165
278
|
let name be data["call"]["name"]
|
|
@@ -167,11 +280,17 @@ task on_event(kind, data, tag)
|
|
|
167
280
|
let d be diff_of(name, out)
|
|
168
281
|
let path be when d != nothing then text(data["call"]["args"]["path"]) otherwise ""
|
|
169
282
|
set turn_outputs to append(turn_outputs, {"name": name, "args": data["call"]["args"], "output": out, "diff": d, "path": path})
|
|
283
|
+
let lead be when pending_call != "" then " " + yellow("▸ " + pending_call) + " " otherwise " "
|
|
284
|
+
set pending_call to ""
|
|
170
285
|
when d != nothing
|
|
171
|
-
|
|
286
|
+
print_diff_with(lead, path, d)
|
|
172
287
|
otherwise
|
|
173
288
|
let mark be when bad then red("✗") otherwise green("✓")
|
|
174
|
-
|
|
289
|
+
let s be summarize(name, out)
|
|
290
|
+
-- una sola línea → cortar al ancho; multilínea (bash) se deja
|
|
291
|
+
when not contains(s, "\n")
|
|
292
|
+
set s to ui.cut(s, ui.cols() - ui.width(lead) - 4)
|
|
293
|
+
print(lead + mark + " " + (when bad then red(s) otherwise dim(s)))
|
|
175
294
|
otherwise when kind == "tool_denied"
|
|
176
295
|
set data to data
|
|
177
296
|
otherwise when kind == "error"
|
|
@@ -180,11 +299,13 @@ task on_event(kind, data, tag)
|
|
|
180
299
|
print(" " + yellow("el proveedor rechazó el nombre del modelo (" + cfg["model"] + "): /model sin argumentos lista los válidos, /model <nombre> lo cambia"))
|
|
181
300
|
otherwise when kind == "compact"
|
|
182
301
|
print(" " + dim("⧗ compactando contexto (~" + text(data["before"]) + " tokens)"))
|
|
302
|
+
busy_on("compactando contexto", "llm")
|
|
183
303
|
flush()
|
|
184
304
|
|
|
185
305
|
-- Human in the loop con el `approve` nativo de Synsema: prompt [approve] … (y/n) en TTY; sin TTY deniega
|
|
186
306
|
-- (fail-closed, un agente no puede auto-aprobarse); `within` acota la espera.
|
|
187
307
|
task ask_user(name, args, why)
|
|
308
|
+
busy_off()
|
|
188
309
|
print("")
|
|
189
310
|
print(" " + yellow("⚠ requiere tu aprobación · " + why))
|
|
190
311
|
print(" " + permission.describe_call(name, args))
|
|
@@ -641,6 +762,9 @@ when sid == ""
|
|
|
641
762
|
share {"id": sid} as "lampson:session"
|
|
642
763
|
|
|
643
764
|
banner(env_info["cwd"], cfg, profile, opts["permission_mode"], sid)
|
|
765
|
+
share nothing as "lampson:busy"
|
|
766
|
+
share false as "lampson:spinner:stop"
|
|
767
|
+
spawn Spinner with esc = ESC, color = COLOR
|
|
644
768
|
flush()
|
|
645
769
|
|
|
646
770
|
let total_usage be {"input": 0, "output": 0}
|
|
@@ -1005,23 +1129,31 @@ while running
|
|
|
1005
1129
|
-- catálogo fresco por turno: si el turno anterior conectó/quitó un server MCP, sus tools
|
|
1006
1130
|
-- entran/salen acá (con los mismos servers el catálogo es idéntico → el prompt cache no se corta)
|
|
1007
1131
|
set opts to opts_for(profile, mode)
|
|
1132
|
+
let t0 be now()
|
|
1008
1133
|
let result be loop.run_turn(cfg, messages, opts, on_event)
|
|
1009
1134
|
trace.turn_end(sid, result)
|
|
1010
1135
|
set messages to result["messages"]
|
|
1011
1136
|
set total_usage to {"input": total_usage["input"] + result["usage"]["input"], "output": total_usage["output"] + result["usage"]["output"]}
|
|
1012
|
-
let summary be text(result["steps"]) + (when result["steps"] == 1 then " paso" otherwise " pasos") + " · " + fmt_tokens(result["usage"]["input"] + result["usage"]["output"]) + " tokens"
|
|
1137
|
+
let summary be text(result["steps"]) + (when result["steps"] == 1 then " paso" otherwise " pasos") + " · " + fmt_tokens(result["usage"]["input"] + result["usage"]["output"]) + " tokens · " + ui.fmt_duration(now() - t0)
|
|
1013
1138
|
when result["stopped"] == "max_steps"
|
|
1014
1139
|
set summary to summary + " · " + red("límite de " + text(opts["max_steps"]) + " pasos por turno")
|
|
1015
1140
|
print(dim(" ─── " + summary))
|
|
1016
1141
|
print(" " + yellow("El agente paró por el límite de pasos, no porque terminó. Escribí «seguí» para que continúe donde quedó, o subí LAMPSON_MAX_STEPS."))
|
|
1017
1142
|
otherwise when result["stopped"] != "done"
|
|
1018
|
-
|
|
1143
|
+
-- el detalle del error ya salió en la línea ‼; acá solo la causa corta
|
|
1144
|
+
let why be result["stopped"]
|
|
1145
|
+
when starts_with(why, "error: network")
|
|
1146
|
+
set why to "error de red (ver ‼ arriba)"
|
|
1147
|
+
otherwise when starts_with(why, "error:")
|
|
1148
|
+
set why to first_line(why, 60)
|
|
1149
|
+
set summary to summary + " · " + red("detenido: " + why)
|
|
1019
1150
|
print(dim(" ─── " + summary))
|
|
1020
1151
|
otherwise
|
|
1021
1152
|
print(dim(" ─── " + summary))
|
|
1022
1153
|
session.save(sid, messages, {"title": session.title_of(messages)})
|
|
1023
1154
|
flush()
|
|
1024
1155
|
-- los procesos gestionados (servidores) y los subagentes en background mueren con lampson
|
|
1156
|
+
share true as "lampson:spinner:stop"
|
|
1025
1157
|
agents.stop_all()
|
|
1026
1158
|
mcp.stop_all()
|
|
1027
1159
|
lsp.stop_all()
|
package/lib/diff.syn
CHANGED
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
-- lib/diff.syn — diff de líneas para mostrar en la terminal qué cambió edit/write
|
|
2
2
|
-- diff(old, new, ctx, color) → {"added": n, "removed": m, "lines": [texto ya formateado…]}
|
|
3
3
|
-- Algoritmo: se recorta el prefijo y sufijo comunes (una edición es local) y se hace LCS solo sobre el
|
|
4
|
-
-- medio. Si el medio
|
|
4
|
+
-- medio. Si el medio supera LIMIT² celdas (80×80 líneas cambiadas ≈ un rewrite) se muestra como bloque
|
|
5
|
+
-- quitado + bloque agregado: exacto y en milisegundos (el LCS interpretado a 300×300 tardaba 2 minutos).
|
|
6
|
+
|
|
7
|
+
use "./ui.syn" as ui
|
|
5
8
|
|
|
6
9
|
let ESC be decode(bytes("1b", "hex"))
|
|
7
|
-
let LIMIT be
|
|
10
|
+
let LIMIT be 80
|
|
11
|
+
|
|
12
|
+
-- línea de diff con fondo (verde/rojo) rellenada hasta el ancho útil, cortada si se pasa
|
|
13
|
+
task row(color, kind, num, body, width)
|
|
14
|
+
let n be sgr(color, "2", lpad(num, width) + " │ ")
|
|
15
|
+
let w be ui.cols() - 8 - width - 3
|
|
16
|
+
when w < 30
|
|
17
|
+
set w to 30
|
|
18
|
+
let txt be ui.cut(kind + " " + body, w)
|
|
19
|
+
let pad be rep(" ", w - ui.width(txt))
|
|
20
|
+
when kind == "+"
|
|
21
|
+
give n + ui.bg_add(color, sgr(color, "32", txt + pad))
|
|
22
|
+
when kind == "-"
|
|
23
|
+
give n + ui.bg_del(color, sgr(color, "31", txt + pad))
|
|
24
|
+
give n + sgr(color, "2", ui.cut(" " + body, w))
|
|
25
|
+
|
|
26
|
+
task rep(ch, n)
|
|
27
|
+
let out be ""
|
|
28
|
+
while length(out) < n
|
|
29
|
+
set out to out + ch
|
|
30
|
+
give out
|
|
8
31
|
|
|
9
32
|
task sgr(color, code, s)
|
|
10
33
|
when not color
|
|
@@ -22,10 +45,13 @@ task lcs_ops(a, b)
|
|
|
22
45
|
let n be length(a)
|
|
23
46
|
let m be length(b)
|
|
24
47
|
-- tabla (n+1)×(m+1) aplanada, fila por fila
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
48
|
+
-- MIGA: append copia la lista entera (O(n) cada vez) → construirla con append era O(n²·m²).
|
|
49
|
+
-- range() la crea de una y set por índice es O(1).
|
|
50
|
+
let table be range(0, (n + 1) * (m + 1))
|
|
51
|
+
let z be 0
|
|
52
|
+
while z < (n + 1) * (m + 1)
|
|
53
|
+
set table[z] to 0
|
|
54
|
+
set z to z + 1
|
|
29
55
|
let i be n - 1
|
|
30
56
|
while i >= 0
|
|
31
57
|
let j be m - 1
|
|
@@ -98,7 +124,7 @@ export task diff(old, new, ctx, color)
|
|
|
98
124
|
set lines to append(lines, sgr(color, "2", lpad("", width) + " ⋯"))
|
|
99
125
|
let k be start
|
|
100
126
|
while k < pre
|
|
101
|
-
set lines to append(lines,
|
|
127
|
+
set lines to append(lines, row(color, " ", text(k + 1), a[k], width))
|
|
102
128
|
set k to k + 1
|
|
103
129
|
-- medio: contexto interno acotado (ctx a cada lado de un cambio)
|
|
104
130
|
let last_change be -1
|
|
@@ -118,16 +144,16 @@ export task diff(old, new, ctx, color)
|
|
|
118
144
|
set w to w + 1
|
|
119
145
|
when o["op"] == " "
|
|
120
146
|
when near
|
|
121
|
-
set lines to append(lines,
|
|
147
|
+
set lines to append(lines, row(color, " ", text(pre + o["b"] + 1), mid_b[o["b"]], width))
|
|
122
148
|
set skipping to false
|
|
123
149
|
otherwise when not skipping
|
|
124
150
|
set lines to append(lines, sgr(color, "2", lpad("", width) + " ⋯"))
|
|
125
151
|
set skipping to true
|
|
126
152
|
otherwise when o["op"] == "-"
|
|
127
|
-
set lines to append(lines,
|
|
153
|
+
set lines to append(lines, row(color, "-", text(pre + o["a"] + 1), mid_a[o["a"]], width))
|
|
128
154
|
set skipping to false
|
|
129
155
|
otherwise
|
|
130
|
-
set lines to append(lines,
|
|
156
|
+
set lines to append(lines, row(color, "+", text(pre + o["b"] + 1), mid_b[o["b"]], width))
|
|
131
157
|
set skipping to false
|
|
132
158
|
set idx to idx + 1
|
|
133
159
|
-- contexto posterior (del sufijo común)
|
|
@@ -135,7 +161,7 @@ export task diff(old, new, ctx, color)
|
|
|
135
161
|
let upto be when after_start + ctx < length(b) then after_start + ctx otherwise length(b)
|
|
136
162
|
set k to after_start
|
|
137
163
|
while k < upto
|
|
138
|
-
set lines to append(lines,
|
|
164
|
+
set lines to append(lines, row(color, " ", text(k + 1), b[k], width))
|
|
139
165
|
set k to k + 1
|
|
140
166
|
when upto < length(b)
|
|
141
167
|
set lines to append(lines, sgr(color, "2", lpad("", width) + " ⋯"))
|
package/lib/line.syn
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
-- devuelva ctx["complete"](cmd, prefijo) (archivos, servers, flags…). Máx MENU_ROWS filas.
|
|
9
9
|
-- MIGA: dibujar SIEMPRE con term_write (print queda buffereado). El runtime restaura la terminal al cerrar.
|
|
10
10
|
|
|
11
|
+
use "./ui.syn" as ui
|
|
12
|
+
|
|
11
13
|
let ESC be decode(bytes("1b", "hex"))
|
|
12
14
|
let MENU_ROWS be 8
|
|
13
15
|
export let INTERRUPT be ESC + "interrupt"
|
|
@@ -18,7 +20,7 @@ task sgr(color, code, s)
|
|
|
18
20
|
give ESC + "[" + code + "m" + s + ESC + "[0m"
|
|
19
21
|
|
|
20
22
|
task vis(s)
|
|
21
|
-
give
|
|
23
|
+
give ui.width(s)
|
|
22
24
|
|
|
23
25
|
task rep(ch, n)
|
|
24
26
|
let out be ""
|
|
@@ -102,13 +104,14 @@ task draw(h, st, color, ctx)
|
|
|
102
104
|
set i to 0
|
|
103
105
|
each l in lines
|
|
104
106
|
let pre be when i == 0 then st["prompt"] otherwise cont
|
|
105
|
-
set out to out + (when i > 0 then "\r\n" otherwise "") + pre + l
|
|
106
|
-
let r be rows_of(vis(pre) +
|
|
107
|
+
set out to out + (when i > 0 then "\r\n" otherwise "") + pre + (when st["final"] and l != "" then sgr(color, "7", " " + l + " ") otherwise l)
|
|
108
|
+
let r be rows_of(vis(pre) + ui.width(l), cols)
|
|
107
109
|
when i < cl
|
|
108
110
|
set rows_before_cursor to rows_before_cursor + r
|
|
109
111
|
otherwise when i == cl
|
|
110
|
-
|
|
111
|
-
set
|
|
112
|
+
let ccw be ui.width(slice(l, 0, cc))
|
|
113
|
+
set rows_before_cursor to rows_before_cursor + floor((vis(pre) + ccw) / cols)
|
|
114
|
+
set cursor_col to (vis(pre) + ccw) % cols
|
|
112
115
|
set total to total + r
|
|
113
116
|
set i to i + 1
|
|
114
117
|
-- menú
|
|
@@ -182,7 +185,8 @@ export task read(prompt, color, ctx)
|
|
|
182
185
|
let h be term_open({"ctrl_c": "exit"})
|
|
183
186
|
when h == nothing
|
|
184
187
|
give ctx["fallback"](prompt)
|
|
185
|
-
|
|
188
|
+
ui.set_cols(term_size(h)["cols"])
|
|
189
|
+
let st be {"buf": "", "cur": 0, "prompt": prompt, "cursor_row": 0, "menu": false, "menu_off": false, "kind": "", "items": [], "sel": 0, "nav": false, "final": false}
|
|
186
190
|
let hist be ctx["history"]
|
|
187
191
|
let hi be length(hist)
|
|
188
192
|
let draft be ""
|
|
@@ -207,7 +211,7 @@ export task read(prompt, color, ctx)
|
|
|
207
211
|
set st to insert(st, replace_text(ev["text"], "\r", ""))
|
|
208
212
|
set st to refresh_menu(st, ctx)
|
|
209
213
|
otherwise when ev["type"] == "resize"
|
|
210
|
-
|
|
214
|
+
ui.set_cols(ev["cols"])
|
|
211
215
|
otherwise when ev["type"] == "key"
|
|
212
216
|
let k be ev["key"]
|
|
213
217
|
let changed be true
|
|
@@ -300,13 +304,14 @@ export task read(prompt, color, ctx)
|
|
|
300
304
|
set st to draw(h, st, color, ctx)
|
|
301
305
|
-- dejar el prompt limpio (sin menú) en el scrollback y bajar a una línea nueva
|
|
302
306
|
set st["menu"] to false
|
|
307
|
+
set st["final"] to result != nothing and result != INTERRUPT and trim(st["buf"]) != ""
|
|
303
308
|
set st to draw(h, st, color, ctx)
|
|
304
309
|
let lines be split(st["buf"], "\n")
|
|
305
310
|
let size be term_size(h)
|
|
306
311
|
let tail be 0
|
|
307
312
|
let i be 0
|
|
308
313
|
each l in lines
|
|
309
|
-
let pre be when i == 0 then vis(st["prompt"]) otherwise 4
|
|
314
|
+
let pre be (when i == 0 then vis(st["prompt"]) otherwise 4) + (when st["final"] then 2 otherwise 0)
|
|
310
315
|
when i > 0 or true
|
|
311
316
|
set tail to tail + rows_of(pre + length(l), size["cols"])
|
|
312
317
|
set i to i + 1
|
package/lib/loop.syn
CHANGED
|
@@ -55,7 +55,7 @@ export task explore_cap()
|
|
|
55
55
|
let e be env("LAMPSON_EXPLORE_CAP", "")
|
|
56
56
|
when e != ""
|
|
57
57
|
give floor(number(e))
|
|
58
|
-
give
|
|
58
|
+
give 24
|
|
59
59
|
|
|
60
60
|
-- {streak, note, refuse}: nuevo streak tras esta llamada y qué hacer con ella
|
|
61
61
|
export task explore_verdict(streak, name, cap)
|
|
@@ -100,6 +100,9 @@ export task spill(name, id, out)
|
|
|
100
100
|
require file(".lampson/*")
|
|
101
101
|
when name == "read" or length(out) <= SPILL_CAP
|
|
102
102
|
give out
|
|
103
|
+
-- el informe de los subagentes es el entregable: no se manda a disco (hasta 60k)
|
|
104
|
+
when name == "delegate" and length(out) <= SPILL_CAP * 6
|
|
105
|
+
give out
|
|
103
106
|
let path be SPILL_DIR + "/" + id + ".txt"
|
|
104
107
|
write_file(path, out)
|
|
105
108
|
let half be floor(SPILL_CAP / 2)
|
|
@@ -276,17 +279,21 @@ task execute(tc, opts, on_event)
|
|
|
276
279
|
when not approved
|
|
277
280
|
emit(on_event, "tool_denied", {"call": tc, "reason": "user declined (" + verdict["reason"] + ")"}, tag)
|
|
278
281
|
give "DENIED by the user (" + verdict["reason"] + "). Do not retry this exact action; ask the user or propose an alternative."
|
|
282
|
+
emit(on_event, "busy", {"label": permission.describe_call(name, args), "kind": "tool", "name": name}, tag)
|
|
283
|
+
let out be ""
|
|
279
284
|
try
|
|
280
285
|
-- tools MCP: no son tasks Synsema (args libres); el registry las marca con "mcp"
|
|
281
286
|
when registry[name] == "mcp"
|
|
282
|
-
|
|
287
|
+
set out to text(mcp.call(name, args))
|
|
283
288
|
-- tools de lámparas: un proceso hijo por llamada (lib/lamps.syn)
|
|
284
|
-
when registry[name] == "lamp"
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
289
|
+
otherwise when registry[name] == "lamp"
|
|
290
|
+
set out to text(lamps.call(name, args))
|
|
291
|
+
otherwise
|
|
292
|
+
set out to text(call_tool(registry[name], args))
|
|
288
293
|
recover err
|
|
289
|
-
|
|
294
|
+
set out to "ERROR: " + err
|
|
295
|
+
emit(on_event, "idle", nothing, tag)
|
|
296
|
+
give out
|
|
290
297
|
|
|
291
298
|
export task run_turn(cfg, messages, opts, on_event)
|
|
292
299
|
require net
|
|
@@ -353,7 +360,9 @@ export task run_turn(cfg, messages, opts, on_event)
|
|
|
353
360
|
when last_step
|
|
354
361
|
let why be when over_budget then "Token budget for this turn is exhausted (" + text(spent) + " of " + text(floor(budget)) + " tokens)" otherwise "Step limit reached for this turn (" + text(max_steps) + " tool calls)"
|
|
355
362
|
set msgs to append(msgs, {"role": "user", "content": "[harness] " + why + ". Do NOT call tools now. Reply with a short status: what you completed, what is verified, and exactly what remains to do next. The user can say 'continue' to resume."})
|
|
363
|
+
emit(on_event, "busy", {"label": "pensando", "kind": "llm"}, tag)
|
|
356
364
|
let r be provider.chat_retry(cfg, msgs, cat, 3)
|
|
365
|
+
emit(on_event, "idle", nothing, tag)
|
|
357
366
|
when r["error"] != nothing
|
|
358
367
|
emit(on_event, "error", r["error"], tag)
|
|
359
368
|
set stopped to "error: " + r["error"]
|
|
@@ -406,16 +415,21 @@ export task run_turn(cfg, messages, opts, on_event)
|
|
|
406
415
|
when ex["note"] != ""
|
|
407
416
|
set out to out + ex["note"]
|
|
408
417
|
emit(on_event, "inbox", "tope de exploración a la mitad (" + text(explore_streak) + "/" + text(cap) + " lecturas seguidas sin actuar): aviso al modelo", tag)
|
|
409
|
-
|
|
418
|
+
-- solo cuentan los errores REALES de la tool; los rechazos del harness (tope de lecturas,
|
|
419
|
+
-- repeticiones) ya castigan con el rechazo y no deben además cortar el turno
|
|
420
|
+
when (starts_with(out, "ERROR") or starts_with(out, "DENIED")) and not starts_with(out, "ERROR: exploration cap") and not starts_with(out, "ERROR: this turn already made") and not starts_with(out, "ERROR: you already called")
|
|
410
421
|
set errors to errors + 1
|
|
411
422
|
emit(on_event, "tool_result", {"call": tc, "output": out}, tag)
|
|
412
423
|
set msgs to append(msgs, tool_result_msg(tc, out))
|
|
413
424
|
when errors >= MAX_ERRORS_PER_TURN
|
|
414
425
|
set msgs to append(msgs, {"role": "user", "content": "[harness] Too many tool errors this turn. Stop calling tools and report what happened."})
|
|
426
|
+
emit(on_event, "busy", {"label": "pensando", "kind": "llm"}, tag)
|
|
415
427
|
let fin be provider.chat_retry(cfg, msgs, [], 2)
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
428
|
+
emit(on_event, "idle", nothing, tag)
|
|
429
|
+
let fin_text be strip_raw_tool_calls(fin["text"])
|
|
430
|
+
set msgs to append(msgs, {"role": "assistant", "content": fin_text, "tool_calls": []})
|
|
431
|
+
emit(on_event, "assistant", fin_text, tag)
|
|
432
|
+
give {"messages": msgs, "text": fin_text, "steps": steps, "usage": usage, "stopped": "too_many_errors"}
|
|
419
433
|
set stopped to when spent > budget then "budget" otherwise "max_steps"
|
|
420
434
|
give {"messages": msgs, "text": final_text, "steps": steps, "usage": usage, "stopped": stopped}
|
|
421
435
|
|
package/lib/md.syn
CHANGED
|
@@ -5,8 +5,26 @@
|
|
|
5
5
|
-- (backrefs \1). En strings "..." la barra va simple ("\*", "\d"): "\\d" NO es \d.
|
|
6
6
|
-- Los bloques ``` se copian tal cual (sin parseo inline). La web ya renderiza markdown por su cuenta.
|
|
7
7
|
|
|
8
|
+
use "./ui.syn" as ui
|
|
9
|
+
|
|
8
10
|
let ESC be decode(bytes("1b", "hex"))
|
|
9
|
-
|
|
11
|
+
-- ancho útil: columnas de la terminal menos la sangría (mín. 40, máx. 100 para que la prosa se lea)
|
|
12
|
+
task width_now()
|
|
13
|
+
let c be ui.cols() - 4
|
|
14
|
+
when c < 40
|
|
15
|
+
give 40
|
|
16
|
+
when c > 100
|
|
17
|
+
give 100
|
|
18
|
+
give c
|
|
19
|
+
|
|
20
|
+
-- rutas y símbolos de código dentro de la prosa → azul (lib/x.syn, src/a/b.ts, describe_call(), foo.bar)
|
|
21
|
+
task paths(s, color)
|
|
22
|
+
when not color
|
|
23
|
+
give s
|
|
24
|
+
let out be replace_re(s, "(^|[\s(])([\w.-]*/[\w./-]{2,})", "\1" + ESC + "[34m\2" + ESC + "[0m")
|
|
25
|
+
set out to replace_re(out, "(^|[\s(])(\w+\.(syn|js|ts|tsx|jsx|json|md|py|rs|go|ps1|sh|html|css|toml|yml|yaml|env))(\b)", "\1" + ESC + "[34m\2" + ESC + "[0m")
|
|
26
|
+
set out to replace_re(out, "(^|[\s(])(\w+\(\))", "\1" + ESC + "[34m\2" + ESC + "[0m")
|
|
27
|
+
give out
|
|
10
28
|
|
|
11
29
|
task sgr(color, code, s)
|
|
12
30
|
when not color or s == ""
|
|
@@ -41,16 +59,16 @@ export task inline(s, color)
|
|
|
41
59
|
-- `código` primero: lo de adentro no se toca
|
|
42
60
|
let parts be split(s, "`")
|
|
43
61
|
when length(parts) < 3
|
|
44
|
-
give emphasis(s, color)
|
|
62
|
+
give paths(emphasis(s, color), color)
|
|
45
63
|
let out be ""
|
|
46
64
|
let idx be 0
|
|
47
65
|
each p in parts
|
|
48
66
|
when idx == length(parts) - 1 and idx % 2 == 1
|
|
49
|
-
set out to out + "`" + emphasis(p, color)
|
|
67
|
+
set out to out + "`" + paths(emphasis(p, color), color)
|
|
50
68
|
otherwise when idx % 2 == 1
|
|
51
|
-
set out to out + sgr(color, "
|
|
69
|
+
set out to out + sgr(color, "36", p)
|
|
52
70
|
otherwise
|
|
53
|
-
set out to out + emphasis(p, color)
|
|
71
|
+
set out to out + paths(emphasis(p, color), color)
|
|
54
72
|
set idx to idx + 1
|
|
55
73
|
give out
|
|
56
74
|
|
|
@@ -79,19 +97,51 @@ task table_lines(rows, pre, color)
|
|
|
79
97
|
each c in r
|
|
80
98
|
when i >= length(widths)
|
|
81
99
|
set widths to append(widths, 0)
|
|
82
|
-
when
|
|
83
|
-
set widths[i] to
|
|
100
|
+
when ui.width(inline(c, false)) > widths[i]
|
|
101
|
+
set widths[i] to ui.width(inline(c, false))
|
|
84
102
|
set i to i + 1
|
|
103
|
+
-- si no entra en la terminal, achicar la columna más ancha hasta que entre (mínimo 10) y envolver
|
|
104
|
+
let avail be width_now() - 3 * (length(widths) - 1)
|
|
105
|
+
let total be 0
|
|
106
|
+
each w in widths
|
|
107
|
+
set total to total + w
|
|
108
|
+
while total > avail
|
|
109
|
+
let widest be 0
|
|
110
|
+
let wi be 0
|
|
111
|
+
each w in widths
|
|
112
|
+
when w > widths[widest]
|
|
113
|
+
set widest to wi
|
|
114
|
+
set wi to wi + 1
|
|
115
|
+
when widths[widest] <= 10
|
|
116
|
+
set total to avail
|
|
117
|
+
otherwise
|
|
118
|
+
set widths[widest] to widths[widest] - 1
|
|
119
|
+
set total to total - 1
|
|
85
120
|
let out be []
|
|
86
121
|
let ri be 0
|
|
87
122
|
each r in rows
|
|
88
|
-
|
|
123
|
+
-- cada celda → lista de líneas envueltas a su ancho
|
|
124
|
+
let wrapped be []
|
|
125
|
+
let height be 1
|
|
89
126
|
let i be 0
|
|
90
127
|
each c in r
|
|
91
|
-
let
|
|
92
|
-
set
|
|
128
|
+
let ls be ui.wrap(c, widths[i], "")
|
|
129
|
+
set wrapped to append(wrapped, ls)
|
|
130
|
+
when length(ls) > height
|
|
131
|
+
set height to length(ls)
|
|
93
132
|
set i to i + 1
|
|
94
|
-
|
|
133
|
+
let li be 0
|
|
134
|
+
while li < height
|
|
135
|
+
let cells be []
|
|
136
|
+
set i to 0
|
|
137
|
+
each ls in wrapped
|
|
138
|
+
let piece be when li < length(ls) then ls[li] otherwise ""
|
|
139
|
+
let vis be ui.width(inline(piece, false))
|
|
140
|
+
let fill be rep(" ", (when widths[i] > vis then widths[i] - vis otherwise 0))
|
|
141
|
+
set cells to append(cells, (when ri == 0 then sgr(color, "1", inline(piece, color)) otherwise inline(piece, color)) + fill)
|
|
142
|
+
set i to i + 1
|
|
143
|
+
set out to append(out, pre + join(cells, sgr(color, "2", " │ ")))
|
|
144
|
+
set li to li + 1
|
|
95
145
|
when ri == 0
|
|
96
146
|
let segs be []
|
|
97
147
|
each w in widths
|
|
@@ -102,7 +152,7 @@ task table_lines(rows, pre, color)
|
|
|
102
152
|
|
|
103
153
|
task fence_top(lang, color)
|
|
104
154
|
let label be when lang != "" then " " + lang + " " otherwise ""
|
|
105
|
-
give sgr(color, "2", "┌──" + label + rep("─",
|
|
155
|
+
give sgr(color, "2", "┌──" + label + rep("─", width_now() - 3 - length(label)))
|
|
106
156
|
|
|
107
157
|
export task render(md, color)
|
|
108
158
|
let pre be " "
|
|
@@ -122,9 +172,9 @@ export task render(md, color)
|
|
|
122
172
|
when in_code
|
|
123
173
|
when starts_with(t, fence)
|
|
124
174
|
set in_code to false
|
|
125
|
-
set out to append(out, pre + sgr(color, "2", "└" + rep("─",
|
|
175
|
+
set out to append(out, pre + sgr(color, "2", "└" + rep("─", width_now() - 1)))
|
|
126
176
|
otherwise
|
|
127
|
-
set out to append(out, pre + sgr(color, "2", "│ ") + sgr(color, "36", line))
|
|
177
|
+
set out to append(out, pre + sgr(color, "2", "│ ") + ui.cut(sgr(color, "36", line), width_now() - 2))
|
|
128
178
|
otherwise when starts_with(t, "```") or starts_with(t, "~~~")
|
|
129
179
|
set in_code to true
|
|
130
180
|
set fence to slice(t, 0, 3)
|
|
@@ -143,7 +193,7 @@ export task render(md, color)
|
|
|
143
193
|
otherwise
|
|
144
194
|
set out to append(out, pre + sgr(color, "1", title))
|
|
145
195
|
otherwise when matches(t, "(-{3,}|\*{3,}|_{3,})")
|
|
146
|
-
set out to append(out, pre + sgr(color, "2", rep("─",
|
|
196
|
+
set out to append(out, pre + sgr(color, "2", rep("─", width_now())))
|
|
147
197
|
otherwise when starts_with(t, ">")
|
|
148
198
|
set out to append(out, pre + sgr(color, "2", "▎ ") + sgr(color, "3", inline(trim(slice(t, 1, length(t))), color)))
|
|
149
199
|
otherwise when matches(t, "\|[\s:|-]+\|")
|
|
@@ -160,12 +210,20 @@ export task render(md, color)
|
|
|
160
210
|
otherwise when starts_with(rest, "[x] ") or starts_with(rest, "[X] ")
|
|
161
211
|
set mark to sgr(color, "32", "☑")
|
|
162
212
|
set rest to slice(rest, 4, length(rest))
|
|
163
|
-
|
|
213
|
+
let wrapped be ui.wrap(rest, width_now() - length(ind) - 2, "")
|
|
214
|
+
let wi be 0
|
|
215
|
+
each wl in wrapped
|
|
216
|
+
set out to append(out, pre + ind + (when wi == 0 then mark + " " otherwise " ") + inline(wl, color))
|
|
217
|
+
set wi to wi + 1
|
|
164
218
|
otherwise
|
|
165
|
-
|
|
219
|
+
when t == ""
|
|
220
|
+
set out to append(out, "")
|
|
221
|
+
otherwise
|
|
222
|
+
each wl in ui.wrap(trim(line), width_now(), "")
|
|
223
|
+
set out to append(out, pre + inline(wl, color))
|
|
166
224
|
when length(table) > 0
|
|
167
225
|
each l in table_lines(table, pre, color)
|
|
168
226
|
set out to append(out, l)
|
|
169
227
|
when in_code
|
|
170
|
-
set out to append(out, pre + sgr(color, "2", "└" + rep("─",
|
|
228
|
+
set out to append(out, pre + sgr(color, "2", "└" + rep("─", width_now() - 1)))
|
|
171
229
|
give join(out, "\n")
|
package/lib/prompt.syn
CHANGED
|
@@ -35,7 +35,7 @@ Operate like a careful senior engineer: precise, honest, and economical with wor
|
|
|
35
35
|
# Method — follow these five steps on every non-trivial request
|
|
36
36
|
1. PLAN. If the task needs 3+ distinct steps (not 3 tool calls for one conceptual step), call todo FIRST and keep it current: one item in_progress at a time, marked completed the moment its work is verified — never batched, never on intent. The todo list survives context compaction; your narration does not. Skip it for a single straightforward change; when in doubt, use it.
|
|
37
37
|
2. LOCATE. Find code with grep and find, never by reading directories at random. For a broad, open-ended question about how the codebase works, delegate ONE focused question to an explore sub-agent instead of reading everything yourself — the search stays out of your context. Batch independent lookups into a single turn.
|
|
38
|
-
3. READ ONLY WHAT YOU NEED. Read the files you will change plus the ones you must understand to change them — not the whole project. Every file you read is re-sent on every later call. Use offset/limit for long files, but avoid tiny repeated slices. Trace a symbol to its definition and usages rather than guessing its shape. Never invent files, symbols, APIs or imports — if you have not seen it in this repo, go look. HARD LIMITS enforced by the harness: after
|
|
38
|
+
3. READ ONLY WHAT YOU NEED. Read the files you will change plus the ones you must understand to change them — not the whole project. Every file you read is re-sent on every later call. Use offset/limit for long files, but avoid tiny repeated slices. Trace a symbol to its definition and usages rather than guessing its shape. Never invent files, symbols, APIs or imports — if you have not seen it in this repo, go look. HARD LIMITS enforced by the harness: after 12 read-only calls in a row without acting you get a warning, after 24 read/ls/find/grep are refused until you act; a file read twice unchanged comes back as a one-line receipt.
|
|
39
39
|
4. CHANGE. Act as soon as you know enough — usually within the first 6-10 reads. Prefer edit (exact-string replacement; old_string must be unique, include surrounding context) over write; write only for new files or full rewrites. edit/write on an existing file are rejected unless you read that file in this session and it has not changed since. Minimal, targeted changes: no drive-by refactors, renames or reformatting; add every import or dependency your code needs. Do NOT print code blocks to the user as a substitute for editing — apply the change, then summarise it. If an edit fails to apply, re-read the file and retry with the exact current text; if the same region fails twice, rewrite the enclosing function with write.
|
|
40
40
|
5. VERIFY AND REPORT. Run the relevant tests, linter or build with bash and confirm they pass before claiming the work is done — AFTER your edits, not before as reconnaissance. Never assume a test command: check the README, the package manifest or the project instructions. Finish with a short summary: what changed (files, path:line for specific code) and how you verified it. Do not narrate every step.
|
|
41
41
|
|
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
package/public/index.html
CHANGED
|
@@ -145,6 +145,10 @@
|
|
|
145
145
|
.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; }
|
|
146
146
|
.sec h2:hover { color:var(--ink); }
|
|
147
147
|
.sec .caret { width:9px; font-size:9px; color:var(--ink-3); }
|
|
148
|
+
.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; }
|
|
149
|
+
.sec h2 .h2act:hover { opacity:1; color:var(--accent); }
|
|
150
|
+
.sec h2 .h2act.spin { animation: h2spin .6s linear; }
|
|
151
|
+
@keyframes h2spin { to { transform: rotate(360deg); } }
|
|
148
152
|
.sec .cnt { margin-left:auto; letter-spacing:0; text-transform:none; }
|
|
149
153
|
.sec .body { display:none; overflow:auto; min-height:0; padding:0 10px 10px 16px; }
|
|
150
154
|
.sec.open .body { display:block; }
|
|
@@ -370,7 +374,7 @@
|
|
|
370
374
|
</header>
|
|
371
375
|
<aside>
|
|
372
376
|
<section class="sec grow open" data-sec="tree">
|
|
373
|
-
<h2><span class="caret">▾</span>Archivos
|
|
377
|
+
<h2><span class="caret">▾</span>Archivos<button class="h2act" id="tree-reload" title="recargar el árbol">↻</button></h2>
|
|
374
378
|
<div class="body" id="tree"></div>
|
|
375
379
|
</section>
|
|
376
380
|
<section class="sec" data-sec="procs">
|
|
@@ -700,6 +704,11 @@ function autoSec(name, hasContent) {
|
|
|
700
704
|
const pref = localStorage.getItem('lampson.sec.' + name);
|
|
701
705
|
setSec(name, pref === null ? hasContent : pref === '1');
|
|
702
706
|
}
|
|
707
|
+
// el árbol se refresca solo cuando una tool pudo crear/borrar archivos (write/edit/bash/process/delegate/lamp),
|
|
708
|
+
// con un pequeño debounce para no repintar 20 veces en un turno con 20 writes; ↻ lo fuerza a mano
|
|
709
|
+
let treeTimer = null;
|
|
710
|
+
function treeChanged() { clearTimeout(treeTimer); treeTimer = setTimeout(loadTree, 400); }
|
|
711
|
+
$('#tree-reload').onclick = e => { e.stopPropagation(); const b = e.currentTarget; b.classList.remove('spin'); void b.offsetWidth; b.classList.add('spin'); loadTree(); };
|
|
703
712
|
document.querySelectorAll('.sec h2').forEach(h => h.onclick = () => {
|
|
704
713
|
const s = h.closest('.sec'); const open = !s.classList.contains('open');
|
|
705
714
|
localStorage.setItem('lampson.sec.' + s.dataset.sec, open ? '1' : '0');
|
|
@@ -1244,6 +1253,7 @@ function handle(chunk, thinking) {
|
|
|
1244
1253
|
else if (k === 'tool_call') { thinking.querySelector('span:last-child').textContent = 'ejecutando ' + esc(d.name) + '…'; pending = add('step', `<span class="ic">⚙</span>${cmdHtml(describe(d))}`); wireMore(pending); if (d.name === 'process') setTimeout(loadProcs, 2500); if (d.name === 'delegate') { setTimeout(loadAgents, 800); setTimeout(loadAgents, 4000); } if (d.name === 'todo') setTimeout(loadTodo, 300); if (d.name === 'lamp') setTimeout(loadLamps, 300); if (d.name === 'lsp') setTimeout(loadLsp, 1500); }
|
|
1245
1254
|
else if (k === 'inbox') { add('meta', '✉ ' + esc(String(d).split('\n')[0].slice(0, 160))); loadAgents(); }
|
|
1246
1255
|
else if (k === 'tool_result') { const out = String(d.output); const bad = /^(ERROR|DENIED)/.test(out);
|
|
1256
|
+
if (!bad && d.call && /^(write|edit|bash|process|delegate|lamp|skill)$/.test(d.call.name)) treeChanged();
|
|
1247
1257
|
// lámpara recién creada: avisar en el chat con un acceso directo al switch (el modelo no sabe cómo se enciende en esta UI)
|
|
1248
1258
|
if (d.call && d.call.name === 'lamp' && /^lamp '([^']+)' created/.test(out)) { const nm = out.match(/^lamp '([^']+)' created/)[1]; const m = add('meta', `☼ lámpara <b>${esc(nm)}</b> creada — está apagada: <a href="#" class="lampgo">encenderla en «lámparas»</a>`); m.querySelector('.lampgo').onclick = (ev) => { ev.preventDefault(); $('#lamps').click(); }; $('#lamps').classList.add('new'); setTimeout(() => $('#lamps').classList.remove('new'), 4000); } const el = pending || add('step', '<span class="ic">→</span>'); pending = null; thinking.querySelector('span:last-child').textContent = 'pensando…'; const ic = el.querySelector('.ic'); if (ic) { ic.textContent = bad ? '✗' : '✓'; ic.className = 'ic ' + (bad ? 'bad' : 'ok'); } el.insertAdjacentHTML('beforeend', `<details><summary class="${bad ? 'bad' : ''}">${esc(out.split('\n')[0].slice(0, 140))} <span style="color:var(--ink-3)">(${out.length} chars)</span></summary><pre>${esc(out)}</pre></details>`); }
|
|
1249
1259
|
else if (k === 'approval_request') { const el = add('approval', `<div class="card"><div class="why">⚠ ${esc(d.why)}</div><code>${esc(describe({name: d.name, args: d.args}))}</code><div class="btns"><button class="primary" data-ok="1">Permitir</button><button data-ok="0">Denegar</button></div></div>`); el.querySelectorAll('button').forEach(b => b.onclick = async () => { el.querySelectorAll('button').forEach(x => x.disabled = true); await fetch('/api/approve', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: d.id, decision: b.dataset.ok === '1' }) }); }); el.dataset.id = d.id; }
|