import type { AttributedEffect } from "#src/access-intent/effect"; import type { BashCommandContext } from "#src/types"; import { allEvidence, type DecidingPath, type PromptPayload, } from "./prompt-payload"; /** * The render vocabulary shared by every renderer over a {@link PromptPayload}. * * Which element an ask flags, what that element is called, and how a nested * execution context reads are all answers a render needs and none of them is * a payload fact — the payload carries `value`, `kind`, and `commandContext`, * and this module is where they acquire a name. It lives apart from any one * renderer so the dialog, the agent-facing text, and the review log cannot * disagree about what a given ask is flagging. */ /** * What the ask is flagging. * * The decision-relevant value for every shape but one: a bash ask that escaped * the working directory flags the paths it referenced, not the command that * referenced them — the command is the context, and the paths are what the * operator is ruling on. */ export function flaggedElements(payload: PromptPayload): readonly string[] { if (payload.kind === "bash_external_directory") { return allEvidence(payload, "external path").map((entry) => entry.text); } return payload.request.value === "" ? [] : [payload.request.value]; } /** * What {@link flaggedElements} returns is called. * * Differs from {@link valueLabel} for exactly one shape: a bash ask that * escaped the working directory flags paths while its value is the command, * so the two nouns are for two different things. */ export function flaggedElementLabel(payload: PromptPayload): string { return payload.kind === "bash_external_directory" ? "path" : valueLabel(payload); } /** What the decision-relevant value is called, per ask shape. */ export function valueLabel(payload: PromptPayload): string { switch (payload.kind) { case "bash": case "bash_external_directory": return "command"; case "mcp": return "target"; case "tool": return "tool"; case "path": case "external_directory": return "path"; case "skill": case "skill_read": return "skill"; case "forwarded": return forwardedValueLabel(payload.request.surface); } } /** * Labels the version-skew render only: a payload-bearing forwarded ask carries * the child's real `kind` and never reaches this arm (#745). * * Without a payload all that survives is the child's *display* projection — its * tool name as the surface — so the label is inferred from it and falls back to * a neutral one. */ function forwardedValueLabel(surface: string): string { switch (surface) { case "bash": return "command"; case "skill": return "skill"; default: return "value"; } } /** * Why a bash path ask landed on the direction it did, or `undefined` for an ask * with no deciding path (ADR 0013 §10's blame, for the human deciding it). * * Names the deciding path unless the value line already does — a bash `path` * ask's value *is* its deciding token — so the line states only what is new. * For an unproven effect it names the key that would prove one, generically: * the deciding path does not know which command produced it. */ export function describeDecidingPath( payload: PromptPayload, ): string | undefined { const { decidingPath, value } = payload.request; if (decidingPath === null) return undefined; const clause = provenanceClause(decidingPath); return decidingPath.path === value ? clause : `${decidingPath.path}: ${clause}`; } function provenanceClause({ effect, effectSource }: DecidingPath): string { switch (effectSource) { case "syntax": return effect === "read" ? "read (redirect source)" : `${effectWords(effect)} (redirect destination)`; case "core": return `${effectWords(effect)} (pure-reader command)`; case "declared-global": return `${effectWords(effect)} (declared in global commandEffects)`; case "declared-project": return `${effectWords(effect)} (declared in project commandEffects)`; case "retracted": return "unproven (an option withdrew the claim), consults both directions"; case "unproven": return `${effectWords("unproven")}; declare the command in commandEffects to classify it`; } } /** An effect as the dialog says it: an unproven one says what that costs. */ function effectWords(effect: AttributedEffect): string { return effect === "unproven" ? "unproven, consults both directions" : effect; } /** * Human-readable label for a nested bash execution context, or `undefined` for * a current-shell (top-level) command. */ export function describeBashCommandContext( context: BashCommandContext | null, ): string | undefined { switch (context) { case "command_substitution": return "command substitution"; case "process_substitution": return "process substitution"; case "subshell": return "subshell"; case null: return undefined; } }