sapper-iq 1.4.3 → 1.4.5

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/sapper-ui.mjs +279 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapper-iq",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
4
4
  "description": "AI-powered development assistant that executes commands and builds projects",
5
5
  "main": "sapper.mjs",
6
6
  "bin": {
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)">&#10148;</button>
881
+ </div>
882
+ </div>
883
+ </div>
772
884
  <div id="dropOverlay">
773
885
  <div class="drop-card">
774
886
  <div class="drop-icon">&#128229;</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' ? '&#128100;' : (t.role === 'system' ? '&#9889;' : '&#129302;');
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">&#129302;</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();
@@ -2866,21 +3136,22 @@ function listEntries(dirPath) {
2866
3136
  }
2867
3137
 
2868
3138
  function looksBinary(buf) {
2869
- const len = Math.min(buf.length, 4096);
3139
+ const len = buf.length;
2870
3140
  // Null byte is a definitive binary indicator
2871
3141
  for (let i = 0; i < len; i++) {
2872
3142
  if (buf[i] === 0) return true;
2873
3143
  }
2874
- // Try decoding as UTF-8; replacement char U+FFFD signals invalid sequences (binary)
2875
- const sample = buf.slice(0, len).toString('utf8');
2876
- if (sample.includes('\uFFFD')) return true;
3144
+ // Decode the FULL buffer as UTF-8 (never a slice slicing can cut a
3145
+ // multibyte char and inject a false U+FFFD). Files are capped at 2MB.
3146
+ const text = buf.toString('utf8');
3147
+ if (text.includes('\uFFFD')) return true;
2877
3148
  // Count true non-printable control chars (bytes < 32 excluding tab/LF/CR)
2878
3149
  let nonText = 0;
2879
- for (let i = 0; i < sample.length; i++) {
2880
- const c = sample.charCodeAt(i);
3150
+ for (let i = 0; i < text.length; i++) {
3151
+ const c = text.charCodeAt(i);
2881
3152
  if (c < 32 && c !== 9 && c !== 10 && c !== 13) nonText++;
2882
3153
  }
2883
- return nonText / Math.max(sample.length, 1) > 0.1;
3154
+ return nonText / Math.max(text.length, 1) > 0.1;
2884
3155
  }
2885
3156
 
2886
3157
  const server = http.createServer(async (req, res) => {