import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { api, type FileSuggestion, type SlashCommand } from "../api";
import { promptEditorStyles, type CompletionItem } from "./shared";
import "./AutocompleteMenu";
@customElement("prompt-editor")
export class PromptEditor extends LitElement {
@property({ type: Boolean }) disabled = false;
@property() sessionId?: string;
@property() cwd?: string;
@property({ attribute: false }) onSend?: (text: string) => void;
@property({ attribute: false }) onCloseSession?: () => void;
@state() private draft = "";
@state() private completions: CompletionItem[] = [];
@state() private selectedIndex = 0;
private requestVersion = 0;
render() {
return html`
`;
}
private updateDraft(value: string) {
this.draft = value;
void this.refreshCompletions();
}
private async refreshCompletions() {
const trigger = this.currentTrigger();
const version = ++this.requestVersion;
this.selectedIndex = 0;
if (!trigger) {
this.completions = [];
return;
}
if (trigger.kind === "command" && this.sessionId) {
const commands = await api.commands(this.sessionId).catch(() => [] as SlashCommand[]);
if (version !== this.requestVersion) return;
this.completions = commands
.filter((command) => command.name.toLowerCase().includes(trigger.query.toLowerCase()))
.slice(0, 12)
.map((command) => ({ kind: "command", replaceFrom: trigger.from, replaceTo: this.draft.length, insertText: `/${command.name}`, detail: command.source, description: command.description }));
} else if (trigger.kind === "file" && this.cwd) {
const files = await api.files(this.cwd, trigger.query, trigger.fileKind).catch(() => [] as FileSuggestion[]);
if (version !== this.requestVersion) return;
this.completions = files
.slice(0, 12)
.map((file) => ({ kind: "file", replaceFrom: trigger.from, replaceTo: this.draft.length, insertText: `@${file.path}`, detail: file.kind }));
}
}
private currentTrigger(): { kind: "command" | "file"; query: string; from: number; fileKind?: FileSuggestion["kind"] } | undefined {
const beforeCursor = this.draft;
if (beforeCursor.endsWith("@ ")) return { kind: "file", query: "", from: beforeCursor.length - 2, fileKind: "untracked" };
const tokenStart = Math.max(beforeCursor.lastIndexOf(" "), beforeCursor.lastIndexOf("\n")) + 1;
const token = beforeCursor.slice(tokenStart);
if (token.startsWith("/")) return { kind: "command", query: token.slice(1), from: tokenStart };
if (token.startsWith("@")) return { kind: "file", query: token.slice(1), from: tokenStart };
return undefined;
}
private handleKeyDown(event: KeyboardEvent) {
if (this.completions.length) {
if (event.key === "ArrowDown") {
event.preventDefault();
this.selectedIndex = (this.selectedIndex + 1) % this.completions.length;
return;
}
if (event.key === "ArrowUp") {
event.preventDefault();
this.selectedIndex = (this.selectedIndex - 1 + this.completions.length) % this.completions.length;
return;
}
if (event.key === "Tab" || event.key === "Enter") {
event.preventDefault();
this.pick(this.completions[this.selectedIndex]);
return;
}
if (event.key === "Escape") {
event.preventDefault();
this.completions = [];
return;
}
}
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
this.send();
}
}
private pick(item: CompletionItem) {
this.draft = `${this.draft.slice(0, item.replaceFrom)}${item.insertText} ${this.draft.slice(item.replaceTo)}`;
this.completions = [];
}
private send() {
const text = this.draft.trim();
if (!text || this.disabled) return;
this.draft = "";
this.completions = [];
this.onSend?.(text);
}
static styles = promptEditorStyles;
}