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
+2 -2
View File
@@ -23,7 +23,7 @@ function runPython(script: string, timeoutMs = 60_000): Promise<any> {
env: { ...process.env },
});
let out = '';
child.stdout.on('data', (d: Buffer) => { out += d.toString(); });
child.stdout.on('data', (d: Buffer) => { out += d.toString('utf8'); });
child.stderr.on('data', () => {});
child.on('close', () => {
try { resolve(JSON.parse(out || '{}')); } catch { resolve({ error: 'Failed to parse output', raw: out.slice(0, 500) }); }
@@ -110,7 +110,7 @@ export const imageReadTool = {
env: { ...process.env },
});
let out = '';
child.stdout.on('data', (d: Buffer) => { out += d.toString(); });
child.stdout.on('data', (d: Buffer) => { out += d.toString('utf8'); });
child.on('close', () => {
try { resolve(JSON.parse(out || '{}')); } catch { resolve({ error: 'Failed to parse OCR output' }); }
});
+2 -2
View File
@@ -113,10 +113,10 @@ export const pythonEvalTool = {
}, timeoutSec * 1000);
child.stdout.on('data', (d: Buffer) => {
stdout += d.toString();
stdout += d.toString('utf8');
if (stdout.length > 50_000) child.kill('SIGKILL');
});
child.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
child.stderr.on('data', (d: Buffer) => { stderr += d.toString('utf8'); });
child.on('close', (code: number | null) => {
if (settled) return;
settled = true;
+4 -3
View File
@@ -135,6 +135,7 @@ export async function executeProposeRepair(args: ProposeRepairArgs): Promise<Too
execSync(`git apply --check --whitespace=nowarn "${tmpPatch}"`, {
cwd: root,
stdio: 'pipe',
encoding: 'utf8',
});
} catch (checkErr: any) {
const details = String(checkErr?.stderr || checkErr?.stdout || checkErr?.message || 'unknown').trim();
@@ -257,7 +258,7 @@ export async function applyApprovedRepair(repairId: string): Promise<ApplyRepair
// Step 1: Final check before apply
try {
execSync(`git apply --check --whitespace=nowarn "${tmpPatch}"`, { cwd: root, stdio: 'pipe' });
execSync(`git apply --check --whitespace=nowarn "${tmpPatch}"`, { cwd: root, stdio: 'pipe', encoding: 'utf8' });
} catch (checkErr: any) {
const details = String(checkErr?.stderr || checkErr?.message || '').slice(0, 500);
repair.status = 'failed';
@@ -271,7 +272,7 @@ export async function applyApprovedRepair(repairId: string): Promise<ApplyRepair
}
// Step 2: Apply the patch
execSync(`git apply --whitespace=nowarn "${tmpPatch}"`, { cwd: root, stdio: 'pipe' });
execSync(`git apply --whitespace=nowarn "${tmpPatch}"`, { cwd: root, stdio: 'pipe', encoding: 'utf8' });
repair.status = 'approved';
savePendingRepair(repair);
@@ -307,7 +308,7 @@ export async function applyApprovedRepair(repairId: string): Promise<ApplyRepair
const revertPatch = path.join(os.tmpdir(), `smallclaw-repair-revert-${Date.now()}.patch`);
try {
fs.writeFileSync(revertPatch, repair.patch, 'utf-8');
execSync(`git apply --reverse --whitespace=nowarn "${revertPatch}"`, { cwd: root, stdio: 'pipe' });
execSync(`git apply --reverse --whitespace=nowarn "${revertPatch}"`, { cwd: root, stdio: 'pipe', encoding: 'utf8' });
} catch {
// Revert also failed — leave a note
repair.buildOutput += '\n\n⚠️ Auto-revert also failed. Source may be in a modified state.';
+3 -3
View File
@@ -43,7 +43,7 @@ async function convertAudioToWav(
outputPath,
]);
let stderr = '';
ffmpeg.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
ffmpeg.stderr.on('data', (d: Buffer) => { stderr += d.toString('utf8'); });
ffmpeg.on('close', (code) => {
if (code === 0) resolve();
else reject(new Error(`ffmpeg exited ${code}: ${stderr}`));
@@ -85,8 +85,8 @@ export async function transcribeAudio(
const whisper = spawn(config.whisperPath, args);
let stdout = '';
let stderr = '';
whisper.stdout.on('data', (d: Buffer) => { stdout += d.toString(); });
whisper.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
whisper.stdout.on('data', (d: Buffer) => { stdout += d.toString('utf8'); });
whisper.stderr.on('data', (d: Buffer) => { stderr += d.toString('utf8'); });
whisper.on('close', (code) => {
if (code !== 0) {
reject(new Error(`whisper-cli exited ${code}: ${stderr.slice(0, 500)}`));
+2 -2
View File
@@ -49,7 +49,7 @@ export async function synthesizeSpeech(text: string): Promise<Buffer> {
await new Promise<void>((resolve, reject) => {
const piper = spawn(config.piperPath, piperArgs);
let stderr = '';
piper.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
piper.stderr.on('data', (d: Buffer) => { stderr += d.toString('utf8'); });
piper.stdin.write(truncatedText);
piper.stdin.end();
piper.on('close', (code) => {
@@ -68,7 +68,7 @@ export async function synthesizeSpeech(text: string): Promise<Buffer> {
oggPath,
]);
let stderr = '';
ffmpeg.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
ffmpeg.stderr.on('data', (d: Buffer) => { stderr += d.toString('utf8'); });
ffmpeg.on('close', (code) => {
if (code === 0) resolve();
else reject(new Error(`ffmpeg exited ${code}: ${stderr}`));