fix(coding-agent/tools): merged ask tool answer rendering into the original question form
- Added a shared `renderAnswerOptions` helper that redrew answered options with markers, custom input, and cancellation state in-place. - Set `mergeCallAndResult` to true so ask prompts now keep their question form visible while showing the final selection. - Kept selection rendering consistent by preserving markers and option ordering while reusing the same layout for completed answers.
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
### Changed
|
||||
|
||||
- Changed the `ask` tool transcript renderer to mark single-choice questions with circular radio glyphs (`○`/`◉`) instead of the rectangular checkbox glyphs (`☐`/`☑`) it shares with multi-select questions, so a "pick one" combo box visually reads as a radio group rather than a checklist. Multi-select questions keep checkboxes. Added a `radio.selected`/`radio.unselected` symbol pair across the unicode, nerd-font, and ASCII presets.
|
||||
- Changed the `ask` tool transcript renderer to mark the chosen answer inside the question form rather than re-listing the questions in a detached summary block below it. Once a question is answered, the standalone prompt preview is dropped and the result redraws the same form — every offered option still shown, with the selected one(s) filled in (`◉`/`☑`, highlighted) and the rest dimmed (`○`/`☐`); custom free-text answers and cancellations render in place as the final entry. This removes the duplicate question/option listing that previously appeared once as the call preview and again as the result.
|
||||
- Changed task-completion and `ask` desktop notifications to structured terminal notifications (title, body, type, and a focus-on-click action). On Kitty these render through OSC 99 as a proper title/body with click-to-focus; terminals without confirmed OSC 99 support collapse them to the previous single-line message (BEL/OSC 9).
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
||||
import { type Component, Container, Markdown, renderInlineMarkdown, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
||||
import { type Component, Container, Markdown, type MarkdownTheme, renderInlineMarkdown, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
||||
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
||||
import * as z from "zod/v4";
|
||||
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
||||
@@ -644,7 +644,56 @@ function optionMarker(uiTheme: Theme, multi: boolean | undefined, selected: bool
|
||||
return selected ? uiTheme.radio.selected : uiTheme.radio.unselected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the answered option list for a question: every offered option with its
|
||||
* selection marker filled in, plus any custom free-text answer. This keeps the
|
||||
* result visually identical to the question form (`renderCall`) so the answer
|
||||
* reads in place rather than as a detached summary block.
|
||||
*
|
||||
* `linePrefix` is the indent that precedes each entry's tree branch — a single
|
||||
* leading space for top-level (single-question) entries, or the question's
|
||||
* vertical continuation for nested (multi-question) entries.
|
||||
*/
|
||||
function renderAnswerOptions(
|
||||
uiTheme: Theme,
|
||||
mdTheme: MarkdownTheme,
|
||||
linePrefix: string,
|
||||
options: string[] | undefined,
|
||||
selectedOptions: string[] | undefined,
|
||||
multi: boolean | undefined,
|
||||
customInput: string | undefined,
|
||||
): string {
|
||||
const selected = new Set(selectedOptions ?? []);
|
||||
// Prefer the full recorded option set; fall back to the selected labels when
|
||||
// details omit the options array.
|
||||
const list = options && options.length > 0 ? options : (selectedOptions ?? []);
|
||||
|
||||
// Nothing was chosen (and no custom answer) → a lone cancelled marker.
|
||||
if (selected.size === 0 && customInput === undefined) {
|
||||
return `${linePrefix}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`;
|
||||
}
|
||||
|
||||
let text = "";
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const label = list[i];
|
||||
const isSelected = selected.has(label);
|
||||
const isLastEntry = i === list.length - 1 && customInput === undefined;
|
||||
const branch = isLastEntry ? uiTheme.tree.last : uiTheme.tree.branch;
|
||||
const marker = optionMarker(uiTheme, multi, isSelected);
|
||||
const markerStyled = isSelected ? uiTheme.fg("success", marker) : uiTheme.fg("dim", marker);
|
||||
const labelStyled = renderInlineMarkdown(label, mdTheme, t =>
|
||||
isSelected ? uiTheme.fg("toolOutput", t) : uiTheme.fg("muted", t),
|
||||
);
|
||||
text += `${text ? "\n" : ""}${linePrefix}${uiTheme.fg("dim", branch)} ${markerStyled} ${labelStyled}`;
|
||||
}
|
||||
if (customInput !== undefined) {
|
||||
text += renderCustomInput(uiTheme, linePrefix, customInput, true, text.length > 0);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
export const askToolRenderer = {
|
||||
mergeCallAndResult: true,
|
||||
renderCall(args: AskRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
||||
const label = formatTitle("Ask", uiTheme);
|
||||
const mdTheme = getMarkdownTheme();
|
||||
@@ -760,43 +809,21 @@ export const askToolRenderer = {
|
||||
for (let i = 0; i < details.results.length; i++) {
|
||||
const r = details.results[i];
|
||||
const isLastQuestion = i === details.results.length - 1;
|
||||
const branch = isLastQuestion ? uiTheme.tree.last : uiTheme.tree.branch;
|
||||
const continuation = isLastQuestion ? " " : `${uiTheme.fg("dim", uiTheme.tree.vertical)} `;
|
||||
const hasSelection = r.customInput !== undefined || r.selectedOptions.length > 0;
|
||||
const statusIcon = hasSelection
|
||||
? uiTheme.styledSymbol("status.success", "success")
|
||||
: uiTheme.styledSymbol("status.warning", "warning");
|
||||
const qBranch = isLastQuestion ? uiTheme.tree.last : uiTheme.tree.branch;
|
||||
const continuation = isLastQuestion ? " " : uiTheme.tree.vertical;
|
||||
const linePrefix = ` ${uiTheme.fg("dim", continuation)} `;
|
||||
|
||||
container.addChild(
|
||||
new Text(` ${uiTheme.fg("dim", branch)} ${statusIcon} ${uiTheme.fg("dim", `[${r.id}]`)}`, 0, 0),
|
||||
new Text(` ${uiTheme.fg("dim", qBranch)} ${uiTheme.fg("dim", `[${r.id}]`)}`, 0, 0),
|
||||
);
|
||||
container.addChild(new Markdown(r.question, 3, 0, mdTheme, accentStyle));
|
||||
|
||||
const answerLines: string[] = [];
|
||||
for (let j = 0; j < r.selectedOptions.length; j++) {
|
||||
const isLast = j === r.selectedOptions.length - 1 && r.customInput === undefined;
|
||||
const optBranch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
||||
const selectedLabel = renderInlineMarkdown(r.selectedOptions[j], mdTheme, t =>
|
||||
uiTheme.fg("toolOutput", t),
|
||||
);
|
||||
answerLines.push(
|
||||
`${continuation}${uiTheme.fg("dim", optBranch)} ${uiTheme.fg("success", optionMarker(uiTheme, r.multi, true))} ${selectedLabel}`,
|
||||
);
|
||||
}
|
||||
if (answerLines.length > 0) {
|
||||
container.addChild(new Text(answerLines.join("\n"), 0, 0));
|
||||
}
|
||||
if (r.customInput !== undefined) {
|
||||
container.addChild(new Text(renderCustomInput(uiTheme, continuation, r.customInput, true, false), 0, 0));
|
||||
} else if (r.selectedOptions.length === 0) {
|
||||
container.addChild(
|
||||
new Text(
|
||||
`${continuation}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
);
|
||||
}
|
||||
container.addChild(
|
||||
new Text(
|
||||
renderAnswerOptions(uiTheme, mdTheme, linePrefix, r.options, r.selectedOptions, r.multi, r.customInput),
|
||||
0,
|
||||
0,
|
||||
),
|
||||
);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
@@ -815,33 +842,21 @@ export const askToolRenderer = {
|
||||
container.addChild(new Text(header, 0, 0));
|
||||
container.addChild(new Markdown(details.question, 1, 0, mdTheme, accentStyle));
|
||||
|
||||
const answerLines: string[] = [];
|
||||
if (details.selectedOptions && details.selectedOptions.length > 0) {
|
||||
for (let i = 0; i < details.selectedOptions.length; i++) {
|
||||
const isLast = i === details.selectedOptions.length - 1 && details.customInput === undefined;
|
||||
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
||||
const selectedLabel = renderInlineMarkdown(details.selectedOptions[i], mdTheme, t =>
|
||||
uiTheme.fg("toolOutput", t),
|
||||
);
|
||||
answerLines.push(
|
||||
` ${uiTheme.fg("dim", branch)} ${uiTheme.fg("success", optionMarker(uiTheme, details.multi, true))} ${selectedLabel}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (answerLines.length > 0) {
|
||||
container.addChild(new Text(answerLines.join("\n"), 0, 0));
|
||||
}
|
||||
if (details.customInput !== undefined) {
|
||||
container.addChild(new Text(renderCustomInput(uiTheme, " ", details.customInput, true, false), 0, 0));
|
||||
} else if (!details.selectedOptions || details.selectedOptions.length === 0) {
|
||||
container.addChild(
|
||||
new Text(
|
||||
` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`,
|
||||
0,
|
||||
0,
|
||||
container.addChild(
|
||||
new Text(
|
||||
renderAnswerOptions(
|
||||
uiTheme,
|
||||
mdTheme,
|
||||
" ",
|
||||
details.options,
|
||||
details.selectedOptions,
|
||||
details.multi,
|
||||
details.customInput,
|
||||
),
|
||||
);
|
||||
}
|
||||
0,
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user