lampson 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +29 -0
- package/LICENSE +21 -0
- package/README.md +382 -0
- package/bin/lampson.js +81 -0
- package/chat.syn +799 -0
- package/lamps/example-hello/lamp.json +16 -0
- package/lamps/example-hello/lamp.syn +19 -0
- package/lampson.cmd +4 -0
- package/lampson.ps1 +88 -0
- package/lampson.sh +42 -0
- package/lib/agents.syn +471 -0
- package/lib/git.syn +58 -0
- package/lib/lamps.syn +386 -0
- package/lib/loop.syn +455 -0
- package/lib/lsp.syn +503 -0
- package/lib/mcp.syn +403 -0
- package/lib/permission.syn +154 -0
- package/lib/prompt.syn +75 -0
- package/lib/provider.syn +522 -0
- package/lib/session.syn +111 -0
- package/lib/settings.syn +70 -0
- package/lib/skills.syn +179 -0
- package/lib/tools/bash.syn +105 -0
- package/lib/tools/common.sh +49 -0
- package/lib/tools/common.syn +91 -0
- package/lib/tools/edit.syn +32 -0
- package/lib/tools/find.syn +35 -0
- package/lib/tools/grep.syn +31 -0
- package/lib/tools/img.ps1 +36 -0
- package/lib/tools/img.sh +22 -0
- package/lib/tools/ls.syn +18 -0
- package/lib/tools/memo.syn +148 -0
- package/lib/tools/proc.sh +42 -0
- package/lib/tools/proc.syn +314 -0
- package/lib/tools/process.syn +46 -0
- package/lib/tools/read.syn +25 -0
- package/lib/tools/skill.syn +14 -0
- package/lib/tools/todo.syn +97 -0
- package/lib/tools/write.syn +22 -0
- package/lib/tools.syn +198 -0
- package/lib/trace.syn +116 -0
- package/lib/tree.syn +59 -0
- package/lib/update.syn +57 -0
- package/package.json +40 -0
- package/public/fonts/plex-mono-400-latin-ext.woff2 +0 -0
- package/public/fonts/plex-mono-400-latin.woff2 +0 -0
- package/public/fonts/plex-mono-600-latin-ext.woff2 +0 -0
- package/public/fonts/plex-mono-600-latin.woff2 +0 -0
- package/public/fonts/plex-serif-400-latin-ext.woff2 +0 -0
- package/public/fonts/plex-serif-400-latin.woff2 +0 -0
- package/public/fonts/plex-serif-400i-latin-ext.woff2 +0 -0
- package/public/fonts/plex-serif-400i-latin.woff2 +0 -0
- package/public/fonts/plex-serif-600-latin-ext.woff2 +0 -0
- package/public/fonts/plex-serif-600-latin.woff2 +0 -0
- package/public/index.html +1268 -0
- package/public/vendor/xterm-addon-fit.js +2 -0
- package/public/vendor/xterm.css +218 -0
- package/public/vendor/xterm.js +2 -0
- package/skills/debugging/SKILL.md +33 -0
- package/skills/lampson/SKILL.md +117 -0
- package/skills/synsema/SKILL.md +75 -0
- package/web.syn +468 -0
|
@@ -0,0 +1,1268 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="es">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Lampson</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<link rel="stylesheet" href="/vendor/xterm.css">
|
|
8
|
+
<script src="/vendor/xterm.js"></script>
|
|
9
|
+
<script src="/vendor/xterm-addon-fit.js"></script>
|
|
10
|
+
<script>(function(){try{var t=localStorage.getItem('lampson.theme');if(t!=='light'&&t!=='dark'){t='dark';}document.documentElement.setAttribute('data-theme',t);}catch(e){}})();</script>
|
|
11
|
+
<style>
|
|
12
|
+
/* Misma dirección que docs.synsema.com: tinta sobre papel, una superfamilia (IBM Plex) en dos
|
|
13
|
+
voces — la SERIF para lo que se lee (mensajes, hero), la MONO para estructura y maquinaria
|
|
14
|
+
(header, sidebar, pasos, código, controles). El color se raciona: azul = link / dónde estás,
|
|
15
|
+
rubric rojo = peligro y permisos (la única cosa que vale la pena ver desde lejos). */
|
|
16
|
+
@font-face{font-family:"Plex Serif";font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-serif-400-latin.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
|
17
|
+
@font-face{font-family:"Plex Serif";font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-serif-400-latin-ext.woff2) format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
|
|
18
|
+
@font-face{font-family:"Plex Serif";font-style:italic;font-weight:400;font-display:swap;src:url(/fonts/plex-serif-400i-latin.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
|
19
|
+
@font-face{font-family:"Plex Serif";font-style:italic;font-weight:400;font-display:swap;src:url(/fonts/plex-serif-400i-latin-ext.woff2) format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
|
|
20
|
+
@font-face{font-family:"Plex Serif";font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/plex-serif-600-latin.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
|
21
|
+
@font-face{font-family:"Plex Serif";font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/plex-serif-600-latin-ext.woff2) format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
|
|
22
|
+
@font-face{font-family:"Plex Mono";font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-mono-400-latin.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
|
23
|
+
@font-face{font-family:"Plex Mono";font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-mono-400-latin-ext.woff2) format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
|
|
24
|
+
@font-face{font-family:"Plex Mono";font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/plex-mono-600-latin.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
|
25
|
+
@font-face{font-family:"Plex Mono";font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/plex-mono-600-latin-ext.woff2) format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
|
|
26
|
+
|
|
27
|
+
:root, :root[data-theme=dark] {
|
|
28
|
+
--paper:#131412; --paper-2:#1b1c19; --paper-3:#212320;
|
|
29
|
+
--ink:#e9e8df; --ink-2:#9b9c90; --ink-3:#848678;
|
|
30
|
+
--rule:#2b2c27; --rule-2:#3c3d36;
|
|
31
|
+
--accent:#8fb4dc; --accent-bg:#1b2836;
|
|
32
|
+
--rubric:#d98670; --str:#a8bd84; --amber:#d9b26f;
|
|
33
|
+
--sel:#33465b; --term-blue:#4f86d1;
|
|
34
|
+
--serif:"Plex Serif",Georgia,"Times New Roman",serif;
|
|
35
|
+
--mono:"Plex Mono",ui-monospace,SFMono-Regular,Consolas,"Liberation Mono",monospace;
|
|
36
|
+
--r:3px; --side:280px;
|
|
37
|
+
}
|
|
38
|
+
:root[data-theme=light] {
|
|
39
|
+
--paper:#fcfcfa; --paper-2:#f4f3ee; --paper-3:#ecebe3;
|
|
40
|
+
--ink:#1a1b18; --ink-2:#56584f; --ink-3:#6a6c61;
|
|
41
|
+
--rule:#e4e3da; --rule-2:#cfcec2;
|
|
42
|
+
--accent:#21578c; --accent-bg:#e9eef4;
|
|
43
|
+
--rubric:#99372a; --str:#4a6b3f; --amber:#8a6a1f;
|
|
44
|
+
--sel:#cfe0f0; --term-blue:#21578c;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
* { box-sizing:border-box; }
|
|
48
|
+
html, body { height:100%; }
|
|
49
|
+
body { margin:0; background:var(--paper); color:var(--ink); font:400 14.5px/1.6 var(--serif); -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
|
|
50
|
+
display:grid; grid-template-rows:auto 1fr; grid-template-columns:var(--side) 1fr; grid-template-areas:"top top" "side main"; }
|
|
51
|
+
::selection { background:var(--sel); }
|
|
52
|
+
:focus-visible { outline:2px solid var(--accent); outline-offset:2px; border-radius:1px; }
|
|
53
|
+
button, select, textarea { font-family:inherit; }
|
|
54
|
+
.spacer { flex:1; }
|
|
55
|
+
|
|
56
|
+
/* ---- header: fila 1 quién/dónde, fila 2 contexto y cómo corre el agente ---- */
|
|
57
|
+
header { grid-area:top; border-bottom:1px solid var(--rule); background:var(--paper); }
|
|
58
|
+
.hrow { display:flex; align-items:center; gap:14px; padding:0 18px; height:44px; min-width:0; }
|
|
59
|
+
.hrow.sub { height:32px; border-top:1px solid var(--rule); gap:12px; }
|
|
60
|
+
.brand { font:600 14px/1 var(--mono); color:var(--ink); letter-spacing:-.01em; white-space:nowrap; }
|
|
61
|
+
.brand .proj { color:var(--ink-3); font-weight:400; margin-left:.6em; }
|
|
62
|
+
.hbtn { display:flex; align-items:center; gap:6px; font:600 10.5px/1 var(--mono); text-transform:uppercase; letter-spacing:.09em; color:var(--ink-2); background:none; border:1px solid var(--rule-2); border-radius:2px; padding:7px 12px; cursor:pointer; }
|
|
63
|
+
.hbtn:hover { color:var(--ink); border-color:var(--ink-3); }
|
|
64
|
+
.hbtn.upd { color:var(--amber); border-color:var(--amber); }
|
|
65
|
+
.hbtn.upd:hover { color:var(--ink); border-color:var(--ink); }
|
|
66
|
+
.theme-toggle { display:flex; align-items:center; justify-content:center; background:none; border:0; color:var(--ink-3); cursor:pointer; padding:6px; border-radius:var(--r); line-height:0; }
|
|
67
|
+
.theme-toggle:hover { color:var(--ink); background:var(--paper-2); }
|
|
68
|
+
header select { font:400 11.5px/1 var(--mono); color:var(--ink-2); background:var(--paper); border:1px solid var(--rule); border-radius:var(--r); padding:5px 6px; cursor:pointer; }
|
|
69
|
+
header select:hover { border-color:var(--rule-2); color:var(--ink); }
|
|
70
|
+
.pill { font:400 11.5px/1 var(--mono); color:var(--ink-3); letter-spacing:.02em; white-space:nowrap; max-width:200px; overflow:hidden; text-overflow:ellipsis; flex:none; cursor:pointer; border-bottom:1px dashed transparent; }
|
|
71
|
+
.pill:hover { color:var(--ink); border-bottom-color:var(--rule-2); }
|
|
72
|
+
.pill.nokey { color:var(--rubric); }
|
|
73
|
+
.pill.on { color:var(--accent); }
|
|
74
|
+
/* ---- lámparas: modal grande, lista + detalle ---- */
|
|
75
|
+
.modal .box.lampsbox { width:min(1100px, 94vw); height:min(760px, 90vh); display:flex; flex-direction:column; padding:14px 18px 12px; }
|
|
76
|
+
.lampsbox .lhead { display:flex; align-items:center; gap:12px; padding-bottom:10px; border-bottom:1px solid var(--rule); }
|
|
77
|
+
.lampsbox .lhead h3 { margin:0; font:600 16px/1.2 var(--serif); }
|
|
78
|
+
.lampsbox .lhead .ds { color:var(--ink-3); font:400 11.5px/1 var(--mono); }
|
|
79
|
+
.lampsbox .lghost { font:400 13px/1 var(--mono); width:26px; height:26px; border:1px solid var(--rule); background:transparent; color:var(--ink-3); border-radius:var(--r); cursor:pointer; }
|
|
80
|
+
.lampsbox .lghost:hover { color:var(--ink); border-color:var(--rule-2); }
|
|
81
|
+
.lampsbox .lbody { flex:1; min-height:0; display:grid; grid-template-columns:260px minmax(0,1fr); grid-template-areas:"list detail"; gap:0; }
|
|
82
|
+
.lampsbox .llist { grid-area:list; }
|
|
83
|
+
.lampsbox .ldetail { grid-area:detail; }
|
|
84
|
+
.lampsbox .llist { display:flex; flex-direction:column; border-right:1px solid var(--rule); padding:10px 12px 8px 0; min-height:0; }
|
|
85
|
+
.lampsbox .llist input { font:400 12px/1.4 var(--mono); height:30px; box-sizing:border-box; background:transparent; color:var(--ink); border:1px solid var(--rule); border-radius:var(--r); padding:0 8px; margin:0 0 8px; width:100%; }
|
|
86
|
+
.lampsbox .llist input:focus { outline:none; border-color:var(--accent); }
|
|
87
|
+
.lampsbox #lampsItems { flex:1; overflow:auto; min-height:0; }
|
|
88
|
+
.lampsbox .li { display:grid; grid-template-columns:14px 1fr; gap:8px; align-items:start; padding:7px 8px; border-radius:var(--r); cursor:pointer; font:400 12px/1.35 var(--mono); }
|
|
89
|
+
.lampsbox .li:hover { background:var(--paper-2, rgba(127,127,127,.08)); }
|
|
90
|
+
.lampsbox .li.sel { background:var(--paper-2, rgba(127,127,127,.12)); box-shadow:inset 2px 0 0 var(--accent); }
|
|
91
|
+
.lampsbox .li .dot { color:var(--ink-3); margin-top:1px; }
|
|
92
|
+
.lampsbox .li.on .dot { color:var(--accent); }
|
|
93
|
+
.lampsbox .li.broken .dot { color:var(--rubric); }
|
|
94
|
+
.lampsbox .li .nm { color:var(--ink); font-weight:600; }
|
|
95
|
+
.lampsbox .li .meta { color:var(--ink-3); font-size:11px; }
|
|
96
|
+
.lampsbox .lcount { color:var(--ink-3); font:400 11px/1 var(--mono); padding-top:8px; border-top:1px solid var(--rule); }
|
|
97
|
+
.lampsbox .ldetail { overflow:auto; min-height:0; padding:12px 4px 8px 18px; font:400 12.5px/1.5 var(--mono); }
|
|
98
|
+
.lampsbox .ldetail .none { color:var(--ink-3); font:400 13px/1.6 var(--serif); max-width:520px; }
|
|
99
|
+
.lampsbox .dhead { display:flex; align-items:center; gap:12px; flex-wrap:wrap; }
|
|
100
|
+
.lampsbox .dhead .nm { font:600 18px/1.2 var(--mono); color:var(--ink); }
|
|
101
|
+
.lampsbox .dhead .meta { color:var(--ink-3); font-size:11.5px; }
|
|
102
|
+
.lampsbox .sw { display:inline-flex; align-items:center; gap:6px; margin-left:auto; cursor:pointer; user-select:none; font:400 11.5px/1 var(--mono); color:var(--ink-3); }
|
|
103
|
+
.lampsbox .sw input { accent-color:var(--accent); width:16px; height:16px; margin:0; }
|
|
104
|
+
.lampsbox .sw.on { color:var(--accent); }
|
|
105
|
+
.lampsbox .ddesc { color:var(--ink-2); font:400 13px/1.5 var(--serif); margin:8px 0 4px; max-width:70ch; }
|
|
106
|
+
.lampsbox .dcap { color:var(--ink-3); font-size:11.5px; margin-bottom:12px; word-break:break-all; }
|
|
107
|
+
.lampsbox .dcap b { color:var(--rubric); font-weight:600; }
|
|
108
|
+
.lampsbox .dcap.err { color:var(--rubric); }
|
|
109
|
+
.lampsbox .off-note { color:var(--ink-3); font:400 12px/1.5 var(--serif); border:1px dashed var(--rule-2); border-radius:var(--r); padding:8px 10px; margin:8px 0 12px; }
|
|
110
|
+
.lampsbox .tool { border:1px solid var(--rule); border-radius:var(--r); padding:10px 12px; margin-bottom:10px; }
|
|
111
|
+
.lampsbox .tool .th { display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; }
|
|
112
|
+
.lampsbox .tool .th code { font-weight:600; color:var(--ink); }
|
|
113
|
+
.lampsbox .tool .th .ds { color:var(--ink-3); font:400 12px/1.4 var(--serif); }
|
|
114
|
+
.lampsbox .lf { display:grid; grid-template-columns:120px 1fr; gap:2px 10px; align-items:center; margin-top:8px; }
|
|
115
|
+
.lampsbox .lf .lk { color:var(--ink); }
|
|
116
|
+
.lampsbox .lf .req { color:var(--rubric); font-size:10.5px; }
|
|
117
|
+
.lampsbox .lf .ds { grid-column:2; color:var(--ink-3); font:400 11.5px/1.4 var(--serif); }
|
|
118
|
+
.lampsbox .lf input, .lampsbox .lf select, .lampsbox .lf textarea { font:400 12px/1.4 var(--mono); background:transparent; color:var(--ink); border:1px solid var(--rule); border-radius:var(--r); padding:4px 6px; width:100%; max-width:420px; }
|
|
119
|
+
.lampsbox .lf textarea { grid-column:2; min-height:56px; resize:vertical; }
|
|
120
|
+
.lampsbox .tool .go { margin-top:10px; display:flex; align-items:center; gap:10px; }
|
|
121
|
+
.lampsbox .tool .go button { font:400 12px/1 var(--mono); padding:5px 10px; }
|
|
122
|
+
.lampsbox .tool .go .st { color:var(--ink-3); font-size:11.5px; }
|
|
123
|
+
.lampsbox .tool pre.out { margin-top:8px; max-height:38vh; overflow:auto; font:400 11.5px/1.45 var(--mono); border:1px solid var(--rule); border-radius:var(--r); padding:8px; white-space:pre-wrap; }
|
|
124
|
+
.lampsbox .dfoot { display:flex; align-items:center; gap:12px; margin-top:14px; padding-top:10px; border-top:1px solid var(--rule); color:var(--ink-3); font-size:11.5px; }
|
|
125
|
+
.lampsbox .dfoot .del { margin-left:auto; cursor:pointer; color:var(--rubric); }
|
|
126
|
+
.lampsbox .dfoot .del.ask .yes { color:var(--rubric); cursor:pointer; font-weight:600; }
|
|
127
|
+
.lampsbox .dfoot .del.ask .no { cursor:pointer; }
|
|
128
|
+
.lampsbox .lhelp { flex:1; overflow:auto; padding:14px 6px; font:400 13.5px/1.6 var(--serif); color:var(--ink-2); max-width:78ch; }
|
|
129
|
+
.lampsbox .lhelp h4 { margin:0 0 8px; font:600 15px/1.3 var(--serif); color:var(--ink); }
|
|
130
|
+
.lampsbox .lhelp b { color:var(--ink); font-weight:600; }
|
|
131
|
+
.lampsbox .lhelp ul { margin:4px 0 12px 18px; padding:0; display:grid; gap:4px; }
|
|
132
|
+
.lampsbox .err { color:var(--rubric); font:400 11.5px/1.4 var(--mono); min-height:14px; }
|
|
133
|
+
.pill.new { animation: lampglow 1.2s ease-in-out 3; }
|
|
134
|
+
@keyframes lampglow { 0%,100% { color:var(--ink-3); } 50% { color:var(--accent); } }
|
|
135
|
+
.gitchip { display:none; align-items:center; gap:8px; font:400 11.5px/1 var(--mono); color:var(--ink-2); max-width:45%; flex:none; }
|
|
136
|
+
.gitchip .br { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
137
|
+
.gitchip b { font-weight:600; }
|
|
138
|
+
.gitchip .w { color:var(--amber); } .gitchip .g { color:var(--str); } .gitchip .r { color:var(--rubric); } .gitchip .c { color:var(--ink-3); font-weight:400; }
|
|
139
|
+
.hpath { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--ink-3); font:400 11.5px/1 var(--mono); }
|
|
140
|
+
|
|
141
|
+
/* ---- sidebar: secciones colapsables; el activo es una muesca en la regla, no una caja ---- */
|
|
142
|
+
aside { grid-area:side; border-right:1px solid var(--rule); background:var(--paper); display:flex; flex-direction:column; min-height:0; overflow:hidden; }
|
|
143
|
+
.sec { display:flex; flex-direction:column; min-height:0; flex:none; overflow:hidden; }
|
|
144
|
+
.sec + .sec { border-top:1px solid var(--rule); }
|
|
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
|
+
.sec h2:hover { color:var(--ink); }
|
|
147
|
+
.sec .caret { width:9px; font-size:9px; color:var(--ink-3); }
|
|
148
|
+
.sec .cnt { margin-left:auto; letter-spacing:0; text-transform:none; }
|
|
149
|
+
.sec .body { display:none; overflow:auto; min-height:0; padding:0 10px 10px 16px; }
|
|
150
|
+
.sec.open .body { display:block; }
|
|
151
|
+
.sec:not(.grow).open .body { max-height:22vh; }
|
|
152
|
+
.sec.grow.open { flex:1; }
|
|
153
|
+
.sec.grow.open .body { flex:1; }
|
|
154
|
+
#tree { font:400 12.5px/1.5 var(--mono); }
|
|
155
|
+
.node { user-select:none; }
|
|
156
|
+
.row { display:flex; align-items:center; gap:6px; padding:3px 6px 3px 8px; cursor:pointer; color:var(--ink-2); white-space:nowrap; margin-left:-1px; border-left:2px solid transparent; }
|
|
157
|
+
.row:hover { color:var(--ink); }
|
|
158
|
+
.row.dir { color:var(--ink); }
|
|
159
|
+
.row .tw { width:9px; color:var(--ink-3); font-size:9px; }
|
|
160
|
+
.row.active { color:var(--ink); font-weight:600; border-left-color:var(--accent); }
|
|
161
|
+
.row.gM > span:nth-child(2) { color:var(--amber); }
|
|
162
|
+
.row.gA > span:nth-child(2), .row.gU > span:nth-child(2) { color:var(--str); }
|
|
163
|
+
.row.gD > span:nth-child(2) { color:var(--rubric); text-decoration:line-through; }
|
|
164
|
+
.row .gs { margin-left:auto; font-size:10px; color:var(--ink-3); }
|
|
165
|
+
.kids { margin-left:12px; border-left:1px solid var(--rule); }
|
|
166
|
+
.kids.hidden { display:none; }
|
|
167
|
+
.s, .m, .p { display:flex; align-items:center; gap:8px; padding:4px 6px 4px 8px; cursor:pointer; color:var(--ink-2); font:400 12.5px/1.5 var(--mono); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-left:-1px; border-left:2px solid transparent; }
|
|
168
|
+
.s:hover, .m:hover, .p:hover { color:var(--ink); }
|
|
169
|
+
.s.active, .m.active, .p.active { color:var(--ink); border-left-color:var(--accent); }
|
|
170
|
+
.s .title { overflow:hidden; text-overflow:ellipsis; font-family:var(--serif); font-size:13px; }
|
|
171
|
+
.s .id { color:var(--ink-3); font-size:11px; flex:none; }
|
|
172
|
+
.m b { color:var(--ink); font-weight:600; flex:none; }
|
|
173
|
+
.m .t { overflow:hidden; text-overflow:ellipsis; font-family:var(--serif); font-size:13px; }
|
|
174
|
+
.p .st { width:7px; height:7px; border-radius:50%; background:var(--ink-3); flex:none; }
|
|
175
|
+
.p.run .st { background:var(--str); }
|
|
176
|
+
.p .nm { font-weight:600; color:var(--ink); }
|
|
177
|
+
.p .cm { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1; }
|
|
178
|
+
.p .port { color:var(--amber); font-weight:600; text-decoration:none; flex:none; }
|
|
179
|
+
.p .port:hover { text-decoration:underline; text-underline-offset:.18em; }
|
|
180
|
+
.p button { padding:1px 7px; font-size:10px; }
|
|
181
|
+
.none { color:var(--ink-3); cursor:default; }
|
|
182
|
+
.none:hover { color:var(--ink-3); }
|
|
183
|
+
|
|
184
|
+
/* ---- main ---- */
|
|
185
|
+
main { grid-area:main; display:grid; grid-template-rows:1fr auto; min-height:0; min-width:0; background:var(--paper); }
|
|
186
|
+
#stage { overflow:auto; min-height:0; }
|
|
187
|
+
#log { padding:26px 40px 30px; max-width:920px; }
|
|
188
|
+
#log.hero { max-width:none; min-height:100%; display:flex; align-items:center; padding:20px 40px; }
|
|
189
|
+
#viewer { display:none; padding:0; }
|
|
190
|
+
.vh { position:sticky; top:0; display:flex; gap:12px; align-items:center; padding:9px 16px; background:var(--paper-3); border-bottom:1px solid var(--rule); font:400 11.5px/1 var(--mono); color:var(--ink-3); letter-spacing:.03em; }
|
|
191
|
+
.vh b { color:var(--ink); font-weight:400; }
|
|
192
|
+
.vh .x { cursor:pointer; color:var(--ink-3); }
|
|
193
|
+
.vh .x:hover { color:var(--ink); }
|
|
194
|
+
#viewer pre { margin:0; padding:14px 18px; font:400 12.5px/1.62 var(--mono); tab-size:4; }
|
|
195
|
+
#viewer pre .ln { display:inline-block; width:4ch; margin-right:16px; color:var(--ink-3); text-align:right; user-select:none; }
|
|
196
|
+
#viewer pre.log { color:var(--ink-2); white-space:pre-wrap; }
|
|
197
|
+
/* terminal real (xterm.js sobre un pty del servidor) — ocupa la zona central como un archivo abierto */
|
|
198
|
+
#termpane { display:none; height:100%; flex-direction:column; }
|
|
199
|
+
#termpane .vh { position:static; }
|
|
200
|
+
#termpane .vh .st { width:7px; height:7px; border-radius:50%; background:var(--ink-3); }
|
|
201
|
+
#termpane.live .vh .st { background:var(--str); }
|
|
202
|
+
#xterm { flex:1; min-height:0; padding:10px 14px; background:var(--paper); }
|
|
203
|
+
#xterm .xterm { height:100%; }
|
|
204
|
+
#xterm .xterm-viewport { background:transparent !important; }
|
|
205
|
+
|
|
206
|
+
/* mensajes: el del usuario es una nota al margen (como el blockquote de la doc), el del agente es prosa */
|
|
207
|
+
.msg { margin:0 0 16px; }
|
|
208
|
+
.user { margin:26px 0 16px; padding:.85em 1.1em; background:var(--paper-2); border-left:2px solid var(--accent); white-space:pre-wrap; font-size:15px; }
|
|
209
|
+
.user::before { content:"vos"; display:block; font:400 10px/1 var(--mono); letter-spacing:.12em; text-transform:uppercase; color:var(--accent); margin-bottom:6px; }
|
|
210
|
+
.assistant { font-size:15px; line-height:1.62; }
|
|
211
|
+
.assistant p { margin:0 0 .9em; }
|
|
212
|
+
.assistant code { font:400 .86em/1.5 var(--mono); background:var(--paper-2); border-radius:2px; padding:.08em .3em; color:var(--ink); }
|
|
213
|
+
.assistant pre { background:var(--paper-2); border:1px solid var(--rule); border-radius:var(--r); padding:14px 16px; overflow-x:auto; margin:1em 0; }
|
|
214
|
+
.assistant pre code { background:none; padding:0; font-size:13px; line-height:1.62; }
|
|
215
|
+
.assistant ul, .assistant ol { margin:0 0 .9em; padding-left:1.25em; }
|
|
216
|
+
.assistant li { margin:.3em 0; }
|
|
217
|
+
.assistant li::marker { color:var(--ink-3); }
|
|
218
|
+
.assistant h1, .assistant h2, .assistant h3 { font:600 1.06rem/1.4 var(--serif); margin:1.4em 0 .5em; }
|
|
219
|
+
.assistant b, .assistant strong { font-weight:600; }
|
|
220
|
+
|
|
221
|
+
/* pasos del agente: maquinaria, en mono */
|
|
222
|
+
.step { display:grid; grid-template-columns:16px 1fr; column-gap:8px; margin:0 0 7px 2px; font:400 12.5px/1.5 var(--mono); color:var(--ink-2); }
|
|
223
|
+
.step .ic { text-align:center; color:var(--ink-3); }
|
|
224
|
+
.step .ok { color:var(--str); }
|
|
225
|
+
.step .bad { color:var(--rubric); }
|
|
226
|
+
.step .name { color:var(--ink); }
|
|
227
|
+
.step .cmd { white-space:pre-wrap; word-break:break-word; max-height:4.6em; overflow:hidden; }
|
|
228
|
+
.step .cmd.open { max-height:none; }
|
|
229
|
+
.step .more { color:var(--accent); cursor:pointer; font-size:11px; }
|
|
230
|
+
.step details { grid-column:2; min-width:0; }
|
|
231
|
+
.step summary { cursor:pointer; list-style:none; color:var(--ink-3); white-space:pre-wrap; word-break:break-word; max-height:3.2em; overflow:hidden; }
|
|
232
|
+
.step summary::before { content:"→ "; }
|
|
233
|
+
.step summary.bad { color:var(--rubric); }
|
|
234
|
+
.step pre { margin:6px 0 4px; padding:10px 12px; background:var(--paper-2); border:1px solid var(--rule); border-radius:var(--r); white-space:pre-wrap; max-height:340px; overflow:auto; font-size:12px; color:var(--ink-2); }
|
|
235
|
+
.denied { color:var(--rubric); font:400 12.5px/1.5 var(--mono); }
|
|
236
|
+
.meta { color:var(--ink-3); font:400 11px/1 var(--mono); letter-spacing:.03em; margin:4px 0 20px 2px; }
|
|
237
|
+
.working { display:flex; align-items:center; gap:8px; color:var(--ink-3); font:400 11.5px/1 var(--mono); margin:4px 0 14px 2px; }
|
|
238
|
+
.working .dot { width:7px; height:7px; border-radius:50%; background:var(--accent); animation:pulse 1s infinite; }
|
|
239
|
+
@keyframes pulse { 0%,100% { opacity:.25 } 50% { opacity:1 } }
|
|
240
|
+
|
|
241
|
+
/* permisos: lo único en rubric */
|
|
242
|
+
.approval .card { border:1px solid var(--rubric); border-radius:var(--r); padding:12px 14px; max-width:720px; }
|
|
243
|
+
.approval .why { color:var(--rubric); font:600 12px/1.5 var(--mono); margin-bottom:6px; }
|
|
244
|
+
.approval code { font:400 12.5px/1.5 var(--mono); display:block; background:var(--paper-2); padding:8px 10px; border-radius:2px; margin-bottom:10px; white-space:pre-wrap; }
|
|
245
|
+
.approval .btns { display:flex; gap:8px; }
|
|
246
|
+
|
|
247
|
+
/* botones: el primario es tinta sobre papel, el secundario solo borde */
|
|
248
|
+
button { font:600 10.5px/1 var(--mono); text-transform:uppercase; letter-spacing:.09em; color:var(--ink-2); background:none; border:1px solid var(--rule-2); border-radius:2px; padding:7px 12px; cursor:pointer; }
|
|
249
|
+
button:hover { color:var(--ink); border-color:var(--ink-3); }
|
|
250
|
+
button.primary { color:var(--paper); background:var(--ink); border-color:var(--ink); }
|
|
251
|
+
button.primary:hover { opacity:.85; color:var(--paper); }
|
|
252
|
+
button:disabled { opacity:.45; cursor:default; }
|
|
253
|
+
|
|
254
|
+
/* ---- hero: el primer golpe de vista ---- */
|
|
255
|
+
.empty { width:100%; max-width:1180px; margin:0 auto; color:var(--ink-2); }
|
|
256
|
+
.empty .eyebrow { font:400 10.5px/1 var(--mono); text-transform:uppercase; letter-spacing:.12em; color:var(--rubric); margin:0 0 12px; }
|
|
257
|
+
.empty h1 { font:600 2.1rem/1.15 var(--serif); letter-spacing:-.022em; color:var(--ink); margin:0 0 .4em; }
|
|
258
|
+
.empty .lead { font-size:1.06rem; margin:0 0 26px; max-width:720px; }
|
|
259
|
+
.empty .grid { display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); gap:14px; }
|
|
260
|
+
.empty .card { border:1px solid var(--rule); border-radius:var(--r); padding:14px 16px 12px; min-width:0; }
|
|
261
|
+
.empty .card h3 { font:400 10.5px/1 var(--mono); text-transform:uppercase; letter-spacing:.1em; color:var(--ink-3); margin:0 0 12px; }
|
|
262
|
+
.empty .card p { margin:0 0 8px; font-size:13.5px; line-height:1.5; }
|
|
263
|
+
.empty code { font:400 .88em/1.5 var(--mono); background:var(--paper-2); padding:.08em .3em; border-radius:2px; color:var(--ink); white-space:nowrap; }
|
|
264
|
+
.empty .k { font:600 12px/1 var(--mono); color:var(--ink); margin-right:.3em; }
|
|
265
|
+
.empty .try { cursor:pointer; color:var(--accent); }
|
|
266
|
+
.empty .try:hover { text-decoration:underline; text-underline-offset:.18em; }
|
|
267
|
+
@media (max-width:1400px) { .empty .grid { grid-template-columns:repeat(2, minmax(0,1fr)); } }
|
|
268
|
+
|
|
269
|
+
/* ---- composer ---- */
|
|
270
|
+
form { padding:12px 40px 12px; border-top:1px solid var(--rule); background:var(--paper); }
|
|
271
|
+
.composer { background:var(--paper-2); border:1px solid var(--rule); border-radius:var(--r); padding:10px 10px 8px 14px; transition:border-color .12s, box-shadow .12s; }
|
|
272
|
+
.composer:focus-within { border-color:var(--rule-2); box-shadow:0 0 0 3px var(--accent-bg); }
|
|
273
|
+
textarea { display:block; width:100%; resize:none; min-height:40px; max-height:220px; background:transparent; color:var(--ink); border:0; padding:2px 0; font:400 15px/1.55 var(--serif); }
|
|
274
|
+
textarea::placeholder { color:var(--ink-3); }
|
|
275
|
+
textarea:focus { outline:none; }
|
|
276
|
+
.crow { display:flex; align-items:center; gap:10px; margin-top:6px; }
|
|
277
|
+
/* imágenes pegadas: miniaturas en el composer y en el mensaje */
|
|
278
|
+
.attach { display:flex; gap:8px; flex-wrap:wrap; }
|
|
279
|
+
.attach:not(:empty) { margin:2px 0 8px; }
|
|
280
|
+
.attach .th, .user .th { position:relative; width:72px; height:72px; border:1px solid var(--rule); border-radius:var(--r); overflow:hidden; background:var(--paper-3); }
|
|
281
|
+
.attach .th img, .user .th img { width:100%; height:100%; object-fit:cover; display:block; }
|
|
282
|
+
.attach .th .rm { position:absolute; top:2px; right:2px; width:18px; height:18px; border-radius:50%; background:var(--paper); color:var(--ink-2); font:600 11px/18px var(--mono); text-align:center; cursor:pointer; border:1px solid var(--rule-2); }
|
|
283
|
+
.attach .th .rm:hover { color:var(--rubric); }
|
|
284
|
+
.attach .th .sz { position:absolute; left:0; right:0; bottom:0; background:rgba(0,0,0,.55); color:#fff; font:400 9.5px/1.4 var(--mono); text-align:center; }
|
|
285
|
+
.user .ims { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
|
|
286
|
+
.user .th { width:120px; height:90px; cursor:zoom-in; }
|
|
287
|
+
.composer.drop { border-color:var(--accent); }
|
|
288
|
+
.attach .novision { flex-basis:100%; color:var(--amber); font:400 11.5px/1.5 var(--mono); }
|
|
289
|
+
.crow .tools { color:var(--ink-3); font:400 11px/1 var(--mono); letter-spacing:.03em; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
290
|
+
.crow .keys { color:var(--ink-3); font:400 10.5px/1 var(--mono); letter-spacing:.03em; }
|
|
291
|
+
#send { width:34px; height:30px; padding:0; font-size:15px; display:inline-flex; align-items:center; justify-content:center; }
|
|
292
|
+
#stop { color:var(--rubric); border-color:var(--rubric); display:none; height:30px; }
|
|
293
|
+
form.busy #send { display:none; } form.busy #stop { display:inline-flex; align-items:center; gap:6px; }
|
|
294
|
+
.hint { display:none; }
|
|
295
|
+
|
|
296
|
+
/* ventana de proveedor / modelo / key — la única "modal" de la app, misma gramática que las tarjetas */
|
|
297
|
+
.modal { position:fixed; inset:0; background:rgba(0,0,0,.45); display:flex; align-items:center; justify-content:center; z-index:50; }
|
|
298
|
+
.modal .box { width:min(560px, 92vw); background:var(--paper); border:1px solid var(--rule-2); border-radius:var(--r); padding:22px 24px 18px; box-shadow:0 10px 30px rgba(0,0,0,.35); }
|
|
299
|
+
.modal .eyebrow { font:400 10.5px/1 var(--mono); text-transform:uppercase; letter-spacing:.12em; color:var(--rubric); margin:0 0 10px; }
|
|
300
|
+
.modal h3 { font:600 1.45rem/1.2 var(--serif); letter-spacing:-.015em; margin:0 0 .4em; }
|
|
301
|
+
.modal .lead { color:var(--ink-2); margin:0 0 16px; font-size:14px; }
|
|
302
|
+
.modal .lead code { font:400 .88em/1.5 var(--mono); background:var(--paper-2); padding:.08em .3em; border-radius:2px; color:var(--ink); }
|
|
303
|
+
.plist { border-top:1px solid var(--rule); border-bottom:1px solid var(--rule); max-height:40vh; overflow:auto; margin:0 0 14px; }
|
|
304
|
+
.plist .pr { display:flex; align-items:center; gap:10px; padding:7px 10px; cursor:pointer; font:400 12.5px/1.4 var(--mono); color:var(--ink-2); border-left:2px solid transparent; }
|
|
305
|
+
.plist .pr:hover { color:var(--ink); }
|
|
306
|
+
.plist .pr.on { color:var(--ink); border-left-color:var(--accent); background:var(--paper-2); }
|
|
307
|
+
.plist .pr b { width:110px; font-weight:600; color:var(--ink); }
|
|
308
|
+
.plist .pr .dm { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--ink-3); }
|
|
309
|
+
.plist .pr .k { font-size:11px; color:var(--str); }
|
|
310
|
+
.plist .pr .k.no { color:var(--ink-3); }
|
|
311
|
+
.modal label { display:flex; align-items:center; gap:12px; font:400 10.5px/1 var(--mono); text-transform:uppercase; letter-spacing:.1em; color:var(--ink-3); margin:0 0 10px; }
|
|
312
|
+
.modal input { flex:1; font:400 13px/1.4 var(--mono); color:var(--ink); background:var(--paper-2); border:1px solid var(--rule); border-radius:var(--r); padding:7px 10px; }
|
|
313
|
+
.modal input:focus { outline:none; border-color:var(--rule-2); box-shadow:0 0 0 3px var(--accent-bg); }
|
|
314
|
+
.modal select { flex:1; font:400 13px/1.4 var(--mono); color:var(--ink); background:var(--paper-2); border:1px solid var(--rule); border-radius:var(--r); padding:7px 10px; }
|
|
315
|
+
.modal .btns { display:flex; gap:8px; align-items:center; margin-top:14px; }
|
|
316
|
+
.modal .err { color:var(--rubric); font:400 12px/1.4 var(--mono); }
|
|
317
|
+
/* el modal de lámparas usa formularios propios: anular las reglas genéricas del modal (label uppercase en
|
|
318
|
+
flex, input/select con flex:1 — el buscador crecía a toda la columna) con mayor especificidad */
|
|
319
|
+
.modal .box.lampsbox .llist input { flex:none; height:30px; padding:0 8px; background:transparent; }
|
|
320
|
+
.modal .box.lampsbox label.lf { display:grid; grid-template-columns:120px minmax(0,1fr); gap:2px 10px; align-items:center; margin:8px 0 0; font:400 12px/1.4 var(--mono); text-transform:none; letter-spacing:0; color:var(--ink); }
|
|
321
|
+
.modal .box.lampsbox label.lf .lk { color:var(--ink); }
|
|
322
|
+
.modal .box.lampsbox label.lf .ds { grid-column:2; color:var(--ink-3); font:400 11.5px/1.4 var(--serif); text-transform:none; letter-spacing:0; }
|
|
323
|
+
.modal .box.lampsbox label.lf input, .modal .box.lampsbox label.lf select, .modal .box.lampsbox label.lf textarea { flex:none; width:100%; max-width:420px; padding:4px 6px; font:400 12px/1.4 var(--mono); background:transparent; }
|
|
324
|
+
.modal .box.lampsbox label.lf textarea { grid-column:2; }
|
|
325
|
+
.modal .box.lampsbox label.sw { display:inline-flex; margin:0 0 0 auto; gap:6px; font:400 11.5px/1 var(--mono); text-transform:none; letter-spacing:0; }
|
|
326
|
+
.modal .box.lampsbox label.sw input { flex:none; width:16px; height:16px; padding:0; }
|
|
327
|
+
.modal .box.lampsbox .tool .th { display:grid; grid-template-columns:auto 1fr auto; gap:8px 10px; align-items:baseline; }
|
|
328
|
+
.modal .box.lampsbox .tool .th .ds { text-transform:none; }
|
|
329
|
+
.modal .box.lampsbox .lhead .eyebrow { margin:0; }
|
|
330
|
+
.modal .box.lampsbox .lghost { display:inline-flex; align-items:center; justify-content:center; width:26px; height:26px; padding:0; margin:0; box-sizing:border-box; font:400 13px/1 var(--mono); letter-spacing:0; text-transform:none; border:1px solid var(--rule); border-radius:var(--r); background:transparent; color:var(--ink-3); }
|
|
331
|
+
.modal .box.lampsbox .lghost + .lghost { margin-left:6px; }
|
|
332
|
+
.modal .box.lampsbox .lhelp { flex:1; min-height:0; overflow:auto; max-width:none; padding:16px 24px 8px 6px; }
|
|
333
|
+
.modal .box.lampsbox .lhelp h4, .modal .box.lampsbox .lhelp p, .modal .box.lampsbox .lhelp ul { max-width: 78ch; }
|
|
334
|
+
.modal .box.lampsbox .lhelp p { margin:0 0 12px; }
|
|
335
|
+
.modal .box.lampsbox .lhelp ul { margin:0 0 16px 20px; }
|
|
336
|
+
.modal .box.lampsbox .lhelp code { font:400 .9em/1.4 var(--mono); background:var(--paper-2); padding:.05em .3em; border-radius:2px; color:var(--ink); }
|
|
337
|
+
.modal .box.lampsbox .lhelp button { text-transform:none; letter-spacing:0; font:400 12px/1 var(--mono); padding:6px 10px; }
|
|
338
|
+
.s .del { margin-left:auto; color:var(--ink-3); visibility:hidden; font-size:11px; padding:0 4px; }
|
|
339
|
+
.s:hover .del { visibility:visible; }
|
|
340
|
+
.s .del:hover { color:var(--rubric); }
|
|
341
|
+
.p .del { margin-left:auto; color:var(--ink-3); visibility:hidden; font-size:11px; padding:0 4px; cursor:pointer; }
|
|
342
|
+
.p:hover .del { visibility:visible; }
|
|
343
|
+
.p .del:hover { color:var(--rubric); }
|
|
344
|
+
.del.ask, .s:hover .del.ask { visibility:visible; color:var(--ink-2); font:400 11px/1.4 var(--mono); white-space:nowrap; }
|
|
345
|
+
.del.ask .yes { color:var(--rubric); cursor:pointer; font-weight:600; }
|
|
346
|
+
.del.ask .no { color:var(--ink-2); cursor:pointer; }
|
|
347
|
+
.del.ask .yes:hover, .del.ask .no:hover { text-decoration:underline; text-underline-offset:.18em; }
|
|
348
|
+
@media (max-width:900px) { body { grid-template-columns:1fr; grid-template-areas:"top" "main"; } aside { display:none; } #log, form { padding-left:20px; padding-right:20px; } }
|
|
349
|
+
@media (prefers-reduced-motion:reduce) { *, *::before, *::after { transition:none !important; animation:none !important; } }
|
|
350
|
+
</style>
|
|
351
|
+
</head>
|
|
352
|
+
<body>
|
|
353
|
+
<header>
|
|
354
|
+
<div class="hrow">
|
|
355
|
+
<span class="brand">lampson<span class="proj" id="proj">…</span></span>
|
|
356
|
+
<span class="spacer"></span>
|
|
357
|
+
<button class="hbtn upd" id="update" style="display:none"></button>
|
|
358
|
+
<button class="hbtn" id="term" title="abrir un terminal real (shell en el workspace)">>_ terminal</button>
|
|
359
|
+
<button class="hbtn" id="new" title="empezar una conversación nueva">+ sesión</button>
|
|
360
|
+
<button class="theme-toggle" id="theme" type="button" title="cambiar tema"></button>
|
|
361
|
+
</div>
|
|
362
|
+
<div class="hrow sub">
|
|
363
|
+
<span class="gitchip" id="git"></span>
|
|
364
|
+
<span class="hpath" id="hpath"></span>
|
|
365
|
+
<select id="agent" title="perfil de agente: qué tools puede usar"></select>
|
|
366
|
+
<select id="perm" title="qué pasa con un comando peligroso (rm -rf, git push --force, sudo…)"><option value="ask">permisos: preguntar</option><option value="yolo">permisos: permitir todo</option><option value="strict">permisos: denegar</option></select>
|
|
367
|
+
<span class="pill" id="lamps" title="lámparas: plugins de tools que vos encendés · clic para ver y activar">lámparas</span>
|
|
368
|
+
<span class="pill" id="model" title="proveedor y modelo · clic para cambiarlos o cargar una API key">…</span>
|
|
369
|
+
</div>
|
|
370
|
+
</header>
|
|
371
|
+
<aside>
|
|
372
|
+
<section class="sec grow open" data-sec="tree">
|
|
373
|
+
<h2><span class="caret">▾</span>Archivos</h2>
|
|
374
|
+
<div class="body" id="tree"></div>
|
|
375
|
+
</section>
|
|
376
|
+
<section class="sec" data-sec="procs">
|
|
377
|
+
<h2 title="procesos que el agente arranca con la tool process"><span class="caret">▸</span>Procesos<span class="cnt" id="procCount"></span></h2>
|
|
378
|
+
<div class="body" id="procs"></div>
|
|
379
|
+
</section>
|
|
380
|
+
<section class="sec" data-sec="todo">
|
|
381
|
+
<h2 title="lista de tareas que el agente mantiene con la tool todo (una en curso a la vez)"><span class="caret">▸</span>Tareas<span class="cnt" id="todoCount"></span></h2>
|
|
382
|
+
<div class="body" id="todoBox"></div>
|
|
383
|
+
</section>
|
|
384
|
+
<section class="sec" data-sec="agents">
|
|
385
|
+
<h2 title="subagentes que el agente lanza con la tool delegate (en paralelo o en background); clic = log en vivo"><span class="caret">▸</span>Subagentes<span class="cnt" id="agentCount"></span></h2>
|
|
386
|
+
<div class="body" id="agentsBox"></div>
|
|
387
|
+
</section>
|
|
388
|
+
<section class="sec" data-sec="mcp">
|
|
389
|
+
<h2 title="servers MCP conectados: globales (lampson/.lampson/mcp.json, valen para todos los proyectos) y del proyecto (.lampson/mcp.json)"><span class="caret">▸</span>MCP<span class="cnt" id="mcpCount"></span></h2>
|
|
390
|
+
<div class="body" id="mcpBox"></div>
|
|
391
|
+
</section>
|
|
392
|
+
<section class="sec" data-sec="lsp">
|
|
393
|
+
<h2 title="language servers: el agente los usa para ver la estructura de un archivo sin leerlo (symbols) y navegar con precisión (definition, references, hover). Arrancan en la primera consulta"><span class="caret">▸</span>LSP<span class="cnt" id="lspCount"></span></h2>
|
|
394
|
+
<div class="body" id="lspBox"></div>
|
|
395
|
+
</section>
|
|
396
|
+
<section class="sec" data-sec="memory">
|
|
397
|
+
<h2 title="notas que el agente guarda sobre este proyecto con la tool memory"><span class="caret">▸</span>Memoria<span class="cnt" id="memCount"></span></h2>
|
|
398
|
+
<div class="body" id="memory"></div>
|
|
399
|
+
</section>
|
|
400
|
+
<section class="sec" data-sec="ports">
|
|
401
|
+
<h2 title="puertos TCP en escucha en esta máquina — de cualquier programa, no solo de lampson (útil para saber quién ocupa un puerto)"><span class="caret">▸</span>Puertos<span class="cnt" id="portCount"></span></h2>
|
|
402
|
+
<div class="body" id="ports"></div>
|
|
403
|
+
</section>
|
|
404
|
+
<section class="sec open" data-sec="sessions">
|
|
405
|
+
<h2><span class="caret">▾</span>Sesiones<span class="cnt" id="sessCount"></span></h2>
|
|
406
|
+
<div class="body" id="sessions"></div>
|
|
407
|
+
</section>
|
|
408
|
+
</aside>
|
|
409
|
+
<main>
|
|
410
|
+
<div id="stage">
|
|
411
|
+
<div id="viewer"><div class="vh"><b id="vpath"></b><span id="vmeta"></span><span class="spacer"></span><span class="x" id="vclose">✕ cerrar</span></div><pre id="vbody"></pre></div>
|
|
412
|
+
<div id="log"></div>
|
|
413
|
+
<div id="termpane"><div class="vh"><span class="st"></span><b>terminal</b><span id="tmeta"></span><span class="spacer"></span><span class="x" id="tclose">✕ cerrar</span></div><div id="xterm"></div></div>
|
|
414
|
+
</div>
|
|
415
|
+
<div id="mcpAdd" class="modal" style="display:none">
|
|
416
|
+
<div class="box">
|
|
417
|
+
<p class="eyebrow">mcp</p>
|
|
418
|
+
<h3>Conectar un server MCP</h3>
|
|
419
|
+
<p class="lead">El comando arranca el server por stdio (formato Claude Code/Cursor). Queda guardado en <code id="mcpAddFile">.lampson/mcp.json</code> y sus tools entran al catálogo del agente como <code>mcp_<server>_<tool></code>.</p>
|
|
420
|
+
<label>nombre <input id="mcpName" spellcheck="false" autocomplete="off" placeholder="github"></label>
|
|
421
|
+
<label>comando <input id="mcpCmd" spellcheck="false" autocomplete="off" placeholder="npx -y @modelcontextprotocol/server-github"></label>
|
|
422
|
+
<label>env <input id="mcpEnv" spellcheck="false" autocomplete="off" placeholder="opcional: GITHUB_TOKEN=… · separá con espacios"></label>
|
|
423
|
+
<label>alcance <select id="mcpScope"><option value="global">global — todos los proyectos</option><option value="project">proyecto — solo este repo</option></select></label>
|
|
424
|
+
<div class="btns"><button class="primary" id="mcpGo">Conectar</button><button id="mcpCancel">Cancelar</button><span class="err" id="mcpErr"></span></div>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
<div id="lampsModal" class="modal" style="display:none">
|
|
428
|
+
<div class="box lampsbox">
|
|
429
|
+
<div class="lhead"><span class="eyebrow" style="margin:0">lámparas</span><h3>Lámparas</h3><span class="ds" id="lampsSub"></span><span class="spacer"></span><button class="lghost" id="lampsHelp" title="¿qué es una lámpara?">?</button><button class="lghost" id="lampsClose" title="cerrar (Esc)">✕</button></div>
|
|
430
|
+
<div class="lbody" id="lampsBody">
|
|
431
|
+
<div class="llist">
|
|
432
|
+
<input id="lampsSearch" placeholder="buscar lámpara o tool…" spellcheck="false" autocomplete="off">
|
|
433
|
+
<div id="lampsItems"></div>
|
|
434
|
+
<div class="lcount" id="lampsCount"></div>
|
|
435
|
+
</div>
|
|
436
|
+
<div class="ldetail" id="lampsDetail"></div>
|
|
437
|
+
</div>
|
|
438
|
+
<div class="lhelp" id="lampsHelpBox" style="display:none">
|
|
439
|
+
<h4>Herramientas hechas a medida para este proyecto</h4>
|
|
440
|
+
<p>Una lámpara es una tool que el agente (o vos) escribe para <em>este</em> repo: una carpeta con <code>lamp.json</code> y el código — en Synsema (corre bajo un techo de capacidades que no puede superar) o en cualquier lenguaje (<code>python</code>, <code>node</code>, un binario). A diferencia de un script suelto, el modelo la ve en su catálogo con parámetros tipados, vos la podés correr desde acá, y queda en el repo para la próxima sesión.</p>
|
|
441
|
+
<p><b>Pedila en el chat</b> — <i>«hacé una lámpara que liste los endpoints del backend»</i>. El agente la escribe en <code id="lampsProject">.lampson/lamps/</code>, la valida y te pide encenderla acá.</p>
|
|
442
|
+
<p><b>Encendida = existe</b> — apagada, el agente ni la ve. Al encenderla, sus tools entran al catálogo como <code>lamp_<lámpara>_<tool></code> desde el próximo turno. Las globales viven en <code id="lampsGlobal">lamps/</code> y valen para todos los proyectos.</p>
|
|
443
|
+
<p><b>Ejemplos</b></p>
|
|
444
|
+
<ul>
|
|
445
|
+
<li><b>Consultas del proyecto</b> — <code>endpoints()</code> que parsea tus rutas y devuelve método + path + handler; <code>env_keys()</code> usadas vs. definidas en <code>.env.example</code>.</li>
|
|
446
|
+
<li><b>Generadores con tus convenciones</b> — <code>new_component(name)</code> que crea el <code>.tsx</code> + test + story con tu plantilla exacta.</li>
|
|
447
|
+
<li><b>Checkers</b> — <code>check_i18n()</code> claves faltantes por idioma; <code>lint_styles()</code> colores hardcodeados fuera de tus tokens.</li>
|
|
448
|
+
<li><b>Datos locales</b> — <code>db_schema()</code> / <code>query(sql)</code> contra tu SQLite con techo <code>db=./dev.db</code>.</li>
|
|
449
|
+
<li><b>Integraciones sin MCP</b> — <code>deploy_status()</code> contra tu API con techo <code>net=api.x.com</code>.</li>
|
|
450
|
+
</ul>
|
|
451
|
+
<p><button class="primary" id="lampsHelpBack">← volver a las lámparas</button></p>
|
|
452
|
+
</div>
|
|
453
|
+
<span class="err" id="lampsErr"></span>
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
<div id="lspAdd" class="modal" style="display:none">
|
|
457
|
+
<div class="box">
|
|
458
|
+
<p class="eyebrow">lsp</p>
|
|
459
|
+
<h3>Agregar un language server</h3>
|
|
460
|
+
<p class="lead">El mismo proceso que usa tu editor para «ir a definición». El agente lo usa para ver la estructura de un archivo sin leerlo entero (<code>symbols</code>) y para navegar sin ambigüedad (<code>definition</code>, <code>references</code>, <code>hover</code>). No se instala nada acá: elegí un preset (con <code>npx</code> se baja solo) o poné tu comando. Arranca en la primera consulta.</p>
|
|
461
|
+
<label>preset <select id="lspPreset"></select></label>
|
|
462
|
+
<div id="lspCustom" style="display:none">
|
|
463
|
+
<label>nombre <input id="lspName" spellcheck="false" autocomplete="off" placeholder="mylang"></label>
|
|
464
|
+
<label>comando <input id="lspCmd" spellcheck="false" autocomplete="off" placeholder="my-language-server --stdio"></label>
|
|
465
|
+
<label>extensiones <input id="lspExt" spellcheck="false" autocomplete="off" placeholder=".ml=ocaml .mli=ocaml"></label>
|
|
466
|
+
</div>
|
|
467
|
+
<p class="lead" id="lspInstall"></p>
|
|
468
|
+
<label>alcance <select id="lspScope"><option value="global">global — todos los proyectos</option><option value="project">proyecto — solo este repo</option></select></label>
|
|
469
|
+
<div class="btns"><button class="primary" id="lspGo">Agregar</button><button id="lspCancel">Cancelar</button><span class="err" id="lspErr"></span></div>
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
<div id="setup" class="modal" style="display:none">
|
|
473
|
+
<div class="box">
|
|
474
|
+
<p class="eyebrow">proveedor</p>
|
|
475
|
+
<h3 id="setupTitle">Modelo</h3>
|
|
476
|
+
<p class="lead" id="setupLead">Elegí con qué modelo trabaja el agente. La API key queda en <code>lampson/.lampson/config.json</code>, solo en esta máquina; nunca vuelve al navegador.</p>
|
|
477
|
+
<div class="plist" id="plist"></div>
|
|
478
|
+
<label>modelo <input id="setModel" spellcheck="false" autocomplete="off"></label>
|
|
479
|
+
<label>API key <input id="setKey" type="password" spellcheck="false" autocomplete="off" placeholder="pegala acá"></label>
|
|
480
|
+
<div class="btns"><button class="primary" id="setSave">Guardar</button><button id="setCancel">Cancelar</button><span class="err" id="setErr"></span></div>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
<div>
|
|
484
|
+
<form id="f">
|
|
485
|
+
<div class="composer">
|
|
486
|
+
<div class="attach" id="attach"></div>
|
|
487
|
+
<textarea id="in" rows="1" placeholder="¿Qué hacemos en este proyecto? · !comando lo corrés vos · Ctrl+V pega una imagen"></textarea>
|
|
488
|
+
<div class="crow"><span class="tools" id="agentHint2"></span><span class="keys">Enter envía · Shift+Enter salto</span><button id="stop" type="button" title="cortar el turno actual">■ detener</button><button id="send" class="primary" type="submit" title="enviar (Enter)">↑</button></div>
|
|
489
|
+
</div>
|
|
490
|
+
</form>
|
|
491
|
+
<div class="hint" id="agentHint"></div>
|
|
492
|
+
</div>
|
|
493
|
+
</main>
|
|
494
|
+
<script>
|
|
495
|
+
const $ = s => document.querySelector(s);
|
|
496
|
+
const log = $('#log');
|
|
497
|
+
let session = new URLSearchParams(location.search).get('session') || localStorage.getItem('lampson.session') || '';
|
|
498
|
+
let busy = false; let cfg = {};
|
|
499
|
+
|
|
500
|
+
function esc(s) { return String(s ?? '').replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c])); }
|
|
501
|
+
function md(src) { // Markdown mínimo: code fences, inline code, bold, headers, listas, párrafos
|
|
502
|
+
const blocks = []; src = String(src ?? '').replace(/```(\w*)\n([\s\S]*?)```/g, (m, l, c) => { blocks.push('<pre><code>' + esc(c) + '</code></pre>'); return '' + (blocks.length - 1) + ''; }); // marcador con chars de uso privado: NUNCA dígitos a secas (2026-08-27: se perdieron y todo número del texto salía undefined)
|
|
503
|
+
let h = esc(src).replace(/`([^`\n]+)`/g, '<code>$1</code>').replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>');
|
|
504
|
+
h = h.split(/\n{2,}/).map(p => {
|
|
505
|
+
if (/^\d+$/.test(p.trim())) return p;
|
|
506
|
+
if (/^#{1,3} /.test(p)) return '<h3>' + p.replace(/^#{1,3} /, '') + '</h3>';
|
|
507
|
+
if (/^(\s*[-*] .*\n?)+$/.test(p)) return '<ul>' + p.split('\n').map(l => '<li>' + l.replace(/^\s*[-*] /, '') + '</li>').join('') + '</ul>';
|
|
508
|
+
if (/^(\s*\d+[.)] .*\n?)+$/.test(p)) return '<ol>' + p.split('\n').map(l => '<li>' + l.replace(/^\s*\d+[.)] /, '') + '</li>').join('') + '</ol>';
|
|
509
|
+
return '<p>' + p.replace(/\n/g, '<br>') + '</p>';
|
|
510
|
+
}).join('');
|
|
511
|
+
return h.replace(/(\d+)/g, (m, i) => blocks[+i]);
|
|
512
|
+
}
|
|
513
|
+
function add(cls, html) { showPane('log'); log.classList.remove('hero'); const d = document.createElement('div'); d.className = 'msg ' + cls; d.innerHTML = html; log.appendChild(d); $('#stage').scrollTop = $('#stage').scrollHeight; return d; }
|
|
514
|
+
function describe(c) { const a = c.args || {}; if (c.name === 'bash') return '$ ' + (a.command || ''); if (c.name === 'delegate') return 'delegate → ' + a.agent + ': ' + String(a.brief || '').slice(0, 80); if (a.path) return c.name + ' ' + a.path; if (a.pattern) return c.name + ' ' + a.pattern; if (a.name) return c.name + ' ' + a.name; return c.name + ' ' + JSON.stringify(a); }
|
|
515
|
+
function cmdHtml(text) { const t = String(text); const long = t.length > 220 || t.split('\n').length > 4; return `<span class="cmd">${esc(t)}</span>` + (long ? ' <span class="more">ver todo</span>' : ''); }
|
|
516
|
+
function wireMore(el) { const m = el.querySelector('.more'); if (m) m.onclick = () => { el.querySelector('.cmd').classList.toggle('open'); m.textContent = el.querySelector('.cmd').classList.contains('open') ? 'menos' : 'ver todo'; }; }
|
|
517
|
+
function step(icon, nameHtml, out) {
|
|
518
|
+
const first = out == null ? '' : String(out).split('\n')[0].slice(0, 140);
|
|
519
|
+
const body = out == null ? '' : `<details><summary>${esc(first)} <span style="color:var(--ink-3)">(${String(out).length} chars)</span></summary><pre>${esc(out)}</pre></details>`;
|
|
520
|
+
add('step', `<span class="ic">${icon}</span><span class="name">${nameHtml}</span>${body}`);
|
|
521
|
+
}
|
|
522
|
+
function empty() {
|
|
523
|
+
const name = (cfg.workspace || '').split(/[\\/]/).filter(Boolean).pop() || 'workspace';
|
|
524
|
+
log.classList.add('hero');
|
|
525
|
+
log.innerHTML = `<div class="empty">
|
|
526
|
+
<p class="eyebrow">workspace</p>
|
|
527
|
+
<h1>${esc(name)}</h1>
|
|
528
|
+
<p class="lead">Un agente con herramientas acotadas a este proyecto. Vos pedís; él lee, busca, edita y corre comandos — y cada paso queda a la vista.</p>
|
|
529
|
+
<div class="grid">
|
|
530
|
+
<div class="card"><h3>Probá</h3>
|
|
531
|
+
<p><span class="try">Explicame la estructura de este proyecto</span></p>
|
|
532
|
+
<p><span class="try">¿Dónde está el punto de entrada y qué hace?</span></p>
|
|
533
|
+
<p><span class="try">Buscá TODOs y proponé un plan para resolverlos</span></p>
|
|
534
|
+
<p><span class="try">Corré los tests y contame qué falla</span></p></div>
|
|
535
|
+
<div class="card"><h3>Agentes</h3>
|
|
536
|
+
<p><span class="k">build</span> edita y ejecuta (por defecto)</p>
|
|
537
|
+
<p><span class="k">plan</span> solo lectura: investiga y propone</p>
|
|
538
|
+
<p><span class="k">review</span> lee y corre tests, no edita</p>
|
|
539
|
+
<p><span class="k">explore</span> localiza código</p></div>
|
|
540
|
+
<div class="card"><h3>Qué puede tocar</h3>
|
|
541
|
+
<p>Solo archivos dentro de <code>${esc(name)}/</code>.</p>
|
|
542
|
+
<p>Comandos peligrosos (<code>rm -rf</code>, <code>git push --force</code>, <code>sudo</code>…) según el selector <span class="k">permisos</span>: preguntar, permitir todo o denegar.</p>
|
|
543
|
+
<p>Lo destructivo del sistema se bloquea siempre.</p></div>
|
|
544
|
+
<div class="card"><h3>Cómo lo ve el modelo</h3>
|
|
545
|
+
<p>Cada tool y su resultado — errores y rechazos incluidos — queda en el historial y se le reenvía en cada turno.</p>
|
|
546
|
+
<p>Hacé clic en un archivo a la izquierda para verlo. Un mensaje que empieza con <code>!</code> lo corrés vos, sin el modelo.</p></div>
|
|
547
|
+
</div></div>`;
|
|
548
|
+
log.querySelectorAll('.try').forEach(el => el.onclick = () => { $('#in').value = el.textContent; $('#in').focus(); });
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ---- tema día / noche (el valor inicial ya lo puso el script del <head>) ----
|
|
552
|
+
const SUN = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2.7v2.1M12 19.2v2.1M2.7 12h2.1M19.2 12h2.1M5.3 5.3l1.5 1.5M17.2 17.2l1.5 1.5M18.7 5.3l-1.5 1.5M6.8 17.2l-1.5 1.5"/></svg>';
|
|
553
|
+
const MOON = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M20.2 14.6A8.6 8.6 0 0 1 9.4 3.8a8.6 8.6 0 1 0 10.8 10.8z"/></svg>';
|
|
554
|
+
function paintTheme() { const light = document.documentElement.getAttribute('data-theme') === 'light'; $('#theme').innerHTML = light ? MOON : SUN; $('#theme').title = light ? 'Cambiar a tema oscuro' : 'Cambiar a tema claro'; }
|
|
555
|
+
$('#theme').onclick = () => { const r = document.documentElement; r.setAttribute('data-theme', r.getAttribute('data-theme') === 'light' ? 'dark' : 'light'); try { localStorage.setItem('lampson.theme', r.getAttribute('data-theme')); } catch (e) {} paintTheme(); if (term) term.options.theme = termTheme(); };
|
|
556
|
+
paintTheme();
|
|
557
|
+
|
|
558
|
+
// ---- terminal real: xterm.js ↔ WebSocket /api/term ↔ pty en el servidor ----
|
|
559
|
+
// Frames binarios = bytes del pty; texto JSON = control (hello / exit). Cerrar el panel cierra el socket y mata el shell.
|
|
560
|
+
let term = null, termWs = null, termFit = null;
|
|
561
|
+
function termTheme() {
|
|
562
|
+
const s = getComputedStyle(document.documentElement); const v = n => s.getPropertyValue(n).trim();
|
|
563
|
+
return { background: v('--paper'), foreground: v('--ink'), cursor: v('--accent'), cursorAccent: v('--paper'), selectionBackground: v('--sel'),
|
|
564
|
+
black: v('--paper-3'), brightBlack: v('--ink-3'), red: v('--rubric'), brightRed: v('--rubric'), green: v('--str'), brightGreen: v('--str'),
|
|
565
|
+
yellow: v('--amber'), brightYellow: v('--amber'), blue: v('--term-blue'), brightBlue: v('--accent'), magenta: v('--rubric'), brightMagenta: v('--rubric'),
|
|
566
|
+
cyan: v('--accent'), brightCyan: v('--accent'), white: v('--ink-2'), brightWhite: v('--ink') };
|
|
567
|
+
}
|
|
568
|
+
function showPane(which) { // 'log' | 'viewer' | 'term'
|
|
569
|
+
log.style.display = which === 'log' ? '' : 'none';
|
|
570
|
+
$('#viewer').style.display = which === 'viewer' ? 'block' : 'none';
|
|
571
|
+
$('#termpane').style.display = which === 'term' ? 'flex' : 'none';
|
|
572
|
+
// volver al chat = al final de la conversación (el viewer de archivos deja el scroll de #stage arriba)
|
|
573
|
+
if (which === 'log') $('#stage').scrollTop = $('#stage').scrollHeight;
|
|
574
|
+
}
|
|
575
|
+
function openTerm() {
|
|
576
|
+
if (term) { showPane('term'); termFit.fit(); term.focus(); return; }
|
|
577
|
+
showPane('term'); procOpen = null; clearInterval(procTimer);
|
|
578
|
+
term = new Terminal({ cursorBlink: true, fontFamily: getComputedStyle(document.documentElement).getPropertyValue('--mono'), fontSize: 13, lineHeight: 1.25, theme: termTheme(), scrollback: 5000, allowProposedApi: true });
|
|
579
|
+
termFit = new FitAddon.FitAddon(); term.loadAddon(termFit); term.open($('#xterm')); termFit.fit();
|
|
580
|
+
// URLs clickeables (npm run dev imprime http://localhost:3000): link provider mínimo con la API nativa
|
|
581
|
+
// de xterm v5 — el addon web-links no está vendorizado y no hace falta para http/https
|
|
582
|
+
const TERM_URL_RE = /https?:\/\/[^\s"'`<>()\[\]{}]*[^\s"'`<>()\[\]{}.,;:!?]/g;
|
|
583
|
+
term.registerLinkProvider({
|
|
584
|
+
provideLinks(y, cb) {
|
|
585
|
+
const line = term.buffer.active.getLine(y - 1);
|
|
586
|
+
if (!line) return cb(undefined);
|
|
587
|
+
const text = line.translateToString(true);
|
|
588
|
+
const links = []; let m; TERM_URL_RE.lastIndex = 0;
|
|
589
|
+
while ((m = TERM_URL_RE.exec(text))) {
|
|
590
|
+
links.push({ range: { start: { x: m.index + 1, y }, end: { x: m.index + m[0].length, y } }, text: m[0], activate: (_e, uri) => window.open(uri, '_blank') });
|
|
591
|
+
}
|
|
592
|
+
cb(links.length ? links : undefined);
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
$('#tmeta').textContent = 'conectando…';
|
|
596
|
+
termWs = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/api/term'); termWs.binaryType = 'arraybuffer';
|
|
597
|
+
const send = (o) => { if (termWs && termWs.readyState === 1) termWs.send(JSON.stringify(o)); };
|
|
598
|
+
termWs.onopen = () => { $('#termpane').classList.add('live'); send({ type: 'resize', cols: term.cols, rows: term.rows }); term.focus(); };
|
|
599
|
+
termWs.onmessage = (e) => { // "o" + salida del pty | "c" + JSON de control
|
|
600
|
+
const s = String(e.data);
|
|
601
|
+
if (s[0] === 'o') { term.write(s.slice(1)); return; }
|
|
602
|
+
let m; try { m = JSON.parse(s.slice(1)); } catch { return; }
|
|
603
|
+
if (m.type === 'hello') $('#tmeta').textContent = `${m.shell} · pid ${m.pid} · ${m.cwd || 'workspace'}`;
|
|
604
|
+
if (m.type === 'exit') { term.write(`\r\n\x1b[2m[shell terminado · código ${m.code}]\x1b[0m\r\n`); $('#termpane').classList.remove('live'); }
|
|
605
|
+
};
|
|
606
|
+
termWs.onclose = () => { $('#termpane').classList.remove('live'); if ($('#tmeta').textContent === 'conectando…') $('#tmeta').textContent = 'sin conexión'; };
|
|
607
|
+
term.onData(d => send({ type: 'in', data: d }));
|
|
608
|
+
term.onResize(({ cols, rows }) => send({ type: 'resize', cols, rows }));
|
|
609
|
+
new ResizeObserver(() => { if ($('#termpane').style.display !== 'none') termFit.fit(); }).observe($('#xterm'));
|
|
610
|
+
}
|
|
611
|
+
function closeTerm() { if (termWs) { try { termWs.close(); } catch (e) {} } if (term) term.dispose(); term = null; termWs = null; termFit = null; $('#xterm').innerHTML = ''; $('#termpane').classList.remove('live'); showPane('log'); }
|
|
612
|
+
$('#term').onclick = openTerm;
|
|
613
|
+
$('#tclose').onclick = closeTerm;
|
|
614
|
+
|
|
615
|
+
// ---- actualizaciones de Lampson (una vez por carga; el fetch a origin puede tardar) ----
|
|
616
|
+
async function checkUpdate() {
|
|
617
|
+
let u; try { u = await (await fetch('/api/update')).json(); } catch (e) { return; }
|
|
618
|
+
const b = $('#update'); if (!u.available) { b.style.display = 'none'; return; }
|
|
619
|
+
b.textContent = `⬆ actualizar · ${u.behind} commit${u.behind === 1 ? '' : 's'}`;
|
|
620
|
+
b.title = `${u.current} → ${u.latest}\n${(u.notes || []).map(n => '· ' + n).join('\n')}\n\nen terminal: ${u.command}`;
|
|
621
|
+
b.style.display = '';
|
|
622
|
+
b.onclick = async () => {
|
|
623
|
+
b.disabled = true; b.textContent = 'actualizando…';
|
|
624
|
+
let r; try { r = await (await fetch('/api/update', { method: 'POST' })).json(); } catch (e) { r = { result: 'no se pudo actualizar: ' + e.message }; }
|
|
625
|
+
add('meta', '⬆ ' + esc(r.result || ''));
|
|
626
|
+
b.disabled = false; b.textContent = '⬆ reiniciá el servidor'; b.onclick = null;
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ---- proveedor / modelo / API key (guardados en el servidor: .lampson/config.json; la key nunca vuelve) ----
|
|
631
|
+
let setupSel = '';
|
|
632
|
+
function paintModel() {
|
|
633
|
+
const p = (cfg.providers || []).find(x => x.name === cfg.provider);
|
|
634
|
+
$('#model').textContent = cfg.provider ? `${cfg.provider} · ${cfg.model || ''}` : '';
|
|
635
|
+
$('#model').classList.toggle('nokey', !!p && !p.has_key);
|
|
636
|
+
$('#model').style.display = cfg.provider ? '' : 'none';
|
|
637
|
+
}
|
|
638
|
+
function openSetup(onboarding) {
|
|
639
|
+
$('#setupTitle').textContent = onboarding ? 'Bienvenido a Lampson' : 'Modelo';
|
|
640
|
+
$('#setupLead').innerHTML = onboarding
|
|
641
|
+
? 'Un agente con herramientas acotadas a este proyecto. Para empezar, elegí un proveedor y pegá su API key. Queda en <code>lampson/.lampson/config.json</code>, solo en esta máquina; nunca vuelve al navegador.'
|
|
642
|
+
: 'Elegí con qué modelo trabaja el agente (vale para la web y la terminal). La API key queda en <code>lampson/.lampson/config.json</code>, solo en esta máquina; nunca vuelve al navegador.';
|
|
643
|
+
$('#setCancel').style.display = onboarding ? 'none' : '';
|
|
644
|
+
$('#setErr').textContent = '';
|
|
645
|
+
setupSel = cfg.provider || (cfg.providers || [])[0]?.name || '';
|
|
646
|
+
const list = $('#plist'); list.innerHTML = '';
|
|
647
|
+
for (const p of (cfg.providers || [])) {
|
|
648
|
+
const r = document.createElement('div'); r.className = 'pr' + (p.name === setupSel ? ' on' : ''); r.dataset.name = p.name;
|
|
649
|
+
r.innerHTML = `<b>${esc(p.name)}</b><span class="dm">${esc(p.model)}</span><span class="k ${p.has_key ? '' : 'no'}">${p.name === 'ollama' ? 'sin key' : (p.has_key ? '● key guardada' : '○ sin key')}</span>`;
|
|
650
|
+
r.onclick = () => { setupSel = p.name; list.querySelectorAll('.pr').forEach(x => x.classList.toggle('on', x.dataset.name === p.name)); fillSetup(); };
|
|
651
|
+
list.appendChild(r);
|
|
652
|
+
}
|
|
653
|
+
fillSetup(); $('#setup').style.display = 'flex'; $('#setKey').focus();
|
|
654
|
+
}
|
|
655
|
+
function fillSetup() {
|
|
656
|
+
const p = (cfg.providers || []).find(x => x.name === setupSel) || {};
|
|
657
|
+
$('#setModel').value = setupSel === cfg.provider ? (cfg.model || p.model || '') : (p.model || '');
|
|
658
|
+
$('#setModel').placeholder = p.model || '';
|
|
659
|
+
// modelos válidos según la API del proveedor (evita tipear "DeepSeek-V4-Pro" cuando la API quiere "deepseek-v4-pro")
|
|
660
|
+
let dl = $('#modelList'); if (!dl) { dl = document.createElement('datalist'); dl.id = 'modelList'; document.body.appendChild(dl); $('#setModel').setAttribute('list', 'modelList'); }
|
|
661
|
+
dl.innerHTML = ''; $('#setModel').title = '';
|
|
662
|
+
if (p.has_key || setupSel === 'ollama') fetch('/api/models?provider=' + encodeURIComponent(setupSel)).then(r => r.json()).then(d => { if (setupSel !== p.name) return; if (d.error) { $('#setModel').title = 'no pude listar modelos: ' + d.error; return; } dl.innerHTML = (d.models || []).map(m => `<option value="${esc(m)}">`).join(''); $('#setModel').title = (d.models || []).length ? 'modelos disponibles: ' + d.models.join(', ') : ''; }).catch(() => {});
|
|
663
|
+
$('#setKey').value = ''; $('#setKey').disabled = setupSel === 'ollama';
|
|
664
|
+
$('#setKey').placeholder = setupSel === 'ollama' ? 'ollama no necesita key' : (p.has_key ? 'hay una key guardada · pegá otra para reemplazarla' : 'pegala acá');
|
|
665
|
+
}
|
|
666
|
+
$('#setSave').onclick = async () => {
|
|
667
|
+
const body = { provider: setupSel, model: $('#setModel').value.trim() }; if ($('#setKey').value.trim()) body.key = $('#setKey').value.trim();
|
|
668
|
+
const p = (cfg.providers || []).find(x => x.name === setupSel) || {};
|
|
669
|
+
if (setupSel !== 'ollama' && !p.has_key && !body.key) { $('#setErr').textContent = 'falta la API key'; return; }
|
|
670
|
+
$('#setSave').disabled = true;
|
|
671
|
+
try {
|
|
672
|
+
const r = await fetch('/api/settings', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }); const d = await r.json();
|
|
673
|
+
if (!r.ok) { $('#setErr').textContent = d.error || ('error ' + r.status); return; }
|
|
674
|
+
cfg.providers = d.providers; cfg.provider = d.provider; cfg.model = d.model; cfg.vision = d.vision; cfg.configured = d.configured; paintModel(); paintAttach();
|
|
675
|
+
$('#setup').style.display = 'none'; if (log.querySelector('.empty')) empty();
|
|
676
|
+
} catch (e) { $('#setErr').textContent = e.message; } finally { $('#setSave').disabled = false; }
|
|
677
|
+
};
|
|
678
|
+
$('#setCancel').onclick = () => { $('#setup').style.display = 'none'; };
|
|
679
|
+
$('#model').onclick = () => openSetup(false);
|
|
680
|
+
$('#setKey').addEventListener('keydown', e => { if (e.key === 'Enter') $('#setSave').click(); });
|
|
681
|
+
|
|
682
|
+
// ---- confirmación en línea (sin confirm() del navegador): el ✕ se convierte en «¿borrar? sí · no» ----
|
|
683
|
+
function inlineConfirm(el, label, onYes) {
|
|
684
|
+
const prev = el.innerHTML; el.classList.add('ask');
|
|
685
|
+
el.innerHTML = `<span class="q">${esc(label)}</span> <span class="yes">sí</span> · <span class="no">no</span>`;
|
|
686
|
+
const restore = () => { el.innerHTML = prev; el.classList.remove('ask'); };
|
|
687
|
+
el.querySelector('.yes').onclick = (ev) => { ev.stopPropagation(); onYes(); };
|
|
688
|
+
el.querySelector('.no').onclick = (ev) => { ev.stopPropagation(); restore(); };
|
|
689
|
+
setTimeout(() => { if (el.classList.contains('ask')) restore(); }, 6000);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ---- secciones del sidebar (acordeón) ----
|
|
693
|
+
function setSec(name, open) {
|
|
694
|
+
const s = document.querySelector(`.sec[data-sec="${name}"]`); if (!s) return;
|
|
695
|
+
s.classList.toggle('open', open);
|
|
696
|
+
s.querySelector('.caret').textContent = open ? '▾' : '▸';
|
|
697
|
+
}
|
|
698
|
+
// sin preferencia del usuario, una sección se abre sola solo si tiene contenido
|
|
699
|
+
function autoSec(name, hasContent) {
|
|
700
|
+
const pref = localStorage.getItem('lampson.sec.' + name);
|
|
701
|
+
setSec(name, pref === null ? hasContent : pref === '1');
|
|
702
|
+
}
|
|
703
|
+
document.querySelectorAll('.sec h2').forEach(h => h.onclick = () => {
|
|
704
|
+
const s = h.closest('.sec'); const open = !s.classList.contains('open');
|
|
705
|
+
localStorage.setItem('lampson.sec.' + s.dataset.sec, open ? '1' : '0');
|
|
706
|
+
setSec(s.dataset.sec, open);
|
|
707
|
+
});
|
|
708
|
+
for (const s of document.querySelectorAll('.sec')) {
|
|
709
|
+
const pref = localStorage.getItem('lampson.sec.' + s.dataset.sec);
|
|
710
|
+
if (pref !== null) setSec(s.dataset.sec, pref === '1');
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// ---- config / header ----
|
|
714
|
+
async function loadCfg() {
|
|
715
|
+
try { cfg = await (await fetch('/api/config')).json(); } catch (e) { cfg = {}; }
|
|
716
|
+
const ws = cfg.workspace || '';
|
|
717
|
+
$('#proj').textContent = ws.split(/[\\/]/).filter(Boolean).pop() || 'workspace'; $('#proj').title = ws;
|
|
718
|
+
$('#hpath').textContent = ws; $('#hpath').title = ws;
|
|
719
|
+
$('#perm').value = localStorage.getItem('lampson.perm') || cfg.permission || 'ask'; $('#perm').onchange = () => localStorage.setItem('lampson.perm', $('#perm').value);
|
|
720
|
+
paintModel();
|
|
721
|
+
if (cfg.configured === false) openSetup(true);
|
|
722
|
+
const sel = $('#agent'); sel.innerHTML = '';
|
|
723
|
+
for (const a of (cfg.agents || ['build'])) { const o = document.createElement('option'); o.value = a; o.textContent = 'agente: ' + a; sel.appendChild(o); }
|
|
724
|
+
sel.value = localStorage.getItem('lampson.agent') || 'build';
|
|
725
|
+
const hint = () => { const p = (cfg.profiles || {})[sel.value]; const t = p ? sel.value + ' · ' + p.tools.join(' · ') : ''; $('#agentHint').textContent = t; $('#agentHint2').textContent = t; };
|
|
726
|
+
sel.onchange = () => { localStorage.setItem('lampson.agent', sel.value); hint(); }; hint();
|
|
727
|
+
if (!log.children.length) empty();
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// ---- file tree + viewer ----
|
|
731
|
+
let gitStatus = { changes: {} };
|
|
732
|
+
async function loadGit() {
|
|
733
|
+
try { gitStatus = await (await fetch('/api/git')).json(); } catch (e) { gitStatus = { repo: false, changes: {} }; }
|
|
734
|
+
const g = $('#git'); if (!gitStatus.repo) { g.style.display = 'none'; return; }
|
|
735
|
+
const k = gitStatus.counts || {}; const nuevos = (k.added||0)+(k.untracked||0);
|
|
736
|
+
const parts = []; // solo contadores distintos de cero — lo demás es ruido
|
|
737
|
+
if (k.modified) parts.push(`<b class="w">~${k.modified}</b>`);
|
|
738
|
+
if (nuevos) parts.push(`<b class="g">+${nuevos}</b>`);
|
|
739
|
+
if (k.deleted) parts.push(`<b class="r">−${k.deleted}</b>`);
|
|
740
|
+
g.innerHTML = `⎇ <span class="br">${esc(gitStatus.branch)}</span>` + (parts.length ? parts.join('') : '<b class="c">✓</b>');
|
|
741
|
+
g.title = parts.length ? `${k.modified||0} modificados · ${nuevos} nuevos · ${k.deleted||0} borrados` : 'working tree limpio';
|
|
742
|
+
g.style.display = 'flex';
|
|
743
|
+
}
|
|
744
|
+
function gitClass(path, isDir) {
|
|
745
|
+
const ch = gitStatus.changes || {};
|
|
746
|
+
if (!isDir) { const c = ch[path]; return c === '??' ? 'gU' : c === 'A' ? 'gA' : c === 'D' ? 'gD' : c ? 'gM' : ''; }
|
|
747
|
+
const p = path + '/'; for (const k in ch) if (k.startsWith(p)) return 'gM'; return '';
|
|
748
|
+
}
|
|
749
|
+
async function loadTree() {
|
|
750
|
+
await loadGit();
|
|
751
|
+
const box = $('#tree'); box.innerHTML = '';
|
|
752
|
+
let t; try { t = await (await fetch('/api/tree')).json(); } catch (e) { box.innerHTML = '<div class="row none">sin árbol</div>'; return; }
|
|
753
|
+
const render = (entries, parent, depth) => {
|
|
754
|
+
for (const e of entries) {
|
|
755
|
+
const n = document.createElement('div'); n.className = 'node';
|
|
756
|
+
const gc = gitClass(e.path, e.is_dir); const code = (gitStatus.changes || {})[e.path];
|
|
757
|
+
const r = document.createElement('div'); r.className = 'row' + (e.is_dir ? ' dir' : '') + (gc ? ' ' + gc : ''); r.title = e.path + (code ? ' · git ' + code : '');
|
|
758
|
+
r.innerHTML = `<span class="tw">${e.is_dir ? '▸' : ''}</span><span>${esc(e.name)}${e.is_dir ? '/' : ''}</span>` + (code && !e.is_dir ? `<span class="gs">${code === '??' ? 'N' : code}</span>` : '');
|
|
759
|
+
n.appendChild(r);
|
|
760
|
+
if (e.is_dir) {
|
|
761
|
+
const k = document.createElement('div'); k.className = 'kids hidden'; render(e.children || [], k, depth + 1); n.appendChild(k);
|
|
762
|
+
r.querySelector('.tw').textContent = '▸';
|
|
763
|
+
r.onclick = () => { k.classList.toggle('hidden'); r.querySelector('.tw').textContent = k.classList.contains('hidden') ? '▸' : '▾'; };
|
|
764
|
+
} else { r.onclick = () => openFile(e.path, r); }
|
|
765
|
+
parent.appendChild(n);
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
render(t.entries || [], box, 0);
|
|
769
|
+
if (t.truncated) { const d = document.createElement('div'); d.className = 'row none'; d.textContent = '… árbol truncado'; box.appendChild(d); }
|
|
770
|
+
}
|
|
771
|
+
async function openFile(path, row) {
|
|
772
|
+
document.querySelectorAll('.row.active').forEach(x => x.classList.remove('active')); if (row) row.classList.add('active');
|
|
773
|
+
const r = await fetch('/api/file?path=' + encodeURIComponent(path)); const d = await r.json();
|
|
774
|
+
if (!r.ok) { add('denied', esc(d.error || 'error')); return; }
|
|
775
|
+
$('#vpath').textContent = d.path; $('#vmeta').textContent = d.lines + ' líneas';
|
|
776
|
+
$('#vbody').className = ''; $('#vbody').innerHTML = String(d.content).split('\n').map((l, i) => `<span class="ln">${i + 1}</span>${esc(l)}`).join('\n');
|
|
777
|
+
showPane('viewer'); $('#stage').scrollTop = 0;
|
|
778
|
+
}
|
|
779
|
+
$('#vclose').onclick = () => { showPane('log'); procOpen = null; clearInterval(procTimer); $('#vbody').className = ''; document.querySelectorAll('.row.active, .m.active, .p.active').forEach(x => x.classList.remove('active')); memOpen = null; };
|
|
780
|
+
|
|
781
|
+
// ---- memoria del proyecto ----
|
|
782
|
+
let memOpen = null;
|
|
783
|
+
async function loadMemory() {
|
|
784
|
+
let r; try { r = await (await fetch('/api/memory')).json(); } catch (e) { return; }
|
|
785
|
+
const box = $('#memory'); box.innerHTML = '';
|
|
786
|
+
const notes = r.notes || [];
|
|
787
|
+
$('#memCount').textContent = notes.length || ''; autoSec('memory', notes.length > 0);
|
|
788
|
+
if (!notes.length) { box.innerHTML = '<div class="m none">sin notas todavía</div>'; return; }
|
|
789
|
+
for (const n of notes) { const d = document.createElement('div'); d.className = 'm' + (memOpen === n.name ? ' active' : ''); d.title = n.title; d.innerHTML = `<b>${esc(n.name)}</b><span class="t">${esc(n.title)}</span>`; d.onclick = () => openMemory(n.name); box.appendChild(d); }
|
|
790
|
+
}
|
|
791
|
+
async function openMemory(name) {
|
|
792
|
+
memOpen = name; procOpen = null; clearInterval(procTimer);
|
|
793
|
+
const r = await (await fetch('/api/memory/note?name=' + encodeURIComponent(name))).json();
|
|
794
|
+
if (r.error) { add('denied', esc(r.error)); return; }
|
|
795
|
+
$('#vpath').textContent = 'memoria · ' + name + '.md'; $('#vmeta').textContent = 'notas del agente sobre este proyecto (editables a mano en lampson/memory/)';
|
|
796
|
+
const pre = $('#vbody'); pre.className = 'log'; pre.textContent = r.content || '(vacía)';
|
|
797
|
+
showPane('viewer'); $('#stage').scrollTop = 0; loadMemory();
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// ---- procesos gestionados ----
|
|
801
|
+
let procOpen = null, procTimer = null;
|
|
802
|
+
async function loadPorts() {
|
|
803
|
+
let r; try { r = await (await fetch('/api/ports')).json(); } catch (e) { return; }
|
|
804
|
+
const box = $('#ports'); box.innerHTML = '';
|
|
805
|
+
const list = (r.ports || []).filter(p => p.port !== 8080 || p.name !== 'synsema.exe');
|
|
806
|
+
$('#portCount').textContent = list.length || ''; autoSec('ports', list.length > 0);
|
|
807
|
+
if (!list.length) { box.innerHTML = '<div class="p none">ninguno</div>'; return; }
|
|
808
|
+
for (const p of list) {
|
|
809
|
+
const d = document.createElement('div'); d.className = 'p'; d.style.cursor = 'default'; d.title = `pid ${p.pid}\n${p.command || '(línea de comando no disponible)'}`;
|
|
810
|
+
const what = p.command ? p.command.replace(/^"?[A-Za-z]:\\[^"]*\\([^"\\]+)"?/, '$1') : p.name;
|
|
811
|
+
d.innerHTML = `<a class="port" href="http://127.0.0.1:${p.port}" target="_blank" rel="noopener" title="abrir http://127.0.0.1:${p.port} en otra pestaña">:${p.port}</a><span class="cm">${esc(what)}</span><button title="matar este proceso (pid ${p.pid}; no lo gestiona lampson)">✕</button>`;
|
|
812
|
+
const kb = d.querySelector('button'); kb.onclick = () => { const holder = document.createElement('span'); holder.className = 'del'; kb.replaceWith(holder); inlineConfirm(holder, `¿matar pid ${p.pid}?`, async () => { await fetch('/api/ports/kill', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ pid: p.pid }) }); loadPorts(); }); setTimeout(() => { if (!holder.classList.contains('ask')) loadPorts(); }, 6100); };
|
|
813
|
+
box.appendChild(d);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
async function loadProcs() {
|
|
817
|
+
loadPorts();
|
|
818
|
+
let r; try { r = await (await fetch('/api/proc')).json(); } catch (e) { return; }
|
|
819
|
+
const box = $('#procs'); box.innerHTML = '';
|
|
820
|
+
const list = r.processes || []; const act = list.filter(p => p.running).length;
|
|
821
|
+
$('#procCount').textContent = list.length ? (act ? `${act} ● · ${list.length}` : `${list.length}`) : '';
|
|
822
|
+
// Procesos queda cerrado salvo que el usuario lo abra (preferencia guardada); el contador avisa
|
|
823
|
+
if (!list.length) { box.innerHTML = '<div class="p none">ninguno</div>'; return; }
|
|
824
|
+
for (const p of list) {
|
|
825
|
+
const d = document.createElement('div'); d.className = 'p' + (p.running ? ' run' : '') + (procOpen === p.name ? ' active' : ''); d.title = p.command;
|
|
826
|
+
d.innerHTML = `<span class="st"></span><span class="nm">${esc(p.name)}</span><span class="cm">$ ${esc(p.command)}</span>` + (p.running ? '<button>■</button>' : '');
|
|
827
|
+
d.onclick = () => openProc(p.name);
|
|
828
|
+
const b = d.querySelector('button'); if (b) b.onclick = async (ev) => { ev.stopPropagation(); await fetch('/api/proc/stop', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: p.name }) }); loadProcs(); if (procOpen === p.name) openProc(p.name); };
|
|
829
|
+
box.appendChild(d);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
async function openProc(name) {
|
|
833
|
+
procOpen = name; document.querySelectorAll('.row.active').forEach(x => x.classList.remove('active'));
|
|
834
|
+
const r = await (await fetch('/api/proc/log?name=' + encodeURIComponent(name) + '&tail=400')).json();
|
|
835
|
+
$('#vpath').textContent = 'proceso ' + name; $('#vmeta').textContent = r.running ? '● corriendo · log en vivo' : '○ terminado';
|
|
836
|
+
const pre = $('#vbody'); pre.className = 'log'; pre.textContent = r.log || '(sin salida todavía)';
|
|
837
|
+
showPane('viewer'); $('#stage').scrollTop = $('#stage').scrollHeight;
|
|
838
|
+
clearInterval(procTimer); if (r.running && !events) procTimer = setInterval(() => refreshProcViewer(name), 2000); // sin stream de eventos: respaldo por timer
|
|
839
|
+
loadProcs();
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ---- subagentes (delegate): lista + log vivo por hijo ----
|
|
843
|
+
let agentOpen = null, agentTimer = null, agentPaint = null;
|
|
844
|
+
async function loadAgents() {
|
|
845
|
+
let r; try { r = await (await fetch('/api/agents')).json(); } catch (e) { return; }
|
|
846
|
+
const box = $('#agentsBox'); box.innerHTML = '';
|
|
847
|
+
const list = (r.agents || []).slice().reverse(); const act = list.filter(a => a.status === 'running').length;
|
|
848
|
+
$('#agentCount').textContent = list.length ? (act ? `${act} ● · ${list.length}` : `${list.length}`) : ''; autoSec('agents', act > 0);
|
|
849
|
+
if (!list.length) { box.innerHTML = '<div class="p none">ninguno</div>'; return; }
|
|
850
|
+
for (const a of list) {
|
|
851
|
+
const run = a.status === 'running';
|
|
852
|
+
const d = document.createElement('div'); d.className = 'p' + (run ? ' run' : '') + (agentOpen === a.id ? ' active' : ''); d.title = `${a.status} · ${a.steps} pasos · ${a.tokens} tokens\n${a.brief}`;
|
|
853
|
+
d.innerHTML = `<span class="st"></span><span class="nm">${esc(a.id)}</span><span class="cm">${esc(String(a.brief || '').split('\n')[0].slice(0, 90))}</span>` + (run ? '<button title="parar: devuelve el informe parcial">■</button>' : '');
|
|
854
|
+
d.onclick = () => openAgent(a.id);
|
|
855
|
+
const b = d.querySelector('button'); if (b) b.onclick = async (ev) => { ev.stopPropagation(); await fetch('/api/agents/stop', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: a.id }) }); setTimeout(loadAgents, 1500); };
|
|
856
|
+
box.appendChild(d);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
async function openAgent(id) {
|
|
860
|
+
agentOpen = id; procOpen = null; document.querySelectorAll('.row.active').forEach(x => x.classList.remove('active'));
|
|
861
|
+
const paint = async () => {
|
|
862
|
+
let r; try { r = await (await fetch('/api/agents/log?id=' + encodeURIComponent(id))).json(); } catch (e) { return false; }
|
|
863
|
+
const meta = (r.agent || {});
|
|
864
|
+
$('#vpath').textContent = 'subagente ' + id; $('#vmeta').textContent = meta.status === 'running' ? `● corriendo · ${meta.steps} pasos · log en vivo` : `○ ${meta.status || 'terminado'} · ${meta.steps ?? '?'} pasos · ${meta.tokens ?? '?'} tokens`;
|
|
865
|
+
const pre = $('#vbody'); pre.className = 'log'; pre.textContent = (r.log || '(sin salida todavía)') + (meta.status && meta.status !== 'running' && meta.text ? '\n\n── informe ──\n' + meta.text : '');
|
|
866
|
+
return meta.status === 'running';
|
|
867
|
+
};
|
|
868
|
+
agentPaint = async () => { if (agentOpen !== id || $('#viewer').style.display === 'none') return; const still = await paint(); if (!still) loadAgents(); };
|
|
869
|
+
const running = await paint(); showPane('viewer'); $('#stage').scrollTop = $('#stage').scrollHeight;
|
|
870
|
+
clearInterval(agentTimer); if (running && !events) agentTimer = setInterval(agentPaint, 2000); // respaldo sin stream
|
|
871
|
+
loadAgents();
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async function refreshProcViewer(name) {
|
|
875
|
+
if (procOpen !== name || $('#viewer').style.display === 'none') { clearInterval(procTimer); return; }
|
|
876
|
+
const x = await (await fetch('/api/proc/log?name=' + encodeURIComponent(name) + '&tail=400')).json();
|
|
877
|
+
$('#vbody').textContent = x.log || ''; $('#vmeta').textContent = x.running ? '● corriendo · log en vivo' : '○ terminado';
|
|
878
|
+
if (!x.running) { clearInterval(procTimer); loadProcs(); }
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// ---- eventos en vivo (SSE /api/events, alimentado por el bus de Synsema) ----
|
|
882
|
+
// Los supervisores de procesos publican cada línea y cambio de estado; los subagentes, inicio/fin/pasos.
|
|
883
|
+
// La UI reacciona a eso (con un pequeño debounce) en vez de recargar por timer; los timers quedan como
|
|
884
|
+
// respaldo lento por si el stream se cae (reconexión automática con backoff).
|
|
885
|
+
let events = null, evBackoff = 1000; const debounces = {};
|
|
886
|
+
function debounce(key, fn, ms) { clearTimeout(debounces[key]); debounces[key] = setTimeout(fn, ms); }
|
|
887
|
+
function connectEvents() {
|
|
888
|
+
try { events = new EventSource('/api/events'); } catch (e) { events = null; return; }
|
|
889
|
+
events.onopen = () => { evBackoff = 1000; };
|
|
890
|
+
events.addEventListener('event', e => {
|
|
891
|
+
let ev; try { ev = JSON.parse(e.data); } catch { return; }
|
|
892
|
+
const t = ev.topic || '';
|
|
893
|
+
if (t.startsWith('proc.')) {
|
|
894
|
+
const name = ev.data && ev.data.name;
|
|
895
|
+
if (!ev.data || ev.data.line === undefined) debounce('procs', loadProcs, 300); // cambio de estado
|
|
896
|
+
if (name && procOpen === name) debounce('procview', () => refreshProcViewer(name), 250);
|
|
897
|
+
} else if (t.startsWith('mcp.')) {
|
|
898
|
+
debounce('mcp', loadMcp, 300);
|
|
899
|
+
} else if (t.startsWith('subagent.')) {
|
|
900
|
+
debounce('agents', loadAgents, 300);
|
|
901
|
+
const id = t.slice('subagent.'.length);
|
|
902
|
+
if (id && agentOpen === id && agentPaint) debounce('agentview', agentPaint, 250);
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
events.onerror = () => { events.close(); events = null; setTimeout(connectEvents, evBackoff); evBackoff = Math.min(evBackoff * 2, 30000); };
|
|
906
|
+
}
|
|
907
|
+
connectEvents();
|
|
908
|
+
|
|
909
|
+
// ---- lista de tareas del agente ----
|
|
910
|
+
async function loadTodo() {
|
|
911
|
+
let r; try { r = await (await fetch('/api/todo' + (session ? '?session=' + encodeURIComponent(session) : ''))).json(); } catch (e) { return; }
|
|
912
|
+
const box = $('#todoBox'); box.innerHTML = '';
|
|
913
|
+
const items = r.items || []; const open = items.filter(i => i.status === 'pending' || i.status === 'in_progress').length;
|
|
914
|
+
$('#todoCount').textContent = items.length ? (open ? `${open} · ${items.length}` : `${items.length}`) : ''; autoSec('todo', open > 0);
|
|
915
|
+
if (!items.length) { box.innerHTML = '<div class="p none">ninguna</div>'; return; }
|
|
916
|
+
const mark = { completed: '☑', in_progress: '▶', cancelled: '✕', pending: '☐' };
|
|
917
|
+
for (const it of items) { const d = document.createElement('div'); d.className = 'p' + (it.status === 'in_progress' ? ' run' : ''); d.style.cursor = 'default'; d.innerHTML = `<span class="nm" style="opacity:${it.status === 'completed' || it.status === 'cancelled' ? .5 : 1}">${mark[it.status] || '☐'} ${esc(it.content)}</span>`; box.appendChild(d); }
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// ---- servers MCP ----
|
|
921
|
+
let mcpFiles = { global: '.lampson/mcp.json', project: '' };
|
|
922
|
+
async function loadMcp() {
|
|
923
|
+
let r; try { r = await (await fetch('/api/mcp')).json(); } catch (e) { return; }
|
|
924
|
+
mcpFiles = { global: r.global || '.lampson/mcp.json', project: r.project || '' };
|
|
925
|
+
const box = $('#mcpBox'); box.innerHTML = '';
|
|
926
|
+
const list = r.servers || []; const ready = list.filter(s => s.status === 'ready').length;
|
|
927
|
+
$('#mcpCount').textContent = list.length ? (ready ? `${ready} ● · ${list.length}` : `${list.length}`) : ''; autoSec('mcp', list.some(s => s.status === 'error'));
|
|
928
|
+
if (!list.length) { const e = document.createElement('div'); e.className = 'p none'; e.style.cursor = 'default'; e.textContent = 'sin servers conectados'; box.appendChild(e); }
|
|
929
|
+
for (const sv of list) {
|
|
930
|
+
const d = document.createElement('div'); d.className = 'p' + (sv.status === 'ready' ? ' run' : ''); d.style.cursor = 'default';
|
|
931
|
+
d.title = `${sv.scope} · $ ${sv.command}` + (sv.error ? `
|
|
932
|
+
${sv.error}` : '') + (sv.tools.length ? `
|
|
933
|
+
tools: ${sv.tools.join(', ')}` : '');
|
|
934
|
+
d.innerHTML = `<span class="st"></span><span class="nm">${esc(sv.name)}</span><span class="cm">${esc(sv.status)} · ${sv.tools.length} tools${sv.error ? ' · ' + esc(sv.error) : ''}</span><span class="del" title="quitar este server (lo saca del mcp.json y apaga su proceso)">✕</span>`;
|
|
935
|
+
d.querySelector('.del').onclick = (ev) => { ev.stopPropagation(); inlineConfirm(d.querySelector('.del'), '¿quitar?', async () => { await fetch('/api/mcp/remove', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: sv.name }) }); loadMcp(); }); };
|
|
936
|
+
box.appendChild(d);
|
|
937
|
+
}
|
|
938
|
+
const add = document.createElement('div'); add.className = 'p'; add.innerHTML = `<span class="nm" style="color:var(--accent)">+ conectar un server…</span>`;
|
|
939
|
+
add.title = 'también podés editar a mano ' + mcpFiles.global + ' (global) o ' + mcpFiles.project + ' (proyecto), o pedírselo al agente';
|
|
940
|
+
add.onclick = openMcpAdd;
|
|
941
|
+
box.appendChild(add);
|
|
942
|
+
}
|
|
943
|
+
function openMcpAdd() {
|
|
944
|
+
$('#mcpErr').textContent = ''; $('#mcpName').value = ''; $('#mcpCmd').value = ''; $('#mcpEnv').value = '';
|
|
945
|
+
$('#mcpAddFile').textContent = $('#mcpScope').value === 'project' ? mcpFiles.project : mcpFiles.global;
|
|
946
|
+
$('#mcpAdd').style.display = 'flex'; $('#mcpName').focus();
|
|
947
|
+
}
|
|
948
|
+
$('#mcpScope').onchange = () => { $('#mcpAddFile').textContent = $('#mcpScope').value === 'project' ? mcpFiles.project : mcpFiles.global; };
|
|
949
|
+
$('#mcpCancel').onclick = () => { $('#mcpAdd').style.display = 'none'; };
|
|
950
|
+
$('#mcpGo').onclick = async () => {
|
|
951
|
+
const name = $('#mcpName').value.trim(), command = $('#mcpCmd').value.trim();
|
|
952
|
+
if (!name || !command) { $('#mcpErr').textContent = 'falta nombre o comando'; return; }
|
|
953
|
+
const env = {};
|
|
954
|
+
for (const tok of $('#mcpEnv').value.trim().split(/\s+/).filter(Boolean)) { const i = tok.indexOf('='); if (i > 0) env[tok.slice(0, i)] = tok.slice(i + 1); }
|
|
955
|
+
$('#mcpErr').textContent = 'conectando…';
|
|
956
|
+
try {
|
|
957
|
+
const r = await fetch('/api/mcp/add', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name, command, env, scope: $('#mcpScope').value }) });
|
|
958
|
+
const d = await r.json();
|
|
959
|
+
if (!r.ok) { $('#mcpErr').textContent = d.error || ('error ' + r.status); return; }
|
|
960
|
+
$('#mcpAdd').style.display = 'none'; add('meta', '⌁ ' + esc(d.result || 'server conectado'));
|
|
961
|
+
loadMcp();
|
|
962
|
+
} catch (e) { $('#mcpErr').textContent = String(e.message || e); }
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
// ---- language servers (LSP): sección del sidebar + modal para agregar (preset o comando) ----
|
|
966
|
+
let lspPresets = {};
|
|
967
|
+
async function loadLsp() {
|
|
968
|
+
let r; try { r = await (await fetch('/api/lsp')).json(); } catch (e) { return; }
|
|
969
|
+
lspPresets = r.presets || {};
|
|
970
|
+
const box = $('#lspBox'); box.innerHTML = '';
|
|
971
|
+
const list = r.servers || []; const ready = list.filter(s => s.status === 'ready').length;
|
|
972
|
+
$('#lspCount').textContent = list.length ? (ready ? `${ready} ● · ${list.length}` : `${list.length}`) : ''; autoSec('lsp', list.some(s => s.status === 'error' || s.status === 'exited'));
|
|
973
|
+
if (!list.length) { const e = document.createElement('div'); e.className = 'p none'; e.style.cursor = 'default'; e.textContent = 'sin language servers'; box.appendChild(e); }
|
|
974
|
+
for (const sv of list) {
|
|
975
|
+
const d = document.createElement('div'); d.className = 'p' + (sv.status === 'ready' ? ' run' : ''); d.style.cursor = 'default';
|
|
976
|
+
d.title = `${sv.scope} · $ ${sv.command}` + (sv.error ? `\n${sv.error}` : '');
|
|
977
|
+
d.innerHTML = `<span class="st"></span><span class="nm">${esc(sv.name)}</span><span class="cm">${esc(sv.status)} · ${esc((sv.extensions || []).join(' '))}${sv.error ? ' · ' + esc(sv.error) : ''}</span><span class="del" title="quitar este server de la config">✕</span>`;
|
|
978
|
+
d.querySelector('.del').onclick = (ev) => { ev.stopPropagation(); inlineConfirm(d.querySelector('.del'), '¿quitar?', async () => { await fetch('/api/lsp/remove', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: sv.name }) }); loadLsp(); }); };
|
|
979
|
+
box.appendChild(d);
|
|
980
|
+
}
|
|
981
|
+
const add = document.createElement('div'); add.className = 'p'; add.innerHTML = `<span class="nm" style="color:var(--accent)">+ agregar un language server…</span>`;
|
|
982
|
+
add.title = 'preset (typescript, python, rust, go, css, html) o tu propio comando · también podés editar ' + (r.global || '.lampson/lsp.json');
|
|
983
|
+
add.onclick = openLspAdd;
|
|
984
|
+
box.appendChild(add);
|
|
985
|
+
}
|
|
986
|
+
function openLspAdd() {
|
|
987
|
+
const sel = $('#lspPreset'); sel.innerHTML = '';
|
|
988
|
+
for (const k of Object.keys(lspPresets)) { const o = document.createElement('option'); o.value = k; o.textContent = `${k} — ${Object.keys(lspPresets[k].languages || {}).join(' ')}`; sel.appendChild(o); }
|
|
989
|
+
const custom = document.createElement('option'); custom.value = ''; custom.textContent = 'otro — mi propio comando'; sel.appendChild(custom);
|
|
990
|
+
$('#lspErr').textContent = ''; $('#lspName').value = ''; $('#lspCmd').value = ''; $('#lspExt').value = '';
|
|
991
|
+
lspPresetChanged(); $('#lspAdd').style.display = 'flex';
|
|
992
|
+
}
|
|
993
|
+
function lspPresetChanged() {
|
|
994
|
+
const k = $('#lspPreset').value; const p = lspPresets[k];
|
|
995
|
+
$('#lspCustom').style.display = k ? 'none' : '';
|
|
996
|
+
$('#lspInstall').textContent = p ? `$ ${p.command} ${(p.args || []).join(' ')}` + (p.install ? ` · si no está instalado: ${p.install}` : '') : '';
|
|
997
|
+
}
|
|
998
|
+
$('#lspPreset').onchange = lspPresetChanged;
|
|
999
|
+
$('#lspCancel').onclick = () => { $('#lspAdd').style.display = 'none'; };
|
|
1000
|
+
$('#lspGo').onclick = async () => {
|
|
1001
|
+
const k = $('#lspPreset').value;
|
|
1002
|
+
const body = { scope: $('#lspScope').value };
|
|
1003
|
+
if (k) { body.name = k; }
|
|
1004
|
+
else {
|
|
1005
|
+
body.name = $('#lspName').value.trim(); body.command = $('#lspCmd').value.trim(); body.languages = {};
|
|
1006
|
+
for (const tok of $('#lspExt').value.trim().split(/\s+/).filter(Boolean)) { const i = tok.indexOf('='); if (i > 0) body.languages[tok.slice(0, i)] = tok.slice(i + 1); }
|
|
1007
|
+
if (!body.name || !body.command || !Object.keys(body.languages).length) { $('#lspErr').textContent = 'falta nombre, comando o extensiones (.ext=languageId)'; return; }
|
|
1008
|
+
}
|
|
1009
|
+
$('#lspErr').textContent = 'guardando…';
|
|
1010
|
+
try {
|
|
1011
|
+
const r = await fetch('/api/lsp/add', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
|
1012
|
+
const d = await r.json();
|
|
1013
|
+
if (!r.ok) { $('#lspErr').textContent = d.error || ('error ' + r.status); return; }
|
|
1014
|
+
$('#lspAdd').style.display = 'none'; add('meta', '⌁ ' + esc(d.result || 'server agregado'));
|
|
1015
|
+
loadLsp();
|
|
1016
|
+
} catch (e) { $('#lspErr').textContent = String(e.message || e); }
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
// ---- lámparas (plugins de tools): pill en la barra superior + modal lista/detalle ----
|
|
1020
|
+
// Pensado para muchas lámparas: buscador (nombre, descripción, tools), lista compacta a la izquierda, la
|
|
1021
|
+
// elegida a la derecha con su switch, sus tools (formulario generado del schema) y acciones. La explicación
|
|
1022
|
+
// vive detrás de «?» para no saturar. Cerrar: ✕, Esc o clic fuera del cuadro.
|
|
1023
|
+
let lampsData = [], lampSel = null, lampsMeta = { global: 'lamps/', project: '.lampson/lamps/' };
|
|
1024
|
+
async function loadLamps() {
|
|
1025
|
+
let r; try { r = await (await fetch('/api/lamps')).json(); } catch (e) { return; }
|
|
1026
|
+
lampsData = r.lamps || []; lampsMeta = { global: r.global || 'lamps/', project: r.project || '.lampson/lamps/' };
|
|
1027
|
+
const on = lampsData.filter(l => l.enabled).length;
|
|
1028
|
+
const pill = $('#lamps'); pill.textContent = lampsData.length ? `lámparas ${on}/${lampsData.length}` : 'lámparas'; pill.classList.toggle('on', on > 0);
|
|
1029
|
+
$('#lampsGlobal').textContent = lampsMeta.global; $('#lampsProject').textContent = lampsMeta.project;
|
|
1030
|
+
$('#lampsSub').textContent = lampsData.length ? `${on} encendida${on === 1 ? '' : 's'} · ${lampsData.length} en total` : '';
|
|
1031
|
+
if (lampSel && !lampsData.some(l => l.name === lampSel)) lampSel = null;
|
|
1032
|
+
renderLampList();
|
|
1033
|
+
}
|
|
1034
|
+
function lampMatches(l, q) {
|
|
1035
|
+
if (!q) return true;
|
|
1036
|
+
const hay = [l.name, l.description || '', l.scope, l.kind, ...(l.tools || [])].join(' ').toLowerCase();
|
|
1037
|
+
return q.split(/\s+/).every(t => hay.includes(t));
|
|
1038
|
+
}
|
|
1039
|
+
function renderLampList() {
|
|
1040
|
+
const q = ($('#lampsSearch').value || '').trim().toLowerCase();
|
|
1041
|
+
const list = lampsData.filter(l => lampMatches(l, q));
|
|
1042
|
+
const box = $('#lampsItems'); box.innerHTML = '';
|
|
1043
|
+
if (!lampsData.length) { box.innerHTML = '<div class="none" style="padding:8px;color:var(--ink-3);font:400 12px/1.5 var(--serif)">ninguna todavía</div>'; }
|
|
1044
|
+
else if (!list.length) { box.innerHTML = '<div class="none" style="padding:8px;color:var(--ink-3);font:400 12px/1.5 var(--serif)">nada coincide con la búsqueda</div>'; }
|
|
1045
|
+
if (list.length && (!lampSel || !list.some(l => l.name === lampSel))) lampSel = list[0].name;
|
|
1046
|
+
for (const l of list) {
|
|
1047
|
+
const d = document.createElement('div'); d.className = 'li' + (l.enabled ? ' on' : '') + (l.error ? ' broken' : '') + (l.name === lampSel ? ' sel' : '');
|
|
1048
|
+
d.innerHTML = `<span class="dot">${l.error ? '!' : (l.enabled ? '●' : '○')}</span><div><div class="nm">${esc(l.name)}</div><div class="meta">${esc(l.scope)} · ${esc(l.kind)} · ${(l.tools || []).length} tool${(l.tools || []).length === 1 ? '' : 's'}${l.error ? ' · rota' : ''}</div></div>`;
|
|
1049
|
+
d.onclick = () => { lampSel = l.name; renderLampList(); };
|
|
1050
|
+
box.appendChild(d);
|
|
1051
|
+
}
|
|
1052
|
+
$('#lampsCount').textContent = q ? `${list.length} de ${lampsData.length}` : `${lampsData.length} lámpara${lampsData.length === 1 ? '' : 's'}`;
|
|
1053
|
+
renderLampDetail();
|
|
1054
|
+
}
|
|
1055
|
+
function lampField(k, p, required) {
|
|
1056
|
+
const def = p.default !== undefined ? p.default : ''; const req = required.includes(k) ? ' <span class="req">(obligatorio)</span>' : '';
|
|
1057
|
+
let input;
|
|
1058
|
+
if (Array.isArray(p.enum)) input = `<select data-k="${esc(k)}">${p.enum.map(v => `<option value="${esc(String(v))}" ${String(v) === String(def) ? 'selected' : ''}>${esc(String(v))}</option>`).join('')}</select>`;
|
|
1059
|
+
else if (p.type === 'boolean') input = `<select data-k="${esc(k)}"><option value="false" ${def === true ? '' : 'selected'}>no</option><option value="true" ${def === true ? 'selected' : ''}>sí</option></select>`;
|
|
1060
|
+
else if (p.type === 'object' || p.type === 'array') input = `<textarea data-k="${esc(k)}" data-json="1" spellcheck="false" placeholder="JSON">${esc(def === '' ? (p.type === 'array' ? '[]' : '{}') : JSON.stringify(def, null, 1))}</textarea>`;
|
|
1061
|
+
else input = `<input data-k="${esc(k)}" type="${p.type === 'integer' || p.type === 'number' ? 'number' : 'text'}" value="${esc(String(def))}" placeholder="${esc(p.type || 'texto')}">`;
|
|
1062
|
+
return `<label class="lf"><span class="lk">${esc(k)}${req}</span>${input}${p.description ? `<span class="ds">${esc(p.description)}</span>` : ''}</label>`;
|
|
1063
|
+
}
|
|
1064
|
+
function renderLampDetail() {
|
|
1065
|
+
const box = $('#lampsDetail'); box.innerHTML = '';
|
|
1066
|
+
const l = lampsData.find(x => x.name === lampSel);
|
|
1067
|
+
if (!l) {
|
|
1068
|
+
box.innerHTML = lampsData.length ? '<div class="none">elegí una lámpara de la lista</div>' : `<div class="none">Todavía no hay lámparas.<br><br>Pedile una al agente en el chat — <i>«hacé una lámpara que liste los endpoints del backend»</i> — o creá una carpeta con <code>lamp.json</code> en <code>${esc(lampsMeta.project)}</code> (proyecto) o <code>${esc(lampsMeta.global)}</code> (global). El botón <b>?</b> de arriba explica qué son y para qué sirven.</div>`;
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
const specs = l.tool_specs || [];
|
|
1072
|
+
const cap = l.error ? `<div class="dcap err">rota: ${esc(l.error)}</div>` : (l.kind === 'syn' ? `<div class="dcap">techo de capacidades: ${esc(l.caps)}</div>` : `<div class="dcap"><b>sin techo</b> (exec) · $ ${esc(l.command)}</div>`);
|
|
1073
|
+
box.innerHTML = `<div class="dhead"><span class="nm">${esc(l.name)}</span><span class="meta">${esc(l.scope)} · ${esc(l.kind)}</span><label class="sw ${l.enabled ? 'on' : ''}" title="${l.error ? 'no se puede encender: está rota' : (l.enabled ? 'apagar' : 'encender')}"><input type="checkbox" ${l.enabled ? 'checked' : ''} ${l.error ? 'disabled' : ''}>${l.enabled ? 'encendida' : 'apagada'}</label></div>`
|
|
1074
|
+
+ (l.description ? `<div class="ddesc">${esc(l.description)}</div>` : '') + cap
|
|
1075
|
+
+ (l.enabled || l.error ? '' : `<div class="off-note">Apagada: el agente no la ve y no se puede correr. Encendela con el switch — el código va a poder ejecutarse${l.kind === 'exec' ? ' <b>sin techo de capacidades</b> (es un ejecutable)' : ' bajo el techo de arriba'}.</div>`)
|
|
1076
|
+
+ specs.map((t, i) => {
|
|
1077
|
+
const props = (t.parameters && t.parameters.properties) || {}; const required = (t.parameters && t.parameters.required) || [];
|
|
1078
|
+
return `<div class="tool" data-i="${i}"><div class="th"><code>${esc(t.name)}</code>${t.description ? `<span class="ds">${esc(t.description)}</span>` : ''}<span class="ds" style="margin-left:auto">lamp_${esc(l.name)}_${esc(t.name)}</span></div>`
|
|
1079
|
+
+ (Object.keys(props).length ? Object.keys(props).map(k => lampField(k, props[k], required)).join('') : '<div class="ds" style="margin-top:6px;color:var(--ink-3)">no toma parámetros</div>')
|
|
1080
|
+
+ (l.enabled ? `<div class="go"><button class="primary run">▶ correr</button><span class="st"></span></div><pre class="out" style="display:none"></pre>` : '')
|
|
1081
|
+
+ `</div>`;
|
|
1082
|
+
}).join('')
|
|
1083
|
+
+ `<div class="dfoot"><span>${esc(l.dir || '')}</span>${l.scope === 'project' ? '<span class="del">eliminar</span>' : '<span title="las globales se borran a mano">global · se borra a mano</span>'}</div>`;
|
|
1084
|
+
box.querySelector('.sw input').onchange = async (ev) => {
|
|
1085
|
+
$('#lampsErr').textContent = '';
|
|
1086
|
+
try {
|
|
1087
|
+
const rr = await fetch('/api/lamps/toggle', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: l.name, enabled: ev.target.checked }) });
|
|
1088
|
+
const dd = await rr.json(); if (!rr.ok) { $('#lampsErr').textContent = dd.error || ('error ' + rr.status); ev.target.checked = !ev.target.checked; return; }
|
|
1089
|
+
add('meta', '☼ ' + esc(dd.result || ''));
|
|
1090
|
+
} catch (e) { $('#lampsErr').textContent = String(e.message || e); }
|
|
1091
|
+
loadLamps();
|
|
1092
|
+
};
|
|
1093
|
+
for (const tdiv of box.querySelectorAll('.tool')) {
|
|
1094
|
+
const t = specs[Number(tdiv.dataset.i)]; const props = (t.parameters && t.parameters.properties) || {}; const required = (t.parameters && t.parameters.required) || [];
|
|
1095
|
+
const b = tdiv.querySelector('button.run'); if (!b) continue;
|
|
1096
|
+
b.onclick = async () => {
|
|
1097
|
+
const st = tdiv.querySelector('.st'), out = tdiv.querySelector('.out'); const args = {};
|
|
1098
|
+
for (const el of tdiv.querySelectorAll('[data-k]')) {
|
|
1099
|
+
const k = el.dataset.k, p = props[k] || {}; let v = el.value;
|
|
1100
|
+
if (el.dataset.json) { if (!v.trim()) continue; try { v = JSON.parse(v); } catch (e) { st.textContent = `${k}: JSON inválido`; return; } }
|
|
1101
|
+
else if (p.type === 'boolean') v = v === 'true';
|
|
1102
|
+
else if (p.type === 'integer' || p.type === 'number') { if (v === '') { if (required.includes(k)) { st.textContent = `${k} es obligatorio`; return; } continue; } v = Number(v); }
|
|
1103
|
+
else if (v === '') { if (required.includes(k)) { st.textContent = `${k} es obligatorio`; return; } continue; }
|
|
1104
|
+
args[k] = v;
|
|
1105
|
+
}
|
|
1106
|
+
st.textContent = 'corriendo…'; out.style.display = 'none'; b.disabled = true;
|
|
1107
|
+
try {
|
|
1108
|
+
const rr = await fetch('/api/lamps/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ tool: 'lamp_' + l.name + '_' + t.name, args }) });
|
|
1109
|
+
const dd = await rr.json(); st.textContent = rr.ok ? '' : (dd.error || 'error ' + rr.status);
|
|
1110
|
+
if (rr.ok) { out.textContent = dd.output; out.style.display = ''; }
|
|
1111
|
+
} catch (e) { st.textContent = String(e.message || e); }
|
|
1112
|
+
b.disabled = false;
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
const del = box.querySelector('.dfoot .del');
|
|
1116
|
+
if (del) del.onclick = () => inlineConfirm(del, `¿borrar ${l.name} del disco?`, async () => {
|
|
1117
|
+
$('#lampsErr').textContent = '';
|
|
1118
|
+
try {
|
|
1119
|
+
const rr = await fetch('/api/lamps/remove', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: l.name }) });
|
|
1120
|
+
const dd = await rr.json(); if (!rr.ok) { $('#lampsErr').textContent = dd.error || ('error ' + rr.status); return; }
|
|
1121
|
+
add('meta', '☼ ' + esc(dd.result || '')); lampSel = null;
|
|
1122
|
+
} catch (e) { $('#lampsErr').textContent = String(e.message || e); }
|
|
1123
|
+
loadLamps();
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
function openLamps() { $('#lampsErr').textContent = ''; $('#lampsHelpBox').style.display = 'none'; $('#lampsBody').style.display = ''; $('#lampsModal').style.display = 'flex'; loadLamps().then(() => $('#lampsSearch').focus()); }
|
|
1127
|
+
function closeLamps() { $('#lampsModal').style.display = 'none'; }
|
|
1128
|
+
$('#lamps').onclick = openLamps;
|
|
1129
|
+
if (location.hash === '#lamps') setTimeout(openLamps, 400); // deep link: abre el panel al cargar
|
|
1130
|
+
$('#lampsClose').onclick = closeLamps;
|
|
1131
|
+
$('#lampsModal').addEventListener('click', (e) => { if (e.target === $('#lampsModal')) closeLamps(); });
|
|
1132
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && $('#lampsModal').style.display !== 'none') closeLamps(); });
|
|
1133
|
+
$('#lampsSearch').addEventListener('input', renderLampList);
|
|
1134
|
+
$('#lampsHelp').onclick = () => { const h = $('#lampsHelpBox'); const show = h.style.display === 'none'; h.style.display = show ? '' : 'none'; $('#lampsBody').style.display = show ? 'none' : ''; };
|
|
1135
|
+
$('#lampsHelpBack').onclick = () => { $('#lampsHelpBox').style.display = 'none'; $('#lampsBody').style.display = ''; };
|
|
1136
|
+
|
|
1137
|
+
// ---- traza legible de una sesión (.lampson/trace/<sid>.log) ----
|
|
1138
|
+
async function openTrace(id) {
|
|
1139
|
+
procOpen = null; agentOpen = null;
|
|
1140
|
+
let r; try { r = await (await fetch('/api/trace?session=' + encodeURIComponent(id) + '&tail=600')).json(); } catch (e) { return; }
|
|
1141
|
+
$('#vpath').textContent = 'traza ' + id; $('#vmeta').textContent = r.file || '';
|
|
1142
|
+
const pre = $('#vbody'); pre.className = 'log'; pre.textContent = r.trace || '(sin traza todavía: se escribe a medida que el agente trabaja)';
|
|
1143
|
+
showPane('viewer'); $('#stage').scrollTop = $('#stage').scrollHeight;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// ---- sessions ----
|
|
1147
|
+
async function loadSessions() {
|
|
1148
|
+
let r; try { r = await (await fetch('/api/sessions')).json(); } catch (e) { return; }
|
|
1149
|
+
const box = $('#sessions'); box.innerHTML = '';
|
|
1150
|
+
$('#sessCount').textContent = r.sessions.length || '';
|
|
1151
|
+
if (!r.sessions.length) { box.innerHTML = '<div class="s none">todavía ninguna</div>'; return; }
|
|
1152
|
+
for (const s of r.sessions) {
|
|
1153
|
+
const d = document.createElement('div'); d.className = 's' + (s.id === session ? ' active' : ''); d.innerHTML = '<span class="id">' + esc(s.id.slice(-6)) + '</span><span class="title">' + esc(s.title || '') + '</span><span class="del tr" title="traza: qué hizo el agente en esta sesión (pasos, tools, tiempos, tokens)">≡</span><span class="del" title="borrar esta sesión">✕</span>'; d.title = s.id; d.onclick = () => open(s.id);
|
|
1154
|
+
d.querySelector('.tr').onclick = (ev) => { ev.stopPropagation(); openTrace(s.id); };
|
|
1155
|
+
d.querySelector('.del:not(.tr)').onclick = (ev) => { ev.stopPropagation(); inlineConfirm(d.querySelector('.del:not(.tr)'), '¿borrar?', async () => { await fetch('/api/sessions/delete', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: s.id }) }); if (s.id === session) { session = ''; localStorage.removeItem('lampson.session'); log.innerHTML = ''; showPane('log'); empty(); } loadSessions(); }); };
|
|
1156
|
+
box.appendChild(d);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
async function open(id) {
|
|
1160
|
+
session = id; localStorage.setItem('lampson.session', id); log.innerHTML = ''; log.classList.remove('hero'); showPane('log'); loadTodo();
|
|
1161
|
+
const r = await fetch('/api/sessions/' + id); if (!r.ok) { session = ''; empty(); return loadSessions(); }
|
|
1162
|
+
const d = await r.json();
|
|
1163
|
+
const queue = []; // pasos sin resultado todavía, en orden (varias tool calls por turno)
|
|
1164
|
+
for (const m of d.messages) {
|
|
1165
|
+
// los avisos del harness viajan con role user (lo exige el wire) pero no los escribió el usuario:
|
|
1166
|
+
// al recargar se muestran como meta (✉), igual que en vivo — nunca con la etiqueta "vos"
|
|
1167
|
+
if (m.role === 'user') { queue.length = 0; const t = String(m.content || ''); if (t.startsWith('[harness]') || t.startsWith('[Context compacted')) add('meta', '✉ ' + esc(t.split('\n')[0].slice(0, 160))); else add('user', esc(m.content) + imagesHtml(m.images)); }
|
|
1168
|
+
else if (m.role === 'assistant') { if (m.content) add('assistant', md(m.content)); for (const c of (m.tool_calls || [])) { const el = add('step', `<span class="ic">⚙</span>${cmdHtml(describe(c))}`); wireMore(el); queue.push(el); } }
|
|
1169
|
+
else if (m.role === 'tool') { const out = String(m.content); const bad = /^(ERROR|DENIED)/.test(out); const el = queue.shift() || add('step', '<span class="ic">→</span>'); 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>`); }
|
|
1170
|
+
}
|
|
1171
|
+
loadSessions();
|
|
1172
|
+
}
|
|
1173
|
+
$('#new').onclick = () => { session = ''; localStorage.removeItem('lampson.session'); log.innerHTML = ''; showPane('log'); empty(); loadSessions(); loadTodo(); };
|
|
1174
|
+
|
|
1175
|
+
// ---- imágenes pegadas / arrastradas al composer ----
|
|
1176
|
+
// Se reducen en el navegador (lado mayor ≤ 1568 px, el límite útil de los modelos con visión) y viajan como
|
|
1177
|
+
// base64 en el POST; PNG si sigue liviano (capturas con texto), JPEG si no. Máximo 4 por mensaje.
|
|
1178
|
+
let attachments = []; // [{media_type, data(base64), preview(dataURL), bytes}]
|
|
1179
|
+
const MAX_IMAGES = 4, MAX_SIDE = 1568;
|
|
1180
|
+
function fmtBytes(n) { return n > 1048576 ? (n / 1048576).toFixed(1) + ' MB' : Math.round(n / 1024) + ' KB'; }
|
|
1181
|
+
async function shrinkImage(file) {
|
|
1182
|
+
const url = URL.createObjectURL(file); const img = new Image();
|
|
1183
|
+
await new Promise((ok, ko) => { img.onload = ok; img.onerror = ko; img.src = url; }); URL.revokeObjectURL(url);
|
|
1184
|
+
const scale = Math.min(1, MAX_SIDE / Math.max(img.width, img.height));
|
|
1185
|
+
const c = document.createElement('canvas'); c.width = Math.round(img.width * scale); c.height = Math.round(img.height * scale);
|
|
1186
|
+
c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
|
|
1187
|
+
let dataUrl = c.toDataURL('image/png'); let type = 'image/png';
|
|
1188
|
+
if (dataUrl.length > 1.4 * 1048576) { dataUrl = c.toDataURL('image/jpeg', 0.85); type = 'image/jpeg'; }
|
|
1189
|
+
const data = dataUrl.slice(dataUrl.indexOf(',') + 1);
|
|
1190
|
+
return { media_type: type, data, preview: dataUrl, bytes: Math.round(data.length * 0.75), w: c.width, h: c.height };
|
|
1191
|
+
}
|
|
1192
|
+
function paintAttach() {
|
|
1193
|
+
const box = $('#attach'); box.innerHTML = '';
|
|
1194
|
+
if (attachments.length && cfg.vision === false) { const w = document.createElement('div'); w.className = 'novision'; w.textContent = `${cfg.provider} · ${cfg.model} no acepta imágenes: el modelo recibirá solo una nota. Cambiá a un modelo con visión (clic en el modelo, arriba) para que la vea.`; box.appendChild(w); }
|
|
1195
|
+
attachments.forEach((a, i) => { const d = document.createElement('div'); d.className = 'th'; d.title = `${a.w}×${a.h} · ${a.media_type} · ${fmtBytes(a.bytes)}`; d.innerHTML = `<img src="${a.preview}" alt=""><span class="rm" title="quitar">✕</span><span class="sz">${fmtBytes(a.bytes)}</span>`; d.querySelector('.rm').onclick = () => { attachments.splice(i, 1); paintAttach(); }; box.appendChild(d); });
|
|
1196
|
+
}
|
|
1197
|
+
async function addImages(files) {
|
|
1198
|
+
for (const f of files) {
|
|
1199
|
+
if (!f.type.startsWith('image/')) continue;
|
|
1200
|
+
if (attachments.length >= MAX_IMAGES) { add('meta', `máximo ${MAX_IMAGES} imágenes por mensaje`); break; }
|
|
1201
|
+
try { attachments.push(await shrinkImage(f)); } catch (e) { add('denied', 'no pude leer la imagen: ' + esc(e.message)); }
|
|
1202
|
+
}
|
|
1203
|
+
paintAttach(); $('#in').focus();
|
|
1204
|
+
}
|
|
1205
|
+
$('#in').addEventListener('paste', e => { const files = [...(e.clipboardData?.items || [])].filter(i => i.kind === 'file').map(i => i.getAsFile()).filter(Boolean); if (files.length) { e.preventDefault(); addImages(files); } });
|
|
1206
|
+
const composer = document.querySelector('.composer');
|
|
1207
|
+
composer.addEventListener('dragover', e => { e.preventDefault(); composer.classList.add('drop'); });
|
|
1208
|
+
composer.addEventListener('dragleave', () => composer.classList.remove('drop'));
|
|
1209
|
+
composer.addEventListener('drop', e => { e.preventDefault(); composer.classList.remove('drop'); addImages([...e.dataTransfer.files]); });
|
|
1210
|
+
function imagesHtml(ims) { if (!ims || !ims.length) return ''; return '<div class="ims">' + ims.map(im => `<div class="th"><img src="data:${esc(im.media_type)};base64,${im.data}" alt="imagen adjunta"></div>`).join('') + '</div>'; }
|
|
1211
|
+
log.addEventListener('click', e => { const im = e.target.closest('.user .th img'); if (im) window.open(im.src, '_blank'); });
|
|
1212
|
+
|
|
1213
|
+
// ---- chat (SSE) ----
|
|
1214
|
+
let aborter = null;
|
|
1215
|
+
function setBusy(b) { busy = b; $('#f').classList.toggle('busy', b); $('#send').disabled = b; }
|
|
1216
|
+
$('#stop').onclick = () => { if (aborter) aborter.abort(); };
|
|
1217
|
+
async function send(message) {
|
|
1218
|
+
setBusy(true); aborter = new AbortController();
|
|
1219
|
+
if (log.querySelector('.empty')) { log.innerHTML = ''; log.classList.remove('hero'); }
|
|
1220
|
+
const images = attachments.map(a => ({ media_type: a.media_type, data: a.data, w: a.w, h: a.h })); attachments = []; paintAttach();
|
|
1221
|
+
add('user', esc(message) + imagesHtml(images));
|
|
1222
|
+
const thinking = add('working', '<span class="dot"></span><span>pensando…</span>');
|
|
1223
|
+
try {
|
|
1224
|
+
const res = await fetch('/api/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ session, message, agent: $('#agent').value || 'build', permission: $('#perm').value || 'ask', images }), signal: aborter.signal });
|
|
1225
|
+
if (!res.ok || !res.body) { add('denied', 'error HTTP ' + res.status + ' ' + esc(await res.text())); }
|
|
1226
|
+
else {
|
|
1227
|
+
const reader = res.body.getReader(); const dec = new TextDecoder(); let buf = '';
|
|
1228
|
+
while (true) {
|
|
1229
|
+
const { value, done } = await reader.read(); if (done) break;
|
|
1230
|
+
buf += dec.decode(value, { stream: true });
|
|
1231
|
+
let i; while ((i = buf.indexOf('\n\n')) >= 0) { const chunk = buf.slice(0, i); buf = buf.slice(i + 2); handle(chunk, thinking); }
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
} catch (e) { add('meta', e.name === 'AbortError' ? '■ detenido por vos (el agente para en el próximo paso; el historial queda guardado hasta ahí)' : '‼ ' + esc(e.message)); }
|
|
1235
|
+
thinking.remove(); aborter = null; setBusy(false); loadSessions(); loadTree(); loadProcs(); loadMemory(); loadAgents();
|
|
1236
|
+
}
|
|
1237
|
+
let pending = null;
|
|
1238
|
+
function handle(chunk, thinking) {
|
|
1239
|
+
const data = chunk.split('\n').filter(l => l.startsWith('data:')).map(l => l.slice(5).trim()).join('\n'); if (!data) return;
|
|
1240
|
+
let ev; try { ev = JSON.parse(data); } catch { return; }
|
|
1241
|
+
const k = ev.kind, d = ev.data;
|
|
1242
|
+
if (k === 'session') { const changed = d !== session; session = d; localStorage.setItem('lampson.session', d); if (changed) loadTodo(); }
|
|
1243
|
+
else if (k === 'assistant') add('assistant', md(d));
|
|
1244
|
+
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
|
+
else if (k === 'inbox') { add('meta', '✉ ' + esc(String(d).split('\n')[0].slice(0, 160))); loadAgents(); }
|
|
1246
|
+
else if (k === 'tool_result') { const out = String(d.output); const bad = /^(ERROR|DENIED)/.test(out);
|
|
1247
|
+
// 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
|
+
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
|
+
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; }
|
|
1250
|
+
else if (k === 'approval_result') { const el = [...log.querySelectorAll('.approval')].find(x => x.dataset.id === d.id); if (el) el.querySelector('.btns').innerHTML = d.approved ? '<span style="color:var(--str)">✓ permitido</span>' : (d.timeout ? '<span class="denied">✗ sin respuesta, denegado</span>' : '<span class="denied">✗ denegado</span>'); }
|
|
1251
|
+
else if (k === 'mode') { $('#agent').value = d.agent; $('#perm').value = d.permission; if (d.has_key === false) { add('denied', `‼ no hay API key para ${esc(d.provider)} — cargala en la ventana de modelo (clic en «${esc(d.provider)} · ${esc(d.model)}» arriba)`); } }
|
|
1252
|
+
else if (k === 'tool_denied') { const el = pending || add('step', '<span class="ic">✗</span>'); el.insertAdjacentHTML('beforeend', `<span class="denied"> ✗ ${esc(d.reason)}</span>`); }
|
|
1253
|
+
// el hint de "modelo inválido" solo cuando el error realmente habla de eso ("/model/ a secas" saltaba
|
|
1254
|
+
// con cualquier error que mencione al modelo, p.ej. "the model used its whole output budget")
|
|
1255
|
+
else if (k === 'error') { add('denied', '‼ ' + esc(d)); if (/(invalid|unknown|no such|not exist|not found|unsupported).{0,30}model|model.{0,30}(invalid|unknown|does not exist|not found|unsupported)/i.test(String(d))) add('meta', `el proveedor rechazó el nombre del modelo (${esc(cfg.model || '')}) — clic en «${esc(cfg.provider || '')} · ${esc(cfg.model || '')}» arriba: la lista de modelos válidos aparece al elegir el proveedor`); }
|
|
1256
|
+
else if (k === 'compact') add('meta', '⧗ compactando contexto (~' + d.before + ' tokens)');
|
|
1257
|
+
else if (k === 'done') { if (d.stopped !== 'user_command') add('meta', `${d.steps} paso${d.steps === 1 ? '' : 's'} · ${d.usage.input + d.usage.output} tokens · ${d.stopped}`); }
|
|
1258
|
+
log.appendChild(thinking); $('#stage').scrollTop = $('#stage').scrollHeight;
|
|
1259
|
+
}
|
|
1260
|
+
$('#f').onsubmit = e => { e.preventDefault(); const t = $('#in').value.trim(); if ((!t && !attachments.length) || busy) return; $('#in').value = ''; send(t || '(imagen adjunta)'); };
|
|
1261
|
+
$('#in').addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (!busy) $('#f').requestSubmit(); } });
|
|
1262
|
+
$('#in').addEventListener('input', () => { const t = $('#in'); t.style.height = 'auto'; t.style.height = Math.min(t.scrollHeight, 220) + 'px'; });
|
|
1263
|
+
loadCfg().then(() => { loadTree(); loadSessions(); loadProcs(); loadMemory(); loadAgents(); loadMcp(); loadLsp(); loadLamps(); loadTodo(); checkUpdate(); const pq = new URLSearchParams(location.search).get('proc'); if (pq) openProc(pq); else if (session) open(session); });
|
|
1264
|
+
setInterval(loadProcs, events ? 60000 : 10000);
|
|
1265
|
+
setInterval(() => { if (!events && ($('#agentCount').textContent.includes('●') || busy)) loadAgents(); }, 5000);
|
|
1266
|
+
</script>
|
|
1267
|
+
</body>
|
|
1268
|
+
</html>
|