Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | 3x 16x 3x 3x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 45x 15x 15x 20x 9x 78x 45x 45x 45x 45x 45x 1x 45x 30x 45x 45x 45x 15x 15x 15x 15x 15x 1x 15x 15x 15x 15x 15x 15x 4x 3x 15x 16x 16x 48x 16x 16x 13x 6x 6x 13x | import { App, Modal } from "obsidian";
import { AGENT_LABELS, MESSAGES } from "../locales/en";
import { bindEscapeToClose } from "../utils";
import { AGENT_CLIENT, type AgentClient, type AgentClientDetection } from "../types";
export type AgentPickerResult = { kind: "connect"; client: AgentClient; remember: boolean } | { kind: "dismiss" };
export const AGENT_PICKER_RESULT = {
CONNECT: "connect",
DISMISS: "dismiss",
} as const;
const asInput = (el: HTMLElement): HTMLInputElement => el as unknown as HTMLInputElement;
/** Card order: the fullest pairing first, so the default selection is the best one. */
const CARD_ORDER: readonly AgentClient[] = [AGENT_CLIENT.OPENCODE, AGENT_CLIENT.CLAUDE, AGENT_CLIENT.HERMES];
const CARD_DESC: Record<AgentClient, string> = {
[AGENT_CLIENT.OPENCODE]: MESSAGES.AGENT_PICKER_OPENCODE_DESC,
[AGENT_CLIENT.CLAUDE]: MESSAGES.AGENT_PICKER_CLAUDE_DESC,
[AGENT_CLIENT.HERMES]: MESSAGES.AGENT_PICKER_HERMES_DESC,
};
/** Offered once when lilbee first sees an agent CLI on the machine. */
export class AgentPickerModal extends Modal {
private resolver: ((r: AgentPickerResult) => void) | null = null;
private resolved = false;
private selected: AgentClient | null = null;
private remember = true;
private cards: Map<AgentClient, HTMLElement> = new Map();
/** Populated by renderFooter during onOpen, before the first select() call. */
private connectBtn!: HTMLButtonElement;
constructor(
app: App,
private detections: AgentClientDetection[],
private claudianInstalled: boolean,
) {
super(app);
bindEscapeToClose(this);
}
openPicker(): Promise<AgentPickerResult> {
return new Promise((resolve) => {
this.resolver = resolve;
this.open();
});
}
onOpen(): void {
const root = this.contentEl;
root.empty();
root.addClass("lilbee-agent-picker");
root.createEl("h3", { text: MESSAGES.AGENT_PICKER_TITLE });
root.createEl("p", { cls: "lilbee-agent-picker-subtitle", text: MESSAGES.AGENT_PICKER_SUBTITLE });
const cards = root.createDiv({ cls: "lilbee-agent-picker-cards" });
for (const client of CARD_ORDER) {
this.renderCard(cards, client);
}
this.renderRemember(root);
this.renderFooter(root);
this.select(CARD_ORDER.find((client) => this.isDetected(client)) ?? null);
}
onClose(): void {
this.decide({ kind: AGENT_PICKER_RESULT.DISMISS });
}
private isDetected(client: AgentClient): boolean {
return this.detections.some((d) => d.client === client && d.cli_detected);
}
private renderCard(parent: HTMLElement, client: AgentClient): void {
const detected = this.isDetected(client);
const card = parent.createDiv({
cls: `lilbee-agent-picker-card${detected ? "" : " is-unavailable"}`,
});
const head = card.createDiv({ cls: "lilbee-agent-picker-card-head" });
head.createEl("strong", { text: AGENT_LABELS[client] });
if (client === AGENT_CLIENT.OPENCODE && this.claudianInstalled) {
head.createSpan({ cls: "lilbee-agent-picker-badge", text: MESSAGES.AGENT_PICKER_BADGE_CLAUDIAN });
}
if (!detected) {
head.createSpan({ cls: "lilbee-agent-picker-missing", text: MESSAGES.AGENT_PICKER_NOT_DETECTED });
}
card.createEl("p", { text: CARD_DESC[client] });
this.cards.set(client, card);
if (!detected) return;
card.addEventListener("click", () => this.select(client));
}
private renderRemember(root: HTMLElement): void {
const label = root.createEl("label", { cls: "lilbee-agent-picker-remember" });
const input = label.createEl("input", { attr: { type: "checkbox" } });
asInput(input).checked = this.remember;
input.addEventListener("change", () => {
this.remember = asInput(input).checked;
});
label.createSpan({ text: MESSAGES.AGENT_PICKER_REMEMBER });
}
private renderFooter(root: HTMLElement): void {
const foot = root.createDiv({ cls: "lilbee-agent-picker-foot" });
const notNow = foot.createEl("button", { text: MESSAGES.BUTTON_AGENT_NOT_NOW });
notNow.addEventListener("click", () => this.decide({ kind: AGENT_PICKER_RESULT.DISMISS }));
const connect = foot.createEl("button", { cls: "mod-cta" });
connect.addEventListener("click", () => {
if (this.selected === null) return;
this.decide({
kind: AGENT_PICKER_RESULT.CONNECT,
client: this.selected,
remember: this.remember,
});
});
this.connectBtn = connect;
}
private select(client: AgentClient | null): void {
this.selected = client;
for (const [key, card] of this.cards) {
card.toggleClass("selected", key === client);
}
this.connectBtn.disabled = client === null;
this.connectBtn.setText(
client === null ? MESSAGES.BUTTON_AGENT_CONNECT_NONE : MESSAGES.BUTTON_AGENT_CONNECT(AGENT_LABELS[client]),
);
}
private decide(result: AgentPickerResult): void {
if (this.resolved) return;
this.resolved = true;
this.resolver?.(result);
this.close();
}
}
|