agentleFS
forcedotcomforcedotcom/salesforcedx-vscode/.claude/skills/services-extension-consumption/SKILL.md

services-extension-consumption

Skill1k starsChanged 27 days agoBSD-3-ClauseView on GitHub

Consume the salesforcedx-vscode-services extension API. Use when an extension depends on salesforcedx-vscode-services and you are registering commands, calling its services (Workspace, Connection, Project, Settings, FS, Channel, Media, prompts), watching files/config/target-org, or wiring the AllServicesLayer/runtime in extensionProvider.ts.

Adopted by

Steady: 0 new projects took it in the last 4 weeks, none the 4 weeks before.thin data

Week of Sep 13: 0 new projectsWeek of Sep 20: 0 new projectsWeek of Sep 27: 0 new projectsWeek of Oct 4: 0 new projectsWeek of Oct 11: 0 new projectsWeek of Oct 18: 0 new projectsWeek of Oct 25: 0 new projectsWeek of Nov 1: 0 new projects
Sep 2026Oct 2026Nov 2026
New projects taking a version of this file each week, over the 8 weeks our latest run read: only 0 in all, so this is a hint, not a trend.

Who's talking about forcedotcom/salesforcedx-vscode

The biggest 5 of 52 mentions linking to the repository that our crawl found. Each line is their own headline.

See all 52

Talked about on GitHub and Stack Overflow (52 times).

On GitHub · 50

45 more on GitHub

The file

We split this skill into the rules it gives an agent

12 rules. About 0 are common, 3 are distinctive, and 9 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 9

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Use ExtensionProviderService from @salesforce/effect-ext-utils:

    line 13
  2. Very specific:

    Do: Use getRuntime().runPromise(effect) / runFork(effect) for ad-hoc execution.

    line 84
  3. Very specific:

    Don't: Use Effect.provide(AllServicesLayer) at call sites — use the runtime instead.

    line 85
  4. Distinctive:

    Prefer Effect scope ownership for resources created inside Effect services/layers:

    line 102
  5. Very specific:

    Don't expose runDispose/dispose solely for consumers to add to context.subscriptions.

    line 108
  6. Very specific:

    Keep context.subscriptions for resources created outside an Effect scope.

    line 109
  7. Very specific:

    Only then await connection resolution, target-org readiness, catalog hydration, or network work.

    line 143
  8. Distinctive:

    No args: interactive flow (palette).

    line 201
  9. Very specific:

    Call ClearDefaultOrgRef() to reset the in-process org ref (e.g., after deleting the default org):

    line 307
  10. Very specific:

    Start with api.services.prebuiltServicesLayer — don't add individual *.Default for services already there

    line 394
  11. Distinctive:

    Only add per-extension layers on top

    line 395
  12. Very specific:

    Use getRuntime().runPromise / runFork instead of Effect.provide(AllServicesLayer) for execution

    line 402

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Consuming salesforcedx-vscode-services

Extensions depending on salesforcedx-vscode-services. Examples: salesforcedx-vscode-metadata, salesforcedx-vscode-org-browser.

Getting the API

Use ExtensionProviderService from @salesforce/effect-ext-utils:

import { ExtensionProviderService, getServicesApi } from '@salesforce/effect-ext-utils';

const ExtensionProviderServiceLive = Layer.effect(
  ExtensionProviderService,
  Effect.sync(() => ({
    getServicesApi
  }))
);

// In an Effect.gen:
const api = yield * (yield * ExtensionProviderService).getServicesApi;

Prebuilt vs Per-Extension Services

api.services.prebuiltServicesLayer — shared service instances plus redacting-logger FiberRef. Not the OTEL tracer. Provide or merge this layer directly.

api.services.prebuiltServicesDependencies — deprecated context-only field. Omits FiberRefs; use prebuiltServicesLayer.

Shares singleton instances (caches, watchers) across extensions; avoids re-building stateful services.

Per-extension layers (must build yourself):

LayerWhy
ChannelServiceLayer(displayName)Own output channel; disposed when layer scope closes
ErrorHandlerService.DefaultDepends on own ChannelService
ExtensionContextServiceLayer(context)Own ExtensionContext
SdkLayerFor(context)Own tracer (extension name/version in resource attributes)
ExtensionProviderServiceLiveLocal singleton

ExtensionContext Setup

Preferred: import buildAllServicesLayer from @salesforce/effect-ext-utils. It reads displayName from package.json, falling back to the second arg. services/extensionProvider.ts only needs the mutable AllServicesLayer + setter:

// services/extensionProvider.ts
import { buildAllServicesLayer } from '@salesforce/effect-ext-utils';

export let AllServicesLayer: ReturnType<typeof buildAllServicesLayer>;
export const setAllServicesLayer = (layer: ReturnType<typeof buildAllServicesLayer>) => {
  AllServicesLayer = layer;
};

In activate — pass the context and a localized fallback channel name:

import { buildAllServicesLayer } from '@salesforce/effect-ext-utils';
import { nls } from './messages';
import { setAllServicesLayer } from './services/extensionProvider';

export const activate = async (context: vscode.ExtensionContext): Promise<void> => {
  setAllServicesLayer(buildAllServicesLayer(context, nls.localize('channel_name')));
  await getRuntime().runPromise(activateEffect(context));
};

Two patterns exist depending on whether the extension adds services beyond the shared base:

  • Shared base only (core, apex, apex-testing, lightning, lwc, org, visualforce): import buildAllServicesLayer directly from @salesforce/effect-ext-utils and pass it to setAllServicesLayer at activation. No local factory needed.
  • Extension-specific services added (apex-debugger, apex-log, apex-oas, apex-replay-debugger, metadata, org-browser, soql): define a local buildAllServicesLayer in services/extensionProvider.ts that calls buildSharedServicesLayer from @salesforce/effect-ext-utils and merges the extension's own Effect services via Layer.mergeAll. The extra services vary — apex-oas adds ApexMetadataService and LLMService; extensions with the notifications system add NotificationModeService.Default; org-browser adds OrgBrowserRetrieveService.

Runtime vs provide

  • buildAllServicesLayer merges suppressVersionMismatchWarning. Extension code keeps ManagedRuntime.make(AllServicesLayer); do not wrap it. The services activation runtime inlines Layer.setVersionMismatchErrorLogLevel(Option.none()) because it cannot import effect-ext-utils.
  • Do: Build ManagedRuntime.make(AllServicesLayer) and export getRuntime().
  • Do: Export runtime disposal, clear the memo, and call it during extension deactivation.
  • Do: Use getRuntime().runPromise(effect) / runFork(effect) for ad-hoc execution.
  • Don't: Use Effect.provide(AllServicesLayer) at call sites — use the runtime instead.
export const disposeRuntime = async (): Promise<void> => {
  if (_runtime) {
    await _runtime.dispose();
    _runtime = undefined;
  }
};

export const deactivate = async (): Promise<void> => {
  await getRuntime().runPromise(deactivation()).finally(disposeRuntime);
};

Resource Lifecycle

Prefer Effect scope ownership for resources created inside Effect services/layers:

  • Define resource-owning services with scoped.
  • Register VS Code Disposables with Effect.addFinalizer.
  • Attach long-lived fibers to the owning scope with Effect.forkIn.
  • Dispose the owning ManagedRuntime on deactivation so layer finalizers run.
  • Don't expose runDispose/dispose solely for consumers to add to context.subscriptions.
  • Keep context.subscriptions for resources created outside an Effect scope.

Allocation and cleanup stay together. See ../effect-best-practices/SKILL.md#effect-owned-resources.

Registering Commands

Use registerCommandWithRuntime:

import { myCommandEffect } from './commands/myCommand';

const api = yield * (yield * ExtensionProviderService).getServicesApi;

const registerCommand = api.services.registerCommandWithRuntime(getRuntime());
yield * registerCommand('sf.my.command', myCommandEffect);

Commands auto:

  • Register with ExtensionContext subscriptions
  • Wrap with error handling
  • Trace with observability spans
  • Handle Cancellation

Activation ordering

activate() awaits getRuntime().runPromise(activateEffect(context)); it does not detach the main activation Effect. Only work explicitly started with Effect.fork* continues after activation completes.

Register all manifest-contributed UI before awaiting work that can be slow or unresolved:

  1. Register tree/webview providers and put any returned Disposable in context.subscriptions when it is not scope-owned.
  2. Restore the extension's persisted UI state and set its context keys.
  3. Register every contributed command.
  4. Set an extension-owned readiness context key only after steps 1-3 succeed, and use it to gate title/menu commands that would otherwise be visible.
  5. Only then await connection resolution, target-org readiness, catalog hydration, or network work. Use Effect.forkIn for long-lived watchers that do not need to block activation.

when clauses can expose a contributed command before its handler has registered. A context key owned by another extension, including sf:has_target_org, is a visibility hint, not proof that this extension has initialized. Do not make a contributed handler's registration depend on it. Keep target-org and authorization checks in the command implementation or shared service layer.

export const activateEffect = Effect.fn(`activation:${EXTENSION_NAME}`)(function* (context: vscode.ExtensionContext) {
  const api = yield* (yield* ExtensionProviderService).getServicesApi;
  const provider = new MyTreeProvider();
  context.subscriptions.push(vscode.window.registerTreeDataProvider(VIEW_ID, provider));

  yield* setInitialContext();
  const registerCommand = api.services.registerCommandWithRuntime(getRuntime());
  yield* registerCommand('sf.my.command', () => myCommand(provider));
  yield* Effect.promise(() => vscode.commands.executeCommand('setContext', 'sf:myExtension.ready', true));

  // Command registration must not wait for org-backed initialization.
  yield* api.services.ConnectionService.getConnection();
});

Success handling

Effect.fn middleware runs left to right after the generator. Effect.tap and *SuccessNotification before catch* (catch, catchAll, catchAllCause, catchCause, catchCauseIf, catchIf, catchSome, catchSomeCause, catchTag, catchTags) — a success combinator after catch treats recovery as success. Later catch valid. Later non-success guard (preventOrgChanges) valid. Lint: local/effect-fn-catch-middleware-last on spanned Effect.fn (fn('span'), fn('name', options)). Skips fnUntraced, unspanned fn(function*), generator-body catch, .pipe catch.

export const deployActiveEditorCommand = Effect.fn('deploySourcePath.deployActiveEditor')(
  function* () {
    // ...core logic...
  },
  // runs only on success — placed before catchTag
  withConfigurableSuccessNotification(nls.localize('command_succeeded_text', label)),
  // catches errors — placed after success middleware
  Effect.catchTag('NoActiveEditorError', () =>
    Effect.promise(() => vscode.window.showErrorMessage(nls.localize('deploy_select_file_or_directory'))).pipe(
      Effect.as(undefined)
    )
  )
);

withConfigurableSuccessNotification wraps the effect with Effect.tap, so it only fires when the effect succeeds:

export const withConfigurableSuccessNotification =
  (message: string) =>
  <A, E, R>(effect: Effect.Effect<A, E, R>) =>
    Effect.tap(effect, () =>
      Effect.sync(() => {
        const show = vscode.workspace.getConfiguration(SECTION).get<boolean>(KEY, false);
        if (show) void vscode.window.showInformationMessage(message);
      })
    );

Invoking sf.org.login.web

Cross-extension / executeCommand: vscode.commands.executeCommand('sf.org.login.web', instanceUrl?, reauthAliasOrUsername?).

  • No args: interactive flow (palette).
  • With instanceUrl: skips org-type quick pick.
  • Second arg applies only when instanceUrl was provided: trimmed non-empty string becomes the auth alias (access-token re-auth); else alias defaults to reauth-vscodeOrg.

Basic Services

Accessor pattern: call methods directly, don't assign to variable first.

  • ChannelService - Output channel
  • ComponentSetService - Build component sets (source, manifest, URIs)
  • MediaService - Icons (ICONS) and NLS descriptions
  • WorkspaceService - Workspace info
  • ConnectionService - Org connections
  • ProjectService - Project resolution, packageDirectories
  • SettingsService - Settings read/write
  • FsService - File ops (web-compatible), uri/path conversion, HashableUri (comparisonKey of URI fields, not .toString())
  • OrgMetadataCatalog - inventory/presence; getChildren / getEntries / resolveComponents. Types: catalog + entries, OrgMetadataCatalogError (type-only), OrgMetadataComponentReference, OrgMetadataCatalogChange. ADR 0021
  • TransmogrifierService - REST/workspace SObject describe → canonical SObject. Types: TransmogrifierService, TransmogrifierError (type-only)
  • EditorService - Active editor changes and current URI
  • Prompts - QuickPick, InputBox, and UserCancellationError handling
  • TerminalService - Run argv commands (desktop-only)
  • NotificationModeService - Configurable success notifications

Watchers

File Watching

FileChangePubSub — workspace FS (**/*), including project .sf/config.json. Filter event.uri / uri.path / Utils.*, not uri.fsPath.

Global ~/.sf/config.json and ~/.sfdx/alias.json: HostFileWatcher (internal, @salesforce/core/fs). Not on the public API; services already watch them. See FileChangePubSub vs HostFileWatcher.

import * as Stream from 'effect/Stream';

const pubsub = yield* api.services.FileChangePubSub;

yield* Stream.fromPubSub(pubsub).pipe(
  Stream.filter(event => /* event.uri / uri.path / Utils.*; not uri.fsPath */),
  Stream.runForEach(event =>
    Effect.sync(() => {
      // { type: 'create'|'change'|'delete', uri }
    })
  )
);

Config Watching

Watch VS Code config changes:

import * as PubSub from 'effect/PubSub';
import * as Stream from 'effect/Stream';
import * as Duration from 'effect/Duration';

const pubsub = yield * PubSub.sliding<vscode.ConfigurationChangeEvent>(100);
const disposable = vscode.workspace.onDidChangeConfiguration(event => {
  Effect.runSync(PubSub.publish(pubsub, event));
});

yield *
  Effect.addFinalizer(() =>
    Effect.sync(() => {
      disposable?.dispose();
    })
  );

yield *
  Stream.fromPubSub(pubsub).pipe(
    Stream.filter(event => event.affectsConfiguration('section.setting')),
    Stream.debounce(Duration.millis(100)),
    Stream.runForEach(() => {
      // Handle config change
    })
  );

Target Org Changes

Watch org changes via TargetOrgRef (SubscriptionRef):

const ref = yield * api.services.TargetOrgRef();
yield *
  ref.changes.pipe(
    Stream.map(org => org.orgId),
    Stream.changes,
    Stream.tap(orgId => {
      // Handle org change
    }),
    Stream.runForEach(() => {
      // Refresh UI, invalidate caches, etc.
    })
  );

TargetOrgRef is a SubscriptionRef: ref.changes already emits the current value first, so never prepend an explicit get. See the SubscriptionRef section of ../effect-best-practices/SKILL.md for the mechanic (incl. Stream.drop(1) to skip the initial snapshot).

Ref behavior (concise):

  • Default-org update: username from User SOQL when present; else conn.getUsername() / AuthInfo login username. Ref treated stale if stored username !== connection auth username; triggers re-query of username/userId/aliases/devHubOrgId. Write: re-get ref after queries (no async gap before set), merge onto it, set only if changed (any SubscriptionRef write publishes, even equivalent; TargetOrgRef subscribers like the org catalog treat each emission as an org change). Span annotated stale: true if ref's orgId moved (write dropped), else changed (+ updated if changed).
  • Username-less snapshot = no target org.
  • TargetOrgRef (DefaultOrgInfoSchema) value is always an object (never undefined); orgId/devHubOrgId optional branded OrgId (Schema.optional(OrgId), like cliId).

Clearing the Default Org

Call ClearDefaultOrgRef() to reset the in-process org ref (e.g., after deleting the default org):

yield* api.services.ClearDefaultOrgRef();

Clears the reactive ref without rewriting config. Use when the CLI already mutated config but the in-process ref must reset to notify observers (e.g., the source tracking status bar icons). See orgDeleteDefaultCommand for an example.

Complete Example Pattern

// services/extensionProvider.ts
import { buildAllServicesLayer } from '@salesforce/effect-ext-utils';

export let AllServicesLayer: ReturnType<typeof buildAllServicesLayer>;
export const setAllServicesLayer = (layer: ReturnType<typeof buildAllServicesLayer>) => {
  AllServicesLayer = layer;
};

// services/runtime.ts
import * as ManagedRuntime from 'effect/ManagedRuntime';
import { AllServicesLayer } from './extensionProvider';

const createRuntime = () => ManagedRuntime.make(AllServicesLayer);
let _runtime: ReturnType<typeof createRuntime> | undefined;
export const getRuntime = () => (_runtime ??= createRuntime());

// index.ts
import { buildAllServicesLayer } from '@salesforce/effect-ext-utils';
import { nls } from './messages';
import { myCommandEffect } from './commands/myCommand';
import { setAllServicesLayer } from './services/extensionProvider';
import { getRuntime } from './services/runtime';

export const activate = async (context: vscode.ExtensionContext) => {
  setAllServicesLayer(buildAllServicesLayer(context, nls.localize('channel_name')));
  await getRuntime().runPromise(activateEffect(context));
};

export const activateEffect = Effect.fn(`activation:${EXTENSION_NAME}`)(function* (_context: vscode.ExtensionContext) {
  const providerService = yield* ExtensionProviderService;
  const api = yield* providerService.getServicesApi;
  yield* api.services.ChannelService.appendToChannel('Extension activating');

  const registerCommand = api.services.registerCommandWithRuntime(getRuntime());
  yield* registerCommand('sf.my.command', myCommandEffect);

  yield* api.services.ChannelService.appendToChannel('Extension activation complete.');
});

Testing

Mock services via Layer.succeed and combine with Layer.mergeAll. For static accessors (e.g., api.services.WorkspaceService.getWorkspaceInfo()), wire both the provider and service:

import { ExtensionProviderService } from '@salesforce/effect-ext-utils';
import { WorkspaceService } from 'salesforcedx-vscode-services/src/vscode/workspaceService';
import * as Effect from 'effect/Effect';
import * as Layer from 'effect/Layer';

// Mock both ExtensionProviderService and WorkspaceService
const mockWorkspaceLayer = Layer.mergeAll(
  Layer.succeed(ExtensionProviderService, {
    getServicesApi: Effect.succeed({
      services: { WorkspaceService } // Accessor sees real class
    } as unknown as SalesforceVSCodeServicesApi)
  }),
  Layer.succeed(
    WorkspaceService,
    new WorkspaceService({
      getWorkspaceInfo: () => Effect.succeed({ path: '/mock', fsPath: '/mock', isEmpty: false, isVirtualFs: false, cwd: '/mock' }),
      getWorkspaceInfoOrThrow: () => Effect.succeed(/* ... */)
    } as unknown as WorkspaceService)
  )
);

// Use in test
const result = await Effect.runPromise(
  myEffect().pipe(Effect.provide(mockWorkspaceLayer))
);

For direct service mocking (no accessor), use Layer.succeed(Service, mockImpl) alone.

Common Patterns

  • Start with api.services.prebuiltServicesLayer — don't add individual *.Default for services already there
  • Only add per-extension layers on top
  • import { ICONS } outside Effect; MediaService inside Effect
  • ChannelServiceLayer before ErrorHandlerService
  • Pass context to SdkLayerFor (extracts name/version from ExtensionContext)
  • Effect.forkIn(..., yield* getExtensionScope()) for watcher cleanup on deactivation
  • Scoped services own their VS Code disposables via finalizers; runtime disposal runs them
  • registerCommandWithRuntime for all commands (tracing + error handling)
  • Use getRuntime().runPromise / runFork instead of Effect.provide(AllServicesLayer) for execution

Don't: rebuild services already in prebuiltServicesLayer

// WRONG — creates new singleton instances, duplicating caches/watchers/state
return Layer.mergeAll(
  ExtensionProviderServiceLive,
  api.services.ExtensionContextServiceLayer(context),
  api.services.FsService.Default,           // ← already in prebuilt
  api.services.AliasService.Default,        // ← already in prebuilt
  api.services.SdkLayerFor(context),
  channelLayer,
  errorHandlerWithChannel
);

// CORRECT — share the already-built singletons
return Layer.mergeAll(
  api.services.prebuiltServicesLayer,
  ExtensionProviderServiceLive,
  api.services.ExtensionContextServiceLayer(context),
  api.services.SdkLayerFor(context),
  channelLayer,
  errorHandlerWithChannel
);

Review

Invoke the effect-advocate subagent on plans and diffs — its top-priority finding category is "you re-implemented something that already exists in salesforcedx-vscode-services."

prebuiltServicesLayer contains ~27 services built once during services extension activation. Calling .Default on any of them creates a second instance with its own caches, watchers, and state — silently breaking cross-extension sharing.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.