Feeds the node-editor's variable autocomplete popover and the var legend.
Data flow: the VarEntry catalog (GET /api/vars, static — one per
gateway process) describes every name/family resolve() accepts, but
carries no live values; those come from the most recent debug trace for
the node's policy. useContextSuggestions stitches the two
together: it loads the catalog once (module-level cache), looks up the
predecessor node's ContextSnapshot within the latest trace, and
calls buildSuggestions to produce concrete, previewable rows.
Exported signatures (kept in sync with this comment — update both if
either drifts):
buildSuggestions was deliberately given optional requestBodyText /
responseBodyText parameters rather than resolving body text itself from
a step index — useContextSuggestions is the only caller that has
a trace + step index to walk, so it computes the texts via bodyText
and passes them in; buildSuggestions stays a pure function of a single
snapshot plus the two already-resolved strings.
predecessorId uses an undefined-vs-null encoding chosen by the caller
(the node editor, in Task 5/6): undefined means "this node has no
incoming edge yet" (nothing to preview from), null means "the incoming
edge comes from the listener" (preview from trace.initial, there being
no prior node step), and a string is a real predecessor node id.
headerFamily (backing both http_* and sent_http_*) skips-with-a-note
any captured header whose name contains _: the suggested var name is
built by lowercasing and mapping - to _ (X-Trace-Id → http_x_trace_id),
but resolve() on the gateway side maps _ back to - when it looks the
header up (src/vars/mod.rs), so a header that had a literal _ in it
(e.g. X_Trace_Id) is not the header that $http_x_trace_id actually
resolves — it would look up x-trace-id instead and most likely miss. The
chosen fix is a per-row note ("header name contains '' — not resolvable
via $http*") rather than omitting the row outright, so the header is
still visible in the popover/legend but is never shown with a (misleading)
live value. That guard is specific to the legacy $http_* mangling and
does not apply to the {{request.headers.<h>}} universal-template
form below — dashes (and underscores) are legal characters in that
syntax, so a header name is used there exactly as captured, with no
mangling and therefore nothing to warn about.
Two suggestion families, one list
Since Task 8, every row built here is tagged Suggestion.trigger:
'dollar' for the legacy $name/${name} completions (unchanged from
before), or 'brace' for the new {{path}} completions keyed off each
catalog entry's path (entries with an empty path — no direct template
equivalent, e.g. protocol, post_arg_* — simply produce no 'brace'
row). Both families are built into the same flat list rather than two
separate hook calls or two separate lists, because useContextSuggestions has exactly one source of truth (one catalog fetch,
one trace snapshot) and a single field only ever wants one trigger's rows
at a time — so the cheapest place to split them is at render time, in
VarInput, by filtering on trigger for whichever token ($/${
vs {{) the user is actually completing. This also means a 'brace' and
a 'dollar' row can legitimately share the same name string coincidence
without colliding (they never render side by side), and callers that
only ever want one family (like the var legend, keyed by the catalog's
legacy name) are unaffected by the other family's presence in the list.
'brace' rows for two different static catalog entries that happen to
share a path (e.g. uri/request_uri both — for now — mapping to
request.path) are de-duplicated by insert text in buildSuggestions, since two rows that insert byte-identical text would
otherwise be indistinguishable clutter in the popover.
A new 'env' group (fed by GET /api/env-vars via useContextSuggestions, module-cached the same way as the catalog) is
'brace'-only: {{env.NAME}} has no $-syntax equivalent (the {{ token
is still what opens the popover on an 'env-only' field), so every
env.* row is tagged 'brace' and carries no value (env-var values are
never fetched or shown — only names). It does, however, carry a second
insertion form, insertEnvOnly (${NAME}), that VarInput swaps in for
'env-only' fields — see Suggestion.insertEnvOnly's doc comment for why
{{env.NAME}} itself would never actually resolve there.
VarInput's new templateMode prop ('full' | 'env-only' | 'none',
default 'full') is a second, orthogonal filter applied on top of
trigger: Task 9 decides which fields pass a non-default mode, but the
filtering itself lives in VarInput now so one hook instance/suggestion
list can serve every field regardless of its mode.
Trace-derived
$varsuggestion engine.Feeds the node-editor's variable autocomplete popover and the var legend. Data flow: the VarEntry catalog (
GET /api/vars, static — one per gateway process) describes every name/familyresolve()accepts, but carries no live values; those come from the most recent debug trace for the node's policy. useContextSuggestions stitches the two together: it loads the catalog once (module-level cache), looks up the predecessor node's ContextSnapshot within the latest trace, and calls buildSuggestions to produce concrete, previewable rows.Exported signatures (kept in sync with this comment — update both if either drifts):
buildSuggestionswas deliberately given optionalrequestBodyText/responseBodyTextparameters rather than resolving body text itself from a step index — useContextSuggestions is the only caller that has a trace + step index to walk, so it computes the texts via bodyText and passes them in;buildSuggestionsstays a pure function of a single snapshot plus the two already-resolved strings.predecessorIduses an undefined-vs-null encoding chosen by the caller (the node editor, in Task 5/6):undefinedmeans "this node has no incoming edge yet" (nothing to preview from),nullmeans "the incoming edge comes from the listener" (preview fromtrace.initial, there being no prior node step), and a string is a real predecessor node id.headerFamily(backing bothhttp_*andsent_http_*) skips-with-a-note any captured header whose name contains_: the suggested var name is built by lowercasing and mapping-to_(X-Trace-Id→http_x_trace_id), butresolve()on the gateway side maps_back to-when it looks the header up (src/vars/mod.rs), so a header that had a literal_in it (e.g.X_Trace_Id) is not the header that$http_x_trace_idactually resolves — it would look upx-trace-idinstead and most likely miss. The chosen fix is a per-row note ("header name contains '' — not resolvable via $http*") rather than omitting the row outright, so the header is still visible in the popover/legend but is never shown with a (misleading) live value. That guard is specific to the legacy$http_*mangling and does not apply to the{{request.headers.<h>}}universal-template form below — dashes (and underscores) are legal characters in that syntax, so a header name is used there exactly as captured, with no mangling and therefore nothing to warn about.Two suggestion families, one list
Since Task 8, every row built here is tagged Suggestion.trigger:
'dollar'for the legacy$name/${name}completions (unchanged from before), or'brace'for the new{{path}}completions keyed off each catalog entry'spath(entries with an emptypath— no direct template equivalent, e.g.protocol,post_arg_*— simply produce no'brace'row). Both families are built into the same flat list rather than two separate hook calls or two separate lists, because useContextSuggestions has exactly one source of truth (one catalog fetch, one trace snapshot) and a single field only ever wants one trigger's rows at a time — so the cheapest place to split them is at render time, in VarInput, by filtering ontriggerfor whichever token ($/${vs{{) the user is actually completing. This also means a'brace'and a'dollar'row can legitimately share the samenamestring coincidence without colliding (they never render side by side), and callers that only ever want one family (like the var legend, keyed by the catalog's legacyname) are unaffected by the other family's presence in the list.'brace'rows for two different static catalog entries that happen to share apath(e.g.uri/request_uriboth — for now — mapping torequest.path) are de-duplicated byinserttext in buildSuggestions, since two rows that insert byte-identical text would otherwise be indistinguishable clutter in the popover.A new
'env'group (fed byGET /api/env-varsvia useContextSuggestions, module-cached the same way as the catalog) is'brace'-only:{{env.NAME}}has no$-syntax equivalent (the{{token is still what opens the popover on an'env-only'field), so everyenv.*row is tagged'brace'and carries novalue(env-var values are never fetched or shown — only names). It does, however, carry a second insertion form,insertEnvOnly(${NAME}), thatVarInputswaps in for'env-only'fields — seeSuggestion.insertEnvOnly's doc comment for why{{env.NAME}}itself would never actually resolve there.VarInput's newtemplateModeprop ('full' | 'env-only' | 'none', default'full') is a second, orthogonal filter applied on top oftrigger: Task 9 decides which fields pass a non-default mode, but the filtering itself lives inVarInputnow so one hook instance/suggestion list can serve every field regardless of its mode.