Files
locode/locode-upgrade-memo.md
T
kim 60c6021390 feat: split system prompt by local/cloud, improve mouse tracking, increase retries & iterations
- 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
2026-08-21 18:01:29 +09:00

33 KiB
Raw Blame History

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 — 두 가지 실패 모드:

  1. JSON 직렬화 오버헤드(따옴표, 중괄호, 이스케이프)를 콘텐츠 토큰으로 계산 → 추정치 15–25% 부풀림 (모델에 안 보내는 것들)
  2. 동일한 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.ts CRLF → write_file + Python 변환
  • tokens.test.ts LF → 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.ts head+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(자동 탐지), capabilityCache
  • src/config/ — config (CLI > env > 저장 > 기본값 우선순위), defaults, store, types
  • src/permissions/ — permissionManager (default/plan/auto-edit/auto-accept), types
  • src/persistence/ — sessionStore (원자 쓰기, 큐잉), exportSession, replayHistory
  • src/utils/ — tokens, truncate, shell, processTree, image, html, mentions, projectInstructions
  • src/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 (Node Readable/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 반환. diagWaiters Map + 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.mutationGate promise 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 empty stop(텍스트/툴콜 없음) 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.tsx CRLF→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_EXTENSIONS Set (30+ 확장자) — 확장자 기반 바이너리 감지
  • null-byte 휴리스틱 (8KB 스캔) — 확장자 없는 바이너리 파일 감지
  • content.split("\n") → content.split(/\r?\n/) — CRLF 호환

#9 writeFile 원자 쓰기 (writeFile.ts)

  • randomUUID()로 임시 파일명 생성 → writeFile → rename
  • 실패 시 unlink로 임시 파일 정리
  • readExisting catch에서 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
  • 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.tsx CRLF → Python 바이너리 편집으로 교체
  • ChatInput.tsx, systemPrompt.ts LF → 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이면 true
  • buildSystemPrompt(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 세션에서 변경됨)