import { createContext, useContext, type ReactElement, type ReactNode } from 'react' import { isAbsolute, relative } from 'node:path' import { Box, Text, renderToString } from 'ink' import { InkThemeProvider, StatusHeader } from '@agentskit/ink' import { splitLines, toPosix } from '@agentskit/cross-platform' import type { DoctorReport } from '../../doctor/run-doctor.js' import type { GateResult, GateRunResult } from '../../gates/run-gates.js' import type { PendingReferenceFinding } from '../../gates/reference-findings.js' import type { KnowledgeDiagnostic } from '../../schemas/knowledge.js' import type { AskResult } from './ask.js' /** Where relative paths are anchored; defaults to the current directory. */ export type ViewOptions = { readonly root?: string } const AMBER = 'yellow' export const relPath = (path: string, root: string = process.cwd()): string => { if (!isAbsolute(path)) return toPosix(path) const rel = relative(root, path) return rel.startsWith('..') || isAbsolute(rel) ? toPosix(path) : toPosix(rel) } /** Set inside the interactive app, which draws its own header: views then render only their body. */ export const EmbeddedContext = createContext(false) /** The status header and spacing every view shares. */ export const Frame = ({ title, children }: { readonly title: string; readonly children?: ReactNode }): ReactElement => { const embedded = useContext(EmbeddedContext) return ( {embedded ? null : } {children} ) } export const Path = ({ path, line }: { readonly path: string; readonly line?: number | undefined }): ReactElement => ( {path} {line === undefined ? null : {`:${line}`}} ) /** `color` only when there is one: Ink rejects an explicit `undefined` under exactOptionalPropertyTypes. */ export const tint = (color: string | undefined): { readonly color?: string } => (color === undefined ? {} : { color }) const codeColor = (code: string): string => (code.startsWith('BROKEN') ? 'red' : AMBER) export const Code = ({ code, width = 0 }: { readonly code: string; readonly width?: number }): ReactElement => ( {code.padEnd(width)} ) /** A tool-call style block: ✓/✗, a title, an optional duration, and the detail underneath. */ export const Step = ({ ok, title, durationMs, children }: { readonly ok: boolean; readonly title: string; readonly durationMs?: number | undefined; readonly children?: ReactNode }): ReactElement => ( {`${ok ? '✓' : '✗'} ${title}`} {` · ${ok ? 'ok' : 'failed'}${durationMs === undefined ? '' : ` · ${durationMs}ms`}`} {children} ) export const Hint = ({ children }: { readonly children: ReactNode }): ReactElement => {children} export const Footer = ({ title, lines }: { readonly title: string; readonly lines: readonly string[] }): ReactElement | null => lines.length === 0 ? null : ( {title} {lines.map((line) => ( {` $ ${line}`} ))} ) /** `CODE file:line → target`, one row per pending reference finding. */ export const FindingsTable = ({ findings, limit = 50 }: { readonly findings: readonly PendingReferenceFinding[]; readonly limit?: number }): ReactElement => { const shown = findings.slice(0, limit) const width = Math.max(0, ...shown.map((finding) => finding.code.length)) return ( {shown.map((finding, position) => ( {' '} {' → '} {finding.target} ))} {findings.length > shown.length ? {`… and ${findings.length - shown.length} more (see the JSON output)`} : null} ) } export const PUBLISH_ANYWAY_HINT = 'Fix the cited documentation, or publish anyway with ak-docs index --allow-pending-findings.' const firstLine = (text: string): string => splitLines(text)[0] ?? '' /** One gate as a step: the pending findings table and the publish-anyway hint for `reference-findings`. */ export const GateStep = ({ gate }: { readonly gate: GateResult & { readonly durationMs?: number } }): ReactElement => { const findings = gate.findings ?? [] return ( {gate.ok ? ( {gate.message} ) : gate.id === 'reference-findings' ? ( {firstLine(gate.message)} {findings.length ? : null} {findings.length ? {PUBLISH_ANYWAY_HINT} : null} ) : ( {gate.message} {gate.expected ? {`expected ${gate.expected}`} : null} {gate.actual ? {`actual ${gate.actual}`} : null} )} ) } // ---- ask -------------------------------------------------------------------------------------- export const AskView = ({ result, root }: { readonly result: AskResult } & ViewOptions): ReactElement => { const { best, matches, handoff } = result return ( Question {result.question} {best ? ( Best match {`${best.type} ${best.id}`} {' ('} {')'} ) : ( Best match: none )} {handoff ? ( Handoff {` start: ${handoff.startHere ?? '(unknown)'}`} {` edit: ${(handoff.editRoots ?? []).join(', ') || '(none)'}`} {` checks: ${(handoff.checks ?? []).join(' · ') || '(none)'}`} ) : null} Matches {matches.length ? ( matches.slice(0, 5).map((match, position) => ( {` ${match.score.toFixed(2).padStart(6)} `} {`[${match.type}] ${match.id}`} {' '} {match.summary ? {` ${match.summary.replace(/\s+/g, ' ').slice(0, 100)}`} : null} )) ) : ( {' No local matches. Try: ak-docs search '} )}