crosspad-mcp-server 9.1.1 → 9.2.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.
- package/dist/config.d.ts +7 -0
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +125 -25
- package/dist/index.js.map +1 -1
- package/dist/tools/stm-build.d.ts +23 -0
- package/dist/tools/stm-build.js +78 -0
- package/dist/tools/stm-build.js.map +1 -0
- package/dist/tools/stm-flash.d.ts +36 -0
- package/dist/tools/stm-flash.js +112 -0
- package/dist/tools/stm-flash.js.map +1 -0
- package/dist/tools/trace-session.d.ts +12 -0
- package/dist/tools/trace-session.js +69 -0
- package/dist/tools/trace-session.js.map +1 -1
- package/dist/tools/trace-webui.d.ts +14 -7
- package/dist/tools/trace-webui.js +77 -33
- package/dist/tools/trace-webui.js.map +1 -1
- package/dist/tools/trace-write.d.ts +9 -0
- package/dist/tools/trace-write.js +32 -0
- package/dist/tools/trace-write.js.map +1 -0
- package/dist/utils/userConfig.d.ts +9 -0
- package/dist/utils/userConfig.js.map +1 -1
- package/package.json +2 -1
- package/skills/swd-tracer/SKILL.md +48 -0
- package/tracer/PROTOCOL.md +69 -0
- package/tracer/swd_tracer.py +449 -12
- package/tracer/ui/config.js +40 -0
- package/tracer/ui/connection.js +170 -0
- package/tracer/ui/index.html +50 -798
- package/tracer/ui/main.js +30 -0
- package/tracer/ui/plot.js +337 -0
- package/tracer/ui/signals.js +39 -0
- package/tracer/ui/state.js +57 -0
- package/tracer/ui/stats.js +52 -0
- package/tracer/ui/style.css +97 -0
- package/tracer/ui/symbols.js +80 -0
- package/tracer/ui/toolbar.js +95 -0
- package/tracer/ui/util.js +62 -0
- package/tracer/ui/watchlist.js +179 -0
- package/vscode-extension/README.md +32 -0
- package/vscode-extension/extension.js +56 -0
- package/vscode-extension/package.json +14 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* Symbol autocomplete data source ("podpowiedzi zmiennych").
|
|
2
|
+
Fetches /symbols (PROTOCOL §8/§9) and builds a flat, deduped suggestion list
|
|
3
|
+
+ a name→metadata map. The watchlist module renders the actual dropdown from
|
|
4
|
+
this data; here we only fetch and shape. Free-form expansion specs the user
|
|
5
|
+
types (vec[*], vec[a:b], mat[*][k]) are never blocked — suggestions only
|
|
6
|
+
ASSIST; the server expands whatever is actually submitted. */
|
|
7
|
+
|
|
8
|
+
import { bus } from "./state.js";
|
|
9
|
+
|
|
10
|
+
let suggestList = []; // deduped suggestion strings (sorted)
|
|
11
|
+
export const symMeta = new Map(); // name -> §8 metadata entry
|
|
12
|
+
export function getSuggestList(){ return suggestList; }
|
|
13
|
+
|
|
14
|
+
/* Build suggestion strings from one symbol metadata entry (§8 shape). */
|
|
15
|
+
export function suggestionsForSymbol(sym){
|
|
16
|
+
const out=[];
|
|
17
|
+
const name=sym&&typeof sym.name==="string"?sym.name:null;
|
|
18
|
+
if(!name)return out;
|
|
19
|
+
const kind=sym.kind;
|
|
20
|
+
if(kind==="array"){
|
|
21
|
+
out.push(name); // bare array name → whole-array expansion
|
|
22
|
+
out.push(name+"[0]"); // first element
|
|
23
|
+
out.push(name+"[*]"); // all elements (wildcard)
|
|
24
|
+
if(Array.isArray(sym.dims)&&sym.dims.length>=2){
|
|
25
|
+
out.push(name+"[0][0]");
|
|
26
|
+
out.push(name+"[*][0]");
|
|
27
|
+
}
|
|
28
|
+
}else if(kind==="struct"||kind==="union"){
|
|
29
|
+
out.push(name);
|
|
30
|
+
if(Array.isArray(sym.members)){
|
|
31
|
+
for(const mb of sym.members){ if(typeof mb==="string"&&mb) out.push(name+"."+mb); }
|
|
32
|
+
}
|
|
33
|
+
}else{
|
|
34
|
+
out.push(name); // scalar / other / unknown
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Strip the [i]/[*]/.member suffix to get the base symbol name for metadata
|
|
40
|
+
lookup. */
|
|
41
|
+
export function baseName(spec){
|
|
42
|
+
const m=/^[A-Za-z_$][A-Za-z0-9_$]*/.exec(spec);
|
|
43
|
+
return m?m[0]:spec;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* A short metadata hint for a suggestion, e.g. "(8×, n=8)" or "(struct)". */
|
|
47
|
+
export function metaHint(spec){
|
|
48
|
+
const meta=symMeta.get(baseName(spec));
|
|
49
|
+
if(!meta)return "";
|
|
50
|
+
if(meta.kind==="array"){
|
|
51
|
+
const dims=Array.isArray(meta.dims)?meta.dims.join("×")+", ":"";
|
|
52
|
+
return "("+dims+"n="+(meta.count!=null?meta.count:"?")+")";
|
|
53
|
+
}
|
|
54
|
+
if(meta.kind==="struct"||meta.kind==="union") return "("+meta.kind+")";
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Fetch /symbols and rebuild the suggestion list. Degrades gracefully: any
|
|
59
|
+
failure (older server, no active session, non-JSON) leaves autocomplete empty
|
|
60
|
+
and manual entry still works. Notifies the view via bus.onSymbols so an open
|
|
61
|
+
dropdown can refresh. */
|
|
62
|
+
export function loadSymbols(){
|
|
63
|
+
fetch("/symbols").then(r=>{
|
|
64
|
+
if(!r.ok) throw new Error("HTTP "+r.status);
|
|
65
|
+
return r.json();
|
|
66
|
+
}).then(j=>{
|
|
67
|
+
const arr=(j&&Array.isArray(j.symbols))?j.symbols:[];
|
|
68
|
+
const seen=new Set(); const list=[];
|
|
69
|
+
symMeta.clear();
|
|
70
|
+
for(const sym of arr){
|
|
71
|
+
if(sym&&typeof sym.name==="string") symMeta.set(sym.name,sym);
|
|
72
|
+
for(const sug of suggestionsForSymbol(sym)){
|
|
73
|
+
if(!seen.has(sug)){ seen.add(sug); list.push(sug); }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
list.sort();
|
|
77
|
+
suggestList=list;
|
|
78
|
+
bus.onSymbols();
|
|
79
|
+
}).catch(()=>{ /* no autocomplete — manual entry still works */ });
|
|
80
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/* Top-bar controls: pause/resume, reset view, PNG/CSV export, force-reconnect,
|
|
2
|
+
and keyboard shortcuts. */
|
|
3
|
+
|
|
4
|
+
import { state, sigs } from "./state.js";
|
|
5
|
+
import { curRange, renderExport } from "./plot.js";
|
|
6
|
+
import { downloadBlob, tstamp, toast } from "./util.js";
|
|
7
|
+
import { forceReconnect, getConnState, hasSocket } from "./connection.js";
|
|
8
|
+
import { toggleFull } from "./config.js";
|
|
9
|
+
|
|
10
|
+
let pauseBtn=null, pauseLbl=null;
|
|
11
|
+
|
|
12
|
+
/* Pause = HOLD: keep ingesting samples (no data lost) but FREEZE the view by
|
|
13
|
+
pinning the current range. Resume clears the pin → back to the live window. */
|
|
14
|
+
export function togglePause(){
|
|
15
|
+
state.paused=!state.paused;
|
|
16
|
+
if(state.paused){
|
|
17
|
+
const [a,b]=curRange();
|
|
18
|
+
state.viewT0=a; state.viewT1=b;
|
|
19
|
+
}else{
|
|
20
|
+
state.viewT0=null; state.viewT1=null;
|
|
21
|
+
}
|
|
22
|
+
pauseBtn.classList.toggle("active",state.paused);
|
|
23
|
+
if(pauseLbl) pauseLbl.textContent=state.paused?"Resume":"Pause";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Reset view: drop manual time AND Y overrides → back to the live window. */
|
|
27
|
+
export function resetView(){ state.viewT0=null; state.viewT1=null; state.viewY0=null; state.viewY1=null; }
|
|
28
|
+
|
|
29
|
+
/* High-contrast, labeled, 2× export (see plot.renderExport) — far more legible
|
|
30
|
+
than dumping the live low-contrast canvas. */
|
|
31
|
+
function exportPng(){
|
|
32
|
+
try{
|
|
33
|
+
const fsText=document.getElementById("fs")?.textContent||"";
|
|
34
|
+
const off=renderExport(fsText);
|
|
35
|
+
off.toBlob(b=>{ if(b) downloadBlob("trace-"+tstamp()+".png",b); else toast("PNG export failed"); });
|
|
36
|
+
}catch(_){ toast("PNG export failed"); }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Top-bar "i" help popover (replaces the canvas tooltip that blocked the view). */
|
|
40
|
+
function initInfo(){
|
|
41
|
+
const info=document.getElementById("info");
|
|
42
|
+
const help=document.getElementById("help");
|
|
43
|
+
if(!info||!help)return;
|
|
44
|
+
info.onclick=(e)=>{ e.stopPropagation(); help.hidden=!help.hidden; };
|
|
45
|
+
document.addEventListener("click",(e)=>{
|
|
46
|
+
if(!help.hidden && e.target!==info && !help.contains(e.target)) help.hidden=true;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* CSV: wide format over the visible window. Sample frames share one timestamp
|
|
51
|
+
across all signals (PROTOCOL §10), so rows key on `t`, columns = signals. */
|
|
52
|
+
function exportCsv(){
|
|
53
|
+
const names=[...sigs.keys()];
|
|
54
|
+
if(!names.length){ toast("nothing to export","info"); return; }
|
|
55
|
+
const [t0,t1]=curRange();
|
|
56
|
+
const rowsByT=new Map();
|
|
57
|
+
for(const n of names) for(const p of sigs.get(n).data){
|
|
58
|
+
if(p.t<t0||p.t>t1)continue;
|
|
59
|
+
let row=rowsByT.get(p.t); if(!row){row={};rowsByT.set(p.t,row);}
|
|
60
|
+
row[n]=p.v;
|
|
61
|
+
}
|
|
62
|
+
if(!rowsByT.size){ toast("no samples in view","info"); return; }
|
|
63
|
+
const ts=[...rowsByT.keys()].sort((a,b)=>a-b);
|
|
64
|
+
const q=v=>{const s=String(v); return /[",\n]/.test(s)?'"'+s.replace(/"/g,'""')+'"':s;};
|
|
65
|
+
let csv="t,"+names.map(q).join(",")+"\n";
|
|
66
|
+
for(const t of ts){ const row=rowsByT.get(t); csv+=t+","+names.map(n=>n in row?row[n]:"").join(",")+"\n"; }
|
|
67
|
+
downloadBlob("trace-"+tstamp()+".csv",new Blob([csv],{type:"text/csv"}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function initToolbar(){
|
|
71
|
+
pauseBtn=document.getElementById("pause");
|
|
72
|
+
pauseLbl=pauseBtn.querySelector(".lbl");
|
|
73
|
+
pauseBtn.onclick=togglePause;
|
|
74
|
+
document.getElementById("auto").onclick=resetView;
|
|
75
|
+
document.getElementById("expPng").onclick=exportPng;
|
|
76
|
+
document.getElementById("expCsv").onclick=exportCsv;
|
|
77
|
+
document.getElementById("reconnect").onclick=()=>forceReconnect();
|
|
78
|
+
initInfo();
|
|
79
|
+
|
|
80
|
+
// Keyboard shortcuts — inert while typing in an input/select/textarea.
|
|
81
|
+
document.addEventListener("keydown",e=>{
|
|
82
|
+
if(e.key==="Escape"){ const help=document.getElementById("help"); if(help) help.hidden=true; }
|
|
83
|
+
const tag=(e.target&&e.target.tagName)||"";
|
|
84
|
+
if(/^(INPUT|SELECT|TEXTAREA)$/.test(tag))return;
|
|
85
|
+
if(e.metaKey||e.ctrlKey||e.altKey)return;
|
|
86
|
+
if(e.key===" "){ e.preventDefault(); togglePause(); }
|
|
87
|
+
else if(e.key==="r"||e.key==="R"){ resetView(); }
|
|
88
|
+
else if(e.key==="f"||e.key==="F"){ toggleFull(); }
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Tab backgrounded + socket died → retry as soon as it's visible again.
|
|
92
|
+
document.addEventListener("visibilitychange",()=>{
|
|
93
|
+
if(document.visibilityState==="visible" && !hasSocket() && getConnState()!=="connected") forceReconnect();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/* Pure helpers + the toast notifier. No app state — safe to import anywhere. */
|
|
2
|
+
|
|
3
|
+
export function esc(n){ return String(n).replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c])); }
|
|
4
|
+
export function cssid(n){ return n.replace(/[^a-z0-9]/gi,"_"); }
|
|
5
|
+
export function cssAttr(n){ return n.replace(/"/g,'\\"'); }
|
|
6
|
+
|
|
7
|
+
/* Expand a #rgb / #rrggbb color to the 6-digit form an <input type=color>
|
|
8
|
+
requires (palette literals are 3-digit shorthand). Non-hex → safe default. */
|
|
9
|
+
export function hex6(c){
|
|
10
|
+
if(typeof c!=="string")return "#4488ff";
|
|
11
|
+
const m=/^#([0-9a-f]{3})$/i.exec(c);
|
|
12
|
+
if(m){const s=m[1];return "#"+s[0]+s[0]+s[1]+s[1]+s[2]+s[2];}
|
|
13
|
+
if(/^#[0-9a-f]{6}$/i.test(c))return c;
|
|
14
|
+
return "#4488ff";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Compact numeric formatter for readouts/stats. */
|
|
18
|
+
export function fmt(x){
|
|
19
|
+
if(x===null||x===undefined||!isFinite(x))return "—";
|
|
20
|
+
const a=Math.abs(x);
|
|
21
|
+
if(a!==0&&(a<0.01||a>=1e6))return x.toExponential(2);
|
|
22
|
+
if(Number.isInteger(x))return String(x);
|
|
23
|
+
return x.toFixed(a<1?4:2);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Binary-search the sample nearest a timestamp (data is time-sorted). Used by
|
|
27
|
+
the crosshair readout. Returns null for an empty series. */
|
|
28
|
+
export function nearestSample(d,t){
|
|
29
|
+
if(!d.length)return null;
|
|
30
|
+
if(t<=d[0].t)return d[0];
|
|
31
|
+
const last=d[d.length-1]; if(t>=last.t)return last;
|
|
32
|
+
let lo=0,hi=d.length-1;
|
|
33
|
+
while(lo<hi){const mid=(lo+hi)>>1; if(d[mid].t<t)lo=mid+1; else hi=mid;}
|
|
34
|
+
const b=d[lo],a=d[lo-1]||b;
|
|
35
|
+
return (t-a.t<=b.t-t)?a:b;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Trigger a client-side file download from a Blob. */
|
|
39
|
+
export function downloadBlob(name,blob){
|
|
40
|
+
const a=document.createElement("a");
|
|
41
|
+
const u=URL.createObjectURL(blob);
|
|
42
|
+
a.href=u;a.download=name;document.body.appendChild(a);a.click();
|
|
43
|
+
setTimeout(()=>{a.remove();URL.revokeObjectURL(u);},0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Local timestamp for export filenames (YYYYMMDD-HHMMSS). */
|
|
47
|
+
export function tstamp(){
|
|
48
|
+
const d=new Date();const p=n=>String(n).padStart(2,"0");
|
|
49
|
+
return d.getFullYear()+p(d.getMonth()+1)+p(d.getDate())+"-"+p(d.getHours())+p(d.getMinutes())+p(d.getSeconds());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Transient toast (unresolved specs / errors / info). */
|
|
53
|
+
export function toast(msg,kind){
|
|
54
|
+
const box=document.getElementById("toast");
|
|
55
|
+
if(!box)return;
|
|
56
|
+
const el=document.createElement("div");
|
|
57
|
+
el.className="t"+(kind==="info"?" info":"");
|
|
58
|
+
el.textContent=msg;
|
|
59
|
+
box.appendChild(el);
|
|
60
|
+
setTimeout(()=>{el.style.opacity="0";el.style.transition="opacity .4s";},kind==="info"?2500:4500);
|
|
61
|
+
setTimeout(()=>el.remove(),kind==="info"?2900:4900);
|
|
62
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/* Unified "Signals" panel: add row with a custom autocomplete dropdown, presets,
|
|
2
|
+
and the live signal list (color / show-hide / name / encoding / remove / stats).
|
|
3
|
+
Replaces the old native <datalist> (clunky, whole-field matching) and merges
|
|
4
|
+
the former separate "Signals & stats" panel into one place.
|
|
5
|
+
|
|
6
|
+
Rows are built with DOM nodes + textContent (signal names are server/spec
|
|
7
|
+
strings) so nothing is interpolated into innerHTML. */
|
|
8
|
+
|
|
9
|
+
import { sigs, bus } from "./state.js";
|
|
10
|
+
import { cssid, hex6 } from "./util.js";
|
|
11
|
+
import { addSpecs, removeSignal, wsSend } from "./connection.js";
|
|
12
|
+
import { getSuggestList, symMeta, baseName, metaHint } from "./symbols.js";
|
|
13
|
+
|
|
14
|
+
const PRESETS=[
|
|
15
|
+
["ADC raw","s_adc_raw[0],s_adc_raw[1],s_adc_raw[2],s_adc_raw[3]"],
|
|
16
|
+
["Voltages","s_vbat_mv,s_vbus_stm_mv,s_vbus_esp_mv"],
|
|
17
|
+
["Pads","s_inputs[1],s_inputs[2]"],
|
|
18
|
+
["Pad pressure","s_inputs[3],s_inputs[4],s_inputs[5],s_inputs[6]"],
|
|
19
|
+
["Encoder","s_inputs[0]"],
|
|
20
|
+
["Buttons","s_inputs[44]"],
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
let inp=null, ac=null, list=null, empty=null;
|
|
24
|
+
let acItems=[]; // current dropdown specs
|
|
25
|
+
let acSel=-1; // highlighted index (-1 = none)
|
|
26
|
+
|
|
27
|
+
/* ---------- live signal list ---------- */
|
|
28
|
+
function rowFor(name, s){
|
|
29
|
+
const row=document.createElement("div");
|
|
30
|
+
row.className="sig"+(s.on?"":" off");
|
|
31
|
+
|
|
32
|
+
const color=document.createElement("input");
|
|
33
|
+
color.type="color"; color.className="sw"; color.value=hex6(s.color); color.title="signal color";
|
|
34
|
+
color.oninput=()=>{ s.color=color.value; };
|
|
35
|
+
|
|
36
|
+
const cb=document.createElement("input");
|
|
37
|
+
cb.type="checkbox"; cb.checked=!!s.on; cb.title="show / hide";
|
|
38
|
+
cb.onchange=()=>{ s.on=cb.checked; row.classList.toggle("off",!s.on); };
|
|
39
|
+
|
|
40
|
+
const nm=document.createElement("span");
|
|
41
|
+
nm.className="signame"; nm.textContent=name; nm.title="click to show / hide";
|
|
42
|
+
nm.onclick=()=>{ s.on=!s.on; cb.checked=s.on; row.classList.toggle("off",!s.on); };
|
|
43
|
+
|
|
44
|
+
const enc=document.createElement("span");
|
|
45
|
+
enc.className="enc"; if(s.encoding) enc.textContent=s.encoding;
|
|
46
|
+
|
|
47
|
+
const rm=document.createElement("span");
|
|
48
|
+
rm.className="rm"; rm.textContent="×"; rm.title="remove";
|
|
49
|
+
rm.onclick=()=>removeSignal(name);
|
|
50
|
+
|
|
51
|
+
const head=document.createElement("div");
|
|
52
|
+
head.className="sighead";
|
|
53
|
+
head.append(color, cb, nm, enc, rm);
|
|
54
|
+
|
|
55
|
+
const stats=document.createElement("div");
|
|
56
|
+
stats.className="stats"; stats.id="st-"+cssid(name);
|
|
57
|
+
|
|
58
|
+
row.append(head, stats);
|
|
59
|
+
return row;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Rebuild the list to match the current sigs map (assigned to bus.rebuildList). */
|
|
63
|
+
function buildList(){
|
|
64
|
+
if(!list)return;
|
|
65
|
+
list.textContent="";
|
|
66
|
+
for(const [name,s] of sigs) list.appendChild(rowFor(name,s));
|
|
67
|
+
if(empty) empty.style.display = sigs.size ? "none" : "block";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* ---------- custom autocomplete dropdown ---------- */
|
|
71
|
+
/* Active comma-token around the caret; preserves earlier/later tokens. */
|
|
72
|
+
function tokenCtx(){
|
|
73
|
+
const val=inp.value;
|
|
74
|
+
const caret=(inp.selectionStart!=null)?inp.selectionStart:val.length;
|
|
75
|
+
const start=val.lastIndexOf(",",caret-1)+1;
|
|
76
|
+
let end=val.indexOf(",",caret); if(end<0)end=val.length;
|
|
77
|
+
return { prefix:val.slice(0,start), token:val.slice(start,end).trim(), suffix:val.slice(end) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function matchesFor(token){
|
|
81
|
+
const all=getSuggestList();
|
|
82
|
+
if(!token) return all.slice(0,200);
|
|
83
|
+
const tl=token.toLowerCase(), pref=[], sub=[];
|
|
84
|
+
for(const sgn of all){
|
|
85
|
+
const sl=sgn.toLowerCase();
|
|
86
|
+
if(sl.startsWith(tl)) pref.push(sgn);
|
|
87
|
+
else if(sl.indexOf(tl)>=0) sub.push(sgn);
|
|
88
|
+
}
|
|
89
|
+
return pref.concat(sub).slice(0,200);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function renderDropdown(){
|
|
93
|
+
if(!ac)return;
|
|
94
|
+
const { token }=tokenCtx();
|
|
95
|
+
acItems=matchesFor(token);
|
|
96
|
+
acSel=-1;
|
|
97
|
+
ac.textContent="";
|
|
98
|
+
if(!acItems.length){ ac.hidden=true; return; }
|
|
99
|
+
acItems.forEach((spec,i)=>{
|
|
100
|
+
const opt=document.createElement("div");
|
|
101
|
+
opt.className="ac-opt";
|
|
102
|
+
const nm=document.createElement("span"); nm.className="ac-spec"; nm.textContent=spec;
|
|
103
|
+
opt.appendChild(nm);
|
|
104
|
+
const hint=metaHint(spec);
|
|
105
|
+
if(hint){ const h=document.createElement("span"); h.className="ac-hint"; h.textContent=hint; opt.appendChild(h); }
|
|
106
|
+
// mousedown (not click) so it fires before the input blur closes the list.
|
|
107
|
+
opt.addEventListener("mousedown",(e)=>{ e.preventDefault(); accept(i); });
|
|
108
|
+
ac.appendChild(opt);
|
|
109
|
+
});
|
|
110
|
+
ac.hidden=false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function highlight(i){
|
|
114
|
+
const opts=ac.children;
|
|
115
|
+
if(acSel>=0&&opts[acSel]) opts[acSel].classList.remove("sel");
|
|
116
|
+
acSel=i;
|
|
117
|
+
if(acSel>=0&&opts[acSel]){ opts[acSel].classList.add("sel"); opts[acSel].scrollIntoView({block:"nearest"}); }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Accept suggestion `i`: replace the active token, keep focus, re-suggest. */
|
|
121
|
+
function accept(i){
|
|
122
|
+
const spec=acItems[i]; if(spec==null)return;
|
|
123
|
+
const { prefix, suffix }=tokenCtx();
|
|
124
|
+
const lead=prefix.length?(prefix.replace(/\s*$/,"")+" "):"";
|
|
125
|
+
inp.value=lead+spec+suffix;
|
|
126
|
+
const caret=lead.length+spec.length;
|
|
127
|
+
inp.focus(); inp.setSelectionRange(caret,caret);
|
|
128
|
+
renderDropdown();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function closeDropdown(){ if(ac){ ac.hidden=true; acSel=-1; } }
|
|
132
|
+
|
|
133
|
+
function submitAdd(){
|
|
134
|
+
const specs=inp.value.split(",");
|
|
135
|
+
if(specs.some(s=>s.trim())){ addSpecs(specs); inp.value=""; }
|
|
136
|
+
closeDropdown();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* ---------- init ---------- */
|
|
140
|
+
export function initWatchlist(){
|
|
141
|
+
inp=document.getElementById("addInput");
|
|
142
|
+
ac=document.getElementById("ac");
|
|
143
|
+
list=document.getElementById("siglist");
|
|
144
|
+
empty=document.getElementById("noSigs");
|
|
145
|
+
|
|
146
|
+
document.getElementById("addBtn").onclick=submitAdd;
|
|
147
|
+
|
|
148
|
+
inp.addEventListener("input",renderDropdown);
|
|
149
|
+
inp.addEventListener("click",renderDropdown);
|
|
150
|
+
inp.addEventListener("focus",renderDropdown);
|
|
151
|
+
inp.addEventListener("blur",()=>setTimeout(closeDropdown,120));
|
|
152
|
+
inp.addEventListener("keydown",(e)=>{
|
|
153
|
+
if(ac && !ac.hidden && acItems.length){
|
|
154
|
+
if(e.key==="ArrowDown"){ e.preventDefault(); highlight((acSel+1)%acItems.length); return; }
|
|
155
|
+
if(e.key==="ArrowUp"){ e.preventDefault(); highlight((acSel-1+acItems.length)%acItems.length); return; }
|
|
156
|
+
if(e.key==="Escape"){ closeDropdown(); return; }
|
|
157
|
+
if(e.key==="Enter" && acSel>=0){ e.preventDefault(); accept(acSel); return; }
|
|
158
|
+
}
|
|
159
|
+
if(e.key==="Enter"){ submitAdd(); }
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// presets
|
|
163
|
+
const box=document.getElementById("presets");
|
|
164
|
+
for(const [label,specs] of PRESETS){
|
|
165
|
+
const b=document.createElement("button");
|
|
166
|
+
b.textContent=label; b.title=specs;
|
|
167
|
+
b.onclick=()=>addSpecs(specs.split(","));
|
|
168
|
+
box.appendChild(b);
|
|
169
|
+
}
|
|
170
|
+
document.getElementById("clearAll").onclick=()=>{
|
|
171
|
+
const names=[...sigs.keys()];
|
|
172
|
+
if(names.length) wsSend({cmd:"remove",signals:names});
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// register view hooks + initial paint
|
|
176
|
+
bus.rebuildList=buildList;
|
|
177
|
+
bus.onSymbols=()=>{ if(ac && !ac.hidden) renderDropdown(); };
|
|
178
|
+
buildList();
|
|
179
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# CrossPad SWD Tracer Bridge
|
|
2
|
+
|
|
3
|
+
Tiny VS Code extension that lets the `crosspad-mcp` SWD tracer open its live
|
|
4
|
+
dashboard **inside VS Code** (a Simple Browser editor tab) instead of the system
|
|
5
|
+
browser.
|
|
6
|
+
|
|
7
|
+
VS Code exposes no CLI / external command to open its in-editor browser, but it
|
|
8
|
+
does register itself as the OS handler for the `vscode://` scheme. This extension
|
|
9
|
+
registers a URI handler for its own id, so the tracer can fire:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
vscode://crosspad.swd-tracer-bridge/open?url=http%3A%2F%2Flocalhost%3A7373%2F
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
and the extension opens that URL in a Simple Browser tab.
|
|
16
|
+
|
|
17
|
+
## Install (local)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
cd vscode-extension
|
|
21
|
+
npx --yes @vscode/vsce package # → swd-tracer-bridge-0.1.0.vsix
|
|
22
|
+
code --install-extension swd-tracer-bridge-0.1.0.vsix
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then set the tracer to VS Code mode (default):
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
crosspad_trace action=config_set key=ui_open value=vscode
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`ui_open`: `vscode` (default — open in a VS Code tab via this bridge) ·
|
|
32
|
+
`browser` (system browser) · `none` (don't auto-open).
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// CrossPad SWD Tracer Bridge — opens the tracer dashboard in a VS Code editor
|
|
2
|
+
// tab in response to a vscode:// deep link fired by the MCP server.
|
|
3
|
+
//
|
|
4
|
+
// Flow: crosspad_trace action=start (ui_open=vscode)
|
|
5
|
+
// → server: xdg-open "vscode://crosspad.swd-tracer-bridge/open?url=<http url>"
|
|
6
|
+
// → OS routes vscode:// to VS Code → activates this extension (onUri)
|
|
7
|
+
// → handleUri() opens the URL in a Simple Browser editor tab.
|
|
8
|
+
//
|
|
9
|
+
// This is the only mechanism that lets an EXTERNAL process open an in-editor
|
|
10
|
+
// browser tab: VS Code has no CLI / generic external command for it, but it does
|
|
11
|
+
// register itself as the OS handler for the vscode:// scheme, and an extension
|
|
12
|
+
// may register a URI handler for its own extension id.
|
|
13
|
+
const vscode = require("vscode");
|
|
14
|
+
|
|
15
|
+
function activate(context) {
|
|
16
|
+
context.subscriptions.push(
|
|
17
|
+
vscode.window.registerUriHandler({
|
|
18
|
+
handleUri(uri) {
|
|
19
|
+
// Expect: vscode://crosspad.swd-tracer-bridge/open?url=<encoded http url>
|
|
20
|
+
let raw = null;
|
|
21
|
+
try {
|
|
22
|
+
raw = new URLSearchParams(uri.query).get("url");
|
|
23
|
+
} catch (_e) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!raw) return;
|
|
27
|
+
// SECURITY: the deep link is invokable by ANY web page / process, so the
|
|
28
|
+
// `url` is untrusted. The only legitimate target is the local tracer
|
|
29
|
+
// dashboard, so hard-restrict to http(s) on a loopback host and reject
|
|
30
|
+
// everything else. Do NOT fall back to env.openExternal — that would let
|
|
31
|
+
// a crafted vscode:// link open arbitrary URIs (file://, mailto:, other
|
|
32
|
+
// schemes) system-wide. Simple Browser is an in-editor webview, safe.
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = new URL(raw);
|
|
36
|
+
} catch (_e) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
|
|
40
|
+
if (!["127.0.0.1", "localhost", "::1"].includes(parsed.hostname)) return;
|
|
41
|
+
const url = parsed.toString();
|
|
42
|
+
Promise.resolve(vscode.commands.executeCommand("simpleBrowser.show", url)).then(
|
|
43
|
+
undefined,
|
|
44
|
+
() =>
|
|
45
|
+
Promise.resolve(
|
|
46
|
+
vscode.commands.executeCommand("simpleBrowser.api.open", vscode.Uri.parse(url)),
|
|
47
|
+
).then(undefined, () => { /* in-editor open failed; do not escalate to external */ }),
|
|
48
|
+
);
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function deactivate() {}
|
|
55
|
+
|
|
56
|
+
module.exports = { activate, deactivate };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "swd-tracer-bridge",
|
|
3
|
+
"displayName": "CrossPad SWD Tracer Bridge",
|
|
4
|
+
"description": "Opens the CrossPad SWD tracer dashboard in a VS Code Simple Browser tab when the tracer fires a vscode:// deep link (so `crosspad_trace action=start` auto-opens the live view inside the editor instead of the system browser).",
|
|
5
|
+
"publisher": "crosspad",
|
|
6
|
+
"version": "0.1.2",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": { "type": "git", "url": "https://github.com/CrossPad/crosspad-mcp.git" },
|
|
9
|
+
"engines": { "vscode": "^1.80.0" },
|
|
10
|
+
"categories": ["Other"],
|
|
11
|
+
"activationEvents": ["onStartupFinished"],
|
|
12
|
+
"main": "./extension.js",
|
|
13
|
+
"contributes": {}
|
|
14
|
+
}
|