import { atom, read, update } from 'claude-code'
import type { EngineInterface, Register, RenderChildren } from 'claude-code'
import type { Correction, Corrections, Exception, RuleRef } from '../types'
const PANE = 'straion-trail'
const TITLE = 'Straion'
const POLL_MS = 1500
const SAFE_SESSION_ID = /^[A-Za-z0-9_-]{1,128}$/
const SIGNET_PX = 20
const TERMINAL_SIGNET_COLUMNS = 4
const TERMINAL_SIGNET_ROWS = 2
const SIGNET_SVG =
''
const trail = atom({ plugin: 'straion-trail', key: 'trail' } as const, null)
const followed = atom({ plugin: 'straion-trail', key: 'followed' } as const, null)
const showCompliant = atom({ plugin: 'straion-trail', key: 'showCompliant' } as const, false)
const showExceptions = atom({ plugin: 'straion-trail', key: 'showExceptions' } as const, false)
const isClosed = atom({ plugin: 'straion-trail', key: 'isClosed' } as const, false)
/** The parts of `straion audit view --json` (the CLI's `AuditModel`) the pane reads. */
type AuditRule = {
shortcode: string
statement: string
state: string
decision: string | null
reason: string | null
humanConfirmed: boolean
priorVerdicts: { state: string; reason: string | null }[]
}
type AuditModel = { rules: AuditRule[]; awaitingApproval?: string[] }
function firstSentence(text: string): string {
return text.split(/(?<=\.)\s/)[0]?.replace(/\.$/, '') ?? text
}
function findFixReason(rule: AuditRule): string | null {
if (rule.state === 'fix-required') return rule.reason
const fixVerdict = [...rule.priorVerdicts].reverse().find(prior => prior.state === 'fix-required')
return fixVerdict?.reason ?? null
}
function wasCorrected(rule: AuditRule): boolean {
return rule.decision === 'fix' || rule.priorVerdicts.some(prior => prior.state === 'fix-required')
}
function isApprovedException(rule: AuditRule): boolean {
return (rule.state === 'accepted' || rule.state === 'not-applicable') && rule.humanConfirmed
}
function deriveCorrections(sessionId: string, model: AuditModel): Corrections {
const ref = (rule: AuditRule): RuleRef => ({ shortcode: rule.shortcode, rule: firstSentence(rule.statement) })
const awaiting = new Set(model.awaitingApproval ?? [])
const corrections: Correction[] = model.rules
.filter(wasCorrected)
.map(rule => ({ ...ref(rule), fix: findFixReason(rule), isFixed: rule.state === 'compliant' }))
.sort((a, b) => Number(a.isFixed) - Number(b.isFixed))
return {
sessionId,
isFound: true,
ruleCount: model.rules.length,
awaiting: model.rules.filter(rule => awaiting.has(rule.shortcode)).map(ref),
corrections,
exceptions: model.rules
.filter(isApprovedException)
.map((rule): Exception => ({ ...ref(rule), kind: rule.state === 'accepted' ? 'accepted' : 'not-applicable' })),
compliant: model.rules.filter(rule => rule.state === 'compliant' && !wasCorrected(rule)).map(ref),
}
}
function summarize(trail: Corrections): string {
const count = trail.corrections.length
if (count === 0) return 'Straion kept everything on track'
return count === 1 ? 'Straion corrected the agent once' : `Straion corrected the agent ${count} times`
}
let lastFileKey = ''
let lastPaneRows = 0
let canDrawImages = false
let isPrimed = false
const seen = new Set()
function announce($: EngineInterface, next: Corrections): void {
const moments = [
...next.awaiting.map(item => ({ key: `ask:${item.shortcode}`, text: `Straion needs your approval: ${item.rule}` })),
...next.corrections.map(item => ({
key: `${item.isFixed ? 'fixed' : 'fix'}:${item.shortcode}`,
text: `${item.isFixed ? 'Fixed' : 'Straion corrected'}: ${item.rule}`,
})),
]
for (const moment of moments) {
if (isPrimed && !seen.has(moment.key)) $.ui.toast(moment.text)
seen.add(moment.key)
}
isPrimed = true
}
/** Null when the CLI could not answer; the pane keeps what it last showed. */
async function readAuditModel($: EngineInterface, sessionId: string): Promise {
const ran = await $.process.run(['straion', 'audit', 'view', sessionId, '--json']).catch(() => null)
if (ran === null || ran.exitCode !== 0) return null
try {
return JSON.parse(ran.stdout) as AuditModel
} catch {
return null
}
}
async function refresh($: EngineInterface): Promise {
const sessionId = (await read($, followed)) ?? (await $.session.id())
const home = await $.env.get('HOME')
if (!home || !SAFE_SESSION_ID.test(sessionId)) return
const path = `${home}/.straion/audit/${sessionId}.jsonl`
const stat = await $.fs.stat(path).catch(() => undefined)
const fileKey = stat ? `${path}:${stat.mtimeMs}:${stat.size}` : `${path}:missing`
if (fileKey === lastFileKey) return
if (!stat) {
lastFileKey = fileKey
await update($, trail, () => ({ sessionId, isFound: false, ruleCount: 0, awaiting: [], corrections: [], exceptions: [], compliant: [] }))
$.ui.status(undefined)
return
}
const model = await readAuditModel($, sessionId)
if (model === null) return
lastFileKey = fileKey
const next = deriveCorrections(sessionId, model)
announce($, next)
await update($, trail, () => next)
$.ui.status(next.corrections.length > 0 ? `Straion · ${next.corrections.length} corrected` : undefined)
await fitPane($)
}
async function follow($: EngineInterface, sessionId: string | null): Promise {
await update($, followed, () => sessionId)
lastFileKey = ''
isPrimed = false
seen.clear()
await refresh($)
}
async function detectImageSupport($: EngineInterface): Promise {
const term = await $.env.get('TERM')
const termProgram = await $.env.get('TERM_PROGRAM')
const kittyWindow = await $.env.get('KITTY_WINDOW_ID')
return term === 'xterm-kitty' || kittyWindow !== undefined || termProgram?.toLowerCase() === 'ghostty'
}
function countCorrectionLines(trail: Corrections): number {
const awaiting = trail.awaiting.length * 2
const corrections = trail.corrections.reduce(
(lines, correction) => lines + (correction.isFixed || correction.fix === null ? 1 : 2),
0,
)
return awaiting + corrections
}
/** The rows the compact terminal layout draws, so an inline pane opens exactly that tall. */
async function countPaneRows($: EngineInterface): Promise {
const current = await read($, trail)
const headerRows = 2
if (current === null || current.ruleCount === 0) return headerRows
const toggles = [
{ count: current.exceptions.length, isOpen: await read($, showExceptions) },
{ count: current.compliant.length, isOpen: await read($, showCompliant) },
].filter(toggle => toggle.count > 0)
const toggleRows = toggles.reduce((rows, toggle) => rows + 1 + (toggle.isOpen ? toggle.count : 0), 0)
const bodyRows = countCorrectionLines(current) + toggleRows
return bodyRows === 0 ? headerRows : headerRows + 1 + bodyRows
}
async function openPane($: EngineInterface): Promise {
lastPaneRows = await countPaneRows($)
await $.ui.open({ id: PANE, title: TITLE, rows: lastPaneRows })
}
async function fitPane($: EngineInterface): Promise {
if (await read($, isClosed)) return
if ((await countPaneRows($)) === lastPaneRows) return
await openPane($)
}
export const register: Register = on => {
on('session.start', async ($, e, next) => {
await $.command.register({
name: 'straion-trail',
description: 'Show what Straion corrected. "close" hides it; a session id replays that session, "this" comes back.',
})
canDrawImages = await detectImageSupport($)
await refresh($)
$.clock.every(POLL_MS, () => refresh($))
if (!(await read($, isClosed))) void openPane($)
return next(e)
})
on('command.run', { command: 'straion-trail' }, async ($, e) => {
const arg = e.args.trim()
if (arg === 'close') {
await update($, isClosed, () => true)
await $.ui.close({ id: PANE })
return { text: 'Straion pane closed. /straion-trail opens it again.' }
}
if (arg !== '' && arg !== 'this' && !SAFE_SESSION_ID.test(arg)) {
return { text: `"${arg}" is not a session id.` }
}
if (arg !== '') await follow($, arg === 'this' ? null : arg)
await update($, isClosed, () => false)
await openPane($)
return { text: 'Straion pane opened.' }
})
on('ui.close', async ($, e, next) => {
if (e.id === PANE && e.origin.kind === 'person') await update($, isClosed, () => true)
return next(e)
})
on('tool.call', async ($, e, next) => {
const ran = await next(e)
await refresh($)
return ran
})
on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
const { Box, Text, Button } = $.ui.resolve(e)
const current = await read($, trail)
const isShowingCompliant = await read($, showCompliant)
const isShowingExceptions = await read($, showExceptions)
const width = Math.max(20, e.props.bodyColumns ?? 60)
const isCompact = e.surface === 'terminal'
const itemGap = isCompact ? 0 : 1
const sectionGap = isCompact ? 1 : 2
const mark = (icon: string, color: string) => {
if (e.surface !== 'desktop') return {icon}
const { Svg } = $.ui.resolve(e)
return
}
const terminalMark = () => {
if (e.surface !== 'terminal' || !canDrawImages) return null
const { Image } = $.ui.resolve(e)
return (
)
}
const header = (icon: string, color: string, title: string, subline: string) =>
isCompact ? (
{terminalMark()}
{title}
{icon} {subline}
) : (
{mark(icon, color)}
{title}
{icon}
{subline}
)
const item = (marker: string, color: string, title: string, detail: string | null, isQuiet: boolean) => (
{marker}
{title}
{detail !== null ? {detail} : null}
)
const collapsible = (key: string, isOpen: boolean, label: string, onPress: () => unknown, rows: RenderChildren[]) => (
{isOpen ? {rows} : null}
)
if (current === null || current.ruleCount === 0) {
return (
{header('◇', 'green', 'Straion is watching', 'Corrections show up here')}
)
}
const fixed = current.corrections.filter(correction => correction.isFixed).length
const inProgress = current.corrections.length - fixed
const hero =
current.awaiting.length > 0
? { icon: '?', color: 'yellow' }
: inProgress > 0
? { icon: '●', color: 'red' }
: { icon: '✓', color: 'green' }
const subline =
current.corrections.length === 0
? `${current.ruleCount} rules checked`
: inProgress > 0
? `${fixed} fixed · ${inProgress} in progress`
: `all fixed · ${current.ruleCount} rules checked`
const listed = [
...current.awaiting.map(rule => (
{item('?', 'yellow', 'Your approval is needed', rule.rule, false)}
)),
...current.corrections.map(correction => (
{correction.isFixed
? item('✓', 'green', correction.rule, null, false)
: item('●', 'red', correction.rule, correction.fix, false)}
)),
]
const sections = [
listed.length > 0 ? {listed} : null,
current.exceptions.length > 0
? collapsible(
'exceptions',
isShowingExceptions,
'approved exceptions',
() => update($, showExceptions, isShown => !isShown).then(() => fitPane($)),
current.exceptions.map(rule => (
{item(
'–',
'gray',
rule.rule,
isCompact ? null : rule.kind === 'accepted' ? 'accepted as is' : 'not applicable here',
true,
)}
)),
)
: null,
current.compliant.length > 0
? collapsible(
'compliant',
isShowingCompliant,
'compliant rules',
() => update($, showCompliant, isShown => !isShown).then(() => fitPane($)),
current.compliant.map(rule => (
{item('✓', 'gray', rule.rule, null, true)}
)),
)
: null,
].filter(section => section !== null)
return (
{header(hero.icon, hero.color, summarize(current), subline)}
{sections.map((section, index) => (
{section}
))}
)
})
}