fix(playground): copy icon, editable secrets and cursor position in the variable hover card - #22
Open
sundram-bruno wants to merge 26 commits into
Open
fix(playground): copy icon, editable secrets and cursor position in the variable hover card#22sundram-bruno wants to merge 26 commits into
sundram-bruno wants to merge 26 commits into
Conversation
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
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.
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.
sachin-bruno
approved these changes
Jul 31, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to BRU-4000, from the review comments on the variable hover card in the playground.
Requirements
(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.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
CopyButtonand theuseCopyhook 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 passesdisabled.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.