t3-code-android-nightly/packages/shared/src/composerInlineTokens.ts
Julius Marminge de34391427
feat(orchestrator): introduce new orchestrator (#2829)
Co-authored-by: maria-rcks <maria@kuuro.net>
Co-authored-by: Bilal Bakr <62337003+Bil0000@users.noreply.github.com>
Co-authored-by: shivam <91240327+shivamhwp@users.noreply.github.com>
Co-authored-by: Vitalii Yehorov <vitalyiegorov@gmail.com>
Co-authored-by: Jake Leventhal <jakeleventhal@me.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Alex Southwell <saphid@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Nicholas Wasmiller <derped@mineperial.com>
Co-authored-by: PB <poilmb@gmail.com>
Co-authored-by: Exotic <118054752+extoci@users.noreply.github.com>
Co-authored-by: Yash Singh <saiansh2525@gmail.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Theo Browne <me@t3.gg>
Co-authored-by: Gabriel De Andrade <30420087+gabrielelpidio@users.noreply.github.com>
Co-authored-by: Dara Adedeji <76637177+SunkenInTime@users.noreply.github.com>
Co-authored-by: scratchyone <scratchywon@gmail.com>
Co-authored-by: Dominic Roy <dominic@sdko.org>
Co-authored-by: chukfinley <chuk@chuk.dev>
Co-authored-by: Primož Ajdišek <bigpod@bigpod.si>
Co-authored-by: benthecarman <benthecarman@live.com>
Co-authored-by: NaveDanan <nave0712@gmail.com>
Co-authored-by: aaditagrawal <103925638+aaditagrawal@users.noreply.github.com>
Co-authored-by: Aditya Garud <153842990+yashranaway@users.noreply.github.com>
Co-authored-by: Nick Anisimov <n.anisimov.23@gmail.com>
Co-authored-by: MacKinley Smith <smithmackinley@gmail.com>
Co-authored-by: Yordis Prieto <yordis.prieto@gmail.com>
Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com>
Co-authored-by: AKolenda <akole779@mtroyal.ca>
Co-authored-by: Guillermo Casanova <75276669+Gigioxx@users.noreply.github.com>
Co-authored-by: Otavio Salvador <otavio@ossystems.com.br>
Co-authored-by: Shirish Pothi <183252392+shirishpothi@users.noreply.github.com>
Co-authored-by: Ishaan Kothari <ishaanko.mail@gmail.com>
Co-authored-by: Bob Fowler <bob@rjf.ca>
Co-authored-by: Anton Bezdenezhnykh <gamer392@yandex.ru>
Co-authored-by: ValeraZSD <48602572+ValeraZSD@users.noreply.github.com>
Co-authored-by: Ephraim <ephraim39hr14m@gmail.com>
Co-authored-by: Ryan Ilano <ryanilano@users.noreply.github.com>
Co-authored-by: Alex <me@pixp.cc>
Co-authored-by: maco <gosarmarcel7@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Tristan Knight <admin@snappeh.com>
Co-authored-by: PR Batch Tester <agent@local.test>
Co-authored-by: oliver <97427849+flamboh@users.noreply.github.com>
Co-authored-by: kamkm <99585688+Kamkmgamer@users.noreply.github.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
2026-10-02 12:22:22 -07:00

153 lines
5.4 KiB
TypeScript

export type ComposerInlineToken =
| {
readonly type: "mention";
readonly value: string;
readonly source: string;
readonly start: number;
readonly end: number;
}
| {
readonly type: "skill";
readonly value: string;
readonly source: string;
readonly start: number;
readonly end: number;
};
export interface CollectComposerInlineTokensOptions {
readonly preserveTrailingFrom?: ReadonlyArray<ComposerInlineToken>;
}
/**
* A skill name may start with a digit, but compact monetary amounts and
* numeric expressions like "$20", "$20k", "$100M", and "$1e6" must stay prose:
* the composer chips any matched `$name` token, known or not. Tokens beginning
* with digits must not match numbers with currency/exponent suffixes, and must
* contain at least one letter. Any currency symbol is accepted as the sigil.
*/
const SKILL_MENTION_SOURCE =
/(^|\s)\p{Sc}(?![0-9][0-9_]*(?:[kKmMbBtT]|[eE][0-9]+)?(?:\s|$))(?=[a-zA-Z0-9:_-]*[a-zA-Z])([a-zA-Z0-9][a-zA-Z0-9:_-]*)/u
.source;
// While typing, a token only becomes a chip once a delimiter follows it, so a
// half-typed name at the end of the text stays plain.
const SKILL_TOKEN_REGEX = new RegExp(`${SKILL_MENTION_SOURCE}(?=\\s)`, "gu");
/**
* Skill mentions in a sent prompt, which may also end at the end of the text.
* Group 1 is the leading delimiter and group 2 the skill name. The pattern is
* global, so use it with `matchAll` or `replace`, not `test` or `exec`.
*/
export const SKILL_MENTION_PATTERN = new RegExp(`${SKILL_MENTION_SOURCE}(?=\\s|$)`, "gu");
const MENTION_TOKEN_REGEX = /(^|\s)@(?:"((?:\\.|[^"\\])*)"|([^\s@"]+))(?=\s)/g;
/**
* The label body is bounded rather than `*`. Unbounded, every whitespace in
* the composer is a candidate start: the engine scans the rest of the text for
* a closing `]`, fails, and rescans from the next whitespace — quadratic on
* input like " [[[[[…". A cap makes each attempt constant-bounded.
*
* Only a basename ever survives the `label !== basename` check below, so this
* cannot reject a link a user could meaningfully write; the longest filename
* any common filesystem allows is 255.
*/
const MAX_FILE_LINK_LABEL_LENGTH = 512;
const FILE_LINK_TOKEN_REGEX = new RegExp(
`(^|\\s)\\[((?:\\\\.|[^\\]\\\\]){0,${MAX_FILE_LINK_LABEL_LENGTH}})\\]\\(([^)\\s]+)\\)(?=\\s)`,
"g",
);
const URI_SCHEME_REGEX = /^[A-Za-z][A-Za-z0-9+.-]*:/;
const WINDOWS_DRIVE_PATH_REGEX = /^[A-Za-z]:[\\/]/;
// Autocomplete emits canonical file links, so ambiguous bare @scope/package text stays a package.
const SCOPED_PACKAGE_REFERENCE_REGEX =
/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*(?:\/[^\s@"]+)*$/;
function collectMentionTokens(text: string): ComposerInlineToken[] {
const matches: ComposerInlineToken[] = [];
for (const match of text.matchAll(FILE_LINK_TOKEN_REGEX)) {
const fullMatch = match[0];
const prefix = match[1] ?? "";
const label = (match[2] ?? "").replace(/\\(.)/g, "$1");
const encodedPath = match[3] ?? "";
let path = encodedPath;
try {
path = decodeURIComponent(encodedPath);
} catch {
// Preserve malformed source rather than dropping a user-authored token.
}
const separatorIndex = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
const basename = separatorIndex >= 0 ? path.slice(separatorIndex + 1) : path;
const hasExternalScheme = URI_SCHEME_REGEX.test(path) && !WINDOWS_DRIVE_PATH_REGEX.test(path);
if (!path || hasExternalScheme || label !== basename) {
continue;
}
const start = (match.index ?? 0) + prefix.length;
const end = start + fullMatch.length - prefix.length;
matches.push({
type: "mention",
value: path,
source: text.slice(start, end),
start,
end,
});
}
for (const match of text.matchAll(MENTION_TOKEN_REGEX)) {
const fullMatch = match[0];
const prefix = match[1] ?? "";
const quotedPath = match[2];
const path = quotedPath !== undefined ? quotedPath.replace(/\\(.)/g, "$1") : (match[3] ?? "");
if (!path || (quotedPath === undefined && SCOPED_PACKAGE_REFERENCE_REGEX.test(path))) {
continue;
}
const start = (match.index ?? 0) + prefix.length;
const end = start + fullMatch.length - prefix.length;
matches.push({
type: "mention",
value: path,
source: text.slice(start, end),
start,
end,
});
}
return matches;
}
export function collectComposerInlineTokens(
text: string,
options: CollectComposerInlineTokensOptions = {},
): ReadonlyArray<ComposerInlineToken> {
const matches = collectMentionTokens(text);
for (const match of text.matchAll(SKILL_TOKEN_REGEX)) {
const fullMatch = match[0];
const prefix = match[1] ?? "";
const value = match[2] ?? "";
if (!value) {
continue;
}
const start = (match.index ?? 0) + prefix.length;
const end = start + fullMatch.length - prefix.length;
matches.push({
type: "skill",
value,
source: text.slice(start, end),
start,
end,
});
}
for (const token of options.preserveTrailingFrom ?? []) {
if (
token.end === text.length &&
text.slice(token.start, token.end) === token.source &&
!matches.some(
(match) =>
match.type === token.type && match.start === token.start && match.end === token.end,
)
) {
matches.push(token);
}
}
return [...matches].sort((left, right) => left.start - right.start);
}