feat(settings/prompt): cycle through all variable occurrences + show use count

Clicking an in-use variable in "Available variables" jumped only to the first
occurrence. Now it cycles: from the occurrence the caret is on it advances to
the next (wrapping past the last), else jumps to the first at/after the caret.
Each badge also shows a ×N count when the variable is used more than once.

Verified live (vite dev → docker backend) on the adviser prompt: a ×7 variable
steps through all 7 occurrences in order and wraps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-06-26 15:21:02 +07:00
co-authored by Claude Opus 4.8
parent a47caa4ff9
commit 3114d13fcd
@@ -214,12 +214,22 @@ function SettingsPrompt() {
if (textarea) {
const currentValue = field.value || '';
const variablePattern = `{{.${variable}}}`;
const existing = currentValue.match(variableActionRegex(variable));
const matches = [...currentValue.matchAll(new RegExp(variableActionRegex(variable).source, 'g'))];
if (existing && existing.index !== undefined) {
const matchStart = existing.index;
if (matches.length > 0) {
// Cycle through occurrences: advance from the one the caret is already on
// (wrapping past the last), else jump to the first occurrence at/after the caret.
const { selectionEnd, selectionStart } = textarea;
const currentIdx = matches.findIndex(
(match) => match.index === selectionStart && match.index + match[0].length === selectionEnd,
);
const target =
currentIdx >= 0
? matches[(currentIdx + 1) % matches.length]
: (matches.find((match) => match.index >= selectionStart) ?? matches[0]);
const matchStart = target.index ?? 0;
textarea.focus();
textarea.setSelectionRange(matchStart, matchStart + existing[0].length);
textarea.setSelectionRange(matchStart, matchStart + target[0].length);
textarea.scrollTop = Math.max(0, caretOffsetTop(textarea, matchStart) - textarea.clientHeight / 2);
} else {
const start = textarea.selectionStart;
@@ -1069,9 +1079,11 @@ function Variables({ currentTemplate, onVariableClick, variables }: VariablesPro
</div>
<div className="bg-background flex flex-wrap gap-1.5 px-4 py-3">
{variables.map((variable) => {
const isUsed = variableActionRegex(variable).test(currentTemplate);
const count = (currentTemplate.match(new RegExp(variableActionRegex(variable).source, 'g')) ?? [])
.length;
const isUsed = count > 0;
const action = isUsed
? `Go to {{.${variable}}} in the template`
? `Go to next {{.${variable}}} in the template${count > 1 ? ` (${count} uses)` : ''}`
: `Insert {{.${variable}}} at the cursor`;
return (
@@ -1093,6 +1105,9 @@ function Variables({ currentTemplate, onVariableClick, variables }: VariablesPro
>
{isUsed ? <Check className="size-3" /> : null}
{`{{.${variable}}}`}
{count > 1 ? (
<span className="ml-0.5 text-[10px] tabular-nums opacity-70">×{count}</span>
) : null}
</Badge>
);
})}