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[]) => (