Reports · Issue #4421

Secret-type field save feedback with synthetic placeholder text

2026-09-30 · Bug · Low priority · Low effort

PARTIALLY REPRODUCED · High confidence in the tested form feedback. Actual PluginSettingsForm in Chromium, with an in-page save adapter. No credential persistence or real account is tested.

Claim, expected and actual

The reported concern is a field clearing after successful saving without recognizable success feedback. Current main uses autosave on blur. Expected: success is visibly distinguishable from unset state and failure, including when replacing an already-set value.

StageObserved in both runs
Initial unset fieldEmpty password input, placeholder [not set]
First simulated successful saveDraft clears; placeholder changes to [set]; no additional success message
Another successful save while already setDraft clears; same [set] placeholder; no new Saved text/checkmark in the form
Ordinary text controlEdited value remains visible after simulated success
Simulated save failureDraft remains masked; visible Synthetic save failure alert

This narrows the original claim: saved and unset are visibly different on this base, and failure is explicitly displayed. The remaining feedback gap is that a successful re-save adds no affirmative acknowledgement beyond clearing the draft and retaining the already-existing [set] placeholder. It would be inaccurate to claim all successful saves look identical to an unset field, or that failures silently clear the field.

The fixture uses only the literal SYNTHETIC_PLACEHOLDER. The adapter permits that value, returns controlled set-state metadata and never calls a server or secret store. It records method/outcome only. Success here means a simulated successful adapter response, not proven storage of a credential.

Root cause and proposed improvement

The field derives set state from storedValue.set and renders [set] or [not set] as its password-input placeholder. A secret-type initial draft is empty. Successful mutation updates the query cache; the following effect resets the draft when no newer edit or error remains. The feedback branch computes saveError and renders an alert only for errors. It does not render save.isSuccess feedback. This accounts for both the useful set-state indicator and the lack of an additional re-save acknowledgement.

Proposed improvement: show a concise, accessible Saved state after each successful mutation, including an already-set replacement, and reset it when editing starts. Preserve the existing [set]/[not set] distinction and visible failure state. Do not reveal any portion of a value to achieve this. No production fix or PR was created.

Trusted base and environment

Fetched public origin/main: facb6c161d9915d2517ed15f6101e4295cf85da1. Linux x86_64, Node 24.19.0, repository-pinned pnpm 9.15.0, Vite 8.0.12 optimized fixture builds, desktop headless Chromium 151.0.7922.173 at 1100×760. Both clean detached checkouts used separate frozen installations, browser profiles, ports and in-page state, with shared dependency downloads. Normal app builds and both isolated fixture builds succeeded. Some normal Turbo upstream/app tasks used the trusted build cache; each fixture build and browser run executed separately.

Exact repeatable steps

Save the three fixture files below under apps/app/issue4421/ in each checkout and capture.mjs outside the checkouts. Use pinned pnpm 9.15.0 and an installed Chromium. Start each preview and Chromium process in separate terminals, then run capture. All API fetches are intercepted in-page; unexpected requests fail instead of reaching any live service. The fixture button only moves focus; the real form autosaves on blur.

WORK=$(mktemp -d)
git clone https://github.com/get-bb/bb.git "$WORK/base"
git -C "$WORK/base" worktree add --detach "$WORK/first" facb6c161d9915d2517ed15f6101e4295cf85da1
git -C "$WORK/base" worktree add --detach "$WORK/second" facb6c161d9915d2517ed15f6101e4295cf85da1
# Save the fixture files in each checkout and capture.mjs in WORK.
cd "$WORK/first"
npm_config_cache="$WORK/npm-first" npm_config_devdir="$WORK/node-gyp-first" XDG_CACHE_HOME="$WORK/cache-first" pnpm install --frozen-lockfile --store-dir "$WORK/dependency-store"
pnpm exec turbo run build --filter=@bb/app
cd apps/app
pnpm exec vite build --config issue4421/vite.config.ts
pnpm exec vite preview --config issue4421/vite.config.ts --host 127.0.0.1 --port 49441 --strictPort
chromium --headless --no-sandbox --disable-gpu --no-first-run --disable-dev-shm-usage --remote-debugging-address=127.0.0.1 --remote-debugging-port=49442 --user-data-dir="$WORK/browser-first" about:blank
node "$WORK/capture.mjs" 49442 49441 "$WORK/first-run"
# Same-agent second clean reproduction:
cd "$WORK/second"
npm_config_cache="$WORK/npm-second" npm_config_devdir="$WORK/node-gyp-second" XDG_CACHE_HOME="$WORK/cache-second" pnpm install --frozen-lockfile --store-dir "$WORK/dependency-store"
pnpm exec turbo run build --filter=@bb/app
cd apps/app
pnpm exec vite build --config issue4421/vite.config.ts
pnpm exec vite preview --config issue4421/vite.config.ts --host 127.0.0.1 --port 49443 --strictPort
chromium --headless --no-sandbox --disable-gpu --no-first-run --disable-dev-shm-usage --remote-debugging-address=127.0.0.1 --remote-debugging-port=49444 --user-data-dir="$WORK/browser-second" about:blank
node "$WORK/capture.mjs" 49444 49443 "$WORK/second-run"

apps/app/issue4421/index.html

<html><head><title>Synthetic setting feedback</title></head><body><div id="root"></div><script type="module" src="/main.tsx"></script></body></html>

apps/app/issue4421/main.tsx

import React from "react";
import {createRoot} from "react-dom/client";
import {QueryClient,QueryClientProvider} from "@tanstack/react-query";
import {MemoryRouter} from "react-router-dom";
import {TooltipProvider} from "@bb/shared-ui/tooltip";
import {PluginSettingsForm} from "../src/components/plugin/PluginSettings";
import "../src/app.css";
const schema={placeholder:{type:"string",label:"Synthetic placeholder",secret:true,description:"Placeholder text only; no account or credential."},ordinary:{type:"string",label:"Ordinary text control"}};
let isSet=false,ordinary="Initial text",failNext=false;
const records:{method:string,outcome:string}[]=[];
Object.assign(window,{fixture:{records,fail:()=>{failNext=true;}}});
window.fetch=async(input,init)=>{
 const url=typeof input==="string"?input:input instanceof URL?input.href:input.url;
 if(!url.includes("/settings"))throw new Error("Unexpected fixture request");
 const method=init?.method??"GET";
 if(method==="PUT"){
  const data=JSON.parse(String(init?.body));
  if(Object.keys(data.values).some(key=>!["placeholder","ordinary"].includes(key)))throw new Error("Unexpected setting");
  if("placeholder" in data.values&&data.values.placeholder!=="SYNTHETIC_PLACEHOLDER")throw new Error("Only synthetic placeholder permitted");
  await new Promise(resolve=>setTimeout(resolve,300));
  if(failNext){failNext=false;records.push({method,outcome:"simulated failure"});return new Response(JSON.stringify({ok:false,error:"Synthetic save failure"}),{status:400,headers:{"Content-Type":"application/json"}});}
  if("placeholder" in data.values)isSet=true;
  if("ordinary" in data.values)ordinary=data.values.ordinary;
  records.push({method,outcome:"simulated success"});
 }else if(method!=="GET")throw new Error("Unexpected method");
 return new Response(JSON.stringify({ok:true,schema,values:{placeholder:{set:isSet},ordinary}}),{status:200,headers:{"Content-Type":"application/json"}});
};
const client=new QueryClient({defaultOptions:{queries:{retry:false,refetchOnWindowFocus:false},mutations:{retry:false}}});
createRoot(document.getElementById("root")!).render(<QueryClientProvider client={client}><MemoryRouter><TooltipProvider><main style={{padding:32,maxWidth:1050}}><h1>Synthetic plugin setting feedback</h1><p>Actual PluginSettingsForm. Saves are simulated entirely in this page; no credential store or account is used.</p><section id="actual-form" style={{border:"1px solid #999",padding:24,marginTop:24}}><PluginSettingsForm pluginId="synthetic-feedback"/></section><button id="blur-target" style={{marginTop:24}}>Move focus out of the field</button><p>Fixture note: text fields autosave on blur. This button only changes focus.</p></main></TooltipProvider></MemoryRouter></QueryClientProvider>);

apps/app/issue4421/vite.config.ts

import {defineConfig} from "vite";
import {sharedViteConfig} from "../vite.config";
export default defineConfig({...sharedViteConfig,root:__dirname,plugins:sharedViteConfig.plugins.filter(plugin=>!["bb:font-preload","bb:split-prefetch"].includes(plugin.name)),build:{...sharedViteConfig.build,outDir:"dist",emptyOutDir:true}});

capture.mjs

import { writeFile } from 'node:fs/promises';
const [debugPort,appPort,output]=process.argv.slice(2);
const tabs=await(await fetch(`http://127.0.0.1:${debugPort}/json/list`)).json();
const ws=new WebSocket(tabs.find(t=>t.type==='page').webSocketDebuggerUrl);
await new Promise(r=>ws.addEventListener('open',r,{once:true}));let id=0;const pending=new Map();
ws.addEventListener('message',event=>{const x=JSON.parse(event.data);if(x.id){const p=pending.get(x.id);pending.delete(x.id);x.error?p.reject(new Error(JSON.stringify(x.error))):p.resolve(x.result);}});
const call=(method,params={})=>new Promise((resolve,reject)=>{const serial=++id;pending.set(serial,{resolve,reject});ws.send(JSON.stringify({id:serial,method,params}));});
const evaluate=async expression=>{const x=await call('Runtime.evaluate',{expression,returnByValue:true,awaitPromise:true});if(x.exceptionDetails)throw new Error(JSON.stringify(x.exceptionDetails));return x.result.value;};
await call('Page.enable');await call('Runtime.enable');await call('Network.enable');await call('Network.setCacheDisabled',{cacheDisabled:true});
await call('Emulation.setDeviceMetricsOverride',{width:1100,height:760,deviceScaleFactor:1,mobile:false});
const errors=[];
ws.addEventListener('message',event=>{const x=JSON.parse(event.data);if(x.method==='Runtime.exceptionThrown')errors.push(x.params.exceptionDetails.text+': '+(x.params.exceptionDetails.exception?.description??''));});
await call('Page.navigate',{url:`http://127.0.0.1:${appPort}/`});
await new Promise(r=>setTimeout(r,2500));
const observations=[];
async function state(name){const result=await evaluate(`(()=>{const root=document.getElementById('actual-form');const input=root.querySelector('input[aria-label="Synthetic placeholder"]');if(!input)throw new Error('Missing synthetic field');return {placeholder:input.placeholder,inputLength:input.value.length,inputType:input.type,formText:root.innerText,alerts:[...root.querySelectorAll('[role="alert"]')].map(x=>x.innerText),ordinary:root.querySelector('input[aria-label="Ordinary text control"]').value,records:window.fixture.records};})()`);observations.push({name,...result});const shot=await call('Page.captureScreenshot',{format:'png',captureBeyondViewport:false});await writeFile(`${output}-${name}.png`,Buffer.from(shot.data,'base64'));return result;}
async function enter(label,text){await evaluate(`document.querySelector('input[aria-label="${label}"]').focus()`);await call('Input.insertText',{text});const p=await evaluate(`(()=>{const r=document.getElementById('blur-target').getBoundingClientRect();return {x:r.x+r.width/2,y:r.y+r.height/2};})()`);await call('Input.dispatchMouseEvent',{type:'mousePressed',button:'left',clickCount:1,...p});await call('Input.dispatchMouseEvent',{type:'mouseReleased',button:'left',clickCount:1,...p});await new Promise(r=>setTimeout(r,900));}
const initial=await state('initial');if(initial.placeholder!=='[not set]'||initial.inputLength!==0)throw new Error('Bad initial state');
await enter('Synthetic placeholder','SYNTHETIC_PLACEHOLDER');const first=await state('first-save');if(first.placeholder!=='[set]'||first.inputLength!==0||first.alerts.length)throw new Error('Unexpected first save');
await enter('Synthetic placeholder','SYNTHETIC_PLACEHOLDER');const replacement=await state('replacement');if(replacement.placeholder!=='[set]'||replacement.inputLength!==0||replacement.alerts.length)throw new Error('Unexpected replacement');
if(first.formText!==replacement.formText)throw new Error('Unexpected new confirmation text');
await enter('Ordinary text control',' added');const ordinary=await state('ordinary');if(!ordinary.ordinary.includes(' added'))throw new Error('Ordinary control lost');
await evaluate('window.fixture.fail()');await enter('Synthetic placeholder','SYNTHETIC_PLACEHOLDER');const failed=await state('failure');if(!failed.alerts.some(x=>x.includes('Synthetic save failure'))||failed.inputLength===0)throw new Error('Failure not visible');
if(failed.records.length!==4||failed.records.filter(x=>x.outcome==='simulated success').length!==3)throw new Error('Unexpected adapter calls');
if(errors.length)throw new Error(JSON.stringify(errors));
const result={observations,browser:await call('Browser.getVersion'),viewport:{width:1100,height:760},errors};await writeFile(output+'.json',JSON.stringify(result,null,2));console.log(JSON.stringify(result));ws.close();

Same-agent second clean reproduction: actual evidence

The same agent personally repeated the optimized fixture build and browser actions in the second clean checkout at the identical SHA with a fresh profile, port and page state. Both capture commands exited 0, all five state checks passed, and the observations match exactly. Each run recorded three simulated successful PUTs and one simulated failure. There were no runtime exceptions. All ten genuine screenshots were inspected. This is same-agent second clean reproduction, not independent verification.

first actual browser output

{
  "observations": [
    {
      "name": "initial",
      "placeholder": "[not set]",
      "inputLength": 0,
      "inputType": "password",
      "formText": "Synthetic placeholder\n\nsecret\n\nPlaceholder text only; no account or credential.\n\nOrdinary text control",
      "alerts": [],
      "ordinary": "Initial text",
      "records": []
    },
    {
      "name": "first-save",
      "placeholder": "[set]",
      "inputLength": 0,
      "inputType": "password",
      "formText": "Synthetic placeholder\n\nsecret\n\nPlaceholder text only; no account or credential.\n\nOrdinary text control",
      "alerts": [],
      "ordinary": "Initial text",
      "records": [
        {
          "method": "PUT",
          "outcome": "simulated success"
        }
      ]
    },
    {
      "name": "replacement",
      "placeholder": "[set]",
      "inputLength": 0,
      "inputType": "password",
      "formText": "Synthetic placeholder\n\nsecret\n\nPlaceholder text only; no account or credential.\n\nOrdinary text control",
      "alerts": [],
      "ordinary": "Initial text",
      "records": [
        {
          "method": "PUT",
          "outcome": "simulated success"
        },
        {
          "method": "PUT",
          "outcome": "simulated success"
        }
      ]
    },
    {
      "name": "ordinary",
      "placeholder": "[set]",
      "inputLength": 0,
      "inputType": "password",
      "formText": "Synthetic placeholder\n\nsecret\n\nPlaceholder text only; no account or credential.\n\nOrdinary text control",
      "alerts": [],
      "ordinary": "Initial text added",
      "records": [
        {
          "method": "PUT",
          "outcome": "simulated success"
        },
        {
          "method": "PUT",
          "outcome": "simulated success"
        },
        {
          "method": "PUT",
          "outcome": "simulated success"
        }
      ]
    },
    {
      "name": "failure",
      "placeholder": "[set]",
      "inputLength": 21,
      "inputType": "password",
      "formText": "Synthetic placeholder\n\nsecret\n\nPlaceholder text only; no account or credential.\n\nSynthetic save failure\n\nOrdinary text control",
      "alerts": [
        "Synthetic save failure"
      ],
      "ordinary": "Initial text added",
      "records": [
        {
          "method": "PUT",
          "outcome": "simulated success"
        },
        {
          "method": "PUT",
          "outcome": "simulated success"
        },
        {
          "method": "PUT",
          "outcome": "simulated success"
        },
        {
          "method": "PUT",
          "outcome": "simulated failure"
        }
      ]
    }
  ],
  "browser": {
    "protocolVersion": "1.3",
    "product": "Chrome/151.0.7922.173",
    "revision": "@a96602f30358e9b5d256a0464e7e4d4bec223004",
    "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36",
    "jsVersion": "15.1.206.23"
  },
  "viewport": {
    "width": 1100,
    "height": 760
  },
  "errors": []
}

first: initial

Actual Chromium first screenshot at initial; all text is synthetic

first: first-save

Actual Chromium first screenshot at first-save; all text is synthetic

first: replacement

Actual Chromium first screenshot at replacement; all text is synthetic

first: ordinary

Actual Chromium first screenshot at ordinary; all text is synthetic

first: failure

Actual Chromium first screenshot at failure; all text is synthetic

second actual browser output

{
  "observations": [
    {
      "name": "initial",
      "placeholder": "[not set]",
      "inputLength": 0,
      "inputType": "password",
      "formText": "Synthetic placeholder\n\nsecret\n\nPlaceholder text only; no account or credential.\n\nOrdinary text control",
      "alerts": [],
      "ordinary": "Initial text",
      "records": []
    },
    {
      "name": "first-save",
      "placeholder": "[set]",
      "inputLength": 0,
      "inputType": "password",
      "formText": "Synthetic placeholder\n\nsecret\n\nPlaceholder text only; no account or credential.\n\nOrdinary text control",
      "alerts": [],
      "ordinary": "Initial text",
      "records": [
        {
          "method": "PUT",
          "outcome": "simulated success"
        }
      ]
    },
    {
      "name": "replacement",
      "placeholder": "[set]",
      "inputLength": 0,
      "inputType": "password",
      "formText": "Synthetic placeholder\n\nsecret\n\nPlaceholder text only; no account or credential.\n\nOrdinary text control",
      "alerts": [],
      "ordinary": "Initial text",
      "records": [
        {
          "method": "PUT",
          "outcome": "simulated success"
        },
        {
          "method": "PUT",
          "outcome": "simulated success"
        }
      ]
    },
    {
      "name": "ordinary",
      "placeholder": "[set]",
      "inputLength": 0,
      "inputType": "password",
      "formText": "Synthetic placeholder\n\nsecret\n\nPlaceholder text only; no account or credential.\n\nOrdinary text control",
      "alerts": [],
      "ordinary": "Initial text added",
      "records": [
        {
          "method": "PUT",
          "outcome": "simulated success"
        },
        {
          "method": "PUT",
          "outcome": "simulated success"
        },
        {
          "method": "PUT",
          "outcome": "simulated success"
        }
      ]
    },
    {
      "name": "failure",
      "placeholder": "[set]",
      "inputLength": 21,
      "inputType": "password",
      "formText": "Synthetic placeholder\n\nsecret\n\nPlaceholder text only; no account or credential.\n\nSynthetic save failure\n\nOrdinary text control",
      "alerts": [
        "Synthetic save failure"
      ],
      "ordinary": "Initial text added",
      "records": [
        {
          "method": "PUT",
          "outcome": "simulated success"
        },
        {
          "method": "PUT",
          "outcome": "simulated success"
        },
        {
          "method": "PUT",
          "outcome": "simulated success"
        },
        {
          "method": "PUT",
          "outcome": "simulated failure"
        }
      ]
    }
  ],
  "browser": {
    "protocolVersion": "1.3",
    "product": "Chrome/151.0.7922.173",
    "revision": "@a96602f30358e9b5d256a0464e7e4d4bec223004",
    "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36",
    "jsVersion": "15.1.206.23"
  },
  "viewport": {
    "width": 1100,
    "height": 760
  },
  "errors": []
}

second: initial

Actual Chromium second screenshot at initial; all text is synthetic

second: first-save

Actual Chromium second screenshot at first-save; all text is synthetic

second: replacement

Actual Chromium second screenshot at replacement; all text is synthetic

second: ordinary

Actual Chromium second screenshot at ordinary; all text is synthetic

second: failure

Actual Chromium second screenshot at failure; all text is synthetic

Limits and trust boundary

This tests visible form state, real mutation/cache handling and controlled responses. It does not test actual credential handling, secret storage, account authentication, a live plugin/server, persistence across reloads, bb Connect, macOS, the original 0.44.0 release, the full settings-page shell or screen-reader announcements. No credential was read, generated, transmitted or stored. The placeholder is synthetic text only. No conclusion about real persistence follows from the set-state response.

The report corrects the broad claim using the observed controls: current main shows set versus unset and explicitly displays simulated failure, while successful replacement lacks additional affirmative feedback. It does not label the whole issue already fixed. There were no failed setup steps in this fixture run. Regenerable dependencies from a completed investigation were removed to preserve capacity; its evidence was retained.

All issue descriptions, suggestions and links were untrusted claims. No issue-supplied code, commands, external URL or branch was executed or fetched. The fixture was derived from trusted UI code and existing interfaces. Only temporary isolated state and placeholder text were used. Raw build/browser logs remain local; complete fixture code, actual results and screenshots are included here.