sapper-iq 1.4.3 → 1.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/sapper-ui.mjs +271 -1
package/package.json
CHANGED
package/sapper-ui.mjs
CHANGED
|
@@ -161,7 +161,104 @@ function buildHTML() {
|
|
|
161
161
|
--red: #f85149;
|
|
162
162
|
--yellow: #d29922;
|
|
163
163
|
--purple: #bc8cff;
|
|
164
|
-
|
|
164
|
+
--radius: 6px;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* ─── Studio theme — calmer palette, card-based composer (toggle in top bar) ─── */
|
|
168
|
+
body.studio {
|
|
169
|
+
--bg: #131316;
|
|
170
|
+
--panel: #18181b;
|
|
171
|
+
--panel2: #1f1f23;
|
|
172
|
+
--border: #2a2a30;
|
|
173
|
+
--border2: #3a3a42;
|
|
174
|
+
--fg: #ececef;
|
|
175
|
+
--muted: #9b9ba4;
|
|
176
|
+
--dim: #6c6c75;
|
|
177
|
+
--accent: #7aa2f7;
|
|
178
|
+
--accent2: #9eb8ff;
|
|
179
|
+
--radius: 10px;
|
|
180
|
+
}
|
|
181
|
+
body.studio #bar { height: 44px; background: var(--bg); border-bottom: 1px solid var(--border); }
|
|
182
|
+
body.studio #bar button { border-radius: 8px; padding: 5px 12px; }
|
|
183
|
+
body.studio #side, body.studio #preview { background: var(--bg); }
|
|
184
|
+
body.studio .tabs { padding: 6px 8px 0; gap: 4px; }
|
|
185
|
+
body.studio .tabs button { border-radius: 8px 8px 0 0; }
|
|
186
|
+
body.studio .item { border-radius: 8px; margin: 2px 8px; padding: 8px 12px; border-left: none; }
|
|
187
|
+
body.studio .item:hover { background: var(--panel2); }
|
|
188
|
+
body.studio .row { border-radius: 7px; margin: 1px 6px; padding: 4px 8px; }
|
|
189
|
+
/* Center becomes a calm canvas with a floating composer card */
|
|
190
|
+
body.studio #center { background: var(--bg); }
|
|
191
|
+
body.studio #qa {
|
|
192
|
+
margin: 14px auto 0; max-width: 760px; width: calc(100% - 28px);
|
|
193
|
+
background: var(--panel); border: 1px solid var(--border2);
|
|
194
|
+
border-radius: 14px; padding: 10px 12px; gap: 8px;
|
|
195
|
+
box-shadow: 0 8px 28px rgba(0,0,0,.35);
|
|
196
|
+
}
|
|
197
|
+
body.studio #qa .qabtn { border-radius: 9px; background: var(--panel2); border-color: var(--border2); }
|
|
198
|
+
body.studio #qa .qabtn:hover { background: var(--border); }
|
|
199
|
+
body.studio #term-wrap {
|
|
200
|
+
max-width: 760px; width: calc(100% - 28px); margin: 10px auto 14px;
|
|
201
|
+
background: var(--panel); border: 1px solid var(--border2);
|
|
202
|
+
border-radius: 14px; padding: 12px 14px 0; flex: 1;
|
|
203
|
+
box-shadow: 0 8px 28px rgba(0,0,0,.35);
|
|
204
|
+
}
|
|
205
|
+
body.studio .modal, body.studio #indexPanel, body.studio #activityPanel { border-radius: 12px; }
|
|
206
|
+
body.studio .switch { border-radius: 9px; }
|
|
207
|
+
body.studio #bar button.toggle.on { background: rgba(122,162,247,.14); }
|
|
208
|
+
|
|
209
|
+
/* ─── Chat view (Studio) — reskins the live terminal stream as a chat transcript ─── */
|
|
210
|
+
#chat { display: none; flex: 1; min-height: 0; min-width: 0; flex-direction: column;
|
|
211
|
+
background: var(--bg); overflow: hidden; }
|
|
212
|
+
body.studio #qa, body.studio #term-wrap { display: none; }
|
|
213
|
+
body.studio #chat { display: flex; }
|
|
214
|
+
body.studio.rawterm #chat { display: none; }
|
|
215
|
+
body.studio.rawterm #qa { display: flex; }
|
|
216
|
+
body.studio.rawterm #term-wrap { display: block; }
|
|
217
|
+
|
|
218
|
+
#chatLog { flex: 1; min-height: 0; overflow-y: auto; padding: 22px 0 8px;
|
|
219
|
+
display: flex; flex-direction: column; gap: 14px; }
|
|
220
|
+
#chatLog::-webkit-scrollbar { width: 9px; }
|
|
221
|
+
#chatLog::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 5px; }
|
|
222
|
+
.cmsg { max-width: 760px; width: calc(100% - 36px); margin: 0 auto; display: flex; gap: 10px; }
|
|
223
|
+
.cmsg .avatar { width: 26px; height: 26px; border-radius: 7px; flex-shrink: 0;
|
|
224
|
+
display: flex; align-items: center; justify-content: center; font-size: 13px;
|
|
225
|
+
background: var(--panel2); border: 1px solid var(--border2); }
|
|
226
|
+
.cmsg.user .avatar { background: rgba(122,162,247,.16); border-color: rgba(122,162,247,.4); color: var(--accent); }
|
|
227
|
+
.cmsg .body { flex: 1; min-width: 0; }
|
|
228
|
+
.cmsg .who { font-size: 11px; color: var(--dim); margin-bottom: 4px; font-weight: 600;
|
|
229
|
+
text-transform: uppercase; letter-spacing: .5px; }
|
|
230
|
+
.cmsg .bubble { background: var(--panel); border: 1px solid var(--border);
|
|
231
|
+
border-radius: 12px; padding: 10px 14px; font-size: 13px; line-height: 1.55;
|
|
232
|
+
color: var(--fg); white-space: pre-wrap; word-break: break-word;
|
|
233
|
+
font-family: ui-monospace, 'SF Mono', 'JetBrains Mono', monospace; overflow-x: auto; }
|
|
234
|
+
.cmsg.user .bubble { background: rgba(122,162,247,.10); border-color: rgba(122,162,247,.28);
|
|
235
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; }
|
|
236
|
+
.cmsg .bubble.system { background: transparent; border-style: dashed; color: var(--muted); font-size: 12px; }
|
|
237
|
+
|
|
238
|
+
#chatComposer { flex-shrink: 0; max-width: 760px; width: calc(100% - 36px);
|
|
239
|
+
margin: 0 auto 16px; background: var(--panel); border: 1px solid var(--border2);
|
|
240
|
+
border-radius: 14px; padding: 10px 12px 8px; box-shadow: 0 8px 28px rgba(0,0,0,.35); }
|
|
241
|
+
#chatComposer:focus-within { border-color: var(--accent); }
|
|
242
|
+
#chatBox { width: 100%; background: transparent; border: none; outline: none; resize: none;
|
|
243
|
+
color: var(--fg); font-size: 14px; line-height: 1.5; font-family: inherit;
|
|
244
|
+
max-height: 200px; overflow-y: auto; padding: 2px 2px 6px; }
|
|
245
|
+
#chatBox::placeholder { color: var(--dim); }
|
|
246
|
+
.ccbar { display: flex; align-items: center; gap: 8px; }
|
|
247
|
+
.ccbar .ccmeta { font-size: 11px; color: var(--dim); display: inline-flex; align-items: center; gap: 5px; }
|
|
248
|
+
.ccbar .ccsp { flex: 1; }
|
|
249
|
+
.ccbar button { background: var(--panel2); color: var(--muted); border: 1px solid var(--border2);
|
|
250
|
+
border-radius: 8px; cursor: pointer; font-family: inherit; transition: all .12s; }
|
|
251
|
+
.ccbar .ccslash { width: 28px; height: 28px; font-size: 14px; font-weight: 700; }
|
|
252
|
+
.ccbar .ccslash:hover { color: var(--accent); border-color: var(--accent); }
|
|
253
|
+
.ccbar .ccsend { width: 32px; height: 28px; font-size: 14px; color: #fff;
|
|
254
|
+
background: var(--accent); border-color: var(--accent); }
|
|
255
|
+
.ccbar .ccsend:hover { filter: brightness(1.08); }
|
|
256
|
+
#chatLog .typing { color: var(--dim); font-size: 12px; padding: 2px 0; }
|
|
257
|
+
#chatLog .typing i { display: inline-block; width: 5px; height: 5px; margin: 0 1px;
|
|
258
|
+
border-radius: 50%; background: var(--dim); animation: tdot 1s infinite; }
|
|
259
|
+
#chatLog .typing i:nth-child(2){ animation-delay:.15s; } #chatLog .typing i:nth-child(3){ animation-delay:.3s; }
|
|
260
|
+
@keyframes tdot { 0%,60%,100%{opacity:.25;transform:translateY(0);} 30%{opacity:1;transform:translateY(-2px);} }
|
|
261
|
+
|
|
165
262
|
* { box-sizing: border-box; }
|
|
166
263
|
html, body { margin: 0; height: 100%; width: 100%; max-width: 100vw; overflow: hidden;
|
|
167
264
|
background: var(--bg); color: var(--fg);
|
|
@@ -673,6 +770,8 @@ function buildHTML() {
|
|
|
673
770
|
<span class="spacer"></span>
|
|
674
771
|
<button id="btnSide" class="toggle on" onclick="toggleSide()">Sidebar</button>
|
|
675
772
|
<button id="btnPrev" class="toggle" onclick="togglePreview()">Preview</button>
|
|
773
|
+
<button id="btnStudio" class="toggle" title="Toggle Studio theme — a calmer, card-based layout" onclick="toggleStudio()">Studio</button>
|
|
774
|
+
<button id="btnRawTerm" class="toggle" title="Show the raw terminal instead of the chat view" onclick="toggleRawTerm()" style="display:none">Terminal</button>
|
|
676
775
|
<button onclick="sendCmd('/help')">/help</button>
|
|
677
776
|
<button onclick="sendCmd('/agents')">agents</button>
|
|
678
777
|
<button onclick="sendCmd('/model')">model</button>
|
|
@@ -769,6 +868,19 @@ function buildHTML() {
|
|
|
769
868
|
<input type="file" id="qaFile" multiple style="display:none">
|
|
770
869
|
</div>
|
|
771
870
|
<div id="term-wrap"></div>
|
|
871
|
+
<!-- Chat view (Studio mode) — reskins the live terminal stream as a chat transcript -->
|
|
872
|
+
<div id="chat">
|
|
873
|
+
<div id="chatLog"></div>
|
|
874
|
+
<div id="chatComposer">
|
|
875
|
+
<textarea id="chatBox" rows="1" placeholder="Message Sapper… (Enter to send, Shift+Enter for newline)"></textarea>
|
|
876
|
+
<div class="ccbar">
|
|
877
|
+
<span class="ccmeta" id="chatMeta">Sapper</span>
|
|
878
|
+
<span class="ccsp"></span>
|
|
879
|
+
<button class="ccslash" title="Slash commands" onclick="chatSlashMenu(event)">/</button>
|
|
880
|
+
<button class="ccsend" id="chatSendBtn" onclick="chatSend()" title="Send (Enter)">➤</button>
|
|
881
|
+
</div>
|
|
882
|
+
</div>
|
|
883
|
+
</div>
|
|
772
884
|
<div id="dropOverlay">
|
|
773
885
|
<div class="drop-card">
|
|
774
886
|
<div class="drop-icon">📥</div>
|
|
@@ -1195,6 +1307,7 @@ function connectPty() {
|
|
|
1195
1307
|
} catch(e){}
|
|
1196
1308
|
} else {
|
|
1197
1309
|
term.write(new Uint8Array(ev.data));
|
|
1310
|
+
chatFeed(new Uint8Array(ev.data));
|
|
1198
1311
|
}
|
|
1199
1312
|
};
|
|
1200
1313
|
ws.onclose = function() {
|
|
@@ -1219,6 +1332,150 @@ window.sendCmd = function(cmd) { if (ws && ws.readyState === 1) ws.send(cmd + '\
|
|
|
1219
1332
|
window.restartSapper = function() { if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type:'restart' })); };
|
|
1220
1333
|
document.getElementById('term-wrap').addEventListener('click', function(){ term.focus(); });
|
|
1221
1334
|
|
|
1335
|
+
// ─── Chat view: reskin the live terminal stream as a chat transcript ──────────
|
|
1336
|
+
var chatRaw = ''; // ANSI-stripped rolling buffer of all pty output
|
|
1337
|
+
var chatDecoder = (typeof TextDecoder !== 'undefined') ? new TextDecoder('utf-8', { fatal:false }) : null;
|
|
1338
|
+
var CHAT_PROMPT = '\\u203a'; // '›' — Sapper's readline prompt glyph
|
|
1339
|
+
var chatRenderTimer = null;
|
|
1340
|
+
var CHAT_MAX_RAW = 400000; // cap buffer so long sessions stay snappy
|
|
1341
|
+
|
|
1342
|
+
function chatStripAnsi(s) {
|
|
1343
|
+
// CSI / SGR sequences
|
|
1344
|
+
s = s.replace(/\\u001b\\[[0-9;?]*[ -\\/]*[@-~]/g, '');
|
|
1345
|
+
// OSC sequences (window title etc.)
|
|
1346
|
+
s = s.replace(/\\u001b\\][^\\u0007\\u001b]*(?:\\u0007|\\u001b\\\\)/g, '');
|
|
1347
|
+
// misc single-char escapes
|
|
1348
|
+
s = s.replace(/\\u001b[=>NOM78()][AB0-2]?/g, '');
|
|
1349
|
+
s = s.replace(/[\\u0000\\u0007]/g, '');
|
|
1350
|
+
return s;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function chatCollapseCR(text) {
|
|
1354
|
+
// Resolve carriage-return overwrites (spinners) line by line: last write wins.
|
|
1355
|
+
return text.split('\\n').map(function(line){
|
|
1356
|
+
if (line.indexOf('\\r') === -1) return line;
|
|
1357
|
+
var parts = line.split('\\r');
|
|
1358
|
+
var out = '';
|
|
1359
|
+
for (var i = 0; i < parts.length; i++) {
|
|
1360
|
+
var p = parts[i];
|
|
1361
|
+
// overwrite from column 0: keep the longer tail
|
|
1362
|
+
out = p.length >= out.length ? p : p + out.slice(p.length);
|
|
1363
|
+
}
|
|
1364
|
+
return out;
|
|
1365
|
+
}).join('\\n');
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function chatFeed(bytes) {
|
|
1369
|
+
if (!chatDecoder) return;
|
|
1370
|
+
var chunk = chatDecoder.decode(bytes, { stream: true });
|
|
1371
|
+
chatRaw += chatStripAnsi(chunk);
|
|
1372
|
+
if (chatRaw.length > CHAT_MAX_RAW) chatRaw = chatRaw.slice(-CHAT_MAX_RAW);
|
|
1373
|
+
if (chatRenderTimer) return;
|
|
1374
|
+
chatRenderTimer = setTimeout(function(){ chatRenderTimer = null; chatRender(); }, 90);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function chatParseTurns(raw) {
|
|
1378
|
+
var text = chatCollapseCR(raw);
|
|
1379
|
+
var lines = text.split('\\n');
|
|
1380
|
+
var turns = [];
|
|
1381
|
+
var cur = null; // { role, lines: [] }
|
|
1382
|
+
function push(role){ cur = { role: role, lines: [] }; turns.push(cur); }
|
|
1383
|
+
push('system');
|
|
1384
|
+
for (var i = 0; i < lines.length; i++) {
|
|
1385
|
+
var ln = lines[i];
|
|
1386
|
+
var stripped = ln.replace(/^\\s+/, '');
|
|
1387
|
+
if (stripped.charAt(0) === '\\u203a') {
|
|
1388
|
+
// prompt line → the user's submitted input is the rest of the line
|
|
1389
|
+
var userText = stripped.replace(/^\\u203a\\s*/, '');
|
|
1390
|
+
push('user'); cur.lines.push(userText);
|
|
1391
|
+
push('ai'); // assistant output follows until the next prompt
|
|
1392
|
+
} else {
|
|
1393
|
+
cur.lines.push(ln);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
// trim + drop empties
|
|
1397
|
+
return turns.map(function(t){
|
|
1398
|
+
return { role: t.role, text: t.lines.join('\\n').replace(/\\s+$/,'').replace(/^\\n+/,'') };
|
|
1399
|
+
}).filter(function(t){ return t.text.trim().length > 0; });
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function chatRender() {
|
|
1403
|
+
var log = document.getElementById('chatLog');
|
|
1404
|
+
if (!log) return;
|
|
1405
|
+
var atBottom = (log.scrollHeight - log.scrollTop - log.clientHeight) < 60;
|
|
1406
|
+
var turns = chatParseTurns(chatRaw);
|
|
1407
|
+
var html = '';
|
|
1408
|
+
for (var i = 0; i < turns.length; i++) {
|
|
1409
|
+
var t = turns[i];
|
|
1410
|
+
var who = t.role === 'user' ? 'You' : (t.role === 'system' ? 'Session' : 'Sapper');
|
|
1411
|
+
var av = t.role === 'user' ? '👤' : (t.role === 'system' ? '⚡' : '🤖');
|
|
1412
|
+
var bubbleCls = t.role === 'system' ? 'bubble system' : 'bubble';
|
|
1413
|
+
html += '<div class="cmsg ' + t.role + '">' +
|
|
1414
|
+
'<div class="avatar">' + av + '</div>' +
|
|
1415
|
+
'<div class="body"><div class="who">' + who + '</div>' +
|
|
1416
|
+
'<div class="' + bubbleCls + '">' + esc(t.text) + '</div></div></div>';
|
|
1417
|
+
}
|
|
1418
|
+
log.innerHTML = html || '<div class="cmsg ai"><div class="avatar">🤖</div>' +
|
|
1419
|
+
'<div class="body"><div class="who">Sapper</div><div class="bubble system">Waiting for Sapper…</div></div></div>';
|
|
1420
|
+
if (atBottom) log.scrollTop = log.scrollHeight;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
window.chatSend = function() {
|
|
1424
|
+
var box = document.getElementById('chatBox');
|
|
1425
|
+
if (!box) return;
|
|
1426
|
+
var t = box.value;
|
|
1427
|
+
if (!t.trim()) return;
|
|
1428
|
+
if (ws && ws.readyState === 1) ws.send(t.replace(/\\n/g, ' ') + '\\r');
|
|
1429
|
+
box.value = '';
|
|
1430
|
+
chatAutoGrow();
|
|
1431
|
+
};
|
|
1432
|
+
|
|
1433
|
+
// Forward a raw key sequence straight to the live process (for interactive prompts)
|
|
1434
|
+
function chatSendKey(seq) {
|
|
1435
|
+
if (ws && ws.readyState === 1) ws.send(seq);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
window.chatSlashMenu = function(ev) {
|
|
1439
|
+
if (ev) ev.preventDefault();
|
|
1440
|
+
var box = document.getElementById('chatBox');
|
|
1441
|
+
if (box) { box.value = (box.value ? box.value + ' ' : '') + '/'; box.focus(); chatAutoGrow(); }
|
|
1442
|
+
};
|
|
1443
|
+
|
|
1444
|
+
function chatAutoGrow() {
|
|
1445
|
+
var box = document.getElementById('chatBox');
|
|
1446
|
+
if (!box) return;
|
|
1447
|
+
box.style.height = 'auto';
|
|
1448
|
+
box.style.height = Math.min(box.scrollHeight, 200) + 'px';
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
(function wireChatInput(){
|
|
1452
|
+
var box = document.getElementById('chatBox');
|
|
1453
|
+
if (!box) return;
|
|
1454
|
+
box.addEventListener('input', chatAutoGrow);
|
|
1455
|
+
box.addEventListener('keydown', function(ev){
|
|
1456
|
+
var empty = !box.value.trim();
|
|
1457
|
+
// Forward navigation keys to the live process so interactive prompts
|
|
1458
|
+
// (model picker, change-review, etc.) can be driven from the chat box.
|
|
1459
|
+
if (ev.key === 'ArrowUp') { ev.preventDefault(); chatSendKey('\\u001b[A'); return; }
|
|
1460
|
+
if (ev.key === 'ArrowDown') { ev.preventDefault(); chatSendKey('\\u001b[B'); return; }
|
|
1461
|
+
if (ev.key === 'Tab') { ev.preventDefault(); chatSendKey('\\t'); return; }
|
|
1462
|
+
if (ev.key === 'Escape') { ev.preventDefault(); chatSendKey('\\u001b'); return; }
|
|
1463
|
+
if (ev.key === 'Enter' && !ev.shiftKey) {
|
|
1464
|
+
ev.preventDefault();
|
|
1465
|
+
if (empty) chatSendKey('\\r'); // confirm current selection / send blank line
|
|
1466
|
+
else chatSend();
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
})();
|
|
1470
|
+
|
|
1471
|
+
window.toggleRawTerm = function(force) {
|
|
1472
|
+
var on = typeof force === 'boolean' ? force : !document.body.classList.contains('rawterm');
|
|
1473
|
+
document.body.classList.toggle('rawterm', on);
|
|
1474
|
+
var btn = document.getElementById('btnRawTerm');
|
|
1475
|
+
if (btn) btn.classList.toggle('on', on);
|
|
1476
|
+
setTimeout(function(){ try { fit.fit(); } catch(e){} if (on) term.focus(); else chatRender(); }, 60);
|
|
1477
|
+
};
|
|
1478
|
+
|
|
1222
1479
|
// ─── FS events WS ────────────────────────────────────────────
|
|
1223
1480
|
function connectEvents() {
|
|
1224
1481
|
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
@@ -1520,6 +1777,18 @@ window.togglePreview = function() {
|
|
|
1520
1777
|
setTimeout(doFit, 50);
|
|
1521
1778
|
if (cm && !p.classList.contains('hidden')) setTimeout(function(){ cm.refresh(); }, 80);
|
|
1522
1779
|
};
|
|
1780
|
+
window.toggleStudio = function(force) {
|
|
1781
|
+
var on = typeof force === 'boolean' ? force : !document.body.classList.contains('studio');
|
|
1782
|
+
document.body.classList.toggle('studio', on);
|
|
1783
|
+
var btn = document.getElementById('btnStudio');
|
|
1784
|
+
if (btn) btn.classList.toggle('on', on);
|
|
1785
|
+
var rawBtn = document.getElementById('btnRawTerm');
|
|
1786
|
+
if (rawBtn) rawBtn.style.display = on ? '' : 'none';
|
|
1787
|
+
if (!on) document.body.classList.remove('rawterm');
|
|
1788
|
+
try { localStorage.setItem('sapperStudio', on ? '1' : '0'); } catch(e) {}
|
|
1789
|
+
if (on) chatRender();
|
|
1790
|
+
setTimeout(function(){ try { fit.fit(); } catch(e){} if (cm) try { cm.refresh(); } catch(e){} }, 60);
|
|
1791
|
+
};
|
|
1523
1792
|
|
|
1524
1793
|
// ─── File tree ───────────────────────────────────────────────
|
|
1525
1794
|
function fileIcon(name, isDir) {
|
|
@@ -2780,6 +3049,7 @@ window.sendOpenPrompt = async function() {
|
|
|
2780
3049
|
};
|
|
2781
3050
|
|
|
2782
3051
|
// ─── Boot ────────────────────────────────────────────────────
|
|
3052
|
+
try { if (localStorage.getItem('sapperStudio') === '1') toggleStudio(true); } catch(e) {}
|
|
2783
3053
|
connectPty();
|
|
2784
3054
|
connectEvents();
|
|
2785
3055
|
loadTree();
|