Skip to content

Dev Guide claude resume

Woojin Kim edited this page Jun 20, 2026 · 2 revisions

개발자 가이드 — claude-resume 플러그인

claude-resume 는 이 머신에 저장된 과거 Claude Code 세션을 시각·프로젝트·AI 제목 으로 나열하고, 하나를 골라 Enter 하면 새 탭에서 그 세션을 리줌(claude --resume <id>)하는 피커 플러그인이다. 이 문서는 플러그인 내부 구조와 확장·테스트 방법을 다루는 개발자용 가이드다.

플러그인 일반 작성 규약은 Plugin-Authoring-Guide 를, Claude Code 통합 플러그인 군 전체는 Claude-Code-Plugins 를 참조하라.

목차


1. 목적과 전체 흐름

Claude Code 는 대화 세션을 JSONL 기록 파일로 디스크에 남긴다. 이 플러그인은 그 기록을 훑어 사람이 알아볼 수 있는 한 줄짜리 라벨(시각·프로젝트·제목)로 정리해 피커로 보여 주고, 사용자가 고른 세션을 새 탭의 셸에서 다시 이어 가게 한다.

핵심 설계 결정은 열거·리줌을 서버에서 수행한다는 것이다. 세션 파일이 서버 측 머신에 있으므로 서버가 직접 읽어야 정확하고(remote-attach 상황에서도 올바른 머신을 본다), 새 탭의 패널 id race 도 피한다. 클라이언트는 목록을 요청하고 받은 결과로 모달을 띄울 뿐이다.

flowchart TD
    A["클라 명령 claude-resume"]
    B["서버 handle_server_request('claude_list_sessions')"]
    C["클라 handle_message(t == 'claude_sessions') → ClaudeResumeScreen 푸시"]
    D["서버 handle_server_request('claude_resume_session')"]
    E["서버 _broadcast_session(sess) → 전 클라에 새 탭 동기화"]
    A -- "app.send_cmd('claude_list_sessions')" --> B
    B -- "sessions.list_sessions(limit=300)" --> C
    C -- "행 선택 + Enter<br/>send_cmd('claude_resume_session', session_id, cwd)" --> D
    D -- "new_window(path=cwd) → 새 패널 pty 에 'claude --resume &lt;id&gt;\r' write" --> E
Loading

delete-to-disable: 이 플러그인 디렉토리를 통째로 지우면 claude-resume 명령과 서버 회신이 조용히 사라지고 코어는 그대로 동작한다(코어는 이 플러그인을 직접 import 하지 않는다).


2. 파일 구성

파일 역할 textual 의존
__init__.py 코어와의 계약 — 명령 메타·디스패치·메시지/요청 핸들러. 가벼움. 없음
sessions.py 세션 열거(순수 로직). 서버에서 import 된다. 없음
screen.py 리줌 피커 모달. 클라에서 실제로 열 때 지연 import. 있음

sessions.py__init__.py 는 textual 을 최상단에서 import 하지 않는다 — 서버 프로세스도 이들을 읽기 때문이다. textual 위젯이 필요한 screen.pyhandle_message 안에서 지연 import 한다.


3. 등록 — 명령·훅·메시지

플러그인은 _ClaudeResumePlugin 인스턴스(PLUGIN)로 레지스트리에 노출된다. 모든 후크 이름은 다른 플러그인과 동일한 계약을 따른다.

명령 메타데이터

COMMANDS = [
    ("claude-resume", "이 머신의 Claude Code 세션 목록 — …", "Claude"),
]
NOARG    = {"claude-resume", "claude-sessions", "cr"}
_ALIASES = ("claude-resume", "claude-sessions", "cr")
  • COMMANDS 는 코어가 COMMANDS / COMPLETIONS / COMMAND_NOARG 에 합쳐 쓴다. 세 번째 요소 "Claude" 는 명령 카테고리다.
  • claude-resume 외에 별칭 claude-sessions, cr 을 받는다. 셋 다 인자가 없으므로 NOARG 에 등록한다.

클라이언트 측 훅

  • attach_client(app)app.request_claude_sessions 진입점을 설치한다. 이 함수는 app._want_claude_sessions = True 플래그를 세우고 app.send_cmd("claude_list_sessions") 로 서버에 목록을 요청한다.
  • handle_command(app, c, args)c 가 별칭 중 하나면 request_claude_sessions() 를 호출하고 True(소비) 반환.
  • handle_message(app, msg) — 서버 응답 t == "claude_sessions" 를 받으면, 요청 플래그가 서 있을 때만(_want_claude_sessions) 화면을 연다. 플래그가 없으면 방어적으로 무시한다(요청하지 않은 응답 차단). 여기서 screen.ClaudeResumeScreen 을 지연 import 해 app.push_screen(...) 한다.

서버 측 훅

  • handle_server_request(server, sess, action, msg) — 두 액션을 처리한다.
    • "claude_list_sessions"sessions.list_sessions(limit=_LIST_LIMIT) 결과를 {"t": "claude_sessions", "sessions": [...]} 로 회신.
    • "claude_resume_session"6장 참조.

메뉴 진입

명령 메타가 카테고리 "Claude" 로 등록되므로 명령 팔레트/컨텍스트 메뉴의 Claude 그룹에서 선택할 수 있고, :claude-resume(또는 :cr)로 직접 호출할 수도 있다.

i18n

i18n.register(...) 로 ko/en 카탈로그를 싣는다. ko 는 COMMANDS 에서 cmd.<name> 키가 자동 시드되고, 피커 화면 문자열(cresume.title / .none / .hint / .opening)을 ko·en 양쪽에 보강한다. 화면 코드는 항상 i18n.t("cresume.…") 로 가져온다.


4. sessions.py — 세션 기록 열거

sessions.py 는 textual 무관 순수 로직이라 서버에서 바로 쓴다.

기록 위치(일반화)

Claude Code 는 대화 세션을 사용자 홈의 Claude 세션 디렉토리 아래에 프로젝트별로 나누어 저장한다. 개략적인 레이아웃은 다음과 같다.

~/.claude/projects/<cwd-슬러그>/<session-uuid>.jsonl
  • <cwd-슬러그> = 그 세션의 작업 디렉토리 경로에서 구분자를 - 로 치환한 이름.
  • .jsonl줄당 1개 JSON 이벤트. 슬러그 역변환은 손실이라 그대로 라벨에 쓰지 않는다.

진입점은 projects_dir() 로, os.path.expanduser("~") 아래의 세션 루트 경로를 돌려준다. 하드코딩된 홈 경로는 없다.

라벨에 쓰는 줄 타입

type 필드 용도
ai-title aiTitle AI 생성 세션 제목(공식 /resume 피커가 보여주는 제목)
last-prompt lastPrompt 마지막 사용자 프롬프트
user message.content 사용자 메시지(첫 메시지 폴백 · 빈 세션 판별)
(아무 줄) cwd 세션 작업 디렉토리(리줌 시 그리로 cd)

주요 함수

  • parse_session(path) — JSONL 한 파일을 한 줄씩 읽어 메타 dict 를 만든다. 깨진 줄 (ValueError/TypeError)·비-dict 는 건너뛴다. cwd 는 처음 본 값을 채택. 제목 우선순위는 ai-title > last-prompt > 첫 user 메시지 > session id. user 이벤트가 0개인 빈 세션은 None 으로 제외한다(리줌 대상 아님). 결과 dict:
    {"id", "cwd", "title", "mtime", "ai_title", "last_prompt"}
    _user_text(o)content 가 str 이거나 [{type:"text", text}] 리스트인 두 형태를 모두 처리한다. _clean(s, n) 은 개행·연속 공백을 한 칸으로 줄이고 _TITLE_MAX(200) 로 자른다.
  • _project_label(cwd, slug) — 표시용 프로젝트 라벨. cwd 가 있으면 마지막 두 경로 요소(예: dir/subdir)를, 한 요소뿐이면 그 요소를, cwd 가 없으면 슬러그를 쓴다.
  • list_sessions(root=None, limit=None) — 루트 아래 모든 슬러그 디렉토리의 *.jsonlparse_session 으로 훑어, 각 항목에 project 라벨을 더하고 mtime 내림차순(최신순) 으로 정렬한다. limit 이 있으면 상위 N 만 반환. 디렉토리/파일 접근 실패(OSError)는 조용히 건너뛰어 부분 결과라도 보여 준다. root 미지정 시 projects_dir() 를 쓴다 — 이 인자 덕에 테스트에서 임시 디렉토리를 주입할 수 있다.

__init__.py_LIST_LIMIT = 300 이 기본 상한이라, 아주 오래된 세션까지 무한정 나열하지 않고 최신 300개만 보낸다.


5. 피커 화면(3열 UI)

claude-resume 피커

ClaudeResumeScreen(ModalScreen)이 서버가 보낸 세션 목록을 한 줄짜리 행으로 보여 준다. 각 행은 세 열로 구성된다.

수정시각          프로젝트              제목
─────────────  ────────────────────  ─────────────────────────
MM-DD HH:MM    dir/subdir            AI 생성 제목 또는 폴백…
  • 시각_when(mtime)"%m-%d %H:%M" 로 포맷(로컬 시간). 실패 시 빈 문자열.
  • 프로젝트_project_label 결과를 _cellpad(s, _PROJ_CELLS)표시 셀폭 22 에 맞춰 우측 패딩·정렬한다. _cellpadclientutil._char_cells 로 글자 폭을 재서 CJK 2칸 을 고려하고, 넘치면 로 자른다(폭 계산이 폰트 무관해야 정렬이 맞는다).
  • 제목parse_session 이 고른 제목.

행 빌드는 _row(s) 가 담당하고, 모든 텍스트 라벨은 markup=False 로 만들어 세션 제목 안의 대괄호 등이 textual 마크업으로 오해되지 않게 한다. 세션이 없으면 리스트 대신 i18n.t("cresume.none") 안내를 보인다. 하단 힌트는 i18n.t("cresume.hint") (↑↓ 이동 · Enter 새 탭에서 리줌 · Esc 닫기).

상호작용

  • on_mount — 세션이 있으면 ListView 에 포커스를 준다.
  • on_list_view_selected — Enter/클릭으로 행 선택. 아이템 id cr_{i} 에서 인덱스를 뽑아 _resume(idx) 호출(event.stop() 으로 전파 차단).
  • on_keyescape 면 닫는다.
  • on_click[x] 버튼(crclose) 클릭이나 모달 바깥(백드롭) 클릭이면 닫는다 (crbox 안쪽인지 부모 체인을 거슬러 판정). 다른 모달과 동일한 닫기 동선.

6. 선택→리줌 경로

행을 고르면 _resume(idx) 가:

  1. self.app.send_cmd("claude_resume_session", session_id=s["id"], cwd=s["cwd"]) 로 서버에 리줌을 요청하고,
  2. i18n.t("cresume.opening", title=…) 안내를 표시한 뒤,
  3. 모달을 닫는다(dismiss(None)).

서버 handle_server_request("claude_resume_session", …) 가:

  1. resume_command(msg["session_id"]) 로 주입할 명령 문자열을 만든다. 위생 실패면 None 을 받아 아무것도 하지 않는다(7장).
  2. server.new_window(sess, path=msg["cwd"])세션의 원래 cwd 에서 새 탭을 연다(설계 결정: cd 후 리줌).
  3. 새 탭의 활성 패널 pty 에 cmd(=claude --resume <id>\r)를 UTF-8 로 write 한다. pty 가 없거나 OSError 면 조용히 무시.
  4. server._broadcast_session(sess) 로 세션 전 클라에 전체 동기화를 방송해 새 탭이 모두에게 보이게 한다.

resume_command(session_id, enter="\r") 는 셸과 무관하게 claude --resume <id> + Enter 를 만든다. enter 를 분리해 둔 덕에 테스트에서 개행을 비워 순수 명령 문자열만 검증할 수 있다.


7. 보안: 세션 id 위생

세션 id 는 새 탭 셸에 그대로 주입되므로 셸 인젝션을 막아야 한다. resume_command 는 정규식 게이트를 통과한 id 만 받는다.

_ID_OK = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
  • 영숫자로 시작하고 영숫자·.·_·- 만 허용(uuid 형식). 공백·;·$·따옴표 등 셸 메타문자가 들어가면 매칭 실패.
  • 위생 실패 시 resume_commandNone 을 돌려주고, 서버는 새 탭조차 열지 않는다.

목록은 디스크의 파일명에서 id 를 얻지만, 리줌 시점에 서버가 다시 한 번 위생 검사하므로 클라가 임의 session_id 를 보내도 안전하다.


8. 확장하기

  • 새 열·정렬 키 추가parse_session 이 돌려주는 dict 에 필드를 더하고(list_sessions 는 그대로 통과시킴), screen._row 에서 새 열을 그린다. 정렬을 바꾸려면 list_sessionssort(key=…) 를 수정.
  • 다른 제목 소스 — JSONL 의 새 줄 타입을 라벨에 쓰려면 parse_session 의 우선순위 사슬 (ai_title or last_prompt or first_user or session_id)에 끼워 넣는다.
  • 상한·페이지네이션_LIST_LIMIT 을 조정하거나, list_sessions(limit=…) 호출부에서 넘긴다. 매우 큰 환경이면 mtime 필터를 list_sessions 에 추가하는 편이 파싱 비용을 줄인다.
  • 리줌 동작 변경 — 새 탭 대신 현재 패널에서 리줌하는 등 동작을 바꾸려면 handle_server_requestclaude_resume_session 분기를 고친다. 주입 문자열은 resume_command 한 곳에만 있으니 거기서 형태를 바꾼다.
  • 새 별칭COMMANDS/NOARG/_ALIASES 세 곳에 추가한다(handle_command_ALIASES 로 판정).

플러그인 계약·레지스트리 훅 전반은 Plugin-Authoring-Guide 를 보라.


9. 테스트

  • 순수 로직(sessions.py) — textual 의존이 없어 가장 쉽게 테스트한다. 임시 디렉토리에 슬러그 폴더와 *.jsonl 픽스처를 만들고 list_sessions(root=<tmp>) 를 호출해 검증한다. 확인 포인트:
    • 제목 우선순위(ai-title > last-prompt > 첫 user > id),
    • 빈 세션(user 0개) 제외,
    • mtime 내림차순 정렬과 limit 절단,
    • 깨진 JSON 줄·접근 실패를 건너뛰고 부분 결과를 돌려주는지(OSError 내성),
    • _project_label 의 마지막 두 경로 요소 규칙. root 인자 덕에 사용자 홈을 건드리지 않고 결정론적으로 돌릴 수 있다.
  • 위생resume_command 에 정상 uuid·셸 메타문자·빈 문자열을 넣어 정상은 명령 문자열, 불량은 None 임을 확인한다. enter="" 로 개행을 빼면 비교가 깔끔하다.
  • 화면(screen.py) — textual 모달이라 driver 로 직접 검증이 제한적이다. 단위 수준에서는 _cellpad 의 셀폭 패딩/말줄임(CJK 2칸 포함)과 _when 포맷·예외 처리를 검증한다. 실제 렌더는 스크린샷 하네스로 SVG 를 떠 시각 확인한다(플러그인 SVG 스크린샷 절차 참조).
  • 전체 스위트python3 tests/run.py 로 돌리고 요약줄(N passed, 0 failed) 을 확인한다. 서브셋 실행은 플러그인 믹스인 poison 으로 가짜 실패가 날 수 있어, 권위는 항상 전체 스위트다.

관련 문서

Clone this wiki locally