Adds two cursor_style values that only touch shape reset (CSI 0 SP q)
and blink mode (CSI ? 12 h/l), instead of selecting one of the fixed
DECSCUSR shapes. Some terminals - Windows Terminal among them - offer
cursor shapes with no DECSCUSR equivalent (vintage/thick underscore,
double underscore, empty box). Existing cursor_style values always
override those with block/underline/bar, but default_steady and
default_blinking let the terminal profile's own shape stand while
still controlling steady vs. blinking.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Renders cursor_style through the trusted template engine before
emitting the DECSCUSR sequence, mirroring the existing pwd and
console_title_template config options. This lets the cursor shape
vary per render, e.g. by branching on .Env.POSH_VI_MODE (set by the
vimode segment's shell hooks) to change shape between vi modes,
without introducing a dedicated config field.
Closes#7767
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Terminal control-rune stripping (edcf3c88, 16bf1387) closed an
injection vector but also blocked literal cursor-shape escapes users
previously put in templates (see discussion #7742). Render DECSCUSR
sequences natively instead, driven by an enum config value, the same
way iterm_features and pwd already expose trusted terminal features.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: afc824f2ed92
An adversarial review verified five silent divergences from the exec
path, each reproduced against real repositories before fixing:
- executable-bit changes were invisible on both sides: the worktree
scan now compares the 0100 bit when core.filemode applies, and the
HEAD tree walk carries entry modes so staged chmods surface too
- a conflict living in an otherwise-untracked directory was counted as
an untracked entry; unmerged paths now participate in the tracked
prefix search
- a staged rename with edits reported add+delete where git pairs them
through similarity detection; unpaired adds and deletes on both
sides now defer to exec git rather than risk diverging counts
- a loose ref holding a symref misreported the branch as unborn; an
unparseable loose ref is now an error, falling back to exec git
- file-to-symlink type changes counted as modified where porcelain
reports T, which the segment ignores; both engines now agree
SHA-256 object format, reftables ref storage, and unknown repository
format versions are also rejected deterministically instead of relying
on the index decoder to stumble. New parity scenarios cover chmod on
both sides, conflicts in fresh subdirectories, inexact renames, and
symlink type changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Apply the project's Go skill end to end: modernize and fieldalignment
rewrites, switch instead of else in the index name decoder, errors.New
for static errors, restored field docs the alignment pass dropped, a
log.Trace on Load, and test cleanups — the three fallback trigger tests
merge into one table, and the benchmark reuses the parity test helpers
through testing.TB instead of duplicating them.
Also fold in the review's simplifications: drop the dead rehash
counter, parse only the cache-tree root record, key the pack object
cache by struct instead of a formatted string, skip the walk entirely
when the flat pool runs with untracked detection off, search a single
sorted-path list per platform, and turn the ahead/behind termination
flag into a plain break.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first native-status iteration leaned on go-git for index decoding
and object access, costing 2MB of binary size and half the load time on
large repositories. Replace both with purpose-built readers:
- index parser: direct v2-v4 decode (including split/sparse detection,
skipHash trailers, and the TREE extension) instead of go-git's
allocation-heavy decoder
- object store: loose objects plus pack files with delta chains, read
via idx binary search, instead of storage/filesystem and its
dotgit/config/billy dependency chain
- worktree scan: on non-Windows platforms a flat worker pool lstats the
index entries directly (git's preload_index equivalent) since ReadDir
carries no stat data there; Windows keeps the enumeration-fed walk
go-git remains only for gitignore matching. Binary size delta over the
exec-only baseline shrinks from +2.05MB to +205KB. Status load drops
from 5.1ms to 3.2ms (1.1k files) and 31ms to 12ms (12.5k files) on
Windows; Linux at 10k files goes from 2.2x slower than git to 2x
faster on ext4. Parity tests now also cover packed objects, delta
chains, index v4, and both scan strategies on every platform.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Spawning git for status costs 22-25ms in process startup alone on
Windows, roughly 70% of the segment's render time. Computing the same
porcelain v2 counts in-process (go-git plumbing for index, gitignore
and object access; custom stat-cache worktree scan with a cache-tree
fast path) removes that tax: 6ms vs 36ms on this repository.
Opt-in via the native_status option. Any unsupported repository shape
(sparse or split index, reftables, submodules) falls back to the git
CLI automatically. Parity with the exec path is enforced by tests that
compare both engines against real repositories, including merge
conflicts, linked worktrees, and index.skipHash zero trailers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pull_request-triggered workflow gets a read-only token on fork
PRs, so posting the size comment failed with 403. Build jobs now only
upload artifacts; a separate workflow_run-triggered workflow with
write permissions downloads them and posts the comment. The PR number
travels via an untrusted artifact, so the report validates it and
checks the PR head SHA against the triggering run before commenting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 302a325be262
A leftover `)}` outside the JSX expression rendered as literal text
below every segment doc's sample preview.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: e8444e478272
The terminal writer accepts osc99, osc7 and osc51 (anything else falls
back to osc99), but schema.json typed pwd as a bare string - so neither
the website editor nor schema-aware editors (VS Code, yaml-language-
server) could offer the values. Same lenient enum-or-string shape the
segment style property already uses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 2b5eecac4b41
The hand-rolled completion engine could not complete YAML key
positions: its cursor-marker probe produced invalid YAML mid-document,
the parser folded the marker into the next line's key, and the popup
never opened. Instead of patching the probe, replace the editor
foundation: CodeMirror 6 provides the popup, gutter, hover, and
tooltip machinery natively.
codemirror-json-schema was evaluated for the schema features and
rejected: enum completion through $ref resolves to nothing, $ref
nodes with sibling keywords crash its resolver, and both its published
ESM and CJS builds are broken under strict module resolution. The
proven schema resolver from the previous engine stays, rewired as a
native completion/hover source that walks the lezer syntax tree
instead of scanning text - which makes mid-edit states (blank lines,
dangling keys, open strings) work in both JSON and YAML, including
per-segment-type options completion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 9045963ce7e2
TestRestrictedFuncMapExactKeySet pinned a 190-entry hardcoded list that
just mirrors the map's own construction data; the drift-guard and
dangerous-funcs-excluded tests already cover the real invariants, so it
only added maintenance burden without extra protection.
TestStripControlRunesFastPath asserted unsafe.StringData pointer
identity to prove the no-allocation path ran — a memory-layout detail,
not behavior. The unchanged-input case is already covered functionally
by TestStripControlRunes's unicode case.
CI's fieldalignment linter flagged the anonymous test-case struct as
larger than necessary; reorder Features (a slice) after the string
fields to match its expected layout.
write's per-rune isControlRune filter only guards segment-body text
streamed through it. Pwd() and RenderItermFeatures() format the raw
current working directory (and username/hostname) directly into OSC
7/51/99/1337 sequences via fmt.Sprintf, bypassing that filter entirely.
A directory name containing ESC/ST/BEL bytes closes Oh My Posh's own
OSC sequence early and lets an independent, attacker-controlled escape
sequence stand on its own once the victim cds into it and the pwd or
iterm_features integration renders it — the same class of injection an
earlier fix closed for segment content, reopened via a sink it didn't
cover.
Add a whole-string control-rune stripper for these one-shot OSC payload
fields and apply it to pwd/userName/hostName before every OSC format
call in both functions.
Entire-Checkpoint: 97966247c0a5
context.init bound Getenv to the real environment regardless of trust
level. patchTemplate rewrites .Env.NAME into (call .Getenv "NAME"), but
that rewrite isn't the only way in: a raw {{ call .Getenv "NAME" }} in
untrusted text reaches the same binding directly, so restricting the
rewrite alone would still leave environment values readable from
attacker-controlled input (e.g. a directory name), letting secrets leak
into the rendered prompt.
Bind Getenv only when the template is trusted; untrusted renders get a
stub that always returns empty. Empty rather than nil keeps templates
that legitimately reference .Env.* from failing to render entirely.
Entire-Checkpoint: cf47aaae6d2a
RenderUntrusted's func map was built by copying every sprig function
except an explicit six-name blocklist, so getHostByName (a live DNS
lookup) and over a dozen CPU-expensive crypto functions stayed reachable
from attacker-controlled text such as a directory name. getHostByName
let a crafted folder name exfiltrate .Env values over DNS the moment the
prompt rendered; the crypto functions let the same input hang the shell
via a loop.
Block both classes in dangerousFuncs, keep them available to trusted
config templates via sharedFuncMap, and replace the untrusted map's
"copy everything except six names" construction with an explicit,
independently maintained allowlist of the sprig functions already
reviewed as safe for untrusted input. A drift-guard test now fails when
a sprig upgrade introduces a function that isn't yet triaged into either
list, so new capabilities can no longer reach untrusted templates by
default.
Entire-Checkpoint: 6beb436f6cd8
Show a description tooltip on hover for completion options (real
prose text, or a "View documentation" link for URL-only schema
descriptions), and flip the completion popup above the caret when
there isn't enough viewport room below, clamping its max-height so
it always stays fully on-screen.
Also remove the entire.json repo hook, whose sh invocation errored
on this machine and blocked every gated tool call.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: da7937bafeeb
The rebase onto origin/main dropped 4 commits whose content already
landed upstream under different hashes, taking with them style fixes that
had been folded into those commits earlier in this branch's history:
- color/shade.go, svg/svg.go, prompt/golden_test.go, terminal/writer.go:
re-apply the avoid-else refactors for in-scope hunks introduced by this
branch's styled-runs/shade/svg work.
- config/dsc_hook.go, cli/dsc/shell.go: reword the stale cobra mentions
back to the command tree/flag machinery wording used elsewhere on this
branch.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: c61e99a57d85
statusline.go's doc comment and root.go's Explorer-launch-guard comment
predate this branch and weren't touched by its cobra/pflag -> cmdtree/cmdflag
rename commits, so they were left referencing the removed dependency and its
'mousetrap' terminology. Reword them now that cmdtree/ExplorerLaunchHelpText
are the only names that exist.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: d987e21b8f4f
Builds a release-equivalent binary for every major OS we support
(linux, windows, darwin; amd64), using the same ldflags/tags/env as
src/.goreleaser.yml, each cross-compiled on ubuntu-latest with
CGO_ENABLED=0 (mirroring how build_code.yml already cross-builds every
OS via goreleaser). Each platform can pick up its own dependencies, so
each is checked independently.
Compares every build against the latest published GitHub release's
matching asset, since that mirrors what users actually download and
avoids rebuilding the base branch from source. Posts (and updates, on
re-runs, via a hidden HTML marker) a single PR comment summarizing the
baseline size, PR size, and delta in bytes/percent per OS, calling out
growth beyond 256 KiB or 2%% so an accidental heavy dependency doesn't
slip in unnoticed.
The job only fails on genuine errors (build/download failures), never
on size growth, so legitimate feature additions aren't blocked.
Triggers only on pull_request changes under src/**, since that's the
only tree the binary is built from.
The config command no longer has a migrate subcommand, but its Long
description still advertised it.
Fixes#7734
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: 80471ee5a91c
Assigning \False stringifies to the literal False, which passes
conda's own truthiness check and gets Write-Host'ed on every prompt.
An empty string matches what conda's prompt wrapper actually expects.
Fixes#7733
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: ce0acf0d99c9
Rasterizes the live prompt SVG to a PNG on a hidden canvas. The font
is embedded as a base64 data URI in the SVG before rasterizing, since
an Image() loaded from a blob URL cannot see the page's own
@font-face and would otherwise fall back to a generic monospace font.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: 0401007e6324
The homepage hero rendered once, against the svg exporter's own fixed
dark canvas background, so it always looked like a dark-mode prompt
even when a reader had the site in light mode.
export_themes.mjs now renders the default config twice - once
unchanged for dark, once with --background-color=#ffffff (Infima's
own light background) for light - and hero.json carries both as svg/
svgLight. The homepage ships both renders in its static HTML and picks
between them with plain CSS keyed off Docusaurus's own
html[data-theme] attribute, the same convention custom.css's
--omp-card-background override already uses, so the switch is instant
and needs no client re-render.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: e4462dff49f1
The svg export's canvas background always came from the theme's own
terminal background (or a fixed dark fallback with none set) - a
caller with no way to ask for a specific canvas color at export time,
which the website needs to render the homepage hero against both a
dark and a light background for its own dark/light toggle.
The flag only ever fills in for a theme that sets no terminal
background of its own: withDefaults still prefers a real
TerminalBackground when the theme has one, so this can't paint over
how a theme would actually look in a real terminal.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: 0ba07d2489e2
Redraws the terminal-window chrome to match a Windows Terminal window
instead of macOS Terminal.app: no colored traffic-light dots, a flat
title bar sharing the window's own background (no two-tone header),
thin line-drawn window controls (- box x) flush against the right
edge, and +/chevron tab controls. All chrome glyphs share one baseline
helper so the title, tab controls and window controls sit on the same
visual line regardless of their own font-size.
Adds Options.Title so a caller can label the window (e.g. the shell
name); unset renders no title, matching the prior chrome's lack of one.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: 0e301f956da9
config/config.go and config/default.go referenced bare segments.CONST option keys
directly and unconditionally, pulling the entire segments package - and its
transitive HCL/go-cty/x-mod-modfile/ini/cli-auth dependency tree - into every
binary linking config, including src/wasm. Mirror the option-key strings as
local options.Option consts in config instead and drop the segments import.
src/dsc (invopop/jsonschema -> go/ast, go/parser, go/doc; spf13/cobra) was
imported unconditionally by config/dsc.go and shell/dsc.go for CLI-only DSC
(Desired State Configuration) bookkeeping never exercised by the wasm render
path. Move both DSC resource types into a new cli/dsc package where they're
actually used, and decouple config from dsc via a nil-by-default hook -
config.NewDSCTracker, set by cli/dsc's init() - so config.Parse() no-ops the
bookkeeping instead of importing dsc directly.
Combined effect on the wasm build: 21,833,910 -> 17,825,752 bytes stripped
(-4.01MB, ~18.4%). go list -deps ./wasm/ confirms segments, dsc,
invopop/jsonschema, spf13/cobra, go/ast, go/parser, and the HCL/go-cty chain
are all gone from the link graph.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: 3ef09bcd218e
Move runtime.Terminal's gopsutil-backed and terminal-dimensions-backed
methods (Shell's parent-process fallback, SystemInfo, Memory, TerminalWidth,
Platform, and the remaining WSL/registry/media/connection probes) into
!js/js file pairs, mirroring the existing terminal_root_js.go /
terminal_writable_js.go split. text/template's reflect.MethodByName call
keeps every exported method of runtime.Terminal linked once it is converted
to the Environment interface (render.Config does this), so a per-method
build-tag split is the only way to drop an implementation's cost - a new,
slimmer Environment type would not help.
Retags terminal_unix.go/terminal_unix_test.go from !windows to
!windows && !js since terminal_js.go now owns those methods for js.
Verified: gopsutil (v4.26.6) already ships GOOS=js fallback stubs, so this
is not a multi-MB win - the stripped omp.wasm build drops from 21,866,822 to
21,833,786 bytes (~33 KB), and go build -ldflags=-dumpdep confirms
gopsutil/process, gopsutil/cpu, gopsutil/load, and terminal-dimensions are
fully gone from the link graph (only the type-only gopsutil/disk.IOCountersStat
used by SystemInfo.Disks remains, out of scope per environment.go).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
`npm start` now runs scripts/dev.mjs instead of `docusaurus start`
directly. It wraps the dev server with watchers that keep generated
artifacts current without a manual regen + restart:
- a /themes edit re-renders just that theme and patches it into
generated/themes.json, so <ThemeGallery/> hot-reloads it
- editing export_themes.mjs, font-metrics.mjs or segment_data.json
falls back to a full `npm run themes`, since those can change
every theme`s render
- editing a Go source file under src/ rebuilds the studio`s wasm
module (`npm run wasm`)
`npm run build` and CI are unaffected: they already run these
scripts explicitly before building and never watch anything.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: 23a2e79bf51a
Recorded data replayed without a segment writer diverged from the same
data replayed with one, in four ways that only surfaced once both paths
rendered the bundled themes side by side:
- Colour templates resolved against the writer rather than the data, so
every one of them fell back to the plain colour where no writer
exists - 49 of 124 themes.
- A value that is not a struct lost its methods. battery.State is an
int, and `.State.String` is what every battery theme switches on.
Method results now travel in a tree of their own, which keeps the
data itself writer-shaped: an entry whose "State" were an object no
longer unmarshals into the writer.
- A field renamed by its json tag was recorded under its Go name alone,
so encoding/json never matched it on the way back and terraform's
version restored as nil. Both names are recorded now.
- `date` fell through to the wall clock for a timestamp arriving as a
string, which is how a recorded time.Time always arrives.
The wasm build carries no zoneinfo, so its idea of local time was
whatever offset the host happened to be on and a gallery built in July
printed an hour later than the same gallery built in January. It
renders in UTC now, with recorded timestamps written to match.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Entire-Checkpoint: 707298e5cc26
Choosing a font was a five-stage terminal UI: fetch the list, scroll it,
download, unzip, install. It pulled bubbletea, lipgloss, bubbles and a
fuzzy matcher for what amounted to a non-filtering list of names with
one highlighted - and Go runs a linked package's init before main
whether or not the subcommand was invoked, so everyone typing at a
prompt paid for it on every render.
A CLI already has a way to answer "which fonts are there": print them.
oh-my-posh font list
oh-my-posh font install Meslo
The list can be grepped, piped and scripted, none of which a picker
allows, and install is what it always was underneath - resolve, fetch,
unzip - now reporting on a status line and a progress bar.
Removes 20 modules including go-runewidth, whose package init built a
2.2MB lookup table nothing in the prompt path ever read. The
atotto/clipboard fork this repo carried goes with them: bubbletea was
the only thing that needed it.
Binary 17,330,176 to 15,981,056 bytes (-7.8%). Init, median of 11 runs,
39.3ms to 6.8ms - on every invocation, prompt renders included.
BREAKING CHANGE: `oh-my-posh font install` now requires a font name; run
`oh-my-posh font list` to see them. The interactive picker is gone, and
with it the --headless flag, which existed to skip it.
Entire-Checkpoint: ef231d69c9bf
Both were display-only shells: a spinner, a status line, and in the
upgrade's case a progress bar. No list, no input, no alternate screen,
no mouse. The upgrade's business logic was already decoupled behind
plain reporter callbacks, added so the core would never import
bubbletea, so it only ever needed a different subscriber.
The auth flows had their device-code polling inside the models. They
report through the same setState calls as before; what changed is that
Run drives them synchronously instead of spawning a goroutine to keep a
message loop fed, because a status line now redraws from its own ticker.
Each flow still words its own steps - GitHub names the device code,
YouTube Music names its base URL - so message() replaces View().
The upgrade UI loses its q-to-quit key. Interrupting an upgrade midway
through replacing a binary was never a good idea, and ctrl+c still
reaches the process.
The font picker still uses the framework, so nothing is removed from
go.mod yet.
Entire-Checkpoint: 0254bffa0c17
oh-my-posh renders a prompt once or twice per keystroke and then exits.
It also ships a font installer, an upgrade UI and two OAuth flows, all
built on bubbletea - and Go runs a linked package's init before main
whether or not the subcommand that needs it was invoked. Someone typing
at a prompt pays for a font picker they are not using: 1.84MB of binary
and 44ms of every render, most of it go-runewidth building a 2.2MB
lookup table nothing in the prompt path reads.
What that framework is asked to do here is small. Across the whole
binary the interactive surface is one non-filtering select list, a
spinner, a progress bar, and quit keys. No text input, no alternate
screen, no mouse.
So: a status line that repaints with a carriage return and an erase, a
progress bar that does the same and keeps reporting to the terminal's
own taskbar indicator, and a numbered picker.
Everything writes in cooked mode. Arrow keys would need raw mode, and
raw mode is the one genuinely risky thing here - termios on POSIX,
console modes on Windows, restoring both when the process panics, and a
fallback for the ptys where stdin is a pipe and none of it applies. A
number and Enter needs none of that, and works when stdin is redirected,
which is how anyone scripts an install.
Nothing uses it yet.
Entire-Checkpoint: 040a19f91be1
Two things a segment lost when there was no writer to build it.
A segment with nothing recorded about it never turned on. Enabled was
only ever set from the writer, so with none the segment fell straight to
its fallback and drew nothing - including a text segment, which needs no
data at all and whose whole content is its template. Render already
settles this correctly for every other case, by asking whether the text
came out blank, so the answer is to let it: with no writer and nothing
recorded, the template decides.
Numbers arrived as the wrong type. JSON has one number type, so
unmarshalling into a map makes every number a float64 where the writer
struct would have produced whatever the field declared. sysinfo renders
`{{ round .PhysicalPercentUsed .Precision }}`, and round wants its
precision as an int, so a Precision of 2.0 failed a template that an int
2 rendered. Whole numbers are restored as ints, which is the guess that
still renders: a function taking a float widens an int, while one taking
an int rejects a float.
The fixture guard learns that a recorded entry is an envelope, and that
a key naming a method is not the typo it hunts for. azpwsh leaves the
fixture: it is not a registered segment type, though two bundled themes
still name it.
Entire-Checkpoint: 1478bb7e734f
The gallery spawns oh-my-posh once per theme and the segment previews
once per segment: 241 processes to draw pictures the studio already
draws in a browser tab, from the same render.SVG.
A Node module instantiates that same wasm once and renders every prompt
in-process. It also decides what the SVG encoder has to stay reachable
from: with this, nothing outside the module needs to draw one.
Two things it needed. An empty config now means the built-in default,
matching what config.Load does with no path, so the homepage can ask for
the prompt someone sees before configuring anything - there is no file
to point at and no bundled theme that matches it. And the data fixture
carries what a template calls as well as what it reads, re-recorded
through the writers so its curated values survive.
The generators still shell out to the CLI. Rendering all 124 themes
through the module leaves 16 whose templates fail against the fixture,
and that is not understood yet - the keys those templates name are all
present. Switching them over waits on that.
Entire-Checkpoint: b83e4917be12
The studio renders a config from recorded data. It has no filesystem, no
processes and no network, so every segment's detection finds nothing the
moment it runs - and yet all 118 implementations were linked into the
module, because a package-level map holds a constructor for each and an
init gob-registers them all. Both are reachable from any config parse,
so between them they pinned every segment package and everything those
pull in: cloud SDKs, HTTP clients, the lot. 4.2MB raw, 0.5MB brotli, for
code no visitor can reach.
The registry moves behind a build tag. The browser build constructs no
writer at all and renders from the recorded data instead, which carries
what a template reads - fields and method results alike. Seven places
assumed a writer exists; they now ask whether there is one, and the
rendered text has somewhere to live when there is not.
The website's own data file moves to the recorded envelope format, which
is what carries a segment's enabled state when there is no writer left
to ask.
Combined with the HTTP stack already gone from this build: 29,351,525
bytes to 21,857,604 raw, 4,675,591 to 3,640,973 brotli - 22% off what a
visitor downloads.
Entire-Checkpoint: da3ad3698634
A recorded data file held a writer's fields. Templates read more than
fields: `{{ .Working.Added }}` reads one and `{{ .Working.String }}`
calls a method, and a template neither knows nor cares which. Replayed
against the writer struct that difference stays invisible, because the
methods come back with it. Replayed against the data alone it does not -
nothing can carry a method - so a template calling one found nothing
where a terminal found a string.
The recorder now walks the writer's exported zero-argument methods and
records what they returned, two levels deep, recovering from any that
panic. Go resolves a name against a map key exactly as it resolves it
against a method, so the recorded result reads identically.
--data joins the command for the same reason the image and print
commands carry it, plus one: a fixture recorded once can be re-recorded
through the writers to pick up whatever the format has since gained,
without giving up the values it was curated with.
Entire-Checkpoint: 880120dac3d1
Rendering reaches into segment.writer for two things: the object a
template evaluates against, and the default template. Both assume a
constructed writer, which means linking the package it lives in - all
118 of them, for a build that renders only from recorded data and never
runs a single segment's detection.
templateContext returns whichever of the two is present: the writer
where one was constructed, and the recorded data as a plain map where
none was. Go templates resolve a name against a map key exactly as they
resolve it against a struct field, so the same recorded values reach the
same templates either way; restoreInto picks the destination to match.
No behaviour changes here - a writer is still constructed everywhere
today, so templateContext always returns it. This only removes the
assumption that one must exist.
Entire-Checkpoint: 6643e0d75be3
A segment with no template of its own falls back to one its writer
defines, and 18 segments across 11 bundled themes relied on that. Most
are text segments used as spacers, whose default is two spaces, but
illusi0n's path, jtracey93's status and lambdageneration's git each
depend on a real one.
Each now carries the default it was already rendering, copied verbatim.
The golden harness is the proof this changes nothing: it pins the
rendered bytes of all 124 themes, and they are unchanged.
A theme that says what it renders can be read without reaching into Go
for the other half, and renders the same anywhere the config goes -
including somewhere with no segment implementations to ask.
Entire-Checkpoint: 174e5f6ea483
Every length the writer computes is a sum of per-rune cell widths, and
go-runewidth answered that from its own Unicode tables. Since v0.0.27 it
also builds a 2.2MB lookup table in package init: 2.23 million calls
before main runs, measured at 25ms. oh-my-posh renders one prompt and
exits, so that was around 10% of a 240ms render, on every prompt, twice
per keystroke on shells that also draw a transient one.
The table is a cache, and a badly aimed one here. Measured over a
realistic 80-rune prompt it saved 48ns per render against the library's
own uncached path, so it needed something past half a million renders in
one process to pay for itself. It could not be turned off either:
Condition.RuneWidth only reads it when StrictEmojiNeutral is set, but
init builds it regardless.
runeCells answers the same question from the standard library's unicode
categories and golang.org/x/text/width, both already linked, with a fast
path for everything below U+0300 - which is nearly all of a prompt.
Checked against go-runewidth v0.0.27 across all 1,114,112 code points:
2,648 disagree, of which 2,048 are UTF-16 surrogates that cannot reach
this code, ~330 are wide symbol blocks no prompt contains (I Ching
hexagrams, Tai Xuan Jing, counting rods, Tangut) and ~150 are format and
tag characters where zero is the better answer. The real gap is ~70
recently assigned combining marks that x/text does not yet classify.
Every rune a prompt actually holds agrees exactly: emoji including flag
sequences, CJK, Nerd Font glyphs, powerline separators, box drawing,
accented Latin, common combining marks, Cyrillic and Greek.
The dependency stays in the module graph for now - bubbletea reaches it
through charmbracelet/x/ansi - so the CLI still pays that init until the
interactive commands stop being linked into the same binary. The wasm
build no longer references it at all.
Entire-Checkpoint: 0b183cf5f6e6
The badge linked to a listing that is no longer the recommended way to
install on Windows; winget is. Removing it takes the two SVG assets
with it.
Entire-Checkpoint: fdf9cbbb3842
The site asked visitors to read a wall of segment pages to find out
what oh-my-posh could do, and to install it before seeing a prompt.
The homepage now leads with the install command and a rendered prompt.
Every one of the 117 segments is in a searchable catalog generated from
the docs themselves, so it cannot drift from what ships, and each
segment page carries a preview rendered from committed sample data at
build time. The theme gallery inlines the SVGs from the previous
commits instead of loading screenshots.
The studio renders a config live in the browser through the wasm
module, and each segment page embeds the same editor: change the
sample config, watch the prompt redraw, then send the segment to the
studio to build a full theme out of it. Nothing is sent anywhere.
The docs move into one shell around all of it, on a single type and
spacing scale, with a filterable sidebar.
Entire-Checkpoint: dc6fc3bdbdf2
Rendering a prompt meant going through the CLI: cobra flags, a terminal
UI, an environment that probes git and cloud tooling on every segment.
Nothing else could ask for a rendered prompt, and nothing could ask for
one that touches nothing.
A render package takes a config, data and options, and returns the run
stream. The CLI becomes one caller of it; a WebAssembly entry point
becomes another, which is what lets a browser render a prompt with no
server and no machine behind it.
DataOnly moves onto the environment rather than sitting beside it as a
flag the call sites had to remember to check. A data-only render still
derives what it can from the data it was given instead of reporting
every segment absent, so a recorded file renders the same prompt it
recorded.
The terminal UI leaves the core packages so the wasm build does not
pull a TUI toolkit into the browser bundle, and the retired PNG
renderer's image package and its dependencies go with it.
Entire-Checkpoint: 603a21cafec5
The PNG exporter rasterized the prompt itself: it re-parsed the ANSI
string, resolved fonts, drew glyphs, and blurred a drop shadow, all to
produce a bitmap that could not be searched, selected, restyled or
scaled. It also carried image and font dependencies for that one
command.
A new svg package encodes the run stream from the terminal writer
(see the previous commit) into a terminal window drawn as vectors:
rounded frame, traffic lights, and one <text> per styled run. Text
stays text.
Everything after the first cut is a rendering fix found by comparing
real themes against what a terminal shows: transparent runs painted as
a cutout rather than a hole, wide glyphs given the room they ink, the
grid taken from the font's own metrics instead of the retired
renderer's picture, template whitespace preserved, the Mono Nerd Font
variants asked for first so icons advance one cell, the cursor placed
where the shell leaves it rather than after the right prompt, short
hex colours resolved, and a segment background ending where its
separator does. The canvas is now the window itself, with no
transparent gutter around it.
BREAKING CHANGE: `oh-my-posh config export image` writes SVG only. The
`--author`, `--background-color`, `--cursor-padding`, `--rprompt-offset`
and `--settings` flags are gone, as is PNG output; a command line using
them now fails rather than quietly producing something different.
Entire-Checkpoint: f6059ae8893a
The prompt was only ever emitted as a finished ANSI string. Anything
that wanted to draw it some other way had to parse those escape
sequences back out, which is why the image exporter carried its own
half-interpreter of them.
The writer now records what it emits as a stream of styled runs: the
text, the cell count, and the colours a segment actually asked for
rather than the ones already resolved to escapes. Emitting ANSI is one
consumer of that stream; anything else can be another.
Getting there meant fixing what the string form had been hiding.
Hyperlink state leaked between writes, plain mode ignored hyperlinks,
and visible-cell counting disagreed with how the runes render, which
matters the moment something lays them out on a grid instead of letting
the terminal do it. Gradients now resolve only the colours they use.
A golden harness covers all 124 bundled themes against one shared,
sanitized --data fixture: a manifest of per-theme hashes so a change
touches one line, plus full ANSI blobs for a representative handful.
Recorded replay had to become hermetic first, or the fixture would not
have been reproducible.
Entire-Checkpoint: d27890fba3f4
Removed doc/inline comments that only paraphrased the name or next
line of code, across Go source, shell init scripts (bash, zsh, fish,
pwsh, nu, elvish, xonsh, yash, Clink lua), and website JS. Comments
encoding non-obvious workarounds, race conditions, or external bug
references were kept and trimmed to just that point.
Moves the minimal-comment rule from the golang skill into AGENTS.md
so it applies repo-wide instead of to Go only.
Entire-Checkpoint: 7a66b31a1ad7
Windows writes the cache file through a memory-mapped view, which per
Microsoft's docs does not reliably update the on-disk last-write-time
on its own. The previous mtime-touch fix only ran when the store had
nothing new to persist, so an actively-used session (dirty on nearly
every render) never hit it, leaving cache.Clear() free to reap a live
session file after 7 days.
Refs #7340
Entire-Checkpoint: 7305e5465ad2
CI's fieldalignment check flagged the test case struct's field
order as wasting padding; fieldalignment -fix reordered it.
Entire-Checkpoint: 28457bdacc88
A single-color gradient effect for segments too narrow for a real
two-stop gradient to render as anything but a solid color:
dark-gradient(#color) runs from that exact color to a darker
shade, light-gradient(#color) to a lighter one.
The shade drops (or raises) HCL lightness only, by a fraction of
the base color's own headroom toward the target end, keeping hue
exact and walking chroma down only as far as the sRGB gamut
forces - blending toward black/white directly pulls chroma down
with it, reading as the color going muddy rather than
deepening/brightening. The reference delta is tuned so a 3-cell
segment - the common case this exists for - lands its shade close
to a just-noticeable-difference rather than a hard color swap; a
width multiplier grows that delta for wider segments as a
saturating curve (not linear growth with a hard clamp - that hit
its clamp by ~15-20 cells and crushed every wider segment to the
exact same near-white/near-black color, which is what "the
gradient stops working on a larger segment" turned out to be),
so a very wide segment still shades further than a moderately
wide one while topping out at a moderate, recognizably-still-the-
same-hue color instead of washing out to white or black.
GradientLastForCells mirrors the same shading (for a given cell
count) on the raw stop text, so powerline separators, diamond
caps, and inline color-override edges (via a new
gradientRenderCells package var in the terminal writer) line up
with the color the body actually ends on for that segment's own
width. GradientLast (no cell count - used by parentBackground/
parentForeground crossing into another segment, and wherever
width isn't known) falls back to the narrowest, gentlest shade.
WithGradientStops rebuilds a gradient string keeping its own
prefix (linear-gradient, dark-gradient, or light-gradient), so
resolving a palette-referenced stop (dark-gradient(p:teal)) keeps
its darken semantics instead of silently becoming a plain
linear-gradient - whole-string palette resolution only ever
expanded a bare "p:name" that itself resolved to a gradient,
never a "p:name" used as one stop inside a gradient literal, so a
palette-referenced stop reached GradientLast unresolved and fell
back to the raw, unshaded base color.
linear-gradient itself goes back to requiring two or more stops;
the single-stop overload it briefly had is now dark-gradient's job.
Entire-Checkpoint: 85522027570b
async: true deferred the entire init script, including the line that
installs the global `prompt` function, to the first prompt draw.
Anything wrapping `prompt` before that draw (e.g. zoxide's directory
hook) got silently overwritten once the deferred script ran, since
PowerShell's global prompt binding is written to unconditionally.
Route the deferred install through a global function pointer variable
instead: the trampoline installed at init time now owns the `prompt`
binding permanently and never gets replaced, so anything wrapping it
keeps a valid reference across the lazy load, re-init, and module
removal.
Fixes#7714
Entire-Checkpoint: 18db4675baa8
CI (PR #7713) failed golangci-lint's whitespace linter on an
unnecessary leading newline right after the slices.Backward loop
header in Resolve. Matrix fail-fast cancelled the macOS/Windows test
legs as a side effect - only this one line was actually broken.
Entire-Checkpoint: 9740999d4333
Two follow-ups to 76ac76d6/cdb4bfe8, found by an independent review:
- keywords.go's Resolve gated parent-keyword resolution on parents !=
nil, not len(parents) != 0. terminal.String() (cdb4bfe8) leaves
ParentColors non-nil-but-empty after the very first block a process
ever renders, so every parentBackground/parentForeground resolved
against an empty stack after that point returned the literal keyword
string instead of Transparent - which then fails color parsing and
renders as no color at all, not even transparent.
- Engine.blockTailColors (76ac76d6) captured a single *color.Set
re-derived from the tail segment's own Resolve*() calls. A segment's
stored color can itself be an unresolved parentBackground keyword
(normal - full resolution happens later against the live stack), so
a one-entry reseed had nowhere further to walk. Capture a full copy
of terminal.ParentColors instead of re-deriving one entry, so a
filler chains through an unresolved tail exactly like the block's
own last segment did.
Entire-Checkpoint: 6582863c12cb
cdb4bfe8 scoped terminal.ParentColors to one block, resetting it in
String() once a block's segments are done rendering. block.Filler (and
the transient prompt's filler) render after that reset - via
shouldFill, called from writeBlock with an already-built blockText -
so a filler template using <parentBackground>/<parentForeground>
started resolving against an empty stack instead of the block's last
segment.
Capture the last segment's colors into a new Engine.blockTailColors
field right before previousActiveSegment resets to nil (same spot in
both renderBlockFromCache and renderBlockSegments), and reseed
terminal.ParentColors with that one entry in shouldFill before writing
the filler. No stock theme uses filler with parent color keywords, so
this was unreachable in practice, but it's a real behavior change from
what shipped in cdb4bfe8.
Entire-Checkpoint: edb9d9774272
The serve daemon reader ($script:StreamingReaderScript) drained stdout
one $stream.ReadByte() call per byte. Fine at the sub-KB record sizes
streaming shipped with, but a gradient-heavy prompt record runs
1-2KB, and a PowerShell method-call per byte to drain it measured at
~9.3us/byte - ~17ms for one cycle's records on this machine, ~47x
slower than a chunked read of the same payload.
PowerShell's prompt function blocks synchronously on one full
render round trip per Enter, and buffered keystrokes each need their
own round trip - so drain time was always queued-Enters x per-Enter
cost. What changed is which side of the ~33ms key-repeat interval
that cost sits on: under it, the queue never grows and releasing
Enter stops the prompt instantly; over it, holding Enter grows an
unbounded backlog that then drains visibly for seconds after release.
Gradients pushed a real theme over that line.
Read in 4KB chunks and split on NUL via [Array]::IndexOf instead of a
per-byte loop. Verified byte-identical output against the old
implementation across chunk-boundary, empty-record, unicode, and
unterminated-trailing-record cases at buffer sizes from 1 to 4096.
Entire-Checkpoint: 1b006db4f6c6
ParentColors accumulated every segment ever rendered across the whole
process lifetime, never reset - harmless for a one-shot render but
quadratic in the pwsh serve daemon, where StreamPrimary keeps calling
Primary() in the same long-lived process. Each render's SetParentColors
also copied the full history on every call, so cost compounded further.
Push onto a real stack instead of prepend-copying, and clear it once
per block in String() where the rest of the per-block render state
already resets. resolveParentColor's walk order flips to match: nearest
ancestor is now the tail, not the head.
BenchmarkEnginePrimary at 64000 iterations: 670µs/op -> 16µs/op, flat
instead of growing with iteration count; SetParentColors accounted for
99% of all allocated bytes in a mem profile before this change.
Entire-Checkpoint: 7b56670e64a2
Segment text (directory names, git commit metadata, environment
variables, command output) can be attacker-controlled and reached the
terminal unfiltered through write(s rune), the only sink Oh My Posh's
own styling never uses. Raw ESC/BEL/CSI/OSC bytes let an attacker
inject terminal escape sequences (title spoofing, OSC 52 clipboard
writes) via a malicious directory name or git commit subject.
GHSA-fwjx-9p69-h25h
Entire-Checkpoint: 65f209a5fba9
template.Render's func map exposed cmd/readFile/stat/glob/env/expandenv
to any string passed as the template argument, with no way to tell a
template the user authored in their config from one built at runtime
out of external data (filesystem names, command output, ...). That
distinction is exactly what let a malicious folder name reach cmd
(fixed for that one call site in a prior commit); nothing stopped the
same class of bug from reappearing at a future call site.
Split Render into two explicitly named functions instead: RenderTrusted
keeps the full func map and is for template text read verbatim from a
config field (segment/block/palette templates, mapped_locations keys,
folder_separator_template, ...) — every existing call site converts to
it. RenderUntrusted drops cmd/readFile/stat/glob/env/expandenv and is
for text that may contain or be composed from runtime data; pt.Path in
path.go's setStyle(), the one sink that re-renders a string composed
from raw filesystem folder names, uses it. Neither name is shorter or
more "default" than the other, so there's no ambient plain Render a
future call site could reach for without first deciding which one it
means. The parsed-template cache key includes the trust level so a
trusted and an untrusted render of identical text can never share a
cached *template.Template and its func map.
Entire-Checkpoint: 597e5a60d968
Foreground and background accept linear-gradient(stop, stop, ...) with
hex or palette-reference stops, interpolated per visible cell in HCL
space and emitted as truecolor escapes (256-color fallback when the
terminal lacks truecolor support). Powerline separators, diamond caps,
and parentBackground/parentForeground collapse to the matching gradient
edge so adjacent glyphs connect with the correct color.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: d84718082c10
TestGetAnsiFromColorString mutated the package-level TrueColor flag
without restoring it, leaking false into every test that ran after it
and making the package order-dependent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 03c5c3c43975
Add a separate e2e Go module that validates the shell integrations
against real shells instead of only asserting generated script text:
- layer 1: generate the init script per shell and feature-config
overlay (base, transient, rprompt, tooltips, full) and validate it
with the shell's own parser
- layer 2: boot each shell interactively in a pty (ConPTY on Windows,
creack/pty elsewhere) with a vt10x screen emulator, assert the
prompt renders and the session exits cleanly
- layer 3: feature scenarios asserting exit-code propagation,
transient prompt replacement, and rprompt right-alignment
Covers bash, zsh, fish, pwsh, and nu; tests skip cleanly when a shell
binary is absent or the platform cannot drive it faithfully. The
harness answers PSReadLine's DSR cursor queries on Unix ptys and
isolates sessions from the host's cache and nu vendor autoloads.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 8a75c4edb40b
Built on the ConfiguredLanguage engine, this exposes it directly to
users: a `type: "language"` segment configured with a `name` and a
`tools` list (executable, args, regex, version_url_template) needs
no dedicated Go file at all. Intended for one-off or less common
tools that don't warrant a built-in segment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Entire-Checkpoint: 7daa163ced91
Most language segments were ~20-30 line Go files that only declared
file extensions, an executable name, and a version regex before
delegating to the shared Language base. Every new simple language
(e.g. the recent GCC PR) meant another one-off struct, test file,
and registration to review and maintain.
Replace fortran, ruby, clojure, crystal, elixir, julia, kotlin, lua,
nim, ocaml, perl, php, r, rust, swift, v, vala, zig, and dart with a
single ConfiguredLanguage engine driven by a built-in preset table.
Public config surface (segment type strings, all existing options)
is unchanged; this is purely an internal consolidation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Entire-Checkpoint: 6d449f3da816
Fish already exposes omp_repaint_prompt for scripts that need the
prompt re-rendered without running a command (theme reloads, custom
key bindings). Bring the same capability to zsh (omp_repaint_prompt,
also registered as a zle widget for bindkey) and PowerShell
(Invoke-PoshPromptRepaint).
Closes#7427
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: f87bb49a5f6b
Render palette values containing a Go template at resolution time,
with access to the global template properties (.Segments, .Env, ...).
A template can resolve to any color value, including another palette
reference, which then follows the existing recursive resolution and
its depth limit. Rendering is guarded by a cheap substring check so
static palettes are unaffected.
Closes#7572
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: e29966ae2d7f
Shells already report whether a command ran before the current prompt
via --no-status, but templates could not access it, so segments kept
showing the previous exit code after pressing enter on an empty line.
Expose the flag as the global .Executed boolean so templates can hide
stale command metadata, and route it through the data file replay with
the same flag precedence as the other env keys.
Closes#7679
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 0dcae3896550
merge() treated a bool/int field the source never mentioned the same
as one explicitly set to its zero value, so extending a config could
silently reset a base config's true/nonzero field back to false/0.
Uint/float fields had the opposite bug: an explicit zero override was
ignored. Both are fixed by tracking which keys were actually present
in the decoded source and consulting that during merge, falling back
to the previous behavior for configs built without going through the
decoder (e.g. existing tests), so no existing merge semantics change.
Entire-Checkpoint: 166050784b12
The legacy mapped_locations regex path replaced the first textual
occurrence of the captured substring anywhere in the path, not the
occurrence the regex actually matched, so a path with the captured
text appearing twice (e.g. re:.*/(src)/deep against
/home/src/project/src/deep) rewrote the wrong copy.
Entire-Checkpoint: de9bd63bb3e0
The color cycle is shared across every block in render order, so a
block always continued from wherever the previous block's segment
count left the rotation. restart_cycle re-points a block at the
cycle's first color, useful for a second prompt line that should
always start fresh.
Closes#5442
Entire-Checkpoint: cdb5e9d96343
A re: mapped_locations value could only reference the first capture
group, with the rest of the matched path left untouched. This adds an
opt-in mode that expands $1, $2, and ${name} references against the
full regex match, so a value can be built from multiple capture
groups. Default behavior is unchanged.
Closes#6334
Entire-Checkpoint: 116e27a41b09
Multi-level extends chains already resolved correctly, but a circular
chain (A extends B extends A) hung forever instead of erroring, and a
relative extends path inside a non-top-level config resolved against
the first config's directory instead of its own.
Closes#7531
Entire-Checkpoint: 352a31511f53
When a directory has files for multiple project types (e.g. both
package.json and composer.json), the fixed detection order always
picked the same one with no way to change it.
Closes#7594
Entire-Checkpoint: 3fc26ec17f3e
goreleaser's --clean flag wipes the dist directory before building,
deleting the deps tarball that was written there beforehand. Move the
packaging step after goreleaser so the tarball survives into the
uploaded release artifacts.
Fixes#7668
Entire-Checkpoint: d7ecded13dbf
Extend the OSC 133;C emission with the just-submitted command line,
percent-encoded as kitty's cmdline_url= shell-integration extension.
Terminals and multiplexers no longer need process-tree inspection to
know what command a pane runs - a primitive Windows lacks entirely.
Encoding happens in-shell with builtins only, so no extra process
spawn per prompt; terminals that ignore the parameter still see the
bare mark.
Closes#7536
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 7f155fd83428
Vale CI lints AGENTS.md and .agents/skills at error level, while
markdownlint-cli2 never matches dot-directories - skill docs pass one
gate invisibly and fail the other locally. Capture that in the
project-knowledge codebase topic so the next docs change checks the
right gates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 9040f7c63ee0
The Vale workflow still pointed at .github/skills, which moved to
.agents/skills - the run failed on the missing path. Point it at the
new location and resolve the error-level findings that surfaces in the
project-knowledge skill: reword three pwsh.md entries and allow
"dynamic scoping" in zsh.md, the proper name of the zsh feature,
via a file-scoped override like the existing ones.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: a1dfa73dfc62
AGENTS.md and .github/copilot-instructions.md drifted apart, each
holding sections the other lacked. Merge the Copilot file's richer
content (project overview, key commands, exploration rule, src/ layout,
shell integration, CLI commands, caching guardrail) into AGENTS.md and
reduce the Copilot file to a pointer, so guidance lives in one place
for every tool.
Also corrects the segment interface name (SegmentWriter, per
src/config/segment_types.go) and the artifact count (five, the list
already said so).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 46ce85fd9728
.agents/skills is the vendor-neutral Agent Skills location that Copilot,
Codex, Claude Code, and most other agents discover automatically, while
.github/skills is read by Copilot alone. Move the embedded skills there
and add project-knowledge, a committed per-topic memory of verified
gotchas (codebase, shells, terminal, testing).
The directory stays gitignored for apm-managed skills; only the three
embedded skills are allowlisted, mirroring the former .github/skills
pattern. AGENTS.md now tells agents to read the relevant topic before
starting work and to commit new findings alongside the change they
relate to.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 70fe8f468dbe
zsh-vi-mode wraps the zle-line-init widget, so its own line-init ran
only after .recursive-edit had consumed the entire editing session.
Every line accepted or interrupted from normal mode then left ZVM_MODE
out of sync with the active keymap, and zvm_select_vi_mode's same-mode
early return made the desync permanent: ESC could never switch back to
normal mode.
Run zvm_zle-line-init up front so keymap and mode bookkeeping are
aligned before editing starts, and shadow zvm_widget_wrapper's rawfunc
local so zvm_reset_prompt cannot resolve it dynamically and re-enter
this widget.
Fixes#5992
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 9c904da30dbe