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/md.syn ADDED
@@ -0,0 +1,229 @@
1
+ -- lib/md.syn — markdown → ANSI para la terminal (sin dependencias)
2
+ -- render(md, color) → texto listo para print. color=false → texto plano legible (sin escapes).
3
+ -- inline(s, color) → solo formato inline (negrita, cursiva, `código`, links, ~~tachado~~).
4
+ -- MIGA: `matches` es full-match; para buscar/reemplazar son `capture`/`find_all`/`replace_re`
5
+ -- (backrefs \1). En strings "..." la barra va simple ("\*", "\d"): "\\d" NO es \d.
6
+ -- Los bloques ``` se copian tal cual (sin parseo inline). La web ya renderiza markdown por su cuenta.
7
+
8
+ use "./ui.syn" as ui
9
+
10
+ let ESC be decode(bytes("1b", "hex"))
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
28
+
29
+ task sgr(color, code, s)
30
+ when not color or s == ""
31
+ give s
32
+ give ESC + "[" + code + "m" + s + ESC + "[0m"
33
+
34
+ task rep(ch, n)
35
+ let out be ""
36
+ while length(out) < n
37
+ set out to out + ch
38
+ give out
39
+
40
+ -- ---------- inline ----------
41
+
42
+ task emphasis(s, color)
43
+ let out be s
44
+ -- links [texto](url) → texto url (antes que la cursiva: la url puede traer _ o *)
45
+ set out to replace_re(out, "\[([^\]]+)\]\(([^)]+)\)", sgr(color, "4", "\1") + sgr(color, "2", " \2"))
46
+ set out to replace_re(out, "\*\*([^*]+)\*\*", sgr(color, "1", "\1"))
47
+ set out to replace_re(out, "__([^_]+)__", sgr(color, "1", "\1"))
48
+ set out to replace_re(out, "~~([^~]+)~~", sgr(color, "9", "\1"))
49
+ -- cursiva: *x* solo si no está pegada a texto/número (2*3 no es cursiva). Dos pasadas:
50
+ -- el grupo \3 consume el separador, así que "*a* *b*" necesita la segunda.
51
+ let pass be 0
52
+ while pass < 2
53
+ set out to replace_re(out, "(^|[^\w*])\*([^*\s][^*]*?)\*($|[^\w*])", "\1" + sgr(color, "3", "\2") + "\3")
54
+ set out to replace_re(out, "(^|[^\w_])_([^_\s][^_]*?)_($|[^\w_])", "\1" + sgr(color, "3", "\2") + "\3")
55
+ set pass to pass + 1
56
+ give out
57
+
58
+ export task inline(s, color)
59
+ -- `código` primero: lo de adentro no se toca
60
+ let parts be split(s, "`")
61
+ when length(parts) < 3
62
+ give paths(emphasis(s, color), color)
63
+ let out be ""
64
+ let idx be 0
65
+ each p in parts
66
+ when idx == length(parts) - 1 and idx % 2 == 1
67
+ set out to out + "`" + paths(emphasis(p, color), color)
68
+ otherwise when idx % 2 == 1
69
+ set out to out + sgr(color, "36", p)
70
+ otherwise
71
+ set out to out + paths(emphasis(p, color), color)
72
+ set idx to idx + 1
73
+ give out
74
+
75
+ -- ---------- bloques ----------
76
+
77
+ task indent_of(line)
78
+ let n be 0
79
+ while n < length(line) and slice(line, n, n + 1) == " "
80
+ set n to n + 1
81
+ give n
82
+
83
+ task table_cells(line)
84
+ let t be trim(line)
85
+ when starts_with(t, "|")
86
+ set t to slice(t, 1, length(t))
87
+ when ends_with(t, "|")
88
+ set t to slice(t, 0, length(t) - 1)
89
+ give apply(trim, split(t, "|"))
90
+
91
+ -- rows: lista de listas de celdas (crudas); la primera es el header. Columnas alineadas al ancho
92
+ -- máximo de cada una (ancho visible = length de inline(c, false), sin escapes ni marcas).
93
+ task table_lines(rows, pre, color)
94
+ let widths be []
95
+ each r in rows
96
+ let i be 0
97
+ each c in r
98
+ when i >= length(widths)
99
+ set widths to append(widths, 0)
100
+ when ui.width(inline(c, false)) > widths[i]
101
+ set widths[i] to ui.width(inline(c, false))
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
120
+ let out be []
121
+ let ri be 0
122
+ each r in rows
123
+ -- cada celda → lista de líneas envueltas a su ancho
124
+ let wrapped be []
125
+ let height be 1
126
+ let i be 0
127
+ each c in r
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)
132
+ set i to i + 1
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
145
+ when ri == 0
146
+ let segs be []
147
+ each w in widths
148
+ set segs to append(segs, rep("─", w))
149
+ set out to append(out, pre + sgr(color, "2", join(segs, "─┼─")))
150
+ set ri to ri + 1
151
+ give out
152
+
153
+ task fence_top(lang, color)
154
+ let label be when lang != "" then " " + lang + " " otherwise ""
155
+ give sgr(color, "2", "┌──" + label + rep("─", width_now() - 3 - length(label)))
156
+
157
+ export task render(md, color)
158
+ let pre be " "
159
+ let out be []
160
+ let in_code be false
161
+ let fence be ""
162
+ let table be []
163
+ each line in split(md, "\n")
164
+ let t be trim(line)
165
+ let heading be capture(t, "^(#{1,6}) (.+)$")
166
+ let item be capture(t, "^(\d{1,3})[.)] (.*)$")
167
+ let is_row be starts_with(t, "|") and ends_with(t, "|") and not in_code
168
+ when length(table) > 0 and not is_row
169
+ each l in table_lines(table, pre, color)
170
+ set out to append(out, l)
171
+ set table to []
172
+ when in_code
173
+ when starts_with(t, fence)
174
+ set in_code to false
175
+ set out to append(out, pre + sgr(color, "2", "└" + rep("─", width_now() - 1)))
176
+ otherwise
177
+ set out to append(out, pre + sgr(color, "2", "│ ") + ui.cut(sgr(color, "36", line), width_now() - 2))
178
+ otherwise when starts_with(t, "```") or starts_with(t, "~~~")
179
+ set in_code to true
180
+ set fence to slice(t, 0, 3)
181
+ set out to append(out, pre + fence_top(trim(slice(t, 3, length(t))), color))
182
+ otherwise when heading != nothing
183
+ let lvl be length(heading[0])
184
+ let raw be trim(heading[1])
185
+ let title be inline(raw, color)
186
+ set out to append(out, "")
187
+ when lvl == 1
188
+ set out to append(out, pre + sgr(color, "1;36", upper(title)))
189
+ set out to append(out, pre + sgr(color, "36", rep("═", length(raw))))
190
+ otherwise when lvl == 2
191
+ set out to append(out, pre + sgr(color, "1;36", title))
192
+ set out to append(out, pre + sgr(color, "2;36", rep("─", length(raw))))
193
+ otherwise
194
+ set out to append(out, pre + sgr(color, "1", title))
195
+ otherwise when matches(t, "(-{3,}|\*{3,}|_{3,})")
196
+ set out to append(out, pre + sgr(color, "2", rep("─", width_now())))
197
+ otherwise when starts_with(t, ">")
198
+ set out to append(out, pre + sgr(color, "2", "▎ ") + sgr(color, "3", inline(trim(slice(t, 1, length(t))), color)))
199
+ otherwise when matches(t, "\|[\s:|-]+\|")
200
+ set table to table
201
+ otherwise when is_row
202
+ set table to append(table, table_cells(t))
203
+ otherwise when starts_with(t, "- ") or starts_with(t, "* ") or starts_with(t, "+ ") or item != nothing
204
+ let ind be rep(" ", indent_of(line))
205
+ let mark be when item != nothing then sgr(color, "36", item[0] + ".") otherwise sgr(color, "36", "•")
206
+ let rest be when item != nothing then item[1] otherwise slice(t, 2, length(t))
207
+ when starts_with(rest, "[ ] ")
208
+ set mark to sgr(color, "2", "☐")
209
+ set rest to slice(rest, 4, length(rest))
210
+ otherwise when starts_with(rest, "[x] ") or starts_with(rest, "[X] ")
211
+ set mark to sgr(color, "32", "☑")
212
+ set rest to slice(rest, 4, length(rest))
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
218
+ otherwise
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))
224
+ when length(table) > 0
225
+ each l in table_lines(table, pre, color)
226
+ set out to append(out, l)
227
+ when in_code
228
+ set out to append(out, pre + sgr(color, "2", "└" + rep("─", width_now() - 1)))
229
+ give join(out, "\n")
@@ -149,6 +149,41 @@ export task describe_call(name, args)
149
149
  give "skill install " + text(args["source"]) + " --skill " + text(args["name"]) + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"
150
150
  when name == "bash"
151
151
  give "$ " + one_line(when contains(args, "command") then text(args["command"]) otherwise "", 160)
152
+ -- lectura/búsqueda: como lo escribiría un humano en la shell
153
+ when name == "read"
154
+ let rng be when contains(args, "offset") or contains(args, "limit") then " (" + (when contains(args, "offset") then "desde " + text(floor(number(text(args["offset"])))) otherwise "") + (when contains(args, "limit") then " " + text(floor(number(text(args["limit"])))) + " líneas" otherwise "") + ")" otherwise ""
155
+ give "read " + arg(args, "path", ".") + rng
156
+ when name == "ls"
157
+ give "ls " + arg(args, "path", ".")
158
+ when name == "find"
159
+ give "find " + arg(args, "pattern", "*") + (when contains(args, "path") then " en " + text(args["path"]) otherwise "")
160
+ when name == "grep"
161
+ let flags be (when contains(args, "regex") and args["regex"] == true then " -E" otherwise "") + (when contains(args, "glob") then " --glob " + text(args["glob"]) otherwise "")
162
+ give "grep" + flags + " \"" + one_line(arg(args, "pattern", ""), 80) + "\"" + (when contains(args, "path") then " " + text(args["path"]) otherwise "")
163
+ when name == "edit"
164
+ give "edit " + arg(args, "path", "?")
165
+ when name == "write"
166
+ give "write " + arg(args, "path", "?") + (when contains(args, "content") then dim_len(text(args["content"])) otherwise "")
167
+ when name == "memory"
168
+ let mact be arg(args, "action", "list")
169
+ give "memory " + mact + (when contains(args, "name") then " " + text(args["name"]) otherwise "") + (when (mact == "write" or mact == "append") and contains(args, "content") then " — " + one_line(text(args["content"]), 70) otherwise "")
170
+ when name == "process"
171
+ let pact be arg(args, "action", "list")
172
+ give "process " + pact + (when contains(args, "name") then " " + text(args["name"]) otherwise "") + (when contains(args, "command") then " $ " + one_line(text(args["command"]), 100) otherwise "")
173
+ when name == "skill"
174
+ give "skill " + arg(args, "action", "load") + " " + arg(args, "name", "?")
175
+ when name == "todo"
176
+ let n be when contains(args, "items") then length(args["items"]) otherwise 0
177
+ let done be when contains(args, "items") then length(where(args["items"], (i) => contains(i, "status") and text(i["status"]) == "done")) otherwise 0
178
+ give "todo " + text(done) + "/" + text(n) + " hechas"
152
179
  when contains(args, "path")
153
180
  give name + " " + text(args["path"])
154
- give name + " " + json_encode(args)
181
+ give name + " " + one_line(json_encode(args), 160)
182
+
183
+ task arg(args, key, default)
184
+ when contains(args, key)
185
+ give text(args[key])
186
+ give default
187
+
188
+ task dim_len(s)
189
+ give " (" + text(length(split(s, "\n"))) + " líneas)"
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 8 read-only calls in a row without acting you get a warning, after 16 read/ls/find/grep are refused until you act; a file read twice unchanged comes back as a one-line receipt.
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
 
@@ -16,8 +16,11 @@ export task tool(path, old_string, new_string, replace_all)
16
16
  when n > 1
17
17
  when not all
18
18
  raise(`old_string appears {text(n)} times in {shown}; include more surrounding context to make it unique, or pass replace_all=true`)
19
- write_file(real, join(parts, new_string))
19
+ let after be join(parts, new_string)
20
+ write_file(real, after)
20
21
  c.mark_observed(real)
22
+ -- antes/después para que la UI muestre el diff (chat.syn / web); el modelo solo recibe la línea de abajo
23
+ share {"path": shown, "old": content, "new": after} as "lampson:ui:diff"
21
24
  give `edited {shown}: {text(n)} replacement(s)`
22
25
 
23
26
  export let SPEC be {
@@ -6,10 +6,14 @@ export task tool(path, content)
6
6
  require file("workspace/*")
7
7
  let real be c.ws(path)
8
8
  -- sobrescribir un archivo existente exige haberlo leído (y que no haya cambiado): igual que edit
9
+ let before be ""
9
10
  when file_exists(real)
10
11
  c.check_observed(real, "write")
12
+ set before to read_file(real)
11
13
  write_file(real, content)
12
14
  c.mark_observed(real)
15
+ -- antes/después para que la UI muestre el diff (archivo nuevo: before = "")
16
+ share {"path": c.unws(real), "old": before, "new": content} as "lampson:ui:diff"
13
17
  give `wrote {text(length(content))} chars to {c.unws(real)}`
14
18
 
15
19
  export let SPEC be {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lampson",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "A coding agent written in Synsema: terminal + web, tools confined to the mounted project, lamps (your own tool plugins), LSP, MCP, sub-agents.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,7 +32,7 @@
32
32
  "node": ">=18"
33
33
  },
34
34
  "dependencies": {
35
- "synsema": "^0.6.10"
35
+ "synsema": "^0.6.11"
36
36
  },
37
37
  "scripts": {
38
38
  "test": "pwsh -NoProfile -File tests/run.ps1"
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</h2>
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; }
@@ -3,7 +3,7 @@ name: synsema
3
3
  description: Writing, checking, running and testing Synsema (.syn) code — syntax reflexes, capabilities, live processes / pseudo-terminals, and the runtime traps that cost hours. Load before touching any .syn file.
4
4
  ---
5
5
 
6
- # Synsema quick reference (v0.6.10)
6
+ # Synsema quick reference (v0.6.11)
7
7
 
8
8
  > Curated 10 KB summary for the agent (the full reference is ~450 KB and lives in the user's editor
9
9
  > skill). Kept in sync by hand with each `synsema update`; if `synsema --version` is newer than the
@@ -22,6 +22,11 @@ description: Writing, checking, running and testing Synsema (.syn) code — synt
22
22
  - `try` … `recover err` (recover SWALLOWS; `raise(err)` to re-throw) · `raise("msg")`
23
23
  - `contains(xs, x)` (on maps checks KEYS) · `append(xs, x)` returns a NEW list → `set xs to append(xs, x)`
24
24
  - `apply(f, xs)`, `where(xs, p)`, `sort_by(xs, f)`, `slice(xs, a, b)`, `split/join/trim/lower/upper`
25
+ - Text/regex: `replace_text(t, old, new)` (literal), `replace_re(t, re, rep)` (`\1` backrefs), `capture(t, re)`
26
+ (first match; with groups → list), `find_all`, `matches` (**full match** only — not a search).
27
+ There is NO `replace`, `regex_replace`, `chars`, `repeat`. Rust regex: no lookahead/lookbehind.
28
+ In `"..."` strings backslashes are literal: write `"\d"`, `"\*"` (`"\\d"` is a literal `\d`).
29
+ Anonymous functions: `(x) => expr` only (no inline `task(x)`).
25
30
  - `json_encode / json_decode` · `length` · `text(x)` · `number(s)` (ALWAYS float → `floor()` for ints)
26
31
  - Modules: `use "./m.syn" as m` (local only, never `../`), `export task/let`. A module cannot have
27
32
  top-level `require` or `serve` — the ENTRY file grants capabilities.
@@ -50,6 +55,12 @@ description: Writing, checking, running and testing Synsema (.syn) code — synt
50
55
  `proc_spawn` in a handler is gone when the handler returns. A process that must outlive requests lives
51
56
  inside an `agent` spawned from the handler (own lifecycle; blackboard `share/observe` + `bus_*` are shared
52
57
  with handlers). That is how lampson's `process` tool works (`lib/tools/proc.syn`).
58
+ - **Own terminal / raw keys (v0.6.11+)**: `let h be term_open({"ctrl_c": "exit"})` → `nothing` without a
59
+ TTY / under `test`/`serve` (fall back to `read_line`); `term_recv(h, secs)` → `{type: "key", key, text,
60
+ ctrl, alt, shift}` (`key` = `"char"|"enter"|"tab"|"backspace"|"up"|…`), `paste`, `resize`, `eof`;
61
+ draw with `term_write(h, ansi)` (**`print` stays buffered**); `term_size(h)`; `term_close(h)`. Alt+Enter
62
+ always arrives (Shift+Enter needs kitty protocol). `ask/approve` still work while open. Lampson's line
63
+ editor is `lib/line.syn`; drive a terminal UI in tests via `proc_spawn(…, {pty: true})` (`tty_test.syn`).
53
64
  - **File watch (v0.6.9+)**: `let w be watch("src", {"interval": 0.2, "ignore": ["*.tmp"]})` → events
54
65
  `{type: "create"|"modify"|"delete", path, is_dir}` via `watch_recv(w, secs)` or `select`; polling with a
55
66
  snapshot (latency = interval), `watch_close(w)`. Gate: `file("src")` + `file("src/*")`.
@@ -67,7 +78,9 @@ description: Writing, checking, running and testing Synsema (.syn) code — synt
67
78
  - `localhost` resolves to IPv6; use `127.0.0.1`.
68
79
  - `run("bash", ...)` hangs on Windows (WSL bash) → `C:\Program Files\Git\bin\bash.exe` or `cmd /c`.
69
80
  - A task named `run` shadows the builtin `run` (infinite recursion).
70
- - Reserved words that break variable/param names: `reason task ask stop decide analyze generate show approve confirm`.
81
+ - Reserved words that break variable/param names: `reason task ask stop type run decide analyze generate show approve confirm`.
82
+ - Reading a MISSING map key is a runtime error (`Map has no key 'x'`), not `nothing` → `contains(m, "x")` first.
83
+ - Maps are passed by reference: `set m["k"] to v` inside a task IS visible to the caller (lists via `append` are not — it returns a new list).
71
84
  - `and`/`or` do NOT short-circuit → nest `when` before indexing.
72
85
  - No `merge`: add a key with `set m["k"] to v`. No `append_file`: read + write (atomic; parents created).
73
86
  - Runtime error messages are Capitalized (`Not a directory: …`) and `contains` is case-sensitive → compare `lower(text(err))`.