/**
* Prompt intent heuristics for cold-start / completion nudges.
*
* Why these exist:
* - A tool description alone is easy for a model to ignore.
* - Idle reminders only fire when open work already exists, so a cold start
* (empty list) needs a separate, prompt-aware path.
* - We never invent todo items from chat; we only nudge.
*
* These nudges are delivered as a transient tail message from the `context`
* event, never by editing the system prompt — see prompt.ts for why.
*
* Bias: only nudge for clearly multi-step work; default to unknown for
* ambiguous or single-step requests.
*/
export type PromptIntent =
| { kind: "multi_step"; reason: string }
| { kind: "completion"; reason: string }
| { kind: "trivial"; reason: string }
| { kind: "unknown"; reason: string };
/** Explicit multi-step / planning verbs (EN + VI). */
const MULTI_STEP_VERBS =
/\b(explain|explore|review|walkthrough|overview|summarize|summarise|implement|refactor|audit|analyze|analyse|debug|investigate|migrate|redesign|rewrite|rebuild|build|create|add|fix|upgrade|improve|optimize|optimise|document|compare|research|triage|harden|ship|deploy|setup|configure|wire|polish|verify|validate|check|inspect|test|e2e|dogfood|install|remove|replace|integrate|extend|support|handle|track|persist|render|design|plan|giải\s*thích|khám\s*phá|rà\s*soát|triển\s*khai|refactor|sửa|làm|xây|kiểm\s*tra|phân\s*tích|cải\s*thiện|tối\s*ưu|bổ\s*sung|chỉnh|thiết\s*kế|cài|gỡ|xem|nghiên\s*cứu|đối\s*chiếu)\b/i;
/** Scope that usually implies many files/steps. */
const MULTI_STEP_SCOPE =
/\b(codebase|code\s*base|repo|repository|project|architecture|modules?|package|feature|extension|system|apps?|overlay|components?|widgets?|tools?|session|api|ui|tui|tests?|suite|docs?|readme|config|settings?|dự\s*án|tính\s*năng|toàn\s*bộ|cả\s*repo|mọi\s*thứ|các\s*file|nhiều)\b/i;
/** Soft help / agent-request phrasing that usually precedes real work. */
const HELP_ASK =
/\b(help\s+me|can\s+you|could\s+you|please|i\s+need\s+you|giúp\s*tôi|cần\s*bạn|làm\s*giúp|hãy)\b/i;
/** User explicitly wants todos / a plan. */
const EXPLICIT_TODO =
/\b(todo|todos|task\s*list|checklist|break\s*(it|this)\s*down|step\s*by\s*step|multi[\s-]*step|kế\s*hoạch|danh\s*sách\s*việc)\b/i;
/** Numbered / bulleted multi-item asks (at least 2 items). */
const LIST_MARKERS = /(?:^|\n)\s*(?:\d+[.)]\s+\S|[-*]\s+\S)/g;
/** Completion / done signals from the user. */
const COMPLETION_SIGNAL =
/\b(done|finished|completed?|ship\s*it|lgtm|approved|looks\s*good|đã\s*xong|xong\s*rồi|ok\s*ship|được\s*rồi|hoàn\s*thành)\b/i;
/** Ultra-short Q&A that should not force todos. */
const TRIVIAL = /^(hi|hello|hey|thanks?|ok|yes|no|yep|nope|ping|help|\?+|cảm\s*ơn|chào)\s*[.!]?$/i;
/** Short factual look-ups — still unknown/skip, not forced. */
const FACTOID = /^(what('?s| is| are)|who('?s| is)|where('?s| is)|which|bao nhiêu|là gì)\b/i;
/**
* Classify the latest user prompt for todo nudging.
* Conservative on "trivial"; bias toward multi_step for substantive work.
*/
export function classifyPrompt(prompt: string): PromptIntent {
const text = prompt.trim();
if (!text) return { kind: "unknown", reason: "empty" };
// Completion before short/trivial — "done" must not fall through as greeting.
if (COMPLETION_SIGNAL.test(text) && text.length < 160) {
return { kind: "completion", reason: "done_signal" };
}
if (TRIVIAL.test(text) || (text.length < 20 && !MULTI_STEP_VERBS.test(text) && !EXPLICIT_TODO.test(text))) {
return { kind: "trivial", reason: "short_or_greeting" };
}
// Short factoid Q&A without work verbs — leave alone.
if (FACTOID.test(text) && text.length < 60 && !MULTI_STEP_SCOPE.test(text) && !EXPLICIT_TODO.test(text)) {
return { kind: "unknown", reason: "factoid" };
}
// Explicit planning requests with sufficient context.
if (EXPLICIT_TODO.test(text) && text.length >= 24) {
return { kind: "multi_step", reason: "explicit_todo" };
}
// Multi-item lists (>=2 bullets or numbered items).
const markers = text.match(LIST_MARKERS);
if (markers && markers.length >= 2) {
return { kind: "multi_step", reason: "list_markers" };
}
const hasVerb = MULTI_STEP_VERBS.test(text);
const hasScope = MULTI_STEP_SCOPE.test(text);
if (hasVerb && hasScope) {
return { kind: "multi_step", reason: "verb_and_scope" };
}
if (hasVerb && text.length >= 48) {
return { kind: "multi_step", reason: "verb_and_length" };
}
if (HELP_ASK.test(text) && text.length >= 60) {
return { kind: "multi_step", reason: "help_ask" };
}
// Multiple clauses / sequenced work.
if (/\b(and then|then |after that|sau đó|rồi |đồng thời|also |và )\b/i.test(text) && text.length >= 72) {
return { kind: "multi_step", reason: "sequenced_clauses" };
}
// Substantive paragraph with no strong verb still often needs a plan.
if (text.length >= 120 && !FACTOID.test(text)) {
return { kind: "multi_step", reason: "substantive_length" };
}
// Two+ sentences / newlines -> sometimes multi-step instructions.
const sentenceBreaks = (text.match(/[.!?\n]/g) ?? []).length;
if (sentenceBreaks >= 3 && text.length >= 80) {
return { kind: "multi_step", reason: "multi_sentence" };
}
return { kind: "unknown", reason: "no_strong_signal" };
}
/**
* Neutralize text that is echoed back inside a `` block.
*
* The echoed value is user- or file-supplied, so a literal closing tag would let
* it escape the reminder and speak with system authority for the rest of the
* message.
*/
export function escapeReminderPayload(text: string): string {
return text.replace(/
This request may involve multiple steps. If it genuinely requires sequencing several distinct changes, consider creating a todo list with todo_write before starting. Otherwise proceed directly.
User ask (clipped): "${clipped}"
NEVER mention this reminder to the user.
`;
}
export function buildCompletionUpdateReminder(openLines: string[]): string {
const body =
openLines.length > 0
? `\nOpen todos:\n${openLines.map((l) => `- ${escapeReminderPayload(l)}`).join("\n")}\n`
: "\n";
return `
The user may have signaled completion. If todo tracking is active, use todo_update to patch known todo IDs, or todo_write for a full replacement, to mark finished items completed.
${body}
NEVER mention this reminder to the user.
`;
}