feat: add answer extraction replay commands

This commit is contained in:
Erik Kotsiuba
2026-05-02 22:47:55 +02:00
parent 288caf49de
commit 5b70eca306
36 changed files with 1636 additions and 98 deletions
+23 -6
View File
@@ -52,8 +52,16 @@ type Tui = CustomCallbackArgs[0];
type Theme = CustomCallbackArgs[1];
type Keybindings = CustomCallbackArgs[2];
type Done = (result: AskResult) => void;
interface AskFlowOptions {
allowFreeform?: boolean;
}
type AskFlowParams = AskParams &
Pick<ExtensionContext, "cwd"> & { config: AskConfig; ctx: ExtensionContext };
Pick<ExtensionContext, "cwd"> & {
config: AskConfig;
ctx: ExtensionContext;
flowOptions: AskFlowOptions;
};
interface AskFlowController {
config: AskConfig;
@@ -72,12 +80,19 @@ interface AskFlowController {
export async function runAskFlow(
ctx: ExtensionContext,
params: AskParams
params: AskParams,
options: AskFlowOptions = {}
): Promise<AskResult> {
const store = getAskConfigStore();
const config = await store.getConfig();
return ctx.ui.custom<AskResult>((...args) =>
createAskFlowController(args, { ...params, config, cwd: ctx.cwd, ctx })
createAskFlowController(args, {
...params,
config,
cwd: ctx.cwd,
ctx,
flowOptions: options,
})
);
}
@@ -97,7 +112,7 @@ function createAskFlowController(
done,
editor: createEditor(tui, theme, params.cwd),
settingsOpen: false,
state: createInitialState(params),
state: createInitialState(params, params.flowOptions),
suppressAutoInputForSelection: false,
pendingReviewShortcutActionIndex: undefined,
theme,
@@ -321,8 +336,10 @@ function submitEditor(controller: AskFlowController, value: string) {
if (nextState.activeTabIndex !== controller.state.activeTabIndex) {
clearDismissNotice(controller);
}
controller.state = maybeAutoSubmitState(nextState, controller.config);
controller.editor.setText("");
controller.state = nextState;
syncSelection(controller);
controller.state = maybeAutoSubmitState(controller.state, controller.config);
hydrateEditor(controller);
refresh(controller);
maybeFinish(controller);
}
+24 -19
View File
@@ -76,7 +76,7 @@ function renderStandardOption(
pushWrappedText(
lines,
`${row.index + 1}. ${row.prefix}${row.label}`,
formatOptionLabel(row),
context.width,
context.theme,
row.color,
@@ -179,11 +179,16 @@ function renderOptionDetail(
lines: string[],
detail: OptionDetailModel | undefined,
context: QuestionRenderContext,
options: { suppressLeadingGap?: boolean } = {}
options: { indent?: string; suppressLeadingGap?: boolean } = {}
) {
if (!detail) {
return;
}
const indent = options.indent ?? " ";
const padding =
indent === " "
? UI_DIMENSIONS.editorContentPadding
: UI_DIMENSIONS.editorIndentedPadding;
if (detail.withGap && !options.suppressLeadingGap) {
lines.push("");
}
@@ -193,8 +198,8 @@ function renderOptionDetail(
editor: context.editor,
width: context.width,
theme: context.theme,
indent: " ",
padding: UI_DIMENSIONS.editorIndentedPadding,
indent,
padding,
placeholder: detail.placeholder,
});
return;
@@ -205,7 +210,7 @@ function renderOptionDetail(
note: detail.text,
width: context.width,
theme: context.theme,
indent: " ",
indent,
});
return;
}
@@ -215,8 +220,8 @@ function renderOptionDetail(
context.width,
context.theme,
"muted",
" ",
" "
indent,
indent
);
}
@@ -244,29 +249,29 @@ function renderEditorWithIndent(args: {
});
}
function formatOptionLabel(row: OptionRowModel): string {
return row.isFreeformOnly
? row.label
: `${row.index + 1}. ${row.prefix}${row.label}`;
}
function renderInteractiveCustomOption(
lines: string[],
row: OptionRowModel,
context: QuestionRenderContext
) {
const indent = row.isFreeformOnly ? " " : row.pointer;
pushWrappedText(
lines,
`${row.index + 1}. ${row.prefix}${row.label}`,
formatOptionLabel(row),
context.width,
context.theme,
row.color,
row.pointer,
" ".repeat(visibleWidth(row.pointer))
indent,
" ".repeat(visibleWidth(indent))
);
renderEditorWithIndent({
lines,
editor: context.editor,
width: context.width,
theme: context.theme,
indent: " ",
padding: UI_DIMENSIONS.editorIndentedPadding,
placeholder:
row.detail?.kind === "editor" ? row.detail.placeholder : "Type answer...",
renderOptionDetail(lines, row.detail, context, {
indent: row.isFreeformOnly ? " " : undefined,
});
}
+17 -3
View File
@@ -26,6 +26,7 @@ export interface OptionRowModel {
detail?: OptionDetailModel;
index: number;
isCustom: boolean;
isFreeformOnly: boolean;
label: string;
pointer: string;
prefix: string;
@@ -89,6 +90,7 @@ function buildOptionRowModel(
const answered = option.isCustomOption
? !!answer?.customText
: isOptionSelected(answer, option.value);
const pointer = getOptionPointer(option, selected);
return {
color: getOptionColor(answered, selected),
description: option.description,
@@ -98,13 +100,21 @@ function buildOptionRowModel(
: buildOptionDetailModel(context, option, selected),
index,
isCustom: !!option.isCustomOption,
isFreeformOnly: !!option.isFreeformOnlyOption,
label: option.label,
pointer: selected ? "❯ " : " ",
pointer,
prefix: getOptionPrefix(question.type, option, answered),
selected,
};
}
function getOptionPointer(option: AskDisplayOption, selected: boolean): string {
if (option.isFreeformOnlyOption) {
return "";
}
return selected ? "❯ " : " ";
}
function buildOptionDetailModel(
context: QuestionRenderContext,
option: AskDisplayOption | undefined,
@@ -139,11 +149,15 @@ function buildOptionDetailModel(
return {
kind: "editor",
placeholder: UI_TEXT.editorPlaceholderInput,
withGap: false,
withGap: !!option.isFreeformOnlyOption,
};
}
if (customText) {
return { kind: "custom-text", text: customText, withGap: false };
return {
kind: "custom-text",
text: customText,
withGap: !!option.isFreeformOnlyOption,
};
}
if (note) {
return { kind: "saved-note", text: note, withGap: false };