Commit 034a45045bb9
Changed files (1)
dots
pi
agent
extensions
dots/pi/agent/extensions/review.ts
@@ -1092,7 +1092,7 @@ export default function reviewExtension(pi: ExtensionAPI) {
});
// Build select items
- const items: SelectItem[] = sorted.map((pr) => {
+ const allItems: SelectItem[] = sorted.map((pr) => {
const draft = pr.isDraft ? " [draft]" : "";
const review = reviewDecisionLabel(pr.reviewDecision);
return {
@@ -1103,27 +1103,55 @@ export default function reviewExtension(pi: ExtensionAPI) {
});
// Add manual entry option at the bottom
- items.push({
+ const manualItem: SelectItem = {
value: "__manual__",
label: "Enter PR number manually…",
description: "(for cross-repo or closed PRs)",
- });
+ };
- // Show selector
+ /** Fuzzy-match: all terms (split by space) must appear somewhere in the searchable text */
+ function fuzzyMatch(item: SelectItem, query: string): boolean {
+ if (!query) return true;
+ const searchable = `${item.label} ${item.description || ""}`.toLowerCase();
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
+ return terms.every((term) => searchable.includes(term));
+ }
+
+ // Show selector with search-as-you-type
const selected = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
+ let searchQuery = "";
+
+ function getFilteredItems(): SelectItem[] {
+ if (!searchQuery) return [...allItems, manualItem];
+ const filtered = allItems.filter((item) => fuzzyMatch(item, searchQuery));
+ filtered.push(manualItem);
+ return filtered;
+ }
+
+ let currentItems = getFilteredItems();
+
const container = new Container();
container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
- container.addChild(new Text(theme.fg("accent", theme.bold("Select a pull request to review"))));
- const selectList = new SelectList(items, Math.min(items.length, 12), {
+ const headerText = new Text("", 0, 0);
+ function updateHeader() {
+ const title = theme.fg("accent", theme.bold("Select a pull request to review"));
+ if (searchQuery) {
+ headerText.setText(`${title} ${theme.fg("warning", `filter: ${searchQuery}`)}`);
+ } else {
+ headerText.setText(title);
+ }
+ }
+ updateHeader();
+ container.addChild(headerText);
+
+ let selectList = new SelectList(currentItems, Math.min(currentItems.length, 12), {
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),
});
-
- selectList.searchable = true;
selectList.onSelect = (item) => done(item.value);
selectList.onCancel = () => done(null);
@@ -1131,10 +1159,47 @@ export default function reviewExtension(pi: ExtensionAPI) {
container.addChild(new Text(theme.fg("dim", "Type to filter • enter to select • esc to cancel")));
container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
+ function rebuildList() {
+ currentItems = getFilteredItems();
+ const newList = new SelectList(currentItems, Math.min(currentItems.length, 12), {
+ 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),
+ });
+ newList.onSelect = (item) => done(item.value);
+ newList.onCancel = () => done(null);
+ // Replace in container (index 2: after border + header)
+ const idx = container.children.indexOf(selectList);
+ if (idx !== -1) container.children[idx] = newList;
+ selectList = newList;
+ updateHeader();
+ }
+
return {
render(width: number) { return container.render(width); },
invalidate() { container.invalidate(); },
handleInput(data: string) {
+ // Backspace: remove last char from search
+ if (data === "\x7f" || data === "\b") {
+ if (searchQuery.length > 0) {
+ searchQuery = searchQuery.slice(0, -1);
+ rebuildList();
+ tui.requestRender();
+ }
+ return;
+ }
+
+ // Printable characters: append to search
+ if (data.length === 1 && data >= " " && data <= "~") {
+ searchQuery += data;
+ rebuildList();
+ tui.requestRender();
+ return;
+ }
+
+ // Everything else (arrows, enter, escape): pass to SelectList
selectList.handleInput(data);
tui.requestRender();
},