Repository navigation
Description
Summary
There is a noticeable delay between clicking Save in the Settings panel and seeing changes reflected in the UI. For settings that only affect the webview (font size, sound, UI toggles), the delay is pure latency — the webview already holds the new value but waits for a full extension host round-trip before rendering it.
Context
The VS Code Webview API provides two distinct persistence mechanisms:
getState()/setState()— lightweight, in-process webview-local persistence. The official docs call this the preferred way to persist state, noting it has "much lower performance overhead" than retaining context. VS Code's own samples demonstrate callingsetState()at 10 Hz without concern.globalState— extension host storage that writes to disk and survives VS Code restarts, but requires an IPC round-trip to access.
Zoo Code routes all settings through globalState on every save. The full round-trip is:
- Webview posts
updateSettings webviewMessageHandlerawaits acontextProxy.setValue()write toglobalStatefor each changed key- Several settings also trigger extension-host side-effects:
languagecallschangeLanguage(),mcpEnabledcallsmcpHub.handleMcpEnabledChange(),allowedCommands/deniedCommandswrite tovscode.workspace.getConfiguration(), terminal settings update theTerminalclass, TTS settings start/stop the TTS service postStateToWebview()assembles a full snapshot — including merged command lists, cloud/org state, experiment merges, sequence numbers, and one-time signals likesettingsImportedAt— and posts it back- Webview receives state, React re-renders, effects fire, UI updates
The round-trip cannot simply be removed. The post-save snapshot is not a simple echo: it includes experiment merges (incoming experiments are merged with stored, not replaced), workspace+global command deduplication, cloud auth state computed at push time, schema validation and legacy migrations in ContextProxy, and sequence numbering that prevents stale state from overwriting concurrent task messages.
However, the VS Code pattern for handling this is well-established: use setState() for immediate UI state, globalState for what must survive restarts. Settings that only affect the webview are good candidates for the lightweight path — apply them immediately via local state, let the globalState write and authoritative snapshot complete in the background, and use the snapshot arrival to confirm or correct.
Settings that are purely UI-local (safe to optimistic-apply):
chatFontSize— sets a CSS variablesoundEnabled,soundVolume— webview audio onlyenterBehavior— chat input behaviourhistoryPreviewCollapsed,reasoningBlockCollapsed— collapse state- Most
alwaysAllow*permission toggles
Settings that must wait for the round-trip (depend on extension-host confirmation):
language—changeLanguage()side-effectmcpEnabled— async MCP server reconciliation that can failallowedCommands/deniedCommands— written to VS Code workspace config, merged on read-back- All terminal settings —
Terminalclass side-effects ttsEnabled/ttsSpeed— TTS service init/teardownexperiments— merged with stored state on write
Developer Notes
Two implementation options:
Option A — Optimistic layer in ExtensionStateContext (preferred, general fix): Introduce a local optimistic state map that is applied immediately on setCachedStateField for UI-local keys, and reconciled when the authoritative snapshot arrives. The existing useEffect hooks (e.g. the one setting --zoo-chat-font-size) would read from merged optimistic+authoritative state. This eliminates the delay uniformly for all UI-local settings.
Option B — Apply effects at point of interaction (lower risk, per-setting): For each UI-local setting, apply the side-effect directly in the component change handler, independent of context state. The useEffect in ExtensionStateContext remains authoritative for load and reset. Scales poorly but easy to reason about.
Relevant files:
src/core/webview/webviewMessageHandler.ts—updateSettingshandler (~line 663)src/core/webview/ClineProvider.ts—getStateToPostToWebview()assemblysrc/core/config/ContextProxy.ts—globalStatecache, Zod validation, migration layerwebview-ui/src/context/ExtensionStateContext.tsx—useEffecthooks applying UI settingswebview-ui/src/components/settings/SettingsView.tsx—setCachedStateFieldand save boundary
Sources:
Activity