From 3114d13fcd2ba6091355cebb7f2a2ad2457e6323 Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Fri, 26 Jun 2026 15:21:02 +0700 Subject: [PATCH] feat(settings/prompt): cycle through all variable occurrences + show use count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/pages/settings/settings-prompt.tsx | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/settings/settings-prompt.tsx b/frontend/src/pages/settings/settings-prompt.tsx index cf7a72a0..dd37a545 100644 --- a/frontend/src/pages/settings/settings-prompt.tsx +++ b/frontend/src/pages/settings/settings-prompt.tsx @@ -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
{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 ? : null} {`{{.${variable}}}`} + {count > 1 ? ( + ×{count} + ) : null} ); })}