Roo Code 3.54.0 — Bug Report: a single failing operation aborts a whole batch and prevents the UI refresh
Found during a full audit of the webview ↔ extension message handlers in the bundled/minified dist/extension.js.
Affected extension: rooveterinaryinc.roo-cline-3.54.0.
Summary
Several flows batch persistence operations with Promise.all(...) and no error handling. When one item rejects:
- the remaining items in the batch are silently never executed (e.g. other settings never get written to
globalState),
- the code after the batch is skipped (e.g.
postStateToWebview(), initialize(), setModeConfig(...), provider-profile-change emit) — so the UI does not refresh and the extension state is left inconsistent.
Users observe this as "settings are not saved" / "UI did not update" after a single bad value/file/mode.
Fix strategy: replace the aborting Promise.all(...) with Promise.allSettled(...) + per-item error logging, or (where the batch result is used, e.g. image data URLs) per-item try/catch + filter.
The 6 bugs and their fixes
1. updateSettings (webview handler)
Trigger: one failing workspace-config update (e.g. allowedCommands / deniedCommands) aborts the whole for loop, so checkpointTimeout, soundEnabled, … are never written to globalState, and postStateToWebview() is never called → settings appear "not saved".
Before:
case"updateSettings":if(e.updatedSettings){for(let[S,Z]of Object.entries(e.updatedSettings)){let V=Z;
// ... resolve V ...
await t.contextProxy.setValue(S,V)}await t.postStateToWebview()
After: each iteration wrapped in try { … } catch { console.error(...) }; the loop always completes and postStateToWebview() always runs.
case"updateSettings":if(e.updatedSettings){for(let[S,Z]of Object.entries(e.updatedSettings)){try{let V=Z;
// ... resolve V ...
await t.contextProxy.setValue(S,V)}catch(RoErr){console.error("Roo Code: updateSettings failed for key '"+S+"':",RoErr)}}await t.postStateToWebview()
2. ContextProxy.setValues (batch persistence primitive)
Location: minified index ≈ 12618341.
Trigger: setValues is the shared primitive used by setConfiguration, deleteProviderProfile, createTask, importSettings, setProviderSettings, activateProviderProfile. One failing setValue rejects the whole Promise.all, so none of the subsequent values are written and every caller that does await setValues(...); postStateToWebview() skips the refresh.
Before:
async setValues(e){let r=Object.entries(e);await Promise.all(r.map(([a,o])=>this.setValue(a,o)))}
After:
async setValues(e){let r=Object.entries(e);(await Promise.allSettled(r.map(([a,o])=>this.setValue(a,o)))).forEach(o=>{if(o.status==="rejected")console.error("Roo Code: setValues failed for a key:",o.reason)})}
3. activateProviderProfile (API provider profile switch)
Location: minified index ≈ 14729861.
Trigger: profile switch runs Promise.all over 3 persistence calls (listApiConfigMeta, currentApiConfigName, setProviderSettings). One failure skips the subsequent setModeConfig(...), postStateToWebview() and the providerProfileChanged event → UI shows stale provider.
Before:
await Promise.all([this.contextProxy.setValue("listApiConfigMeta",await this.providerSettingsManager.listConfig()),this.contextProxy.setValue("currentApiConfigName",o),this.contextProxy.setProviderSettings(n)]);let{mode:h}=await this.getState();
After:
(await Promise.allSettled([this.contextProxy.setValue("listApiConfigMeta",await this.providerSettingsManager.listConfig()),this.contextProxy.setValue("currentApiConfigName",o),this.contextProxy.setProviderSettings(n)])).forEach(I=>{if(I.status==="rejected")console.error("Roo Code: activateProviderProfile failed to persist a value:",I.reason)});let{mode:h}=await this.getState();
4. resetAllState ("Reset State" / factory reset)
Location: minified index ≈ 12618638.
Trigger: reset deletes every globalState key and secret via one Promise.all. One failing globalState.update(...)/secrets.delete(...) aborts the whole reset and initialize() is never called → partially cleared, inconsistent state.
Before:
async resetAllState(){this.stateCache={},this.secretCache={},await Promise.all([...vye.map(e=>this.originalContext.globalState.update(e,void 0)),...Ck.map(e=>this.originalContext.secrets.delete(e)),...wk.map(e=>this.originalContext.secrets.delete(e))]),await this.initialize()}
After:
async resetAllState(){this.stateCache={},this.secretCache={},(await Promise.allSettled([...vye.map(e=>this.originalContext.globalState.update(e,void 0)),...Ck.map(e=>this.originalContext.secrets.delete(e)),...wk.map(e=>this.originalContext.secrets.delete(e))])).forEach(o=>{if(o.status==="rejected")console.error("Roo Code: resetAllState failed to clear a value:",o.reason)}),await this.initialize()}
5. importSettings → custom modes import
Location: minified index ≈ 14199255.
Trigger: importing settings first imports all custom modes via Promise.all(...updateCustomMode(...)). One malformed custom mode rejects the batch, so no custom mode is imported and — worse — the subsequent e.import(A) (tasks/settings import) and r.setValues(h) never run → the whole "Import Settings" flow fails because of a single bad custom-mode entry.
Before:
await Promise.all((h.customModes??[]).map(E=>a.updateCustomMode(E.slug,E))),await e.import(A),await r.setValues(h);
After:
(await Promise.allSettled((h.customModes??[]).map(E=>a.updateCustomMode(E.slug,E)))).forEach(o=>{if(o.status==="rejected")console.error("Roo Code: importSettings failed to import a custom mode:",o.reason)}),await e.import(A),await r.setValues(h);
6. selectImages (image picker → data URLs)
Location: minified index ≈ 14650972.
Trigger: converting the selected images to base64 data URLs runs Promise.all(e.map(...readFile...)). One unreadable file rejects the whole batch → selectedImages is never sent to the webview → the user loses all selected images because of one bad file.
Before:
return!e||e.length===0?[]:await Promise.all(e.map(async r=>{let a=r.fsPath,s=(await Ypi.default.readFile(a)).toString("base64");return`data:${tRs(a)};base64,${s}`}))}
After: per-item try/catch, failed files are skipped (null filtered out), good images are still returned.
return!e||e.length===0?[]:await Promise.all(e.map(async r=>{try{let a=r.fsPath,s=(await Ypi.default.readFile(a)).toString("base64");return`data:${tRs(a)};base64,${s}`}catch(RoErr){return console.error("Roo Code: selectImages failed to read an image:",RoErr),null}})).then(o=>o.filter(Boolean))}
Files attached
| File |
Purpose |
roo-cline-3.54.0-extension-fixed.js |
The fully patched, node --check-validated dist/extension.js bundle (drop-in replacement) |
patch-extension.mjs |
Patch script for bug 1 (updateSettings) |
patch-audit-fixes.mjs |
Patch script for bugs 2–6 (idempotent, with pre-flight checks) |
Verification performed on the installed bundle:
- all 6 patch markers present (exact substring match),
node --check on the patched bundle → syntax OK,
- file size: original
14 806 332 B → patched 14 807 051 B,
- each needle appears exactly once; re-running the scripts reports "already applied" and skips.
Roo Code 3.54.0 — Bug Report: a single failing operation aborts a whole batch and prevents the UI refresh
Summary
Several flows batch persistence operations with
Promise.all(...)and no error handling. When one item rejects:globalState),postStateToWebview(),initialize(),setModeConfig(...), provider-profile-change emit) — so the UI does not refresh and the extension state is left inconsistent.Users observe this as "settings are not saved" / "UI did not update" after a single bad value/file/mode.
Fix strategy: replace the aborting
Promise.all(...)withPromise.allSettled(...)+ per-item error logging, or (where the batch result is used, e.g. image data URLs) per-itemtry/catch+ filter.The 6 bugs and their fixes
1.
updateSettings(webview handler)Trigger: one failing workspace-config update (e.g.
allowedCommands/deniedCommands) aborts the wholeforloop, socheckpointTimeout,soundEnabled, … are never written toglobalState, andpostStateToWebview()is never called → settings appear "not saved".Before:
After: each iteration wrapped in
try { … } catch { console.error(...) }; the loop always completes andpostStateToWebview()always runs.2.
ContextProxy.setValues(batch persistence primitive)Location: minified index ≈ 12618341.
Trigger:
setValuesis the shared primitive used bysetConfiguration,deleteProviderProfile,createTask,importSettings,setProviderSettings,activateProviderProfile. One failingsetValuerejects the wholePromise.all, so none of the subsequent values are written and every caller that doesawait setValues(...); postStateToWebview()skips the refresh.Before:
After:
3.
activateProviderProfile(API provider profile switch)Location: minified index ≈ 14729861.
Trigger: profile switch runs
Promise.allover 3 persistence calls (listApiConfigMeta,currentApiConfigName,setProviderSettings). One failure skips the subsequentsetModeConfig(...),postStateToWebview()and theproviderProfileChangedevent → UI shows stale provider.Before:
After:
4.
resetAllState("Reset State" / factory reset)Location: minified index ≈ 12618638.
Trigger: reset deletes every globalState key and secret via one
Promise.all. One failingglobalState.update(...)/secrets.delete(...)aborts the whole reset andinitialize()is never called → partially cleared, inconsistent state.Before:
After:
5.
importSettings→ custom modes importLocation: minified index ≈ 14199255.
Trigger: importing settings first imports all custom modes via
Promise.all(...updateCustomMode(...)). One malformed custom mode rejects the batch, so no custom mode is imported and — worse — the subsequente.import(A)(tasks/settings import) andr.setValues(h)never run → the whole "Import Settings" flow fails because of a single bad custom-mode entry.Before:
After:
6.
selectImages(image picker → data URLs)Location: minified index ≈ 14650972.
Trigger: converting the selected images to base64 data URLs runs
Promise.all(e.map(...readFile...)). One unreadable file rejects the whole batch →selectedImagesis never sent to the webview → the user loses all selected images because of one bad file.Before:
After: per-item
try/catch, failed files are skipped (nullfiltered out), good images are still returned.Files attached
roo-cline-3.54.0-extension-fixed.jsnode --check-validateddist/extension.jsbundle (drop-in replacement)patch-extension.mjsupdateSettings)patch-audit-fixes.mjsVerification performed on the installed bundle:
node --checkon the patched bundle → syntax OK,14 806 332 B→ patched14 807 051 B,