This commit is contained in:
@@ -187,3 +187,70 @@ export function toolsForContext(contextId, disabled = [], customSkills = []) {
|
||||
*/
|
||||
export const toolAllowed = (name, allowed = []) =>
|
||||
allowed.some((tool) => tool.name === name);
|
||||
|
||||
/* ── Typed action intent ────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* How much has to be typed before a partial word counts as an intent.
|
||||
*
|
||||
* Three, and the number is the whole point of the rule. One or two characters
|
||||
* cannot say what somebody meant — "wh" is the start of four unrelated
|
||||
* questions — and offering anything on them is how the composer ended up
|
||||
* interrupting every reader who had already decided what to ask. Three is the
|
||||
* shortest prefix of a real verb, and it still matches nothing unless it is
|
||||
* genuinely the beginning of an action this page can perform.
|
||||
*/
|
||||
const ACTION_INTENT_MIN = 3;
|
||||
|
||||
/**
|
||||
* A phrase reduced to what it means, so matching is about intent not typing.
|
||||
*
|
||||
* Case, surrounding space and an article are not differences: "Create",
|
||||
* "create" and "create a" are all the beginning of the same request. Kept local
|
||||
* and deliberately tiny — it exists to compare two short phrases, and anything
|
||||
* cleverer would start deciding what a reader meant.
|
||||
*/
|
||||
const canon = (value) => String(value || '')
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((w) => w && !/^(?:a|an|the)$/.test(w))
|
||||
.join(' ');
|
||||
|
||||
/**
|
||||
* The actions this page can perform that the typed text is starting to name.
|
||||
*
|
||||
* Derived, never listed. The phrases come from the skills themselves — a
|
||||
* definition's `prompt:` is the sentence its author wrote for exactly this
|
||||
* purpose — and the candidate set is whatever `skillsForContext` already
|
||||
* resolved, which has the page filter and the agent's scoping applied to it.
|
||||
* So a skill added tomorrow is offered here without this file changing, a skill
|
||||
* switched off in Settings is not offered at all, and there is no second list
|
||||
* of action names to keep in step with the first.
|
||||
*
|
||||
* Only skills that DECLARE an action are eligible. A reading skill has nothing
|
||||
* to autocomplete towards: "which positions are in draft" is a question, and
|
||||
* offering it while somebody types is the generic-catalogue behaviour this
|
||||
* replaced.
|
||||
*
|
||||
* Matched on `canon`, so "Create" reaches "Create a position" — the article and
|
||||
* the case are not differences — and "create position" reaches it too. A prefix
|
||||
* rather than a substring: typing the middle of a phrase is not evidence of
|
||||
* intent, and substring matching is what made every keystroke produce chips.
|
||||
*/
|
||||
export function actionSuggestions(typed, skills = []) {
|
||||
const query = canon(typed);
|
||||
if (query.length < ACTION_INTENT_MIN) return [];
|
||||
|
||||
const seen = new Set();
|
||||
const out = [];
|
||||
for (const skill of skills) {
|
||||
if (!skill?.prompt || !skill.actions?.length) continue;
|
||||
const phrase = canon(skill.prompt);
|
||||
if (!phrase.startsWith(query)) continue;
|
||||
if (seen.has(phrase)) continue;
|
||||
seen.add(phrase);
|
||||
out.push({ label: skill.prompt, prompt: skill.prompt });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user