lampson 0.1.1 → 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/lib/diff.syn ADDED
@@ -0,0 +1,168 @@
1
+ -- lib/diff.syn — diff de líneas para mostrar en la terminal qué cambió edit/write
2
+ -- diff(old, new, ctx, color) → {"added": n, "removed": m, "lines": [texto ya formateado…]}
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 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
8
+
9
+ let ESC be decode(bytes("1b", "hex"))
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
31
+
32
+ task sgr(color, code, s)
33
+ when not color
34
+ give s
35
+ give ESC + "[" + code + "m" + s + ESC + "[0m"
36
+
37
+ task lpad(s, n)
38
+ let out be s
39
+ while length(out) < n
40
+ set out to " " + out
41
+ give out
42
+
43
+ -- LCS clásico: devuelve la lista de ops {"op": " "|"-"|"+", "a": idx_old, "b": idx_new}
44
+ task lcs_ops(a, b)
45
+ let n be length(a)
46
+ let m be length(b)
47
+ -- tabla (n+1)×(m+1) aplanada, fila por fila
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
55
+ let i be n - 1
56
+ while i >= 0
57
+ let j be m - 1
58
+ while j >= 0
59
+ when a[i] == b[j]
60
+ set table[i * (m + 1) + j] to table[(i + 1) * (m + 1) + j + 1] + 1
61
+ otherwise
62
+ let down be table[(i + 1) * (m + 1) + j]
63
+ let right be table[i * (m + 1) + j + 1]
64
+ set table[i * (m + 1) + j] to (when down >= right then down otherwise right)
65
+ set j to j - 1
66
+ set i to i - 1
67
+ let ops be []
68
+ set i to 0
69
+ let j be 0
70
+ while i < n and j < m
71
+ when a[i] == b[j]
72
+ set ops to append(ops, {"op": " ", "a": i, "b": j})
73
+ set i to i + 1
74
+ set j to j + 1
75
+ otherwise when table[(i + 1) * (m + 1) + j] >= table[i * (m + 1) + j + 1]
76
+ set ops to append(ops, {"op": "-", "a": i, "b": j})
77
+ set i to i + 1
78
+ otherwise
79
+ set ops to append(ops, {"op": "+", "a": i, "b": j})
80
+ set j to j + 1
81
+ while i < n
82
+ set ops to append(ops, {"op": "-", "a": i, "b": j})
83
+ set i to i + 1
84
+ while j < m
85
+ set ops to append(ops, {"op": "+", "a": i, "b": j})
86
+ set j to j + 1
87
+ give ops
88
+
89
+ export task diff(old, new, ctx, color)
90
+ let a be when old == "" then [] otherwise split(old, "\n")
91
+ let b be when new == "" then [] otherwise split(new, "\n")
92
+ -- prefijo común
93
+ let pre be 0
94
+ while pre < length(a) and pre < length(b) and a[pre] == b[pre]
95
+ set pre to pre + 1
96
+ -- sufijo común (sin pisar el prefijo)
97
+ let suf be 0
98
+ while suf < length(a) - pre and suf < length(b) - pre and a[length(a) - 1 - suf] == b[length(b) - 1 - suf]
99
+ set suf to suf + 1
100
+ let mid_a be slice(a, pre, length(a) - suf)
101
+ let mid_b be slice(b, pre, length(b) - suf)
102
+ let ops be []
103
+ when length(mid_a) * length(mid_b) > LIMIT * LIMIT
104
+ each e in enumerate(mid_a)
105
+ set ops to append(ops, {"op": "-", "a": e["index"], "b": 0})
106
+ each e in enumerate(mid_b)
107
+ set ops to append(ops, {"op": "+", "a": length(mid_a), "b": e["index"]})
108
+ otherwise
109
+ set ops to lcs_ops(mid_a, mid_b)
110
+ let added be 0
111
+ let removed be 0
112
+ each o in ops
113
+ when o["op"] == "+"
114
+ set added to added + 1
115
+ otherwise when o["op"] == "-"
116
+ set removed to removed + 1
117
+ -- render: ctx líneas de contexto antes y después del bloque cambiado (números de línea del archivo NUEVO
118
+ -- para "+"/" ", del viejo para "-"), con "⋯" entre hunks
119
+ let width be length(text(length(b)))
120
+ let lines be []
121
+ -- contexto previo (del prefijo común)
122
+ let start be when pre - ctx > 0 then pre - ctx otherwise 0
123
+ when start > 0
124
+ set lines to append(lines, sgr(color, "2", lpad("", width) + " ⋯"))
125
+ let k be start
126
+ while k < pre
127
+ set lines to append(lines, row(color, " ", text(k + 1), a[k], width))
128
+ set k to k + 1
129
+ -- medio: contexto interno acotado (ctx a cada lado de un cambio)
130
+ let last_change be -1
131
+ let idx be 0
132
+ each o in ops
133
+ when o["op"] != " "
134
+ set last_change to idx
135
+ set idx to idx + 1
136
+ set idx to 0
137
+ let skipping be false
138
+ each o in ops
139
+ let near be false
140
+ let w be idx - ctx
141
+ while w <= idx + ctx and not near
142
+ when w >= 0 and w < length(ops) and ops[w]["op"] != " "
143
+ set near to true
144
+ set w to w + 1
145
+ when o["op"] == " "
146
+ when near
147
+ set lines to append(lines, row(color, " ", text(pre + o["b"] + 1), mid_b[o["b"]], width))
148
+ set skipping to false
149
+ otherwise when not skipping
150
+ set lines to append(lines, sgr(color, "2", lpad("", width) + " ⋯"))
151
+ set skipping to true
152
+ otherwise when o["op"] == "-"
153
+ set lines to append(lines, row(color, "-", text(pre + o["a"] + 1), mid_a[o["a"]], width))
154
+ set skipping to false
155
+ otherwise
156
+ set lines to append(lines, row(color, "+", text(pre + o["b"] + 1), mid_b[o["b"]], width))
157
+ set skipping to false
158
+ set idx to idx + 1
159
+ -- contexto posterior (del sufijo común)
160
+ let after_start be length(b) - suf
161
+ let upto be when after_start + ctx < length(b) then after_start + ctx otherwise length(b)
162
+ set k to after_start
163
+ while k < upto
164
+ set lines to append(lines, row(color, " ", text(k + 1), b[k], width))
165
+ set k to k + 1
166
+ when upto < length(b)
167
+ set lines to append(lines, sgr(color, "2", lpad("", width) + " ⋯"))
168
+ give {"added": added, "removed": removed, "lines": lines}
package/lib/line.syn ADDED
@@ -0,0 +1,370 @@
1
+ -- lib/line.syn — editor de línea para la terminal (Synsema v0.6.11+: term_open/term_recv)
2
+ -- read(prompt, color, ctx) → texto (puede tener "\n"), INTERRUPT si ctx["idle"]() pidió cortar,
3
+ -- nothing en EOF. Sin TTY (pipe/CI) delega en ctx["fallback"](prompt).
4
+ -- choose(question, options, color) → índice elegido, -1 sin TTY (usar approve), nothing en EOF/Esc.
5
+ -- Teclas: ←→ Home End · ↑↓ historial o menú · Tab completa · Alt+Enter salto de línea · Enter envía
6
+ -- Ctrl+A/E inicio/fin · Ctrl+U borra la línea · Ctrl+W borra palabra · Ctrl+O = /out · Esc cierra el menú
7
+ -- Menú: al escribir "/" aparecen los comandos (primero los recientes); con "/cmd " los argumentos que
8
+ -- devuelva ctx["complete"](cmd, prefijo) (archivos, servers, flags…). Máx MENU_ROWS filas.
9
+ -- MIGA: dibujar SIEMPRE con term_write (print queda buffereado). El runtime restaura la terminal al cerrar.
10
+
11
+ use "./ui.syn" as ui
12
+
13
+ let ESC be decode(bytes("1b", "hex"))
14
+ let MENU_ROWS be 8
15
+ export let INTERRUPT be ESC + "interrupt"
16
+
17
+ task sgr(color, code, s)
18
+ when not color or s == ""
19
+ give s
20
+ give ESC + "[" + code + "m" + s + ESC + "[0m"
21
+
22
+ task vis(s)
23
+ give ui.width(s)
24
+
25
+ task rep(ch, n)
26
+ let out be ""
27
+ while length(out) < n
28
+ set out to out + ch
29
+ give out
30
+
31
+ task starts(s, p)
32
+ give starts_with(s, p)
33
+
34
+ -- ---------- candidatos ----------
35
+
36
+ -- buf = texto del editor. give {"kind": "cmd"|"arg"|"", "items": [{"label","fill","desc"}], "token": prefijo}
37
+ task candidates(buf, ctx)
38
+ when not starts(buf, "/") or contains(buf, "\n")
39
+ give {"kind": "", "items": [], "token": ""}
40
+ let sp be capture(buf, "^(/\S*)\s+(.*)$")
41
+ when sp == nothing
42
+ -- comando a medias: recientes primero cuando es "/" pelado
43
+ let items be []
44
+ let seen be {}
45
+ when buf == "/"
46
+ each r in ctx["recent"]
47
+ each c in ctx["commands"]
48
+ when c[0] == r and not contains(seen, r)
49
+ set seen[r] to true
50
+ set items to append(items, {"label": c[0], "fill": c[0] + (when c[1] != "" then " " otherwise ""), "args": c[1], "desc": c[2], "recent": true})
51
+ each c in ctx["commands"]
52
+ when starts(c[0], buf) and not contains(seen, c[0])
53
+ set seen[c[0]] to true
54
+ set items to append(items, {"label": c[0], "fill": c[0] + (when c[1] != "" then " " otherwise ""), "args": c[1], "desc": c[2], "recent": false})
55
+ give {"kind": "cmd", "items": items, "token": buf}
56
+ let cmd be sp[0]
57
+ let rest be sp[1]
58
+ -- último token (lo que se completa); lo anterior queda fijo
59
+ let toks be split(rest, " ")
60
+ let last be toks[length(toks) - 1]
61
+ let head be slice(rest, 0, length(rest) - length(last))
62
+ let items be []
63
+ let cands be ctx["complete"](cmd, head, last)
64
+ each c in cands
65
+ when starts(c, last)
66
+ set items to append(items, {"label": c, "fill": cmd + " " + head + c + (when ends_with(c, "/") then "" otherwise " "), "args": "", "desc": "", "recent": false})
67
+ give {"kind": "arg", "items": items, "token": last}
68
+
69
+ -- ---------- dibujo ----------
70
+
71
+ -- filas físicas que ocupa un texto de ancho w en una terminal de cols columnas
72
+ task rows_of(w, cols)
73
+ when cols <= 0
74
+ give 1
75
+ give floor(w / cols) + 1
76
+
77
+ -- dibuja prompt + buffer (multilínea) + menú; deja el cursor en su lugar. give filas totales dibujadas
78
+ -- (para poder volver al inicio en el próximo redraw)
79
+ task draw(h, st, color, ctx)
80
+ let size be term_size(h)
81
+ let cols be size["cols"]
82
+ let lines be split(st["buf"], "\n")
83
+ -- posición del cursor: línea y columna dentro del buffer
84
+ let cl be 0
85
+ let cc be st["cur"]
86
+ let i be 0
87
+ let found be false
88
+ while i < length(lines) and not found
89
+ when cc <= length(lines[i])
90
+ set cl to i
91
+ set found to true
92
+ otherwise
93
+ set cc to cc - length(lines[i]) - 1
94
+ set i to i + 1
95
+ let out be ""
96
+ -- volver al inicio de lo dibujado la vez anterior y limpiar hacia abajo
97
+ when st["cursor_row"] > 0
98
+ set out to out + ESC + "[" + text(st["cursor_row"]) + "A"
99
+ set out to out + "\r" + ESC + "[J"
100
+ let cont be sgr(color, "2", " … ")
101
+ let rows_before_cursor be 0
102
+ let total be 0
103
+ let cursor_col be 0
104
+ set i to 0
105
+ each l in lines
106
+ let pre be when i == 0 then st["prompt"] otherwise cont
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)
109
+ when i < cl
110
+ set rows_before_cursor to rows_before_cursor + r
111
+ otherwise when i == cl
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
115
+ set total to total + r
116
+ set i to i + 1
117
+ -- menú
118
+ let menu_rows be 0
119
+ when st["menu"] and length(st["items"]) > 0
120
+ let items be st["items"]
121
+ let from be 0
122
+ when st["sel"] >= MENU_ROWS
123
+ set from to st["sel"] - MENU_ROWS + 1
124
+ let k be from
125
+ while k < length(items) and k < from + MENU_ROWS
126
+ let it be items[k]
127
+ let row be ""
128
+ when st["kind"] == "cmd"
129
+ let full be it["label"] + (when it["args"] != "" then " " + it["args"] otherwise "")
130
+ let name be when length(full) > 34 then slice(full, 0, 33) + "…" otherwise full
131
+ let padn be name + rep(" ", 34 - length(name))
132
+ let desc be when length(it["desc"]) > cols - 40 then slice(it["desc"], 0, cols - 43) + "…" otherwise it["desc"]
133
+ set row to (when k == st["sel"] then sgr(color, "7", " " + padn + " ") otherwise " " + sgr(color, "1", it["label"]) + sgr(color, "2", slice(padn, length(it["label"]), length(padn)) + " ")) + (when it["recent"] then sgr(color, "36", "↺ ") otherwise " ") + sgr(color, "2", desc)
134
+ otherwise
135
+ set row to when k == st["sel"] then sgr(color, "7", " " + it["label"] + " ") otherwise " " + it["label"]
136
+ set out to out + "\r\n" + " " + row
137
+ set menu_rows to menu_rows + 1
138
+ set k to k + 1
139
+ when length(items) > MENU_ROWS
140
+ set out to out + "\r\n" + sgr(color, "2", " (" + text(length(items)) + " opciones · ↑↓ elegir · Tab completa · Esc cierra)")
141
+ set menu_rows to menu_rows + 1
142
+ -- recolocar el cursor: subir (filas debajo de la línea del cursor + menú), ir a la columna
143
+ let below be total - rows_before_cursor - 1 + menu_rows
144
+ when below > 0
145
+ set out to out + ESC + "[" + text(below) + "A"
146
+ set out to out + "\r"
147
+ when cursor_col > 0
148
+ set out to out + ESC + "[" + text(cursor_col) + "C"
149
+ term_write(h, out)
150
+ set st["cursor_row"] to rows_before_cursor
151
+ give st
152
+
153
+ -- ---------- edición ----------
154
+
155
+ task insert(st, s)
156
+ set st["buf"] to slice(st["buf"], 0, st["cur"]) + s + slice(st["buf"], st["cur"], length(st["buf"]))
157
+ set st["cur"] to st["cur"] + length(s)
158
+ give st
159
+
160
+ task refresh_menu(st, ctx)
161
+ let c be candidates(st["buf"], ctx)
162
+ set st["kind"] to c["kind"]
163
+ set st["items"] to c["items"]
164
+ set st["sel"] to 0
165
+ set st["nav"] to false
166
+ set st["menu"] to c["kind"] != "" and length(c["items"]) > 0 and not st["menu_off"]
167
+ give st
168
+
169
+ task word_start(buf, cur)
170
+ let i be cur
171
+ while i > 0 and slice(buf, i - 1, i) == " "
172
+ set i to i - 1
173
+ while i > 0 and slice(buf, i - 1, i) != " " and slice(buf, i - 1, i) != "\n"
174
+ set i to i - 1
175
+ give i
176
+
177
+ task accept(st)
178
+ let it be st["items"][st["sel"]]
179
+ set st["buf"] to it["fill"]
180
+ set st["cur"] to length(it["fill"])
181
+ set st["menu"] to false
182
+ give st
183
+
184
+ export task read(prompt, color, ctx)
185
+ let h be term_open({"ctrl_c": "exit"})
186
+ when h == nothing
187
+ give ctx["fallback"](prompt)
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}
190
+ let hist be ctx["history"]
191
+ let hi be length(hist)
192
+ let draft be ""
193
+ let result be nothing
194
+ let done be false
195
+ term_write(h, "\r\n")
196
+ set st to draw(h, st, color, ctx)
197
+ while not done
198
+ let ev be term_recv(h, 1)
199
+ let dirty be true
200
+ when ev == nothing
201
+ set dirty to false
202
+ when ctx["idle"]()
203
+ set result to INTERRUPT
204
+ set done to true
205
+ otherwise when ev["type"] == "focus"
206
+ set dirty to false
207
+ otherwise when ev["type"] == "eof"
208
+ set result to nothing
209
+ set done to true
210
+ otherwise when ev["type"] == "paste"
211
+ set st to insert(st, replace_text(ev["text"], "\r", ""))
212
+ set st to refresh_menu(st, ctx)
213
+ otherwise when ev["type"] == "resize"
214
+ ui.set_cols(ev["cols"])
215
+ otherwise when ev["type"] == "key"
216
+ let k be ev["key"]
217
+ let changed be true
218
+ when k == "enter" and ev["alt"]
219
+ set st to insert(st, "\n")
220
+ otherwise when k == "enter"
221
+ when st["menu"] and st["kind"] == "cmd" and st["buf"] != st["items"][st["sel"]]["label"] and st["buf"] != st["items"][st["sel"]]["fill"]
222
+ set st to accept(st)
223
+ when st["items"][st["sel"]]["args"] == ""
224
+ set result to trim(st["buf"])
225
+ set done to true
226
+ otherwise when st["menu"] and st["kind"] == "arg" and st["nav"]
227
+ -- Enter solo completa el argumento si el humano navegó el menú; si no, envía tal cual
228
+ set st to accept(st)
229
+ otherwise
230
+ set result to st["buf"]
231
+ set done to true
232
+ otherwise when k == "tab" and st["menu"]
233
+ set st to accept(st)
234
+ otherwise when k == "escape"
235
+ when st["menu"]
236
+ set st["menu"] to false
237
+ set st["menu_off"] to true
238
+ set changed to false
239
+ otherwise when k == "up" and st["menu"]
240
+ set st["sel"] to (when st["sel"] > 0 then st["sel"] - 1 otherwise length(st["items"]) - 1)
241
+ set st["nav"] to true
242
+ set changed to false
243
+ otherwise when k == "down" and st["menu"]
244
+ set st["sel"] to (when st["sel"] < length(st["items"]) - 1 then st["sel"] + 1 otherwise 0)
245
+ set st["nav"] to true
246
+ set changed to false
247
+ otherwise when k == "up"
248
+ when hi > 0
249
+ when hi == length(hist)
250
+ set draft to st["buf"]
251
+ set hi to hi - 1
252
+ set st["buf"] to hist[hi]
253
+ set st["cur"] to length(st["buf"])
254
+ set changed to false
255
+ otherwise when k == "down"
256
+ when hi < length(hist)
257
+ set hi to hi + 1
258
+ set st["buf"] to (when hi == length(hist) then draft otherwise hist[hi])
259
+ set st["cur"] to length(st["buf"])
260
+ set changed to false
261
+ otherwise when k == "left"
262
+ when st["cur"] > 0
263
+ set st["cur"] to st["cur"] - 1
264
+ set changed to false
265
+ otherwise when k == "right"
266
+ when st["cur"] < length(st["buf"])
267
+ set st["cur"] to st["cur"] + 1
268
+ set changed to false
269
+ otherwise when k == "home" or (k == "char" and ev["ctrl"] and ev["text"] == "a")
270
+ set st["cur"] to 0
271
+ set changed to false
272
+ otherwise when k == "end" or (k == "char" and ev["ctrl"] and ev["text"] == "e")
273
+ set st["cur"] to length(st["buf"])
274
+ set changed to false
275
+ otherwise when k == "backspace"
276
+ when st["cur"] > 0
277
+ set st["buf"] to slice(st["buf"], 0, st["cur"] - 1) + slice(st["buf"], st["cur"], length(st["buf"]))
278
+ set st["cur"] to st["cur"] - 1
279
+ otherwise when k == "delete"
280
+ when st["cur"] < length(st["buf"])
281
+ set st["buf"] to slice(st["buf"], 0, st["cur"]) + slice(st["buf"], st["cur"] + 1, length(st["buf"]))
282
+ otherwise when k == "char" and ev["ctrl"] and ev["text"] == "u"
283
+ set st["buf"] to ""
284
+ set st["cur"] to 0
285
+ otherwise when k == "char" and ev["ctrl"] and ev["text"] == "w"
286
+ let ws be word_start(st["buf"], st["cur"])
287
+ set st["buf"] to slice(st["buf"], 0, ws) + slice(st["buf"], st["cur"], length(st["buf"]))
288
+ set st["cur"] to ws
289
+ otherwise when k == "char" and ev["ctrl"] and ev["text"] == "o"
290
+ set result to "/out"
291
+ set done to true
292
+ otherwise when k == "char" and ev["ctrl"] and ev["text"] == "l"
293
+ term_write(h, ESC + "[2J" + ESC + "[H")
294
+ set st["cursor_row"] to 0
295
+ set changed to false
296
+ otherwise when k == "char" and not ev["ctrl"]
297
+ set st to insert(st, ev["text"])
298
+ otherwise
299
+ set changed to false
300
+ when changed
301
+ set st["menu_off"] to false
302
+ set st to refresh_menu(st, ctx)
303
+ when not done and dirty
304
+ set st to draw(h, st, color, ctx)
305
+ -- dejar el prompt limpio (sin menú) en el scrollback y bajar a una línea nueva
306
+ set st["menu"] to false
307
+ set st["final"] to result != nothing and result != INTERRUPT and trim(st["buf"]) != ""
308
+ set st to draw(h, st, color, ctx)
309
+ let lines be split(st["buf"], "\n")
310
+ let size be term_size(h)
311
+ let tail be 0
312
+ let i be 0
313
+ each l in lines
314
+ let pre be (when i == 0 then vis(st["prompt"]) otherwise 4) + (when st["final"] then 2 otherwise 0)
315
+ when i > 0 or true
316
+ set tail to tail + rows_of(pre + length(l), size["cols"])
317
+ set i to i + 1
318
+ let down be tail - st["cursor_row"] - 1
319
+ term_write(h, (when down > 0 then ESC + "[" + text(down) + "B" otherwise "") + "\r\n")
320
+ term_close(h)
321
+ give result
322
+
323
+ -- menú vertical de opciones (aprobaciones): ↑↓ + Enter, o la letra inicial / número; Esc = nothing
324
+ export task choose(question, options, color)
325
+ let h be term_open({"ctrl_c": "exit"})
326
+ when h == nothing
327
+ give -1
328
+ let sel be 0
329
+ let done be false
330
+ let result be nothing
331
+ let drawn be 0
332
+ while not done
333
+ let out be ""
334
+ when drawn > 0
335
+ set out to out + ESC + "[" + text(drawn) + "A"
336
+ set out to out + "\r" + ESC + "[J" + question
337
+ let i be 0
338
+ each o in options
339
+ set out to out + "\r\n" + " " + (when i == sel then sgr(color, "7", " " + o + " ") otherwise " " + sgr(color, "2", o))
340
+ set i to i + 1
341
+ term_write(h, out)
342
+ set drawn to length(options)
343
+ let ev be term_recv(h, 600)
344
+ when ev == nothing
345
+ set result to nothing
346
+ set done to true
347
+ otherwise when ev["type"] == "eof"
348
+ set done to true
349
+ otherwise when ev["type"] == "key"
350
+ let k be ev["key"]
351
+ when k == "up"
352
+ set sel to (when sel > 0 then sel - 1 otherwise length(options) - 1)
353
+ otherwise when k == "down" or k == "tab"
354
+ set sel to (when sel < length(options) - 1 then sel + 1 otherwise 0)
355
+ otherwise when k == "enter"
356
+ set result to sel
357
+ set done to true
358
+ otherwise when k == "escape"
359
+ set done to true
360
+ otherwise when k == "char" and not ev["ctrl"]
361
+ let t be lower(ev["text"])
362
+ let j be 0
363
+ each o in options
364
+ when t == text(j + 1) or t == lower(slice(o, 0, 1))
365
+ set result to j
366
+ set done to true
367
+ set j to j + 1
368
+ term_write(h, "\r\n")
369
+ term_close(h)
370
+ give result
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 16
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
- give text(mcp.call(name, args))
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
- give text(lamps.call(name, args))
286
- let result be call_tool(registry[name], args)
287
- give text(result)
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
- give "ERROR: " + err
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
- when starts_with(out, "ERROR") or starts_with(out, "DENIED")
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
- set msgs to append(msgs, {"role": "assistant", "content": fin["text"], "tool_calls": []})
417
- emit(on_event, "assistant", fin["text"], tag)
418
- give {"messages": msgs, "text": fin["text"], "steps": steps, "usage": usage, "stopped": "too_many_errors"}
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