.clinerules/general.md
This file is the secret sauce for working effectively in this codebase. It captures tribal knowledge—the nuanced, non-obvious patterns that make the difference between a quick fix and hours of back-and-forth & human intervention.
When to add to this file:
Proactively suggest additions when any of the above happen—don't wait to be asked.
What NOT to add: Stuff you can figure out from reading a few files, obvious patterns, or standard practices. This file should be high-signal, not comprehensive.
apps/vscode) uses bun for package management and task running. Emit bun run X / bun install / bunx <bin> / bun file.ts, never npm/npx. Node remains the runtime (VS Code's extension host and the standalone cline-core are Node), so Node-runtime tokens are legitimate and must not be "fixed" to bun — see @.clinerules/bun-and-node.md for the keep-list vs rewrite-list.providerId === "...". If a provider exception seems necessary, stop and explain why instead of adding ad-hoc string matching.package.json for available scripts before trying to verify builds (e.g., bun run compile, not bun run build).readFileStrippingUtf8Bom, readFileSyncStrippingUtf8Bom, or stripUtf8Bom from @cline/shared/node. DON'T strip byte order marks of user files handled by tools/passed to models.Several directories contain build output or generated code that produces
noisy or unusable results with search_files / grep:
| Directory | What it is | Why it's a problem |
|---|---|---|
out/ | esbuild bundle output | Mirrors src/ structure as minified JS — every search gets duplicate hits on single-line files |
dist/ | Packaged extension | Entire extension bundled into one minified extension.js (~1 long line) |
dist-standalone/ | Standalone build output | Same minification issue |
src/generated/ | Generated protobuf code | Auto-generated from proto/; not the source of truth |
src/shared/proto/ | Generated proto type defs | Auto-generated from proto/; not the source of truth |
node_modules/ | Dependencies | Huge, not project source |
search_files — Point at src/ (not the project root) and use file_pattern:
search_files(path="src/core", regex="myFunction", file_pattern="*.ts")
The file_pattern parameter is the most effective filter — e.g. "*.ts",
"*.tsx", "*.proto".
grep directly — Exclude build dirs and restrict to source extensions:
grep -rn "myFunction" src/ --include="*.ts" --exclude-dir={out,dist,node_modules,generated}
Sometimes you need to verify what got bundled (e.g., checking if a change
made it into the build). Minified files are typically one long line, so
normal grep shows the entire file as context. Use these approaches:
grep -oP to extract just the match with limited surrounding context:
grep -oP '.{0,40}myFunction.{0,40}' dist/extension.js
read_file on files in out/src/ — these have source maps and are
more readable than dist/extension.js (which is the fully bundled output).out/src/*.js.map and dist/extension.js.map can be
used to trace minified output back to original source locations.The extension and webview communicate via gRPC-like protocol over VS Code message passing.
Proto files live in proto/ (e.g., proto/cline/task.proto, proto/cline/ui.proto)
.proto fileproto/cline/common.proto (StringRequest, Empty, Int64Request).proto filePascalCaseService, RPCs camelCase, Messages PascalCasestream keyword (see subscribeToAuthCallback in account.proto)Run bun run protos after any proto changes—generates types in:
src/shared/proto/ - Shared type definitionssrc/generated/grpc-js/ - Service implementationssrc/generated/nice-grpc/ - Promise-based clientssrc/generated/hosts/ - Generated handlersAdding new enum values (like a new ClineSay type) requires updating conversion mappings in src/shared/proto-conversions/cline-message.ts
Adding new RPC methods requires:
src/core/controller/<domain>/UiServiceClient.scrollToSettings(StringRequest.create({ value: "browser" }))Example—the explain-changes feature touched:
proto/cline/task.proto - Added ExplainChangesRequest message and explainChanges RPCproto/cline/ui.proto - Added GENERATE_EXPLANATION = 29 to ClineSay enumsrc/shared/ExtensionMessage.ts - Added ClineSayGenerateExplanation typesrc/shared/proto-conversions/cline-message.ts - Added mapping for new say typesrc/core/controller/task/explainChanges.ts - Handler implementationwebview-ui/src/components/chat/ChatRow.tsx - UI renderingAdding a new key to global state requires updates in multiple places. Missing any step causes silent failures.
Required steps:
src/shared/storage/state-keys.ts - Add to GlobalState or Settings interfacesrc/shared/storage/state-keys.ts if the key needs oneStateManager (setGlobalState() / getGlobalStateKey()) after initializationPersistent state is file-backed through StateManager; do not add new runtime reads or writes against VS Code ExtensionContext storage. That storage is only a legacy migration source.
Settings plumbing gotcha: if a key is user-toggleable from settings, wire both controller update paths:
src/core/controller/state/updateSettings.ts for webview updateSetting(...)src/core/controller/state/updateSettingsCli.ts for CLI/ACP settings updates
Missing one path causes a toggle to appear to change in one surface while the backend state stays unchanged.Webview toggle gotcha: settings changes must also round-trip back in state payloads.
UpdateSettingsRequest in proto/cline/state.proto (for webview update requests), then run bun run protosController.getStateToPostToWebview() (src/core/controller/index.ts)ExtensionState and webview defaults include the key (src/shared/ExtensionMessage.ts, webview-ui/src/context/ExtensionStateContext.tsx)
If this round-trip wiring is missing, the backend value can update but the toggle in webview appears stuck or reverts.StateManager uses an in-memory cache populated during StateManager.initialize() from file-backed storage. For most state, use controller.stateManager.setGlobalState()/getGlobalStateKey().
Exception: host migration code may read legacy VS Code storage before file-backed storage is initialized.
Example pattern:
// Writing (normal pattern)
controller.stateManager.setGlobalState("myKey", value)
// Reading after initialization
const value = controller.stateManager.getGlobalStateKey("myKey")
Use context.globalState only in VS Code migration code that copies legacy ExtensionContext values into the shared file-backed stores.
When a ChatRow displays a loading/in-progress state (spinner), you must handle what happens when the task is cancelled. This is non-obvious because cancellation doesn't update the message content—you have to infer it from context.
The pattern:
status field (e.g., "generating", "complete", "error") stored in message.text as JSON"generating" forever—no one updates it!isLast — if this message is no longer the last message, something else happened after it (interrupted)lastModifiedMessage?.ask === "resume_task" || "resume_completed_task" — task was just cancelled and is waiting to resumeExample from generate_explanation:
const wasCancelled =
explanationInfo.status === "generating" &&
(!isLast ||
lastModifiedMessage?.ask === "resume_task" ||
lastModifiedMessage?.ask === "resume_completed_task")
const isGenerating = explanationInfo.status === "generating" && !wasCancelled
Why both checks?
!isLast catches: cancelled → resumed → did other stuff → this old message is stalelastModifiedMessage?.ask === "resume_task" catches: just cancelled, hasn't resumed yet, this message is still technically "last"See also: BrowserSessionRow.tsx uses similar pattern with isLastApiReqInterrupted and isLastMessageResume.
Backend side: When streaming is cancelled, clean up properly (close tabs, clear comments, etc.) by checking taskState.abort after the streaming function returns.
The debug harness (apps/vscode/src/dev/debug-harness/server.ts) launches a child
VSCode via Playwright's _electron.launch({ env: { ...process.env, ... } }). If you
run the harness from a process that was itself spawned by VSCode (e.g. the Cline
extension host, an integrated terminal, or an agent running inside VSCode), the
parent's VSCode/Electron env vars leak into the child and break the launch.
The fatal one is ELECTRON_RUN_AS_NODE=1: it makes the child VSCode binary run
as plain Node, so it rejects every VSCode CLI flag. Symptom:
.../Visual Studio Code.app/Contents/MacOS/Code: bad option: --extensionDevelopmentPath=...
Error: Process failed to launch! (Playwright _electron.launch)
This is NOT the macOS Playwright flakiness mentioned in the harness README — it's env inheritance. Fix: strip the inherited vars before starting the harness:
env -u ELECTRON_RUN_AS_NODE -u ELECTRON_NO_ATTACH_CONSOLE \
-u VSCODE_CLI -u VSCODE_CODE_CACHE_PATH -u VSCODE_CRASH_REPORTER_PROCESS_TYPE \
-u VSCODE_CWD -u VSCODE_ESM_ENTRYPOINT -u VSCODE_HANDLES_UNCAUGHT_ERRORS \
-u VSCODE_IPC_HOOK -u VSCODE_NLS_CONFIG -u VSCODE_PID -u VSCODE_L10N_BUNDLE_LOCATION \
bun src/dev/debug-harness/server.ts --auto-launch --skip-build
Check your own env with env | grep -iE 'electron|vscode_' first; ELECTRON_RUN_AS_NODE=1
present means you must scrub before launching.
Other harness notes confirmed in practice:
VSCODE_ESM_ENTRYPOINT), so ext.evaluate has no
require and module-internal functions aren't reachable as globals. To inspect
internal builders (e.g. buildBedrockProviderConfig), set a breakpoint with
ext.set_breakpoint and read locals via ext.evaluate with the paused callFrameId
— don't try to require() the bundle.web.evaluate wraps the expression as a single returned expression; multi-statement
snippets must be an IIFE (() => { ...; return x; })(), otherwise you get
SyntaxError: Unexpected token ';'.vscode-text-field web components with debounced React
onChange. Setting .value + dispatching events via web.evaluate is unreliable for
some fields; focus the inner shadow input then use real keystrokes (ui.type +
ui.press Tab, or click the dropdown option) to make the value persist.