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:
kim
2026-05-22 12:20:16 +09:00
co-authored by Claude Sonnet 4.6
parent 7ebf8cf86f
commit eaf6678a21
15 changed files with 1114 additions and 916 deletions
+898 -867
View File
File diff suppressed because it is too large Load Diff
+37 -23
View File
@@ -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);