SLOPSHOPPER

next-steps

턴이 끝나면 다음에 할 일을 최대 4개 제안하고, 체크박스로 골라 입력창에 채운다

newbandcommandtoastpromptmodel
v0.1.0no licenseupdated 2026-10-11urban6/my-harness/mods/next-steps
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · next-steps
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /next-steps ⎿ next-steps: 다음 스텝을 정리하고 있어요. 다음 스텝 정리 중… ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
다음 스텝 정리 중… ⟨Claude Code's own drawing⟩
README

my-harness

새 백엔드 프로젝트를 시작할 때 가져다 쓰는 Claude Code 기본 세팅입니다.

에이전트, 스킬, 커맨드를 이 저장소에서 만들고 검증한 뒤, install.sh로 전역(~/.claude)이나 프로젝트(.claude/)에 심링크를 걸어 씁니다. 다루는 범위는 백엔드(REST API, 서비스, 영속성)이고, 특정 프로젝트나 스택에 묶이지 않게 작성합니다.

구조

my-harness/
├── agents/      # 서브에이전트
├── skills/      # 스택별 구현 패턴
├── commands/    # 슬래시 커맨드
├── rules/       # 규칙 (경로별 지침)
├── hooks/       # 훅 (폴더 하나 = 훅 하나, install.sh가 settings에 병합)
├── mods/        # Claude Code 모드 (폴더 하나 = 모드 하나, install.sh가 전역 settings에 등록)
├── plugins/     # 외부 플러그인 목록 (파일 하나 = 플러그인 하나, install.sh가 claude plugin install로 설치)
├── prompts/     # 복사해 쓰는 프롬프트 (설치 대상 아님)
├── docs/        # 플러그인 사용법, guide/ AI 사용 가이드 (설치 대상 아님)
├── playground/  # 방식별 비교 실험 (설치 대상 아님)
├── install.sh
└── CLAUDE.md

에이전트

기능 개발 — feature-pm이 나머지 넷을 순서대로 부립니다.

이름하는 일
feature-pm요구사항 분해, 스택 확정, 워커 조율 (메인 세션에서 직접 호출)
backend-designerAPI와 DB 스키마를 함께 설계
backend-impl설계를 코드로 구현
boundary-verifier설계와 구현이 서로 맞는지 검증
test-writer단위·통합 테스트 작성

진단 — 코드를 직접 고치지 않고 원인과 개선안만 알려 줍니다.

이름하는 일
architecture-expert구조·의존성 진단, 기술 선택 비교
code-reviewer가독성, 네이밍, 에러 처리, 중복 리뷰
debugger버그 재현, 근본 원인 특정
performance-optimizer병목, N+1, 복잡도 진단
security-auditor인증·인가, 인젝션 등 보안 점검

스킬

이름하는 일
spring-bootSpring Boot 3.x 구현 패턴
nestjsNestJS 구현 패턴
screen-spec-analyzer화면 캡처나 화면정의서에서 API 응답 필드 추출

커맨드

이름하는 일
/commit변경을 논리 단위로 나눠 커밋
/push보호 브랜치와 강제 푸시를 확인하고 푸시
/pre-prPR 올리기 전 코드 리뷰 (테스트·린트·진단 후 BLOCK/READY 판정)
/post-prPR의 CI 결과와 리뷰 코멘트 대응, 머지 후 브랜치 정리

모드

이름하는 일
next-steps턴이 끝나면 다음 할 일을 최대 4개 제안, 체크박스로 골라 입력창에 채움 (mods/README.md)
context-bar입력창 위에 컨텍스트 창 사용량을 카테고리별 색 막대와 비율로 표시 (mods/README.md)
agent-monitor서브에이전트의 상태·경과 시간·도구 호출 수·토큰을 사이드 패널에 실시간 표시 (mods/README.md)

프롬프트 예시

프롬프트를 복사해 {빈칸}만 채워 씁니다.

상황프롬프트
기능 개발 (설계 → 구현 → 테스트)prompts/feature-development.md
커밋·배포 전 코드 리뷰prompts/code-review.md
화면 캡처로 API 응답 설계prompts/screen-spec.md

디버깅 · 성능 개선

{증상 — 무엇이 언제부터 어떻게}. 순서대로 진행해줘.

1) debugger로 재현 경로를 잡고 문제 구간을 file:line으로 특정
2) performance-optimizer로 {관찰 지점}을 포함해 병목을 우선순위로 진단
3) architecture-expert 관점에서 구조적 개선안을 트레이드오프로 비교

셋 다 진단만 하고 코드는 고치지 마. 마지막에 안 A/B/C로 정리해줘.

플러그인

AI 사용 가이드

설치

./install.sh            # 대화형: 대상·유형·항목을 골라 설치
./install.sh list       # 설치 상태 확인
./install.sh uninstall  # 우리 심링크만 제거

스크립트에서 쓰거나 세부 옵션(--type, --project, --force, --dry-run 등)이 필요하면 ./install.sh -h를 본다.

Source 3 files
hooks/register.tsx 289 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, ModelForkResult, Register, RenderElement } from 'claude-code'
3
4import type { Mode, Phase, UiSettings, Usage } from '../types'
5import {
6  addUsage,
7  applyUi,
8  askFor,
9  compactUsage,
10  composePrompt,
11  DEFAULT_LIMIT,
12  DEFAULT_UI,
13  formatTokens,
14  formatUi,
15  formatUsage,
16  NO_USAGE,
17  normalizeUi,
18  parseSteps,
19  recentTranscript,
20  toggle,
21} from './steps'
22
23const phase = atom({ plugin: 'next-steps', key: 'phase' } as const, 'idle' as Phase)
24const steps = atom({ plugin: 'next-steps', key: 'steps' } as const, [] as string[])
25const picked = atom({ plugin: 'next-steps', key: 'picked' } as const, [] as number[])
26// 제안을 요청한 턴. 새 프롬프트가 들어오면 비워서 늦게 도착한 제안을 버린다.
27const turnId = atom({ plugin: 'next-steps', key: 'turnId' } as const, '')
28const isOn = atom({ plugin: 'next-steps', key: 'isOn' } as const, true)
29const mode = atom({ plugin: 'next-steps', key: 'mode' } as const, 'haiku' as Mode)
30const limit = atom({ plugin: 'next-steps', key: 'limit' } as const, DEFAULT_LIMIT)
31// 자동 제안을 건너뛰었을 때의 컨텍스트 크기. 밴드 안내에 쓴다.
32const contextTokens = atom({ plugin: 'next-steps', key: 'contextTokens' } as const, 0)
33// 제안 생성 호출의 토큰 사용량. 이 세션 동안만 모은다.
34const lastUsage = atom({ plugin: 'next-steps', key: 'lastUsage' } as const, null as Usage | null)
35const totalUsage = atom({ plugin: 'next-steps', key: 'totalUsage' } as const, NO_USAGE)
36const calls = atom({ plugin: 'next-steps', key: 'calls' } as const, 0)
37const ui = atom({ plugin: 'next-steps', key: 'ui' } as const, DEFAULT_UI as UiSettings)
38
39const STORE_IS_ON = 'isOn'
40const STORE_MODE = 'mode'
41const STORE_LIMIT = 'limit'
42const STORE_UI = 'ui'
43const HELP = '사용법: /next-steps [on|off|usage|model haiku|fork|limit <토큰, 0은 끔>|ui [count|bold|gap|tokens <값>]]'
44
45async function clear($: EngineInterface) {
46  await update($, turnId, () => '')
47  await update($, phase, () => 'idle' as Phase)
48  await update($, steps, () => [])
49  await update($, picked, () => [])
50}
51
52async function ask($: EngineInterface): Promise<ModelForkResult> {
53  const prompt = askFor((await read($, ui)).count)
54  if ((await read($, mode)) === 'fork') return $.model.fork({ prompt })
55
56  const transcript = recentTranscript(await $.session.messages())
57  if (transcript === '') return { isAnswered: false, reason: 'nothing-to-fork' }
58  return $.model.complete({
59    model: 'haiku',
60    prompt: `<대화>\n${transcript}\n</대화>\n\n${prompt}`,
61    maxTokens: 500,
62  })
63}
64
65async function suggest($: EngineInterface, id: string) {
66  await update($, turnId, () => id)
67  await update($, steps, () => [])
68  await update($, picked, () => [])
69  await update($, phase, () => 'loading' as Phase)
70
71  const reply = await ask($)
72  // 버려진 호출도 비용은 들었으니 누적에는 넣는다.
73  if ('usage' in reply) {
74    const usage = reply.usage
75    await update($, totalUsage, total => addUsage(total, usage))
76    await update($, calls, n => n + 1)
77  }
78  if ((await read($, turnId)) !== id) return
79
80  // 밴드에 보이는 토큰은 지금 보이는 목록을 만든 호출의 것이어야 한다.
81  if ('usage' in reply) {
82    const usage = reply.usage
83    await update($, lastUsage, () => usage)
84  }
85  const found = reply.isAnswered ? parseSteps(reply.text, (await read($, ui)).count) : []
86  await update($, steps, () => found)
87  await update($, phase, () => (found.length > 0 ? 'ready' : 'idle') as Phase)
88}
89
90// fork는 대화 전체를 보내므로, 대화가 기준보다 길면 자동 제안은 건너뛴다.
91async function isTooLong($: EngineInterface): Promise<boolean> {
92  const max = await read($, limit)
93  if ((await read($, mode)) !== 'fork' || max <= 0) return false
94
95  const tokens = (await $.session.usage()).context.tokens ?? 0
96  if (tokens <= max) return false
97
98  await clear($)
99  await update($, contextTokens, () => tokens)
100  await update($, phase, () => 'skipped' as Phase)
101  return true
102}
103
104async function fill($: EngineInterface) {
105  const list = await read($, steps)
106  const chosen = (await read($, picked)).map(i => list[i]).filter((s): s is string => !!s)
107  if (chosen.length === 0) {
108    $.ui.toast('먼저 번호로 항목을 골라 주세요')
109    return
110  }
111
112  const filled = await $.prompt.fill({ text: composePrompt(chosen), mode: 'insert' })
113  if (filled.isFilled) await clear($)
114  else $.ui.toast('입력창에 넣지 못했어요')
115}
116
117export const register: Register = on => {
118  on('session.start', async ($, e, next) => {
119    const storedIsOn = await $.store.get(STORE_IS_ON)
120    const storedMode = await $.store.get(STORE_MODE)
121    const storedLimit = await $.store.get(STORE_LIMIT)
122    await update($, isOn, () => storedIsOn !== false)
123    await update($, mode, () => (storedMode === 'fork' ? 'fork' : 'haiku') as Mode)
124    await update($, limit, () => (typeof storedLimit === 'number' ? storedLimit : DEFAULT_LIMIT))
125    const storedUi = normalizeUi(await $.store.get(STORE_UI))
126    await update($, ui, () => storedUi)
127    await $.command.register({
128      name: 'next-steps',
129      description: '다음 스텝 제안: 인자 없으면 지금 제안, on/off, usage, model haiku|fork, limit <토큰>, ui <항목> <값>',
130      argumentHint: '[on|off|usage|model|limit|ui]',
131    })
132
133    // 모드가 다시 로드되면 진행 중이던 호출은 끊기지만 $.state는 남는다.
134    // "정리 중"인 채로 남은 턴이 있으면 이어서 다시 요청한다.
135    const pending = await read($, turnId)
136    if (pending !== '' && (await read($, phase)) === 'loading') {
137      void suggest($, pending)
138    }
139
140    return next(e)
141  })
142
143  on('command.run', { command: 'next-steps' }, async ($, e) => {
144    const [sub = '', value = ''] = e.args.trim().toLowerCase().split(/\s+/)
145
146    if (sub === 'on' || sub === 'off') {
147      const isOnNow = sub === 'on'
148      await update($, isOn, () => isOnNow)
149      await $.store.set(STORE_IS_ON, isOnNow)
150      if (!isOnNow) await clear($)
151      return { text: `다음 스텝 자동 제안을 ${isOnNow ? '켰어요' : '껐어요'}.` }
152    }
153    if (sub === 'model') {
154      if (value !== 'haiku' && value !== 'fork') return { text: HELP }
155      await update($, mode, () => value as Mode)
156      await $.store.set(STORE_MODE, value)
157      return {
158        text: value === 'haiku'
159          ? '다음 스텝을 Haiku로 만들어요. 최근 대화만 보내서 비용이 낮고 일정해요.'
160          : '다음 스텝을 세션 모델로 만들어요. 대화 전체를 보고 만들어서 정확하지만, 대화가 길수록 비용이 늘어요.',
161      }
162    }
163    if (sub === 'limit') {
164      const max = Number(value)
165      if (value === '' || !Number.isInteger(max) || max < 0) return { text: HELP }
166      await update($, limit, () => max)
167      await $.store.set(STORE_LIMIT, max)
168      return {
169        text: max === 0
170          ? '대화 길이와 상관없이 자동 제안해요.'
171          : `fork 모드에서 컨텍스트가 ${formatTokens(max)} 토큰을 넘으면 자동 제안을 건너뛰어요.`,
172      }
173    }
174    if (sub === 'ui') {
175      const [, key = '', value = ''] = e.args.trim().toLowerCase().split(/\s+/)
176      const current = await read($, ui)
177      if (key === '') return { text: `다음 스텝 화면: ${formatUi(current)}\n바꾸기: /next-steps ui count 1~9 · bold on|off · gap 0~2 · tokens on|off` }
178      const changed = applyUi(current, key, value)
179      if (typeof changed === 'string') return { text: changed }
180      await update($, ui, () => changed)
181      await $.store.set(STORE_UI, changed)
182      return { text: `다음 스텝 화면: ${formatUi(changed)}${key === 'count' ? ' (항목 수는 다음 제안부터)' : ''}` }
183    }
184    if (sub === 'usage') {
185      const max = await read($, limit)
186      const settings = `설정: 모델 ${await read($, mode)} · 자동 제안 기준 ${max === 0 ? '없음' : `${formatTokens(max)} 토큰 (fork일 때)`}`
187      const last = await read($, lastUsage)
188      if (last === null) return { text: `${settings}\n이 세션에서는 아직 다음 스텝을 만든 적이 없어요.` }
189      return {
190        text: [
191          settings,
192          `마지막 호출: ${formatUsage(last)}`,
193          `이 세션 누적 (${await read($, calls)}회): ${formatUsage(await read($, totalUsage))}`,
194        ].join('\n'),
195      }
196    }
197    if (sub !== '') return { text: HELP }
198
199    // 직접 요청은 길이 기준과 상관없이 만든다.
200    void suggest($, `manual-${await $.clock.now()}`)
201    return { text: '다음 스텝을 정리하고 있어요.' }
202  })
203
204  // 새 프롬프트가 나가면 이전 제안은 의미가 없다.
205  on('prompt.submit', async ($, e, next) => {
206    await clear($)
207    return next(e)
208  })
209
210  on('turn.complete', async ($, e, next) => {
211    const done = await next(e)
212    const isMainAnswer = e.agentId === undefined && e.reason === 'answer'
213    if (isMainAnswer && (await read($, isOn)) && !(await isTooLong($))) {
214      // 턴 종료를 붙잡지 않도록 제안 생성은 기다리지 않는다.
215      void suggest($, e.turnId)
216    }
217
218    return done
219  })
220
221  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
222    // 다른 모드(context-bar 등)가 그린 밴드. 제안이 있으면 그 아래에 쌓는다.
223    const below = await next(e)
224    if (e.props.hasSurvey || e.props.isWorking) return below
225
226    const now = await read($, phase)
227    if (now === 'idle') return below
228
229    const { Box, Button, Text } = $.ui.resolve(e)
230    const withBelow = (mine: RenderElement) => (
231      <Box flexDirection="column">
232        {mine}
233        {below}
234      </Box>
235    )
236
237    if (now === 'loading') return withBelow(<Text dimColor>다음 스텝 정리 중…</Text>)
238    if (now === 'skipped') {
239      const tokens = formatTokens(await read($, contextTokens))
240      return withBelow(
241        <Text dimColor>
242          대화가 길어(약 {tokens} 토큰) 자동 제안을 건너뛰었어요 · /next-steps로 직접 요청
243        </Text>,
244      )
245    }
246
247    const list = await read($, steps)
248    const chosen = await read($, picked)
249    const usage = await read($, lastUsage)
250    const current = await read($, mode)
251    const look = await read($, ui)
252
253    return withBelow(
254      <Box flexDirection="column" gap={look.gap}>
255        <Text dimColor>다음 스텝 — 숫자로 고르고 0으로 입력창에 채우기</Text>
256        {list.map((step, i) => (
257          <Button
258            key={`step-${i}`}
259            plain
260            hotkey={String(i + 1)}
261            onPress={() => update($, picked, list => toggle(list, i))}
262          >
263            <Text bold={look.bold}>
264              {chosen.includes(i) ? '[x] ' : '[ ] '}
265              {step}
266            </Text>
267          </Button>
268        ))}
269        <Box flexDirection="row">
270          <Button
271            key="fill"
272            variant="primary"
273            hotkey="0"
274            label={`입력창에 채우기 (${chosen.length})`}
275            onPress={() => fill($)}
276          />
277          <Text> </Text>
278          <Button key="dismiss" role="dismiss" label="닫기" onPress={() => clear($)} />
279        </Box>
280        {look.tokens && usage !== null && (
281          <Text dimColor>
282            토큰 {compactUsage(usage)} · {current}
283          </Text>
284        )}
285      </Box>,
286    )
287  })
288}
289
hooks/steps.ts 135 lines
1import type { UiSettings, Usage } from '../types'
2
3export const DEFAULT_UI: UiSettings = { count: 4, bold: true, gap: 1, tokens: true }
4// 항목 단축키가 숫자 한 자리(1~9)라서 9개까지.
5export const MAX_COUNT = 9
6export const MAX_GAP = 2
7
8// count는 상한일 뿐이다. 모델은 "최대 N개"를 꽉 채우려 해서 채우지 말라고 따로 못 박는다.
9export const askFor = (count: number) => `지금까지의 대화를 바탕으로, 사용자가 바로 이어서 요청할 만한 다음 작업을 제안해. 많아야 ${count}개.
10- 확실히 유용한 것만 넣는다. 개수를 채우려고 만들지 말 것. 1~2개만 유용하면 1~2개만 낸다.
11- 유용하다는 건 방금 대화의 흐름에서 사용자가 실제로 다음에 할 법한 일이라는 뜻이다. 막연한 일반론(문서화·테스트·리팩터링 등)을 대화와 상관없이 덧붙이지 않는다.
12- 각 항목은 사용자가 그대로 AI에게 보낼 수 있는 한 문장짜리 요청문(명령형)으로, 60자 안팎.
13- 대화에서 쓰인 언어로 쓴다.
14- 서로 겹치지 않게, 가치가 큰 순서로.
15- 이어서 할 일이 없거나 대화가 마무리됐으면 빈 배열.
16JSON 문자열 배열 하나만 출력하고 다른 말은 하지 마.`
17
18// 모델 답에서 첫 JSON 배열을 꺼낸다. 코드 펜스나 앞뒤 군말이 붙어도 견딘다.
19export function parseSteps(text: string, max = DEFAULT_UI.count): string[] {
20  const found = text.match(/\[[\s\S]*\]/)
21  if (!found) return []
22  try {
23    const value: unknown = JSON.parse(found[0])
24    if (!Array.isArray(value)) return []
25    return value
26      .filter((step): step is string => typeof step === 'string')
27      .map(step => step.trim())
28      .filter(step => step.length > 0)
29      .slice(0, max)
30  } catch {
31    return []
32  }
33}
34
35export function composePrompt(steps: string[]): string {
36  if (steps.length === 1) return steps[0] ?? ''
37  return ['다음을 순서대로 진행해 줘:', ...steps.map((step, i) => `${i + 1}. ${step}`)].join('\n')
38}
39
40export function toggle(picked: number[], index: number): number[] {
41  return picked.includes(index)
42    ? picked.filter(i => i !== index)
43    : [...picked, index].sort((a, b) => a - b)
44}
45
46export const NO_USAGE: Usage = {
47  input_tokens: 0,
48  output_tokens: 0,
49  cache_read_input_tokens: 0,
50  cache_creation_input_tokens: 0,
51}
52
53export function addUsage(a: Usage, b: Usage): Usage {
54  return {
55    input_tokens: a.input_tokens + b.input_tokens,
56    output_tokens: a.output_tokens + b.output_tokens,
57    cache_read_input_tokens: a.cache_read_input_tokens + b.cache_read_input_tokens,
58    cache_creation_input_tokens: a.cache_creation_input_tokens + b.cache_creation_input_tokens,
59  }
60}
61
62export function formatUsage(u: Usage): string {
63  const n = (v: number) => v.toLocaleString('en-US')
64  return `입력 ${n(u.input_tokens)} · 캐시 읽기 ${n(u.cache_read_input_tokens)} · 캐시 쓰기 ${n(u.cache_creation_input_tokens)} · 출력 ${n(u.output_tokens)}`
65}
66
67// fork 모드에서 컨텍스트가 이보다 크면 자동 제안을 건너뛴다. 0이면 건너뛰지 않는다.
68export const DEFAULT_LIMIT = 100_000
69
70const PER_MESSAGE_CHARS = 2_000
71const TRANSCRIPT_CHARS = 24_000
72
73// Haiku에 보낼 최근 대화. 뒤에서부터 메시지당·전체 글자 수를 잘라 비용을 일정하게 둔다.
74export function recentTranscript(messages: readonly { role: 'user' | 'assistant'; text: string }[]): string {
75  const lines: string[] = []
76  let size = 0
77  for (const message of [...messages].reverse()) {
78    const text = message.text.trim()
79    if (text === '') continue
80    const cut = text.length > PER_MESSAGE_CHARS ? `${text.slice(0, PER_MESSAGE_CHARS)}…` : text
81    const line = `${message.role === 'user' ? '사용자' : 'AI'}: ${cut}`
82    if (size + line.length > TRANSCRIPT_CHARS) break
83    lines.unshift(line)
84    size += line.length
85  }
86  return lines.join('\n\n')
87}
88
89export function formatTokens(n: number): string {
90  if (n < 1_000) return String(n)
91  return `${(n / 1_000).toFixed(n < 10_000 ? 1 : 0)}k`
92}
93
94// 밴드 한 줄용: 보낸 토큰 전체(그중 캐시 읽기)와 받은 토큰.
95export function compactUsage(u: Usage): string {
96  const sent = u.input_tokens + u.cache_read_input_tokens + u.cache_creation_input_tokens
97  const cached = u.cache_read_input_tokens > 0 ? ` (캐시 ${formatTokens(u.cache_read_input_tokens)})` : ''
98  return `입력 ${formatTokens(sent)}${cached} · 출력 ${formatTokens(u.output_tokens)}`
99}
100
101// $.store에서 읽은 값을 믿지 않고 범위 안의 설정으로 맞춘다.
102export function normalizeUi(value: unknown): UiSettings {
103  const v = (typeof value === 'object' && value !== null ? value : {}) as Partial<Record<keyof UiSettings, unknown>>
104  const int = (x: unknown, min: number, max: number, fallback: number) =>
105    typeof x === 'number' && Number.isInteger(x) && x >= min && x <= max ? x : fallback
106  return {
107    count: int(v.count, 1, MAX_COUNT, DEFAULT_UI.count),
108    bold: typeof v.bold === 'boolean' ? v.bold : DEFAULT_UI.bold,
109    gap: int(v.gap, 0, MAX_GAP, DEFAULT_UI.gap),
110    tokens: typeof v.tokens === 'boolean' ? v.tokens : DEFAULT_UI.tokens,
111  }
112}
113
114// `/next-steps ui <key> <value>` 한 건을 적용한다. 값이 틀리면 이유 문자열을 돌려준다.
115export function applyUi(ui: UiSettings, key: string, value: string): UiSettings | string {
116  const onOff = value === 'on' ? true : value === 'off' ? false : undefined
117  const n = Number(value)
118  switch (key) {
119    case 'count':
120      return value !== '' && Number.isInteger(n) && n >= 1 && n <= MAX_COUNT ? { ...ui, count: n } : `count는 1~${MAX_COUNT} 사이 정수예요.`
121    case 'gap':
122      return value !== '' && Number.isInteger(n) && n >= 0 && n <= MAX_GAP ? { ...ui, gap: n } : `gap은 0~${MAX_GAP} 사이 정수예요.`
123    case 'bold':
124      return onOff === undefined ? 'bold는 on 또는 off예요.' : { ...ui, bold: onOff }
125    case 'tokens':
126      return onOff === undefined ? 'tokens는 on 또는 off예요.' : { ...ui, tokens: onOff }
127    default:
128      return `바꿀 수 있는 항목은 count·bold·gap·tokens예요. 글자 크기는 앱(Cmd +)이나 터미널 글꼴 설정으로 바꿔 주세요.`
129  }
130}
131
132export function formatUi(ui: UiSettings): string {
133  return `항목 수 ${ui.count} · 굵게 ${ui.bold ? 'on' : 'off'} · 간격 ${ui.gap} · 토큰 줄 ${ui.tokens ? 'on' : 'off'}`
134}
135
types/index.d.ts 39 lines
1export type Phase = 'idle' | 'loading' | 'ready' | 'skipped'
2
3// haiku: 최근 대화만 잘라 Haiku에 보낸다(싸고 비용이 일정). fork: 대화 전체를 세션 모델로(정확하지만 길수록 비쌈).
4export type Mode = 'haiku' | 'fork'
5
6// 다음 스텝 밴드 표시 설정. 글자 크기는 화면(앱·터미널)이 정해서 여기 없다.
7export type UiSettings = {
8  count: number
9  bold: boolean
10  gap: number
11  tokens: boolean
12}
13
14export type Usage = {
15  input_tokens: number
16  output_tokens: number
17  cache_read_input_tokens: number
18  cache_creation_input_tokens: number
19}
20
21declare module 'claude-code' {
22  interface PluginState {
23    'next-steps': {
24      phase: Phase
25      steps: string[]
26      picked: number[]
27      turnId: string
28      isOn: boolean
29      lastUsage: Usage | null
30      totalUsage: Usage
31      calls: number
32      mode: Mode
33      limit: number
34      contextTokens: number
35      ui: UiSettings
36    }
37  }
38}
39