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 +1 -1
- package/sapper-ui.mjs +89 -7
- package/sapper.mjs +2 -2
package/package.json
CHANGED
package/sapper-ui.mjs
CHANGED
|
@@ -2752,10 +2752,17 @@ function setEditorMode(path) {
|
|
|
2752
2752
|
|
|
2753
2753
|
// ─── Config tab ──────────────────────────────────────────────
|
|
2754
2754
|
window.reloadConfig = function() {
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
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, '"') + '">!</span>'; };
|
|
2780
|
-
|
|
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
|
-
|
|
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: '
|
|
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: '
|
|
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.
|