Band above the prompt showing the state of every cross-review review and consult this session has started

A Claude Code mod that draws a band above the prompt showing the cross-review reviews and consults the session has started, one row each, and where each one stands:
cross-review (running) fix-autocrlf t2 running 4m12s (x)
(changes) feat-x t1 changes requested . 3 open
(ok) docs t3 converged
(failed) other RATE_LIMITED
Running jobs come first with their elapsed time (redrawn every 5s while one runs), then the three most recently finished. The close button at the right drops the finished rows. The band sits above whatever else is drawn there (another mod's band, the engine's own) rather than replacing it.
| Glyph (drawn as) | Meaning |
|---|---|
| U+27F3 cyan | running |
| U+2713 green | converged, or a consult that was answered |
| U+270E yellow | changes_requested, with the open-finding count |
| U+26A0 red | escalate or rebaseline |
| U+2717 red | the job failed, with its code (RATE_LIMITED, SESSION_BUSY, ...) |
| U+2298 gray | cancelled |
The glyphs are written as \uXXXX escapes in the source, so every file in this folder is plain ASCII.
It is not part of the cross-review binary and the server knows nothing about it. It only watches the calls the agent makes to the cross_model_* tools and reads their responses: the review_id: and session: lines of a start, and the machine envelope of a result (structuredContent, or the CROSS_REVIEW_ENVELOPE_OUT block in the text). It never changes a call or its result. Any MCP server name works; it matches on the tool name's cross_model_* suffix.
In this repository it loads by itself: Claude Code loads a plugin from a project's .claude/skills/<name>/ folder. To use it in another repository, copy this folder into that repository's .claude/skills/, or into ~/.claude/skills/ to have it everywhere. Nothing is built or installed.
The mod API is early access and may change between Claude Code releases. If the band stops appearing after an update, run claude plugin validate .claude/skills/cross-review-status.
claude plugin validate .claude/skills/cross-review-status
claude plugin test .claude/skills/cross-review-status
The tests feed the hooks canned server responses through the engine; they call no model. Claude Code writes the API's type declarations into .claude-plugin/types/ when it loads the mod (gitignored), which tsconfig.json extends for an editor.
hooks/register.tsx 303 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, Timer } from 'claude-code'
3
4import type { Review, ReviewKind } from '../types'
5
6const reviews = atom({ plugin: 'cross-review-status', key: 'reviews' } as const, {})
7// The time the band draws elapsed times against, advanced by a timer while a job runs.
8const clock = atom({ plugin: 'cross-review-status', key: 'now' } as const, 0)
9
10// Any server name: `.mcp.json` calls it `cross-review`, but a user may not.
11const TOOL = /^mcp__.+__cross_model_(review|consult)(_result|_cancel)?$/
12const ENVELOPE = /<<<CROSS_REVIEW_ENVELOPE_OUT:([^>\n]+)>>>\n([\s\S]*?)\n<<<CROSS_REVIEW_ENVELOPE_OUT_END:\1>>>/
13// Finished jobs kept in the band beside every running one.
14const FINISHED_SHOWN = 3
15const TICK_MS = 5_000
16
17type Record_ = Record<string, unknown>
18
19const field = (text: string, name: string): string | null =>
20 new RegExp(`^${name}:\\s+(.+)$`, 'm').exec(text)?.[1]?.trim() ?? null
21
22const asRecord = (v: unknown): Record_ | null =>
23 typeof v === 'object' && v !== null && !Array.isArray(v) ? (v as Record_) : null
24
25/**
26 * The machine envelope: `structuredContent` when core hands it over, else the `_OUT` text block,
27 * else the text itself. When a tool declares an output schema, core gives the model (and so `text`)
28 * the structured result serialised as JSON in place of the text body, with no `_OUT` block in it.
29 */
30const envelopeOf = (result: unknown, text: string): Record_ | null => {
31 const structured = asRecord(asRecord(result)?.structuredContent)
32 if (structured) return structured
33 const json = ENVELOPE.exec(text)?.[2] ?? (text.trimStart().startsWith('{') ? text : undefined)
34 if (json === undefined) return null
35 try {
36 return asRecord(JSON.parse(json))
37 } catch {
38 return null
39 }
40}
41
42const num = (v: unknown): number | null => (typeof v === 'number' ? v : null)
43const str = (v: unknown): string | null => (typeof v === 'string' ? v : null)
44
45const elapsed = (ms: number): string => {
46 const s = Math.max(0, Math.round(ms / 1000))
47 if (s < 60) return `${s}s`
48 const m = Math.floor(s / 60)
49 if (m < 60) return `${m}m${String(s % 60).padStart(2, '0')}s`
50 return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, '0')}m`
51}
52
53type Look = { glyph: string; color: string; label: string }
54
55const look = (r: Review, now: number): Look => {
56 switch (r.status) {
57 case 'running':
58 return { glyph: '\u27F3', color: 'cyan', label: `running ${elapsed(now - r.startedAt)}` }
59 case 'cancelled':
60 return { glyph: '\u2298', color: 'gray', label: 'cancelled' }
61 case 'failed':
62 return { glyph: '\u2717', color: 'red', label: r.code ?? 'failed' }
63 case 'completed':
64 if (r.kind === 'consult' || r.outcome === null) return { glyph: '\u2713', color: 'green', label: 'answered' }
65 if (r.outcome === 'converged') return { glyph: '\u2713', color: 'green', label: 'converged' }
66 if (r.outcome === 'changes_requested') {
67 const open = r.openCount === null ? '' : ` \u00B7 ${r.openCount} open`
68 return { glyph: '\u270E', color: 'yellow', label: `changes requested${open}` }
69 }
70 return { glyph: '\u26A0', color: 'red', label: r.outcome.replace(/_/g, ' ') }
71 }
72}
73
74/** Running jobs, then the most recently finished ones. */
75const shown = (all: Review[]): Review[] => {
76 const running = all.filter(r => r.status === 'running').sort((a, b) => a.startedAt - b.startedAt)
77 const finished = all
78 .filter(r => r.status !== 'running')
79 .sort((a, b) => (b.finishedAt ?? b.startedAt) - (a.finishedAt ?? a.startedAt))
80 return [...running, ...finished.slice(0, FINISHED_SHOWN)]
81}
82
83const tick = async ($: EngineInterface) => {
84 const now = await $.clock.now()
85 await update($, clock, () => now)
86}
87
88// Advances `now` while a job runs, and only then: armed when one starts, cancelled once none is
89// left. A reload drops it with the old module; `session.start` arms it again if a job still runs.
90let ticker: Timer | null = null
91
92const pace = async ($: EngineInterface) => {
93 const isRunning = Object.values((await read($, reviews)) as Record<string, Review>).some(
94 r => r.status === 'running',
95 )
96 if (isRunning && ticker === null) {
97 ticker = $.clock.every(TICK_MS, () => void tick($).then(() => pace($)))
98 } else if (!isRunning && ticker !== null) {
99 ticker.cancel()
100 ticker = null
101 }
102}
103
104const put = async ($: EngineInterface, r: Review) => {
105 await update($, reviews, prev => ({ ...(prev as Record<string, Review>), [r.session]: r }))
106 await tick($)
107 await pace($)
108}
109
110const clearFinished = ($: EngineInterface) =>
111 update($, reviews, prev =>
112 Object.fromEntries(Object.entries(prev as Record<string, Review>).filter(([, r]) => r.status === 'running')),
113 )
114
115/**
116 * The job a call names, looked up as the server does (src/tools.rs): by `review_id` when one is
117 * given, and only by `session` when it is not.
118 */
119const target = async (
120 $: EngineInterface,
121 reviewId: string | null,
122 session: string | null,
123): Promise<Review | null> => {
124 const all = (await read($, reviews)) as Record<string, Review>
125 if (reviewId !== null) return Object.values(all).find(r => r.reviewId === reviewId) ?? null
126 return session === null ? null : (all[session] ?? null)
127}
128
129// The suffix the server prints after a session name on a result: `(turn 2)`, `(turn 1, new review)`.
130const RESULT_SESSION_SUFFIX = / \(turn \d+(?:, [a-z ]+)?\)$/
131
132export const register: Register = on => {
133 on('session.start', async ($, e, next) => {
134 // 0.1.0 pinned a status line, which outlives a reload of the module; take it down.
135 $.ui.status(undefined)
136 await tick($)
137 await pace($)
138 return next(e)
139 })
140
141 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
142 // Whatever the plugins beneath and the engine draw stays, below this plugin's rows.
143 const below = await next(e)
144 const rows = shown(Object.values((await read($, reviews)) as Record<string, Review>))
145 if (e.props.hasSurvey || rows.length === 0) return below
146
147 const now = await read($, clock)
148 const { Box, Button, Text } = $.ui.resolve(e)
149 const hasFinished = rows.some(r => r.status !== 'running')
150
151 return (
152 <Box flexDirection="column">
153 <Box flexDirection="row" gap={2}>
154 <Box flexShrink={0}>
155 <Text bold>cross-review</Text>
156 </Box>
157 <Box flexDirection="column" flexGrow={1} flexShrink={1} minWidth={0}>
158 {rows.map(r => {
159 const { glyph, color, label } = look(r, now)
160 return (
161 <Box key={`row:${r.session}`} flexDirection="row" gap={1}>
162 <Box flexShrink={0}>
163 <Text color={color}>{glyph}</Text>
164 </Box>
165 <Box flexShrink={1} minWidth={0}>
166 <Text wrap="truncate-end">{r.kind === 'consult' ? `consult ${r.session}` : r.session}</Text>
167 </Box>
168 {r.turn === null ? null : (
169 <Box flexShrink={0}>
170 <Text dimColor>{`t${r.turn}`}</Text>
171 </Box>
172 )}
173 <Box flexShrink={0}>
174 <Text color={color}>{label}</Text>
175 </Box>
176 </Box>
177 )
178 })}
179 </Box>
180 {hasFinished ? (
181 <Box flexShrink={0}>
182 {/* Drawn as the X other bands close with: the desktop does not yet draw role="dismiss" itself. */}
183 <Button key="clear" label={'\u2715'} plain dimColor role="dismiss" onPress={() => clearFinished($)} />
184 </Box>
185 ) : null}
186 </Box>
187 {below}
188 </Box>
189 )
190 })
191
192 on('tool.call', async ($, e, next) => {
193 const match = TOOL.exec(e.tool)
194 if (!match) return next(e)
195
196 const kind = match[1] as ReviewKind
197 const verb = match[2] ?? 'start'
198 const args = e as unknown as Record_
199 const ran = await next(e)
200 const text = ran.text ?? ''
201 const now = await $.clock.now()
202
203 if (verb === 'start') {
204 const session = field(text, 'session')
205 const reviewId = field(text, 'review_id')
206 const asked = str(args.session) ?? 'review'
207 if (ran.deny !== undefined || ran.isError || !reviewId || !session) {
208 // A refused start (SESSION_BUSY, most often) leaves the job already running untouched.
209 if ((await target($, null, asked))?.status === 'running') return ran
210 await put($, {
211 session: asked,
212 reviewId: reviewId ?? '',
213 kind,
214 turn: null,
215 status: 'failed',
216 outcome: null,
217 openCount: null,
218 code: field(text, 'code') ?? (ran.deny !== undefined ? 'denied' : null),
219 startedAt: now,
220 finishedAt: now,
221 })
222 return ran
223 }
224 // `session: name (new)` or `session: name (resumed, turn 3)`
225 const parsed = /^(.*) \((?:new|resumed, turn (\d+))\)$/.exec(session)
226 await put($, {
227 session: parsed?.[1] ?? asked,
228 reviewId,
229 kind,
230 turn: parsed?.[2] ? Number(parsed[2]) : 1,
231 status: 'running',
232 outcome: null,
233 openCount: null,
234 code: null,
235 startedAt: now,
236 finishedAt: null,
237 })
238 return ran
239 }
240
241 const known = await target($, str(args.review_id), str(args.session))
242 // Only an answered call carries an envelope; an error's structured body says nothing about a job.
243 const env = ran.deny === undefined && !ran.isError ? envelopeOf(ran.result, text) : null
244 // A bare-JSON result has no `review_id:` line and may not carry the id, so the call's own
245 // argument names the job it answered.
246 const reviewId =
247 field(text, 'review_id') ?? str(env?.review_id) ?? known?.reviewId ?? (env ? str(args.review_id) : null)
248 const session = field(text, 'session')?.replace(RESULT_SESSION_SUFFIX, '') ?? str(env?.session)
249 // A job this mod never saw is recorded only from a response that names it; an error for one
250 // (an unknown or evicted id) says nothing about any job the band shows.
251 if (!known && (ran.deny !== undefined || ran.isError || !reviewId || !session)) return ran
252 // A job started before this mod loaded: rebuild what we can from the response.
253 const base: Review = known ? { ...known, reviewId: reviewId ?? known.reviewId } : {
254 session: session ?? '',
255 reviewId: reviewId ?? '',
256 kind,
257 turn: null,
258 status: 'running',
259 outcome: null,
260 openCount: null,
261 code: null,
262 startedAt: now,
263 finishedAt: null,
264 }
265
266 if (verb === '_cancel') {
267 if (ran.deny === undefined && !ran.isError) {
268 await put($, { ...base, status: 'cancelled', finishedAt: now })
269 }
270 return ran
271 }
272
273 if (ran.deny !== undefined) return ran
274 if (ran.isError) {
275 await put($, { ...base, status: 'failed', code: field(text, 'code'), finishedAt: now })
276 return ran
277 }
278
279 const status = str(env?.result_status) ?? str(env?.status) ?? field(text, 'status')
280 const turn = num(env?.turn) ?? base.turn
281
282 if (status === 'running') {
283 const elapsed = num(env?.elapsed_seconds)
284 await put($, {
285 ...base,
286 turn,
287 status: 'running',
288 startedAt: elapsed === null ? base.startedAt : now - elapsed * 1000,
289 })
290 } else if (status === 'completed') {
291 await put($, {
292 ...base,
293 turn,
294 status: 'completed',
295 outcome: str(env?.outcome),
296 openCount: num(env?.open_count),
297 finishedAt: now,
298 })
299 }
300 return ran
301 })
302}
303types/index.d.ts 26 lines1export type ReviewKind = 'review' | 'consult'
2
3export type ReviewStatus = 'running' | 'completed' | 'failed' | 'cancelled'
4
5/** The latest job seen for one cross-review session name. */
6export type Review = {
7 session: string
8 reviewId: string
9 kind: ReviewKind
10 turn: number | null
11 status: ReviewStatus
12 /** The envelope's `outcome` (converged, changes_requested, escalate, rebaseline). */
13 outcome: string | null
14 openCount: number | null
15 /** The failure code (RATE_LIMITED, SESSION_NOT_RESUMABLE, ...) when the job failed. */
16 code: string | null
17 startedAt: number
18 finishedAt: number | null
19}
20
21declare module 'claude-code' {
22 interface PluginState {
23 'cross-review-status': { reviews: Record<string, Review>; now: number }
24 }
25}
26