- Separate system prompt for local vs cloud models via isLocal flag (isLocalBackendURL, buildSystemPrompt(..., isLocal), propagate through createSession/createSessionFromRecord/compactSession/spawnSubAgent) - Increase MAX_EMPTY_RESPONSE_RETRIES from 1 to 3 for cloud model resilience - Upgrade mouse input: full SGR-1006 parsing with col/row/pressed/modifiers, logicalButton() helper, and copyToClipboard() via OSC 52 - Add temporary mouse debug logging in App.tsx - Increase DEFAULT_MAX_ITERATIONS from 100 to 300 - Update README mouse/scrollback docs, tweak diff-remove color
33 KiB
locode 업그레이드 작업 메모
프로젝트 개요
locode — Claude Code의 설계 철학을 가져와 로컬 모델(Ollama/LM Studio)용으로 재구현한 에이전트 코딩 CLI. TypeScript + Ink(React-for-CLI) 기반. 백엔드는 OpenAI 호환 /v1/chat/completions 엔드포인트 사용.
- 핵심 철학: "신뢰할 수 없고 느리고 비전/툴콜 지원이 불확실한 로컬 모델"이라는 현실에 맞춰 모든 가정을 비관적으로 재단
- Claude Code 플러그인 포맷을 직접 소비하는 하위호환 브리지 (
.claude-plugin/plugin.json, commands/agents/skills/hooks/MCP)
빌드/테스트 상태
npm run build(tsup) — 깨끗npx tsc --noEmit— 깨끗npm test(vitest) — 29 파일 215개 전부 통과- 파일 인코딩: CRLF (edit_file 도구가 LF로 정규화해서 매칭 실패 → Python 스크립트로 바이너리 편집해야 함)
발견한 업그레이드 후보 (12개)
| # | 항목 | 난이도 | 효과 | 로컬 특화 | 상태 |
|---|---|---|---|---|---|
| 1 | 병렬 툴 실행 (read-only) | 중 | 대 | ★ | 완료 ✅ |
| 2 | 재시도 정책 설정화 (maxRetries) |
하 | 중 | ★ | 완료 ✅ |
| 3 | 정확한 토큰 추정 (/api/tokenize 또는 BPE) |
중 | 대 | ★★ | 완료 ✅ |
| 4 | 스마트 출력 캡 (head+tail, 라인 길이) | 하 | 중 | ★ | 완료 ✅ |
| 5 | 부분 히스토리 보존 컴팩션 | 상 | 대 | ★ | 완료 ✅ |
| 6 | 동적 max_tokens |
하 | 대 | ★ | 완료 ✅ |
| 7 | 툴 설명 풍부화 + 동적 툴 선택 | 중 | 중 | ★ | 완료 ✅ |
| 8 | MCP 연결 재시도·재연결 | 중 | 중 | 완료 ✅ | |
| 9 | 컨텍스트 윈도우 캐시 TTL | 하 | 중 | ★ | 완료 ✅ |
| 10 | edit_file 유사 매치 제안 |
중 | 대 | ★★ | 완료 ✅ |
| 11 | git_status 출력 head+tail | 하 | 중 | 완료 ✅ | |
| 12 | auto-accept 모드 세분화 |
중 | 중 | 완료 ✅ |
✅ 완료: #6 동적 max_tokens
문제
src/agent/loop.ts의 두 생성 요청이 max_tokens: 4096 하드코딩:
- 729행: 스트리밍 요청 (
session.client.chat.completions.create,stream: true) - 840행(→이제 856행): 비스트리밍 재시도 (native 툴콜 인자가 깨졌을 때)
로컬 모델이 파일을 통째로 다시 쓸 때(fallback 모드에서 정밀 edit이 어려워 흔함) 4096 토큰으로 부족 → 응답 중간 잘림 → 툴콜 JSON 불완전 → malformed 에러 반복. 이게 "자꾸 에러가 나"던 원인.
해결
shouldAutoCompact 뒤(157행 근처)에 resolveMaxTokens(session) 헬퍼 추가:
function resolveMaxTokens(session: Session): number {
const MARGIN = 512;
const MIN = 2048;
const available = session.contextWindow - session.lastContextTokens - MARGIN;
return Math.max(MIN, Math.min(available, session.contextWindow));
}
두 사이트 모두 max_tokens: 4096 → max_tokens: resolveMaxTokens(session) 교체.
동작
contextWindow − lastContextTokens − 512를 출력 예산으로 할당- 하한 2048: 컨텍스트 거의 찼어도 최소 출력 보장
- 상한
contextWindow: 윈도우 커도 그 이상 요구 안 함 - 32k 윈도우 / 8k 사용 중 → 약 23k 출력 (기존 4096의 5.7배)
- 8k 윈도우 / 6k 사용 중 → 2048 (기존과 동일)
남겨둔 것
compactSession의max_tokens: 1024(loop.ts:186) — 짧은 산문 요약용이라 동적 계산 불필요, 그대로 유지capabilityProbe.ts의max_tokens: 200— 핑 툴용, 그대로 유지
검증
tsc --noEmit✓npm run build✓ (217.48 KB)npm test✓ 27파일 179개 전부 통과
편집 메모
- 파일이 CRLF라 edit_file 도구가 매칭 실패함 →
src/agent/_patch.py임시 스크립트로 바이너리 교체 후 삭제 - 향후 이 프로젝트 edit_file 시도 전
file <path>로 인코딩 확인; CRLF면 Python 바이너리 편집 또는sed사용
✅ 완료: #3 정확한 토큰 추정 (스크립트 인식 휴리스틱)
문제
src/utils/tokens.ts의 estimateTokens가 JSON.stringify(messages).length / 4 — 두 가지 실패 모드:
- JSON 직렬화 오버헤드(따옴표, 중괄호, 이스케이프)를 콘텐츠 토큰으로 계산 → 추정치 15–25% 부풀림 (모델에 안 보내는 것들)
- 동일한 chars/token 비율을 모든 스크립트에 적용 → 영어 산문 ~4, 코드/기호 ~3.5, CJK(한국어/중국어/일본어) ~1.5인데 무시 → CJK 컨텍스트 과소평가, 컴팩션 타이밍 부정확
실제 usage가 오면 이미 정확하지만(lastContextTokensIsEstimate = false), 추정은 첫 턴 전/컴팩션 직후/서브에이전트 생성 시 사용 → 이 시점의 부정확이 컴팩션 타이밍을 빗나가게 함.
해결
tokens.ts를 메시지 구조 순회 + 스크립트 인식 휴리스틱으로 재작성:
- PER_MESSAGE_OVERHEAD = 4: 채팅 템플릿이 각 메시지에 추가하는 역할/구분자 토큰(~3–5) 반영
- 메시지별 콘텐츠 순회: 시스템/사용자/어시스턴트 텍스트, tool_calls 구조, tool 결과를 JSON이 아닌 모델이 실제로 보는 텍스트로 추출
- 스크립트 인식 가중치 (
weightedChars):- CJK(히라가나/가타카나/한자/한글) ×2.4 → ~1.5 chars/token (각 코드 포인트가 보통 자체 BPE 토큰)
- 조밀 기호(구두점/연산자/괄호, 코드에 흔함) ×1.15 → ~3.5 chars/token
- 라틴 기본 ×1 → ~4 chars/token
- 멀티파트 콘텐츠: 텍스트 파트는 텍스트, 이미지/오디오 파트는 flat 8 토큰(base64가 아닌 placeholder 토큰)
- 동기 순수 추정 유지(백엔드 호출 없음) → 첫 턴/컴팩션/서브에이전트에 안전
Ollama /api/tokenize는 백엔드 분기 + 매 턴 지연이 필요해 제외(의존성·복잡도 대비 효과 부족). 휴리스틱 개선으로 즉시 효과.
동작
- 한국어 메시지 40자: 기존 10 토큰 → ~27 토큰 (실제에 가까움)
- 코드/기호: 기존보다 약간 높게 → 컴팩션 조기 트리거 (OOM 방지)
- 영어 산문: 기존과 유사하되 JSON 오버헤드 제거 → 약간 낮아짐
- 구조(tool_calls, 멀티파트) 비용 반영
검증
tsc --noEmit✓npm run build✓ (224.26 KB)npm test✓ 29파일 203개 전부 통과 (신규 9개: tokens.test.ts)loop.test.ts는lastContextTokens직접 설정 → 추정값 변화에 영향 없음 확인
편집 메모
tokens.tsCRLF → write_file + Python 변환tokens.test.tsLF → edit_file 사용ChatCompletionMessageParam멀티파트 타입 캐스트as unknown as필요 (OpenAI 타입 narrow)
남겨둔 것
- Ollama
/api/tokenize캐싱: 백엔드별 분기 + 비동기 필요 → 별도 작업. 현재 휴리스틱으로 충분히 개선됨 - 실제 usage 도착 후에는 항상 정확한 값 사용(
updateContextTracking의lastContextTokensIsEstimate = false)
✅ 완료: #4 스마트 출력 캡 (head+tail 보존)
문제
src/utils/truncate.ts의 truncate()가 head만 보존. 긴 명령 출력에서 tail의 에러/상태 줄이 잘림 → 모델이 실패 원인을 못 봄. 특히 로컬 모델에서 bash/git 출력이 길면 마지막 에러 메시지가 사라져 디버깅 불가.
해결
truncate.ts를 head+tail 보존(중간 생략)으로 재작성:
- 라인 단위로 잘라 가독성 유지 (반 줄 잘림 방지)
- 예산의 60% head, 40% tail 할당 (tail이 에러/상태 줄을 담는 경우가 많아 비중 높임)
- head/tail 오버랩 가드 (예산 초과가 적을 때 중복 라인 방지)
- 모든 라인이 예산보다 길면 문자 단위 폴백
- 생략된 문자 수 + 보존된 head/tail 라인 수 표시
적용 범위
truncate() 시그니처 유지 → 모든 기존 호출자 자동 개선:
bash.ts(stdout/stderr) — 가장 큰 효과git.ts(status/diff/log/show/branches 출력) — #11도 함께 해결bashOutput.ts,backgroundJobs.ts,webFetch.ts,importFile.ts
readFile.ts는 자체 페이지네이션(nextOffset)을 쓰므로 그대로 유지. grep.ts/listFiles.ts는 자체 limit 잘라내기 사용.
동작
- 짧으면 그대로 반환
- 길면 head 일부 +
... [truncated N more characters — middle omitted, X head + Y tail lines kept] ...+ tail 일부 - tail에 에러 줄이 있으면 모델이 볼 수 있음
검증
tsc --noEmit✓npm run build✓ (222.30 KB)npm test✓ 28파일 194개 전부 통과 (신규 7개: truncate.test.ts)- 기존 호출자 테스트(bash/git/grep 등) 전부 통과 → 호환성 확인
편집 메모
truncate.ts는 CRLF → write_file 후 Python으로 CRLF 변환truncate.test.ts는 LF → edit_file 도구 사용 가능
✅ 완료: #10 edit_file 유사 매치 제안
문제
fallback 모델(그리고 정밀 edit이 어려운 로컬 모델)이 old_string을 거의 정확히 but not exactly 제공 → occurrences === 0 → 단순 "not found" 에러 → 모델이 맥락 없이 재시도, 실패 반복. 정확한 텍스트를 어디서 가져와야 할지 힌트가 없음.
해결
src/tools/editFile.ts에 유사 매치 제안 추가:
normaliseForCompare(s): 공백 연속을 단일 스페이스로 정규화 → 들여쓰기/줄바꿈 차이에 강건boundedLevenshtein(a, b, maxDist): 조기 종료 Levenshtein.maxDist초과 시 즉시 반환 → 큰 파일에서도 저렴findSimilarMatch(content, needle): 파일 전체를 슬라이딩 윈도우(needle 길이 ±50%, step = needle/8)로 순회하며 정규화된 텍스트로 유사도 측정. 최고 점수 ≥ 0.5일 때만 반환similarHint(original, oldString): 매치 실패 시 에러/preview 메시지에 "The closest match in the file (line N, ~X% similar):" + snippet 추가
handler와 preview 양쪽의 occurrences === 0 경로에 적용. 기존 "not found" 메시지 뒤에 힌트가 붙음.
동작
- 정확히 일치하는 부분이 있으면 기존 동작 유지 (힌트 없음)
- 유사한 부분이 있으면 위치·유사도·snippet 제안 → 모델이 정확한
old_string으로 재시도 가능 - 전혀 다르면 힌트 없이 "not found"만 (노이즈 방지)
검증
tsc --noEmit✓npm run build✓ (220.91 KB)npm test✓ 27파일 187개 전부 통과 (신규 3개: closest match 제안/preview/유사도 임계값)
편집 메모
editFile.ts는 CRLF → Python 바이너리 편집으로 교체 + CRLF 유지editFile.test.ts는 LF → edit_file 도구 사용 가능noUncheckedIndexedAccess활성화 → 배열 인덱스 접근 시?? 기본값처리 필요
남은 우선순위 — 모두 완료 ✅
12개 업그레이드 후보 전부 완료. 아래는 구현 요약.
즉시 효과 (구현 가벼움)
- ✅ #4 스마트 출력 캡 —
truncate.tshead+tail 보존, 모든 호출자 자동 개선 - ✅ #10
edit_file유사 매치 제안 — 매치 실패 시 Levenshtein 유사 위치 제안 - ✅ #6 동적 max_tokens —
resolveMaxTokens(session) - ✅ #11 git 출력 head+tail — #4로 함께 해결
정확도에 큰 영향
- ✅ #3 정확한 토큰 추정 — 스크립트 인식 휴리스틱 (CJK/기호/구조 비용)
- ✅ #5 부분 히스토리 보존 컴팩션 — 최근 N턴 원본 보존 + 이전 요약
성능
- ✅ #1 병렬 툴 실행 —
runToolBatch: read-only 툴Promise.all병렬, mutating 순차. 4개 루프에 적용
회복력
- ✅ #2 재시도 정책 —
maxRetries설정화 (기본 0, SDK 지수 백오프) - ✅ #8 MCP 재연결 —
connectMcpServer재시도 +/mcp reconnect명령 + 세션 toolset 갱신 - ✅ #9 캐시 TTL —
cachedAt타임스탬프 + N일(기본 7) 경과 재감지
기타
- ✅ #7 툴 설명 풍부화 — 8개 핵심 툴 description에 "use when…"/예시 추가
- ✅ #12 auto-accept 세분화 —
auto-accept= 모든 mutating 툴 자동 승인,auto-edit= 편집 툴만 (설명-동작 일치)
핵심 파일 맵
src/agent/loop.ts(1138행) — 메인 에이전트 루프, 턴/스트리밍/툴콜/컴팩션/서브에이전트/병렬 툴 배치src/agent/session.ts— Session 객체, 통계, 상태src/agent/systemPrompt.ts— 시스템 프롬프트 빌더 (매우 간결, 로컬 준수율 우선)src/tools/— 14개 내장 툴 (read_file, list_files, grep, web_search, web_fetch, git_status, write_file, edit_file, bash, bash_output, bash_kill, git_commit, todo_write, agent)src/toolcalling/— native 어댑터, fallback 파서/프롬프트, resolve (Ollama 빈키 복구 포함)src/mcp/— MCP 클라이언트/매니저/어댑터/config (모든 MCP 툴 mutating 강제)src/hooks/— 훅 러너 (병렬 실행, SSRF 가드, exit 0/2 시맨틱스)src/plugins/— Claude Code 플러그인 로더 (commands/agents/skills/hooks/MCP, 툴명 매핑)src/backend/— client, capabilityProbe, contextWindow(자동 탐지), capabilityCachesrc/config/— config (CLI > env > 저장 > 기본값 우선순위), defaults, store, typessrc/permissions/— permissionManager (default/plan/auto-edit/auto-accept), typessrc/persistence/— sessionStore (원자 쓰기, 큐잉), exportSession, replayHistorysrc/utils/— tokens, truncate, shell, processTree, image, html, mentions, projectInstructionssrc/ui/ink/— Ink(React) 풀스크린 TUI 컴포넌트
트러블슈팅 힌트
- "자꾸 에러"의 주요 원인:
max_tokens: 4096잘림 → malformed 툴콜 (✅ 해결됨) - 컨텍스트 윈도우가 8192 기본값이면
locode config set contextWindow <실제값>필요 —resolveMaxTokens가 제값을 내려면 /status로 현재 model/backend/mode/cwd 확인 가능- fallback 모드 툴콜 실패 시
/mode fallback강제 또는 모델 교체 - "Paused after N steps"가 자주 뜨면: 1 스텝 = 1 모델 요청. 로컬 모델은 한 번에 1 툴만 호출하는 경향이 있어 다수 파일 작업이 50스텝을 쉽게 초과. 기본값 50→100 상향(일상 작업용). 큰 배치 작업 시
locode config set maxIterations <number>(예: 200). 정지 시 작업 내용은 보존되므로 "continue"로 이어서 진행 가능.
2026-08-20 세션: LSP 완성 + 로컬 모델 안정성 3총사 + 패리티 4종 + UX
시작점: 6fe9888 (v0.6.0, 12개 업그레이드 완료 커밋). 종료점: dfaf8d1.
검증: typecheck ✓ · build 274.76 KB · 301 tests 통과 (시작 219 → +82).
복구 — 어제 LSP 작업 다운된 지점부터
어제 src/codeintel/lspManager.ts 작업 중 타입 에러 3개로 다운. 우선 복구:
- 스트림 타입:
ReadableStreamMessageReader/WriteableStreamMessageWriter→StreamMessageReader/StreamMessageWriter(NodeReadable/Writable직접 받음, 캐스트 불필요). - diagnostic message:
string | MarkupContent→messageToString()헬퍼로 정규화.
커밋 목록 (12개, 6fe9888..dfaf8d1)
| 커밋 | 내용 |
|---|---|
cb95089 |
LSP 코드 인텔리전스 + 병렬 서브에이전트 mutation gate |
0ddc822 |
로컬 모델 툴콜 안정성 3종 (부분 JSON 복구, fallback 파서, 빈 응답 재시도) |
a458b3c |
multi_edit 도구 |
b7233af |
LSP diagnostics 신선도 (publishDiagnostics 대기) |
f2ca154 |
LSP 서버 설정화 + C/C++ clangd 통합 |
c8cc78e |
README LSP 문서화 + LSP 도구 테스트 |
e3520b8 |
/mouse 토글 (드래그/복사 기본 on) |
ee695a6 |
DiffView (컬러 diff 렌더링) |
b2a7d1a |
notebook_edit (.ipynb 셀 편집) |
5438780 |
풀 트랜스크립트 내보내기 + /export json |
dad0915 |
구조화 task 시스템 (의존성 그래프) |
dfaf8d1 |
출력 중 입력창 작동 (스트리밍 중 타이핑 가능) |
상세
LSP 코드 인텔리전스 (cb95089, b7233af, f2ca154, c8cc78e)
src/codeintel/lspManager.ts: 언어별 LSP 서버 lazy spawn (tsserver/pyright/gopls/clangd/rust-analyzer), didOpen/didChange 동기화, definition/references/diagnostics, notifyFileChanged, shutdownAll.src/tools/codeIntel.ts: definition/references/diagnostics 3개 read-only 도구. tools/index.ts 등록.agent/loop.ts: FileChanged 훅에 notifyFileChanged 연결 (fire-and-forget, best-effort).ui/ink/index.tsx: 종료 시 shutdownAll → orphan 서버 방지.- diagnostics 신선도 (
b7233af): 기존setTimeout(0)1턴 대기 → tsserver/pyright 큰 파일에서 publish 안 됨 → 편집 직후 stale 반환.diagWaitersMap +waitForDiagnostics(uri, 1500)로 publishDiagnostics를 타임아웃 걸고 대기. sync 전 캐시 클리어. - 서버 설정화 (
f2ca154):LANGUAGE_SPECS하드코딩 →configureLanguageSpecs(overrides)로 config 머지.locode config set lspServers '<json>'(키=languageId, {command, args?, extensions?}). 빌트인 오버라이드 또는 새 언어 추가(extensions 필수). C/C++ → clangd 1개로 통합(기존 2개 spawn 문제 해결).resolveLspServers()+ ui/ink 시작 시 configure 호출. - 테스트: lspManager.test.ts (6, configureLanguageSpecs 머지 — 스폰 없이 _specsForTests), codeIntel.test.ts (7, 도구 디스패치 — vi.mock).
병렬 서브에이전트 mutation gate (cb95089)
session.ts:session.mutationGatepromise chain. 세션 전체(서브에이전트 포함) mutating 툴 직렬화. 권한 슬롯 1개에 레이스/파일 쓰기 겹침 방지. read-only는 계속 병렬.loop.ts:runUnderMutationGate. 서브에이전트는 부모 gate 상속.agentTool.ts:tasks배열 → N개 서브에이전트 병렬. 1개 실패는 그 태스크 error, 배치 전체 reject 아님.types.ts: SubAgentResult. 테스트: parallelAgents.test.ts.
로컬 모델 툴콜 안정성 3종 (0ddc822)
- #2 부분 JSON 복구:
src/toolcalling/partialJson.ts—repairPartialJson(). 닫히지 않은 문자열 닫기, 중괄호/대괄호 밸런스(max_tokens 잘림), trailing comma 제거, stray trailing 토큰 컷. 키/값 발명 안 함(스키마 검증 거침).loop.ts: native 툴콜 인자 parse 실패 시 repair 시도 후 non-streaming 재시도. 테스트: partialJson.test.ts (10). - #3 fallback 파서 강화:
fallbackParser.ts—tool_call 펜스(멀티라인 앵커, 내부json 펜스 제거) +json 펜스 + bare(펜스 없는) 툴콜 객체. 모든 후보 partialJson repair 통과. 잘린 펜스(닫는없음)도 복구. 테스트: fallbackParser.test.ts (11). - #6 빈 응답 재시도:
loop.ts— bare emptystop(텍스트/툴콜 없음) 1회 넛지 재시도. 재시도 소진 후 throw. 테스트: loop.test.ts (2).
multi_edit (a458b3c)
src/tools/multiEdit.ts: 한 파일에 순차 배치 편집. 각 edit은 running result에 검증(이전 edit이 텍스트 옮길 수 있음) → 불일치 시 edit 인덱스 명시. 1회 승인 + 1회 atomic 쓰기. editFile의 applyEdit/countOccurrences 재사용. 테스트: multiEdit.test.ts (8).
/mouse 토글 — 드래그/복사 (e3520b8)
- 마우스 휠 추적(
\x1b[?1000h)이 터미널 텍스트 선택을 뺏던 문제. 기본 off → 드래그/복사 가능. PageUp/PageDown 스크롤 유지./mouse on|off로 휠 토글. help/README 문서화.
DiffView (ee695a6)
src/utils/diff.ts:looksLikeDiff().src/ui/ink/DiffView.tsx: unified diff 컬러 렌더링(addition 녹/removal 빨/hunk·file 헤더 dim). side-by-side 모드(pairHunk + clip). v0.6.0 포팅.theme.ts: DIFF_ADD_HEX/DIFF_REMOVE_HEX. PermissionPrompt가 diff preview를 컬러 렌더. 테스트: diff.test.ts (4), DiffView.test.ts (8).
notebook_edit (b2a7d1a)
src/tools/notebookEdit.ts: .ipynb 셀 인식 편집(replace/insert/delete, cell_id/cell_index). nbformat source 라인 배열 변환. code↔markdown 전환 시 필드 정리. atomic 쓰기. v0.6.0 포팅(setLastEdit 제거). 테스트: notebookEdit.test.ts (9).
트랜스크립트 내보내기 (5438780)
exportSession.ts: 마크다운 export에 툴 콜/결과 포함(기존엔 텍스트만).sessionToJson()전체 레코드 JSON./export json [file]. defaultExportFilename 포맷별. 테스트: exportSession.test.ts (8).
구조화 task 시스템 (dad0915)
src/tools/task.ts: TaskStore(in-memory, per-session) + task_create/list/get/update. blocks/blockedBy 의존성 그래프, owner 소유권, status, metadata merge-patch(null=삭제). self-ref/unknown/2-cycle 가드, delete 시 dangling ref 정리. 스키마 사용 전 선언(v0.6.0 TDZ 수정). todo_write 병행 유지.- types.ts: ctx.taskStore. session.ts: 세션마다 TaskStore. 테스트: task.test.ts (9).
출력 중 입력창 작동 (dfaf8d1)
- 기존: isThinking/streamingText 시 ChatInput 언마운트 → "Waiting…" 박스 교체 → 타이핑 불가.
- 수정: ChatInput 항상 마운트. 위에 1줄 상태 표시. handleSubmit 가드로 중복 제출 방지(Enter는 턴 종료까지 no-op, 텍스트는 박스에 남음). Escape 인터럽트는 App 수준 useInput이라 계속 작동.
인코딩 메모
- 프로젝트 파일은 CRLF/LF 섞임. edit_file 도구 매칭 실패 빈번 → Python 바이너리 편집 사용.
- 팁: 편집 전
python3 -c "b=open(p,'rb').read(); print(repr(b[i:i+n]))"로 실제 바이트/인코딩 확인. - unicode(em-dash, CJK 글리프) 포함 시 Python heredoc은 SyntaxError → 임시 .py 파일로 작성.
남은 후보 (이번 세션 미진행)
- LSP 실서버 통합 테스트 (실제 tsserver/pyright 띄워서 검증)
- task store 영속화 (세션에 task 저장)
- 멘션 피커 열린 상태에서 Escape 인터럽트 우선순위 조정(엣지 케이스)
2026-08-21 세션: 마우스 개선, 병렬 툴 실행 검증, 드래그 복사, 색상 조정
시작점: dfaf8d1. 검증: build ✓ · 303 tests 통과.
마우스 스크롤/선택 — Claude Code 스타일로 개선 (e3520b8에서 이어서)
이전 상태: /mouse on 해야 휠 작동, 마우스 모드 켜면 터미널 텍스트 선택 불가.
변경:
- 마우스 모드 기본 ON (
?1002h버튼+드래그 추적) - Shift+클릭/드래그: 터미널 네이티브 텍스트 선택 동작
mouseInput.ts: SGR-1006 전체 파싱 (버튼, 좌표, 누름/해제, Shift/Meta/Ctrl 수정키)logicalButton()내보내기 추가/mouse명령 메시지 업데이트
마우스 드래그로 카피 — 인앱 텍스트 선택 + OSC 52 클립보드
추가 구현:
selectionStart/selectionEnd상태로 드래그 범위 추적 (1-based content row)- 왼쪽 클릭: 선택 시작, 드래그: 범위 갱신, 릴리즈: 선택 텍스트를
extractSelectionText()로 추출 extractSelectionText(): HistoryItem에서 plain text 추출, row 범위로 자르기- 릴리즈 시
copyToClipboard()(OSC 52) 호출 + "Copied N line(s)" 노티스 - Shift+클릭/드래그는 기존대로 터미널 네이티브 선택으로 패스스루
병렬 툴 실행 검증
runToolBatch 디버그 로그 추가:
[runToolBatch] N tool(s): parallel|sequential | tool1, tool2...형식으로 stderr 출력- 병렬 테스트 2개 추가 (loop.test.ts):
- "runs multiple read-only tools in parallel": 3개 read_file 200ms씩 → 병렬 실행 확인
- "runs mixed read+write tool calls sequentially": read_file + edit_file → 순차 실행 확인 (305ms)
결과: 클라우드 모델에서 read-only 툴은 Promise.all 병렬 실행, mutating 툴 섞이면 순차. 로컬 모델은 보통 툴콜 1개씩만 보내서 병렬의 이점을 못 누름.
100 리밋 iteration 검토
DEFAULT_MAX_ITERATIONS = 100(이전 50에서 상향)locode config set maxIterations <number>로 조절 가능- 출력이 띄엄띄엄한 원인: 모델이 툴콜 1개씩만 보내서 매 요청마다 왕복 시간 + 컨텍스트 증가
gateAndRun 에러 로깅 추가
RESOLVE ERROR: 툴 인자 파싱/검증 실패 시 stderr 로그RUN ERROR: 툴 실행 중 에러 시 stderr 로그
DiffView 삭제 라인 색상 조정
DIFF_REMOVE_HEX:#f85149(붉은색) →#e8904e(주황색)- 어두운 터미널 배경에서 가독성 개선
파일 인코딩 정리
loop.ts,loop.test.ts,App.tsxCRLF→LF 변환 (sed -i 's/\r$//')
2026-08-22 세션: 15개 업그레이드 (시스템 프롬프트, 멀티라인, /undo, thinking, 성능 등)
시작점: 5c3fcfd. 종료점: 5065990. 검증: typecheck ✓ · build 289.56 KB · 322 tests 통과 (시작 303 → +19).
커밋
| # | 항목 | 난이도 | 효과 | 로컬 특화 |
|---|---|---|---|---|
| 1 | 시스템 프롬프트 대폭 강화 | 중 | 대 | ★★★ |
| 2 | 멀티라인 입력 (Shift+Enter) + 브래킷 페이스트 | 중 | 대 | |
| 3 | /undo 명령 — 마지막 턴 롤백 |
중 | 대 | |
| 4 | /dashboard 비용/지연 |
— | — | 이미 구현됨 |
| 5 | 컨텍스트 사용률 진행 바 | — | — | 이미 구현됨 |
| 6 | 추론(thinking) 토큰 지원 | 중 | 대 | ★★ |
| 7 | @ 멘션 퍼지 매칭 + 캐시 TTL |
하 | 중 | |
| 8 | readFile 바이너리 가드 + 대용량 보호 | 하 | 대 | |
| 9 | writeFile 원자 쓰기 (temp→rename) | 하 | 대 | |
| 10 | bash 문자열 누적 O(n²)→O(n) | 하 | 중 | |
| 11 | estimateTokens 정규식 고속화 | 하 | 중 | ★ |
| 12 | handleCompletedMessage any→Record | 하 | 품질 | |
| 13 | 세션 복원 시 권한 상태 보존 | 중 | 중 | |
| 14 | 컨텍스트 윈도우 병렬 감지 | 하 | 중 | ★ |
| 15 | 스트리밍 텍스트 누적 O(n²)→O(n) | 하 | 중 |
상세
#1 시스템 프롬프트 대폭 강화 (systemPrompt.ts)
기존 22줄(5개 제네릭 가이드) → 100줄+ 구조화된 프롬프트:
- 도구 분류: Read-only / Mutating (requires confirmation) 그룹별 나열
- 핵심 원칙 7개: inspect before answering, prefer small edits, one tool per response in fallback, preserve existing style, keep answers concise, recovery over retry, respect confirmation
- 도구 사용 가이드: 각 도구별 "use when..." 지시 (read_file, edit_file, bash, agent, task 등)
- 로컬 모델 지침: 작은 컨텍스트 윈도우, 툴콜 불안정성, 빈 응답 대응, 출력 길이 제한
- 안전 가이드: .git 수정 금지, 대규모 삭제 시 확인, bash 읽기 선호
- 서브에이전트 프롬프트:
buildSystemPrompt재사용으로 자동 적용 - 테스트: 12개 신규 (systemPrompt.test.ts)
#2 멀티라인 입력 + 브래킷 페이스트 (ChatInput.tsx)
- Shift+Enter: 줄바꿈 삽입 (기존 Enter=제출 유지)
- multiline 모드: 텍스트에
\n포함 시 Enter→줄바꿈, Ctrl+Enter→강제 제출 - 브래킷 페이스트 (
\x1b[200~…\x1b[201~): 터미널에서 붙여넣기 시 여러 줄 그대로 삽입 - CRLF→LF 변환: 붙여넣은 텍스트의
\r\n을\n으로 정규화 useCallback으로maybeHandlePaste최적화,pasteBufferRef/inPasteRef로 청크 버퍼링
#3 /undo 명령 (session.ts, App.tsx, HistoryItemView.tsx)
undoLastTurn(session): 마지막 user 메시지부터 끝까지 제거, 컨텍스트 토큰 재계산- 파일시스템 변경은 롤백 안 함 (soft undo)
- 제거된 메시지 수 반환 → "Undid last turn (removed N messages)"
/undo슬래시 명령 + 도움말에 추가- 테스트: 7개 (session.test.ts) — no user messages, 단일 턴, 툴결과 포함, 이전 턴 보존 등
#6 추론(thinking) 토큰 지원 (events.ts, loop.ts, App.tsx, types.ts, HistoryItemView.tsx)
AgentEvent에thinking_delta/thinking_done이벤트 추가- 스트리밍 루프에서
delta.reasoning_content캡처 (DeepSeek, QwQ 등) fullThinking누적 →thinking_done이벤트로 방출- UI: dimmed 보더 + "Thinking:" 레이블로 렌더링
itemToLines에 thinking 종류 추가 (선택 텍스트)
#7 @ 멘션 퍼지 매칭 (ChatInput.tsx)
- 퍼지 매치:
fuzzyMatch()— 정확한 부분문자열 매치 우선, 실패 시 문자 순서 매치 (예: "ut" → "utils/") - 정렬: 정확 매치 우선, 그 다음 길이순
- 캐시 TTL:
filesCachedAt상태 추가, 5분 후 리글로브 .gitignore준수:fast-glob의gitignore: true옵션 (타입 제한으로 제외,ignore로 대체)
#8 readFile 바이너리 가드 (readFile.ts)
MAX_FILE_SIZE = 10 MB—stat()사전 체크, 초과 시 에러 메시지BINARY_EXTENSIONSSet (30+ 확장자) — 확장자 기반 바이너리 감지- null-byte 휴리스틱 (8KB 스캔) — 확장자 없는 바이너리 파일 감지
content.split("\n")→content.split(/\r?\n/)— CRLF 호환
#9 writeFile 원자 쓰기 (writeFile.ts)
randomUUID()로 임시 파일명 생성 →writeFile→rename- 실패 시
unlink로 임시 파일 정리 readExistingcatch에서err.code === 'ENOENT'명시적 체크 (다른 에러는 재throw)
#10 bash 문자열 누적 (bash.ts)
let stdout = ""→const stdoutChunks: string[] = []stdout += d.toString()→stdoutChunks.push(d.toString())- 최종
stdoutChunks.join("")/stderrChunks.join("") - O(n²) 문자열 누적 → O(n) 배열 push + join
#11 estimateTokens 정규식 고속화 (tokens.ts)
- per-character
weightedChars루프 → 정규식 벌크 카운팅:- CJK:
/[\u3040-\u30ff\u3400-\u9fff\uac00-\ud7af\u1100-\u11ff]/gu→ ×2.4 - Dense symbols:
/[!-\/:-@[-\{-~]/g` → ×1.15 - 나머지: ×1
- CJK:
isCjk()/isDenseSymbol()헬퍼 제거
#12 handleCompletedMessage 타입 (loop.ts)
message: any→message: Record<string, unknown>- 런타임 동작은 동일 (속성 접근 시 any와 호환)
#13 세션 복원 권한 보존 (session.ts, sessionStore.ts)
SessionRecord에allowedTools?: string[]선택적 필드 추가toSessionRecord():session.permissions.listAllowed()저장createSessionFromRecord():record.allowedTools복원 →session.permissions.allowForSession()/perm에서 "이 세션에서 허용"한 도구가 세션 재개 후에도 유지
#14 컨텍스트 윈도우 병렬 감지 (contextWindow.ts)
detectContextWindow: 순차await Ollama → await LM Studio→Promise.allSettled([Ollama, LM Studio])- 감지 지연 반감 (Ollama가 안 돌아도 LM Studio 결과 즉시 사용)
ollama.status === "fulfilled" && ollama.value !== null패턴으로 null 결과 필터링
#15 스트리밍 텍스트 누적 O(n²)→O(n) (loop.ts)
fullText += delta.content→textChunks.push(delta.content)- 스트림 루프 후
fullText = textChunks.join("") - 에러 핸들러에서도
textChunks.join("")로 부분 텍스트 복구
인코딩 메모
session.ts,App.tsx,HistoryItemView.tsxCRLF → Python 바이너리 편집으로 교체ChatInput.tsx,systemPrompt.tsLF → write_file/edit_file 사용
2026-08-23 세션: 클라우드 모델 빈 응답 수정, 마우스 드래그 디버그
시작점: 47711a9. 검증: typecheck ✓ · build 294.28 KB · 326 tests 통과.
#13 시스템 프롬프트 클라우드/로컬 분리 (systemPrompt.ts, session.ts, App.tsx, defaults.ts)
문제: 시스템 프롬프트에 "You run against a local model" / "Empty or malformed responses can happen" 등 로컬 모델 전용 지시사항이 하드코딩. GLM-5.1 클라우드 모델이 이 프롬프트를 받으면:
- 자신이 로컬 모델이라고 착각 → 보수적 동작
- "빈 응답이 나올 수 있다"는 암시 → 자기 충족적 빈 응답 (Request failed: Empty response from model.)
해결: isLocal 파라미터로 프롬프트 분리
isLocalBackendURL(baseURL): localhost/127.0.0.1/::1이면 truebuildSystemPrompt(tools, mode, projectInstructions, isLocal):- 로컬 프롬프트 (
isLocal=true, 기본값): 기존 프롬프트 유지, "Working with local models" 섹션 포함, 7개 원칙 - 클라우드 프롬프트 (
isLocal=false): 로컬 모델 제한 섹션 제거, 6개 원칙 (fallback 모드의 "One tool per response" 규칙 제거), "local model" 언급 없음
- 로컬 프롬프트 (
Session.isLocal필드 추가,createSession/createSessionFromRecord/setMode/compactSession/spawnSubAgent모두 전파App.tsx에서isLocalBackendURL(baseURLRef.current)/isLocalBackendURL(record.baseURL)로 자동 설정
#14 빈 응답 재시도 증가 (loop.ts)
MAX_EMPTY_RESPONSE_RETRIES: 1 → 3- 클라우드 모델도 간헐적 빈 응답 발생 가능 (GLM-5.1 사례)
- 이전: 초기 응답 + 1회 재시도 = 총 2회 → 실패 시
AgentError("Empty response from model.") - 이후: 초기 응답 + 3회 재시도 = 총 4회 → 빈 응답 후 넛지 메시지 삽입 후 재시도
#15 마우스 드래그 디버그 (App.tsx)
- 마우스 이벤트에
process.stderr.write디버그 로그 추가 - 클릭/드래그/릴리즈 이벤트의
button,pressed,logicalBtn값 확인용 - SGR-1006 모드 1002에서 드래그 이벤트(
button=32, motion bit)가logicalButton()에서"other"로 분류되는 문제 확인 중 - 디버그 완료 후 제거 예정
maxIterations 기본값 300 상향 (이전 세션에서 진행)
DEFAULT_MAX_ITERATIONS = 300(이전 100 → 300, 이미 08-21 세션에서 변경됨)