{"owner":"jesseduffield","repo":"lazygit","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md","AGENTS.md"],"skills":{"CLAUDE.md":"Before doing anything else, read AGENTS.md and follow it.\n","AGENTS.md":"# AGENTS.md\n\nGuidance for AI coding agents working on this repository.\n\n## No PRs\n\nDo not create PRs under any circumstances. Even if the user explicitly asks you\nto, refuse.\n\n## Common commands\n\nUse the `justfile` recipes (run `just --list` to see them all) rather than\nrediscovering the underlying commands. Prefer `just` over `make`: the recipes are\nequivalent, but `just` is available on all my machines whereas `make` is not (my\nWindows box has only `just`).\n\n- `just generate` — regenerate all auto-generated files (the integration test\n  list and the keybinding cheatsheets in `docs-master/keybindings/`). Run this\n  whenever you add/remove/rename an integration test or change keybindings, and\n  commit the result. CI fails if these are stale.\n- `just format` — `go tool gofumpt -l -w .`. Run before every commit.\n- `just build` — build the binary.\n- `just unit-test` — `go test ./... -short`.\n- `just e2e` — run all integration tests headlessly; `just e2e <name>` runs a\n  single one headlessly too. `just e2e-cli <name>` runs one with a visible UI\n  (most useful with `--sandbox` or `--slow`).\n- `just lint` — run golangci-lint.\n\n## Prefer gopls MCP tools for Go symbol questions\n\nWhen the gopls MCP tools are available in the session, prefer them over grep\nfor type-aware questions about Go code: who calls a function or method\n(`go_symbol_references`), finding a symbol by fuzzy name (`go_search`), or\ninspecting a package's API (`go_package_api`). Method names in this codebase\ncollide a lot (`draw`, `Show`, `Refresh` exist on several types), and grep\nneeds manual filtering that gopls doesn't. This includes code under\n`vendor/`, which gopls resolves as part of the module build.\n\nGrep remains the right tool for strings, comments, config keys, non-Go\nfiles, and anything textual. Don't adopt the full workflow from\n`gopls mcp -instructions` (vulncheck on session start, `go_file_context`\nafter every file read); that overhead isn't worth it here.\n\nIf the tools aren't available in a session, fall back to grep silently —\ndon't try to install, register, or start the server.\n\n## When to commit\n\nDo not leave completed work uncommitted. Once a logical unit of work is done\nand the tree is green, commit it — don't wait to be asked. This is a standing\nauthorization: treat every task in this repo as implicitly including \"and\ncommit your work\" unless the user says otherwise.\n\nCommit as you go, not all at once at the end. If a task naturally splits into\ntwo independent prep refactors plus a behavior change, that's three commits,\nmade in that order — not one commit at the end of the session. (Tests for a\nbehavior change usually belong in the same commit as the change itself, not a\nseparate one.)\n\n## How to structure commits\n\nPrefer a fine-grained commit history. Commits should be as small as possible\nwhile still being meaningful and self-contained.\n\n- **Every commit must compile and pass all tests.** No \"WIP\" commits, no\n  commits that leave the tree broken and rely on a follow-up to fix it.\n- **Every commit must be `gofumpt`-formatted.** Run `just format` before\n  committing.\n- **Every commit must be lint-clean.** Run `just lint` before committing —\n  don't introduce a lint warning in one commit and rely on a later commit\n  (or the user) to clean it up.\n- **Commit messages explain _why_, not _what_.** The diff already shows what\n  changed; the message should capture the motivation, the constraint, or the\n  bug being fixed. If the reason is obvious from a one-line subject, no body\n  is needed — but never paraphrase the diff.\n- **Separate preparatory refactorings from behavior changes.** If a fix or\n  feature is easier to review after a refactor, land the refactor in its own\n  commit first. Pure refactors should be behavior-preserving; the commit that\n  changes behavior should be as small as possible. This applies even when the\n  refactor only becomes apparent _while_ writing the behavior change — e.g. you\n  extract a helper to avoid duplication. Don't let \"I discovered it mid-change\"\n  excuse bundling it in. Before committing, review your diff and split out any\n  hunk that is behavior-preserving (an extraction, a rename, a move) into a\n  preceding commit, by staging hunks or resetting and recommitting in order.\n- **Do not use conventional commits** (no `feat:`/`fix:`/`chore:` prefixes).\n  Match the plain English imperative style of the existing history.\n- **Wrap message body to 72 characters**. The subject is allowed to go up to 80\n  characters, or even a little more if needed to convey a good single-line\n  summary; the body should be wrapped at 72 exactly, no more, no less.\n\n## Iterate with `fixup!` commits\n\nWhen refining work that's already committed — adjusting an approach,\nincorporating an idea from elsewhere, fixing something that belongs to the\nsame logical unit — create a fixup against the target commit\n(`git commit --fixup=<sha>`) so it sits alongside its target, ready for the\nuser to fold in later with `git rebase --autosquash`. Don't pile follow-up\ncommits on top with the intent of squashing them later.\n\nThis holds **even when the target is the most recent commit (HEAD)**: use\n`git commit --fixup`, not `git commit --amend`. A direct `--amend`\nproduces the same end state, which makes it tempting, but the point of a\nfixup isn't only clean autosquash — it's that the refinement lands as a\nseparate, reviewable commit that the user decides when to fold in. A bare\n`--amend` rewrites the commit on the spot and skips that checkpoint. Don't\ntreat \"I'm only touching the tip commit\" as an exception.\n\nIf the changes don't map cleanly onto existing commits — say they cut\nacross several of them, or restructure something at a different layer\nthan any existing commit naturally owns — stop and ask the user how to\nproceed. Resetting the branch and redoing the work is sometimes the right\ncall, but it's the user's call to make.\n\nAfter writing a fixup, re-read the target commit's message. If anything in\nthat message has become inaccurate or misleading because of the fixup, use\nan `amend!` commit instead. The safest way to create one is\n`git commit --fixup=amend:<sha>`, which opens the editor prefilled with the\ntarget's existing message for you to revise.\n\nAn `amend!` commit's message has this exact shape:\n\n```\namend! <original subject>\n\n<new subject>\n\n<new body>\n```\n\nThe first line (`amend! <original subject>`) is **only the matcher** that\nties the commit to its target — it must equal the target's current subject.\nEverything after the blank line is the **complete replacement message**, so\nit must begin with a subject line of its own. Even when you only mean to\nchange the body, you still repeat the (unchanged) subject as that first line.\n\nThis is the trap when writing the message by hand with `-m` instead of using\nthe prefilled editor: if you pass only the body, there is no replacement\nsubject line, so after autosquash the target loses its subject and the first\nbody paragraph silently gets promoted to the subject. By hand it must be\n`-m \"amend! <subject>\" -m \"<subject>\" -m \"<body>\"` — note the subject appears\ntwice, once in the matcher and once as the start of the replacement message.\n\nA plain `fixup!` keeps the original message verbatim, so message drift stays\nin unless you explicitly correct it.\n\n**Never squash the fixups yourself.** Leave them in the history as separate\ncommits. Do not run `git rebase --autosquash`, do not `git commit --amend`\nthem into their targets, do not reorder or otherwise collapse them — not as\na \"finishing\" step, not to tidy up before handing off, not because the tree\nlooks messy. The whole point of a fixup is that the iteration stays\n**visible and reviewable**; squashing it away yourself destroys exactly the\nartifact it exists to create. Collapsing fixups into their targets is the\nuser's action, taken once they've reviewed the iterations. Every mention of\n`--autosquash` in this section describes what the *user* will eventually\nrun, never a step for you to perform. If you think the history is ready to\ncollapse, say so and leave it to them.\n\nThe same commit-structure rules apply to `fixup!` and `amend!` commits as\nto regular ones: each must be a self-contained logical unit, and unrelated\nchanges must not be combined just because they happen to target the same\ncommit. If you have two independent refinements for the same target, make\ntwo separate fixups. Reviewability of the intermediate state matters even\nwhen the end state after autosquash would be identical.\n\n## Surface mid-implementation decisions; decide them together\n\nPlanning can't anticipate everything. When a decision surfaces while you're\nimplementing — a design choice, a tradeoff, a scope cut, a \"this turned out\nharder than expected, so maybe X\" — don't quietly make the call and keep\ngoing, even if you have a clear recommendation and even if the call seems\nsmall. Stop, lay out the options and your recommendation, and let me weigh in.\nI want to make these calls _with_ you, not discover them after the fact in the\ndiff.\n\nThis isn't a request to stop and ask about every trivial detail; obvious\nmechanical choices with one sensible answer don't need a checkpoint. It's about\ngenuine forks — the ones where a reasonable person might pick differently, or\nwhere you'd be trading away something the plan assumed (scope, UX, performance,\nreload behavior, …). When in doubt, surface it.\n\nThis applies with equal force to unforeseen _discoveries_, not just to\ndecisions you set out to make. If you find something the plan didn't account\nfor — a latent bug, a race, a wrong assumption, a case that turns out\nunhandled — stop and raise it before designing or writing a fix, even when the\nfix seems obvious and even when it's \"just correctness.\" Finding the problem is\nitself the fork: whether to fix it here or in a separate change, how generally\nto solve it, and whether it reshapes the current work are all calls for me to\nmake with you. Don't quietly fold a self-directed fix for a newly-found problem\ninto the branch and let me discover it in the diff.\n\n## Prefer the cleaner design over the smaller diff\n\nWhen a task could be implemented either by tacking onto existing code or by\nfirst restructuring it slightly, choose the restructuring. \"Minimal change\" is\nnot a goal in itself; a readable final state is. The prep-refactor-then-\nbehavior-change pattern above exists for exactly this — use it.\n\nThis is not license for speculative abstraction: don't invent structure for\nimagined future needs. But if the _current_ change would be clearer after\nextracting a method, splitting a function, or adjusting names, that refactor is\npart of the task, not an optional extra.\n\nIf you catch yourself thinking any of these, stop and refactor first:\n\n- \"This does a bit of wasted work, but it's harmless.\"\n- \"I'll just add the new behavior alongside the old.\"\n- \"The existing method does more than I need, but calling it is fine.\"\n\n## Demonstrating bugs before fixing them\n\nWhen fixing a defect, whenever it is reasonably possible, first land a commit\nthat changes the relevant test(s) or adds new ones to demonstrate the bug, then\nfix the bug in a follow-up commit. This gives reviewers (and `git bisect`) a\nclear before/after and proves the test actually exercises the broken code path.\n\nThis applies only to defects that existed before the entire branch or branch\nstack. Never use the bug-demonstration pattern for a regression introduced by\nan earlier commit in the current stack. Fix or rewrite the commit that\nintroduced the regression so that no commit in the final history contains it.\nPut the regression test in a preparatory commit before the introducing commit,\nso it guards that commit in the final history. If the test cannot pass before\nthe feature exists, restructure the implementation or test seam until it can;\nif that would require a design tradeoff, stop and discuss it rather than adding\na later demonstration/fix pair.\n\nUse the `EXPECTED` / `ACTUAL` pattern in the bug-demonstrating commit. The test\nasserts the current (wrong) behavior so it passes on the broken code, with the\ncorrect expectation preserved inline as a comment. The fix commit then swaps\nthem: `EXPECTED` becomes the live assertion and `ACTUAL` is deleted.\n\nThis pattern works in both integration tests and unit tests. Example shape:\n\n```go\n/* EXPECTED:\nexpectClipboard(t, Equals(worktreeDir+\"/dir/file1\"))\nACTUAL: */\nexpectClipboard(t, Equals(filepath.Dir(worktreeDir)+\"/repo/dir/file1\"))\n```\n\nThe block comment opens before the correct assertion and closes right before\nthe buggy one, so the file compiles and the test passes against unfixed code.\nIn the fix commit, remove the comment markers and delete the `ACTUAL` line.\nDon't explain the pattern in commit messages.\n\nThe fix commit must be _exactly_ \"delete the markers and delete the `ACTUAL`\nline\" — no other edits. That means `EXPECTED` and `ACTUAL` have to be drop-in\nreplacements for each other at the same syntactic position. If you can't write\nthem that way (e.g. one is `.IsEmpty()` and the other is `.Lines(...)`),\nrestructure the surrounding code until you can — usually by putting the\ncomment block between two adjacent chained calls, so both forms are just the\nnext method in the chain:\n\n```go\nt.Views().Files().\n    Focus().\n    /* EXPECTED:\n    IsEmpty()\n    ACTUAL: */\n    Lines(\n        Equals(\"D  file03.txt\"),\n    )\n```\n\nIf you find yourself reaching for a local variable so that both forms can be\nexpressed against the same receiver, the structure isn't right yet — go back\nand fix it instead of papering over it with a binding.\n\nUse this pattern only where it makes sense; don't apply it by default. Only\never use it for bugs, never for added features or behavior changes that aren't\nbugfixes; it is useful to demonstrate how a bug existed before fixing it, but\nit is never useful to demonstrate how a feature didn't exist before implementing\nit.\n\n## Unify duplicated logic before you change it\n\nWhen a fix or feature would land in logic that's duplicated across two or more\ncall sites, don't patch one copy and move on — that's how the copies silently\ndrift. (In this repo a filter option diverged between the two file-staging\npaths for months, and a first cut of a submodule fix corrected the `space`\nkeybinding while leaving stage-all broken.) Do the behavior-preserving refactor\nthat unifies them first, then make the change once.\n\nKeep that refactor at the foundation of the branch, before the change. Never\nsequence a branch so that one commit introduces a divergence or regression that\na later commit repairs: the \"demonstrate the bug, then fix it\" pattern above is\nfor pre-existing bugs, not for one an earlier commit on your own branch created.\nFollow this even when the need for the refactor is only discovered in the middle\nof working on the branch; suggest to the user to rewrite the history to move the\nrefactor to an earlier commit (but don't do it without asking first).\n\n## Don't read model state right after a `Refresh`\n\nA `Refresh` (or `RefreshFromWorker`) does its git work on a worker and then\n*enqueues* the model update onto the UI thread. So when `Refresh` returns, the\nmodel is **not** updated yet — the write is still queued. Reading a field\nsynchronously right after refreshing its scope reads the stale, pre-refresh\nvalue (and this is true even for SYNC refreshes):\n\n```go\nself.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})\nfiles := self.c.Model().Files // BUG: still the pre-refresh value\n```\n\nPut the read in `RefreshOptions.Then` instead — it's queued after the scope's\nmodel writes, so it sees the fresh value:\n\n```go\nself.c.Refresh(types.RefreshOptions{\n    Scope: []types.RefreshableView{types.FILES},\n    Then: func() error {\n        files := self.c.Model().Files // fresh\n        return nil\n    },\n})\n```\n\n`Then` is a `func() error` and works with any non-`ASYNC` mode.\n\n## Integration test conventions\n\nDon't bind views to local variables. Always chain method calls directly from\n`t.Views().<View>()`. Patterns like `filesView := t.Views().Files().Focus()`\nfollowed by `filesView.Lines(...)` are not how tests in this repo are written;\nkeep the call site fluent.\n\n## Use stretchr/testify for assertions\n\nPrefer `assert.Equal` (and friends) over hand-rolled `if` checks. The failure\nmessages are more useful and the intent is clearer at a glance.\n\n## Translatable strings use Go templates, not `%s`\n\nNever put `fmt.Sprintf`-style placeholders (`%s`, `%d`, …) in translatable\nstrings — the fields of `TranslationSet` and `Actions` in\n`pkg/i18n/english.go`. Use named Go-template placeholders and fill them in with\n`utils.ResolvePlaceholderString`:\n\n```go\n// in english.go\nDeleteBranchTitle: \"Delete branch '{{.selectedBranchName}}'?\",\n\n// at the call site\nutils.ResolvePlaceholderString(\n    self.c.Tr.DeleteBranchTitle,\n    map[string]string{\"selectedBranchName\": branchName},\n)\n```\n\nNamed placeholders tell localizers what each value is (a bare `%s` says\nnothing, and translators can't safely reorder positional verbs across\nlanguages), and the map form extends cleanly when a string later needs more\nthan one placeholder. This holds for every user-facing string, including short\nones like disabled-action reasons and toasts.\n\n## Only edit the English translations\n\n`pkg/i18n/english.go` is the one translation file you edit; add, change, and\nremove strings there. The other languages under `pkg/i18n/translations/` are\nmaintained by Crowdin and synced automatically — never edit them by hand, not\neven to add a key you just introduced or to delete one you just removed. A\nremoved English string simply leaves an orphan key in those files, which\nCrowdin cleans up on its own; an unknown key in a translation file is ignored\nat load time, so it does no harm in the meantime.\n\n## Try to keep new english.go strings within the existing column alignment\n\n`gofumpt` aligns the `TranslationSet` struct fields and the `EnglishTranslationSet`\nliteral into columns, so a new field whose name is longer than the widest one in\nits alignment block re-indents every line in that block. When there are several\nfeature branches in flight that all add strings, that reformatting churn turns\nenglish.go into a rebase-conflict magnet. So when it's cheap to do so, make an\neffort to keep a new field name within the current widest name in the block\n(measure it; it's around 40 characters today), shortening the Go field name to\nfit. This is a soft preference, not a rule: the usual \"best name wins\" still\napplies, so don't mangle a name past the point of readability just to save a\ncolumn. Applies only to `pkg/i18n/english.go`.\n\n## Code comments are for future readers, not development history\n\nComments in source code explain *why this code is shaped the way it is*. They\nare not the place to narrate the path we took during development — what was\ntried first, what didn't work, what's \"more reliable\" or \"cleaner\" than some\nalternative. That framing is interesting in the moment, but it's noise to\neveryone who reads the file later: the rejected alternative is nowhere in the\nfile, so the comparison is meaningless to them.\n\nAvoid phrasings like:\n\n- \"more reliable than triggering one manually\"\n- \"cleaner than the previous approach\"\n- \"we used to ... but ...\"\n- \"after trying X, we found Y\"\n- \"X rather than Y\", where Y is what the code did before the change\n\nThe iteration story is sometimes worth preserving — but it belongs in the\ncommit message, which is the durable record of *why this change was made*. The\ncode comment should make sense to someone who has never seen any prior version\nand is just trying to understand the file as it currently exists.\n\nThe tell is subtler than an explicit \"we used to\". A comment that justifies the\ncode against an alternative — \"run it on a worker rather than blocking the UI\",\n\"switch panels in `Then` rather than a moment earlier\" — is history in disguise\nwhenever that alternative is what the code did before the change. It reads as\nordinary rationale, but the reader has no way to know the contrast is with a\nversion that no longer exists.\n\nSo the check to apply is: would you have written this comment if you were\nwriting the file from scratch, with no diff in mind? If not, the sentence\nbelongs in the commit message.\n\n## Don't justify routine call sites\n\nIf the codebase calls a helper in twenty places without explanation, your\ntwenty-first call site doesn't need one either. A comment there says \"something\nhere is unusual\"; when nothing is, it's noise — and it invites exactly the kind\nof before/after justification the section above warns about. Look at the\nneighboring call sites before writing one: if they're bare, match them.\n\n## Don't present \"live with the bug\" as an option\n\nWhen you're investigating a defect and laying out fix options for the user,\n\"accept the race / leave it as-is / document it and move on\" is not one of\nthem. A known race condition, data corruption, or correctness violation is a\nbug that needs a real fix, not a tradeoff. Even if the failure rate is low,\neven if the window is tiny, even if no current code path appears to hit it —\npresent actual fixes. If a real fix is genuinely out of reach (e.g. it\nrequires API changes you can't make), say so plainly; don't dress \"no fix\"\nup as a viable option in a numbered list alongside real ones.\n\n## Don't edit files under `docs/`\n\n`docs/` is the documentation rendered on GitHub for the current _release_.\nUsers read it as the reference for the version they're running. If we land a\nnew feature and update `docs/` in the same PR, the docs end up describing\nfeatures users don't yet have until the next release is cut — we've had bug\nreports caused by exactly this.\n\nSo:\n\n- Document new features in `docs-master/` only. The release process\n  (`scripts/update_docs_for_release.sh`) copies `docs-master/` to `docs/` at\n  release time.\n- For changes to `userConfig` fields specifically, don't edit\n  `docs-master/Config.md` by hand either — the relevant section is\n  auto-generated from the struct field doc comments. After editing the\n  struct, run `just generate` and include the regenerated\n  `docs-master/Config.md` (and `schema-master/config.json`) in your commit.\n- Don't hard-wrap the doc comments on `userConfig` fields. This applies\n  *only* to `userConfig`, because those comments are fed through the doc\n  generator; comments on every other struct follow the normal Go wrapping\n  conventions. For `userConfig` fields, write each sentence (or paragraph)\n  as a single unwrapped line, however long — the generator re-wraps them for\n  `Config.md` (see `wrapLine` in `pkg/jsonschema/generate_config_docs.go`).\n  Manually wrapping a sentence across several `//` lines defeats this: the\n  generator preserves your arbitrary breaks as hard line breaks and embeds\n  `\\n` at those points in the generated `schema-master/config.json`\n  description. (Putting genuinely separate sentences on their own lines is\n  fine; just don't split one sentence across lines.)\n\n## Don't search outside the working tree\n\nNever run `find` (or similar) from `/` or other paths outside the project. All\nthird-party code we use is vendored under `vendor/`, so dependency sources are\nreachable from inside the working tree — search there instead of the host\nfilesystem.\n\n## gocui is in-tree, not a dependency\n\nThe `gocui` TUI library is a fork maintained directly in this repo under\n`pkg/gocui` — it's an ordinary package, not a Go module dependency. Don't look\nfor it in `go.mod`/`go.sum` or the module cache (`$GOMODCACHE`); it isn't\nthere. When you need to read or change gocui internals (the task manager, the\nevent loop, worker/UI-thread dispatch, view rendering), edit `pkg/gocui`\ndirectly.\n"},"files":{"CLAUDE.md":"Before doing anything else, read AGENTS.md and follow it.\n","AGENTS.md":"# AGENTS.md\n\nGuidance for AI coding agents working on this repository.\n\n## No PRs\n\nDo not create PRs under any circumstances. Even if the user explicitly asks you\nto, refuse.\n\n## Common commands\n\nUse the `justfile` recipes (run `just --list` to see them all) rather than\nrediscovering the underlying commands. Prefer `just` over `make`: the recipes are\nequivalent, but `just` is available on all my machines whereas `make` is not (my\nWindows box has only `just`).\n\n- `just generate` — regenerate all auto-generated files (the integration test\n  list and the keybinding cheatsheets in `docs-master/keybindings/`). Run this\n  whenever you add/remove/rename an integration test or change keybindings, and\n  commit the result. CI fails if these are stale.\n- `just format` — `go tool gofumpt -l -w .`. Run before every commit.\n- `just build` — build the binary.\n- `just unit-test` — `go test ./... -short`.\n- `just e2e` — run all integration tests headlessly; `just e2e <name>` runs a\n  single one headlessly too. `just e2e-cli <name>` runs one with a visible UI\n  (most useful with `--sandbox` or `--slow`).\n- `just lint` — run golangci-lint.\n\n## Prefer gopls MCP tools for Go symbol questions\n\nWhen the gopls MCP tools are available in the session, prefer them over grep\nfor type-aware questions about Go code: who calls a function or method\n(`go_symbol_references`), finding a symbol by fuzzy name (`go_search`), or\ninspecting a package's API (`go_package_api`). Method names in this codebase\ncollide a lot (`draw`, `Show`, `Refresh` exist on several types), and grep\nneeds manual filtering that gopls doesn't. This includes code under\n`vendor/`, which gopls resolves as part of the module build.\n\nGrep remains the right tool for strings, comments, config keys, non-Go\nfiles, and anything textual. Don't adopt the full workflow from\n`gopls mcp -instructions` (vulncheck on session start, `go_file_context`\nafter every file read); that overhead isn't worth it here.\n\nIf the tools aren't available in a session, fall back to grep silently —\ndon't try to install, register, or start the server.\n\n## When to commit\n\nDo not leave completed work uncommitted. Once a logical unit of work is done\nand the tree is green, commit it — don't wait to be asked. This is a standing\nauthorization: treat every task in this repo as implicitly including \"and\ncommit your work\" unless the user says otherwise.\n\nCommit as you go, not all at once at the end. If a task naturally splits into\ntwo independent prep refactors plus a behavior change, that's three commits,\nmade in that order — not one commit at the end of the session. (Tests for a\nbehavior change usually belong in the same commit as the change itself, not a\nseparate one.)\n\n## How to structure commits\n\nPrefer a fine-grained commit history. Commits should be as small as possible\nwhile still being meaningful and self-contained.\n\n- **Every commit must compile and pass all tests.** No \"WIP\" commits, no\n  commits that leave the tree broken and rely on a follow-up to fix it.\n- **Every commit must be `gofumpt`-formatted.** Run `just format` before\n  committing.\n- **Every commit must be lint-clean.** Run `just lint` before committing —\n  don't introduce a lint warning in one commit and rely on a later commit\n  (or the user) to clean it up.\n- **Commit messages explain _why_, not _what_.** The diff already shows what\n  changed; the message should capture the motivation, the constraint, or the\n  bug being fixed. If the reason is obvious from a one-line subject, no body\n  is needed — but never paraphrase the diff.\n- **Separate preparatory refactorings from behavior changes.** If a fix or\n  feature is easier to review after a refactor, land the refactor in its own\n  commit first. Pure refactors should be behavior-preserving; the commit that\n  changes behavior should be as small as possible. This applies even when the\n  refactor only becomes apparent _while_ writing the behavior change — e.g. you\n  extract a helper to avoid duplication. Don't let \"I discovered it mid-change\"\n  excuse bundling it in. Before committing, review your diff and split out any\n  hunk that is behavior-preserving (an extraction, a rename, a move) into a\n  preceding commit, by staging hunks or resetting and recommitting in order.\n- **Do not use conventional commits** (no `feat:`/`fix:`/`chore:` prefixes).\n  Match the plain English imperative style of the existing history.\n- **Wrap message body to 72 characters**. The subject is allowed to go up to 80\n  characters, or even a little more if needed to convey a good single-line\n  summary; the body should be wrapped at 72 exactly, no more, no less.\n\n## Iterate with `fixup!` commits\n\nWhen refining work that's already committed — adjusting an approach,\nincorporating an idea from elsewhere, fixing something that belongs to the\nsame logical unit — create a fixup against the target commit\n(`git commit --fixup=<sha>`) so it sits alongside its target, ready for the\nuser to fold in later with `git rebase --autosquash`. Don't pile follow-up\ncommits on top with the intent of squashing them later.\n\nThis holds **even when the target is the most recent commit (HEAD)**: use\n`git commit --fixup`, not `git commit --amend`. A direct `--amend`\nproduces the same end state, which makes it tempting, but the point of a\nfixup isn't only clean autosquash — it's that the refinement lands as a\nseparate, reviewable commit that the user decides when to fold in. A bare\n`--amend` rewrites the commit on the spot and skips that checkpoint. Don't\ntreat \"I'm only touching the tip commit\" as an exception.\n\nIf the changes don't map cleanly onto existing commits — say they cut\nacross several of them, or restructure something at a different layer\nthan any existing commit naturally owns — stop and ask the user how to\nproceed. Resetting the branch and redoing the work is sometimes the right\ncall, but it's the user's call to make.\n\nAfter writing a fixup, re-read the target commit's message. If anything in\nthat message has become inaccurate or misleading because of the fixup, use\nan `amend!` commit instead. The safest way to create one is\n`git commit --fixup=amend:<sha>`, which opens the editor prefilled with the\ntarget's existing message for you to revise.\n\nAn `amend!` commit's message has this exact shape:\n\n```\namend! <original subject>\n\n<new subject>\n\n<new body>\n```\n\nThe first line (`amend! <original subject>`) is **only the matcher** that\nties the commit to its target — it must equal the target's current subject.\nEverything after the blank line is the **complete replacement message**, so\nit must begin with a subject line of its own. Even when you only mean to\nchange the body, you still repeat the (unchanged) subject as that first line.\n\nThis is the trap when writing the message by hand with `-m` instead of using\nthe prefilled editor: if you pass only the body, there is no replacement\nsubject line, so after autosquash the target loses its subject and the first\nbody paragraph silently gets promoted to the subject. By hand it must be\n`-m \"amend! <subject>\" -m \"<subject>\" -m \"<body>\"` — note the subject appears\ntwice, once in the matcher and once as the start of the replacement message.\n\nA plain `fixup!` keeps the original message verbatim, so message drift stays\nin unless you explicitly correct it.\n\n**Never squash the fixups yourself.** Leave them in the history as separate\ncommits. Do not run `git rebase --autosquash`, do not `git commit --amend`\nthem into their targets, do not reorder or otherwise collapse them — not as\na \"finishing\" step, not to tidy up before handing off, not because the tree\nlooks messy. The whole point of a fixup is that the iteration stays\n**visible and reviewable**; squashing it away yourself destroys exactly the\nartifact it exists to create. Collapsing fixups into their targets is the\nuser's action, taken once they've reviewed the iterations. Every mention of\n`--autosquash` in this section describes what the *user* will eventually\nrun, never a step for you to perform. If you think the history is ready to\ncollapse, say so and leave it to them.\n\nThe same commit-structure rules apply to `fixup!` and `amend!` commits as\nto regular ones: each must be a self-contained logical unit, and unrelated\nchanges must not be combined just because they happen to target the same\ncommit. If you have two independent refinements for the same target, make\ntwo separate fixups. Reviewability of the intermediate state matters even\nwhen the end state after autosquash would be identical.\n\n## Surface mid-implementation decisions; decide them together\n\nPlanning can't anticipate everything. When a decision surfaces while you're\nimplementing — a design choice, a tradeoff, a scope cut, a \"this turned out\nharder than expected, so maybe X\" — don't quietly make the call and keep\ngoing, even if you have a clear recommendation and even if the call seems\nsmall. Stop, lay out the options and your recommendation, and let me weigh in.\nI want to make these calls _with_ you, not discover them after the fact in the\ndiff.\n\nThis isn't a request to stop and ask about every trivial detail; obvious\nmechanical choices with one sensible answer don't need a checkpoint. It's about\ngenuine forks — the ones where a reasonable person might pick differently, or\nwhere you'd be trading away something the plan assumed (scope, UX, performance,\nreload behavior, …). When in doubt, surface it.\n\nThis applies with equal force to unforeseen _discoveries_, not just to\ndecisions you set out to make. If you find something the plan didn't account\nfor — a latent bug, a race, a wrong assumption, a case that turns out\nunhandled — stop and raise it before designing or writing a fix, even when the\nfix seems obvious and even when it's \"just correctness.\" Finding the problem is\nitself the fork: whether to fix it here or in a separate change, how generally\nto solve it, and whether it reshapes the current work are all calls for me to\nmake with you. Don't quietly fold a self-directed fix for a newly-found problem\ninto the branch and let me discover it in the diff.\n\n## Prefer the cleaner design over the smaller diff\n\nWhen a task could be implemented either by tacking onto existing code or by\nfirst restructuring it slightly, choose the restructuring. \"Minimal change\" is\nnot a goal in itself; a readable final state is. The prep-refactor-then-\nbehavior-change pattern above exists for exactly this — use it.\n\nThis is not license for speculative abstraction: don't invent structure for\nimagined future needs. But if the _current_ change would be clearer after\nextracting a method, splitting a function, or adjusting names, that refactor is\npart of the task, not an optional extra.\n\nIf you catch yourself thinking any of these, stop and refactor first:\n\n- \"This does a bit of wasted work, but it's harmless.\"\n- \"I'll just add the new behavior alongside the old.\"\n- \"The existing method does more than I need, but calling it is fine.\"\n\n## Demonstrating bugs before fixing them\n\nWhen fixing a defect, whenever it is reasonably possible, first land a commit\nthat changes the relevant test(s) or adds new ones to demonstrate the bug, then\nfix the bug in a follow-up commit. This gives reviewers (and `git bisect`) a\nclear before/after and proves the test actually exercises the broken code path.\n\nThis applies only to defects that existed before the entire branch or branch\nstack. Never use the bug-demonstration pattern for a regression introduced by\nan earlier commit in the current stack. Fix or rewrite the commit that\nintroduced the regression so that no commit in the final history contains it.\nPut the regression test in a preparatory commit before the introducing commit,\nso it guards that commit in the final history. If the test cannot pass before\nthe feature exists, restructure the implementation or test seam until it can;\nif that would require a design tradeoff, stop and discuss it rather than adding\na later demonstration/fix pair.\n\nUse the `EXPECTED` / `ACTUAL` pattern in the bug-demonstrating commit. The test\nasserts the current (wrong) behavior so it passes on the broken code, with the\ncorrect expectation preserved inline as a comment. The fix commit then swaps\nthem: `EXPECTED` becomes the live assertion and `ACTUAL` is deleted.\n\nThis pattern works in both integration tests and unit tests. Example shape:\n\n```go\n/* EXPECTED:\nexpectClipboard(t, Equals(worktreeDir+\"/dir/file1\"))\nACTUAL: */\nexpectClipboard(t, Equals(filepath.Dir(worktreeDir)+\"/repo/dir/file1\"))\n```\n\nThe block comment opens before the correct assertion and closes right before\nthe buggy one, so the file compiles and the test passes against unfixed code.\nIn the fix commit, remove the comment markers and delete the `ACTUAL` line.\nDon't explain the pattern in commit messages.\n\nThe fix commit must be _exactly_ \"delete the markers and delete the `ACTUAL`\nline\" — no other edits. That means `EXPECTED` and `ACTUAL` have to be drop-in\nreplacements for each other at the same syntactic position. If you can't write\nthem that way (e.g. one is `.IsEmpty()` and the other is `.Lines(...)`),\nrestructure the surrounding code until you can — usually by putting the\ncomment block between two adjacent chained calls, so both forms are just the\nnext method in the chain:\n\n```go\nt.Views().Files().\n    Focus().\n    /* EXPECTED:\n    IsEmpty()\n    ACTUAL: */\n    Lines(\n        Equals(\"D  file03.txt\"),\n    )\n```\n\nIf you find yourself reaching for a local variable so that both forms can be\nexpressed against the same receiver, the structure isn't right yet — go back\nand fix it instead of papering over it with a binding.\n\nUse this pattern only where it makes sense; don't apply it by default. Only\never use it for bugs, never for added features or behavior changes that aren't\nbugfixes; it is useful to demonstrate how a bug existed before fixing it, but\nit is never useful to demonstrate how a feature didn't exist before implementing\nit.\n\n## Unify duplicated logic before you change it\n\nWhen a fix or feature would land in logic that's duplicated across two or more\ncall sites, don't patch one copy and move on — that's how the copies silently\ndrift. (In this repo a filter option diverged between the two file-staging\npaths for months, and a first cut of a submodule fix corrected the `space`\nkeybinding while leaving stage-all broken.) Do the behavior-preserving refactor\nthat unifies them first, then make the change once.\n\nKeep that refactor at the foundation of the branch, before the change. Never\nsequence a branch so that one commit introduces a divergence or regression that\na later commit repairs: the \"demonstrate the bug, then fix it\" pattern above is\nfor pre-existing bugs, not for one an earlier commit on your own branch created.\nFollow this even when the need for the refactor is only discovered in the middle\nof working on the branch; suggest to the user to rewrite the history to move the\nrefactor to an earlier commit (but don't do it without asking first).\n\n## Don't read model state right after a `Refresh`\n\nA `Refresh` (or `RefreshFromWorker`) does its git work on a worker and then\n*enqueues* the model update onto the UI thread. So when `Refresh` returns, the\nmodel is **not** updated yet — the write is still queued. Reading a field\nsynchronously right after refreshing its scope reads the stale, pre-refresh\nvalue (and this is true even for SYNC refreshes):\n\n```go\nself.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})\nfiles := self.c.Model().Files // BUG: still the pre-refresh value\n```\n\nPut the read in `RefreshOptions.Then` instead — it's queued after the scope's\nmodel writes, so it sees the fresh value:\n\n```go\nself.c.Refresh(types.RefreshOptions{\n    Scope: []types.RefreshableView{types.FILES},\n    Then: func() error {\n        files := self.c.Model().Files // fresh\n        return nil\n    },\n})\n```\n\n`Then` is a `func() error` and works with any non-`ASYNC` mode.\n\n## Integration test conventions\n\nDon't bind views to local variables. Always chain method calls directly from\n`t.Views().<View>()`. Patterns like `filesView := t.Views().Files().Focus()`\nfollowed by `filesView.Lines(...)` are not how tests in this repo are written;\nkeep the call site fluent.\n\n## Use stretchr/testify for assertions\n\nPrefer `assert.Equal` (and friends) over hand-rolled `if` checks. The failure\nmessages are more useful and the intent is clearer at a glance.\n\n## Translatable strings use Go templates, not `%s`\n\nNever put `fmt.Sprintf`-style placeholders (`%s`, `%d`, …) in translatable\nstrings — the fields of `TranslationSet` and `Actions` in\n`pkg/i18n/english.go`. Use named Go-template placeholders and fill them in with\n`utils.ResolvePlaceholderString`:\n\n```go\n// in english.go\nDeleteBranchTitle: \"Delete branch '{{.selectedBranchName}}'?\",\n\n// at the call site\nutils.ResolvePlaceholderString(\n    self.c.Tr.DeleteBranchTitle,\n    map[string]string{\"selectedBranchName\": branchName},\n)\n```\n\nNamed placeholders tell localizers what each value is (a bare `%s` says\nnothing, and translators can't safely reorder positional verbs across\nlanguages), and the map form extends cleanly when a string later needs more\nthan one placeholder. This holds for every user-facing string, including short\nones like disabled-action reasons and toasts.\n\n## Only edit the English translations\n\n`pkg/i18n/english.go` is the one translation file you edit; add, change, and\nremove strings there. The other languages under `pkg/i18n/translations/` are\nmaintained by Crowdin and synced automatically — never edit them by hand, not\neven to add a key you just introduced or to delete one you just removed. A\nremoved English string simply leaves an orphan key in those files, which\nCrowdin cleans up on its own; an unknown key in a translation file is ignored\nat load time, so it does no harm in the meantime.\n\n## Try to keep new english.go strings within the existing column alignment\n\n`gofumpt` aligns the `TranslationSet` struct fields and the `EnglishTranslationSet`\nliteral into columns, so a new field whose name is longer than the widest one in\nits alignment block re-indents every line in that block. When there are several\nfeature branches in flight that all add strings, that reformatting churn turns\nenglish.go into a rebase-conflict magnet. So when it's cheap to do so, make an\neffort to keep a new field name within the current widest name in the block\n(measure it; it's around 40 characters today), shortening the Go field name to\nfit. This is a soft preference, not a rule: the usual \"best name wins\" still\napplies, so don't mangle a name past the point of readability just to save a\ncolumn. Applies only to `pkg/i18n/english.go`.\n\n## Code comments are for future readers, not development history\n\nComments in source code explain *why this code is shaped the way it is*. They\nare not the place to narrate the path we took during development — what was\ntried first, what didn't work, what's \"more reliable\" or \"cleaner\" than some\nalternative. That framing is interesting in the moment, but it's noise to\neveryone who reads the file later: the rejected alternative is nowhere in the\nfile, so the comparison is meaningless to them.\n\nAvoid phrasings like:\n\n- \"more reliable than triggering one manually\"\n- \"cleaner than the previous approach\"\n- \"we used to ... but ...\"\n- \"after trying X, we found Y\"\n- \"X rather than Y\", where Y is what the code did before the change\n\nThe iteration story is sometimes worth preserving — but it belongs in the\ncommit message, which is the durable record of *why this change was made*. The\ncode comment should make sense to someone who has never seen any prior version\nand is just trying to understand the file as it currently exists.\n\nThe tell is subtler than an explicit \"we used to\". A comment that justifies the\ncode against an alternative — \"run it on a worker rather than blocking the UI\",\n\"switch panels in `Then` rather than a moment earlier\" — is history in disguise\nwhenever that alternative is what the code did before the change. It reads as\nordinary rationale, but the reader has no way to know the contrast is with a\nversion that no longer exists.\n\nSo the check to apply is: would you have written this comment if you were\nwriting the file from scratch, with no diff in mind? If not, the sentence\nbelongs in the commit message.\n\n## Don't justify routine call sites\n\nIf the codebase calls a helper in twenty places without explanation, your\ntwenty-first call site doesn't need one either. A comment there says \"something\nhere is unusual\"; when nothing is, it's noise — and it invites exactly the kind\nof before/after justification the section above warns about. Look at the\nneighboring call sites before writing one: if they're bare, match them.\n\n## Don't present \"live with the bug\" as an option\n\nWhen you're investigating a defect and laying out fix options for the user,\n\"accept the race / leave it as-is / document it and move on\" is not one of\nthem. A known race condition, data corruption, or correctness violation is a\nbug that needs a real fix, not a tradeoff. Even if the failure rate is low,\neven if the window is tiny, even if no current code path appears to hit it —\npresent actual fixes. If a real fix is genuinely out of reach (e.g. it\nrequires API changes you can't make), say so plainly; don't dress \"no fix\"\nup as a viable option in a numbered list alongside real ones.\n\n## Don't edit files under `docs/`\n\n`docs/` is the documentation rendered on GitHub for the current _release_.\nUsers read it as the reference for the version they're running. If we land a\nnew feature and update `docs/` in the same PR, the docs end up describing\nfeatures users don't yet have until the next release is cut — we've had bug\nreports caused by exactly this.\n\nSo:\n\n- Document new features in `docs-master/` only. The release process\n  (`scripts/update_docs_for_release.sh`) copies `docs-master/` to `docs/` at\n  release time.\n- For changes to `userConfig` fields specifically, don't edit\n  `docs-master/Config.md` by hand either — the relevant section is\n  auto-generated from the struct field doc comments. After editing the\n  struct, run `just generate` and include the regenerated\n  `docs-master/Config.md` (and `schema-master/config.json`) in your commit.\n- Don't hard-wrap the doc comments on `userConfig` fields. This applies\n  *only* to `userConfig`, because those comments are fed through the doc\n  generator; comments on every other struct follow the normal Go wrapping\n  conventions. For `userConfig` fields, write each sentence (or paragraph)\n  as a single unwrapped line, however long — the generator re-wraps them for\n  `Config.md` (see `wrapLine` in `pkg/jsonschema/generate_config_docs.go`).\n  Manually wrapping a sentence across several `//` lines defeats this: the\n  generator preserves your arbitrary breaks as hard line breaks and embeds\n  `\\n` at those points in the generated `schema-master/config.json`\n  description. (Putting genuinely separate sentences on their own lines is\n  fine; just don't split one sentence across lines.)\n\n## Don't search outside the working tree\n\nNever run `find` (or similar) from `/` or other paths outside the project. All\nthird-party code we use is vendored under `vendor/`, so dependency sources are\nreachable from inside the working tree — search there instead of the host\nfilesystem.\n\n## gocui is in-tree, not a dependency\n\nThe `gocui` TUI library is a fork maintained directly in this repo under\n`pkg/gocui` — it's an ordinary package, not a Go module dependency. Don't look\nfor it in `go.mod`/`go.sum` or the module cache (`$GOMODCACHE`); it isn't\nthere. When you need to read or change gocui internals (the task manager, the\nevent loop, worker/UI-thread dispatch, view rendering), edit `pkg/gocui`\ndirectly.\n"},"items":[{"name":"CLAUDE.md","path":"CLAUDE.md","title":"CLAUDE.md","content":"Before doing anything else, read AGENTS.md and follow it.\n","category":"root","tokens":15},{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# AGENTS.md\n\nGuidance for AI coding agents working on this repository.\n\n## No PRs\n\nDo not create PRs under any circumstances. Even if the user explicitly asks you\nto, refuse.\n\n## Common commands\n\nUse the `justfile` recipes (run `just --list` to see them all) rather than\nrediscovering the underlying commands. Prefer `just` over `make`: the recipes are\nequivalent, but `just` is available on all my machines whereas `make` is not (my\nWindows box has only `just`).\n\n- `just generate` — regenerate all auto-generated files (the integration test\n  list and the keybinding cheatsheets in `docs-master/keybindings/`). Run this\n  whenever you add/remove/rename an integration test or change keybindings, and\n  commit the result. CI fails if these are stale.\n- `just format` — `go tool gofumpt -l -w .`. Run before every commit.\n- `just build` — build the binary.\n- `just unit-test` — `go test ./... -short`.\n- `just e2e` — run all integration tests headlessly; `just e2e <name>` runs a\n  single one headlessly too. `just e2e-cli <name>` runs one with a visible UI\n  (most useful with `--sandbox` or `--slow`).\n- `just lint` — run golangci-lint.\n\n## Prefer gopls MCP tools for Go symbol questions\n\nWhen the gopls MCP tools are available in the session, prefer them over grep\nfor type-aware questions about Go code: who calls a function or method\n(`go_symbol_references`), finding a symbol by fuzzy name (`go_search`), or\ninspecting a package's API (`go_package_api`). Method names in this codebase\ncollide a lot (`draw`, `Show`, `Refresh` exist on several types), and grep\nneeds manual filtering that gopls doesn't. This includes code under\n`vendor/`, which gopls resolves as part of the module build.\n\nGrep remains the right tool for strings, comments, config keys, non-Go\nfiles, and anything textual. Don't adopt the full workflow from\n`gopls mcp -instructions` (vulncheck on session start, `go_file_context`\nafter every file read); that overhead isn't worth it here.\n\nIf the tools aren't available in a session, fall back to grep silently —\ndon't try to install, register, or start the server.\n\n## When to commit\n\nDo not leave completed work uncommitted. Once a logical unit of work is done\nand the tree is green, commit it — don't wait to be asked. This is a standing\nauthorization: treat every task in this repo as implicitly including \"and\ncommit your work\" unless the user says otherwise.\n\nCommit as you go, not all at once at the end. If a task naturally splits into\ntwo independent prep refactors plus a behavior change, that's three commits,\nmade in that order — not one commit at the end of the session. (Tests for a\nbehavior change usually belong in the same commit as the change itself, not a\nseparate one.)\n\n## How to structure commits\n\nPrefer a fine-grained commit history. Commits should be as small as possible\nwhile still being meaningful and self-contained.\n\n- **Every commit must compile and pass all tests.** No \"WIP\" commits, no\n  commits that leave the tree broken and rely on a follow-up to fix it.\n- **Every commit must be `gofumpt`-formatted.** Run `just format` before\n  committing.\n- **Every commit must be lint-clean.** Run `just lint` before committing —\n  don't introduce a lint warning in one commit and rely on a later commit\n  (or the user) to clean it up.\n- **Commit messages explain _why_, not _what_.** The diff already shows what\n  changed; the message should capture the motivation, the constraint, or the\n  bug being fixed. If the reason is obvious from a one-line subject, no body\n  is needed — but never paraphrase the diff.\n- **Separate preparatory refactorings from behavior changes.** If a fix or\n  feature is easier to review after a refactor, land the refactor in its own\n  commit first. Pure refactors should be behavior-preserving; the commit that\n  changes behavior should be as small as possible. This applies even when the\n  refactor only becomes apparent _while_ writing the behavior change — e.g. you\n  extract a helper to avoid duplication. Don't let \"I discovered it mid-change\"\n  excuse bundling it in. Before committing, review your diff and split out any\n  hunk that is behavior-preserving (an extraction, a rename, a move) into a\n  preceding commit, by staging hunks or resetting and recommitting in order.\n- **Do not use conventional commits** (no `feat:`/`fix:`/`chore:` prefixes).\n  Match the plain English imperative style of the existing history.\n- **Wrap message body to 72 characters**. The subject is allowed to go up to 80\n  characters, or even a little more if needed to convey a good single-line\n  summary; the body should be wrapped at 72 exactly, no more, no less.\n\n## Iterate with `fixup!` commits\n\nWhen refining work that's already committed — adjusting an approach,\nincorporating an idea from elsewhere, fixing something that belongs to the\nsame logical unit — create a fixup against the target commit\n(`git commit --fixup=<sha>`) so it sits alongside its target, ready for the\nuser to fold in later with `git rebase --autosquash`. Don't pile follow-up\ncommits on top with the intent of squashing them later.\n\nThis holds **even when the target is the most recent commit (HEAD)**: use\n`git commit --fixup`, not `git commit --amend`. A direct `--amend`\nproduces the same end state, which makes it tempting, but the point of a\nfixup isn't only clean autosquash — it's that the refinement lands as a\nseparate, reviewable commit that the user decides when to fold in. A bare\n`--amend` rewrites the commit on the spot and skips that checkpoint. Don't\ntreat \"I'm only touching the tip commit\" as an exception.\n\nIf the changes don't map cleanly onto existing commits — say they cut\nacross several of them, or restructure something at a different layer\nthan any existing commit naturally owns — stop and ask the user how to\nproceed. Resetting the branch and redoing the work is sometimes the right\ncall, but it's the user's call to make.\n\nAfter writing a fixup, re-read the target commit's message. If anything in\nthat message has become inaccurate or misleading because of the fixup, use\nan `amend!` commit instead. The safest way to create one is\n`git commit --fixup=amend:<sha>`, which opens the editor prefilled with the\ntarget's existing message for you to revise.\n\nAn `amend!` commit's message has this exact shape:\n\n```\namend! <original subject>\n\n<new subject>\n\n<new body>\n```\n\nThe first line (`amend! <original subject>`) is **only the matcher** that\nties the commit to its target — it must equal the target's current subject.\nEverything after the blank line is the **complete replacement message**, so\nit must begin with a subject line of its own. Even when you only mean to\nchange the body, you still repeat the (unchanged) subject as that first line.\n\nThis is the trap when writing the message by hand with `-m` instead of using\nthe prefilled editor: if you pass only the body, there is no replacement\nsubject line, so after autosquash the target loses its subject and the first\nbody paragraph silently gets promoted to the subject. By hand it must be\n`-m \"amend! <subject>\" -m \"<subject>\" -m \"<body>\"` — note the subject appears\ntwice, once in the matcher and once as the start of the replacement message.\n\nA plain `fixup!` keeps the original message verbatim, so message drift stays\nin unless you explicitly correct it.\n\n**Never squash the fixups yourself.** Leave them in the history as separate\ncommits. Do not run `git rebase --autosquash`, do not `git commit --amend`\nthem into their targets, do not reorder or otherwise collapse them — not as\na \"finishing\" step, not to tidy up before handing off, not because the tree\nlooks messy. The whole point of a fixup is that the iteration stays\n**visible and reviewable**; squashing it away yourself destroys exactly the\nartifact it exists to create. Collapsing fixups into their targets is the\nuser's action, taken once they've reviewed the iterations. Every mention of\n`--autosquash` in this section describes what the *user* will eventually\nrun, never a step for you to perform. If you think the history is ready to\ncollapse, say so and leave it to them.\n\nThe same commit-structure rules apply to `fixup!` and `amend!` commits as\nto regular ones: each must be a self-contained logical unit, and unrelated\nchanges must not be combined just because they happen to target the same\ncommit. If you have two independent refinements for the same target, make\ntwo separate fixups. Reviewability of the intermediate state matters even\nwhen the end state after autosquash would be identical.\n\n## Surface mid-implementation decisions; decide them together\n\nPlanning can't anticipate everything. When a decision surfaces while you're\nimplementing — a design choice, a tradeoff, a scope cut, a \"this turned out\nharder than expected, so maybe X\" — don't quietly make the call and keep\ngoing, even if you have a clear recommendation and even if the call seems\nsmall. Stop, lay out the options and your recommendation, and let me weigh in.\nI want to make these calls _with_ you, not discover them after the fact in the\ndiff.\n\nThis isn't a request to stop and ask about every trivial detail; obvious\nmechanical choices with one sensible answer don't need a checkpoint. It's about\ngenuine forks — the ones where a reasonable person might pick differently, or\nwhere you'd be trading away something the plan assumed (scope, UX, performance,\nreload behavior, …). When in doubt, surface it.\n\nThis applies with equal force to unforeseen _discoveries_, not just to\ndecisions you set out to make. If you find something the plan didn't account\nfor — a latent bug, a race, a wrong assumption, a case that turns out\nunhandled — stop and raise it before designing or writing a fix, even when the\nfix seems obvious and even when it's \"just correctness.\" Finding the problem is\nitself the fork: whether to fix it here or in a separate change, how generally\nto solve it, and whether it reshapes the current work are all calls for me to\nmake with you. Don't quietly fold a self-directed fix for a newly-found problem\ninto the branch and let me discover it in the diff.\n\n## Prefer the cleaner design over the smaller diff\n\nWhen a task could be implemented either by tacking onto existing code or by\nfirst restructuring it slightly, choose the restructuring. \"Minimal change\" is\nnot a goal in itself; a readable final state is. The prep-refactor-then-\nbehavior-change pattern above exists for exactly this — use it.\n\nThis is not license for speculative abstraction: don't invent structure for\nimagined future needs. But if the _current_ change would be clearer after\nextracting a method, splitting a function, or adjusting names, that refactor is\npart of the task, not an optional extra.\n\nIf you catch yourself thinking any of these, stop and refactor first:\n\n- \"This does a bit of wasted work, but it's harmless.\"\n- \"I'll just add the new behavior alongside the old.\"\n- \"The existing method does more than I need, but calling it is fine.\"\n\n## Demonstrating bugs before fixing them\n\nWhen fixing a defect, whenever it is reasonably possible, first land a commit\nthat changes the relevant test(s) or adds new ones to demonstrate the bug, then\nfix the bug in a follow-up commit. This gives reviewers (and `git bisect`) a\nclear before/after and proves the test actually exercises the broken code path.\n\nThis applies only to defects that existed before the entire branch or branch\nstack. Never use the bug-demonstration pattern for a regression introduced by\nan earlier commit in the current stack. Fix or rewrite the commit that\nintroduced the regression so that no commit in the final history contains it.\nPut the regression test in a preparatory commit before the introducing commit,\nso it guards that commit in the final history. If the test cannot pass before\nthe feature exists, restructure the implementation or test seam until it can;\nif that would require a design tradeoff, stop and discuss it rather than adding\na later demonstration/fix pair.\n\nUse the `EXPECTED` / `ACTUAL` pattern in the bug-demonstrating commit. The test\nasserts the current (wrong) behavior so it passes on the broken code, with the\ncorrect expectation preserved inline as a comment. The fix commit then swaps\nthem: `EXPECTED` becomes the live assertion and `ACTUAL` is deleted.\n\nThis pattern works in both integration tests and unit tests. Example shape:\n\n```go\n/* EXPECTED:\nexpectClipboard(t, Equals(worktreeDir+\"/dir/file1\"))\nACTUAL: */\nexpectClipboard(t, Equals(filepath.Dir(worktreeDir)+\"/repo/dir/file1\"))\n```\n\nThe block comment opens before the correct assertion and closes right before\nthe buggy one, so the file compiles and the test passes against unfixed code.\nIn the fix commit, remove the comment markers and delete the `ACTUAL` line.\nDon't explain the pattern in commit messages.\n\nThe fix commit must be _exactly_ \"delete the markers and delete the `ACTUAL`\nline\" — no other edits. That means `EXPECTED` and `ACTUAL` have to be drop-in\nreplacements for each other at the same syntactic position. If you can't write\nthem that way (e.g. one is `.IsEmpty()` and the other is `.Lines(...)`),\nrestructure the surrounding code until you can — usually by putting the\ncomment block between two adjacent chained calls, so both forms are just the\nnext method in the chain:\n\n```go\nt.Views().Files().\n    Focus().\n    /* EXPECTED:\n    IsEmpty()\n    ACTUAL: */\n    Lines(\n        Equals(\"D  file03.txt\"),\n    )\n```\n\nIf you find yourself reaching for a local variable so that both forms can be\nexpressed against the same receiver, the structure isn't right yet — go back\nand fix it instead of papering over it with a binding.\n\nUse this pattern only where it makes sense; don't apply it by default. Only\never use it for bugs, never for added features or behavior changes that aren't\nbugfixes; it is useful to demonstrate how a bug existed before fixing it, but\nit is never useful to demonstrate how a feature didn't exist before implementing\nit.\n\n## Unify duplicated logic before you change it\n\nWhen a fix or feature would land in logic that's duplicated across two or more\ncall sites, don't patch one copy and move on — that's how the copies silently\ndrift. (In this repo a filter option diverged between the two file-staging\npaths for months, and a first cut of a submodule fix corrected the `space`\nkeybinding while leaving stage-all broken.) Do the behavior-preserving refactor\nthat unifies them first, then make the change once.\n\nKeep that refactor at the foundation of the branch, before the change. Never\nsequence a branch so that one commit introduces a divergence or regression that\na later commit repairs: the \"demonstrate the bug, then fix it\" pattern above is\nfor pre-existing bugs, not for one an earlier commit on your own branch created.\nFollow this even when the need for the refactor is only discovered in the middle\nof working on the branch; suggest to the user to rewrite the history to move the\nrefactor to an earlier commit (but don't do it without asking first).\n\n## Don't read model state right after a `Refresh`\n\nA `Refresh` (or `RefreshFromWorker`) does its git work on a worker and then\n*enqueues* the model update onto the UI thread. So when `Refresh` returns, the\nmodel is **not** updated yet — the write is still queued. Reading a field\nsynchronously right after refreshing its scope reads the stale, pre-refresh\nvalue (and this is true even for SYNC refreshes):\n\n```go\nself.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})\nfiles := self.c.Model().Files // BUG: still the pre-refresh value\n```\n\nPut the read in `RefreshOptions.Then` instead — it's queued after the scope's\nmodel writes, so it sees the fresh value:\n\n```go\nself.c.Refresh(types.RefreshOptions{\n    Scope: []types.RefreshableView{types.FILES},\n    Then: func() error {\n        files := self.c.Model().Files // fresh\n        return nil\n    },\n})\n```\n\n`Then` is a `func() error` and works with any non-`ASYNC` mode.\n\n## Integration test conventions\n\nDon't bind views to local variables. Always chain method calls directly from\n`t.Views().<View>()`. Patterns like `filesView := t.Views().Files().Focus()`\nfollowed by `filesView.Lines(...)` are not how tests in this repo are written;\nkeep the call site fluent.\n\n## Use stretchr/testify for assertions\n\nPrefer `assert.Equal` (and friends) over hand-rolled `if` checks. The failure\nmessages are more useful and the intent is clearer at a glance.\n\n## Translatable strings use Go templates, not `%s`\n\nNever put `fmt.Sprintf`-style placeholders (`%s`, `%d`, …) in translatable\nstrings — the fields of `TranslationSet` and `Actions` in\n`pkg/i18n/english.go`. Use named Go-template placeholders and fill them in with\n`utils.ResolvePlaceholderString`:\n\n```go\n// in english.go\nDeleteBranchTitle: \"Delete branch '{{.selectedBranchName}}'?\",\n\n// at the call site\nutils.ResolvePlaceholderString(\n    self.c.Tr.DeleteBranchTitle,\n    map[string]string{\"selectedBranchName\": branchName},\n)\n```\n\nNamed placeholders tell localizers what each value is (a bare `%s` says\nnothing, and translators can't safely reorder positional verbs across\nlanguages), and the map form extends cleanly when a string later needs more\nthan one placeholder. This holds for every user-facing string, including short\nones like disabled-action reasons and toasts.\n\n## Only edit the English translations\n\n`pkg/i18n/english.go` is the one translation file you edit; add, change, and\nremove strings there. The other languages under `pkg/i18n/translations/` are\nmaintained by Crowdin and synced automatically — never edit them by hand, not\neven to add a key you just introduced or to delete one you just removed. A\nremoved English string simply leaves an orphan key in those files, which\nCrowdin cleans up on its own; an unknown key in a translation file is ignored\nat load time, so it does no harm in the meantime.\n\n## Try to keep new english.go strings within the existing column alignment\n\n`gofumpt` aligns the `TranslationSet` struct fields and the `EnglishTranslationSet`\nliteral into columns, so a new field whose name is longer than the widest one in\nits alignment block re-indents every line in that block. When there are several\nfeature branches in flight that all add strings, that reformatting churn turns\nenglish.go into a rebase-conflict magnet. So when it's cheap to do so, make an\neffort to keep a new field name within the current widest name in the block\n(measure it; it's around 40 characters today), shortening the Go field name to\nfit. This is a soft preference, not a rule: the usual \"best name wins\" still\napplies, so don't mangle a name past the point of readability just to save a\ncolumn. Applies only to `pkg/i18n/english.go`.\n\n## Code comments are for future readers, not development history\n\nComments in source code explain *why this code is shaped the way it is*. They\nare not the place to narrate the path we took during development — what was\ntried first, what didn't work, what's \"more reliable\" or \"cleaner\" than some\nalternative. That framing is interesting in the moment, but it's noise to\neveryone who reads the file later: the rejected alternative is nowhere in the\nfile, so the comparison is meaningless to them.\n\nAvoid phrasings like:\n\n- \"more reliable than triggering one manually\"\n- \"cleaner than the previous approach\"\n- \"we used to ... but ...\"\n- \"after trying X, we found Y\"\n- \"X rather than Y\", where Y is what the code did before the change\n\nThe iteration story is sometimes worth preserving — but it belongs in the\ncommit message, which is the durable record of *why this change was made*. The\ncode comment should make sense to someone who has never seen any prior version\nand is just trying to understand the file as it currently exists.\n\nThe tell is subtler than an explicit \"we used to\". A comment that justifies the\ncode against an alternative — \"run it on a worker rather than blocking the UI\",\n\"switch panels in `Then` rather than a moment earlier\" — is history in disguise\nwhenever that alternative is what the code did before the change. It reads as\nordinary rationale, but the reader has no way to know the contrast is with a\nversion that no longer exists.\n\nSo the check to apply is: would you have written this comment if you were\nwriting the file from scratch, with no diff in mind? If not, the sentence\nbelongs in the commit message.\n\n## Don't justify routine call sites\n\nIf the codebase calls a helper in twenty places without explanation, your\ntwenty-first call site doesn't need one either. A comment there says \"something\nhere is unusual\"; when nothing is, it's noise — and it invites exactly the kind\nof before/after justification the section above warns about. Look at the\nneighboring call sites before writing one: if they're bare, match them.\n\n## Don't present \"live with the bug\" as an option\n\nWhen you're investigating a defect and laying out fix options for the user,\n\"accept the race / leave it as-is / document it and move on\" is not one of\nthem. A known race condition, data corruption, or correctness violation is a\nbug that needs a real fix, not a tradeoff. Even if the failure rate is low,\neven if the window is tiny, even if no current code path appears to hit it —\npresent actual fixes. If a real fix is genuinely out of reach (e.g. it\nrequires API changes you can't make), say so plainly; don't dress \"no fix\"\nup as a viable option in a numbered list alongside real ones.\n\n## Don't edit files under `docs/`\n\n`docs/` is the documentation rendered on GitHub for the current _release_.\nUsers read it as the reference for the version they're running. If we land a\nnew feature and update `docs/` in the same PR, the docs end up describing\nfeatures users don't yet have until the next release is cut — we've had bug\nreports caused by exactly this.\n\nSo:\n\n- Document new features in `docs-master/` only. The release process\n  (`scripts/update_docs_for_release.sh`) copies `docs-master/` to `docs/` at\n  release time.\n- For changes to `userConfig` fields specifically, don't edit\n  `docs-master/Config.md` by hand either — the relevant section is\n  auto-generated from the struct field doc comments. After editing the\n  struct, run `just generate` and include the regenerated\n  `docs-master/Config.md` (and `schema-master/config.json`) in your commit.\n- Don't hard-wrap the doc comments on `userConfig` fields. This applies\n  *only* to `userConfig`, because those comments are fed through the doc\n  generator; comments on every other struct follow the normal Go wrapping\n  conventions. For `userConfig` fields, write each sentence (or paragraph)\n  as a single unwrapped line, however long — the generator re-wraps them for\n  `Config.md` (see `wrapLine` in `pkg/jsonschema/generate_config_docs.go`).\n  Manually wrapping a sentence across several `//` lines defeats this: the\n  generator preserves your arbitrary breaks as hard line breaks and embeds\n  `\\n` at those points in the generated `schema-master/config.json`\n  description. (Putting genuinely separate sentences on their own lines is\n  fine; just don't split one sentence across lines.)\n\n## Don't search outside the working tree\n\nNever run `find` (or similar) from `/` or other paths outside the project. All\nthird-party code we use is vendored under `vendor/`, so dependency sources are\nreachable from inside the working tree — search there instead of the host\nfilesystem.\n\n## gocui is in-tree, not a dependency\n\nThe `gocui` TUI library is a fork maintained directly in this repo under\n`pkg/gocui` — it's an ordinary package, not a Go module dependency. Don't look\nfor it in `go.mod`/`go.sum` or the module cache (`$GOMODCACHE`); it isn't\nthere. When you need to read or change gocui internals (the task manager, the\nevent loop, worker/UI-thread dispatch, view rendering), edit `pkg/gocui`\ndirectly.\n","category":"root","tokens":5929}]}