입력창에 붙여 넣은 이미지를 보내기 전에 입력창 위 목록으로 보여주고, 누르면 사진 뷰어로 크게 엽니다

Claude Code 터미널의 입력창 위에 한 줄짜리 정보를 더해 주는 모드 두 개입니다.
| 모드 | 하는 일 | 지원 환경 |
|---|---|---|
| total-stats | 모델·effort·fast, 남은 컨텍스트와 사용 한도, 직전 대화의 토큰과 걸린 시간, 세션 전체 턴·토큰·스킬 횟수를 한 줄로 보여주고, 마우스로 자세히 보거나 바로 바꿀 수 있습니다 | 모든 OS |
| image-peek | 입력창에 붙여 넣은 이미지([Image #1])를 보내기 전에 목록으로 보여주고, 누르면 원본을 사진 뷰어로 엽니다 | Windows |
📎 [ Image 1 ] [ Image 2 ]
🧠 Opus 5.5 · medium · ⚡fast · ctx ███████░░░ 68% · 남은 5h 77% 주 60% · 직전 4.0k (1m23s) · 세션 12턴 244.0k · 스킬 3
> 이거 봐줘 [Image #1] [Image #2]
Claude Code 터미널의 입력창에 입력합니다. 필요한 것만 설치해도 됩니다.
/plugin install total-stats --marketplace glglekdy/claude-mods
/plugin install image-peek --marketplace glglekdy/claude-mods
처음 설치할 때 마켓플레이스를 추가할지 물으면 y, 설치 범위는 user(모든 세션)를 고르면 됩니다. 설치한 세션에서 바로 켜집니다.
새 버전이 나오면 터미널에서 마켓플레이스 목록을 새로 받은 뒤 플러그인을 업데이트합니다. Claude Code를 다시 켜거나 /reload-plugins를 입력하면 적용됩니다.
claude plugin marketplace update glglekdy-mods
claude plugin update total-stats@glglekdy-mods
claude plugin update image-peek@glglekdy-mods
Claude Code 안에서 /plugin 메뉴로 업데이트해도 됩니다.
끄기와 지우기:
claude plugin disable total-stats
claude plugin uninstall total-stats
| 항목 | 내용 | 색 | 누르면 |
|---|---|---|---|
| 🧠 모델 | 지금 대화에 쓰이는 모델 | 마우스를 올리면 Claude 색 | 모델·effort·fast 선택 창 |
| effort | 실제 요청에 적용된 effort | 마우스를 올리면 강조색 | 모델·effort·fast 선택 창 |
| fast | fast 모드 상태 | 켜짐은 노랑, 꺼짐은 흐리게, 켜지지 않는 환경이면 빨강 ⚡fast 불가 | |
| ctx | 남은 컨텍스트, 10칸 게이지 | 50% 넘게 남으면 초록, 20~50% 노랑, 20% 밑 빨강 | ctx 글자: /context 실행 |
| 남은 | 5시간·주간 한도가 남은 비율(구독 요금제일 때만) | 같은 기준 | 남은 글자: /usage 실행 |
| 직전 | 바로 전 대화 한 번(턴)에 쓴 토큰과 (걸린 시간) | 시간은 강조색 | |
| 세션 | 이번 세션의 턴 수와 전체 토큰(서브에이전트 포함) | ||
| 스킬 | 이번 세션에서 스킬을 부른 횟수(/이름, Skill 도구, 서브에이전트 미리 불러오기 포함) |
컨텍스트가 20% 이하로 남으면 ctx 옆에 빨간 경고와 버튼이 생깁니다. 버튼을 누르면 /compact가 실행됩니다.
ctx ██░░░░░░░░ 15% ⚠ 곧 가득 참 [ /compact ]
줄 바로 위에 카드가 펼쳐지고, 마우스를 치우면 사라집니다.
╭──────────────────────────────────────────╮ ╭──────────────────────────────────────────────╮
│ 컨텍스트 64.0k / 200.0k │ │ 5시간 15% 남음 (85% 사용) · 리셋까지 2h13m │
│ Messages ███░░░░░░░ 50.0k 25% │ │ 주간 60% 남음 (40% 사용) · 리셋까지 3d4h │
│ MCP tools █░░░░░░░░░ 10.0k 5% │ │ 이번 세션 API 환산 비용 $0.31 │
│ System prompt █░░░░░░░░░ 4.0k 2% │ ╰──────────────────────────────────────────────╯
╰──────────────────────────────────────────╯ 남은 위에 올렸을 때
ctx 위에 올렸을 때
╭──────────────────────────────────────────────────────────╮
│ 모델 [ Opus ] [ Sonnet ] [ Haiku ] [ Fable ] [ 더 보기 ] │
│ effort [ low ] [ medium ] [ high ] [ xhigh ] [ max ] │
│ fast [ 켜짐 → 끄기 ] 크레딧(추가 사용량)을 씁니다 │
╰──────────────────────────────────────────────────────────╯
모델·effort·fast 위에 올렸을 때
/context처럼 컨텍스트를 무엇이 얼마나 차지하는지 많은 순서대로 최대 8개 보여줍니다. 대화가 한 번 끝날 때마다 새로 계산하고, 로컬에서 어림하므로 API를 따로 부르지 않습니다./model, /effort, /fast를 직접 입력한 것처럼 실행합니다. 지금 쓰는 값은 강조색으로 보입니다. 모델이나 effort를 클릭하면 같은 내용의 선택 창이 열리고, 그 창에서는 숫자 키(모델 1~4, effort 5~9, fast 0)와 Esc도 쓸 수 있습니다./fast를 직접 쳤든 빨간색으로 표시하고 이유를 메뉴에 적습니다./model, /effort, /fast를 직접 쳐도 늦어도 2초 안에 반영됩니다.[Image #1]이 생기면 바로 위에 [ Image 1 ] 버튼이 생깁니다.[Image #1]을 지우면 목록에서도 빠지고, 메시지를 보내면 목록이 비워집니다.원본 파일은 이렇게 찾습니다.
%TEMP%\orca-paste-….png로 저장합니다.[Image #N]만 들어오면 %TEMP%에서 15초 안에 새로 생긴 이미지 파일을 씁니다.Windows의 Orca 터미널에서 확인했습니다. 다른 터미널은 이미지를 붙여 넣는 방식이 달라 원본을 못 찾을 수 있고, macOS와 Linux에서는 이미지가 열리지 않습니다.
git clone https://github.com/glglekdy/claude-mods
claude --plugin-dir ./claude-mods/total-stats
claude plugin validate ./claude-mods/total-stats
claude plugin test ./claude-mods/total-statshooks/register.tsx 102 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Peek } from '../types'
5
6const images = atom({ plugin: 'image-peek', key: 'images' } as const, [])
7
8// 붙여 넣은 글에 들어 있는 이미지 파일 경로 (따옴표로 감싸였어도)
9const IMAGE_PATH = /[A-Za-z]:\\[^"'\r\n]*?\.(?:png|jpe?g|gif|webp|bmp)/i
10// Claude Code가 입력창에 넣는 자리표시자: [Image #1]
11const PLACEHOLDER = /\[Image #(\d+)\]/g
12// 자리표시자가 생긴 순간에 만들어진 파일로 볼 시간 범위
13const FRESH_MS = 15_000
14
15// PowerShell 작은따옴표 문자열 안에 넣을 수 있게 합니다.
16const quote = (s: string) => `'${s.replace(/'/g, "''")}'`
17
18const basename = (path: string) => path.slice(Math.max(path.lastIndexOf('\\'), path.lastIndexOf('/')) + 1)
19
20// 자리표시자 번호 → 원본 파일 경로 (못 찾으면 '')
21let sources = new Map<number, string>()
22
23// 붙여 넣은 직후 임시 폴더에 새로 생긴 이미지 파일을 찾습니다.
24async function freshTempImage($: EngineInterface) {
25 const temp = (await $.env.get('TEMP')) ?? (await $.env.get('TMP'))
26 if (!temp) return ''
27 const now = await $.clock.now()
28 const entries = await $.fs.list(temp).catch(() => [])
29 const newest = entries
30 .filter(f => f.kind === 'file' && /\.(?:png|jpe?g|gif|webp|bmp)$/i.test(f.name) && now - f.mtimeMs < FRESH_MS)
31 .sort((a, b) => b.mtimeMs - a.mtimeMs)[0]
32 return newest ? `${temp}\\${newest.name}` : ''
33}
34
35async function track($: EngineInterface, inputText: string, draft: string) {
36 const pasted = IMAGE_PATH.exec(inputText)?.[0] ?? ''
37
38 const list: Peek[] = []
39 for (const [, n] of draft.matchAll(PLACEHOLDER)) {
40 const num = Number(n)
41 if (!sources.has(num)) sources.set(num, pasted || (await freshTempImage($)))
42 list.push({ label: `Image ${num}`, path: sources.get(num) ?? '' })
43 }
44 // 터미널이 경로를 글자 그대로 넣은 경우도 목록에 올립니다.
45 const raw = IMAGE_PATH.exec(draft)?.[0]
46 if (raw) list.push({ label: basename(raw), path: raw })
47
48 const current = await read($, images)
49 if (JSON.stringify(current) !== JSON.stringify(list)) await update($, images, () => list)
50}
51
52async function openImage($: EngineInterface, path: string) {
53 if (!path) {
54 $.ui.toast('이 이미지의 원본 파일을 찾지 못했어요')
55 return
56 }
57 const { exitCode } = await $.process.run([
58 'powershell.exe', '-NoProfile', '-NonInteractive', '-Command', `Start-Process -FilePath ${quote(path)}`,
59 ])
60 if (exitCode !== 0) $.ui.toast('이미지를 열지 못했어요')
61}
62
63export const register: Register = on => {
64 sources = new Map()
65
66 // 입력창이 바뀔 때마다, 지금 들어 있는 이미지 자리표시자로 목록을 맞춥니다.
67 on('prompt.edit', async ($, e, next) => {
68 const ran = await next(e)
69 const isImageEdit = IMAGE_PATH.test(e.inputText) || e.inputText.includes('[Image') ||
70 e.text.includes('[Image') || ran.text.includes('[Image') || IMAGE_PATH.test(ran.text)
71 if (isImageEdit) await track($, e.inputText, ran.text)
72 return ran
73 }).catch(($, e, next) => next(e))
74
75 // 보내고 나면 입력창이 비므로 목록도 비웁니다.
76 on('prompt.submit', async ($, e, next) => {
77 const ran = await next(e)
78 await update($, images, () => [])
79 return ran
80 }).catch(($, e, next) => next(e))
81
82 // 아래 플러그인(예: total-stats)이 그린 줄을 감싸서 함께 보여줍니다.
83 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
84 const list = await read($, images)
85 if (e.props.hasSurvey || list.length === 0) return next(e)
86
87 const below = await next(e)
88 const { Box, Button, Text } = $.ui.resolve(e)
89 return (
90 <Box flexDirection="column">
91 <Box gap={1}>
92 <Text dimColor>📎</Text>
93 {list.map(img => (
94 <Button key={`${img.label}:${img.path}`} label={img.label} onPress={() => openImage($, img.path)} />
95 ))}
96 </Box>
97 {below}
98 </Box>
99 )
100 })
101}
102types/index.d.ts 11 lines1export type Peek = {
2 label: string
3 path: string
4}
5
6declare module 'claude-code' {
7 interface PluginState {
8 'image-peek': { images: Peek[] }
9 }
10}
11