v2.8.0 — Code editor tab + context panel improvements
- Add 💻 코드 nav button; code mode shows aside + right-panel - Code mode left sidebar: 파일/모델/설정 tabs (file tree, model selector, Monaco settings) - Monaco editor settings restored from localStorage on init - Server log panel: collapsible (▾ toggle), height fixed to 50% of aside - Context panel compact layout: reduced padding/margins/min-heights throughout so all sections (시스템통계/에이전트컨텍스트/빠른작업/프로세스로그) fit without scrolling - POST /api/code/save, GET /api/code/files, GET /api/code/file, DELETE /api/code/file endpoints - Coder AI: glm-5.1:cloud default, auto-open generated code in Monaco tabs - Code folder browser with fixed-position dropdown (escape overflow clip) - Code editor terminal panel with PTY/xterm.js (Ctrl+` shortcut) - Remove canvas panel; restore chat terminal infrastructure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+37
-23
@@ -163,7 +163,8 @@ let ph = 0; // playhead ticks
|
||||
let playing = false;
|
||||
let midiAccess = null, midiOut = null;
|
||||
let animId = null;
|
||||
let playT0 = 0, playTick0 = 0;
|
||||
let playT0 = 0, playTick0 = 0, nextSchedTick = 0;
|
||||
const SCHED_AHEAD_MS = 300;
|
||||
let velDrag = null;
|
||||
let clip = [];
|
||||
|
||||
@@ -477,8 +478,14 @@ function cycleColor(i) {
|
||||
midi.tracks[i].color=COLORS[(idx+1)%COLORS.length];
|
||||
renderAll(); renderTracks();
|
||||
}
|
||||
function toggleMute(i) { pushH(); midi.tracks[i].muted=!midi.tracks[i].muted; renderAll(); renderTracks(); }
|
||||
function toggleSolo(i) { pushH(); midi.tracks[i].solo=!midi.tracks[i].solo; renderAll(); renderTracks(); }
|
||||
function toggleMute(i) {
|
||||
pushH(); midi.tracks[i].muted=!midi.tracks[i].muted; renderAll(); renderTracks();
|
||||
if(playing){ try{midiOut.clear();}catch{} for(let c=0;c<16;c++) try{midiOut.send(new Uint8Array([0xB0|c,123,0]));}catch{} nextSchedTick=ph; }
|
||||
}
|
||||
function toggleSolo(i) {
|
||||
pushH(); midi.tracks[i].solo=!midi.tracks[i].solo; renderAll(); renderTracks();
|
||||
if(playing){ try{midiOut.clear();}catch{} for(let c=0;c<16;c++) try{midiOut.send(new Uint8Array([0xB0|c,123,0]));}catch{} nextSchedTick=ph; }
|
||||
}
|
||||
function delTrack(i) { pushH(); midi.tracks.splice(i,1); if(activeTi>=midi.tracks.length)activeTi=Math.max(0,midi.tracks.length-1); renderAll(); renderTracks(); }
|
||||
function setTrackProg(i, prog) { pushH(); if(!midi.tracks[i].pcs.length)midi.tracks[i].pcs.push({tick:0,prog}); else midi.tracks[i].pcs[0].prog=prog; renderTracks(); }
|
||||
function doAddTrack() {
|
||||
@@ -781,16 +788,37 @@ function doStop() {
|
||||
playing=false;
|
||||
document.getElementById('play-btn').textContent='▶';
|
||||
if(animId){cancelAnimationFrame(animId);animId=null;}
|
||||
if(midiOut) for(let c=0;c<16;c++) try{midiOut.send(new Uint8Array([0xB0|c,123,0]));}catch{}
|
||||
if(midiOut){
|
||||
try{midiOut.clear();}catch{} // cancel future-scheduled notes
|
||||
for(let c=0;c<16;c++) try{midiOut.send(new Uint8Array([0xB0|c,123,0]));}catch{}
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleChunk(mspt, loopEn, loopOut) {
|
||||
const now = performance.now();
|
||||
const schedUntilTick = playTick0 + (now - playT0 + SCHED_AHEAD_MS) / mspt;
|
||||
const anySolo = midi.tracks.some(t=>t.solo);
|
||||
for(const trk of midi.tracks){
|
||||
if(trk.muted||(anySolo&&!trk.solo)) continue;
|
||||
for(const n of trk.notes){
|
||||
if(n.tick < nextSchedTick) continue;
|
||||
if(n.tick >= schedUntilTick) continue;
|
||||
if(loopEn && n.tick >= loopOut) continue;
|
||||
const on = playT0 + (n.tick - playTick0) * mspt;
|
||||
const off = playT0 + (n.tick+n.dur - playTick0) * mspt;
|
||||
try{midiOut.send(new Uint8Array([0x90|trk.channel,n.pitch,n.vel]), on);}catch{}
|
||||
try{midiOut.send(new Uint8Array([0x80|trk.channel,n.pitch,0]), off);}catch{}
|
||||
}
|
||||
}
|
||||
nextSchedTick = schedUntilTick;
|
||||
}
|
||||
|
||||
function doPlay() {
|
||||
if(!midiOut){alert('MIDI 출력 기기를 선택하세요.');return;}
|
||||
playing=true;
|
||||
document.getElementById('play-btn').textContent='⏸';
|
||||
playTick0=ph; playT0=performance.now();
|
||||
playTick0=ph; playT0=performance.now(); nextSchedTick=ph;
|
||||
const mspt=mspT();
|
||||
const anySolo=midi.tracks.some(t=>t.solo);
|
||||
const loopEn=document.getElementById('loop-en').checked;
|
||||
const loopIn=(+document.getElementById('loop-in').value-1)*midi.tpq*(midi.timeSigs[0]?.num||4);
|
||||
const loopOut=(+document.getElementById('loop-out').value)*midi.tpq*(midi.timeSigs[0]?.num||4);
|
||||
@@ -805,30 +833,16 @@ function doPlay() {
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule notes
|
||||
const t0=performance.now()+200;
|
||||
for(const trk of midi.tracks){
|
||||
if(trk.muted||(anySolo&&!trk.solo)) continue;
|
||||
for(const n of trk.notes){
|
||||
if(n.tick<playTick0) continue;
|
||||
if(loopEn&&n.tick>=loopOut) continue;
|
||||
const on =(n.tick-playTick0)*mspt+t0;
|
||||
const off=(n.tick+n.dur-playTick0)*mspt+t0;
|
||||
try{midiOut.send(new Uint8Array([0x90|trk.channel,n.pitch,n.vel]),on);}catch{}
|
||||
try{midiOut.send(new Uint8Array([0x80|trk.channel,n.pitch,0]),off);}catch{}
|
||||
}
|
||||
}
|
||||
|
||||
const maxTick=Math.max(0,...midi.tracks.flatMap(t=>t.notes.map(n=>n.tick+n.dur)));
|
||||
|
||||
function loop(){
|
||||
if(!playing) return;
|
||||
const elapsed=(performance.now()-playT0-200);
|
||||
ph=playTick0+elapsed/mspt;
|
||||
const now=performance.now();
|
||||
ph=playTick0+(now-playT0)/mspt;
|
||||
if(loopEn&&ph>=loopOut){ doStop(); ph=loopIn; doPlay(); return; }
|
||||
if(ph>=maxTick){ doStop(); return; }
|
||||
scheduleChunk(mspt, loopEn, loopOut);
|
||||
updPos();
|
||||
// Auto-scroll playhead into view
|
||||
const phx=tX(ph);
|
||||
if(phx>rollC.width*.8) view.scrollX+=rollC.width*.4;
|
||||
else if(phx<20&&view.scrollX>0) view.scrollX=Math.max(0,view.scrollX-rollC.width*.4);
|
||||
|
||||
Reference in New Issue
Block a user