import type { AgentToolResult, Theme, ToolDefinition, ToolRenderResultOptions, } from "@earendil-works/pi-coding-agent"; import { type Component, Container, Text, visibleWidth, } from "@earendil-works/pi-tui"; import { stringify } from "yaml"; import { sliceTextByWidth } from "../../shared/display-width.ts"; import { renderLabeledWrappedText } from "../../shared/labeled-wrapped-text.ts"; import { normalizeCollapsedToolText } from "../../shared/terminal-display-text.ts"; import { BoundedToolResult, getToolResultText, } from "../../shared/tool-presentation/bounded.ts"; import { findSelectedGround, type WorkflowDefinition, type WorkflowGround, type WorkflowState, } from "./workflow.ts"; const PRESENTATION_KIND = "workflow-tool"; const STAGE_CHANGE_ARROW = "->"; /** Matches the standard four-row collapsed content budget used by package tools. */ const COLLAPSED_REFERENCE_CONTENT_LINE_LIMIT = 4; /** Limits workflow creation YAML without counting its label or expansion hint. */ const COLLAPSED_WORKFLOW_CONTENT_LINE_LIMIT = 3; /** Reserves indentation, quotes, and continuation markers around wrapped YAML scalars. */ const YAML_SYNTAX_WIDTH_RESERVE = 8; type WorkflowToolRenderContext = Parameters< NonNullable >[2]; /** Identifies one workflow or stage without depending on the current catalog. */ interface WorkflowPresentationReference { readonly id: string; readonly description?: string; } /** Contains the fields represented by one catalog workflow YAML file. */ interface WorkflowContent { readonly kind?: string; readonly nested?: boolean; readonly description?: string; readonly prompt?: string; readonly stages?: readonly unknown[]; readonly transitions?: readonly unknown[]; } /** Holds stage attributes for inspection and edit presentation. */ interface WorkflowStageContent { readonly id: string; readonly description: string; readonly goal?: string; readonly prompt: string; readonly model: { readonly thinking?: string | undefined }; readonly initial?: boolean; readonly final?: boolean; } /** Persists creation identity supplied before validation or state replacement. */ interface WorkflowCreatePresentation { readonly presentationKind: typeof PRESENTATION_KIND; readonly toolName: "workflow_create"; readonly workflow: WorkflowPresentationReference; readonly stage?: WorkflowPresentationReference; } /** Preserves the workflow identity for activation and exit rows across replay. */ interface WorkflowReferencePresentation { readonly presentationKind: typeof PRESENTATION_KIND; readonly toolName: "workflow_activate" | "workflow_exit"; readonly workflow: WorkflowPresentationReference; readonly transition?: WorkflowTransitionOutcome; } /** Records how the exit of a workflow that a transition started resolved this transition. */ interface WorkflowTransitionOutcome { readonly stage: WorkflowPresentationReference; readonly allowed: boolean; } /** Preserves transition endpoints and the selected ground for tool presentation. */ interface WorkflowTransitionPresentation { readonly presentationKind: typeof PRESENTATION_KIND; readonly toolName: "workflow_transition"; /** Identifies the workflow that owns the transition before execution changes state. */ readonly workflowId?: string; readonly from?: WorkflowPresentationReference; readonly to: WorkflowPresentationReference; readonly ground?: WorkflowGround; readonly workflow?: WorkflowPresentationReference; } /** Persists one stage and the values needed to render reads or edits. */ interface WorkflowStagePresentation { readonly presentationKind: typeof PRESENTATION_KIND; readonly toolName: "workflow_get_stage" | "workflow_edit_stage"; readonly stage: WorkflowPresentationReference; readonly content?: WorkflowStageContent; readonly editedContent?: WorkflowStageContent; } export type WorkflowToolPresentationDetails = | WorkflowCreatePresentation | WorkflowReferencePresentation | WorkflowTransitionPresentation | WorkflowStagePresentation; /** Stores presentation evidence shared by one call/result renderer pair. */ interface WorkflowRenderState { presentation?: WorkflowToolPresentationDetails; } /** Defers width-sensitive rows until Pi supplies the default shell child width. */ class WorkflowRows implements Component { public constructor( private readonly renderRows: (width: number) => readonly string[], ) {} /** Renders semantic rows within the width owned by Pi's default tool shell. */ public render(width: number): string[] { return [...this.renderRows(Math.max(0, Math.floor(width)))]; } /** Keeps the width-aware component compatible with Pi's invalidation contract. */ public invalidate(): void {} } /** Captures semantic identity before a workflow tool can change route state. */ export function createWorkflowPresentationDetails( toolName: string, args: unknown, catalog: readonly WorkflowDefinition[], state: WorkflowState | undefined, ): WorkflowToolPresentationDetails | undefined { if (toolName === "workflow_create") { return createWorkflowCreatePresentation(args); } if (toolName === "workflow_activate") { return createWorkflowActivatePresentation(args, catalog, state); } if (toolName === "workflow_exit") { return state === undefined ? undefined : createWorkflowExitPresentation(args, state); } if (toolName === "workflow_transition") { return createWorkflowTransitionPresentation(args, state, catalog); } if (toolName === "workflow_get_stage" || toolName === "workflow_edit_stage") { return createWorkflowStagePresentation(toolName, args, state); } return undefined; } /** Captures the caller-owned dynamic workflow reference even when validation later fails. */ function createWorkflowCreatePresentation( args: unknown, ): WorkflowCreatePresentation | undefined { const workflowId = readInputString(args, "id"); if (workflowId === undefined) { return undefined; } const presentation: WorkflowCreatePresentation = { presentationKind: PRESENTATION_KIND, toolName: "workflow_create", workflow: createReference(workflowId, readInputString(args, "description")), }; const stage = findInitialStageReference(args); return stage === undefined ? presentation : { ...presentation, stage }; } /** Resolves activation descriptions from catalog or saved state before replacement. */ function createWorkflowActivatePresentation( args: unknown, catalog: readonly WorkflowDefinition[], state: WorkflowState | undefined, ): WorkflowReferencePresentation | undefined { const workflowId = readInputString(args, "workflowId"); if (workflowId === undefined) { return undefined; } const workflow = catalog.find(({ id }) => id === workflowId) ?? (state?.workflow.id === workflowId ? state.workflow : undefined); return { presentationKind: PRESENTATION_KIND, toolName: "workflow_activate", workflow: createReference(workflowId, workflow?.description), }; } /** Captures the exited workflow and, for a pending transition, whether the exit allows it. */ function createWorkflowExitPresentation( args: unknown, state: WorkflowState, ): WorkflowReferencePresentation { const allow = isRecord(args) ? args["allow"] : undefined; const pending = state.pendingTransition; return { presentationKind: PRESENTATION_KIND, toolName: "workflow_exit", workflow: createReference(state.workflow.id, state.workflow.description), ...(pending === undefined || typeof allow !== "boolean" ? {} : { transition: { stage: createReference(pending.stageId, undefined), allowed: allow, }, }), }; } /** Captures transition endpoints and the bound workflow before route mutation removes the source stage. */ function createWorkflowTransitionPresentation( args: unknown, state: WorkflowState | undefined, catalog: readonly WorkflowDefinition[], ): WorkflowTransitionPresentation | undefined { const stageId = readInputString(args, "stageId"); if (stageId === undefined) { return undefined; } const fromId = state?.route.at(-1); const groundId = readInputString(args, "ground"); const edge = state?.workflow.transitions.find( ({ from, to }) => from === fromId && to === stageId, ); const ground = edge === undefined ? undefined : findSelectedGround(edge, groundId); const binding = edge?.workflow; return { ...(state === undefined ? {} : { workflowId: state.workflow.id }), ...(ground === undefined ? {} : { ground: { ...ground } }), ...(binding === undefined ? {} : { workflow: createReference( binding, catalog.find(({ id }) => id === binding)?.description, ), }), presentationKind: PRESENTATION_KIND, toolName: "workflow_transition", ...(fromId === undefined || state === undefined ? {} : { from: createReference( fromId, findStageDescription(state.workflow, fromId), ), }), to: createReference( stageId, state === undefined ? undefined : findStageDescription(state.workflow, stageId), ), }; } /** Captures stage content from saved state for get or replacement arguments for edit. */ function createWorkflowStagePresentation( toolName: "workflow_get_stage" | "workflow_edit_stage", args: unknown, state: WorkflowState | undefined, ): WorkflowStagePresentation | undefined { const stageId = readInputString(args, "stageId"); if (stageId === undefined) { return undefined; } const savedStage = state?.workflow.stages.find(({ id }) => id === stageId); const content = savedStage === undefined ? undefined : { id: savedStage.id, description: savedStage.description, ...(savedStage.goal === undefined ? {} : { goal: savedStage.goal }), prompt: savedStage.prompt, model: { thinking: savedStage.model?.thinking }, initial: savedStage.initial, final: savedStage.final, }; const editedContent = toolName === "workflow_edit_stage" ? createEditedStageContent(args, savedStage?.initial, savedStage?.final) : undefined; return { presentationKind: PRESENTATION_KIND, toolName, stage: createReference(stageId, savedStage?.description), ...(content === undefined ? {} : { content }), ...(editedContent === undefined ? {} : { editedContent }), }; } /** Reads the closed editable stage fields without inventing immutable flags. */ function createEditedStageContent( args: unknown, initial: boolean | undefined, final: boolean | undefined, ): WorkflowStageContent | undefined { if (!isRecord(args) || !isRecord(args["model"])) { return undefined; } const id = readInputString(args, "stageId"); const description = readInputString(args, "description"); const prompt = readInputString(args, "prompt"); const thinking = readInputString(args["model"], "thinking"); if ( id === undefined || description === undefined || prompt === undefined || thinking === undefined ) { return undefined; } const goal = readInputString(args, "goal")?.trim(); return { id, description, ...(goal === undefined ? {} : { goal }), prompt, model: { thinking }, ...(initial === undefined ? {} : { initial }), ...(final === undefined ? {} : { final }), }; } /** Keeps live pending calls semantic until persisted result evidence is available. */ export function primeWorkflowRenderState( context: WorkflowToolRenderContext, presentation: WorkflowToolPresentationDetails | undefined, ): void { const state = context.state as WorkflowRenderState; if ( context.argsComplete && state.presentation === undefined && presentation !== undefined ) { state.presentation = presentation; } } /** Renders one workflow creation with its caller-supplied reference. */ export function renderWorkflowCreateCall( args: unknown, theme: Theme, context: WorkflowToolRenderContext, ): Component { const presentation = readCreatePresentation(context); const fallback = createWorkflowCreatePresentation(args); return renderWorkflowReferenceCall({ toolName: "workflow_create", workflow: presentation?.workflow ?? fallback?.workflow, stage: presentation?.stage ?? fallback?.stage, content: createWorkflowContent(args), theme, context, }); } /** Renders one workflow activation with a bounded or complete reference section. */ export function renderWorkflowActivateCall( args: unknown, theme: Theme, context: WorkflowToolRenderContext, ): Component { const presentation = readActivatePresentation(context); const workflow = presentation?.workflow ?? createOptionalReference(readInputString(args, "workflowId")); return renderWorkflowReferenceCall({ toolName: "workflow_activate", workflow, stage: undefined, theme, context, }); } /** Renders the exited workflow from presentation evidence shared by both screens. */ export function renderWorkflowExitCall( _args: unknown, theme: Theme, context: WorkflowToolRenderContext, ): Component { return new WorkflowRows((width) => { const presentation = (context.state as WorkflowRenderState).presentation; const workflow = presentation?.toolName === "workflow_exit" ? presentation.workflow : undefined; const transition = presentation?.toolName === "workflow_exit" ? presentation.transition : undefined; return [ ...renderWorkflowReferenceCall({ toolName: "workflow_exit", workflow, stage: undefined, theme, context, }).render(width), ...(transition === undefined ? [] : renderReference({ label: "Transition", reference: createReference( transition.stage.id, transition.allowed ? "allowed" : "refused", ), expanded: context.expanded, width, theme, })), ]; }); } /** Shares bounded workflow-reference rows between workflow lifecycle calls. */ function renderWorkflowReferenceCall(options: { readonly toolName: | "workflow_create" | "workflow_activate" | "workflow_exit" | "workflow_get_stage" | "workflow_edit_stage"; readonly workflow: WorkflowPresentationReference | undefined; readonly stage: WorkflowPresentationReference | undefined; readonly content?: WorkflowContent | WorkflowStageContent | undefined; readonly theme: Theme; readonly context: WorkflowToolRenderContext; }): Component { const workflowInTitle = options.toolName === "workflow_activate" || options.toolName === "workflow_exit"; return new WorkflowRows((width) => { let workflowRows: readonly string[] = []; if (options.workflow !== undefined) { workflowRows = workflowInTitle ? renderWorkflowDescription( options.workflow.description, options.context.expanded, width, options.theme, ) : renderReference({ label: "Workflow", reference: options.workflow, expanded: options.context.expanded, width, theme: options.theme, }); } return [ renderToolName( options.toolName, width, options.theme, workflowInTitle ? options.workflow?.id : undefined, ), ...workflowRows, ...(options.stage === undefined ? [] : renderReference({ label: "Stage", reference: options.stage, expanded: options.context.expanded, width, theme: options.theme, })), ...(options.content === undefined ? [] : renderWorkflowContent({ content: options.content, expanded: options.context.expanded, width, theme: options.theme, })), ]; }); } /** Renders one workflow transition with bounded or complete endpoint sections. */ export function renderWorkflowTransitionCall( args: unknown, theme: Theme, context: WorkflowToolRenderContext, ): Component { return new WorkflowRows((width) => { const presentation = readTransitionPresentation(context); const to = presentation?.to ?? createOptionalReference(readInputString(args, "stageId")); return [ renderToolName( "workflow_transition", width, theme, presentation?.workflowId, ), ...(presentation?.from === undefined ? [] : renderReference({ label: "From", reference: presentation.from, expanded: context.expanded, width, theme, })), ...(to === undefined ? [] : renderReference({ label: "To", reference: to, expanded: context.expanded, width, theme, })), ...(presentation?.ground === undefined ? [] : renderReference({ label: "Ground", reference: createReference( presentation.ground.id, presentation.ground.when, ), compactText: presentation.ground.when, expanded: context.expanded, width, theme, })), ...(presentation?.workflow === undefined ? [] : renderReference({ label: "Nested workflow", reference: presentation.workflow, expanded: context.expanded, width, theme, })), ]; }); } /** Renders one stage inspection or edit with bounded semantic content. */ export function renderWorkflowStageCall( toolName: "workflow_get_stage" | "workflow_edit_stage", args: unknown, theme: Theme, context: WorkflowToolRenderContext, ): Component { return new WorkflowRows((width) => { const presentation = readStagePresentation(context, toolName); const fallback = createWorkflowStagePresentation(toolName, args, undefined); const resolved = presentation ?? fallback; const stageId = resolved?.stage.id; const title = stageId === undefined ? toolName : `${toolName}: ${stageId}`; const attributes = toolName === "workflow_get_stage" ? createStageAttributes(resolved?.content) : createStageChanges(resolved?.content, resolved?.editedContent); return [ renderToolName(title, width, theme), ...renderStageAttributes(attributes, context.expanded, width, theme), ]; }); } /** Restores persisted evidence and hides successful internal result content. */ export function renderWorkflowResult( result: AgentToolResult, _options: ToolRenderResultOptions, theme: Theme, context: WorkflowToolRenderContext, ): Component { const presentation = parseWorkflowPresentationDetails(result.details); if (presentation !== undefined) { (context.state as WorkflowRenderState).presentation = presentation; } if (!context.isError) { return new Container(); } return new WorkflowRows((width) => renderLabeledWrappedText({ label: "Error:", text: getToolResultText(result), width, labelStyle: (value) => theme.fg("toolTitle", theme.bold(value)), textStyle: (value) => theme.fg("muted", value), }), ); } /** Parses persisted UI evidence before it enters renderer-owned state. */ function parseWorkflowPresentationDetails( value: unknown, ): WorkflowToolPresentationDetails | undefined { if (!isRecord(value) || value["presentationKind"] !== PRESENTATION_KIND) { return undefined; } if (value["toolName"] === "workflow_create") { return parseWorkflowReferencePresentation(value, "workflow_create"); } if ( value["toolName"] === "workflow_activate" || value["toolName"] === "workflow_exit" ) { return parseWorkflowReferencePresentation(value, value["toolName"]); } if ( value["toolName"] === "workflow_get_stage" || value["toolName"] === "workflow_edit_stage" ) { return parseStagePresentation(value, value["toolName"]); } return value["toolName"] === "workflow_transition" ? parseTransitionPresentation(value) : undefined; } /** Parses persisted lifecycle evidence with a workflow reference and optional initial stage. */ function parseWorkflowReferencePresentation( value: Record, toolName: "workflow_create" | "workflow_activate" | "workflow_exit", ): WorkflowCreatePresentation | WorkflowReferencePresentation | undefined { const workflow = parseReference(value["workflow"]); if (workflow === undefined) { return undefined; } if (toolName === "workflow_create") { const stage = parseReference(value["stage"]); const presentation: WorkflowCreatePresentation = { presentationKind: PRESENTATION_KIND, toolName, workflow, }; return stage === undefined ? presentation : { ...presentation, stage }; } const saved = value["transition"]; if (saved === undefined) { return { presentationKind: PRESENTATION_KIND, toolName, workflow }; } const stage = isRecord(saved) ? parseReference(saved["stage"]) : undefined; const allowed = isRecord(saved) ? saved["allowed"] : undefined; if ( toolName !== "workflow_exit" || stage === undefined || typeof allowed !== "boolean" ) { return undefined; } return { presentationKind: PRESENTATION_KIND, toolName, workflow, transition: { stage, allowed }, }; } /** Parses persisted transition evidence and rejects a malformed optional source. */ function parseTransitionPresentation( value: Record, ): WorkflowTransitionPresentation | undefined { const workflowId = value["workflowId"]; const from = value["from"] === undefined ? undefined : parseReference(value["from"]); const to = parseReference(value["to"]); const savedGround = value["ground"]; let ground: WorkflowGround | undefined; if (savedGround !== undefined) { if ( !isRecord(savedGround) || !isNonEmptyString(savedGround["id"]) || !isNonEmptyString(savedGround["when"]) ) { return undefined; } ground = { id: savedGround["id"], when: savedGround["when"] }; } const workflow = value["workflow"] === undefined ? undefined : parseReference(value["workflow"]); if ( (workflowId !== undefined && !isNonEmptyString(workflowId)) || (value["from"] !== undefined && from === undefined) || to === undefined || (value["workflow"] !== undefined && workflow === undefined) ) { return undefined; } return { presentationKind: PRESENTATION_KIND, toolName: "workflow_transition", ...(workflowId === undefined ? {} : { workflowId }), ...(from === undefined ? {} : { from }), to, ...(ground === undefined ? {} : { ground }), ...(workflow === undefined ? {} : { workflow }), }; } /** Parses one persisted stage reference and its optional content. */ function parseStagePresentation( value: Record, toolName: "workflow_get_stage" | "workflow_edit_stage", ): WorkflowStagePresentation | undefined { const stage = parseReference(value["stage"]); const content = parseStageContent(value["content"]); const editedContent = parseStageContent(value["editedContent"]); if ( stage === undefined || (value["content"] !== undefined && content === undefined) || (value["editedContent"] !== undefined && editedContent === undefined) ) { return undefined; } return { presentationKind: PRESENTATION_KIND, toolName, stage, ...(content === undefined ? {} : { content }), ...(editedContent === undefined ? {} : { editedContent }), }; } /** Validates saved stage attributes for inspection and edit display. */ function parseStageContent(value: unknown): WorkflowStageContent | undefined { if (!isRecord(value) || !isRecord(value["model"])) { return undefined; } const id = value["id"]; const description = value["description"]; const goal = value["goal"]; const prompt = value["prompt"]; const thinking = value["model"]["thinking"]; const initial = value["initial"]; const final = value["final"]; if ( !isNonEmptyString(id) || !isNonEmptyString(description) || !isNonEmptyString(prompt) || (goal !== undefined && !isNonEmptyString(goal)) || (thinking !== undefined && !isNonEmptyString(thinking)) || (initial !== undefined && typeof initial !== "boolean") || (final !== undefined && typeof final !== "boolean") ) { return undefined; } return { id, description, ...(goal === undefined ? {} : { goal }), prompt, model: { thinking }, ...(initial === undefined ? {} : { initial }), ...(final === undefined ? {} : { final }), }; } /** Reads creation evidence from the shared row state. */ function readCreatePresentation( context: WorkflowToolRenderContext, ): WorkflowCreatePresentation | undefined { const presentation = (context.state as WorkflowRenderState).presentation; return presentation?.toolName === "workflow_create" ? presentation : undefined; } /** Reads activation evidence from the shared row state. */ function readActivatePresentation( context: WorkflowToolRenderContext, ): WorkflowReferencePresentation | undefined { const presentation = (context.state as WorkflowRenderState).presentation; return presentation?.toolName === "workflow_activate" ? presentation : undefined; } /** Reads transition evidence from the shared row state. */ function readTransitionPresentation( context: WorkflowToolRenderContext, ): WorkflowTransitionPresentation | undefined { const presentation = (context.state as WorkflowRenderState).presentation; return presentation?.toolName === "workflow_transition" ? presentation : undefined; } /** Reads stage evidence from the shared row state. */ function readStagePresentation( context: WorkflowToolRenderContext, toolName: "workflow_get_stage" | "workflow_edit_stage", ): WorkflowStagePresentation | undefined { const presentation = (context.state as WorkflowRenderState).presentation; return presentation?.toolName === toolName ? presentation : undefined; } /** Renders a width-bounded bright tool title with an optional workflow ID. */ function renderToolName( toolName: string, width: number, theme: Theme, workflowId?: string, ): string { const title = workflowId === undefined ? toolName : `${toolName} · ${workflowId}`; return theme.fg("toolTitle", theme.bold(sliceTextByWidth(title, width))); } /** Renders the workflow description below an identity-bearing tool title. */ function renderWorkflowDescription( description: string | undefined, expanded: boolean, width: number, theme: Theme, ): readonly string[] { if (description === undefined) { return []; } if (expanded) { return new Text(theme.fg("toolOutput", description), 0, 0).render(width); } return new BoundedToolResult({ text: description, theme, isError: false, expanded: false, collapsedContentLineLimit: COLLAPSED_REFERENCE_CONTENT_LINE_LIMIT, showHiddenLineHint: true, showExpandedErrorLabel: false, }).render(width); } interface StageAttributeRow { readonly label: string; readonly value: string; readonly nextValue?: string; } /** Selects available stage attributes for inspection display. */ function createStageAttributes( content: WorkflowStageContent | undefined, ): readonly StageAttributeRow[] | undefined { if (content === undefined) { return undefined; } return [ { label: "Description", value: content.description }, ...(content.goal === undefined ? [] : [{ label: "Goal", value: content.goal }]), { label: "Prompt", value: content.prompt }, ...(content.model.thinking === undefined ? [] : [{ label: "Thinking", value: content.model.thinking }]), ...(content.initial === undefined ? [] : [{ label: "Initial", value: String(content.initial) }]), ...(content.final === undefined ? [] : [{ label: "Final", value: String(content.final) }]), ]; } /** Selects captured stage changes with a known replacement value. */ function createStageChanges( content: WorkflowStageContent | undefined, editedContent: WorkflowStageContent | undefined, ): readonly StageAttributeRow[] | undefined { if (content === undefined || editedContent === undefined) { return undefined; } const editableValues: ReadonlyArray< readonly [string, string | undefined, string | undefined] > = [ ["Description", content.description, editedContent.description], ...(content.goal === undefined || editedContent.goal === undefined ? [] : [["Goal", content.goal, editedContent.goal] as const]), ["Prompt", content.prompt, editedContent.prompt], ["Thinking", content.model.thinking, editedContent.model.thinking], ]; return editableValues.flatMap(([label, previous, next]) => { if (next === undefined || previous === next) { return []; } return [ { label, value: previous ?? next, ...(previous === undefined ? {} : { nextValue: next }), }, ]; }); } /** Renders one collapsed attribute after normalizing its source line breaks. */ function renderCollapsedStageAttribute( attribute: StageAttributeRow, width: number, theme: Theme, ): readonly string[] { const value = normalizeCollapsedToolText(attribute.value); const nextValue = attribute.nextValue === undefined ? undefined : normalizeCollapsedToolText(attribute.nextValue); const label = theme.fg("toolTitle", theme.bold(`${attribute.label}:`)); const renderedValue = nextValue === undefined ? theme.fg("toolOutput", ` ${value}`) : `${theme.fg("toolOutput", ` ${value} `)}${theme.fg("success", STAGE_CHANGE_ARROW)}${theme.fg("toolOutput", ` ${nextValue}`)}`; return new Text(`${label}${renderedValue}`, 0, 0).render(width); } /** Renders one expanded attribute as a standard named section. */ function renderExpandedStageAttribute( attribute: StageAttributeRow, width: number, theme: Theme, ): readonly string[] { return [ ...new Text(theme.fg("muted", `--- ${attribute.label} ---`), 0, 0).render( width, ), ...new Text(theme.fg("toolOutput", attribute.value), 0, 0).render(width), ...(attribute.nextValue === undefined ? [] : [ ...new Text(theme.fg("success", STAGE_CHANGE_ARROW), 0, 0).render( width, ), ...new Text(theme.fg("toolOutput", attribute.nextValue), 0, 0).render( width, ), ]), ]; } /** Applies one shared collapsed budget after rendering independent attributes. */ function renderStageAttributes( attributes: readonly StageAttributeRow[] | undefined, expanded: boolean, width: number, theme: Theme, ): readonly string[] { if (attributes === undefined) { return []; } if (attributes.length === 0) { return new Text(theme.fg("toolOutput", "No changes."), 0, 0).render(width); } if (expanded) { return attributes.flatMap((attribute) => renderExpandedStageAttribute(attribute, width, theme), ); } return new BoundedToolResult({ text: "", theme, isError: false, expanded: false, collapsedContentLineLimit: COLLAPSED_REFERENCE_CONTENT_LINE_LIMIT, showHiddenLineHint: true, showExpandedErrorLabel: false, renderCollapsedLines: (renderWidth) => attributes.flatMap((attribute) => renderCollapsedStageAttribute(attribute, renderWidth, theme), ), }).render(width); } /** Renders complete expanded YAML or a three-line collapsed YAML preview. */ function renderWorkflowContent(options: { readonly content: WorkflowContent | WorkflowStageContent; readonly expanded: boolean; readonly width: number; readonly theme: Theme; }): readonly string[] { const serialize = (width: number) => serializeWorkflowContent(options.content, width).map((line) => options.theme.fg("toolOutput", line), ); if (options.expanded) { return [ ...new Text(options.theme.fg("muted", "--- Content ---"), 0, 0).render( options.width, ), ...serialize(options.width), ]; } return [ ...new Text( options.theme.fg("toolTitle", options.theme.bold("Content:")), 0, 0, ).render(options.width), ...new BoundedToolResult({ text: "", theme: options.theme, isError: false, expanded: false, collapsedContentLineLimit: COLLAPSED_WORKFLOW_CONTENT_LINE_LIMIT, showHiddenLineHint: true, showExpandedErrorLabel: false, renderCollapsedLines: serialize, }).render(options.width), ]; } /** Selects the complete section or Pi's standard bounded collapsed preview. */ function renderReference(options: { readonly label: | "Workflow" | "Stage" | "From" | "To" | "Ground" | "Nested workflow" | "Transition"; readonly reference: WorkflowPresentationReference; readonly compactText?: string; readonly expanded: boolean; readonly width: number; readonly theme: Theme; }): readonly string[] { if (options.expanded) { return [ ...new Text( options.theme.fg("muted", `--- ${options.label} ---`), 0, 0, ).render(options.width), ...new Text( options.theme.fg("toolOutput", formatReference(options.reference)), 0, 0, ).render(options.width), ]; } return new BoundedToolResult({ text: "", theme: options.theme, isError: false, expanded: false, collapsedContentLineLimit: COLLAPSED_REFERENCE_CONTENT_LINE_LIMIT, showHiddenLineHint: true, showExpandedErrorLabel: false, renderCollapsedLines: (width) => renderLabeledWrappedText({ label: `${options.label}:`, text: normalizeCollapsedToolText( options.compactText ?? formatReference(options.reference), ), width, labelStyle: (value) => options.theme.fg("toolTitle", options.theme.bold(value)), textStyle: (value) => options.theme.fg("toolOutput", value), }), }).render(options.width); } /** Formats optional description only when validated evidence contains it. */ function formatReference(reference: WorkflowPresentationReference): string { return reference.description === undefined ? reference.id : `${reference.id} · ${reference.description}`; } /** Creates one immutable reference without inventing unavailable descriptions. */ function createReference( id: string, description: string | undefined, ): WorkflowPresentationReference { return description === undefined ? { id } : { id, description }; } /** Converts an optional identifier into renderable fallback evidence. */ function createOptionalReference( id: string | undefined, ): WorkflowPresentationReference | undefined { return id === undefined ? undefined : { id }; } /** Finds the saved stage description authoritative for the active session. */ function findStageDescription( workflow: WorkflowDefinition, stageId: string, ): string | undefined { return workflow.stages.find(({ id }) => id === stageId)?.description; } function findInitialStageReference( args: unknown, ): WorkflowPresentationReference | undefined { if (!isRecord(args) || !Array.isArray(args["stages"])) { return undefined; } const stage = args["stages"].find( (value) => isRecord(value) && value["initial"] === true, ); if (!isRecord(stage)) { return undefined; } const stageId = readInputString(stage, "id"); return stageId === undefined ? undefined : createReference(stageId, readInputString(stage, "description")); } /** Parses one persisted reference with an optional non-empty description. */ function parseReference( value: unknown, ): WorkflowPresentationReference | undefined { if (!isRecord(value) || !isNonEmptyString(value["id"])) { return undefined; } const description = value["description"]; if (description !== undefined && !isNonEmptyString(description)) { return undefined; } return createReference(value["id"], description); } /** Selects YAML serialization that fits without display-layer line wrapping. */ function serializeWorkflowContent( content: WorkflowContent | WorkflowStageContent, width: number, ): readonly string[] { const renderWidth = Math.max(1, Math.floor(width)); const defaultLines = yamlLines(stringify(content)); if (defaultLines.every((line) => visibleWidth(line) <= renderWidth)) { return defaultLines; } let narrowestLines = defaultLines; for ( let lineWidth = Math.max(1, renderWidth - YAML_SYNTAX_WIDTH_RESERVE); lineWidth >= 1; lineWidth-- ) { narrowestLines = yamlLines( stringify(content, { defaultKeyType: "PLAIN", defaultStringType: "QUOTE_DOUBLE", lineWidth, minContentWidth: 0, }), ); if (narrowestLines.every((line) => visibleWidth(line) <= renderWidth)) { return narrowestLines; } } return narrowestLines; } /** Removes only the serializer's final newline before row rendering. */ function yamlLines(content: string): readonly string[] { return content.trimEnd().split("\n"); } /** Selects the definition fields owned by catalog workflow YAML files. */ function createWorkflowContent(args: unknown): WorkflowContent | undefined { if (!isRecord(args)) { return undefined; } // Streaming parsers expose fields independently, so select each compatible value. const content: WorkflowContent = { ...(typeof args["kind"] === "string" ? { kind: args["kind"] } : {}), ...(typeof args["nested"] === "boolean" ? { nested: args["nested"] } : {}), ...(typeof args["description"] === "string" ? { description: args["description"] } : {}), ...(typeof args["prompt"] === "string" ? { prompt: args["prompt"] } : {}), ...(Array.isArray(args["stages"]) ? { stages: args["stages"] } : {}), ...(Array.isArray(args["transitions"]) ? { transitions: args["transitions"] } : {}), }; return Object.keys(content).length === 0 ? undefined : content; } /** Reads one exact string field without normalizing tool arguments. */ function readInputString(value: unknown, key: string): string | undefined { if (!isRecord(value)) { return undefined; } const field = value[key]; return isNonEmptyString(field) ? field : undefined; } /** Narrows persisted values to plain key-value objects. */ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } /** Rejects empty persisted identifiers and descriptions. */ function isNonEmptyString(value: unknown): value is string { return typeof value === "string" && value.length > 0; }