sapper-iq 1.4.8 → 1.4.10

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapper-iq",
3
- "version": "1.4.8",
3
+ "version": "1.4.10",
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
@@ -2752,10 +2752,17 @@ function setEditorMode(path) {
2752
2752
 
2753
2753
  // ─── Config tab ──────────────────────────────────────────────
2754
2754
  window.reloadConfig = function() {
2755
- fetch('/api/config').then(function(r){return r.json();}).then(function(d){
2756
- document.getElementById('cfgJson').value = JSON.stringify(d.config || {}, null, 2);
2757
- renderQuickConfig(d.config || {});
2758
- }).catch(function(e){ showToast('Config read failed: ' + e.message, 'err'); });
2755
+ // Fetch local Ollama models first so the model dropdowns can populate, then load config.
2756
+ var modelsPromise = fetch('/api/models')
2757
+ .then(function(r){ return r.json(); })
2758
+ .then(function(d){ window._sapperModels = (d && d.models) || []; })
2759
+ .catch(function(){ window._sapperModels = window._sapperModels || []; });
2760
+ modelsPromise.then(function(){
2761
+ fetch('/api/config').then(function(r){return r.json();}).then(function(d){
2762
+ document.getElementById('cfgJson').value = JSON.stringify(d.config || {}, null, 2);
2763
+ renderQuickConfig(d.config || {});
2764
+ }).catch(function(e){ showToast('Config read failed: ' + e.message, 'err'); });
2765
+ });
2759
2766
  };
2760
2767
  window.saveConfig = function() {
2761
2768
  var raw = document.getElementById('cfgJson').value;
@@ -2777,15 +2784,36 @@ function renderQuickConfig(cfg) {
2777
2784
  host.innerHTML = '';
2778
2785
  function add(html) { host.insertAdjacentHTML('beforeend', html); }
2779
2786
  var T = function(tip, below) { return '<span class="tip' + (below ? ' below' : '') + '" data-tip="' + tip.replace(/"/g, '&quot;') + '">!</span>'; };
2780
- add('<label>Default model ' + T('The AI model used when no model is specified at startup. E.g. gpt-4o, claude-3-5-sonnet, qwen3.5. Leave blank to be prompted on launch.') + '</label><input type="text" id="qDefMod" placeholder="auto" value="' + esc(cfg.defaultModel || '') + '">');
2787
+
2788
+ // Build a <select> populated from local Ollama models, with a free-text fallback
2789
+ // for models that are not (yet) installed locally.
2790
+ function modelSelect(id, currentVal, allowEmpty) {
2791
+ var opts = '';
2792
+ if (allowEmpty) opts += '<option value="">(auto / prompt at launch)</option>';
2793
+ var seen = false;
2794
+ (window._sapperModels || []).forEach(function(m){
2795
+ var sel = (m.name === currentVal) ? ' selected' : '';
2796
+ if (m.name === currentVal) seen = true;
2797
+ opts += '<option value="' + esc(m.name) + '"' + sel + '>' + esc(m.name) + (m.size ? ' \u2014 ' + esc(m.size) : '') + '</option>';
2798
+ });
2799
+ if (currentVal && !seen) opts += '<option value="' + esc(currentVal) + '" selected>' + esc(currentVal) + ' (not installed locally)</option>';
2800
+ return '<select id="' + id + '">' + opts + '</select>';
2801
+ }
2802
+
2803
+ var cConsult = (cfg.consultant && cfg.consultant.model) || '';
2804
+ var cThink = (cfg.deepthink && cfg.deepthink.model) || '';
2805
+
2806
+ add('<label>Default model ' + T('The AI model used when no model is specified at startup. Pick one from your local Ollama models.') + '</label>' + modelSelect('qDefMod', cfg.defaultModel || '', true));
2781
2807
  add('<label>Default agent ' + T('Agent role (.sapper/agents/*.md) to activate automatically when Sapper starts. Leave blank for the default general-purpose assistant.') + '</label><input type="text" id="qDefAgent" placeholder="(none)" value="' + esc(cfg.defaultAgent || '') + '">');
2808
+ add('<label>Consultant model ' + T('Heavyweight model used by the consult_expert tool. Pick something stronger than your main model \u2014 the consultant is supposed to be a deeper reasoner.') + '</label>' + modelSelect('qConsultMod', cConsult, false));
2809
+ add('<label>Deep-think model ' + T('Model used by the deep_think tool for multi-turn back-and-forth reasoning. Pick a strong reasoning model.') + '</label>' + modelSelect('qThinkMod', cThink, false));
2782
2810
  add('<label>Context limit ' + T('Hard cap on tokens sent to the model per request. Leave blank to use the model\\'s full context window. Useful to reduce cost or avoid slow responses.') + '</label><input type="number" id="qCtxLim" value="' + esc(cfg.contextLimit == null ? '' : cfg.contextLimit) + '">');
2783
2811
  add('<label>Tool round limit ' + T('Maximum number of tool calls (file reads, shell commands, patches…) Sapper may make in a single response turn. Default is 40. Lower to prevent runaway loops.') + '</label><input type="number" id="qToolRnd" value="' + esc(cfg.toolRoundLimit != null ? cfg.toolRoundLimit : 40) + '">');
2784
2812
  add('<div class="toggle-row"><span>Summary phases ' + T('When ON, Sapper displays a step-by-step progress bar while it compresses long conversations. Turn OFF for a quieter experience.') + '</span><div class="switch ' + (cfg.summaryPhases ? 'on' : '') + '" id="qSumPh"></div></div>');
2785
2813
  add('<label>Summary trigger % ' + T('When the conversation reaches this percentage of the context window, Sapper automatically summarizes older messages to keep the window from overflowing. Default 65%.') + '</label><input type="number" id="qSumTr" value="' + esc(cfg.summarizeTriggerPercent != null ? cfg.summarizeTriggerPercent : 65) + '">');
2786
2814
  add('<div class="toggle-row"><span>Debug mode ' + T('Enables verbose output — shows raw tool call details, API request sizes, and internal errors. Useful for troubleshooting but noisy during normal use.') + '</span><div class="switch ' + (cfg.debug ? 'on' : '') + '" id="qDebug"></div></div>');
2787
2815
  add('<div class="toggle-row"><span>Auto-attach files ' + T('When ON, files you open in the sidebar are automatically referenced in the AI context so Sapper knows what you are looking at without you typing @filename.') + '</span><div class="switch ' + (cfg.autoAttach !== false ? 'on' : '') + '" id="qAutoAtt"></div></div>');
2788
- add('<div class="row-btns"><button class="primary" onclick="saveQuickConfig()">Apply quick changes</button></div>');
2816
+ add('<div class="row-btns"><button onclick="refreshModelList()" title="Re-fetch local Ollama models">\u21bb Refresh models</button><button class="primary" onclick="saveQuickConfig()">Apply quick changes</button></div>');
2789
2817
 
2790
2818
  function bindSwitch(id) {
2791
2819
  var el = document.getElementById(id);
@@ -2794,6 +2822,18 @@ function renderQuickConfig(cfg) {
2794
2822
  bindSwitch('qSumPh'); bindSwitch('qDebug'); bindSwitch('qAutoAtt');
2795
2823
  }
2796
2824
 
2825
+ window.refreshModelList = function() {
2826
+ fetch('/api/models').then(function(r){return r.json();}).then(function(d){
2827
+ window._sapperModels = (d && d.models) || [];
2828
+ // Re-render with the current config so the dropdowns repopulate
2829
+ try {
2830
+ var cfg = JSON.parse(document.getElementById('cfgJson').value || '{}');
2831
+ renderQuickConfig(cfg);
2832
+ showToast('Loaded ' + window._sapperModels.length + ' local model' + (window._sapperModels.length === 1 ? '' : 's'));
2833
+ } catch (e) {}
2834
+ }).catch(function(e){ showToast('Could not list models: ' + e.message, 'err'); });
2835
+ };
2836
+
2797
2837
  window.saveQuickConfig = function() {
2798
2838
  var current;
2799
2839
  try { current = JSON.parse(document.getElementById('cfgJson').value || '{}'); }
@@ -2803,6 +2843,10 @@ window.saveQuickConfig = function() {
2803
2843
  var v;
2804
2844
  v = val('qDefMod').trim(); current.defaultModel = v || null;
2805
2845
  v = val('qDefAgent').trim(); current.defaultAgent = v || null;
2846
+ v = val('qConsultMod').trim();
2847
+ if (v) { current.consultant = Object.assign({}, current.consultant || {}, { model: v }); }
2848
+ v = val('qThinkMod').trim();
2849
+ if (v) { current.deepthink = Object.assign({}, current.deepthink || {}, { model: v }); }
2806
2850
  v = val('qCtxLim').trim(); current.contextLimit = v === '' ? null : parseInt(v, 10);
2807
2851
  v = val('qToolRnd').trim(); current.toolRoundLimit = v === '' ? 40 : parseInt(v, 10);
2808
2852
  v = val('qSumTr').trim(); current.summarizeTriggerPercent = v === '' ? 65 : parseInt(v, 10);
@@ -3393,7 +3437,23 @@ const server = http.createServer(async (req, res) => {
3393
3437
  const body = await readReqJSON(req);
3394
3438
  try {
3395
3439
  ensureDir(SAPPER_DIR);
3396
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(body.config || {}, null, 2));
3440
+ // Deep-merge incoming over existing on-disk config so that fields the
3441
+ // Web UI doesn't know about (e.g. deepthink, consultant.verbose) are
3442
+ // preserved instead of being silently wiped by a stale textarea save.
3443
+ const existing = readJSON(CONFIG_FILE, {}) || {};
3444
+ const incoming = body.config || {};
3445
+ const isPlainObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
3446
+ const deepMerge = (base, over) => {
3447
+ if (!isPlainObj(base)) return over;
3448
+ if (!isPlainObj(over)) return over;
3449
+ const out = { ...base };
3450
+ for (const k of Object.keys(over)) {
3451
+ out[k] = isPlainObj(base[k]) && isPlainObj(over[k]) ? deepMerge(base[k], over[k]) : over[k];
3452
+ }
3453
+ return out;
3454
+ };
3455
+ const merged = deepMerge(existing, incoming);
3456
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2));
3397
3457
  return json(res, { ok: true });
3398
3458
  } catch (e) { return json(res, { error: e.message }, 500); }
3399
3459
  }
@@ -3402,6 +3462,28 @@ const server = http.createServer(async (req, res) => {
3402
3462
  if (req.method === 'GET' && path === '/api/agents') return json(res, { agents: listMdDir(AGENTS_DIR) });
3403
3463
  if (req.method === 'GET' && path === '/api/skills') return json(res, { skills: listMdDir(SKILLS_DIR) });
3404
3464
 
3465
+ // ── Local Ollama models (for model-picker dropdowns in the UI)
3466
+ if (req.method === 'GET' && path === '/api/models') {
3467
+ try {
3468
+ const out = await new Promise((resolve) => {
3469
+ let buf = '';
3470
+ const p = spawn('ollama', ['list']);
3471
+ const t = setTimeout(() => { try { p.kill(); } catch {} resolve(''); }, 4000);
3472
+ p.stdout.on('data', (d) => { buf += d.toString(); });
3473
+ p.on('error', () => { clearTimeout(t); resolve(''); });
3474
+ p.on('close', () => { clearTimeout(t); resolve(buf); });
3475
+ });
3476
+ const models = out.split('\n').slice(1) // drop header
3477
+ .map(line => line.trim()).filter(Boolean)
3478
+ .map(line => {
3479
+ const parts = line.split(/\s+/);
3480
+ return { name: parts[0], size: parts.slice(1, 3).join(' '), modified: parts.slice(3).join(' ') };
3481
+ })
3482
+ .filter(m => m.name && !m.name.startsWith('NAME'));
3483
+ return json(res, { models });
3484
+ } catch (e) { return json(res, { error: e.message, models: [] }, 500); }
3485
+ }
3486
+
3405
3487
  res.writeHead(404, { 'Content-Type': 'text/plain' });
3406
3488
  res.end('Not found');
3407
3489
  } catch (e) {
package/sapper.mjs CHANGED
@@ -1164,7 +1164,7 @@ const DEFAULT_CONFIG = Object.freeze({
1164
1164
  // every invocation requires a written summary + a specific question + supporting files.
1165
1165
  // Use it when the agent is stuck or about to make an important / hard-to-reverse change.
1166
1166
  enabled: true,
1167
- model: 'juilpark/Qwen3.5-27B-Claude-4.6-Opus-Reasoning-Distilled-heretic:q4_k_m', // Default consultant model — change to any model you have via `ollama list`.
1167
+ model: 'qwen3.5:9b-q8_0', // Default consultant model — change to any model you have via `ollama list`.
1168
1168
  contextLimit: null, // Tokens to give the consultant (null = use model default).
1169
1169
  temperature: 0.2, // Lower = more deliberate.
1170
1170
  thinking: 'on', // 'on' | 'off' | 'auto' — reasoning models should stay on.
@@ -1189,7 +1189,7 @@ const DEFAULT_CONFIG = Object.freeze({
1189
1189
  // main agent can have a real back-and-forth: ask, get a thoughtful answer, ask follow-up,
1190
1190
  // refine, etc. Use when the main agent hits something unexpected or needs deep reasoning.
1191
1191
  enabled: true,
1192
- model: 'juilpark/Qwen3.5-27B-Claude-4.6-Opus-Reasoning-Distilled-heretic:q4_k_m', // Pick a strong reasoning model.
1192
+ model: 'qwen3.5:9b-q8_0', // Pick a strong reasoning model.
1193
1193
  contextLimit: null, // Tokens to give the thinker (null = use model default).
1194
1194
  temperature: 0.7, // Higher than consultant — reasoning benefits from a little exploration.
1195
1195
  thinking: 'on', // 'on' | 'off' | 'auto' — should usually stay on for reasoning models.