[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
206
infrastructure/ai/quickMessages.ts
Normal file
206
infrastructure/ai/quickMessages.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
export interface AIQuickMessage {
|
||||
id: string;
|
||||
/** Display label shown in pickers and settings. */
|
||||
name: string;
|
||||
/** Slash-command token, e.g. `status` for `/status`. */
|
||||
slug: string;
|
||||
/** Prompt text inserted into the composer when selected. */
|
||||
content: string;
|
||||
/** Optional short hint shown in the slash picker. */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface UserSkillSlashOption {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export type SystemSlashCommandSlug = 'stop' | 'compact';
|
||||
|
||||
export interface SystemSlashCommand {
|
||||
slug: SystemSlashCommandSlug;
|
||||
name: string;
|
||||
descriptionKey: string;
|
||||
}
|
||||
|
||||
export type SlashCommandItem =
|
||||
| { kind: 'quickMessage'; message: AIQuickMessage }
|
||||
| { kind: 'system'; command: SystemSlashCommand }
|
||||
| { kind: 'skill'; skill: UserSkillSlashOption };
|
||||
|
||||
export const QUICK_MESSAGE_LIMITS = {
|
||||
name: 120,
|
||||
slug: 48,
|
||||
description: 240,
|
||||
content: 10000,
|
||||
maxItems: 200,
|
||||
} as const;
|
||||
|
||||
const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
||||
|
||||
export function normalizeQuickMessageSlug(input: string): string {
|
||||
return input
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, QUICK_MESSAGE_LIMITS.slug);
|
||||
}
|
||||
|
||||
export function isValidQuickMessageSlug(slug: string): boolean {
|
||||
return slug.length > 0 && SLUG_PATTERN.test(slug);
|
||||
}
|
||||
|
||||
export function slugFromQuickMessageName(name: string): string {
|
||||
return normalizeQuickMessageSlug(name);
|
||||
}
|
||||
|
||||
function clampString(value: unknown, maxLen: number): string {
|
||||
if (typeof value !== 'string') return '';
|
||||
return value.slice(0, maxLen);
|
||||
}
|
||||
|
||||
/** Validate and dedupe quick messages from localStorage or cloud sync. */
|
||||
export function sanitizeQuickMessages(raw: unknown): AIQuickMessage[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
|
||||
const seenSlugs = new Set<string>();
|
||||
const result: AIQuickMessage[] = [];
|
||||
|
||||
for (const entry of raw) {
|
||||
if (!entry || typeof entry !== 'object') continue;
|
||||
const record = entry as Record<string, unknown>;
|
||||
const idRaw = typeof record.id === 'string' ? record.id.trim() : '';
|
||||
const id = idRaw.length > 0 ? idRaw.slice(0, 64) : createQuickMessageId();
|
||||
const name = clampString(record.name, QUICK_MESSAGE_LIMITS.name).trim();
|
||||
const slug = normalizeQuickMessageSlug(clampString(record.slug, QUICK_MESSAGE_LIMITS.slug));
|
||||
const content = clampString(record.content, QUICK_MESSAGE_LIMITS.content).trim();
|
||||
const description = clampString(record.description, QUICK_MESSAGE_LIMITS.description).trim();
|
||||
|
||||
if (!name || !isValidQuickMessageSlug(slug) || !content) continue;
|
||||
if (seenSlugs.has(slug)) continue;
|
||||
seenSlugs.add(slug);
|
||||
|
||||
result.push({
|
||||
id,
|
||||
name,
|
||||
slug,
|
||||
content,
|
||||
description: description || undefined,
|
||||
});
|
||||
|
||||
if (result.length >= QUICK_MESSAGE_LIMITS.maxItems) break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function filterQuickMessages(
|
||||
messages: AIQuickMessage[],
|
||||
query: string,
|
||||
): AIQuickMessage[] {
|
||||
const lowerQuery = query.trim().toLowerCase();
|
||||
if (!lowerQuery) return messages;
|
||||
return messages.filter((message) => {
|
||||
const slug = message.slug.toLowerCase();
|
||||
const name = message.name.toLowerCase();
|
||||
return slug.startsWith(lowerQuery) || name.includes(lowerQuery);
|
||||
});
|
||||
}
|
||||
|
||||
export function filterUserSkillsForSlash(
|
||||
skills: UserSkillSlashOption[],
|
||||
query: string,
|
||||
): UserSkillSlashOption[] {
|
||||
return skills.filter((skill) => {
|
||||
if (typeof skill.slug !== 'string' || skill.slug.length === 0) return false;
|
||||
if (!slashQueryMatches(query, skill.slug, skill.name)) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export const SYSTEM_BUILTIN_SLASH_COMMANDS: SystemSlashCommand[] = [
|
||||
{ slug: 'compact', name: 'Compact context', descriptionKey: 'ai.chat.slashCompactDesc' },
|
||||
{ slug: 'stop', name: 'Stop', descriptionKey: 'ai.chat.slashStopDesc' },
|
||||
];
|
||||
|
||||
export function filterSystemSlashCommands(
|
||||
commands: SystemSlashCommand[],
|
||||
query: string,
|
||||
): SystemSlashCommand[] {
|
||||
const lowerQuery = query.trim().toLowerCase();
|
||||
if (!lowerQuery) return commands;
|
||||
return commands.filter((command) => (
|
||||
command.slug.startsWith(lowerQuery) || command.name.toLowerCase().includes(lowerQuery)
|
||||
));
|
||||
}
|
||||
|
||||
function slashQueryMatches(query: string, slug: string, name: string): boolean {
|
||||
const lowerQuery = query.trim().toLowerCase();
|
||||
if (!lowerQuery) return true;
|
||||
return slug.toLowerCase().startsWith(lowerQuery) || name.toLowerCase().includes(lowerQuery);
|
||||
}
|
||||
|
||||
export function buildSlashCommandItems(
|
||||
quickMessages: AIQuickMessage[],
|
||||
userSkills: UserSkillSlashOption[],
|
||||
query: string,
|
||||
includeSystemCommands = false,
|
||||
): SlashCommandItem[] {
|
||||
const systemCommands = includeSystemCommands
|
||||
? filterSystemSlashCommands(SYSTEM_BUILTIN_SLASH_COMMANDS, query)
|
||||
: [];
|
||||
const systemSlugs = new Set<string>(SYSTEM_BUILTIN_SLASH_COMMANDS.map((command) => command.slug));
|
||||
const reservedSlugs = new Set([
|
||||
...quickMessages.map((message) => message.slug),
|
||||
...(includeSystemCommands ? [...systemSlugs] : []),
|
||||
]);
|
||||
const filteredMessages = filterQuickMessages(quickMessages, query)
|
||||
.filter((message) => !systemSlugs.has(message.slug));
|
||||
return [
|
||||
...systemCommands.map((command) => ({
|
||||
kind: 'system' as const,
|
||||
command,
|
||||
})),
|
||||
...filteredMessages.map((message) => ({
|
||||
kind: 'quickMessage' as const,
|
||||
message,
|
||||
})),
|
||||
...filterUserSkillsForSlash(userSkills, query)
|
||||
.filter((skill) => !reservedSlugs.has(skill.slug))
|
||||
.map((skill) => ({
|
||||
kind: 'skill' as const,
|
||||
skill,
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
export function getSlashCommandItemKey(item: SlashCommandItem): string {
|
||||
if (item.kind === 'quickMessage') return `qm:${item.message.id}`;
|
||||
if (item.kind === 'system') return `sys:${item.command.slug}`;
|
||||
return `sk:${item.skill.id}`;
|
||||
}
|
||||
|
||||
export function getSlashCommandItemId(item: SlashCommandItem): string {
|
||||
if (item.kind === 'quickMessage') return item.message.id;
|
||||
if (item.kind === 'system') return item.command.slug;
|
||||
return item.skill.id;
|
||||
}
|
||||
|
||||
export function createQuickMessageId(): string {
|
||||
return `qm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
export function isSystemStopSlashCommand(input: string): boolean {
|
||||
const trimmed = input.trim();
|
||||
return /^\/stop(?:\s|$)/i.test(trimmed) || trimmed.toLowerCase() === '/stop';
|
||||
}
|
||||
|
||||
export function getSystemSlashCommand(input: string): SystemSlashCommandSlug | null {
|
||||
const trimmed = input.trim().toLowerCase();
|
||||
if (/^\/stop(?:\s|$)/.test(trimmed)) return 'stop';
|
||||
if (trimmed === '/compact') return 'compact';
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user