Skip to content

fix(playground): copy icon, editable secrets and cursor position in the variable hover card - #22

Open
sundram-bruno wants to merge 26 commits into
usebruno:mainfrom
sundram-bruno:feat/bru-4000-followup
Open

fix(playground): copy icon, editable secrets and cursor position in the variable hover card#22
sundram-bruno wants to merge 26 commits into
usebruno:mainfrom
sundram-bruno:feat/bru-4000-followup

Conversation

@sundram-bruno

@sundram-bruno sundram-bruno commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Follow-up to BRU-4000, from the review comments on the variable hover card in the playground.

Requirements

  1. The copy icon does not appear in the tooltip, though it is there in the Bruno app.
  2. Secrets show (Secret) and cannot be edited. In the app the value is blank with an eye icon instead, and it should be editable in the playground so a request that needs a secret can actually be sent. Same for external secrets.
  3. Clicking into the tooltip input puts the cursor at the start of the value instead of the end.

What was done

Copy icon. The button was built inside the value branches, so it disappeared whenever the value was being edited and never showed on a placeholder. It is now built once and sits beside the value, so it stays put in every state, including on a secret nobody has filled in yet. As in the app, clicking it confirms with a tick whether or not there was anything to copy. The docs pages are unchanged and still hide it for secrets.

Secrets. In the playground a secret now shows as asterisks, one per character, with an eye icon to reveal it. The value is editable, and what you type is used when the request is sent. The field holds the asterisks themselves rather than the secret, so the secret is never present in the page while hidden; copying a selection out of it still gives the real characters, not asterisks.

External secrets work the same way. They name a value that a secrets provider would supply, which a browser cannot reach, so a reference such as {{vaultKey}} used to go out as literal text with no way to fill it in. Now you can type a value for them too. Any secret still left blank keeps its own reference in the request, so {{vaultKey}} is sent as {{vaultKey}}, the same as any other unresolved variable, rather than as an empty value.

Typed values live only in memory for the session. They are never written back to the collection and a refresh clears them, which matters because a published collection does not carry secret values.

Secrets are referenced by their plain name, {{apiKey}} or {{aws_token}}, the same as any other variable.

Cursor. Clicking the value now puts the cursor at the end of the text.

Change outside the playground

Matching the app's copy button meant changing the shared CopyButton and the useCopy hook added in #21, so this part applies everywhere one appears, including the docs pages and the code snippet panel. After a copy the icon turns into a green tick for a second. The tick clears early if the value changes underneath it, so it never confirms text that was not the text copied, and an empty value now confirms too rather than doing nothing. A caller that needs to block a copy still passes disabled.

Docs are otherwise unchanged

The secret behaviour above is playground only. In the rendered docs a secret still shows (Secret) with no reveal, no copy and no editing, and external secrets are still left unresolved.

Note for reviewers

The generated code snippet still shows {{apiKey}} rather than a typed secret value. Display and request execution read from different places, and the existing test covering this was left untouched.

The copy button lived inside the editable and read-only display branches,
so it vanished while the value was being edited and never rendered on the
(Secret) or (empty) placeholders. Render it as a sibling of the value node
instead, matching the desktop app where the icons row is built once.
CopyButton no-ops on an empty string, so rendering it for process.env,
undefined variables and blank values left a visible control that does
nothing.
Matches the desktop copy button: the glyph swaps to a checkmark tinted
with the success token, the button is inert for the second the tick is
shown, and both glyphs use rounded stroke ends.

The tick is stroke="currentColor", so the tint is applied as the button's
colour, the same way brunoVarInfo.js does it.
Both glyphs hand-repeated the nine attributes baseIconProps already
exports, including the rounded stroke ends. Also drops a redundant
:hover selector and renames the card's icons row after what it holds.
… them

Secrets rendered a (Secret) placeholder and refused edits, so a request
needing one could not be sent from the playground. They now mask by
length, reveal behind an eye toggle, and accept a typed value.

External secrets ($secrets.name) are pointers with no value of their
own and were previously transmitted as literal text; they take a session
value too and resolve through the interpolator's existing dot-path
lookup, the same way process.env does.

Values live only on the in-memory playground collection, which is
rebuilt on reload and never serialized. Secrets stay out of the display
model, so the generated code snippet still shows {{name}}.
…ets prefix

Bruno has no $secrets namespace: an external secret's `name` is the
variable the collection references as {{name}}, and `secretName` only
says where a provider would fetch it. Keying entries by the bare name
makes {{vaultKey}} resolve and drops the interpolator bucket entirely,
since the runner now folds external secrets into the environment bag.

$secrets survives only as an internal scope, so the card can badge these
as Secret and mark them env-bound.

Fixture also gains folder- and collection-scoped secrets, referenced from
request headers, so every scope is verifiable.
Masking, reveal and copy for secrets are gated on the card's editable
prop, and external secrets only enter the resolver for the playground
provider. The docs card keeps its original (Secret) placeholder with no
reveal or copy, and leaves external secrets unresolved.
@sundram-bruno
sundram-bruno marked this pull request as ready for review July 30, 2026 22:21
The declaration-site scope union did not include external secrets, so
tsc -p tsconfig.build.json rejected the new variable source.
The docs pages also mount ItemVariableResolverProvider, so hardcoding
withExternalSecrets there resolved external secrets in the docs too.
It now follows the writable flag, and the unit test asserts through that
provider rather than one the docs never use.

A literal {{$secrets.foo}} provider reference lost its read-only note
when the scope was dropped from getReadOnlyNote. The note now keys off
whether the variable is editable, which separates a provider reference
from a declared external secret without a second scope label.

Rebuilding external secret rows in the environments editor dropped the
session value written by the hover card. The merge moved to a tested
helper in utils/environments.

Replaced a vacuous playground assertion: the card portals to
document.body so it is never inside playground-view, and no snippet is
rendered there at all.
The mirror layer painted asterisks over a transparent-text textarea, so
the real secret stayed in the DOM: visible under selection, read out by
screen readers, and exposed in forced-colors mode where author colours
are overridden. The asterisks were also drawn in a proportional font, so
they drifted from the caret by about 8px over an 18-character value.

The field now contains the mask itself and the real string lives in
component state. Display and real indices line up one to one, so an edit
is replayed onto the real value by splicing what the browser inserted at
the recorded selection. Nothing to drift, and no plaintext on the page.

Also from review: a cleared external secret is no longer dropped on the
way to the request, both spellings of the enable toggle are honoured, the
copy button no longer claims success for text it did not copy, and the
copied tint moved from an inline style to CSS.
…cleanups

The runner's new path had no end-to-end cover, so nothing checked the
thing the ticket exists for. The new test routes the outgoing request and
asserts both a typed environment secret and a typed external secret
appear in it.

Also: re-masking on a second reveal click is now asserted; the copied
tick is one locator requiring both the marker and the checkmark, so the
test no longer races three assertions against the one-second reset;
the tick locator moved into the page object; .reveal-button stopped
duplicating every .copy-button declaration; the spec's optional-chained
assertions and an inline cast chain are gone.
The change handler rebuilt the real value by splicing the browser's
insertion into the selection captured before the edit. With a collapsed
caret that range is empty, so a deletion spliced nothing into nothing and
the value was left unchanged while the field visibly shrank.

The replaced range now comes from the caret after the edit, which
describes a Backspace, a forward Delete, typing and a range replacement
alike.

The existing tests missed this because fill() sets the value in one shot;
the new test presses the keys.

Also: the copy button clears its tick when a getText-driven value changes,
and presence checks use toBeVisible rather than counting elements.
…oggles

The external-secret merge carried the whole existing entry, including a
legacy enabled key, while the row is written as disabled. An entry
authored enabled:false round-tripped to { enabled: false, disabled: false },
which the active check reads as off, so the secret silently stopped
resolving while the checkbox showed it on. The merge now drops enabled,
and the editor builds its rows with the same predicate the resolver uses.

Also records the agreed semantics for an unfilled secret: environment and
external secrets alike leave the reference unresolved rather than sending
an empty value.
… comments

Seeding the caret position when editing starts lets a single effect handle
both cases, since placing the caret at the end of a fresh edit is just
another position to restore.

Comments across the change are rewritten to explain the code to someone
reading it for the first time, rather than narrating how it came to be
written, and two that repeated an explanation given elsewhere are removed.
Comment thread packages/bruno-api-docs/e2e/components/variable-card/variable-card.component.ts Outdated
Replaces a compound attribute-and-element selector in the page object.
The id is derived from the button's own testId, matching how other child
ids in this codebase are built.
Copy state and its reset timer are needed by more than one control, so
they move out of CopyButton into a hook. The signature and return shape
match the one being added in usebruno#21 so the two can be reconciled easily; the
hook additionally clears its confirmation when the value changes, which
CopyButton needed and any other caller wants too.
…r-safe

The reset now returns the existing state when nothing has been copied, so
React bails out instead of scheduling a render. CodeEditor passes the
editor contents as `text`, so this effect runs on every keystroke.
Takes the useCopy hook and the tabler-icon CopyButton from usebruno#21, keeping
this branch's additions on top: the hook clears its confirmation when the
value changes, and the button still exposes data-copied for the green
tint and a derived test id on the tick.
…con spacing

Copy now sits after Download, and the warning icon loses its bottom
margin.
…mask

The card now shows the copy control whenever it is editable, matching the
app, so a secret waiting to be filled in still has one.

Selecting inside a masked field and copying put asterisks on the
clipboard, because the field contains the mask rather than the secret.
Copy and cut now take the selected range from the real value, which works
because a position in the field is the same position in the value.
The app ticks on every copy, whatever the variable holds, so the empty
value guard is gone. A caller that needs to block a copy passes disabled,
which ResponseActions already does for an empty response.
The icons row was guarded by `showCopy || secretFillable`, but a fillable
secret is always editable and an editable card always offers copy, so the
second term could never decide anything.

Selection was also captured on mousedown, which fires before the click has
moved the caret, so it only ever stored a value that onSelect immediately
replaced. The copy handler was a wrapper that discarded its return value.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants