sapper-iq 1.3.2 → 1.4.1
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 +444 -32
- package/sapper.mjs +141 -3
package/package.json
CHANGED
package/sapper-ui.mjs
CHANGED
|
@@ -313,6 +313,12 @@ function buildHTML() {
|
|
|
313
313
|
#indexPanel .chip .cp { max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
314
314
|
#indexPanel .chip .cx { cursor: pointer; opacity: .55; padding: 0 3px; font-size: 12px; }
|
|
315
315
|
#indexPanel .chip .cx:hover { opacity: 1; color: var(--red); }
|
|
316
|
+
#indexPanel .chip .cdeep { cursor: pointer; font-size: 9px; line-height: 1;
|
|
317
|
+
padding: 1px 5px; border-radius: 7px; border: 1px solid var(--border2);
|
|
318
|
+
background: rgba(255,255,255,.04); color: var(--muted); text-transform: uppercase;
|
|
319
|
+
letter-spacing: .04em; user-select: none; }
|
|
320
|
+
#indexPanel .chip .cdeep:hover { color: var(--fg); border-color: var(--accent); }
|
|
321
|
+
#indexPanel .chip .cdeep.on { color: #fff; background: var(--accent); border-color: var(--accent); }
|
|
316
322
|
#indexPanel .empty { padding: 8px 10px; color: var(--dim); font-style: italic; }
|
|
317
323
|
#indexPanel .icmt { display: block; margin: 4px 10px 8px; width: calc(100% - 20px);
|
|
318
324
|
box-sizing: border-box; background: var(--panel); color: var(--fg);
|
|
@@ -389,6 +395,32 @@ function buildHTML() {
|
|
|
389
395
|
.modal .actions button.primary:hover { filter: brightness(1.1); }
|
|
390
396
|
.modal .actions button.danger { background: var(--red); color: #fff; border-color: var(--red); }
|
|
391
397
|
|
|
398
|
+
/* ─── Terminal Settings modal ─── */
|
|
399
|
+
#tsModal .modal { width: 520px; max-height: 92vh; overflow-y: auto; }
|
|
400
|
+
.ts-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 18px; }
|
|
401
|
+
.ts-full { grid-column: 1 / -1; }
|
|
402
|
+
.ts-row { display: flex; align-items: center; gap: 8px; margin: 2px 0 6px; }
|
|
403
|
+
.ts-row input[type=range] { flex: 1; accent-color: var(--accent); cursor: pointer; }
|
|
404
|
+
.ts-row .tsv { min-width: 34px; text-align: right; font-size: 11px;
|
|
405
|
+
color: var(--accent); font-family: ui-monospace,'SF Mono',monospace; }
|
|
406
|
+
.ts-row select, .ts-row input[type=color], .ts-row input[type=text] { flex: 1; }
|
|
407
|
+
.ts-row input[type=color] { height: 28px; padding: 2px; border-radius: 4px;
|
|
408
|
+
border: 1px solid var(--border2); background: var(--bg); cursor: pointer; }
|
|
409
|
+
.ts-swatches { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0 10px; }
|
|
410
|
+
.ts-swatch { display: flex; flex-direction: column; align-items: center; gap: 3px;
|
|
411
|
+
cursor: pointer; user-select: none; }
|
|
412
|
+
.ts-swatch .tsb { width: 44px; height: 30px; border-radius: 6px; border: 2px solid transparent;
|
|
413
|
+
transition: border-color .15s, transform .1s; }
|
|
414
|
+
.ts-swatch:hover .tsb { transform: scale(1.06); }
|
|
415
|
+
.ts-swatch.active .tsb { border-color: var(--accent); }
|
|
416
|
+
.ts-swatch span { font-size: 9px; color: var(--muted); }
|
|
417
|
+
.ts-section { font-size: 10px; color: var(--dim); text-transform: uppercase;
|
|
418
|
+
letter-spacing: .6px; margin: 12px 0 6px; padding-bottom: 4px;
|
|
419
|
+
border-bottom: 1px solid var(--border); }
|
|
420
|
+
.ts-check { display: flex; align-items: center; gap: 6px; margin: 4px 0 8px;
|
|
421
|
+
font-size: 12px; color: var(--muted); cursor: pointer; }
|
|
422
|
+
.ts-check input { accent-color: var(--accent); cursor: pointer; }
|
|
423
|
+
|
|
392
424
|
/* Config / Agents / Skills lists */
|
|
393
425
|
.pane-section { padding: 10px 14px; }
|
|
394
426
|
.pane-section h4 { font-size: 11px; color: var(--dim); text-transform: uppercase;
|
|
@@ -617,6 +649,7 @@ function buildHTML() {
|
|
|
617
649
|
<button onclick="sendCmd('/model')">model</button>
|
|
618
650
|
<button onclick="sendCmd('/clear')">clear</button>
|
|
619
651
|
<button onclick="restartSapper()">restart</button>
|
|
652
|
+
<button title="Terminal appearance settings" onclick="openTermSettings()" style="font-size:13px;padding:3px 7px;">⚙</button>
|
|
620
653
|
</div>
|
|
621
654
|
|
|
622
655
|
<div id="body">
|
|
@@ -741,6 +774,114 @@ function buildHTML() {
|
|
|
741
774
|
</div>
|
|
742
775
|
<div id="toast"></div>
|
|
743
776
|
|
|
777
|
+
<!-- Terminal Settings Modal -->
|
|
778
|
+
<div id="tsModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:12000;display:none;align-items:center;justify-content:center;">
|
|
779
|
+
<div class="modal" style="width:520px;max-height:92vh;overflow-y:auto;">
|
|
780
|
+
<h3>⚙ Terminal Appearance</h3>
|
|
781
|
+
|
|
782
|
+
<div class="ts-section">Themes</div>
|
|
783
|
+
<div class="ts-swatches" id="tsSwatches"></div>
|
|
784
|
+
|
|
785
|
+
<div class="ts-section">Colors</div>
|
|
786
|
+
<div class="ts-grid">
|
|
787
|
+
<div>
|
|
788
|
+
<label>Background</label>
|
|
789
|
+
<div class="ts-row">
|
|
790
|
+
<input type="color" id="tsBg" />
|
|
791
|
+
<input type="text" id="tsBgHex" placeholder="#0a0e14" style="width:90px;flex:none;font-family:ui-monospace,monospace;font-size:11px;" />
|
|
792
|
+
</div>
|
|
793
|
+
</div>
|
|
794
|
+
<div>
|
|
795
|
+
<label>Foreground</label>
|
|
796
|
+
<div class="ts-row">
|
|
797
|
+
<input type="color" id="tsFg" />
|
|
798
|
+
<input type="text" id="tsFgHex" placeholder="#e6edf3" style="width:90px;flex:none;font-family:ui-monospace,monospace;font-size:11px;" />
|
|
799
|
+
</div>
|
|
800
|
+
</div>
|
|
801
|
+
<div>
|
|
802
|
+
<label>Cursor</label>
|
|
803
|
+
<div class="ts-row">
|
|
804
|
+
<input type="color" id="tsCursor" />
|
|
805
|
+
<input type="text" id="tsCursorHex" placeholder="#58a6ff" style="width:90px;flex:none;font-family:ui-monospace,monospace;font-size:11px;" />
|
|
806
|
+
</div>
|
|
807
|
+
</div>
|
|
808
|
+
<div>
|
|
809
|
+
<label>Selection</label>
|
|
810
|
+
<div class="ts-row">
|
|
811
|
+
<input type="color" id="tsSel" />
|
|
812
|
+
<input type="text" id="tsSelHex" placeholder="#58a6ff" style="width:90px;flex:none;font-family:ui-monospace,monospace;font-size:11px;" />
|
|
813
|
+
</div>
|
|
814
|
+
</div>
|
|
815
|
+
</div>
|
|
816
|
+
|
|
817
|
+
<div class="ts-section">Font</div>
|
|
818
|
+
<div class="ts-grid">
|
|
819
|
+
<div class="ts-full">
|
|
820
|
+
<label>Font Family</label>
|
|
821
|
+
<div class="ts-row">
|
|
822
|
+
<select id="tsFont">
|
|
823
|
+
<option value='"SF Mono","Fira Code","JetBrains Mono",Menlo,ui-monospace,monospace'>SF Mono / Fira Code</option>
|
|
824
|
+
<option value='"JetBrains Mono","Fira Code",ui-monospace,monospace'>JetBrains Mono</option>
|
|
825
|
+
<option value='"Fira Code","Source Code Pro",ui-monospace,monospace'>Fira Code</option>
|
|
826
|
+
<option value='"Cascadia Code","Consolas",ui-monospace,monospace'>Cascadia Code</option>
|
|
827
|
+
<option value='"Monaco",Menlo,ui-monospace,monospace'>Monaco</option>
|
|
828
|
+
<option value='Menlo,ui-monospace,monospace'>Menlo</option>
|
|
829
|
+
<option value='"Courier New",monospace'>Courier New</option>
|
|
830
|
+
<option value='ui-monospace,monospace'>System Mono</option>
|
|
831
|
+
</select>
|
|
832
|
+
</div>
|
|
833
|
+
</div>
|
|
834
|
+
<div>
|
|
835
|
+
<label>Size — <span id="tsFontSzV">13</span>px</label>
|
|
836
|
+
<div class="ts-row">
|
|
837
|
+
<input type="range" id="tsFontSz" min="9" max="28" step="1" value="13" oninput="document.getElementById('tsFontSzV').textContent=this.value" />
|
|
838
|
+
</div>
|
|
839
|
+
</div>
|
|
840
|
+
<div>
|
|
841
|
+
<label>Line Height — <span id="tsLhV">1.25</span></label>
|
|
842
|
+
<div class="ts-row">
|
|
843
|
+
<input type="range" id="tsLh" min="1.0" max="2.0" step="0.05" value="1.25" oninput="document.getElementById('tsLhV').textContent=parseFloat(this.value).toFixed(2)" />
|
|
844
|
+
</div>
|
|
845
|
+
</div>
|
|
846
|
+
</div>
|
|
847
|
+
|
|
848
|
+
<div class="ts-section">Cursor & Behavior</div>
|
|
849
|
+
<div class="ts-grid">
|
|
850
|
+
<div>
|
|
851
|
+
<label>Cursor Style</label>
|
|
852
|
+
<div class="ts-row">
|
|
853
|
+
<select id="tsCursorStyle">
|
|
854
|
+
<option value="bar">Bar |</option>
|
|
855
|
+
<option value="block">Block ▮</option>
|
|
856
|
+
<option value="underline">Underline _</option>
|
|
857
|
+
</select>
|
|
858
|
+
</div>
|
|
859
|
+
</div>
|
|
860
|
+
<div>
|
|
861
|
+
<label>Scrollback Lines</label>
|
|
862
|
+
<div class="ts-row">
|
|
863
|
+
<select id="tsScrollback">
|
|
864
|
+
<option value="1000">1,000</option>
|
|
865
|
+
<option value="5000">5,000</option>
|
|
866
|
+
<option value="10000" selected>10,000</option>
|
|
867
|
+
<option value="50000">50,000</option>
|
|
868
|
+
</select>
|
|
869
|
+
</div>
|
|
870
|
+
</div>
|
|
871
|
+
<div class="ts-full">
|
|
872
|
+
<label class="ts-check"><input type="checkbox" id="tsCursorBlink" checked /> Cursor blink</label>
|
|
873
|
+
<label class="ts-check"><input type="checkbox" id="tsLigatures" /> Font ligatures (requires compatible font)</label>
|
|
874
|
+
</div>
|
|
875
|
+
</div>
|
|
876
|
+
|
|
877
|
+
<div class="modal-actions" style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px;">
|
|
878
|
+
<button onclick="tsReset()" style="background:transparent;color:var(--muted);border:1px solid var(--border);border-radius:5px;padding:6px 12px;font-size:12px;cursor:pointer;">Reset defaults</button>
|
|
879
|
+
<button onclick="document.getElementById('tsModal').style.display='none'" style="background:transparent;color:var(--muted);border:1px solid var(--border);border-radius:5px;padding:6px 12px;font-size:12px;cursor:pointer;">Close</button>
|
|
880
|
+
<button onclick="tsApply(true)" style="background:var(--accent);color:#fff;border:1px solid var(--accent);border-radius:5px;padding:6px 14px;font-size:12px;cursor:pointer;">Apply & Save</button>
|
|
881
|
+
</div>
|
|
882
|
+
</div>
|
|
883
|
+
</div>
|
|
884
|
+
|
|
744
885
|
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
|
745
886
|
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
|
|
746
887
|
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.min.js"></script>
|
|
@@ -824,7 +965,184 @@ try { term.loadAddon(new WebLinksAddon.WebLinksAddon()); } catch(e){}
|
|
|
824
965
|
term.open(document.getElementById('term-wrap'));
|
|
825
966
|
setTimeout(function(){ try { fit.fit(); } catch(e){} }, 30);
|
|
826
967
|
|
|
827
|
-
|
|
968
|
+
// ─── Terminal Settings ────────────────────────────────────────
|
|
969
|
+
var TS_DEFAULTS = {
|
|
970
|
+
fontSize: 13, lineHeight: 1.25, fontFamily: '"SF Mono","Fira Code","JetBrains Mono",Menlo,ui-monospace,monospace',
|
|
971
|
+
cursorStyle: 'bar', cursorBlink: true, scrollback: 10000, ligatures: false,
|
|
972
|
+
bg: '#0a0e14', fg: '#e6edf3', cursor: '#58a6ff', sel: '#58a6ff'
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
var TS_THEMES = [
|
|
976
|
+
{ name:'Sapper Dark', bg:'#0a0e14', fg:'#e6edf3', cursor:'#58a6ff', sel:'#58a6ff',
|
|
977
|
+
colors:['#0a0e14','#484f58','#ff7b72','#3fb950','#d29922','#58a6ff','#bc8cff','#39c5cf'] },
|
|
978
|
+
{ name:'GitHub Dark', bg:'#161b22', fg:'#c9d1d9', cursor:'#58a6ff', sel:'#388bfd',
|
|
979
|
+
colors:['#161b22','#484f58','#ff7b72','#3fb950','#d29922','#58a6ff','#bc8cff','#39c5cf'] },
|
|
980
|
+
{ name:'Dracula', bg:'#282a36', fg:'#f8f8f2', cursor:'#ff79c6', sel:'#44475a',
|
|
981
|
+
colors:['#282a36','#44475a','#ff5555','#50fa7b','#f1fa8c','#6272a4','#ff79c6','#8be9fd'] },
|
|
982
|
+
{ name:'One Dark', bg:'#282c34', fg:'#abb2bf', cursor:'#528bff', sel:'#3e4451',
|
|
983
|
+
colors:['#282c34','#3e4451','#e06c75','#98c379','#e5c07b','#61afef','#c678dd','#56b6c2'] },
|
|
984
|
+
{ name:'Monokai', bg:'#272822', fg:'#f8f8f2', cursor:'#f8f8f2', sel:'#49483e',
|
|
985
|
+
colors:['#272822','#75715e','#f92672','#a6e22e','#f4bf75','#66d9e8','#ae81ff','#a1efe4'] },
|
|
986
|
+
{ name:'Solarized', bg:'#002b36', fg:'#839496', cursor:'#268bd2', sel:'#073642',
|
|
987
|
+
colors:['#002b36','#073642','#dc322f','#859900','#b58900','#268bd2','#d33682','#2aa198'] },
|
|
988
|
+
{ name:'Nord', bg:'#2e3440', fg:'#d8dee9', cursor:'#88c0d0', sel:'#4c566a',
|
|
989
|
+
colors:['#2e3440','#4c566a','#bf616a','#a3be8c','#ebcb8b','#5e81ac','#b48ead','#88c0d0'] },
|
|
990
|
+
{ name:'Light', bg:'#ffffff', fg:'#24292e', cursor:'#0366d6', sel:'#0366d6',
|
|
991
|
+
colors:['#ffffff','#f6f8fa','#d73a49','#22863a','#b08800','#0366d6','#6f42c1','#005cc5'] }
|
|
992
|
+
];
|
|
993
|
+
|
|
994
|
+
var tsSettings = {};
|
|
995
|
+
try { tsSettings = JSON.parse(localStorage.getItem('sapperTermSettings') || '{}') || {}; } catch(e) {}
|
|
996
|
+
|
|
997
|
+
function tsGet(k) { return tsSettings[k] !== undefined ? tsSettings[k] : TS_DEFAULTS[k]; }
|
|
998
|
+
|
|
999
|
+
function tsApplyToTerm(s) {
|
|
1000
|
+
s = s || tsSettings;
|
|
1001
|
+
var fontSize = s.fontSize !== undefined ? s.fontSize : TS_DEFAULTS.fontSize;
|
|
1002
|
+
var lineHeight = s.lineHeight !== undefined ? s.lineHeight : TS_DEFAULTS.lineHeight;
|
|
1003
|
+
var fontFamily = s.fontFamily !== undefined ? s.fontFamily : TS_DEFAULTS.fontFamily;
|
|
1004
|
+
var cursorStyle = s.cursorStyle !== undefined ? s.cursorStyle : TS_DEFAULTS.cursorStyle;
|
|
1005
|
+
var cursorBlink = s.cursorBlink !== undefined ? s.cursorBlink : TS_DEFAULTS.cursorBlink;
|
|
1006
|
+
var scrollback = s.scrollback !== undefined ? s.scrollback : TS_DEFAULTS.scrollback;
|
|
1007
|
+
var bg = s.bg !== undefined ? s.bg : TS_DEFAULTS.bg;
|
|
1008
|
+
var fg = s.fg !== undefined ? s.fg : TS_DEFAULTS.fg;
|
|
1009
|
+
var cursor = s.cursor !== undefined ? s.cursor : TS_DEFAULTS.cursor;
|
|
1010
|
+
var sel = s.sel !== undefined ? s.sel : TS_DEFAULTS.sel;
|
|
1011
|
+
|
|
1012
|
+
term.options.fontSize = parseInt(fontSize, 10);
|
|
1013
|
+
term.options.lineHeight = parseFloat(lineHeight);
|
|
1014
|
+
term.options.fontFamily = fontFamily;
|
|
1015
|
+
term.options.cursorStyle = cursorStyle;
|
|
1016
|
+
term.options.cursorBlink = !!cursorBlink;
|
|
1017
|
+
term.options.scrollback = parseInt(scrollback, 10);
|
|
1018
|
+
term.options.theme = {
|
|
1019
|
+
background: bg, foreground: fg,
|
|
1020
|
+
cursor: cursor, cursorAccent: bg,
|
|
1021
|
+
selectionBackground: sel + '59',
|
|
1022
|
+
black:'#484f58', red:'#ff7b72', green:'#3fb950', yellow:'#d29922',
|
|
1023
|
+
blue:'#58a6ff', magenta:'#bc8cff', cyan:'#39c5cf', white:'#e6edf3',
|
|
1024
|
+
brightBlack:'#6e7681', brightRed:'#ffa198', brightGreen:'#56d364',
|
|
1025
|
+
brightYellow:'#e3b341', brightBlue:'#79c0ff', brightMagenta:'#d2a8ff',
|
|
1026
|
+
brightCyan:'#56d4dd', brightWhite:'#f0f6fc'
|
|
1027
|
+
};
|
|
1028
|
+
// Also update the page background to match terminal background
|
|
1029
|
+
document.documentElement.style.setProperty('--bg', bg);
|
|
1030
|
+
document.documentElement.style.setProperty('--fg', fg);
|
|
1031
|
+
document.documentElement.style.setProperty('--accent', cursor);
|
|
1032
|
+
document.documentElement.style.setProperty('--accent2', cursor);
|
|
1033
|
+
// Also update the xterm viewport background directly
|
|
1034
|
+
document.querySelectorAll('.xterm .xterm-viewport').forEach(function(el){ el.style.backgroundColor = bg; });
|
|
1035
|
+
try { fit.fit(); } catch(e) {}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Apply saved settings on load (if any)
|
|
1039
|
+
(function(){
|
|
1040
|
+
if (Object.keys(tsSettings).length > 0) tsApplyToTerm(tsSettings);
|
|
1041
|
+
})();
|
|
1042
|
+
|
|
1043
|
+
function tsColorInputPair(colorId, hexId) {
|
|
1044
|
+
var col = document.getElementById(colorId);
|
|
1045
|
+
var hex = document.getElementById(hexId);
|
|
1046
|
+
if (!col || !hex) return;
|
|
1047
|
+
col.addEventListener('input', function() {
|
|
1048
|
+
hex.value = col.value;
|
|
1049
|
+
});
|
|
1050
|
+
hex.addEventListener('change', function() {
|
|
1051
|
+
var v = hex.value.trim();
|
|
1052
|
+
if (/^#[0-9a-fA-F]{6}$/.test(v)) col.value = v;
|
|
1053
|
+
});
|
|
1054
|
+
hex.addEventListener('input', function() {
|
|
1055
|
+
var v = hex.value.trim();
|
|
1056
|
+
if (/^#[0-9a-fA-F]{6}$/.test(v)) col.value = v;
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function tsApply(save) {
|
|
1061
|
+
var s = {
|
|
1062
|
+
fontSize: parseInt(document.getElementById('tsFontSz').value, 10),
|
|
1063
|
+
lineHeight: parseFloat(document.getElementById('tsLh').value),
|
|
1064
|
+
fontFamily: document.getElementById('tsFont').value,
|
|
1065
|
+
cursorStyle: document.getElementById('tsCursorStyle').value,
|
|
1066
|
+
cursorBlink: document.getElementById('tsCursorBlink').checked,
|
|
1067
|
+
scrollback: parseInt(document.getElementById('tsScrollback').value, 10),
|
|
1068
|
+
ligatures: document.getElementById('tsLigatures').checked,
|
|
1069
|
+
bg: document.getElementById('tsBgHex').value || document.getElementById('tsBg').value,
|
|
1070
|
+
fg: document.getElementById('tsFgHex').value || document.getElementById('tsFg').value,
|
|
1071
|
+
cursor: document.getElementById('tsCursorHex').value || document.getElementById('tsCursor').value,
|
|
1072
|
+
sel: document.getElementById('tsSelHex').value || document.getElementById('tsSel').value,
|
|
1073
|
+
};
|
|
1074
|
+
tsApplyToTerm(s);
|
|
1075
|
+
if (save) {
|
|
1076
|
+
tsSettings = s;
|
|
1077
|
+
try { localStorage.setItem('sapperTermSettings', JSON.stringify(s)); } catch(e) {}
|
|
1078
|
+
document.getElementById('tsModal').style.display = 'none';
|
|
1079
|
+
showToast('Terminal settings saved');
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function tsReset() {
|
|
1084
|
+
tsSettings = {};
|
|
1085
|
+
try { localStorage.removeItem('sapperTermSettings'); } catch(e) {}
|
|
1086
|
+
tsApplyToTerm(TS_DEFAULTS);
|
|
1087
|
+
tsLoadIntoForm(TS_DEFAULTS);
|
|
1088
|
+
showToast('Reset to defaults');
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function tsLoadIntoForm(s) {
|
|
1092
|
+
var get = function(k){ return s[k] !== undefined ? s[k] : TS_DEFAULTS[k]; };
|
|
1093
|
+
document.getElementById('tsFontSz').value = get('fontSize');
|
|
1094
|
+
document.getElementById('tsFontSzV').textContent = get('fontSize');
|
|
1095
|
+
document.getElementById('tsLh').value = get('lineHeight');
|
|
1096
|
+
document.getElementById('tsLhV').textContent = parseFloat(get('lineHeight')).toFixed(2);
|
|
1097
|
+
document.getElementById('tsFont').value = get('fontFamily');
|
|
1098
|
+
document.getElementById('tsCursorStyle').value = get('cursorStyle');
|
|
1099
|
+
document.getElementById('tsCursorBlink').checked = !!get('cursorBlink');
|
|
1100
|
+
document.getElementById('tsScrollback').value = get('scrollback');
|
|
1101
|
+
document.getElementById('tsLigatures').checked = !!get('ligatures');
|
|
1102
|
+
var bg = get('bg'); document.getElementById('tsBg').value = bg; document.getElementById('tsBgHex').value = bg;
|
|
1103
|
+
var fg = get('fg'); document.getElementById('tsFg').value = fg; document.getElementById('tsFgHex').value = fg;
|
|
1104
|
+
var c = get('cursor'); document.getElementById('tsCursor').value = c; document.getElementById('tsCursorHex').value = c;
|
|
1105
|
+
var sl = get('sel'); document.getElementById('tsSel').value = sl; document.getElementById('tsSelHex').value = sl;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function tsBuildSwatches() {
|
|
1109
|
+
var cont = document.getElementById('tsSwatches');
|
|
1110
|
+
if (!cont) return;
|
|
1111
|
+
cont.innerHTML = TS_THEMES.map(function(t, i) {
|
|
1112
|
+
var grad = 'background:linear-gradient(135deg,' + t.bg + ' 55%,' + t.cursor + ' 100%)';
|
|
1113
|
+
return '<div class="ts-swatch" data-i="' + i + '" title="' + t.name + '">' +
|
|
1114
|
+
'<div class="tsb" style="' + grad + '"></div>' +
|
|
1115
|
+
'<span>' + t.name + '</span></div>';
|
|
1116
|
+
}).join('');
|
|
1117
|
+
cont.querySelectorAll('.ts-swatch').forEach(function(el) {
|
|
1118
|
+
el.addEventListener('click', function() {
|
|
1119
|
+
cont.querySelectorAll('.ts-swatch').forEach(function(e){ e.classList.remove('active'); });
|
|
1120
|
+
el.classList.add('active');
|
|
1121
|
+
var t = TS_THEMES[parseInt(el.getAttribute('data-i'), 10)];
|
|
1122
|
+
document.getElementById('tsBg').value = t.bg; document.getElementById('tsBgHex').value = t.bg;
|
|
1123
|
+
document.getElementById('tsFg').value = t.fg; document.getElementById('tsFgHex').value = t.fg;
|
|
1124
|
+
document.getElementById('tsCursor').value = t.cursor; document.getElementById('tsCursorHex').value = t.cursor;
|
|
1125
|
+
document.getElementById('tsSel').value = t.sel; document.getElementById('tsSelHex').value = t.sel;
|
|
1126
|
+
tsApply(false); // live preview
|
|
1127
|
+
});
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
window.openTermSettings = function() {
|
|
1132
|
+
var modal = document.getElementById('tsModal');
|
|
1133
|
+
tsLoadIntoForm(tsSettings);
|
|
1134
|
+
tsBuildSwatches();
|
|
1135
|
+
// Wire color input pairs
|
|
1136
|
+
tsColorInputPair('tsBg','tsBgHex');
|
|
1137
|
+
tsColorInputPair('tsFg','tsFgHex');
|
|
1138
|
+
tsColorInputPair('tsCursor','tsCursorHex');
|
|
1139
|
+
tsColorInputPair('tsSel','tsSelHex');
|
|
1140
|
+
modal.style.display = 'flex';
|
|
1141
|
+
// Close on backdrop click
|
|
1142
|
+
modal.onclick = function(ev){ if (ev.target === modal) modal.style.display = 'none'; };
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
|
|
828
1146
|
|
|
829
1147
|
function connectPty() {
|
|
830
1148
|
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
@@ -1895,9 +2213,19 @@ function renderIndex() {
|
|
|
1895
2213
|
var info = state.indexSet[p];
|
|
1896
2214
|
var cls = info.isDir ? 'chip dir' : 'chip';
|
|
1897
2215
|
var ico = info.isDir ? '📁' : '🗎';
|
|
2216
|
+
var deepBtn = '';
|
|
2217
|
+
if (info.isDir) {
|
|
2218
|
+
var deepOn = info.deep ? ' on' : '';
|
|
2219
|
+
var deepLabel = info.deep ? 'deep' : 'scan';
|
|
2220
|
+
var deepTitle = info.deep
|
|
2221
|
+
? 'Deep: include the contents of every readable file in this folder'
|
|
2222
|
+
: 'Scan: list the folder and include code-file contents (smaller payload). Click to switch to deep.';
|
|
2223
|
+
deepBtn = '<span class="cdeep' + deepOn + '" data-p="' + esc(p) + '" title="' + deepTitle + '">' + deepLabel + '</span>';
|
|
2224
|
+
}
|
|
1898
2225
|
return '<span class="' + cls + '" title="' + esc(p) + '">' +
|
|
1899
2226
|
'<span>' + ico + '</span>' +
|
|
1900
2227
|
'<span class="cp">' + esc(p) + '</span>' +
|
|
2228
|
+
deepBtn +
|
|
1901
2229
|
'<span class="cx" data-p="' + esc(p) + '" title="Remove">×</span>' +
|
|
1902
2230
|
'</span>';
|
|
1903
2231
|
}).join('');
|
|
@@ -1907,23 +2235,42 @@ function renderIndex() {
|
|
|
1907
2235
|
toggleIndex(el.getAttribute('data-p'), state.indexSet[el.getAttribute('data-p')] && state.indexSet[el.getAttribute('data-p')].isDir);
|
|
1908
2236
|
});
|
|
1909
2237
|
});
|
|
2238
|
+
chips.querySelectorAll('.cdeep').forEach(function(el){
|
|
2239
|
+
el.addEventListener('click', function(ev){
|
|
2240
|
+
ev.stopPropagation();
|
|
2241
|
+
var p = el.getAttribute('data-p');
|
|
2242
|
+
var info = state.indexSet[p];
|
|
2243
|
+
if (!info || !info.isDir) return;
|
|
2244
|
+
info.deep = !info.deep;
|
|
2245
|
+
saveIndex();
|
|
2246
|
+
renderIndex();
|
|
2247
|
+
});
|
|
2248
|
+
});
|
|
1910
2249
|
}
|
|
1911
2250
|
|
|
1912
2251
|
window.sendIndexToChat = function() {
|
|
1913
2252
|
var paths = Object.keys(state.indexSet);
|
|
1914
2253
|
if (!paths.length) { showToast('Index is empty', 'err'); return; }
|
|
1915
2254
|
if (!ws || ws.readyState !== 1) { showToast('Terminal not connected', 'err'); return; }
|
|
1916
|
-
var files = [],
|
|
2255
|
+
var files = [], dirsShallow = [], dirsDeep = [];
|
|
1917
2256
|
paths.forEach(function(p){
|
|
1918
|
-
|
|
2257
|
+
var info = state.indexSet[p];
|
|
2258
|
+
if (!info) return;
|
|
2259
|
+
if (info.isDir) {
|
|
2260
|
+
if (info.deep) dirsDeep.push(p); else dirsShallow.push(p);
|
|
2261
|
+
} else {
|
|
2262
|
+
files.push(p);
|
|
2263
|
+
}
|
|
1919
2264
|
});
|
|
1920
2265
|
// Helper: quote paths that contain spaces, leave plain otherwise.
|
|
1921
2266
|
function quoteIfNeeded(p) {
|
|
1922
2267
|
return /\\s/.test(p) ? '"' + p.replace(/"/g, '\\\\"') + '"' : p;
|
|
1923
2268
|
}
|
|
1924
|
-
// 1) /scan
|
|
1925
|
-
|
|
1926
|
-
// 2)
|
|
2269
|
+
// 1) /scan for shallow folders (file tree + code-file contents)
|
|
2270
|
+
dirsShallow.forEach(function(d){ sendPasteToTerm('/scan ' + quoteIfNeeded(d)); });
|
|
2271
|
+
// 2) /include for deep folders (every readable file's contents)
|
|
2272
|
+
dirsDeep.forEach(function(d){ sendPasteToTerm('/include ' + quoteIfNeeded(d)); });
|
|
2273
|
+
// 3) Build attachments token for files
|
|
1927
2274
|
var atTokens = files.map(function(f){ return '@' + quoteIfNeeded(f); }).join(' ');
|
|
1928
2275
|
var comment = (document.getElementById('idxComment') || {}).value || '';
|
|
1929
2276
|
comment = comment.trim();
|
|
@@ -1936,8 +2283,10 @@ window.sendIndexToChat = function() {
|
|
|
1936
2283
|
// Stage at cursor — no Enter, so the user can type their question
|
|
1937
2284
|
sendRawToTerm(atTokens + ' ');
|
|
1938
2285
|
}
|
|
2286
|
+
var dirsTotal = dirsShallow.length + dirsDeep.length;
|
|
1939
2287
|
showToast('Sent ' + files.length + ' file' + (files.length === 1 ? '' : 's') +
|
|
1940
|
-
(
|
|
2288
|
+
(dirsTotal ? ' and ' + dirsTotal + ' folder' + (dirsTotal === 1 ? '' : 's') +
|
|
2289
|
+
(dirsDeep.length ? ' (' + dirsDeep.length + ' deep)' : '') : '') +
|
|
1941
2290
|
' to chat');
|
|
1942
2291
|
// Clear comment, clear index, refocus terminal
|
|
1943
2292
|
var cmt = document.getElementById('idxComment'); if (cmt) cmt.value = '';
|
|
@@ -2777,41 +3126,98 @@ function spawnSapper(cols, rows) {
|
|
|
2777
3126
|
});
|
|
2778
3127
|
}
|
|
2779
3128
|
|
|
3129
|
+
// ─── Persistent PTY (survives browser refresh) ───────────────────
|
|
3130
|
+
// The pty process lives at module scope so it outlives any WS connection.
|
|
3131
|
+
// All output is stored in a ring buffer; new clients replay it on connect.
|
|
3132
|
+
|
|
3133
|
+
const PTY_SCROLLBACK_MAX = 512 * 1024; // 512 KB replay buffer
|
|
3134
|
+
let sharedPty = null;
|
|
3135
|
+
let ptyScrollback = ''; // raw bytes (utf-8) for replay
|
|
3136
|
+
let ptyCols = 220, ptyRows = 50; // last known size
|
|
3137
|
+
|
|
3138
|
+
function appendPtyScrollback(chunk) {
|
|
3139
|
+
ptyScrollback += chunk;
|
|
3140
|
+
if (ptyScrollback.length > PTY_SCROLLBACK_MAX) {
|
|
3141
|
+
// Drop oldest half to stay near cap without constant slicing
|
|
3142
|
+
ptyScrollback = ptyScrollback.slice(ptyScrollback.length - Math.floor(PTY_SCROLLBACK_MAX * 0.6));
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
function ensurePty(cols, rows) {
|
|
3147
|
+
if (sharedPty) return;
|
|
3148
|
+
ptyCols = cols || 220; ptyRows = rows || 50;
|
|
3149
|
+
try {
|
|
3150
|
+
sharedPty = spawnSapper(ptyCols, ptyRows);
|
|
3151
|
+
} catch (e) {
|
|
3152
|
+
console.error('[ui] spawn failed:', e.message);
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
dbg('pty pid=' + sharedPty.pid + ' ' + ptyCols + 'x' + ptyRows);
|
|
3156
|
+
sharedPty.onData((d) => {
|
|
3157
|
+
appendPtyScrollback(d);
|
|
3158
|
+
// Broadcast to every connected pty client
|
|
3159
|
+
for (const ws of ptyClients) {
|
|
3160
|
+
if (ws.readyState === ws.OPEN) {
|
|
3161
|
+
try { ws.send(Buffer.from(d, 'utf8')); } catch {}
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
});
|
|
3165
|
+
sharedPty.onExit(({ exitCode, signal }) => {
|
|
3166
|
+
dbg('pty exit code=' + exitCode);
|
|
3167
|
+
sharedPty = null;
|
|
3168
|
+
// Notify all clients so they can show "exited" badge
|
|
3169
|
+
const msg = JSON.stringify({ type: 'exit', code: exitCode, signal });
|
|
3170
|
+
for (const ws of ptyClients) {
|
|
3171
|
+
if (ws.readyState === ws.OPEN) { try { ws.send(msg); } catch {} }
|
|
3172
|
+
}
|
|
3173
|
+
});
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
const ptyClients = new Set(); // all currently-connected pty websockets
|
|
3177
|
+
|
|
2780
3178
|
wssPty.on('connection', (ws) => {
|
|
2781
3179
|
dbg('pty client connected');
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
function start(cols, rows) {
|
|
2785
|
-
if (pty) { try { pty.kill(); } catch {} }
|
|
2786
|
-
try { pty = spawnSapper(cols, rows); }
|
|
2787
|
-
catch (e) {
|
|
2788
|
-
console.error('[ui] spawn failed:', e.message);
|
|
2789
|
-
try { ws.send(Buffer.from('\x1b[31mFailed to spawn sapper: ' + e.message + '\x1b[0m\r\n', 'utf8')); } catch {}
|
|
2790
|
-
return;
|
|
2791
|
-
}
|
|
2792
|
-
dbg('pty pid=' + pty.pid + ' ' + cols + 'x' + rows);
|
|
2793
|
-
pty.onData((d) => { if (ws.readyState === ws.OPEN) ws.send(Buffer.from(d, 'utf8')); });
|
|
2794
|
-
pty.onExit(({ exitCode, signal }) => {
|
|
2795
|
-
dbg('pty exit code=' + exitCode);
|
|
2796
|
-
if (ws.readyState === ws.OPEN) { try { ws.send(JSON.stringify({ type: 'exit', code: exitCode, signal })); } catch {} }
|
|
2797
|
-
});
|
|
2798
|
-
try { ws.send(JSON.stringify({ type: 'cwd', path: workingDir })); } catch {}
|
|
2799
|
-
}
|
|
3180
|
+
ptyClients.add(ws);
|
|
2800
3181
|
|
|
2801
3182
|
ws.on('message', (raw, isBinary) => {
|
|
2802
3183
|
const str = raw.toString('utf8');
|
|
2803
3184
|
if (!isBinary && str.startsWith('{')) {
|
|
2804
3185
|
try {
|
|
2805
3186
|
const m = JSON.parse(str);
|
|
2806
|
-
if (m.type === 'init') {
|
|
2807
|
-
|
|
2808
|
-
|
|
3187
|
+
if (m.type === 'init') {
|
|
3188
|
+
// Spawn pty if not running yet
|
|
3189
|
+
ensurePty(m.cols, m.rows);
|
|
3190
|
+
// Replay scrollback so the refreshed browser sees prior output
|
|
3191
|
+
if (ptyScrollback.length > 0) {
|
|
3192
|
+
try { ws.send(Buffer.from(ptyScrollback, 'utf8')); } catch {}
|
|
3193
|
+
}
|
|
3194
|
+
// Always send current cwd
|
|
3195
|
+
try { ws.send(JSON.stringify({ type: 'cwd', path: workingDir })); } catch {}
|
|
3196
|
+
return;
|
|
3197
|
+
}
|
|
3198
|
+
if (m.type === 'resize' && sharedPty) {
|
|
3199
|
+
ptyCols = m.cols || ptyCols; ptyRows = m.rows || ptyRows;
|
|
3200
|
+
try { sharedPty.resize(ptyCols, ptyRows); } catch {}
|
|
3201
|
+
return;
|
|
3202
|
+
}
|
|
3203
|
+
if (m.type === 'restart') {
|
|
3204
|
+
// Kill current pty and start fresh; clear scrollback
|
|
3205
|
+
if (sharedPty) { try { sharedPty.kill(); } catch {} sharedPty = null; }
|
|
3206
|
+
ptyScrollback = '';
|
|
3207
|
+
ensurePty(ptyCols, ptyRows);
|
|
3208
|
+
try { ws.send(JSON.stringify({ type: 'cwd', path: workingDir })); } catch {}
|
|
3209
|
+
return;
|
|
3210
|
+
}
|
|
2809
3211
|
} catch {}
|
|
2810
3212
|
}
|
|
2811
|
-
if (
|
|
3213
|
+
if (sharedPty) sharedPty.write(str);
|
|
2812
3214
|
});
|
|
2813
3215
|
|
|
2814
|
-
ws.on('close', () => {
|
|
3216
|
+
ws.on('close', () => {
|
|
3217
|
+
ptyClients.delete(ws);
|
|
3218
|
+
// Do NOT kill the pty — keep it alive for the next reconnect.
|
|
3219
|
+
dbg('pty client disconnected (' + ptyClients.size + ' remaining)');
|
|
3220
|
+
});
|
|
2815
3221
|
});
|
|
2816
3222
|
|
|
2817
3223
|
// ── FS watcher: broadcast to all /events clients ─────────────────
|
|
@@ -3088,5 +3494,11 @@ server.on('listening', () => {
|
|
|
3088
3494
|
|
|
3089
3495
|
tryListen(PORT);
|
|
3090
3496
|
|
|
3091
|
-
|
|
3092
|
-
|
|
3497
|
+
function shutdown() {
|
|
3498
|
+
console.log('\nShutting down…');
|
|
3499
|
+
try { watcher && watcher.close(); } catch {}
|
|
3500
|
+
if (sharedPty) { try { sharedPty.kill(); } catch {} }
|
|
3501
|
+
process.exit(0);
|
|
3502
|
+
}
|
|
3503
|
+
process.on('SIGINT', shutdown);
|
|
3504
|
+
process.on('SIGTERM', shutdown);
|
package/sapper.mjs
CHANGED
|
@@ -1019,6 +1019,12 @@ let _useNativeToolsFlag = false;
|
|
|
1019
1019
|
// Models known to reject `think:true` (populated lazily on first failure).
|
|
1020
1020
|
const modelsWithoutThinking = new Set();
|
|
1021
1021
|
|
|
1022
|
+
// Models known to emit malformed native tool calls (e.g. broken XML that
|
|
1023
|
+
// Ollama's parser rejects with "element <function> closed by </parameter>").
|
|
1024
|
+
// Once a model lands here we stop sending `tools` to it for the rest of the
|
|
1025
|
+
// session — it falls back to text-marker tools, which any model can produce.
|
|
1026
|
+
const modelsWithBrokenNativeTools = new Set();
|
|
1027
|
+
|
|
1022
1028
|
function buildSystemPrompt(agentContent = null, skillContents = []) {
|
|
1023
1029
|
const now = new Date();
|
|
1024
1030
|
const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
|
@@ -3013,7 +3019,8 @@ const COMMAND_GROUPS = Object.freeze([
|
|
|
3013
3019
|
rows: [
|
|
3014
3020
|
['@ or /attach', 'Pick files to attach interactively'],
|
|
3015
3021
|
['@file', 'Attach a file inline, for example @src/app.js'],
|
|
3016
|
-
['/scan', 'Scan
|
|
3022
|
+
['/scan [path]', 'Scan a folder (code files only) into context'],
|
|
3023
|
+
['/include [path]', 'Attach every readable file in a folder (deep)'],
|
|
3017
3024
|
['/index', 'Rebuild the workspace graph'],
|
|
3018
3025
|
['/graph file', 'Show related files from the graph'],
|
|
3019
3026
|
['/symbol name', 'Search indexed functions and classes'],
|
|
@@ -4185,6 +4192,50 @@ function scanCodebase(dir = '.', depth = 0, maxDepth = 5) {
|
|
|
4185
4192
|
return { files, totalSize };
|
|
4186
4193
|
}
|
|
4187
4194
|
|
|
4195
|
+
// Walk a folder and read every readable text file's contents (broader than
|
|
4196
|
+
// scanCodebase, which is restricted to CODE_EXTENSIONS). Used by /include
|
|
4197
|
+
// when the user wants ALL files in a folder attached (not just a tree+code).
|
|
4198
|
+
function includeFolderContents(dir, depth = 0, maxDepth = 8, acc = null) {
|
|
4199
|
+
acc = acc || { files: [], totalSize: 0, skipped: [], reachedCap: false };
|
|
4200
|
+
if (acc.reachedCap || depth > maxDepth) return acc;
|
|
4201
|
+
let entries;
|
|
4202
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
4203
|
+
catch (e) { return acc; }
|
|
4204
|
+
for (const entry of entries) {
|
|
4205
|
+
if (acc.reachedCap) break;
|
|
4206
|
+
const fullPath = dir === '.' ? entry.name : `${dir}/${entry.name}`;
|
|
4207
|
+
if (entry.isDirectory()) {
|
|
4208
|
+
if (shouldIgnore(entry.name) || entry.name.startsWith('.')) continue;
|
|
4209
|
+
includeFolderContents(fullPath, depth + 1, maxDepth, acc);
|
|
4210
|
+
continue;
|
|
4211
|
+
}
|
|
4212
|
+
if (!entry.isFile()) continue;
|
|
4213
|
+
if (shouldIgnore(fullPath) || shouldIgnore(entry.name)) continue;
|
|
4214
|
+
let stats;
|
|
4215
|
+
try { stats = fs.statSync(fullPath); } catch (e) { continue; }
|
|
4216
|
+
if (stats.size > getMaxFileSize()) {
|
|
4217
|
+
acc.skipped.push({ path: fullPath, size: stats.size, reason: 'too large' });
|
|
4218
|
+
continue;
|
|
4219
|
+
}
|
|
4220
|
+
if (acc.totalSize + stats.size > getMaxScanSize()) {
|
|
4221
|
+
acc.skipped.push({ path: fullPath, size: stats.size, reason: 'total limit reached' });
|
|
4222
|
+
acc.reachedCap = true;
|
|
4223
|
+
continue;
|
|
4224
|
+
}
|
|
4225
|
+
let content;
|
|
4226
|
+
try { content = fs.readFileSync(fullPath, 'utf8'); }
|
|
4227
|
+
catch (e) { acc.skipped.push({ path: fullPath, reason: e.message }); continue; }
|
|
4228
|
+
// Skip binary-looking files (NUL byte in first 4KB)
|
|
4229
|
+
if (content.length && content.slice(0, 4096).indexOf('\u0000') !== -1) {
|
|
4230
|
+
acc.skipped.push({ path: fullPath, size: stats.size, reason: 'binary' });
|
|
4231
|
+
continue;
|
|
4232
|
+
}
|
|
4233
|
+
acc.files.push({ path: fullPath, size: stats.size, content });
|
|
4234
|
+
acc.totalSize += stats.size;
|
|
4235
|
+
}
|
|
4236
|
+
return acc;
|
|
4237
|
+
}
|
|
4238
|
+
|
|
4188
4239
|
// Scan directory for files (for @ file picker)
|
|
4189
4240
|
function getFilesForPicker(dir = '.', prefix = '', maxFiles = 50) {
|
|
4190
4241
|
let files = [];
|
|
@@ -6263,6 +6314,12 @@ async function runSapper() {
|
|
|
6263
6314
|
} else {
|
|
6264
6315
|
toolModeLabel = 'text markers';
|
|
6265
6316
|
}
|
|
6317
|
+
// If we previously caught this model emitting malformed tool XML, stay
|
|
6318
|
+
// on text-marker tools for the whole session.
|
|
6319
|
+
if (modelsWithBrokenNativeTools.has(selectedModel)) {
|
|
6320
|
+
useNativeTools = false;
|
|
6321
|
+
toolModeLabel = 'text markers (native tools disabled — model emits broken XML)';
|
|
6322
|
+
}
|
|
6266
6323
|
// Extract context window size from model_info
|
|
6267
6324
|
// Different model families use different keys: llama.context_length, qwen2.context_length, etc.
|
|
6268
6325
|
if (modelInfo.model_info) {
|
|
@@ -8424,6 +8481,61 @@ async function runSapper() {
|
|
|
8424
8481
|
continue;
|
|
8425
8482
|
}
|
|
8426
8483
|
|
|
8484
|
+
// /include <path> — deep-attach every readable file in a folder
|
|
8485
|
+
// (broader than /scan: not limited to code extensions). Used by the
|
|
8486
|
+
// Index tray's "deep" toggle on folder chips.
|
|
8487
|
+
if (input.toLowerCase() === '/include' || input.toLowerCase().startsWith('/include ')) {
|
|
8488
|
+
let incTarget = '.';
|
|
8489
|
+
const incRest = input.slice(8).trim();
|
|
8490
|
+
if (incRest) {
|
|
8491
|
+
if ((incRest.startsWith('"') && incRest.endsWith('"')) ||
|
|
8492
|
+
(incRest.startsWith("'") && incRest.endsWith("'"))) {
|
|
8493
|
+
incTarget = incRest.slice(1, -1);
|
|
8494
|
+
} else {
|
|
8495
|
+
incTarget = incRest;
|
|
8496
|
+
}
|
|
8497
|
+
}
|
|
8498
|
+
if (!fs.existsSync(incTarget)) {
|
|
8499
|
+
console.log(chalk.red(`Path not found: ${incTarget}`));
|
|
8500
|
+
continue;
|
|
8501
|
+
}
|
|
8502
|
+
const incStat = fs.statSync(incTarget);
|
|
8503
|
+
if (!incStat.isDirectory()) {
|
|
8504
|
+
console.log(chalk.red(`Not a directory: ${incTarget}`));
|
|
8505
|
+
continue;
|
|
8506
|
+
}
|
|
8507
|
+
console.log(uiCleanMode()
|
|
8508
|
+
? chalk.cyan(`\nIncluding all files in ${incTarget}...`)
|
|
8509
|
+
: chalk.cyan(`\n📚 Including all files in ${incTarget}...`));
|
|
8510
|
+
const incResult = includeFolderContents(incTarget);
|
|
8511
|
+
if (incResult.files.length === 0) {
|
|
8512
|
+
console.log(chalk.yellow(`No readable files found in ${incTarget}.`));
|
|
8513
|
+
if (incResult.skipped.length) {
|
|
8514
|
+
console.log(chalk.gray(`(${incResult.skipped.length} skipped — too large, binary, or ignored)`));
|
|
8515
|
+
}
|
|
8516
|
+
continue;
|
|
8517
|
+
}
|
|
8518
|
+
const incBlock = formatFileAttachments(incResult.files);
|
|
8519
|
+
messages.push({
|
|
8520
|
+
role: 'user',
|
|
8521
|
+
content: `Including all readable files from ${incTarget}:\n${incBlock}`
|
|
8522
|
+
});
|
|
8523
|
+
console.log(uiCleanMode()
|
|
8524
|
+
? chalk.green(`Included ${incResult.files.length} files (~${Math.round(incResult.totalSize/1024)}KB) from ${incTarget}`)
|
|
8525
|
+
: chalk.green(`✅ Included ${incResult.files.length} files (~${Math.round(incResult.totalSize/1024)}KB) from ${incTarget}`));
|
|
8526
|
+
if (incResult.skipped.length > 0) {
|
|
8527
|
+
console.log(uiCleanMode()
|
|
8528
|
+
? chalk.yellow(`Skipped ${incResult.skipped.length} files (too large, binary, or limit reached)`)
|
|
8529
|
+
: chalk.yellow(`⏭️ Skipped ${incResult.skipped.length} files (too large, binary, or limit reached)`));
|
|
8530
|
+
}
|
|
8531
|
+
if (incResult.reachedCap) {
|
|
8532
|
+
console.log(chalk.yellow(`⚠️ Hit total-size cap (~${Math.round(getMaxScanSize()/1024)}KB); some files were not included.`));
|
|
8533
|
+
}
|
|
8534
|
+
ensureSapperDir();
|
|
8535
|
+
fs.writeFileSync(CONTEXT_FILE, JSON.stringify(messages, null, 2));
|
|
8536
|
+
continue;
|
|
8537
|
+
}
|
|
8538
|
+
|
|
8427
8539
|
if (input.startsWith('/') && !input.startsWith('//') && !agentHandled) {
|
|
8428
8540
|
const commandToken = input.slice(1).trim().split(/\s+/)[0] || '';
|
|
8429
8541
|
const suggestions = suggestSlashCommands(commandToken, 5);
|
|
@@ -8654,6 +8766,9 @@ async function runSapper() {
|
|
|
8654
8766
|
let turnThinkingEnabled = shouldUseThinkingForInput(input);
|
|
8655
8767
|
if (modelsWithoutThinking.has(selectedModel)) turnThinkingEnabled = false;
|
|
8656
8768
|
|
|
8769
|
+
// Auto-recover once per turn if the model emits malformed native tool XML.
|
|
8770
|
+
let nativeToolXmlRetried = false;
|
|
8771
|
+
|
|
8657
8772
|
let active = true;
|
|
8658
8773
|
while (active) {
|
|
8659
8774
|
if (stepMode) await safeQuestion(chalk.gray(promptLabel('questions.stepContinue', '[STEP] Press Enter to let AI think...')));
|
|
@@ -8866,8 +8981,31 @@ async function runSapper() {
|
|
|
8866
8981
|
process.stdout.write(`\n${UI.slate(' └─')}\n`);
|
|
8867
8982
|
}
|
|
8868
8983
|
process.stdout.write('\r\x1b[K');
|
|
8869
|
-
|
|
8870
|
-
|
|
8984
|
+
const streamErrMsg = streamErrored.message || String(streamErrored);
|
|
8985
|
+
// Detect malformed native-tool XML from small/quantized models.
|
|
8986
|
+
// Ollama's parser surfaces this as e.g. "XML syntax error on line 3:
|
|
8987
|
+
// element <function> closed by </parameter>". Disable native tools
|
|
8988
|
+
// for this model and retry the same turn once.
|
|
8989
|
+
const isXmlToolErr = /xml syntax|element <\w+> closed by|invalid character|unexpected end of/i.test(streamErrMsg);
|
|
8990
|
+
if (isXmlToolErr && useNativeTools && !nativeToolXmlRetried) {
|
|
8991
|
+
nativeToolXmlRetried = true;
|
|
8992
|
+
modelsWithBrokenNativeTools.add(selectedModel);
|
|
8993
|
+
useNativeTools = false;
|
|
8994
|
+
toolModeLabel = 'text markers (auto: model emits broken tool XML)';
|
|
8995
|
+
console.error(chalk.yellow(
|
|
8996
|
+
`\n⚠ "${selectedModel}" emitted malformed tool XML — disabling native tool calling for this session and retrying.`
|
|
8997
|
+
));
|
|
8998
|
+
console.error(chalk.gray(' Tip: a larger / instruction-tuned model (e.g. qwen2.5:7b, llama3.1) will not hit this.'));
|
|
8999
|
+
logEntry('warn', { message: `Disabled native tools for ${selectedModel}: ${streamErrMsg}` });
|
|
9000
|
+
// Reset per-attempt state and try this same turn again without tools.
|
|
9001
|
+
msg = '';
|
|
9002
|
+
thinkMsg = '';
|
|
9003
|
+
nativeToolCalls = [];
|
|
9004
|
+
streamErrored = null;
|
|
9005
|
+
continue;
|
|
9006
|
+
}
|
|
9007
|
+
console.error(chalk.red(`\n❌ Stream error: ${streamErrMsg}`));
|
|
9008
|
+
logEntry('error', { message: `Stream error: ${streamErrMsg}` });
|
|
8871
9009
|
active = false;
|
|
8872
9010
|
continue;
|
|
8873
9011
|
}
|