CoDEVX / lib /composer-autocomplete.ts
CodexMacTiger
feat: live package-scoped chat and thinking logs
837e3ac
import type { CommandMode, WorkPackage } from "./work-package-types";
export type ComposerPackageSuggestion = {
kind: "package";
id: string;
title: string;
shortName: string;
};
export type ComposerCommandSuggestion = {
kind: "command";
mode: CommandMode;
label: string;
description: string;
};
export type ComposerSuggestion =
| ComposerPackageSuggestion
| ComposerCommandSuggestion;
const AT_TRIGGER = /(^|\s)@([A-Za-z\s-]*)$/;
const SLASH_TRIGGER = /(^|\s)\/([A-Za-z-]*)$/i;
const COMMAND_SUGGESTIONS: ComposerCommandSuggestion[] = [
{
kind: "command",
mode: "ask",
label: "/ask",
description: "Ask about the current package context",
},
{
kind: "command",
mode: "plan",
label: "/plan",
description: "Break a package into actionable work",
},
{
kind: "command",
mode: "change",
label: "/change",
description: "Request updates to the current package",
},
{
kind: "command",
mode: "execute",
label: "/execute",
description: "Run the current package flow",
},
];
function normalize(value: string) {
return value.trim().replace(/\s+/g, " ").toLowerCase();
}
function packageScore(workPackage: WorkPackage, query: string) {
const title = normalize(workPackage.title);
const shortName = normalize(workPackage.shortName);
if (!query) {
return 1;
}
return (
Number(shortName.startsWith(query)) * 4 +
Number(title.startsWith(query)) * 3 +
Number(shortName.includes(query)) * 2 +
Number(title.includes(query))
);
}
export function getComposerSuggestions(
draft: string,
workPackages: WorkPackage[],
): ComposerSuggestion[] {
const atMatch = draft.match(AT_TRIGGER);
if (atMatch) {
const query = normalize(atMatch[2] ?? "");
return workPackages
.map((workPackage) => ({
kind: "package" as const,
id: workPackage.id,
title: workPackage.title,
shortName: workPackage.shortName,
score: packageScore(workPackage, query),
}))
.filter((suggestion) => suggestion.score > 0)
.sort((left, right) => right.score - left.score)
.slice(0, 6)
.map((suggestion) => ({
kind: suggestion.kind,
id: suggestion.id,
title: suggestion.title,
shortName: suggestion.shortName,
}));
}
const slashMatch = draft.match(SLASH_TRIGGER);
if (slashMatch) {
const query = normalize(slashMatch[2] ?? "");
return COMMAND_SUGGESTIONS.filter((suggestion) => {
return !query || suggestion.mode.startsWith(query);
});
}
return [];
}
export function applyComposerSuggestion(args: {
draft: string;
suggestion: ComposerSuggestion;
selectedWorkPackage?: Pick<WorkPackage, "shortName" | "title">;
}) {
const { draft, suggestion, selectedWorkPackage } = args;
if (suggestion.kind === "package") {
return draft.replace(
AT_TRIGGER,
(_, prefix: string) => `${prefix}@${suggestion.shortName} `,
);
}
const packageRef =
selectedWorkPackage?.shortName || selectedWorkPackage?.title || "";
const replacement = packageRef
? `@${packageRef} ${suggestion.mode} `
: `/${suggestion.mode} `;
return draft.replace(SLASH_TRIGGER, (_, prefix: string) => `${prefix}${replacement}`);
}