Show a file in a pane and leave inline comments the model can read

This is where I back up random projects and tools I make for myself. It doubles as a Claude Code plugin marketplace to make it easier to sync back to my Claude.
| Plugin | Description |
|---|---|
inline-review | Show a file or a git diff in a side pane and leave inline comments the model can read |
deslop | Find AI-authored slop in a change with read-only finder agents, grade each finding with a verifier, then fix the ones you pick |
/plugin marketplace add elayeek/elayeek-ai
/plugin install inline-review@elayeek-ai
/plugin install deslop@elayeek-ai
Outside a session, use claude plugin marketplace add … and claude plugin install ….
inline-review is a mod and needs Claude Code 2.1.287 or later.
Run claude plugin update. Auto-update is off by default for this marketplace.
To release, raise version in the plugin's .claude-plugin/plugin.json.
Add this to the project's .claude/settings.json:
{
"extraKnownMarketplaces": {
"elayeek-ai": { "source": { "source": "github", "repo": "elayeek/elayeek-ai" } }
},
"enabledPlugins": { "inline-review@elayeek-ai": true }
}hooks/register.tsx 322 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Change, DiffItem, ReviewFile, Side } from '../types'
5
6const PANE = 'inline-review'
7const SHOW = 'mcp__inline-review__show_file'
8const LIST = 'mcp__inline-review__list_comments'
9const MAX_FILE = 80_000
10const MAX_CODE = 10_000
11
12const file = atom({ plugin: 'inline-review', key: 'file' } as const, null)
13const comments = atom({ plugin: 'inline-review', key: 'comments' } as const, [])
14const changes = atom({ plugin: 'inline-review', key: 'changes' } as const, null)
15
16const basename = (path: string) => path.split('/').pop() || path
17const dirname = (path: string) => path.slice(0, path.lastIndexOf('/')) || '/'
18const message = (error: unknown) => (error instanceof Error ? error.message : String(error))
19// Code rejects control characters other than tab and newline.
20const clean = (line: string) => line.replace(/[\u0000-\u0008\u000a-\u001f\u007f]/g, ' ').slice(0, MAX_CODE)
21const splitLines = (source: string) => {
22 const lines = source.split('\n')
23 if (lines.length > 1 && lines[lines.length - 1] === '') lines.pop()
24 return lines
25}
26
27const HUNK = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/
28
29// Rows of a unified diff, with the old and new line number of each; file headers
30// and "\ No newline" notes are dropped.
31const parseDiff = (text: string): DiffItem[] => {
32 const items: DiffItem[] = []
33 let inHunk = false
34 let o = 0
35 let n = 0
36 let isFirst = false
37 let header = ''
38 for (const raw of text.split('\n')) {
39 const head = HUNK.exec(raw)
40 if (head) {
41 inHunk = true
42 header = clean(raw)
43 isFirst = true
44 o = Number(head[1])
45 n = Number(head[2])
46 continue
47 }
48 if (raw.startsWith('diff ')) inHunk = false
49 if (!inHunk || raw === '' || raw.startsWith('\\')) continue
50 const m = raw[0]
51 if (m !== ' ' && m !== '+' && m !== '-') continue
52 const item: DiffItem = { m, t: clean(raw.slice(1)), o: m === '+' ? 0 : o, n: m === '-' ? 0 : n }
53 if (isFirst) item.header = header
54 isFirst = false
55 items.push(item)
56 if (m !== '+') o += 1
57 if (m !== '-') n += 1
58 }
59
60 return items
61}
62
63type Opened = { error: string } | { text: string; title: string }
64type Engine = EngineInterface
65
66const resolvePath = async ($: Engine, path: string) =>
67 path.startsWith('/') ? path : `${await $.session.cwd()}/${path.replace(/^\.\//, '')}`
68
69const repoRoot = async ($: Engine, dir: string): Promise<string | null> => {
70 const r = await $.process.run(['git', 'rev-parse', '--show-toplevel'], { cwd: dir })
71
72 return r.exitCode === 0 ? r.stdout.trim() : null
73}
74
75// Shows a file whole, or its diff against `base`, and clears the comments.
76const openFile = async ($: Engine, input: { path: string; base?: string; fromList?: boolean }): Promise<Opened> => {
77 const path = await resolvePath($, input.path)
78 const base = input.base === undefined || input.base === '' ? null : input.base
79 if (base !== null && base.startsWith('-')) return { error: `Bad base ${base}.` }
80 let items: DiffItem[] = []
81 let source = ''
82 let note: string | null = null
83
84 if (base !== null) {
85 const root = await repoRoot($, dirname(path))
86 if (root === null) return { error: `${path} is not in a git repository.` }
87 let diff = await $.process.run(['git', 'diff', '--no-color', base, '--', path], { cwd: root })
88 if (diff.exitCode !== 0) return { error: `git diff failed: ${diff.stderr.trim() || `exit ${diff.exitCode}`}` }
89 if (diff.stdout.trim() === '') {
90 const other = await $.process.run(['git', 'ls-files', '--others', '--exclude-standard', '--', path], { cwd: root })
91 if (other.stdout.trim() !== '') {
92 // An untracked file is all added; exit code 1 means "differences" here.
93 diff = await $.process.run(['git', 'diff', '--no-color', '--no-index', '--', '/dev/null', path], { cwd: root })
94 if (diff.exitCode > 1) return { error: `git diff failed: ${diff.stderr.trim() || `exit ${diff.exitCode}`}` }
95 }
96 }
97 if (diff.stdout.length > MAX_FILE) return { error: `The diff of ${path} is too large for this prototype.` }
98 items = parseDiff(diff.stdout)
99 if (items.length === 0) note = `No changes against ${base}`
100 }
101
102 if (base === null || items.length === 0) {
103 try {
104 source = await $.fs.read(path)
105 } catch (error) {
106 return { error: `Cannot read ${path}: ${message(error)}` }
107 }
108 if (source.length > MAX_FILE) {
109 return { error: `${path} is too large for this prototype (${source.length} characters, limit ${MAX_FILE}).` }
110 }
111 }
112
113 const shown: ReviewFile = {
114 path,
115 base,
116 lines: splitLines(source).map(clean),
117 items,
118 note,
119 fromList: input.fromList === true,
120 }
121 await update($, file, () => shown)
122 await update($, comments, () => [])
123 const isDiff = items.length > 0
124
125 return {
126 title: isDiff ? `Diff: ${basename(path)} vs ${base}` : `Review: ${basename(path)}`,
127 text: isDiff ? `Diff of ${path} against ${base}` : `Reviewing ${path}`,
128 }
129}
130
131const openChanges = async ($: Engine): Promise<Opened> => {
132 const root = await repoRoot($, await $.session.cwd())
133 if (root === null) return { error: 'The working directory is not in a git repository.' }
134 const status = await $.process.run(['git', 'status', '--porcelain'], { cwd: root })
135 if (status.exitCode !== 0) return { error: `git status failed: ${status.stderr.trim() || `exit ${status.exitCode}`}` }
136 const list: Change[] = status.stdout
137 .split('\n')
138 .filter(l => l.length > 3)
139 .map(l => {
140 const path = l.slice(3).split(' -> ').pop() as string
141
142 return { code: l.slice(0, 2).trim(), path, abs: `${root}/${path}` }
143 })
144 await update($, file, () => null)
145 await update($, comments, () => [])
146 await update($, changes, () => list)
147
148 return { title: 'Changes', text: list.length === 0 ? 'No changes.' : `${list.length} changed files` }
149}
150
151export const register: Register = on => {
152 on('session.start', async ($, e, next) => {
153 await $.tool.register({
154 name: 'show_file',
155 description:
156 'Show a file in the Review pane so the user can leave inline comments on it. With base (a git ref such as HEAD), show the diff of the file against that ref instead.',
157 inputSchema: {
158 type: 'object',
159 properties: {
160 path: { type: 'string', description: 'Absolute path of the file' },
161 base: { type: 'string', description: 'Optional git ref to diff against; omit for the whole file' },
162 },
163 required: ['path'],
164 },
165 })
166 await $.tool.register({
167 name: 'list_comments',
168 description: "List the user's inline comments on the file shown in the Review pane, then clear them.",
169 inputSchema: { type: 'object', properties: {} },
170 })
171 await $.command.register({
172 name: 'inline-review',
173 description: 'Open the inline review pane, or review a file',
174 argumentHint: '[path]',
175 })
176 await $.command.register({
177 name: 'inline-diff',
178 description: 'Review the diff of a file, or list the changed files',
179 argumentHint: '[path] [base]',
180 })
181
182 return next(e)
183 })
184
185 on('command.run', { command: 'inline-review' }, async ($, e) => {
186 const [path] = e.args.trim().split(/\s+/).filter(Boolean)
187 if (path === undefined) {
188 await $.ui.open({ id: PANE, title: 'Review' })
189
190 return { text: 'Review pane opened.' }
191 }
192 const opened = await openFile($, { path })
193 if ('error' in opened) {
194 $.ui.toast(opened.error)
195
196 return { text: opened.error }
197 }
198 await $.ui.open({ id: PANE, title: opened.title })
199
200 return { text: opened.text }
201 })
202
203 on('command.run', { command: 'inline-diff' }, async ($, e) => {
204 const [path, base] = e.args.trim().split(/\s+/).filter(Boolean)
205 const opened = path === undefined ? await openChanges($) : await openFile($, { path, base: base ?? 'HEAD' })
206 if ('error' in opened) {
207 $.ui.toast(opened.error)
208
209 return { text: opened.error }
210 }
211 await $.ui.open({ id: PANE, title: opened.title })
212
213 return { text: opened.text }
214 })
215
216 on('tool.call', { tool: SHOW }, async ($, e) => {
217 const path = e.path
218 if (typeof path !== 'string' || path === '') return { deny: 'show_file needs an absolute file path.' }
219 const base = typeof e.base === 'string' && e.base !== '' ? e.base : undefined
220 const opened = await openFile($, { path, base })
221 if ('error' in opened) return { deny: opened.error }
222 await $.ui.open({ id: PANE, title: opened.title })
223
224 return {
225 result: `${base === undefined ? `Showing ${path}` : `Showing the diff of ${path} against ${base}`} in the Review pane. Call ${LIST} to read the user's comments.`,
226 }
227 })
228
229 on('tool.call', { tool: LIST }, async $ => {
230 const shown = await read($, file)
231 const list = [...(await read($, comments))].sort((a, b) => a.line - b.line || (a.side === b.side ? 0 : a.side === 'old' ? -1 : 1))
232 if (shown === null || list.length === 0) return { result: 'No comments yet.' }
233 await update($, comments, () => [])
234
235 const isDiff = shown.items.length > 0
236 const quote = (line: number, side: Side) => {
237 if (!isDiff) return shown.lines[line - 1] ?? ''
238 const it = shown.items.find(i => (side === 'old' ? i.m === '-' && i.o === line : i.m !== '-' && i.n === line))
239
240 return it ? `${it.m}${it.t}` : ''
241 }
242
243 return {
244 result: list
245 .map(c => {
246 const head = isDiff ? `${shown.path}:${c.line} (${c.side})` : `${shown.path}:${c.line}`
247
248 return `${head}: ${c.body.replace(/\n/g, '\n ')}\n > ${quote(c.line, c.side)}`
249 })
250 .join('\n'),
251 }
252 })
253
254 on('ui.message', { component: 'Pane', requestId: PANE, element: 'file' }, async ($, e) => {
255 const d = e.data as { line?: unknown; side?: unknown; body?: unknown } | null
256 if (d && typeof d.line === 'number' && (d.side === 'old' || d.side === 'new') && typeof d.body === 'string') {
257 const { line, body } = d
258 const side: Side = d.side
259 // One comment per line and side: a body replaces it, an empty body deletes it.
260 await update($, comments, c => {
261 const rest = c.filter(x => !(x.line === line && x.side === side))
262 return body === '' ? rest : [...rest, { line, side, body }]
263 })
264 }
265
266 return {}
267 })
268
269 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e, next) => {
270 // Only the terminal and desktop tables have both Client and Input.
271 if (e.surface !== 'terminal' && e.surface !== 'desktop') return next(e)
272 const { Box, Text, Client, Button } = $.ui.resolve(e)
273 const shown = await read($, file)
274 const changed = await read($, changes)
275
276 const open = async (path: string, base: string, fromList: boolean) => {
277 const opened = await openFile($, { path, base, fromList })
278 if ('error' in opened) $.ui.toast(opened.error)
279 else await $.ui.open({ id: PANE, title: opened.title })
280 }
281
282 if (shown === null && changed !== null) {
283 if (changed.length === 0) return <Text dimColor>No changes.</Text>
284
285 return (
286 <Box flexDirection="column">
287 {changed.map((c, i) => (
288 <Button key={`f-${i}`} plain label={`${c.code.padEnd(3)} ${c.path}`} onPress={() => open(c.abs, 'HEAD', true)} />
289 ))}
290 </Box>
291 )
292 }
293 if (shown === null) {
294 return <Text dimColor>No file shown. The model calls show_file to open one.</Text>
295 }
296 const list = await read($, comments)
297
298 return (
299 <Box flexDirection="column">
300 {shown.fromList && (
301 <Button
302 key="back"
303 plain
304 label="← Changes"
305 onPress={async () => {
306 const opened = await openChanges($)
307 if ('error' in opened) $.ui.toast(opened.error)
308 else await $.ui.open({ id: PANE, title: opened.title })
309 }}
310 />
311 )}
312 {shown.note !== null && <Text dimColor>{shown.note}</Text>}
313 <Client
314 key="file"
315 module="./file-view.tsx"
316 props={{ path: shown.path, isDiff: shown.items.length > 0, lines: shown.lines, items: shown.items, comments: list }}
317 />
318 </Box>
319 )
320 })
321}
322hooks/file-view.tsx 233 lines1import type { ClientModule } from 'claude-code'
2
3type Side = 'old' | 'new'
4type Item = { m: string; t: string; o: number; n: number; header?: string }
5type Sel = { line: number; side: Side }
6type Props = {
7 path: string
8 isDiff: boolean
9 lines: string[]
10 items: Item[]
11 comments: { line: number; side: Side; body: string }[]
12}
13// draft is null while no comment row is open.
14type State = { sel: Sel | null; draft: string | null }
15
16const MAX_CODE = 10_000
17const HUNK = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/
18
19// Every key and click commits here at once and then asks for a redraw, so keys
20// that arrive between two draws build on each other instead of on the last
21// drawn state. The module is loaded once per instance and reloads on a save.
22let live: State | null = null
23
24// Breaks at the last space within `width`, else hard-breaks at `width`.
25const wrapText = (text: string, width: number): string[] => {
26 const out: string[] = []
27 let rest = text
28 while (rest.length > width) {
29 const at = rest.lastIndexOf(' ', width)
30 if (at > 0) {
31 out.push(rest.slice(0, at))
32 rest = rest.slice(at + 1)
33 } else {
34 out.push(rest.slice(0, width))
35 rest = rest.slice(width)
36 }
37 }
38 out.push(rest)
39 return out
40}
41
42const keyOf = (it: Item): Sel => (it.m === '-' ? { line: it.o, side: 'old' } : { line: it.n, side: 'new' })
43const same = (a: Sel | null, b: Sel) => a !== null && a.line === b.line && a.side === b.side
44
45// Draws the source one screen row per source line (wrap truncate-end), so a
46// click's row maps to a line through `rowLine`. Comment, draft and hunk-header
47// rows map to null. Typing goes through onKey because a click gives this Client
48// the keys and an Input drawn inside it never receives them.
49const View: ClientModule<Props, State> = (props, s) => {
50 const { Box, Text, Code } = s.elements
51 const { path, isDiff, comments } = props
52 const all: Item[] = isDiff ? props.items : props.lines.map((t, i) => ({ m: '', t, o: 0, n: i + 1 }))
53 if (live === null || s.state === undefined) live = s.state ?? { sel: null, draft: null }
54 const commit = (next: State) => {
55 live = next
56 s.setState(next)
57 }
58 const { sel, draft } = live
59 // 0 before the first layout.
60 const columns = s.columns > 0 ? s.columns : 60
61
62 // Diff gutter: selection bar, old and new number columns, then a rule. Comment rows indent past it.
63 const digits = isDiff ? String(Math.max(1, ...all.map(i => Math.max(i.o, i.n)))).length : 0
64 const lead = isDiff ? ' '.repeat(2 * digits + 4) + '┃' : ''
65 const num = (v: number) => (v > 0 ? String(v) : '').padStart(digits)
66 const rows: JSX.Element[] = []
67 const rowLine: (Sel | null)[] = []
68 // One Text per screen row keeps `rowLine` exact; the continuation indent is the prefix width.
69 const pushWrapped = (key: string, color: string, first: string, text: string, at: Sel | null = null) => {
70 const indent = ' '.repeat(first.length)
71 wrapText(text, Math.max(8, columns - lead.length - first.length)).forEach((piece, k) => {
72 rows.push(<Text key={`${key}-${k}`} color={color} wrap="truncate-end">{`${lead}${k === 0 ? first : indent}${piece}`}</Text>)
73 rowLine.push(at)
74 })
75 }
76 const bodyOf = (k: Sel) => comments.find(c => c.line === k.line && c.side === k.side)?.body
77
78 let chunk: Item[] = []
79 let size = 0
80 // Whole-file view only: a diff row is its own one-row Text, drawn as the raw diff line.
81 const flush = () => {
82 if (chunk.length === 0) return
83 const first = chunk[0] as Item
84 const id = `${first.m}-${first.o}-${first.n}`
85 const code = <Code key={`code-${first.n}`} path={path} startLine={first.n} wrap="truncate-end" source={chunk.map(i => i.t).join('\n')} />
86 rows.push(chunk.length === 1 && same(sel, keyOf(first)) ? <Box key={`sel-${id}`} backgroundColor="blue">{code}</Box> : code)
87 chunk.forEach(i => rowLine.push(keyOf(i)))
88 chunk = []
89 }
90
91 // A diff run is contiguous rows of one tint: one tinted Box holding a gutter column beside one Code.
92 let run: Item[] = []
93 const flushRun = () => {
94 if (run.length === 0) return
95 const first = run[0] as Item
96 // A run is one row when selected, so `isSel` styles the whole run.
97 const isSel = same(sel, keyOf(first))
98 const base = first.m === '+' ? '#1f3d2b' : first.m === '-' ? '#4a2227' : undefined
99 const lit = first.m === '+' ? '#2a5a3c' : first.m === '-' ? '#6a3038' : '#2a3340'
100 const tint = isSel ? lit : base
101 const markColor = first.m === '+' ? 'greenBright' : first.m === '-' ? 'redBright' : undefined
102 rows.push(
103 <Box key={`run-${first.m}-${first.o}-${first.n}`} flexDirection="row" backgroundColor={tint}>
104 {/* Fixed-width, non-shrinking columns of one single-row Text each, so a gutter row can never wrap. */}
105 <Box flexDirection="column" width={1} flexShrink={0}>
106 {run.map((i, r) => (
107 <Text key={`b${r}`} color="cyan" wrap="truncate-end">{same(sel, keyOf(i)) ? '▌' : ' '}</Text>
108 ))}
109 </Box>
110 <Box flexDirection="column" width={2 * digits + 3} flexShrink={0}>
111 {run.map((i, r) => (
112 <Text key={`g${r}`} dimColor={first.m === ' ' && !isSel} bold={isSel} wrap="truncate-end">{`${num(i.m === '+' ? 0 : i.o)} ${num(i.m === '-' ? 0 : i.n)} │`}</Text>
113 ))}
114 </Box>
115 <Box flexDirection="column" width={1} flexShrink={0}>
116 {run.map((i, r) => (
117 <Text key={`m${r}`} color={markColor} bold={isSel} wrap="truncate-end">{i.m}</Text>
118 ))}
119 </Box>
120 <Box flexGrow={1} flexShrink={1}>
121 <Code path={path} wrap="truncate-end" source={run.map(i => i.t).join('\n')} />
122 </Box>
123 </Box>,
124 )
125 run.forEach(i => rowLine.push(keyOf(i)))
126 run = []
127 }
128
129 if (isDiff) {
130 const adds = all.filter(i => i.m === '+').length
131 const dels = all.filter(i => i.m === '-').length
132 rows.push(
133 <Box key="file-head" flexDirection="row">
134 <Box flexGrow={1}>
135 <Text bold wrap="truncate-end">{` ${path.split('/').pop() || path}`}</Text>
136 </Box>
137 <Text color="greenBright">{`+${adds}`}</Text>
138 <Text color="redBright">{` −${dels}`}</Text>
139 </Box>,
140 )
141 rowLine.push(null)
142 }
143
144 all.forEach(it => {
145 const k = keyOf(it)
146 if (isDiff) {
147 if (it.header !== undefined) {
148 flushRun()
149 const hunk = HUNK.exec(it.header)
150 const label = hunk ? `⋯ ${hunk[1]},${hunk[2] ?? 1} → ${hunk[3]},${hunk[4] ?? 1} ` : '⋯ '
151 rows.push(
152 <Box key={`sep-${it.o}-${it.n}`} flexDirection="row">
153 <Box flexShrink={0}>
154 <Text>{` ${label}`}</Text>
155 </Box>
156 <Box flexGrow={1}>
157 <Text dimColor wrap="truncate-end">{'─'.repeat(columns)}</Text>
158 </Box>
159 </Box>,
160 )
161 rowLine.push(null)
162 }
163 if (run.length > 0 && (run[0]?.m !== it.m || same(sel, k) || run.reduce((n, i) => n + i.t.length + 1, 0) + it.t.length > MAX_CODE)) flushRun()
164 run.push(it)
165 } else {
166 if (chunk.length > 0 && (size + it.t.length + 4 > MAX_CODE || same(sel, k))) flush()
167 chunk.push(it)
168 size += it.t.length + 4
169 }
170 const saved = bodyOf(k)
171 const isEditing = same(sel, k) && draft !== null
172 if (saved !== undefined || same(sel, k)) (isDiff ? flushRun : flush)()
173 if (saved !== undefined && !isEditing) {
174 const label = ` ↳ ${k.side === 'old' ? 'old ' : ''}${k.line}: `
175 saved.split('\n').forEach((part, j) => pushWrapped(`c-${k.side}${k.line}-${j}`, 'yellow', j === 0 ? label : ' '.repeat(label.length), part, k))
176 }
177 if (isEditing) {
178 const parts = draft.split('\n')
179 const hint = 'Enter to save'
180 parts.forEach((part, j) =>
181 pushWrapped(`draft-${j}`, 'green', j === 0 ? ' ✎ ' : ' ', j === parts.length - 1 ? `${part}█` : part),
182 )
183 rows.push(
184 <Text key="hint" dimColor wrap="truncate-end">
185 {` ${hint}`}
186 </Text>,
187 )
188 rowLine.push(null)
189 }
190 })
191 flush()
192 flushRun()
193
194 s.onPointer(e => {
195 if (e.type !== 'down' || e.button !== 'left') return
196 const k = rowLine[e.y]
197 if (k == null) return
198 const cur = live as State
199 // A second click on the open line cancels the draft and keeps the saved comment.
200 commit(same(cur.sel, k) && cur.draft !== null ? { sel: k, draft: null } : { sel: k, draft: bodyOf(k) ?? '' })
201 })
202 s.onKey(e => {
203 const { sel: at, draft: text } = live as State
204 if (text !== null && at !== null) {
205 if (e.ctrl || e.meta) return
206 if (e.key === 'return') {
207 if (e.shift) return commit({ sel: at, draft: text + '\n' })
208 const body = text.trim()
209 if (body === '' && bodyOf(at) === undefined) return
210 s.post({ line: at.line, side: at.side, body })
211 commit({ sel: at, draft: null })
212 } else if (e.key === 'backspace' || e.key === 'delete') {
213 commit({ sel: at, draft: text.slice(0, -1) })
214 } else if (e.key === 'space') {
215 commit({ sel: at, draft: text + ' ' })
216 } else if (e.key.length === 1) {
217 commit({ sel: at, draft: text + e.key })
218 }
219 return
220 }
221 if (e.key !== 'up' && e.key !== 'down') return
222 const idx = at === null ? -1 : all.findIndex(i => same(at, keyOf(i)))
223 const step = e.key === 'down' ? 1 : -1
224 const next = Math.min(Math.max((idx === -1 ? (step === 1 ? -1 : all.length) : idx) + step, 0), all.length - 1)
225 const target = all[next]
226 if (target) commit({ sel: keyOf(target), draft: null })
227 })
228
229 return <Box flexDirection="column">{rows}</Box>
230}
231
232export default View
233types/index.d.ts 25 lines1export type Side = 'old' | 'new'
2// One row of a diff: m is the marker, o and n the old and new line numbers
3// (0 where the row has none), header is the hunk header line on the first row of a hunk.
4export type DiffItem = { m: ' ' | '+' | '-'; t: string; o: number; n: number; header?: string }
5export type ReviewFile = {
6 path: string
7 base: string | null
8 lines: string[]
9 items: DiffItem[]
10 note: string | null
11 fromList: boolean
12}
13export type ReviewComment = { line: number; side: Side; body: string }
14export type Change = { code: string; path: string; abs: string }
15
16declare module 'claude-code' {
17 interface PluginState {
18 'inline-review': {
19 file: ReviewFile | null
20 comments: ReviewComment[]
21 changes: Change[] | null
22 }
23 }
24}
25