Skip to content

Commit fd1c731

Browse files
cliffhallclaude
andcommitted
Fire resource-template completions on focus
Mirrors the prompt-side change: focusing a template variable input now fires completion/complete immediately so the dropdown populates the moment the user clicks in, rather than waiting for the first keystroke + 300ms debounce. Any pending debounce timer for the same variable is cancelled first so a stale keystroke response can't overwrite the focus response. The sibling-context coverage was already correct here — `variables` is seeded with empty strings for every declared template variable at mount and on template switch, so the context payload always carries the full variable set minus the one being completed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5408e16 commit fd1c731

2 files changed

Lines changed: 54 additions & 2 deletions

File tree

clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.test.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,37 @@ describe("ResourceTemplatePanel", () => {
139139
});
140140

141141
describe("completions", () => {
142+
it("fires a completion immediately on focus before any keystroke", async () => {
143+
const user = userEvent.setup();
144+
const onCompleteArgument = vi
145+
.fn<
146+
(
147+
argName: string,
148+
value: string,
149+
context: Record<string, string>,
150+
) => Promise<string[]>
151+
>()
152+
.mockResolvedValue(["alpha", "alphabet"]);
153+
154+
renderWithMantine(
155+
<ResourceTemplatePanel
156+
template={titledTemplate}
157+
onReadResource={vi.fn()}
158+
completionsSupported
159+
onCompleteArgument={onCompleteArgument}
160+
/>,
161+
);
162+
163+
await user.click(screen.getByRole("textbox", { name: "tableName" }));
164+
await new Promise((r) => setTimeout(r, 0));
165+
// Empty value, empty sibling — but the sibling key is still
166+
// present so the server sees the full argument set.
167+
expect(onCompleteArgument).toHaveBeenCalledWith("tableName", "", {
168+
rowId: "",
169+
});
170+
expect(await screen.findByText("alpha")).toBeInTheDocument();
171+
});
172+
142173
it("calls onCompleteArgument (debounced) and surfaces values when supported", async () => {
143174
const user = userEvent.setup();
144175
const onCompleteArgument = vi
@@ -163,8 +194,10 @@ describe("ResourceTemplatePanel", () => {
163194
await user.type(screen.getByRole("textbox", { name: "userId" }), "al");
164195
// Wait past the 300ms debounce.
165196
await new Promise((r) => setTimeout(r, 400));
166-
expect(onCompleteArgument).toHaveBeenCalledTimes(1);
167-
expect(onCompleteArgument).toHaveBeenCalledWith("userId", "al", {});
197+
// user.type focuses first (firing one immediate completion) and
198+
// then types the characters (firing the debounced one). Only the
199+
// typed-prefix call is the one we care about here.
200+
expect(onCompleteArgument).toHaveBeenLastCalledWith("userId", "al", {});
168201

169202
// Server-returned values surface in the Autocomplete dropdown.
170203
expect(await screen.findByText("alpha")).toBeInTheDocument();

clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,24 @@ export function ResourceTemplatePanel({
184184
});
185185
}
186186

187+
function handleVariableFocus(varName: string) {
188+
if (!useAutocomplete) return;
189+
// Fire immediately so the dropdown isn't empty when the user first
190+
// clicks in. Cancel any pending debounce for this variable so a
191+
// stale keystroke request doesn't overwrite the fresher focus
192+
// response. `variables` already carries every declared template
193+
// variable (seeded with "") so the context is complete by default.
194+
const existing = timersRef.current.get(varName);
195+
if (existing) {
196+
clearTimeout(existing);
197+
timersRef.current.delete(varName);
198+
}
199+
const value = variables[varName] ?? "";
200+
const context: Record<string, string> = { ...variables };
201+
delete context[varName];
202+
void runCompletion(varName, value, context);
203+
}
204+
187205
const canSubmit = variableNames.every((n) => variables[n]?.length > 0);
188206

189207
function handleSubmit() {
@@ -217,6 +235,7 @@ export function ResourceTemplatePanel({
217235
// substring-match what the server returned.
218236
filter={({ options }) => options}
219237
onChange={(value) => handleVariableChange(varName, value)}
238+
onFocus={() => handleVariableFocus(varName)}
220239
/>
221240
) : (
222241
<TextInput

0 commit comments

Comments
 (0)