sapper-iq 1.2.4 → 1.3.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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/sapper-ui.mjs +186 -0
  3. package/sapper.mjs +24 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapper-iq",
3
- "version": "1.2.4",
3
+ "version": "1.3.0",
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
@@ -285,6 +285,47 @@ function buildHTML() {
285
285
  font-style: italic; font-size: 11px; white-space: pre-wrap; word-break: break-word; }
286
286
  #activityPanel .note:before { content: '💬 '; margin-right: 2px; font-style: normal; }
287
287
  #activityPanel .empty { padding: 12px; color: var(--dim); text-align: center; font-size: 11px; }
288
+
289
+ /* Index tray — multi-select files/folders to send into chat */
290
+ #indexPanel { display: none; border-bottom: 1px solid var(--border);
291
+ background: linear-gradient(180deg, rgba(88,166,255,.08), rgba(88,166,255,.02));
292
+ font-family: ui-monospace, 'SF Mono', monospace; font-size: 11px; }
293
+ #indexPanel.on { display: block; }
294
+ #indexPanel .ih { display: flex; align-items: center; gap: 6px; padding: 5px 10px;
295
+ border-bottom: 1px solid var(--border); color: var(--accent); font-size: 10px;
296
+ text-transform: uppercase; letter-spacing: .5px; }
297
+ #indexPanel .ih .icnt { color: var(--muted); text-transform: none; letter-spacing: 0; }
298
+ #indexPanel .ih .iact { margin-left: auto; display: inline-flex; gap: 4px; }
299
+ #indexPanel .ih .iact button { background: transparent; color: var(--accent);
300
+ border: 1px solid var(--border2); border-radius: 3px; padding: 1px 7px; font-size: 10px;
301
+ cursor: pointer; font-family: inherit; line-height: 1.3; }
302
+ #indexPanel .ih .iact button:hover { border-color: var(--accent); }
303
+ #indexPanel .ih .iact button.primary { color: #fff; background: var(--accent); border-color: var(--accent); }
304
+ #indexPanel .ih .iact button.primary:hover { background: var(--accent2); border-color: var(--accent2); }
305
+ #indexPanel .ih .iact button.danger { color: var(--muted); }
306
+ #indexPanel .ih .iact button.danger:hover { color: var(--red); border-color: var(--red); }
307
+ #indexPanel .chips { display: flex; flex-wrap: wrap; gap: 4px; padding: 6px 10px 4px;
308
+ max-height: 110px; overflow-y: auto; }
309
+ #indexPanel .chip { display: inline-flex; align-items: center; gap: 4px;
310
+ background: rgba(88,166,255,.12); border: 1px solid rgba(88,166,255,.3);
311
+ border-radius: 10px; padding: 1px 4px 1px 8px; font-size: 10px; color: var(--fg); }
312
+ #indexPanel .chip.dir { background: rgba(210,153,34,.12); border-color: rgba(210,153,34,.3); }
313
+ #indexPanel .chip .cp { max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
314
+ #indexPanel .chip .cx { cursor: pointer; opacity: .55; padding: 0 3px; font-size: 12px; }
315
+ #indexPanel .chip .cx:hover { opacity: 1; color: var(--red); }
316
+ #indexPanel .empty { padding: 8px 10px; color: var(--dim); font-style: italic; }
317
+ #indexPanel .icmt { display: block; margin: 4px 10px 8px; width: calc(100% - 20px);
318
+ box-sizing: border-box; background: var(--panel); color: var(--fg);
319
+ border: 1px solid var(--border2); border-radius: 4px; padding: 4px 6px;
320
+ font-size: 11px; font-family: inherit; resize: vertical; min-height: 26px; max-height: 80px; }
321
+ #indexPanel .icmt:focus { outline: none; border-color: var(--accent); }
322
+ /* Per-row index checkbox (visible only when index mode is on) */
323
+ .row .chk { display: none; width: 12px; flex-shrink: 0; color: var(--dim);
324
+ text-align: center; font-size: 11px; line-height: 1; }
325
+ body.indexmode .row .chk { display: inline-block; cursor: pointer; }
326
+ body.indexmode .row .chk:hover { color: var(--accent); }
327
+ .row .chk.on { color: var(--accent); }
328
+ .ftb.on { color: var(--accent); }
288
329
  .tree { font-family: ui-monospace, 'SF Mono', monospace; font-size: 12px; padding-bottom: 12px; }
289
330
  .row { display: flex; align-items: center; gap: 4px; padding: 3px 8px; cursor: pointer; color: var(--muted);
290
331
  white-space: nowrap; user-select: none; position: relative; }
@@ -592,6 +633,7 @@ function buildHTML() {
592
633
  <button class="ftb" title="New file" onclick="newItemPrompt('file','')">&#128462;<sup>+</sup></button>
593
634
  <button class="ftb" title="New folder" onclick="newItemPrompt('folder','')">&#128193;<sup>+</sup></button>
594
635
  <button class="ftb" id="ftbAct" title="Show activity log" onclick="toggleActivity()">&#9737;</button>
636
+ <button class="ftb" id="ftbIdx" title="Index files/folders into chat (multi-select)" onclick="toggleIndexMode()">&#128218;</button>
595
637
  <span class="ftb-spacer"></span>
596
638
  <button class="ftb" title="Clear change marks" onclick="clearAllMarks()">&#10005;</button>
597
639
  <button class="ftb" title="Refresh tree" onclick="loadTree()">&#8634;</button>
@@ -601,6 +643,17 @@ function buildHTML() {
601
643
  <div class="ah">Recent activity<span class="acl" onclick="clearActivity()">clear</span></div>
602
644
  <div id="activityList"></div>
603
645
  </div>
646
+ <div id="indexPanel">
647
+ <div class="ih">
648
+ <span>Index</span><span class="icnt" id="idxCount">0 items</span>
649
+ <span class="iact">
650
+ <button class="primary" title="Send to chat (Enter sends if a prompt is filled, otherwise files are staged at the cursor)" onclick="sendIndexToChat()">Send</button>
651
+ <button class="danger" title="Clear all" onclick="clearIndex()">Clear</button>
652
+ </span>
653
+ </div>
654
+ <div class="chips" id="idxChips"></div>
655
+ <textarea class="icmt" id="idxComment" placeholder="Optional prompt — fill this to send immediately. Empty = stage at cursor so you can keep typing."></textarea>
656
+ </div>
604
657
  <div class="tree" id="tree"></div>
605
658
  </div>
606
659
  <div class="pane" id="pane-config">
@@ -721,6 +774,8 @@ var state = {
721
774
  marks: {}, // path -> { kind, count, ts }
722
775
  activity: [], // ordered list of {kind, path, isDir, ts}
723
776
  activityOpen: false,
777
+ indexMode: false, // true = show checkboxes on tree rows
778
+ indexSet: {}, // path -> { isDir, ts } selected for "Index to chat"
724
779
  };
725
780
 
726
781
  var cm = null; // CodeMirror instance (lazy)
@@ -1172,7 +1227,10 @@ function renderEntries(container, basePath, entries, depth) {
1172
1227
  row.dataset.isdir = entry.isDir ? '1' : '0';
1173
1228
  row.style.paddingLeft = (8 + depth * 14) + 'px';
1174
1229
  var chev = entry.isDir ? (state.expanded[path] ? '&#9662;' : '&#9656;') : '';
1230
+ var chkOn = state.indexSet[path] ? ' on' : '';
1231
+ var chkChar = state.indexSet[path] ? '&#9745;' : '&#9744;'; // ☑ / ☐
1175
1232
  row.innerHTML =
1233
+ '<span class="chk' + chkOn + '" title="Add to index">' + chkChar + '</span>' +
1176
1234
  '<span class="chev">' + chev + '</span>' +
1177
1235
  '<span class="ico">' + fileIcon(entry.name, entry.isDir) + '</span>' +
1178
1236
  '<span class="name">' + esc(entry.name) + '</span>' +
@@ -1181,6 +1239,11 @@ function renderEntries(container, basePath, entries, depth) {
1181
1239
  '<span class="badge">&#9679;</span>' +
1182
1240
  '<span class="rmenu" title="Options">&#8943;</span>';
1183
1241
  row.addEventListener('click', function(ev){
1242
+ if (ev.target && ev.target.classList && ev.target.classList.contains('chk')) {
1243
+ ev.stopPropagation();
1244
+ toggleIndex(path, entry.isDir);
1245
+ return;
1246
+ }
1184
1247
  if (ev.target && ev.target.classList && ev.target.classList.contains('rmenu')) {
1185
1248
  ev.stopPropagation();
1186
1249
  openRowMenu(ev.target, path, entry.isDir);
@@ -1281,6 +1344,12 @@ function openRowMenu(anchor, path, isDir) {
1281
1344
  items.push({ label: 'Copy path', fn: function(){ copyText(path); showToast('Path copied'); } });
1282
1345
  items.push({ label: 'Copy name', fn: function(){ copyText(path.split('/').pop()); showToast('Name copied'); } });
1283
1346
  items.push({ sep: true });
1347
+ var inIdx = !!state.indexSet[path];
1348
+ items.push({
1349
+ label: (inIdx ? '&#128218; Remove from index' : '&#128218; Add to index'),
1350
+ fn: function(){ toggleIndex(path, isDir); if (!state.indexMode) toggleIndexMode(true); }
1351
+ });
1352
+ items.push({ sep: true });
1284
1353
  items.push({ label: 'Reveal in Finder', fn: function(){
1285
1354
  fetch('/api/fs/reveal', { method: 'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ path: path }) });
1286
1355
  }});
@@ -1748,6 +1817,123 @@ function sendPasteToTerm(text) {
1748
1817
  return true;
1749
1818
  }
1750
1819
 
1820
+ function sendRawToTerm(text) {
1821
+ if (!ws || ws.readyState !== 1) {
1822
+ showToast('Terminal not connected', 'err');
1823
+ return false;
1824
+ }
1825
+ ws.send(text);
1826
+ return true;
1827
+ }
1828
+
1829
+ // ─── Index tray: multi-select files/folders into the chat ────────
1830
+ try { state.indexSet = JSON.parse(localStorage.getItem('sapperIndex') || '{}') || {}; } catch(e) { state.indexSet = {}; }
1831
+ function saveIndex() {
1832
+ try { localStorage.setItem('sapperIndex', JSON.stringify(state.indexSet)); } catch(e) {}
1833
+ }
1834
+
1835
+ window.toggleIndexMode = function(forceOn) {
1836
+ state.indexMode = (forceOn === true) ? true : !state.indexMode;
1837
+ document.body.classList.toggle('indexmode', state.indexMode);
1838
+ var btn = document.getElementById('ftbIdx');
1839
+ if (btn) btn.classList.toggle('on', state.indexMode);
1840
+ var panel = document.getElementById('indexPanel');
1841
+ if (panel) panel.classList.toggle('on', state.indexMode);
1842
+ if (state.indexMode) renderIndex();
1843
+ };
1844
+
1845
+ window.toggleIndex = function(path, isDir) {
1846
+ if (state.indexSet[path]) {
1847
+ delete state.indexSet[path];
1848
+ } else {
1849
+ state.indexSet[path] = { isDir: !!isDir, ts: Date.now() };
1850
+ }
1851
+ saveIndex();
1852
+ // Update the row checkbox without full rerender
1853
+ var row = document.querySelector('.row[data-path="' + cssEscape(path) + '"]');
1854
+ if (row) {
1855
+ var chk = row.querySelector('.chk');
1856
+ if (chk) {
1857
+ var on = !!state.indexSet[path];
1858
+ chk.classList.toggle('on', on);
1859
+ chk.innerHTML = on ? '&#9745;' : '&#9744;';
1860
+ }
1861
+ }
1862
+ renderIndex();
1863
+ };
1864
+
1865
+ window.clearIndex = function() {
1866
+ state.indexSet = {};
1867
+ saveIndex();
1868
+ document.querySelectorAll('.row .chk.on').forEach(function(el){
1869
+ el.classList.remove('on'); el.innerHTML = '&#9744;';
1870
+ });
1871
+ renderIndex();
1872
+ showToast('Index cleared');
1873
+ };
1874
+
1875
+ function renderIndex() {
1876
+ var panel = document.getElementById('indexPanel');
1877
+ if (!panel) return;
1878
+ var chips = document.getElementById('idxChips');
1879
+ var count = document.getElementById('idxCount');
1880
+ var paths = Object.keys(state.indexSet).sort();
1881
+ if (count) count.textContent = paths.length + ' item' + (paths.length === 1 ? '' : 's');
1882
+ if (!chips) return;
1883
+ if (!paths.length) {
1884
+ chips.innerHTML = '<div class="empty">Tick files or folders in the tree, or right-click &gt; Add to index.</div>';
1885
+ return;
1886
+ }
1887
+ chips.innerHTML = paths.map(function(p){
1888
+ var info = state.indexSet[p];
1889
+ var cls = info.isDir ? 'chip dir' : 'chip';
1890
+ var ico = info.isDir ? '&#128193;' : '&#128462;';
1891
+ return '<span class="' + cls + '" title="' + esc(p) + '">' +
1892
+ '<span>' + ico + '</span>' +
1893
+ '<span class="cp">' + esc(p) + '</span>' +
1894
+ '<span class="cx" data-p="' + esc(p) + '" title="Remove">&times;</span>' +
1895
+ '</span>';
1896
+ }).join('');
1897
+ chips.querySelectorAll('.cx').forEach(function(el){
1898
+ el.addEventListener('click', function(ev){
1899
+ ev.stopPropagation();
1900
+ toggleIndex(el.getAttribute('data-p'), state.indexSet[el.getAttribute('data-p')] && state.indexSet[el.getAttribute('data-p')].isDir);
1901
+ });
1902
+ });
1903
+ }
1904
+
1905
+ window.sendIndexToChat = function() {
1906
+ var paths = Object.keys(state.indexSet);
1907
+ if (!paths.length) { showToast('Index is empty', 'err'); return; }
1908
+ if (!ws || ws.readyState !== 1) { showToast('Terminal not connected', 'err'); return; }
1909
+ var files = [], dirs = [];
1910
+ paths.forEach(function(p){
1911
+ if (state.indexSet[p] && state.indexSet[p].isDir) dirs.push(p); else files.push(p);
1912
+ });
1913
+ // 1) /scan each folder (each sent as its own command + Enter)
1914
+ dirs.forEach(function(d){ sendPasteToTerm('/scan ' + d); });
1915
+ // 2) Build attachments token for files
1916
+ var atTokens = files.map(function(f){ return '@' + f; }).join(' ');
1917
+ var comment = (document.getElementById('idxComment') || {}).value || '';
1918
+ comment = comment.trim();
1919
+ if (comment) {
1920
+ // Send a complete message that Sapper will execute immediately
1921
+ var msg = comment;
1922
+ if (atTokens) msg = comment + ' ' + atTokens;
1923
+ sendPasteToTerm(msg);
1924
+ } else if (atTokens) {
1925
+ // Stage at cursor — no Enter, so the user can type their question
1926
+ sendRawToTerm(atTokens + ' ');
1927
+ }
1928
+ showToast('Sent ' + files.length + ' file' + (files.length === 1 ? '' : 's') +
1929
+ (dirs.length ? ' and ' + dirs.length + ' folder' + (dirs.length === 1 ? '' : 's') : '') +
1930
+ ' to chat');
1931
+ // Clear comment, clear index, refocus terminal
1932
+ var cmt = document.getElementById('idxComment'); if (cmt) cmt.value = '';
1933
+ clearIndex();
1934
+ try { term.focus(); } catch(e) {}
1935
+ };
1936
+
1751
1937
  window.askAboutSelection = async function() {
1752
1938
  if (!state.currentFile) return;
1753
1939
  var sel = getCurrentSelection();
package/sapper.mjs CHANGED
@@ -8634,31 +8634,31 @@ async function runSapper() {
8634
8634
  spinner.start('Thinking...');
8635
8635
  const aiStartTime = Date.now();
8636
8636
  let response;
8637
- try {
8638
- // Build chat options pass native tools when supported
8639
- const chatOpts = { model: selectedModel, messages, stream: true };
8640
- if (effectiveContextLength()) {
8641
- chatOpts.options = { num_ctx: effectiveContextLength() };
8642
- }
8643
- // Thinking can be forced on, forced off, or auto-disabled for simple prompts.
8644
- if (turnThinkingEnabled) chatOpts.think = true;
8645
- if (useNativeTools) {
8646
- // Filter tool defs by agent restrictions if any
8647
- if (currentAgentTools) {
8648
- const toolNameMap = {
8649
- list_directory: 'LIST', read_file: 'READ', search_files: 'SEARCH',
8650
- write_file: 'WRITE', patch_file: 'PATCH', create_directory: 'MKDIR',
8651
- ls: 'LS', cat: 'CAT', head: 'HEAD', tail: 'TAIL', grep: 'GREP', find: 'FIND',
8652
- pwd: 'PWD', cd: 'CD', rmdir: 'RMDIR', changes: 'CHANGES',
8653
- fetch_web: 'FETCH', recall_memory: 'MEMORY', open_url: 'OPEN', run_shell: 'SHELL'
8654
- };
8655
- chatOpts.tools = nativeToolDefs.filter(t =>
8656
- isToolAllowedForAgent(currentAgentTools, toolNameMap[t.function.name])
8657
- );
8658
- } else {
8659
- chatOpts.tools = nativeToolDefs;
8660
- }
8637
+ // Build chat options — pass native tools when supported
8638
+ const chatOpts = { model: selectedModel, messages, stream: true };
8639
+ if (effectiveContextLength()) {
8640
+ chatOpts.options = { num_ctx: effectiveContextLength() };
8641
+ }
8642
+ // Thinking can be forced on, forced off, or auto-disabled for simple prompts.
8643
+ if (turnThinkingEnabled) chatOpts.think = true;
8644
+ if (useNativeTools) {
8645
+ // Filter tool defs by agent restrictions if any
8646
+ if (currentAgentTools) {
8647
+ const toolNameMap = {
8648
+ list_directory: 'LIST', read_file: 'READ', search_files: 'SEARCH',
8649
+ write_file: 'WRITE', patch_file: 'PATCH', create_directory: 'MKDIR',
8650
+ ls: 'LS', cat: 'CAT', head: 'HEAD', tail: 'TAIL', grep: 'GREP', find: 'FIND',
8651
+ pwd: 'PWD', cd: 'CD', rmdir: 'RMDIR', changes: 'CHANGES',
8652
+ fetch_web: 'FETCH', recall_memory: 'MEMORY', open_url: 'OPEN', run_shell: 'SHELL'
8653
+ };
8654
+ chatOpts.tools = nativeToolDefs.filter(t =>
8655
+ isToolAllowedForAgent(currentAgentTools, toolNameMap[t.function.name])
8656
+ );
8657
+ } else {
8658
+ chatOpts.tools = nativeToolDefs;
8661
8659
  }
8660
+ }
8661
+ try {
8662
8662
  response = await ollama.chat(chatOpts);
8663
8663
  } catch (ollamaError) {
8664
8664
  const errMsg = ollamaError && ollamaError.message ? ollamaError.message : String(ollamaError);