Claude's replies in a window headed CLAUDE; sentences that guess (아마, 추정, probably, might …) marked [?], with a footer counting them

Claude's replies draw in a window headed CLAUDE, and a sentence that guesses (아마, 추정, ~것 같다, ~로 보입니다, probably, might, I think …) is marked [?], so what was checked and what is a guess read apart:
╭───────────────────────────────────────────────╮
│ CLAUDE │
│ 테스트 3개 중 2개가 실패합니다. │
│ [?] 아마 issuedAt 단위가 원인일 것입니다. │
│ ? 추정 1곳 · 확인되기 전까지는 가설 │
╰───────────────────────────────────────────────╯
Markdown is kept (the reply is drawn with the engine's Markdown element); code blocks and inline code are not marked. The stored reply is unchanged (ctrl+o shows it as written).
Part of the GAME MODE pack. Requires Claude Code 2.1.287+; built and tested on 2.1.291.
claude plugin marketplace add Reasonofmoon/bitgame-mods
claude plugin install game-hedge-mark@bitgame-mods
intensity (casual) | off · casual (the window and the marks) · hardcore (the marks alone, in the engine's own drawing) |
palette (nes) | nes · gameboy · amber · theme |
다음과 같습니다 is left alone, 원인으로 보입니다 is marked.Claude의 답을 CLAUDE 창으로 그리고, 추측하는 문장(아마·추정·~것 같다·probably…)에 [?]를 붙여 확인된 사실과 가설을 구분합니다. 아래에 추정 개수를 셉니다.
hooks/register.tsx 75 lines1import type { Register } from 'claude-code'
2
3import { intensityOf, paletteOf } from './palette'
4
5// GAME MODE · HEDGE MARK
6//
7// Claude's replies draw in a window headed CLAUDE, and a sentence that guesses
8// (아마, 추정, 짐작, ~것 같다, ~라고 본다, probably, might …) is marked [?], so what is checked and what is a
9// guess read apart:
10// ╭─────────────────────────────────────────────╮
11// │ CLAUDE │
12// │ 테스트 3개 중 2개가 실패합니다. │
13// │ [?] 아마 issuedAt 단위가 원인일 것입니다. │
14// │ ? 추정 1곳 · 확인되기 전까지는 가설 │
15// Code blocks are left as they are. The stored reply is unchanged (ctrl+o shows it).
16
17const KOREAN = /아마(?:도)?|추정|추측|짐작|(?:라고|으로|로|고)\s?(?:본다|봅니다|봐요)|것\s?같(?:습니다|다|아요|네요|은데)|(?<![과와])\s같(?:습니다|아요|네요)|듯(?:합니다|하다|해요|싶)|수도\s?있|(?:았|었|였|됐|했|겼|졌)을\s?수\s?(?:도\s?)?있|일\s?수\s?(?:도\s?)?있|가능성(?:이|도)\s?(?:있|높|크)|(?:로|으로|처럼)\s?보입니다|확실하지\s?않|확실치\s?않|모르겠/
18const ENGLISH = /\b(?:probably|likely|perhaps|maybe|might|presumably|seems?|appears? to|i think|i believe|i guess|i suspect|not sure|could be)\b/i
19const MARK = '**[?]** '
20
21export const register: Register = (on, options) => {
22 const intensity = intensityOf(options.intensity)
23 const pal = paletteOf(options.palette)
24
25 on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
26 if (intensity === 'off' || e.props.isSummary === true) return next(e)
27 const { text, count } = markHedges(e.props.text)
28 // hardcore: the marks alone, in the engine's own drawing.
29 if (intensity === 'hardcore') return count === 0 ? next(e) : next({ ...e, props: { ...e.props, text } })
30 const { Box, Markdown, Text } = $.ui.resolve(e)
31 return (
32 <Box flexDirection="column" borderStyle="round" borderColor={pal.frame} paddingX={1}>
33 {e.props.isFirstOfReply && (
34 <Text color={pal.title} bold>
35 CLAUDE
36 </Text>
37 )}
38 <Markdown text={text} />
39 {count > 0 && <Text color={pal.warn}>{`? 추정 ${count}곳 · 확인되기 전까지는 가설`}</Text>}
40 </Box>
41 )
42 })
43}
44
45export function isHedge(sentence: string): boolean {
46 return KOREAN.test(sentence) || ENGLISH.test(sentence)
47}
48
49/** The reply with each guessing sentence marked `[?]`, and how many were marked; code is left alone. */
50export function markHedges(markdown: string): { text: string; count: number } {
51 let count = 0
52 let isCode = false
53 const lines = markdown.split('\n').map(line => {
54 if (/^\s*(?:```|~~~)/.test(line)) {
55 isCode = !isCode
56 return line
57 }
58 if (isCode || /^ {4}|^\t/.test(line) || line.trim() === '') return line
59 const lead = /^\s*(?:[-*+]\s+|\d+[.)]\s+|#{1,6}\s+|>\s*)*/.exec(line)?.[0] ?? ''
60 const body = line.slice(lead.length)
61 const sentences = body.split(/(?<=[.!?。])\s+/)
62 const marked = sentences.map(s => {
63 if (!isHedge(withoutCode(s))) return s
64 count++
65 return MARK + s
66 })
67 return lead + marked.join(' ')
68 })
69 return { text: lines.join('\n'), count }
70}
71
72function withoutCode(s: string): string {
73 return s.replace(/`[^`]*`/g, '')
74}
75hooks/palette.ts 124 lines1// GAME MODE shared look (copied into each GAME MODE plugin: a hooks module
2// may only import files of its own plugin). Change every copy together;
3// scripts/check.sh fails when the copies differ.
4//
5// nes / gameboy / amber are raw colors made for dark terminals, taken from
6// the design boards. `theme` uses Claude Code's theme keys, so it follows the
7// person's theme: pick it on a light background. Its `win` is empty: windows
8// draw with no fill there.
9
10export type PaletteName = 'nes' | 'gameboy' | 'amber' | 'theme'
11
12export type Palette = {
13 /** Headings, gold, the level. */
14 title: string
15 ok: string
16 warn: string
17 bad: string
18 info: string
19 dim: string
20 text: string
21 /** A window's border. */
22 frame: string
23 /** A window's fill; '' for none. */
24 win: string
25 /** The bright inner edge of a window, and a highlighted badge's text. */
26 edge: string
27 /** A bar's empty cells. */
28 off: string
29}
30
31const PALETTES: Record<PaletteName, Palette> = {
32 nes: {
33 title: '#f8d830',
34 ok: '#58d854',
35 warn: '#ff8a6a',
36 bad: '#f83800',
37 info: '#3cbcfc',
38 dim: '#b8b8f0',
39 text: '#fcfcfc',
40 frame: '#5060d8',
41 win: '#1c2a8a',
42 edge: '#fcfcfc',
43 off: '#2c3a98',
44 },
45 gameboy: {
46 title: '#e0f8d0',
47 ok: '#9bbc0f',
48 warn: '#e0f8d0',
49 bad: '#f8f8a0',
50 info: '#c4e890',
51 dim: '#8bac0f',
52 text: '#e0f8d0',
53 frame: '#9bbc0f',
54 win: '#1e4a1e',
55 edge: '#e0f8d0',
56 off: '#306230',
57 },
58 amber: {
59 title: '#ffd060',
60 ok: '#ffb000',
61 warn: '#ff7040',
62 bad: '#ff5a2a',
63 info: '#ffe0a0',
64 dim: '#c89040',
65 text: '#ffd9a0',
66 frame: '#ffb000',
67 win: '#2a1700',
68 edge: '#ffe0a0',
69 off: '#4a2a00',
70 },
71 theme: {
72 title: 'claude',
73 ok: 'success',
74 warn: 'warning',
75 bad: 'error',
76 info: 'suggestion',
77 dim: 'subtle',
78 text: 'text',
79 frame: 'promptBorder',
80 win: '',
81 edge: 'text',
82 off: 'subtle',
83 },
84}
85
86export function paletteOf(name: unknown): Palette {
87 return name === 'gameboy' || name === 'amber' || name === 'theme' ? PALETTES[name] : PALETTES.nes
88}
89
90export type Intensity = 'off' | 'casual' | 'hardcore'
91
92export function intensityOf(value: unknown): Intensity {
93 return value === 'off' || value === 'hardcore' ? value : 'casual'
94}
95
96/** A bar of `cells` blocks, `percent` of them full. */
97export function bar(percent: number, cells: number): string {
98 const full = Math.max(0, Math.min(cells, Math.round((percent / 100) * cells)))
99 return '█'.repeat(full) + '░'.repeat(cells - full)
100}
101
102/** Box props for a window in this palette: a double border and, where the palette has one, a fill. */
103export function windowProps(pal: Palette): { borderStyle: 'double'; borderColor: string; backgroundColor?: string; paddingX: number } {
104 return pal.win === ''
105 ? { borderStyle: 'double', borderColor: pal.frame, paddingX: 1 }
106 : { borderStyle: 'double', borderColor: pal.frame, backgroundColor: pal.win, paddingX: 1 }
107}
108
109/** `m:ss`, or `h:mm:ss` past an hour. */
110export function clock(ms: number): string {
111 const s = Math.max(0, Math.round(ms / 1000))
112 const h = Math.floor(s / 3600)
113 const m = Math.floor((s % 3600) / 60)
114 const sec = s % 60
115 const two = (n: number) => String(n).padStart(2, '0')
116 return h > 0 ? `${h}:${two(m)}:${two(sec)}` : `${m}:${two(sec)}`
117}
118
119/** A cost in the chosen currency: `$1.23`, or `₩1,722` at `krwPerUsd`. */
120export function money(usd: number, currency: 'usd' | 'krw', krwPerUsd: number): string {
121 if (currency === 'krw') return '₩' + String(Math.round(usd * krwPerUsd)).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
122 return '$' + usd.toFixed(2)
123}
124