feat(segments): activation gates to skip provably-inactive segments - #7838
Merged
Conversation
Add a cheap, declarative pre-check to the segment execution path: a writer can implement the optional segments.Activator interface to declare the preconditions (cwd file globs) under which it can possibly be enabled. When none of the globs match the cached directory listing, Segment.Execute skips the writer's Enabled() probe entirely and leaves the segment disabled. The gate is conservative by design: - Writers without Activator, or whose Activation resolves to Always (or the zero value), execute exactly as before. - Forced segments, segments with a fallback template, and segments pinned via a hand-written data file bypass the gate and run the full path. - The shared Language helper returns Always whenever anything but a cwd file could enable the segment: project files (parent-directory search), folders (stat-based check), or a display mode other than "files". The whole language family opts in by extracting its spec construction out of Enabled() into an idempotent loadSpec(), letting Activation() resolve the same extensions/folders/display-mode options the probe would. The helper is deliberately unexported so embedders that still build their spec inside Enabled() never gate against an empty spec. The gate evaluation lives beside the writer registry so the js/wasm build, which has no writers, keeps linking without the segments package. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4RA9ZrP7vuqVSR7nddNiQ
📦 Release binary size reportCompares this PR's release-equivalent build against the latest published release, per OS (amd64).
|
Activation() is now part of the SegmentWriter interface itself, with segments.Base providing the ungated (Always) default, replacing the optional Activator interface and its type-assertion helper. The Activation type moves to the runtime package - where its evaluation (Active) lives - so the config package can reference it in the interface without pulling the segments package into the js/wasm build. The spec grows three condition kinds beyond file globs, OR'd across all kinds: Folders (cwd directories, stat-based), ProjectFiles (upward parent-directory search, both symlink variants), and EnvVars (non-empty environment variables). HasParentFilePath results are now memoized per prompt invocation, so the gate and a segment's own Enabled() doing the same search only walk the tree once. Gate contract changes (deliberate): - A fallback template no longer forces the full Enabled() evaluation when the gate fails; the segment counts as evaluated and the fallback renders against the zero-state writer. - Force and pinned data (pendingData) still bypass the gate entirely. Migration: - Language family: the file/extension/folder presence checks move out of Enabled() into activation(); files display mode now trusts the gate. Environment/context modes gate on declared context triggers (contextEnvVars/contextFiles): python and mojo on their venv variables, cds on package.json. An opaque context callback still resolves to Always (ui5tooling's depth-limited parent glob search). A python venv only discoverable through pyvenv.cfg, with no env var and no python files in the cwd, no longer activates the segment - an accepted trade-off. - VCS family gates on its repository marker (.git, .hg, .svn, .sl, .jj, .plastic, .dvc; gitversion on .git), keeping the search in Enabled() since its result feeds the segment. fossil stays ungated: its detection runs the CLI rather than a marker search. - terraform's context check is lifted into the gate and removed from Enabled(); docker gates its files mode; unity, umbraco, project and sitecore gate on their markers. nixshell stays ungated (PATH-based nix shell detection). Everything else inherits Always from Base. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4RA9ZrP7vuqVSR7nddNiQ
Run the full pre-commit gate from the golang skill (modernize, fieldalignment, go mod tidy, gofmt, golangci-lint) over the activation changes and resolve what it surfaced: - modernize: fold the FileGlobs loop into slices.ContainsFunc - gocritic hugeParam: Activation.Active takes a pointer receiver; call sites bind the returned spec to a local first - dupl: react/aurelia are deliberately parallel, suppressed with reason fieldalignment and go mod tidy came back clean; golangci-lint now reports zero issues module-wide. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4RA9ZrP7vuqVSR7nddNiQ
The activation gate is bypassed for forced segments and for segments pinned via a hand-written data file, but the previous refactor made several Enabled() branches TRUST that the gate had run: a forced language segment (or a pinned one) in a directory without matching files became enabled - and ran its version toolchain - where the old code kept it hidden. The symlink-following project-file gate variant could likewise pass where the segment's own non-following search misses. Restore the presence re-checks in the weakened branches so Enabled stays standalone-correct and the gate remains a pure skip-optimization: - language files mode re-runs hasLanguageFiles/hasLanguageFolders - docker files mode re-checks its file globs - terraform re-checks inContext, rebuilt on a helper shared with Activation so the two cannot diverge - sitecore re-checks the sitecore.json marker The re-checks are near-free: the directory listing and parent-path searches are memoized per invocation, so after a gate pass they are map hits. Also align hasLanguageFolders' stat with the gate's (joined with Pwd), removing the remaining gate/Enabled divergence when the PWD flag differs from the process cwd. Tests: reinstate the standalone Enabled() assertions for gradle, docker, terraform and the language base (alongside the gate-composition checks), and add a regression test proving force does not enable a segment its own detection rejects. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4RA9ZrP7vuqVSR7nddNiQ
HasFilesInDir answered every glob with a linear filepath.Match scan over the cached directory listing. Activation gates turned that into the hot path: ~30 segments times several globs each times every entry in the directory, on every prompt, with no win from gating in directories with many files. Invert the work instead of repeating it: build a per-directory index once (a set of lower-cased file names, plus every dotted suffix of each name - ".c" and ".b.c" for "a.b.c" - so multi-dot patterns like "*.gradle.kts" resolve in one lookup) and classify each incoming pattern into a literal name-set lookup, a dotted-suffix-set lookup, or the unchanged linear scan for anything shaped differently. The index is built lazily per Terminal instance, the same lifecycle and duplicate-build tolerance as the existing lsDirMap listing cache. Classification is deliberately conservative: any pattern shape that is not provably a literal or a "*.suffix" glob (bare "*", non-dot-anchored suffixes, character classes, escapes, extra wildcards) falls through to the original filepath.Match scan, which stays as the correctness fallback and as the reference implementation a differential test checks the fast paths against. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4RA9ZrP7vuqVSR7nddNiQ
readDir deliberately re-reads a directory whose cached listing is empty, but the index built from that empty listing was cached unconditionally: a file appearing mid-render would surface in the fresh listing (and in linearMatch fallbacks) while the stale empty index kept answering false for fast-path patterns. Mirror readDir's quirk so both caches share one lifecycle. Also cover the empty-stem dotfile shape (*.env vs .env) in the curated differential patterns - filepath.Match's * matches the empty string, the least intuitive equivalence the suffix set must reproduce. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4RA9ZrP7vuqVSR7nddNiQ
Fetch options used to be the only way to switch on the git segment's expensive probes, even when the config's templates clearly display the data those probes produce. The config now analyzes every template at engine construction - segment templates, foreground/background/extra templates, tooltips, extra prompts and the console title, including cross-segment .Segments.<name> references - and stamps each segment with the set of top-level fields its rendering can touch. Writers implementing the new config.FieldSetConsumer interface receive that set right after Init. Git is the first consumer: each optional probe (status, push status, upstream icon, user, bare info) runs when the analyzed templates reference one of the fields it populates. An explicitly configured fetch_* option always wins in both directions, and a template shape the analysis cannot follow (whole-dot use, index over the context, variable laundering, template includes) falls back to the option defaults, so an unanalyzable config never fetches more than it used to. The referenced-field set is folded into the segment cache key for field-set consumers, so a gob snapshot taken under one set is never restored under another. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4RA9ZrP7vuqVSR7nddNiQ
Language segments run <tool> --version subprocesses on every prompt even though the output is constant for a given binary. Cache it in the device cache keyed by the resolved executable's path, mtime and size plus the exact args (FNV hash), with a one-week TTL that exists only for garbage-collection hygiene - correctness comes entirely from the key, so a toolchain upgrade misses the cache automatically and nothing time-based has to be guessed. Each cmd opts in individually via a new versionCacheable flag, defaulting to off. Enabled it for tools verified to depend only on the binary (node, npm, bun, deno, java, and most single-purpose language/build-tool binaries). Left it off, with a comment explaining why, for tools whose output is directory- or environment-dependent even though the resolved binary doesn't change: dotnet (reads global.json), python/pyenv and ruby/elixir (asdf/rbenv shims), yarn/pnpm (Corepack's package.json pinning), gradle/mvn (wrapper scripts), bazel (bazelisk + .bazelversion), rustc (rustup toolchain switching, verified directly), go (GOTOOLCHAIN auto-switching via go.mod, verified directly), cds (reports the local project's dependency version), stack and fvm (project-scoped resolver/SDK config). Added Environment.StatFile so the mtime/size lookup goes through the same mockable runtime layer as everything else language.go already depends on. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4RA9ZrP7vuqVSR7nddNiQ
The field-set analysis ran on every prompt render because the stamps lived in unexported fields the session cache's gob round trip dropped - a measured ~1ms wall regression per prompt on template-heavy themes. The stamps (Segment.ReferencedFields/FieldsAnalyzable and the Config.FieldSetsResolved marker) are now exported and ride along in the gob payload, kept out of json/yaml/toml like Needs so no config format ever surfaces them. Store stamps before encoding, so init's seed write and every reload persist an analyzed config in the write they were already making; a restored, stamped config makes ResolveFieldSets a no-op. A pre-stamp cache entry written by an older binary analyzes once on restore and re-Stores - one extra cache write, after which the rest of the session skips the analysis. Reload parses fresh content and re-stamps through the same Store path, and the serve daemon's per-cycle config decode inherits the stamps for free. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4RA9ZrP7vuqVSR7nddNiQ
Review of the version-cache commit surfaced tools whose resolved binary stays byte-identical while its output follows external state, so the path+mtime+size key can serve stale versions: - flutter: bin/flutter is a stable wrapper dispatching into the SDK checkout next to it; `flutter upgrade`/`flutter channel` swap that SDK via git without touching the script (same class as mvnw/gradlew). - swift: on macOS /usr/bin/swift is an xcrun shim dispatching per xcode-select/DEVELOPER_DIR (the rustup-proxy class). - java (plain): /usr/bin/java on macOS is Apple's stub resolving the JDK via java_home at run time. The JAVA_HOME-resolved cmd keeps the cache: there the resolved path itself keys correctly. - ui5: the global @ui5/cli shim delegates to a project-local node_modules install when present, reporting per-project versions. Also hardened versionCacheKey to mix cmd.envs into the hash: no cacheable cmd sets envs today, but the key must not silently under-specify one that does. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4RA9ZrP7vuqVSR7nddNiQ
Review of the template-derived fetch units surfaced template sources that render with segment or global context without the analysis seeing them, plus two hardening gaps: - A segment's style is itself a template resolved against its writer (SegmentStyle.resolve); it now counts toward that segment's field set, as do fillers on extra prompts. - Templated option values (branch_template and friends) render against per-option contexts the walk cannot model; a segment carrying any is now stamped unanalyzable - falling back to the explicit fetch options, display-correct by construction - while cross-segment references inside those options still count toward the segments they name. - Global nil-context strings that can reach .Segments - block fillers, pwd, cursor style, terminal background, and palette values - now go through the same cross-segment extraction as the console title. - Persisted stamps carry an analyzer generation marker (fieldSetAnalysisVersion): a session-cached config stamped by another binary generation re-analyzes once on restore and re-Stores, so long-lived sessions never trust stale stamps after an upgrade. - Bare repos populate .Upstream through the upstream-icon probe (getBareRepoInfo), so Upstream now maps to that unit as well as the status unit. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4RA9ZrP7vuqVSR7nddNiQ
JanDeDobbeleer
force-pushed
the
claude/lazy-load-segment-perf-8wo3t7
branch
2 times, most recently
from
August 28, 2026 16:32
3e8bf85 to
2d81119
Compare
SCM segments now fetch the data referenced by their templates, so unused status and metadata commands no longer need manual controls. BREAKING CHANGE: SCM fetch options are no longer used. Remove fetch_status, fetch_push_status, fetch_upstream_icon, fetch_bare_info, and fetch_user. To control fetching, add or remove the corresponding fields from the segment templates. Existing configs still load, and obsolete options are ignored. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4RA9ZrP7vuqVSR7nddNiQ
…erprints Two review findings on the template-derived fetch fallback: The heuristic scanned only the unanalyzable segment's own templates and options, but unanalyzability can be caused by a text OUTSIDE the segment - another segment laundering a cross-reference through a variable, or a global template. In that shape the field name that defeated the analysis was never scanned, the probe stayed off, and with the fetch options gone there was no user-side fix. ResolveFieldSets now assembles the whole-config text corpus (every segment's templates and templated options plus the global nil-context strings) once and stamps it on each unanalyzable segment; the fallback scan covers it all. The stamp gained a field, so fieldSetAnalysisVersion bumps. templatedOptionValues iterated option maps in Go's randomized order, and the collected sources feed the unanalyzable cache-key fingerprint: any segment with two or more templated option values got a fresh key per process, so its snapshots never hit and stale entries piled up. Collection now sorts map keys recursively, making the delivered order - and the fingerprint - stable across parses. Also strips the removed fetch_* keys from the src/test fixtures and updates the segment-docs skill's worked example off the deleted FetchStatus const. Acknowledged default changes now stated in the git segment docs' migration note: the default config's statusline derives the upstream icon its old explicit fetch_upstream_icon: false suppressed; the svn, mercurial and jujutsu default templates fetch status by default; and jujutsu's default rendering requires the jj binary. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4RA9ZrP7vuqVSR7nddNiQ
Jujutsu templates can now render bookmark names and their distance independently with .ClosestBookmarks and .AheadCount. BREAKING CHANGE: fetch_ahead_counter and ahead_icon are no longer used. .ClosestBookmarks now returns undecorated bookmark names. Add .AheadCount and an icon to the template to keep showing the distance. Existing configs still load, and obsolete options are ignored. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4RA9ZrP7vuqVSR7nddNiQ
Language and Terraform segments now fetch version data only when their templates use it. BREAKING CHANGE: fetch_version is no longer used by language and Terraform segments. Remove the option and reference a version field in the template to fetch it. Remove version fields from the template to skip version commands. Existing configs still load, and the obsolete option is ignored. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4RA9ZrP7vuqVSR7nddNiQ
Three segments populate template-visible fields inside the gated version fetch that the shared unit list missed, so referencing them alone silently skipped the fetch and rendered empty. Language gains an extraVersionFields hook: a concrete segment declares in its spec the fields its own fetch-path closures populate, and versionFields appends them to the shared list for the fetch decision. - gradle: KotlinVersion/GroovyVersion/AntVersion/JVMVersion, populated by parseExtraVersions inside the gated getVersion closure - dotnet: Unsupported, derived from the gated fetch's exit code - python: Venv, which the pyenv getVersion overrides with the resolved virtualenv name - a .Venv-only template now fetches to keep that naming (a slight over-fetch for non-pyenv users, deliberately) A sweep of the remaining custom getVersion/tooling closures (node packages, golang mod parsers, helm and friends) found no other gated field writes. Also restores the contextEnvVars/contextFiles contract comments the struct reshuffle dropped and removes the stale fetch_version row from the segment-docs skill. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4RA9ZrP7vuqVSR7nddNiQ
JanDeDobbeleer
force-pushed
the
claude/lazy-load-segment-perf-8wo3t7
branch
from
August 30, 2026 09:43
aa17142 to
7dd5e8b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add a cheap, declarative pre-check to the segment execution path: a writer
can implement the optional segments.Activator interface to declare the
preconditions (cwd file globs) under which it can possibly be enabled. When
none of the globs match the cached directory listing, Segment.Execute skips
the writer's Enabled() probe entirely and leaves the segment disabled.
The gate is conservative by design:
zero value), execute exactly as before.
via a hand-written data file bypass the gate and run the full path.
could enable the segment: project files (parent-directory search), folders
(stat-based check), or a display mode other than "files".
The whole language family opts in by extracting its spec construction out of
Enabled() into an idempotent loadSpec(), letting Activation() resolve the
same extensions/folders/display-mode options the probe would. The helper is
deliberately unexported so embedders that still build their spec inside
Enabled() never gate against an empty spec.
The gate evaluation lives beside the writer registry so the js/wasm build,
which has no writers, keeps linking without the segments package.
Co-Authored-By: Claude noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01M4RA9ZrP7vuqVSR7nddNiQ