mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-09 11:51:15 +02:00
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>
153 lines
5.4 KiB
TypeScript
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);
|
|
}
|