pi config update

This commit is contained in:
Jonas H
2026-03-19 07:58:49 +01:00
parent a3c9183485
commit 871caa5adc
24 changed files with 6198 additions and 555 deletions

View File

@@ -0,0 +1,223 @@
import type { ExtensionUIContext } from "@mariozechner/pi-coding-agent";
import { Editor, type EditorTheme, Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
import {
OTHER_OPTION,
appendRecommendedTagToOptionLabels,
buildSingleSelectionResult,
type AskOption,
type AskSelection,
} from "./ask-logic";
import { INLINE_NOTE_WRAP_PADDING, buildWrappedOptionLabelWithInlineNote } from "./ask-inline-note";
interface SingleQuestionInput {
question: string;
options: AskOption[];
recommended?: number;
}
interface InlineSelectionResult {
cancelled: boolean;
selectedOption?: string;
note?: string;
}
function resolveInitialCursorIndexFromRecommendedOption(
recommendedOptionIndex: number | undefined,
optionCount: number,
): number {
if (recommendedOptionIndex == null) return 0;
if (recommendedOptionIndex < 0 || recommendedOptionIndex >= optionCount) return 0;
return recommendedOptionIndex;
}
export async function askSingleQuestionWithInlineNote(
ui: ExtensionUIContext,
questionInput: SingleQuestionInput,
): Promise<AskSelection> {
const baseOptionLabels = questionInput.options.map((option) => option.label);
const optionLabelsWithRecommendedTag = appendRecommendedTagToOptionLabels(
baseOptionLabels,
questionInput.recommended,
);
const selectableOptionLabels = [...optionLabelsWithRecommendedTag, OTHER_OPTION];
const initialCursorIndex = resolveInitialCursorIndexFromRecommendedOption(
questionInput.recommended,
optionLabelsWithRecommendedTag.length,
);
const result = await ui.custom<InlineSelectionResult>((tui, theme, _keybindings, done) => {
let cursorOptionIndex = initialCursorIndex;
let isNoteEditorOpen = false;
let cachedRenderedLines: string[] | undefined;
const noteByOptionIndex = new Map<number, string>();
const editorTheme: EditorTheme = {
borderColor: (text) => theme.fg("accent", text),
selectList: {
selectedPrefix: (text) => theme.fg("accent", text),
selectedText: (text) => theme.fg("accent", text),
description: (text) => theme.fg("muted", text),
scrollInfo: (text) => theme.fg("dim", text),
noMatch: (text) => theme.fg("warning", text),
},
};
const noteEditor = new Editor(tui, editorTheme);
const requestUiRerender = () => {
cachedRenderedLines = undefined;
tui.requestRender();
};
const getRawNoteForOption = (optionIndex: number): string => noteByOptionIndex.get(optionIndex) ?? "";
const getTrimmedNoteForOption = (optionIndex: number): string => getRawNoteForOption(optionIndex).trim();
const loadCurrentNoteIntoEditor = () => {
noteEditor.setText(getRawNoteForOption(cursorOptionIndex));
};
const saveCurrentNoteFromEditor = (value: string) => {
noteByOptionIndex.set(cursorOptionIndex, value);
};
const submitCurrentSelection = (selectedOptionLabel: string, note: string) => {
done({
cancelled: false,
selectedOption: selectedOptionLabel,
note,
});
};
noteEditor.onChange = (value) => {
saveCurrentNoteFromEditor(value);
requestUiRerender();
};
noteEditor.onSubmit = (value) => {
saveCurrentNoteFromEditor(value);
const selectedOptionLabel = selectableOptionLabels[cursorOptionIndex];
const trimmedNote = value.trim();
if (selectedOptionLabel === OTHER_OPTION && !trimmedNote) {
requestUiRerender();
return;
}
submitCurrentSelection(selectedOptionLabel, trimmedNote);
};
const render = (width: number): string[] => {
if (cachedRenderedLines) return cachedRenderedLines;
const renderedLines: string[] = [];
const addLine = (line: string) => renderedLines.push(truncateToWidth(line, width));
addLine(theme.fg("accent", "─".repeat(width)));
for (const questionLine of wrapTextWithAnsi(questionInput.question, Math.max(1, width - 1))) {
addLine(` ${theme.fg("text", questionLine)}`);
}
renderedLines.push("");
for (let optionIndex = 0; optionIndex < selectableOptionLabels.length; optionIndex++) {
const optionLabel = selectableOptionLabels[optionIndex];
const isCursorOption = optionIndex === cursorOptionIndex;
const isEditingThisOption = isNoteEditorOpen && isCursorOption;
const cursorPrefixText = isCursorOption ? "→ " : " ";
const cursorPrefix = isCursorOption ? theme.fg("accent", cursorPrefixText) : cursorPrefixText;
const bullet = isCursorOption ? "●" : "○";
const markerText = `${bullet} `;
const optionColor = isCursorOption ? "accent" : "text";
const prefixWidth = visibleWidth(cursorPrefixText) + visibleWidth(markerText);
const wrappedInlineLabelLines = buildWrappedOptionLabelWithInlineNote(
optionLabel,
getRawNoteForOption(optionIndex),
isEditingThisOption,
Math.max(1, width - prefixWidth),
INLINE_NOTE_WRAP_PADDING,
);
const continuationPrefix = " ".repeat(prefixWidth);
addLine(`${cursorPrefix}${theme.fg(optionColor, `${markerText}${wrappedInlineLabelLines[0] ?? ""}`)}`);
for (const wrappedLine of wrappedInlineLabelLines.slice(1)) {
addLine(`${continuationPrefix}${theme.fg(optionColor, wrappedLine)}`);
}
}
renderedLines.push("");
if (isNoteEditorOpen) {
addLine(theme.fg("dim", " Typing note inline • Enter submit • Tab/Esc stop editing"));
} else if (getTrimmedNoteForOption(cursorOptionIndex).length > 0) {
addLine(theme.fg("dim", " ↑↓ move • Enter submit • Tab edit note • Esc cancel"));
} else {
addLine(theme.fg("dim", " ↑↓ move • Enter submit • Tab add note • Esc cancel"));
}
addLine(theme.fg("accent", "─".repeat(width)));
cachedRenderedLines = renderedLines;
return renderedLines;
};
const handleInput = (data: string) => {
if (isNoteEditorOpen) {
if (matchesKey(data, Key.tab) || matchesKey(data, Key.escape)) {
isNoteEditorOpen = false;
requestUiRerender();
return;
}
noteEditor.handleInput(data);
requestUiRerender();
return;
}
if (matchesKey(data, Key.up)) {
cursorOptionIndex = Math.max(0, cursorOptionIndex - 1);
requestUiRerender();
return;
}
if (matchesKey(data, Key.down)) {
cursorOptionIndex = Math.min(selectableOptionLabels.length - 1, cursorOptionIndex + 1);
requestUiRerender();
return;
}
if (matchesKey(data, Key.tab)) {
isNoteEditorOpen = true;
loadCurrentNoteIntoEditor();
requestUiRerender();
return;
}
if (matchesKey(data, Key.enter)) {
const selectedOptionLabel = selectableOptionLabels[cursorOptionIndex];
const trimmedNote = getTrimmedNoteForOption(cursorOptionIndex);
if (selectedOptionLabel === OTHER_OPTION && !trimmedNote) {
isNoteEditorOpen = true;
loadCurrentNoteIntoEditor();
requestUiRerender();
return;
}
submitCurrentSelection(selectedOptionLabel, trimmedNote);
return;
}
if (matchesKey(data, Key.escape)) {
done({ cancelled: true });
}
};
return {
render,
invalidate: () => {
cachedRenderedLines = undefined;
},
handleInput,
};
});
if (result.cancelled || !result.selectedOption) {
return { selectedOptions: [] };
}
return buildSingleSelectionResult(result.selectedOption, result.note);
}