// /websearch-order UI — tabbed grab-and-move lists for the search and fetch
// fallback chains. Tab switches tools; ↑↓ navigates or moves a grabbed item;
// space grabs/drops an item; enter saves; esc cancels.
import type {
ExtensionCommandContext,
KeybindingsManager,
Theme,
} from "@earendil-works/pi-coding-agent";
import type { Component, TUI } from "@earendil-works/pi-tui";
import { truncateToWidth } from "@earendil-works/pi-tui";
export interface OrderItem {
id: string;
/** Short credential/config summary shown next to the name. */
detail: string;
/** True when the provider is currently available for this tool. */
active: boolean;
}
export interface OrderTab {
id: "search" | "fetch";
label: string;
items: OrderItem[];
}
export interface ProviderOrders {
search: string[];
fetch: string[];
}
const NAME_WIDTH = 11;
/** Build the editable full order without losing unavailable providers from a
* previously saved order. */
export function completeProviderOrder
(
canonical: readonly P[],
resolved: readonly P[],
configuredHead?: P,
configuredOrder?: readonly P[],
): P[] {
const known = new Set(canonical);
return [
...new Set([
...(configuredHead && known.has(configuredHead) ? [configuredHead] : []),
...(Array.isArray(configuredOrder)
? configuredOrder.filter((provider) => known.has(provider))
: []),
...resolved,
...canonical,
]),
];
}
/** Open the tabbed reorder dialog; resolves with both orders, or null on cancel. */
export function promptProviderOrder(
ctx: ExtensionCommandContext,
tabs: [OrderTab, OrderTab],
): Promise {
return ctx.ui.custom(
(tui: TUI, theme: Theme, keybindings: KeybindingsManager, done) => {
let tabIndex = 0;
let grabbed = false;
const orders: ProviderOrders = {
search: tabs.find((tab) => tab.id === "search")?.items.map((item) => item.id) ?? [],
fetch: tabs.find((tab) => tab.id === "fetch")?.items.map((item) => item.id) ?? [],
};
const cursors: Record = { search: 0, fetch: 0 };
const activeTab = (): OrderTab => tabs[tabIndex];
const activeOrder = (): string[] => orders[activeTab().id];
const move = (delta: -1 | 1): void => {
const tab = activeTab();
const order = activeOrder();
const cursor = cursors[tab.id];
if (grabbed) {
const target = cursor + delta;
if (target < 0 || target >= order.length) return;
[order[cursor], order[target]] = [order[target], order[cursor]];
cursors[tab.id] = target;
} else if (order.length > 0) {
cursors[tab.id] = (cursor + delta + order.length) % order.length;
}
tui.requestRender();
};
const switchTab = (): void => {
grabbed = false;
tabIndex = (tabIndex + 1) % tabs.length;
tui.requestRender();
};
const component: Component = {
render: (width: number): string[] => {
const tab = activeTab();
const order = activeOrder();
const itemById = new Map(tab.items.map((item) => [item.id, item]));
const tabLabels = tabs
.map((candidate, index) => {
const label = ` ${candidate.label} `;
return index === tabIndex
? theme.bg("selectedBg", theme.fg("text", theme.bold(label)))
: theme.fg("dim", label);
})
.join(" ");
const help = grabbed
? "↑↓ move item • space drop • enter save • tab switch • esc cancel"
: "↑↓ navigate • space grab • enter save • tab switch • esc cancel";
const lines = [
theme.fg("accent", theme.bold("Web provider order")),
"",
`${tabLabels}${theme.fg("dim", " Tab switches tool")}`,
"",
theme.fg("dim", help),
"",
];
order.forEach((id, index) => {
const item = itemById.get(id);
const isCursor = index === cursors[tab.id];
const isGrabbed = isCursor && grabbed;
const marker = isGrabbed
? theme.fg("accent", "● ")
: isCursor
? theme.fg("accent", "→ ")
: " ";
const name = isGrabbed ? theme.fg("accent", theme.bold(id)) : theme.bold(id);
const pad = " ".repeat(Math.max(0, NAME_WIDTH - id.length));
const status = item?.active ? theme.fg("success", "✓") : theme.fg("dim", "•");
const detail = theme.fg("muted", item?.detail ?? "");
lines.push(`${marker}${name}${pad} ${status} ${detail}`);
});
lines.push(
"",
theme.fg(
"dim",
"Top runs first; unavailable (•) providers keep their position and are skipped.",
),
);
return lines.map((line) => truncateToWidth(line, Math.max(0, width)));
},
invalidate: (): void => {},
handleInput: (data: string): void => {
if (keybindings.matches(data, "tui.select.cancel")) {
done(null);
return;
}
if (keybindings.matches(data, "tui.input.tab")) {
switchTab();
return;
}
if (keybindings.matches(data, "tui.select.up")) {
move(-1);
return;
}
if (keybindings.matches(data, "tui.select.down")) {
move(1);
return;
}
if (keybindings.matches(data, "tui.select.confirm")) {
done({ search: [...orders.search], fetch: [...orders.fetch] });
return;
}
if (data === " ") {
grabbed = !grabbed;
tui.requestRender();
}
},
};
return component;
},
);
}