tars 0.65 CASE를 로봇으로 다시 그리고 어디서나 상태 묻기
tars는 브라우저 콘솔이 있는 로컬 AI 에이전트 런타임이고, 콘솔 구석에는 CASE라는 작은 컴패니언이 있다. 승인을 기다리는 요청이나 실패한 작업이 생기면 표정과 말풍선으로 알려 주는 역할이다. 10월 16일에는 포커스 모드에 붙이려던 Crew의 동물들과 맞추려고 CASE를 미어캣으로 그렸다. 다음 날 Crew를 접기로 하면서 CASE가 동물일 이유도 없어졌고, TARS의 짝이니 다시 로봇으로 돌리기로 했다.
몸통은 스프라이트, 얼굴은 CSS로 나누기
처음 계획은 CSS와 span만으로 그리는 것이었는데 이번에 스프라이트 이미지를 허용하는 쪽으로 바꿨다. 몸통은 codex CLI의 이미지 생성으로 포즈 몇 장을 그리고, 얼굴은 로봇 화면 위에 CSS로 그린다. 콘솔에서 CASE는 64px 크기로 보여서 얼굴을 이미지에 넣으면 뭉개지고, 커서를 따라가는 시선이나 표정 전환도 CSS여야 부드럽게 이어진다고 판단했다. 상태 색은 가슴의 칩에서 안테나 LED로 옮겨서 색이 들어가는 곳은 여전히 한 군데다.
먼저 headless로 codex exec를 돌려 봤다. 이미지 한 장에 2분쯤 걸렸고 결과는 투명 배경(RGBA)으로 나왔다. -i로 참조 이미지를 주고 고쳐 그리게 하는 것도 됐다. 걸린 데가 두 군데 있었는데, -i 뒤에는 --를 붙여 끝내야 했고, 설정해 둔 gpt-6.1-sol 모델은 ChatGPT 계정에서 거절돼서 -m gpt-5.6-sol로 돌렸다. 로봇 스타일을 세 가지 뽑아 부드러운 음영이 들어간 B를 골랐다.
고른 그림이 실제 크기에서 읽히는지는 64px 검토 페이지로 확인했다. 표정 여덟 개를 늘어놓고, 안테나 LED를 숨긴 줄에서도 모양만으로 구분되는지 봤다.

이 검토에서 나온 수정은 다음 PR에서 반영했다. 눈썹은 최소 2px로 굵히고, 작업 중 표정은 점 대신 움직이는 진행 줄을 쓰고, 시트를 잘라 칸을 꽉 채웠다.
포즈 이미지를 따로따로 쓰면 프레임마다 몸통이 조금씩 달라진다. 그래서 scripts/case_sprites.py가 각 포즈를 빈 화면 원본에 발 위치로 맞춘 뒤 팔이나 안테나 영역만 합성하게 해서, 몸통은 모든 프레임에서 픽셀 단위로 같다. 이 스크립트는 프레임마다 화면 영역과 LED 위치를 담은 TypeScript 파일도 같이 만들어서, 시트를 다시 뽑아도 CSS의 픽셀 값을 손으로 고칠 일이 없다. 처음에는 다섯 프레임에 16KB쯤이었다. 테스트에는 CASE의 이미지는 src/assets/case/ 아래 시트 하나만 쓴다는 규칙과 150KB 예산, CSS에 나오는 포즈 프레임이 시트에 다 있는지 확인하는 검사를 넣었다.
움직임과 잠재우기 붙이기
그다음 PR에서 움직임을 넣었다. 표정이 바뀔 때 얼굴은 180ms 동안 모양이 이어지듯 바뀌고, 포즈는 서로 겹치며 넘어간다. 가만히 있을 때 주변을 훑는 동작도 넣었다. CASE가 직접 답할 때는 첫 글자가 오기 전까지 생각하는 얼굴이었다가, 답이 흘러나오는 동안 음파 막대가 있는 말하는 얼굴이 된다.
말풍선 메뉴에서 CASE를 한 시간 동안, 또는 다음 알림이 올 때까지 재울 수도 있다. 이 설정은 보는 사람마다 브라우저에 저장되고, 새 알림이 오거나 클릭하거나 한 시간이 지나면 깨어난다. 화면이 꺼진 포즈를 하나 더 그려서 시트는 여섯 프레임, 18KB쯤이 됐고, 깨어날 때와 처음 나타날 때는 부팅 애니메이션이 돈다.
어느 화면에서나 tars 상태 묻기
CASE의 말풍선에도 원래 질문을 적을 수 있었지만, 그 질문은 메인 채팅 세션의 일반 턴으로 들어갔다. “TARS와 이야기하기”에는 맞는 자리인데 “지금 tars에서 무슨 일이 일어나고 있나”를 묻기에는 맞지 않았다. 메인 세션은 포커스 파이프라인, 다른 세션의 턴, 승인 대기열, cron, 비용을 볼 수 없고, 이런 질문이 쌓이면 메인 대화 기록만 지저분해진다.
그래서 CASE의 Bridge를 따로 만들었다. 어느 화면에서든 Mod+/나 명령 팔레트, 말풍선 메뉴의 “Ask about TARS →“로 연다. Mod+J는 이미 터미널을 여닫는 키라 쓰지 않았다. CASE는 한두 문장으로 답하고, 관련 화면으로 가는 카드를 네 개까지 붙인다.
계획에서는 모델이 부르는 tars_status 도구를 만들려고 했는데 만들면서 바꿨다. claude-code-cli와 antigravity-cli 프로바이더는 tars 도구를 부를 수 없어서 그 티어에서는 CASE가 상태를 못 보게 되고, 도구를 등록하면 모든 세션의 프롬프트에 같이 실린다. 그래서 서버가 Go 코드로 상태를 모은다. 포커스 파이프라인, 실행 중인 턴, 채팅과 대기열의 승인 요청, 최근 24시간의 cron 실패, 오늘과 이번 주 비용, pulse 경고를 4KB 이하의 텍스트로 만들어 CASE의 턴에만 <tars-status> 맥락으로 넘긴다. 대화 기록에는 저장하지 않고 다른 세션 프롬프트에도 들어가지 않는다.
카드도 모델이 만든 링크를 그대로 믿지 않는다. 질문할 때 찍은 스냅숏에 있는 대상을 가리키는 카드만 남기고, 링크는 서버가 만든다. 모델이 지어낸 카드는 버려진다. 질문은 하루에 하나씩 만드는 숨은 worker 세션에서 plan 모드, light 티어로 돌고, 읽기 전용이라 승인이 필요한 호출은 대기열에 올리지 않고 바로 거절한다. CASE를 끄면 Bridge도 같이 꺼진다.
서버 쪽 경로는 두 개다.
POST /v1/case/ask 202, 답하는 중이면 409, 서버 준비 전이면 503
GET /v1/case/answer 카드 블록을 뺀 답과 검증된 카드
확인한 것과 남은 것
네 PR 모두 10월 18일에 머지했고 10월 19일에 낸 v0.65.0에 들어갔다. Bridge PR에서는 콘솔 e2e 전체 157개가 통과했고, 지어낸 카드가 빠지는지, 질문이 메인 세션에 들어가지 않는지도 e2e로 확인했다. 움직임 PR에서는 e2e 하나가 부팅 중인 CASE를 복제하다 실패해서 고쳤는데, 고친 뒤에는 CASE 관련 스펙만 다시 돌렸고 전체 묶음은 다시 돌리지 않았다.
계획 문서에서 크기와 움직임 설정은 아직 남아 있다. 말풍선 질문을 Bridge로 옮기는 것도 다음 작업으로 미뤘다.