{"owner":"gitbutlerapp","repo":"gitbutler","hasSkills":true,"totalSkillsCount":22,"totalTokensCount":29833,"categories":["anthropic-skill","copilot-instructions","root-instruction","claude-rule","plugin-manifest"],"hasMcp":false,"mcpConfig":null,"found":[".agents/skills/but-performance-tests/SKILL.md",".agents/skills/cli-commands/SKILL.md",".agents/skills/lite-render-perf/SKILL.md",".agents/skills/lite-screenshots/SKILL.md",".agents/skills/tui-tests/SKILL.md",".github/copilot-instructions.md","AGENTS.md","claude.md","crates/AGENTS.md","crates/CLAUDE.md","crates/but-agentlog/skill/SKILL.md","crates/but/skill/SKILL.md","packages/but-mcp-app/README.md","packages/but-sdk/README.md","packages/no-relative-imports/README.md","packages/shared/README.md","packages/svelte-comment-injector/README.md","packages/ui-react/AGENTS.md","packages/ui-react/CLAUDE.md","packages/ui-react/skill/SKILL.md","packages/ui-svelte/scripts/README.md","packages/ui-svelte/src/lib/components/chipToast/README.md"],"skills":{".agents/skills/but-performance-tests/SKILL.md":"---\nname: but-performance-tests\ndescription: Use when creating, changing, running, or debugging shell-based `but` CLI performance scenarios under `crates/but/tests/performance`, including Hyperfine runs, fixture setup, setup-to-test state, and output inspection.\n---\n\n# `but` CLI performance tests\n\nRead [performance framework documentation](../../../crates/but/tests/performance/README.md)\ncompletely before answering questions or changing performance scenarios. Inspect nearby\nscenarios and current shared helpers as applicable; README is canonical source for\nframework behavior, commands, helper APIs, and debugging options.\n\n## Workflow for changes\n\n1. Confirm requested operation and timing boundary.\n2. Inspect closest existing scenarios plus `lib.sh` and `run.sh`.\n3. Add or update only scenario's `setup.sh` and `test.sh` unless shared behavior is\n   genuinely required.\n4. Prefer real, representative GitButler repository state over toy data.\n5. Keep setup, validation, and ID discovery outside timed operation.\n6. Reuse documented helpers instead of creating parallel fixture, state, output, or\n   timing mechanisms.\n7. List every scenario in top-level performance README's included-scenario list, and\n   update its entry when materially changing scenario.\n8. Run README's shell validation and short single-scenario benchmark.\n\nFor usage or debugging questions, answer from current README and scripts. Do not edit\nfiles unless user asks for changes.\n\n## Guardrails\n\n- Keep changes inside `crates/but/tests/performance/` unless task specifically requests\n  related documentation or skill updates.\n- Preserve two-file scenario contract.\n- Do not add custom timing/statistics code; Hyperfine owns measurement.\n- Do not include fixture setup or compilation in measured command.\n- Do not run all scenarios when user requests or changes only one.\n- Report fixture/setup limitations and unavailable validation explicitly.\n",".agents/skills/cli-commands/SKILL.md":"---\nname: cli-commands\ndescription: Use when adding or modifying CLI command (`but` commands) under `crates/but/src`.\n---\n\nImagine we're implementing a new `commit3` command. The high-level structure\nfor that must be as follows.\n\n## Arguments\n\nArguments live in `crates/but/src/args`. For our commit command that would be\n`crates/but/src/args/commit3.rs`:\n\n```rust\nuse crate::args::atoms::CliIdArg;\n\n/// Create a commit.\n///\n/// More details about the command here...\n#[derive(Debug, clap::Parser)]\n#[cfg_attr(feature = \"raw-clap-docs\", clap(verbatim_doc_comment))]\n#[deny(missing_docs)]\npub struct Platform {\n    /// The message to use for the commit.\n    #[clap(short, long, group = \"commit_message\")]\n    pub message: Option<Vec<String>>,\n\n    /// Place the commit on the branch `BRANCH`.\n    #[clap(short, long, value_name = \"BRANCH\", group = \"targeting\")]\n    pub branch: Option<Option<CliIdArg>>,\n\n    /// One or more changes to commit.\n    pub changes: Vec<CliIdArg>,\n}\n```\n\nIn `crates/but/src/args/mod.rs`:\n\n```rust\n#[cfg(feature = \"legacy\")]\n#[cfg_attr(feature = \"raw-clap-docs\", clap(verbatim_doc_comment))]\n#[clap(hide = true, name = \"_commit3\")]\nCommit3(commit3::Platform),\n```\n\nThings to note:\n\nArguments that refer to Git objects (such as commits, branches, files, hunks,\netc.) use some type from `crates/but/src/args/atoms/` and not `String` or another\nloose type.\n\nUse `CliIdArg` for arguments that reference existing Git objects such as\nbranches, commits, files, etc. This allows the user to use short IDs or fully\nqualified names.\n\n`String` should only be used for truly loose text input such as commit\nmessages.\n\n`Platform` and all of its fields must have documentation. The `Subcommands`\nvariant intentionally has no doc comment because clap obtains the command\ndocumentation from `Platform`.\n\nUse `#[clap(group = \"...\")]` to create mutually exclusive groups of arguments.\n\nCommands with tricky grammar can define a `pub(crate) const ERROR_EXAMPLES`\nnext to `Platform` and register it in `args::error_examples`; the block is\nappended after clap parse errors. At most 4 lines, each a\n`but <cmd> ...   # what it does` invocation that works as written (e.g.\ninclude `-m` where omitting it would open an editor).\n\n## Handling the command\n\nAdd a match arm to `crates/but/src/lib.rs` to handle the command:\n\n```rust\nmatch cmd {\n    Subcommands::Commit3(commit_args) => {\n        use crate::utils::IntermediateChannel;\n\n        let status_after = args.status_after;\n        let mut ctx = setup::init_ctx(\n            &args,\n            InitCtxOptions {\n                background_sync: BackgroundSync::Enabled { silent: false },\n                ..Default::default()\n            },\n            out,\n        )?;\n        out.begin_status_after(status_after);\n\n        let outcome = command::legacy::commit3::commit(\n            &mut ctx,\n            IntermediateChannel::new(out),\n            commit_args,\n        )\n        .emit_metrics(metrics_ctx)?;\n        out.print_cli_output(outcome)?;\n\n        run_status_after_if_requested(status_after, &mut ctx, out);\n\n        Ok(())\n    }\n\n    // all the other commands...\n}\n```\n\nThings to note:\n\nUse `IntermediateChannel`. Do not pass `OutputChannel` to commands.\n\nUse `OutputChannel::print_cli_output` to print the final output from the\ncommand. This ensures we handle all supported formats. If only human format is\nsupported, use `OutputChannel::print_cli_output_human`.\n\n## Implementing the command\n\nIn `crates/but/src/command/legacy/commit3.rs`\n\n```\npub fn commit(\n    ctx: &mut Context,\n    out: IntermediateChannel<'_>,\n    args: Platform,\n) -> CliResult<CommitOutcome> {\n    // get whatever dependencies we need from `Context` such as\n    // `RepoExclusiveGuard`, `IdMap`, `RefInfo`, etc.\n\n    // resolve the arguments into a `CommitOperation`\n    let commit_operation = resolve(ctx, args)?;\n\n    // Run the operation\n    let outcome = run(ctx, commit_operation)?;\n\n    // Return the outcome which will be printed by the caller\n    Ok(outcome)\n}\n\nfn resolve(ctx: &mut Context, args: Platform) -> CliResult<CommitOperation> {\n    let Platform { message, branch, changes } = args;\n    // ...\n}\n\nfn run(ctx: &mut Context, commit_op: CommitOperation) -> anyhow::Result<CommitOutcome> {\n    match commit_op {\n        // ...\n    }\n}\n\n#[must_use]\nstruct CommitOutcome {\n    new_commit: ObjectId,\n}\n\nimpl CliOutputHuman for CommitOutcome {\n    fn on_human(self, out: &mut dyn WriteWithUtils, _theme: &Theme) -> anyhow::Result<()> {\n        let Self { new_commit } = self;\n\n        writeln!(\n            out,\n            \"Created commit {}\",\n            theme::Commit(new_commit, None),\n        )?;\n\n        Ok(())\n    }\n}\n\nimpl CliOutput for CommitOutcome {\n    fn on_shell(self, out: &mut dyn WriteWithUtils) -> anyhow::Result<()> {\n        let Self { new_commit } = self;\n\n        writeln!(out, \"{}\", new_commit.to_hex_with_len(7))?;\n\n        Ok(())\n    }\n\n    fn on_json(self) -> impl serde::Serialize {\n        #[derive(Serialize)]\n        struct Output {\n            commit: HexHash,\n        }\n\n        let Self { new_commit } = self;\n\n        Output { commit: new_commit.into() }\n    }\n}\n```\n\nThings to note:\n\nCommands follow a `resolve` then `run` structure.\n\n`run` doesn't print its final output. It returns something that implements\n`CliOutput` / `CliOutputHuman` which the caller can then print.\n\n`resolve` returns `CliResult` because it needs to reject bad user input.\n\n`run` returns `anyhow::Result` because it can only hit internal errors. Bad\nuser input is handled by `resolve`.\n\n`resolve` translates CLI arguments into domain targets and validates user\ninput. It may query repository state to disambiguate or reject input, but\nshould not retain derived data in the operation.\n\n`run()` loads current repository state and computes results. Commit counts,\ndiffs, statistics, branch details, current tips, and workspace projections\ngenerally belong in `run()`. State derivable from those identifiers and the\ncontext belongs in `run()`.\n\nThe operation should not contain types from `crate::args::atoms`.\n\nA non-CLI caller such as the TUI should be able to construct an operation from\ndomain identifiers without reproducing repository queries from `resolve()`. If\nit must compute counts, diffs, or branch metadata to build the operation, those\nfields probably belong in `run()`.\n\nThe operation does not contain diff specs (usually in the form\n`Vec<DiffSpec>`). It should instead contain `CliId`s which `run` turns into\n`Vec<DiffSpec>` using `DiffSpecBuilder`. This makes it easier for the status\nTUI to call `run` directly.\n\nUser-input pickers and prompts are created via `InputOutputChannel`\naccessed through `IntermediateChannel::prepare_for_terminal_input`.\n\nThe JSON output includes both commit IDs and change IDs.\n\nUse `AllowMergedArg` and `MergedUpstream` to verify we don't mutate merged\ncommits and branches.\n\nPrinting of commits, branches, change IDs, etc. uses newtypes from `theme` such\nas `theme::Branch` and `theme::Commit`. This ensures consistent coloring.\n\nBad user input errors use `bad_input(...)`, optionally with `.arg_name()`,\n`.arg_value()`, and `.hint()`.\n\nAvoid making the operation or outcome types implement `serde::Serialize`.\nDefine the specific types needed inside `fn on_json`. This makes it harder to\nbreak compatibility by accident. Reusing `Serialize` from existing domain types\nis fine.\n\n## More examples\n\nFor examples of this structure in practice see\n\n- `crates/but/src/command/legacy/commit.rs`\n- `crates/but/src/command/legacy/move.rs`\n- `crates/but/src/command/legacy/squash.rs`\n- `crates/but/src/command/legacy/diff.rs`\n",".agents/skills/lite-render-perf/SKILL.md":"---\nname: lite-render-perf\ndescription: Use when working on React code in `apps/lite` that derives values during render, adds or consumes a context, subscribes to the redux store or a query, or renders lists of row components — and whenever the lite UI is reported slow, laggy, or re-rendering too much. React Compiler does not prevent these regressions; load this skill BEFORE writing such code, not only when debugging it.\n---\n\nThe recurring render-performance failure in this app is accidental\nover-re-rendering: one interaction re-renders every row instead of the few\naffected ones. Repositories can have hundreds of branch rows, so this turns\nsingle-digit-millisecond interactions into triple-digit ones. The compiler\nmakes it easy to cause by accident because it silently stops memoizing\nexactly where you assume it won't.\n\n## The three mechanisms behind past regressions\n\n### 1. Whether the compiler memoizes a derivation is not predictable from the source\n\nThe compiler only caches what it can prove safe to cache, and it declines\nmore often than you would guess. Two derivations in this app are both\nmodule-level functions that call imported helpers, and they compile\ndifferently: `buildOutlineNavigationIndex` in `WorkspacePage.tsx` gets a\nmemo block keyed on its inputs, while `getDiffView` in `Details.tsx` got\nnone and re-ran on every render of a component that re-renders whenever the\nfile selection changes.\n\nUnmemoized inputs are contagious. `getDiffView` was called with values\nbuilt by `Match` pipes that the compiler had not memoized either; feeding\nit stable values instead was enough to make the memo block appear. So the\nchurn spreads outward from whichever value first fails to get a scope,\nwhich is why one unmemoized derivation can quietly cost far more than\nitself.\n\nEither way the effect is the same: the derivation re-runs and returns fresh\nidentities even when its data never changed, and everything keyed on those\nidentities — context values, props, effect deps — churns with it.\n\nWhat to do instead:\n\n- Derive query data inside the query's `select`. react-query caches the\n  result on `(data, select function identity)`, and the compiler DOES\n  memoize the closure on its captures — so the closure's identity becomes\n  exactly the dependency key you want. Extra inputs are simply captured by\n  the closure. Precedent: `useBranchesOutline.ts`. `useQueries` /\n  `useSuspenseQueries` give you the same lever in `combine` — precedent:\n  `lineStats` in `OutlineTree/UncommittedChangesRow.tsx`, which folds the\n  results down inside `combine` instead of mapping them out and deriving in\n  render the way `Details.tsx` does. Check what the closure captures:\n  capture a value that is itself rebuilt each render and you have only\n  moved the churn one level up.\n- For redux-derived collections, use `createSelector` (see\n  `projects/project.ts`), never a plain function returning a fresh array or\n  object.\n- Never conclude from the source that something is or is not memoized —\n  read the compiler's output. A compiled function imports\n  `react/compiler-runtime` and allocates slots with `const $ = _c(n)`; a\n  memoized value is assigned inside an `if ($[i] !== dep)` block, and its\n  dependencies are exactly the values that block tests. A derivation\n  sitting outside every such block runs on every render. Two ways to look:\n  fetch the vite-transformed module from the running app\n  (`fetch('/src/path/to/file.ts').then(r => r.text())` over CDP — see\n  below), or run the plugin offline on any file, which needs no app:\n\n```js\n// Resolve both from node_modules/.pnpm — pnpm does not hoist them. There is\n// no @babel/preset-typescript here, so strip types via parserOpts instead.\nawait babel.transformAsync(source, {\n\tfilename: file,\n\tparserOpts: { plugins: [\"typescript\", \"jsx\"] },\n\tplugins: [[\"babel-plugin-react-compiler\", { target: \"19\" }]],\n\tconfigFile: false,\n\tbabelrc: false,\n});\n```\n\n### 2. Context re-renders every consumer; it has no selector\n\n`use(SomeContext)` re-renders the component whenever the context value's\nidentity changes — there is no way to subscribe to a slice, and neither the\ncompiler nor `React.memo` can bail a context read out. Note this is a\ndifferent failure from mechanism 1: when the value's content legitimately\nchanged, fixing identities cannot help — the only cure is consumers\nsubscribing to something narrower than the whole value.\n\nWhat to do instead:\n\n- Context is for values that change rarely relative to their consumers.\n- Per-row state must be a narrow `useAppSelector` returning a primitive\n  (usually a boolean). Redux subscriptions only re-render when the selected\n  value changes, so a selection change re-renders two rows, not all of them:\n\n```tsx\nconst isSelected = useAppSelector((state) => {\n\tconst stored = projectSlice.selectors.selectPrimaryOutlineSelection(state, projectId);\n\treturn stored !== null && operandEquals(stored, operand);\n});\n```\n\nFor the current shape, see `useIsSelected` in `BranchesList.tsx` and the\n`selectIsSelectedOutline` subscription in `OutlineTree/ItemRow.tsx`.\n\n- If rows need a value that requires resolution against a big object,\n  resolve it once at the top and store the resolved form, rather than\n  letting every row resolve independently.\n\n### 3. Identity discipline in the store and in query results\n\n- Reducers must early-return when the update is a no-op, otherwise they\n  produce a new state identity and wake every subscriber.\n- Reading store data inside an event callback needs no subscription at all —\n  take `const store = useAppStore()` at the top and call `store.getState()`\n  in the callback, instead of subscribing with `useAppSelector` to a value\n  only the callback reads.\n- Destructure `useMutation` results (`const { isPending, mutate } = ...`) —\n  the top-level result object is a new identity every render\n  (`@tanstack/query` `no-unstable-deps`).\n- Hook call order can change what the compiler memoizes; moving plain\n  `useQuery` calls above a `useQueries` call has fixed memoization here\n  before. If the compiled output shows a value outside every memo slot for\n  no visible reason, try reordering the hooks around it.\n\n## Driving the dev app over CDP\n\n`pnpm dev` in `apps/lite` starts vite (port 5173) and Electron with\n`--remote-debugging-port=9222`. The first run compiles a helper binary with\ncargo and takes minutes; `Port 5173 is already in use` means an instance is\nalready running — reuse it, or `lsof -ti tcp:5173 | xargs kill`. React and\nRedux DevTools are auto-installed in dev builds.\n\n`curl http://127.0.0.1:9222/json` lists debug targets. Pick the entry with\n`\"type\": \"page\"` whose URL is on 5173 — the others are extension service\nworkers. Its `webSocketDebuggerUrl` speaks the Chrome DevTools Protocol; a\nfew lines of Node (global `WebSocket`) are enough to drive it:\n\n```js\n// Usage: node cdp.mjs eval \"<expr>\"   or:   node cdp.mjs shot out.png\nconst page = (await (await fetch(\"http://127.0.0.1:9222/json\")).json()).find(\n\t(t) => t.type === \"page\" && t.url.includes(\"5173\"),\n);\nconst ws = new WebSocket(page.webSocketDebuggerUrl);\nlet id = 0;\nconst pending = new Map();\nconst send = (method, params) =>\n\tnew Promise((res) => {\n\t\tpending.set(++id, res);\n\t\tws.send(JSON.stringify({ id, method, params }));\n\t});\nws.onmessage = (e) => {\n\tconst m = JSON.parse(e.data);\n\tpending.get(m.id)?.(m.result);\n};\nawait new Promise((res) => (ws.onopen = res));\nconst [mode, arg] = process.argv.slice(2);\nif (mode === \"eval\")\n\tconsole.log(\n\t\tJSON.stringify(\n\t\t\tawait send(\"Runtime.evaluate\", {\n\t\t\t\texpression: arg,\n\t\t\t\tawaitPromise: true,\n\t\t\t\treturnByValue: true,\n\t\t\t}),\n\t\t),\n\t);\nelse if (mode === \"shot\") {\n\tconst { data } = await send(\"Page.captureScreenshot\", { format: \"png\" });\n\t(await import(\"node:fs\")).writeFileSync(arg, Buffer.from(data, \"base64\"));\n}\nprocess.exit(0);\n```\n\nUseful from `Runtime.evaluate`:\n\n- The whole preload API is callable: `window.lite.branchList(projectId)`\n  etc. — the fastest way to inspect real API payloads and sizes.\n- Interact by dispatching events on DOM nodes found via `aria-label`s;\n  screenshot to verify visually.\n\nGotchas:\n\n- Repeated evaluates share one top-level scope: wrap every expression in a\n  `{ ... }` block or `const x` collides with the previous call\n  (\"Identifier 'x' has already been declared\").\n- Synthetic `MouseEvent`s do not move real focus; behavior wired to\n  `onFocus` (selection scopes) needs an explicit `.focus()` or real input.\n- Renderer edits arrive via HMR, but edits to store/redux modules trigger a\n  full page reload that wipes `window.*` state — re-arm any probes after.\n  Electron main-process edits are only compiled at startup: restart\n  `pnpm dev`.\n\n## Measuring: prove it, in both directions\n\nReact DevTools being installed means its hook is available in-page:\n\n1. Arm a commit logger and interact:\n\n```js\nconst hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;\nwindow.__log = [];\nconst orig = hook.onCommitFiberRoot.bind(hook);\nhook.onCommitFiberRoot = (id, root, ...rest) => {\n\twindow.__log.push({ dur: root.current.actualDuration });\n\treturn orig(id, root, ...rest);\n};\n```\n\n2. Judge by `actualDuration` per commit: an interaction should cost\n   single-digit milliseconds and re-render O(affected rows). A commit whose\n   duration scales with total row count is the signature of a blast.\n3. To find WHY, read the compiled module (mechanism 1 above). To watch a\n   specific context value's identity across commits, extend the logger to\n   walk `root.current` for the provider fiber (its `type` is the context\n   object, so match on `type.displayName`) and compare `memoizedProps.value`\n   against the previous commit's. Do NOT add `window.*` probe writes inside\n   components or hooks to trace identities — that mutation makes the\n   compiler bail out of the whole function and destroys the very memoization\n   you are measuring.\n4. Restore `onCommitFiberRoot` when done; measure before and after the fix.\n\n## Review checklist\n\n- Any non-trivial derivation in render: is its result's identity\n  load-bearing (context value, prop to many children, effect dep)? Move it\n  into `select`/`combine`/`createSelector`, or read the compiled output to\n  prove it harmless — do not assume the compiler handled it.\n- Any new context: how often does its value change, and how many consumers?\n- Any `useAppSelector`: does it return a primitive or a stored reference,\n  not a fresh object?\n- Any new reducer: does it no-op cleanly when the value is unchanged?\n",".agents/skills/lite-screenshots/SKILL.md":"---\nname: lite-screenshots\ndescription: Use when asked to add before/after screenshots of a Lite UI change to a pull request, when a PR touching apps/lite/ui needs its visual change shown, or when extending the screenshot catalogue in apps/lite/e2e/tests/screenshots.spec.ts. Captures both sides locally against seeded fixtures, publishes the surfaces that changed, and posts them to the PR.\n---\n\nA reviewer reading a CSS diff cannot see the result. This captures the Lite UI\nwith the branch applied and again with it unapplied, and posts the surfaces that\ndiffer to the pull request.\n\nCapture runs **on this machine**, not in CI. It was tried in CI and abandoned:\nunder the bare X server on the Linux runners this Electron build paints a frame\non load and not reliably afterwards, so several surfaces hung indefinitely\nwhether the capture went through Playwright or the DevTools protocol. The same\nspec passes here. The CI workflow only leaves a reminder.\n\n## Before capturing: is the surface covered?\n\nWork this out first, by reading the diff — not by capturing and inferring it from\nthe result. A run that reports \"8 surfaces, 0 changed\" looks identical whether the\nchange is invisible or whether nothing looked at it.\n\n1. Identify which screen the change affects.\n2. Check it against the catalogue in `apps/lite/e2e/tests/screenshots.spec.ts`\n   (currently: workspace sidebar, diff pane, branches tab, upstream tab, project\n   picker, uncommitted rows, commit form, settings).\n3. **Covered** — capture, as below.\n4. **Not covered** — stop and ask the developer, offering the two lanes below.\n   Either way it is their call, not one to make quietly inside a screenshot\n   request. Tell them which screen is uncovered and which fixture would reach it.\n   - **Add it to the catalogue** when the gap is a _screen_ — something anyone\n     working nearby will change again. It edits a shared file and becomes\n     permanent coverage for every later pull request. See \"Extending the\n     catalogue\" below.\n   - **Capture it ad hoc** when the gap is a _state_ — a conflicted commit\n     mid-resolution, an empty list, one specific dialog — that no future pull\n     request is likely to want. Nothing shared changes and nothing is left\n     behind. See \"One-off capture\" below.\n\n   Prefer the catalogue when it is a close call. The ad-hoc lane photographs the\n   change once and protects nothing afterwards, and a catalogue that stops growing\n   makes every later request re-derive the same fixtures.\n\nIf they decline both, **do not post a comment**. A normal comment would report\n\"N surfaces, 0 changed\", which asserts the change is not visual when in truth it\nwas never photographed. Say plainly that the affected screen is not in the\ncatalogue and no screenshots were taken.\n\n## Changes the capture cannot see\n\nSome diffs are invisible to this harness for reasons that have nothing to do with\nthe change being non-visual. Rule these out before reading a \"0 changed\" result\nas an answer:\n\n- **Scrollbars and their gutter.** macOS hides scrollbars unless a mouse is\n  attached, and a styled `::-webkit-scrollbar` does _not_ override that — the\n  scroller simply reserves no gutter. On a machine set to \"When scrolling\", any\n  change to the track, thumb, gutter width, or a separator meeting the gutter\n  captures identically on both sides. Set System Settings → Appearance → Show\n  scroll bars → **Always** for the run, and say in the comment that you did.\n- **Hover, focus, and drag states** are not entered by the spec.\n- **Anything behind a scroll position** — surfaces are captured at the top.\n\n## Prerequisites\n\n- `cargo build -p but` once, giving `target/debug/but`.\n- The branch under test applied, and **not yet merged** — the base side comes\n  from unapplying it. Check `but status` first.\n- **Run every capture through `env -u ELECTRON_RUN_AS_NODE`.** An agent hosted\n  inside VS Code (itself Electron) inherits `ELECTRON_RUN_AS_NODE=1`, which makes\n  the Electron the test launches behave as plain Node. Every surface then fails\n  with `Error: Process failed to launch!` and nothing says why — the binary is\n  fine, and running it by hand from another shell works. Check with\n  `env | grep ELECTRON`.\n\n## The flow\n\nRun every command from the repository root.\n\n### 1. Capture the branch as it stands\n\n```console\n$ env -u ELECTRON_RUN_AS_NODE SCREENSHOT_OUT=head BUT=$PWD/target/debug/but \\\n    pnpm -F @gitbutler/lite test:e2e screenshots.spec.ts\n```\n\nOutput lands in `apps/lite/e2e/screenshots/head/` (gitignored).\n\n### 2. Capture the base\n\n```console\n$ but unapply <branch>\n$ but status        # confirm the branch is gone from the workspace\n$ env -u ELECTRON_RUN_AS_NODE SCREENSHOT_OUT=base BUT=$PWD/target/debug/but \\\n    pnpm -F @gitbutler/lite test:e2e screenshots.spec.ts\n$ but apply <branch>\n```\n\n**Check the unapply took effect.** It silently does nothing on a stale id or an\nalready-merged branch, and both runs then capture identical code — which looks\nexactly like \"this change is not visual\". If every pair matches in step 3, assume\nthis went wrong before concluding anything about the change.\n\nRe-apply immediately, before anything else can fail and leave the workspace short\na branch.\n\n**Stacked branches take the whole stack down.** `but unapply` on a branch that is\npart of a stack unapplies every branch in it, including ones this capture depends\non — unapplying a stacked change also removes the spec, and the base run then has\nno tests to execute. Check `but status` first. When the branch is stacked, capture\nthe base by reverting the change in the working tree instead:\n\n1. Edit the changed files back to their pre-change state by hand.\n2. Capture into `base`.\n3. `but discard <file-id>` to restore the committed version.\n\nConfirm the restore: the change must be back before anything else happens.\n\n### 3. Select what changed\n\n```console\n$ node apps/lite/e2e/compare-screenshots.mjs \\\n    apps/lite/e2e/screenshots/base \\\n    apps/lite/e2e/screenshots/head \\\n    /tmp/publish \\\n    \"https://raw.githubusercontent.com/<owner>/<repo>/pr-screenshots/pr-<number>/<short-sha>\" \\\n    /tmp/section.md\n```\n\nIt prints `total=`, `changed=`, `added=`, stages only the differing pairs into\n`/tmp/publish`, and writes the comment body. Surfaces that did not change are\nbyte-identical, so they are folded into a collapsed list rather than shown.\n\n### 4. Publish the images\n\nImages go on an orphan `pr-screenshots` branch, under `pr-<number>/<short-sha>/`.\nA per-commit directory matters: reusing one path lets GitHub's image proxy serve\nthe previous run's screenshots from cache, which reads as \"my change did nothing\".\n\nWrite them with the git data API rather than a local clone. There is nothing to\nclone, nothing to clean up, and no local `git` writes — which agents are often\nnot permitted to make outside the project, and which cannot touch the GitButler\nworkspace by accident.\n\n```console\n$ REPO=<owner>/<repo>; DIR=pr-<number>/<short-sha>\n$ HEAD_SHA=$(gh api repos/$REPO/git/refs/heads/pr-screenshots --jq '.object.sha')\n$ BASE_TREE=$(gh api repos/$REPO/git/commits/$HEAD_SHA --jq '.tree.sha')\n\n# One blob per image. base64 must be stripped of newlines.\n$ jq -n --rawfile c <(base64 -i /tmp/publish/<name>.png) \\\n      '{content: ($c | gsub(\"\\n\";\"\")), encoding: \"base64\"}' > /tmp/blob.json\n$ BLOB=$(gh api -X POST repos/$REPO/git/blobs --input /tmp/blob.json --jq '.sha')\n\n# One tree and one commit for all of them, then move the ref.\n$ jq -n --arg t \"$BASE_TREE\" --arg b \"$BLOB\" --arg d \"$DIR\" '{base_tree: $t, tree: [\n      {path: ($d + \"/<name>.png\"), mode: \"100644\", type: \"blob\", sha: $b}]}' > /tmp/tree.json\n$ TREE=$(gh api -X POST repos/$REPO/git/trees --input /tmp/tree.json --jq '.sha')\n$ jq -n --arg t \"$TREE\" --arg p \"$HEAD_SHA\" \\\n      '{message: \"Screenshots for #<number>\", tree: $t, parents: [$p]}' > /tmp/commit.json\n$ COMMIT=$(gh api -X POST repos/$REPO/git/commits --input /tmp/commit.json --jq '.sha')\n$ gh api -X PATCH repos/$REPO/git/refs/heads/pr-screenshots -f sha=\"$COMMIT\"\n```\n\nWhen the branch does not exist yet, omit `base_tree` and `parents`, then create\nthe ref with `POST git/refs -f ref=refs/heads/pr-screenshots -f sha=$COMMIT`.\n\n**Fetch one raw URL before posting.** A comment full of broken images is worse\nthan no comment, and the failure is invisible until someone opens the pull\nrequest:\n\n```console\n$ curl -s -o /dev/null -w '%{http_code}\\n' \\\n    https://raw.githubusercontent.com/<owner>/<repo>/pr-screenshots/<dir>/<name>.png\n```\n\n### 5. Post to the pull request\n\nUpsert a single comment rather than stacking one per run — find a previous\ncomment starting with `<!-- lite-screenshots -->` and PATCH it, else POST.\n\n```console\n$ jq -Rs '{body: .}' /tmp/section.md > /tmp/payload.json\n$ gh api \"repos/<owner>/<repo>/issues/<number>/comments\" --paginate \\\n    --jq 'map(select(.body | startswith(\"<!-- lite-screenshots -->\"))) | first | .id // empty'\n$ gh api -X PATCH \"repos/<owner>/<repo>/issues/comments/<id>\" --input /tmp/payload.json\n```\n\nDo not edit the pull request description: rewriting text a human owns risks\nclobbering their concurrent edits.\n\n### 6. Add the label\n\n`screenshots` is the record that they were posted. Add it once the comment is\nup, so the label says what is true:\n\n```console\n$ gh pr edit <number> --remove-label \"screenshots needed\" --add-label \"screenshots\"\n```\n\nCI labels any pull request with CSS in it `CSS`, and asks one that touches\nLite's for screenshots only if the change is visible; leave that label alone,\nit describes the diff.\n`screenshots needed` is one a human applies by hand when they do want them, and\nthe command above clears it if it is there.\n\nOnly after a comment with images actually went up. A pull request labelled\n`screenshots` with none attached is worse than an unlabelled one — it tells a\nreviewer the change has been shown when it has not. If you declined to post (an\nuncovered surface, a capture that failed), leave the labels as they are.\n\nScreenshots are asked for once per pull request and never again, so the label\nis final — a later push does not undo it, and a set posted here is not a claim\nabout code pushed after it. Recapture when the surfaces move; the command above\nis idempotent either way.\n\nIf you ever post images that are **not** captures of the running app — a page\nrendered from the branch's CSS, say, when the harness cannot be run — label them\nas such in the comment and mark it `<!-- lite-screenshots-replica -->` instead.\nA reviewer assumes a screenshot is a photograph of the app; anything else has to\nsay so, and the different marker keeps a later real run from overwriting it.\n\n## Before finishing\n\n- **Look at the images**, do not just trust exit codes. A green run proves files\n  were written, not that they show the surface.\n- Confirm the workspace is whole: `but status` should show the branch applied and\n  no stray uncommitted changes.\n\n## One-off capture\n\nFor a state the catalogue should not carry permanently. The run command filters\nby filename, so any spec whose name contains `screenshots` is picked up by the\nsame command and writes into the same `SCREENSHOT_OUT` directory — the compare\nstep cannot tell the difference.\n\nWrite `apps/lite/e2e/tests/screenshots-adhoc.spec.ts`, importing the shared\nhelpers rather than copying them (every workaround in them was earned, and a copy\nloses the comments explaining why):\n\n```ts\nimport { enabled, openProject, shoot } from \"../screenshot-helpers.ts\";\nimport { test } from \"../test.ts\";\n\ntest.describe(\"screenshots\", () => {\n\ttest.skip(!enabled, \"set SCREENSHOT_OUT to capture screenshots\");\n\ttest.describe.configure({ timeout: 180_000 });\n\ttest.use({ scenario: \"project-in-single-branch-three-branch-stack.sh\" });\n\n\ttest(\"<surface>\", async ({ appWindow }) => {\n\t\tawait openProject(appWindow);\n\t\t// ...reach the state...\n\t\tawait shoot(appWindow, \"<surface-name>\", \"<selector>\");\n\t});\n});\n```\n\nThe same fixture and selector rules as the catalogue apply — see below.\n\nThen capture both sides as usual, and:\n\n- **Delete the file** once the base run is done. It is untracked and not ignored,\n  so left behind it shows up as a stray change in `but status` and eventually in\n  someone's commit. The \"confirm the workspace is whole\" check at the end is what\n  catches this.\n- **Say in the comment** that the surface was captured ad hoc and is not covered\n  going forward, so nobody reads it as a regression guarantee.\n\n**Never run the two specs in one command.** The config sets `fullyParallel`, and\nthe catalogue spec wipes `SCREENSHOT_OUT` in a `beforeAll` — run concurrently, that\nwipe lands in the middle of the ad-hoc capture and deletes it. What survives is a\nplausible-looking directory missing exactly the surface you were asked for.\n\nThe trailing argument is a **substring match on the file path**, so a bare\n`screenshots` selects the ad-hoc spec as well and puts you straight into that race.\nName the file: `screenshots.spec.ts` for the catalogue, `screenshots-adhoc` for the\nad-hoc spec. Steps 1 and 2 above already do.\n\nCapture the ad-hoc surface **alone**, filtering to its filename, into its own\noutput directory on each side:\n\n```console\n$ env -u ELECTRON_RUN_AS_NODE SCREENSHOT_OUT=adhoc-head BUT=$PWD/target/debug/but \\\n    pnpm -F @gitbutler/lite test:e2e screenshots-adhoc\n```\n\nIts own directory also avoids inheriting stale PNGs from an earlier catalogue run,\nsince only the catalogue spec cleans up after itself. Then compare\n`adhoc-base` against `adhoc-head` as in step 3.\n\nWhen a change touches both a covered screen and an uncovered state, run the\ncatalogue first and the ad-hoc spec second, in separate commands, pointing both at\nthe same directory — that order is safe because only the first one wipes.\n\n## Extending the catalogue\n\nOnly after the developer has agreed (see the top of this document). Add the\nsurface to `apps/lite/e2e/tests/screenshots.spec.ts`, then capture as usual —\nthe same run proves the new surface works and shows the change. Coverage applies\nto every later pull request, not just this one.\n\n```ts\ntest.describe(\"<area>\", () => {\n\ttest.use({ scenario: \"<fixture>.sh\" });\n\n\ttest(\"<surface>\", async ({ appWindow }) => {\n\t\tawait openProject(appWindow);\n\t\tawait goToTab(appWindow, \"branches\"); // when it lives on another tab\n\t\tawait shoot(appWindow, \"<surface-name>\", \"<selector>\");\n\t});\n});\n```\n\n- `<surface-name>` becomes the filename and the heading a reviewer reads: name it\n  `commit-form`, not `outline-panel-2`.\n- Only fixtures calling `\"$BUT\" setup` register a project; the rest leave the app\n  on \"Select a project.\" and every capture fails. Known good:\n  `project-in-single-branch-three-branch-stack.sh`,\n  `project-with-remote-branches.sh`, `project-with-conflicting-commits.sh`.\n- If no fixture reaches the state, build it in the test — `uncommitted` writes\n  files into `testEnvironment.workdir/local-clone` and reloads.\n- Prefer a stable id for the clip (`#outline-panel`, `#details-panel`), else a\n  CSS-module prefix or an ARIA hook. Never nth-child or a generated class.\n- One screenshot per test, taken after a fresh load. This costs nothing here and\n  keeps the spec usable if CI capture is ever revisited.\n\n## Out of scope\n\n- **Native context menus** cannot be captured; they are OS windows.\n- **Dark mode** is not covered — the harness seeds `theme: \"light\"`.\n- **States needing a real remote or credentials** cannot be fixtured.\n",".agents/skills/tui-tests/SKILL.md":"---\nname: tui-tests\ndescription: Use when adding or modifying tests for one of GitButler's Ratatui TUIs\n---\n\n## Where tests live\n\n- Main TUI tests: `crates/but/src/command/legacy/status/tui/tests/`\n- Test harness/helpers: `crates/but/src/tui/test_utils.rs` and `crates/but/src/command/legacy/status/tui/tests/utils.rs`\n- Snapshots: `crates/but/src/command/legacy/status/tui/tests/snapshots/`\n\n## Basic pattern\n\n```rust\n#[test]\nfn describes_behavior_under_test() {\n    let env = Sandbox::init_scenario_with_target_and_default_settings(\"one-stack\").unwrap();\n    env.setup_metadata(&[\"A\"]).unwrap();\n\n    let mut tui = test_status_tui(env);\n\n    tui.input_then_render(KeyCode::Down)\n        .assert_rendered_term_svg_eq(file![\"snapshots/describes_behavior_under_test_001.svg\"]);\n}\n```\n\n## Driving the TUI\n\nUseful input examples:\n\n```rust\ntui.input_then_render(None);                                       // render without inputs\ntui.input_then_render('j');                                        // single char input\ntui.input_then_render(KeyCode::Down);                              // special key\ntui.input_then_render(Shift('j'));                                 // keys with shift\ntui.input_then_render(Control('j'));                               // keys with control\ntui.input_then_render([KeyCode::Down, KeyCode::Down]);             // multiple keys from array\ntui.input_then_render(\"commit message text\");                      // multiple keys from string\ntui.reload();                                                      // reload state after making external changes\n```\n\n## Assertions\n\nGenerally prefer\n\n- `assert_current_line_eq(str![...])` for cursor/selection behavior.\n- `assert_rendered_term_svg_eq(file![\"snapshots/test_function_name_001.svg\"])`\n  for everything else.\n\nGenerally you should include one `assert_rendered_term_svg_eq` per logical\ngroup of inputs, to catch bad states early.\n\nBe careful using `assert_rendered_contains` and `assert_rendered_not_contains`\nsince they might lead to false positives. They're intended to use while\niterating on a test where snapshots would cause too much churn.\n\nRead `crates/but/src/tui/test_utils.rs` and\n`crates/but/src/command/legacy/status/tui/tests/utils.rs` for more specialized\nassertions.\n\nYou're not allowed to add new kinds of assertions to `crates/but/src/tui/test_utils.rs` or\n`crates/but/src/command/legacy/status/tui/tests/utils.rs`. Rely entirely on the\nexisting assertions.\n\nDon't reuse snapshots from other tests. Give each test its own snapshots.\n\nAvoid using `tui.env().invoke_git(\"...\")`. Prefer performing operations via the TUI.\n\n## Running tests\n\n- `cargo test -p but <test-name>` to run one test.\n- `SNAPSHOTS=overwrite cargo test -p but <test-name>` to run and update\n  snapshots.\n- `cargo test -p but tui` to run all tui tests. Do this after changing\n  things.\n\nIf a test fails the output will include the rendered state of the test backend.\nThis can be used when iterating on a test as a way of inspecting the state.\n",".github/copilot-instructions.md":"## General information\n\nThis is a Rust/Svelte/React/TypeScript monorepo with multiple projects. For\nagent instruction precedence and scoped guidance, follow the nearest applicable\n`AGENTS.md`; use this file as Copilot-oriented project context.\n\nThe main applications are found in the `apps` directory:\n\n- `desktop` containing the Tauri application's frontend code\n- `web` containing the web application's frontend code\n- `lite` containing the Electron/React desktop app\n\nRust code is found in the `crates` directory. It contains `gitbutler-tauri` for\nthe Tauri application, `but-api` for the API surface used by Tauri, Electron,\nCLI, and TUI callers, and the `but` CLI crate.\n\nThe `packages` directory contains different self-contained npm packages.\nThese are shared between the applications.\nThe packages are:\n\n- `but-sdk` containing the generated TypeScript/Node SDK for Rust APIs\n- `core` containing shared TypeScript helpers\n- `ui` containing the shared UI components\n- `shared` containing the shared types and utils\n- `no-cross-domain-imports` containing the no-cross-domain-imports ESLint package\n- `no-relative-imports` containing the no-relative-imports ESLint package\n- `svelte-comment-injector` containing the Svelte comment injector package\n\nThe `e2e` directory contains Playwright, WebdriverIO, and blackbox end-to-end\ntests.\n\n## Technology Stack\n\n- **Frontend**: Svelte with TypeScript\n- **Backend**: Rust\n- **Desktop Framework**: Tauri (like Electron but using Rust instead of Node.js)\n- **Build Tools**: Turborepo for monorepo management, pnpm for package management\n- **Git Libraries**: gix (gitoxide) and git2 (libgit2)\n- **Testing**: Vitest for JS/TS, Playwright for E2E tests, standard Rust testing framework\n\n## Development Setup\n\n### Prerequisites\n\n1. **Rust**: as specified in `rust-toolchain.toml`. Install via rustup.\n2. **pnpm**: Install standalone pnpm as described in [DEVELOPMENT.md](../DEVELOPMENT.md#prerequisites).\n3. **System Dependencies**: Tauri requires platform-specific dependencies (see DEVELOPMENT.md for details).\n\n### Initial Setup\n\n```bash\n# Clone the repository\ngit clone https://github.com/gitbutlerapp/gitbutler.git\ncd gitbutler\n\n# Install dependencies and the project-managed Node.js runtime\npnpm install\n\n# Build Rust binaries (required before running the app)\n# look at https://github.com/gitbutlerapp/gitbutler/blob/fd9a58de5579074c0526193143d531c21907a26b/scripts/install-tauri-debian-dependencies.sh\n# for Linux prerequisites for tauri.\ncargo build\n\n# or use this to build the `but` CLI, without the need for tauri dependencies.\ncargo build -p but\n```\n\n## Building and Running\n\n### Development\n\n```bash\n# Run desktop app in development mode\npnpm dev:desktop\n\n# Run with debug logs\nLOG_LEVEL=debug pnpm dev:desktop\n\n# Run web app\npnpm dev:web\n\n# Run UI component storybook\npnpm dev:ui\n```\n\n### Building\n\n```bash\n# Build all packages\npnpm build\n\n# Build desktop app only\npnpm build:desktop\n\n# Build for production (used for releases)\npnpm tauri build --features devtools --config crates/gitbutler-tauri/tauri.conf.nightly.json\n```\n\n## Testing\n\nWhen asked about frontend tests please read `@frontend.md` for context.\n\nTo run rust tests run `cargo test`, and for specific crates `cargo test -p CRATE_NAME`\n\n## Code Style and Linting\n\n### JavaScript/TypeScript\n\n**Linting**: ESLint with TypeScript and Svelte plugins\n**Formatting**: Prettier with Svelte plugin\n\n```bash\n# Check linting\npnpm lint\n\n# Fix linting issues\npnpm fix\n\n# Format code\npnpm format\n\n# Check formatting\npnpm prettier\n\n# Shortcut to check everything\npnpm isgood\n\n# Shortcut to fix and format everything\npnpm begood\n```\n\n**Important ESLint Rules**:\n\n- No relative imports (use `@gitbutler/` package references)\n- Import order: alphabetically sorted with specific group ordering\n- No console.log (use console.warn or console.error)\n- Prefer function declarations over arrow functions at top level\n- Svelte-specific rules for button types, unused props, etc.\n\n**Prettier Config**:\n\n- Tabs for indentation\n- Double quotes\n- No trailing commas\n- 100 character line width\n\n### Rust\n\n**Formatting**: Use rustfmt with nightly toolchain for imports grouping\n\n```bash\n# Format Rust code\ncargo fmt\n\n# Format with nightly settings\npnpm rustfmt\n# or (but don't do this unless you are asked to)\ncargo +nightly fmt -- --config-path rustfmt-nightly.toml\n\n# Run clippy for linting\ncargo clippy --all-targets\n```\n\n**Rust Formatting**:\n\n- Group imports: Std, External, Crate\n- Imports granularity: Crate level\n\n**temporary directories** - use `but-testsupport` for scenario creation. Read-only functions should use read-only fixtures. DO NOT USE the `std::env::temp_dir().join(format!(…)` pattern.\n\n## Dependency Management\n\n### Adding JavaScript/TypeScript Dependencies\n\n```bash\n# Add to root workspace\npnpm add -D <package>\n\n# Add to specific package\npnpm add <package> --filter @gitbutler/desktop\npnpm add <package> --filter @gitbutler/ui-svelte\n```\n\n**Always check for vulnerabilities before adding npm packages** (ecosystem: npm).\n\n### Adding Rust Dependencies\n\nEdit the appropriate `Cargo.toml` file:\n\n- Workspace-level dependencies go in `/Cargo.toml` under `[workspace.dependencies]`\n- Individual crate dependencies reference workspace versions when possible, and when it's used more than once.\n\n**Always check for vulnerabilities before adding Rust dependencies** (ecosystem: rust).\n\n## Monorepo Structure\n\n### Workspace Navigation\n\nThe repository uses:\n\n- **Turborepo** for JavaScript/TypeScript build orchestration\n- **Cargo workspace** for Rust crate management\n- **pnpm workspace** for npm package management\n\nWhen making changes:\n\n1. Identify which package/crate is affected\n2. Run builds/tests in that package first\n3. Then verify dependent packages still work\n\n### Crate Organization\n\nThe `crates/` directory contains ~65 Rust crates organized by functionality:\n\n- `gitbutler-*` prefix: Core GitButler functionality. These are old, and we want to port them to `but-*`.\n- `but-*` prefix: Backend utilities and services\n- Many crates have specific purposes (see DEVELOPMENT.md \"Code Hitlist\" for technical debt items)\n\n**Note**: Some crates are marked for refactoring (see DEVELOPMENT.md). Be cautious when modifying:\n\n- gitbutler-reference\n- gitbutler-branch-actions\n- gitbutler-repository\n\n## CI/CD\n\n### Workflows\n\nLocated in `.github/workflows/`:\n\n- `push.yaml`: Main CI for linting, building, and testing on push\n- `publish.yaml`: Release builds for different platforms\n- `test-e2e.yml`: E2E tests (Playwright and blackbox)\n- `test-client-fe-integration.yml`: Frontend integration tests\n\n### Pre-commit Checks\n\nBefore committing, ensure:\n\n1. Code is formatted: `pnpm format && pnpm rustfmt`\n2. Linting passes: `pnpm lint && cargo clippy --all-targets`\n3. Tests pass: `pnpm test && cargo test`\n4. Build succeeds: `pnpm build`\n\nOr use the shortcut: `pnpm isgood`.\nAuto-fix with `pnpm begood && cargo clippy --fix --all-targets`\n\n## Common Patterns and Conventions\n\n### Frontend File Organization\n\n- Use absolute imports via package references (e.g., `@gitbutler/ui-svelte`) instead of relative imports\n- Components should be in logical directories by feature\n- Shared utilities go in `packages/shared`\n- UI components go in `packages/ui-svelte`\n\n### Naming Conventions\n\n**JavaScript/TypeScript**:\n\n- Components: PascalCase (e.g., `BranchCard.svelte`)\n- Files: kebab-case (e.g., `branch-service.ts`)\n- Variables/functions: camelCase\n\n**Rust**:\n\n- Follow standard Rust naming conventions\n\n### Error Handling\n\n**JavaScript/TypeScript**: Throw errors and handle with try/catch at appropriate boundaries\n\n**Rust**: Use `Result<T, E>` and `anyhow` for error handling. Most functions should return `anyhow::Result`.\n\n## Troubleshooting\n\n### Build Issues\n\n```bash\n# Clear Turbo cache\npnpm exec turbo daemon stop\npnpm exec turbo daemon clean\n\n# Clear node_modules and reinstall\nrm -rf .turbo node_modules\npnpm install\n\n# Clear Rust build artifacts\ncargo clean\n```\n\n### Platform-Specific Issues\n\n- **macOS**: Ensure Xcode and cmake are installed\n- **Linux**: Install webkit2gtk and other dependencies (see DEVELOPMENT.md)\n- **Windows**: May require Perl, OpenSSL setup, and specific compiler settings (see DEVELOPMENT.md)\n\n## Performance Considerations\n\n### Development Builds\n\n- Set `GITBUTLER_PERFORMANCE_LOG=1` for performance logging\n- Use `--release` flag for more realistic performance testing\n\n### Turbo Cache\n\n- Turborepo caches build outputs\n- If experiencing stale builds, clear cache: `pnpm exec turbo daemon clean`\n- Case-sensitive filesystem issues may affect Turbo on macOS (see DEVELOPMENT.md)\n\n## Additional Resources\n\n- **Development Guide**: See DEVELOPMENT.md for detailed setup instructions\n- **Contributing Guide**: See CONTRIBUTING.md for contribution guidelines\n- **Discord**: https://discord.gg/MmFkmaJ42D for questions and discussions\n- See `crates/AGENTS.md` for Rust-specific API, repository, and testing conventions\n\n## Important Notes for AI Coding Agents\n\n1. **Always run lints and tests** after making changes\n2. **Use the existing patterns** in the codebase - don't introduce new patterns without discussion\n3. **Keep changes minimal** - surgical changes are preferred over large refactors\n4. **Follow the monorepo structure** - understand which package your change affects\n5. **Check both JS and Rust** - changes to the backend often require frontend updates\n6. **Documentation** - update relevant docs if changing public APIs or user-facing features\n7. **Avoid relative imports** - ESLint will error on relative imports; use package references\n8. **Test on target platform** if making platform-specific changes\n9. **Security**: Check dependencies for vulnerabilities before adding them\n10. **Code marked for refactoring**: Be extra careful with crates in the \"Code Hitlist\" section\n11. **but CLI happy path testing only**: CLI tests are expensive and should be limited to what really matters.\n\n## Review Guidelines\n\nFollow these guidelines when reviewing code.\n\n- Do not report type errors. CI will catch those.\n- Don't review formatting such as commas, indentation, etc\n","AGENTS.md":"# GitButler Agent Instructions\n\nGitButler is a Rust/Svelte/React/TypeScript monorepo.\n\nApply all relevant instruction files. If instructions conflict, resolve them in\nthis order:\n\n1. Explicit human instructions\n2. Nearest nested `AGENTS.md`\n3. This file\n\n## Repo Map\n\n- `crates/` - Rust crates.\n- `apps/desktop/` - Tauri/Svelte desktop app.\n- `apps/web/` - Svelte web app.\n- `apps/lite/` - Electron/React desktop app.\n- `packages/` - shared TypeScript packages, including the SDK.\n- `e2e/` - Playwright, WebdriverIO, and blackbox end-to-end tests.\n\n## Working Style\n\n- Treat questions about the codebase as read-only unless the user asks for changes.\n- Make focused, reviewable changes; avoid unrelated rewrites.\n- Use the simplest design that solves the actual problem; do not add\n  speculative machinery, and remove machinery your change makes unnecessary.\n- Inspect nearby code before introducing patterns.\n- Prefer existing APIs, tests, and conventions.\n- Before declaring shared behavior done, check each applicable surface and contract\n  (desktop, web, Lite, CLI/TUI, N-API, SDK, and docs) and update it or explicitly\n  determine that it is unaffected.\n- Run targeted validation for the area touched.\n- Before adding new machinery to fix a behavior bug, reproduce the bug in a failing\n  test and survey the target file's existing loops and classifications as candidate\n  hosts; let the tests, not the diagnosis, set how much implementation the fix needs.\n- When a fix calls for a new mechanism (a new module, a new public API, or a parallel\n  walk where one already exists), propose the intended shape before building it.\n\n## Scoped Instructions\n\n- For Rust work under `crates/`, follow `crates/AGENTS.md`.\n- For Lite work under `apps/lite/`, follow `apps/lite/AGENTS.md`.\n- For the React component library under `packages/ui-react/`, follow\n  `packages/ui-react/AGENTS.md`.\n","claude.md":"AGENTS.md","crates/AGENTS.md":"# Rust Instructions\n\nThese are defaults for Rust source and test changes under `crates/`; more\nspecific instruction files override them. Do not apply them to generated,\nvendored, or fixture data unless the task is specifically about that code.\n\n## API Boundaries and Legacy\n\n- Treat `gitbutler-*` crates as legacy-heavy, but preserve local ownership and\n  nearby patterns for localized fixes. Do not introduce new `gitbutler-*` usage\n  in newer code unless the surrounding legacy boundary requires it. Keep legacy\n  fixes localized; migrate only when necessary, tiny, tested, and\n  behavior-neutral.\n- Minimize new `VirtualBranchesHandle` usage. At API boundaries, prefer\n  `but_ctx::Context` workspace helpers and newer `but-*` APIs.\n- `but-api` is the API surface for Tauri, Electron/N-API, CLI, and TUI callers.\n  Outer callers should prefer existing `but-api` functions when they fit, but\n  lower-level crates should not depend on `but-api`.\n- Preserve existing `but-api` macro, transport, serialization, and conversion\n  patterns instead of hand-rolling parallel wrappers.\n- Keep transport DTOs at the API boundary, commonly in a local `json` module,\n  and convert them to domain types before calling lower-level crates.\n- For permissioned `but-api` functions, follow the existing composition shape:\n  acquire permission near the wrapper and delegate to a `_with_perm` or other\n  permission-taking implementation.\n- API consumers such as the CLI, TUI, and other composed callers should use\n  `_with_perm` variants when they already hold permission to avoid extra locking\n  and deadlock risk.\n- Keep `Context` at API/composition boundaries; pass granular dependencies\n  like repo, workspace, metadata, database handles, or an `Editor` into\n  lower-level crates where possible.\n- Acquire repository access locks at top-level API/command boundaries. Do not\n  call helpers that acquire permissions while holding a guard; drop the guard or\n  call a permission-taking helper.\n- Preserve existing `DryRun` semantics; add dry-run support when the API/product\n  surface calls for preview behavior. Dry runs should avoid persisting refs,\n  objects, or oplog entries.\n- When an API offers both action-only and timeline-recording behavior, keep the\n  `*_only*` function free of oplog side effects; prepare a best-effort oplog\n  snapshot in the wrapper and commit it only after the mutation succeeds.\n- New user-facing mutations should participate in undo: record an oplog\n  snapshot via the existing `SnapshotDetails`/`OperationKind` wrapper pattern,\n  and call out any mutation intentionally left out of the timeline rather than\n  omitting it silently. Undo is snapshot-based; do not design bespoke inverse\n  operations for it.\n- Before changing or reviewing code that derives graph/workspace/branch/stack/commit\n  relationships, reachability, dependencies, ordering, operation targets, or Git\n  graph/history/ref-placement mutations, use `crates/WORKSPACE_MODEL.md` as the\n  reference. In short: prefer commit IDs and refs at API boundaries, convert to\n  operation-local selectors inside editor-backed operations, use\n  `but_graph::Graph` for relationship/reachability questions, use\n  `but_rebase::graph_rebase::Editor` for Git graph/history/ref rewrites where an\n  editor model exists, and treat `but_graph::Workspace` and\n  `but_workspace::RefInfo` as lossy presentation/compatibility views unless an\n  existing workspace-shaped boundary requires them.\n\n## Code Shape and Naming\n\n- Use Git/GitButler domain names and keep types/helpers in the crate or module\n  that owns the concept.\n- Avoid vague names like `Manager`, `Service`, `Helper`, `Util`, or `Processor`\n  unless nearby code uses that concept.\n- Solve the present problem directly. Avoid speculative abstractions: one-use\n  traits/types/functions, fake extension points, ceremonial wrappers/modules,\n  and public APIs larger than real callers need.\n- Keep module boundaries discoverable from names and the call graph.\n- Avoid drive-by refactors while fixing a specific bug.\n- Prefer explicit domain enum matches when wildcard arms would hide behavior.\n- Comment public API purpose/invariants/errors and complex algorithm intent;\n  avoid comments that merely repeat the code.\n\n## Git Repository Semantics\n\n- Do not rediscover Git repositories inside business logic; pass repo/context\n  explicitly.\n- Prefer repository APIs over shelling out to `git`, except at\n  shell/executable boundaries such as hooks, debug tooling, tests, or Git\n  interop helpers.\n- Preserve Git graph semantics even when the shape looks redundant or odd; do\n  not deduplicate, reorder, or smooth graph data unless Git semantics allow it.\n- Use `gix` for new repository logic. Treat `git2` and `Context::git2_repo` as\n  legacy/boundary-only escape hatches for libgit2 checkout/index, hooks,\n  transport/auth, or existing code that cannot reasonably move yet.\n- Keep Git paths, refnames, commit messages, and diff payloads byte-preserving\n  until UI/API boundaries; avoid lossy `String` conversion in business logic.\n- Treat filesystem and worktree writes carefully; use existing locking,\n  transaction, and access patterns.\n- Avoid implicit `SystemTime::now()` in testable business logic; pass time in\n  when deterministic behavior matters.\n- Use existing reload/invalidation helpers after external repo or worktree\n  changes, especially when working through `Context` caches.\n- Use `anyhow::Context` to explain what operation failed. When frontend/API\n  consumers need classification, use existing `but_error::Code` patterns; do\n  not make consumers match error strings.\n\n## Database Migrations (`but-db`)\n\n- Keep migrations forward-compatible: stay on the current `SchemaVersion` and\n  leave columns or tables that new code stopped using in place. Bumping the\n  version locks every older binary out of the database; reserve it for planned,\n  coordinated breaks, never routine cleanup.\n\n## Version Control\n\n- Assume the worktree may contain other agents' changes. Do not overwrite, clean\n  up, stage, commit, or amend changes you did not make.\n- When asked to branch, commit, push, or open a PR, use the GitButler `but`\n  CLI/workflow when available.\n- When the user says \"ship it\", commit your changes on a session branch,\n  creating one if needed, then push and open or update the PR. Reuse the\n  existing branch/PR when it already fits the session.\n- For small cleanup or follow-up fixes on your own branch, amend the relevant\n  existing commit(s) when that is the cleaner history.\n- Keep commit messages and PR descriptions succinct: why, impact, and core\n  decisions. Do not list local validation commands in commit messages or add AI\n  co-author trailers or tool branding.\n\n## Testing and Validation\n\n- Run the narrowest relevant test or check first, for example `cargo test\n  -p <crate> <test-name>` or `cargo check -p <crate> --all-targets`.\n- Use existing test-support crates and fixtures; prefer read-only fixtures for\n  read-only behavior.\n- For graph, rebase, or workspace behavior, prefer fixture-backed before/after\n  snapshots using existing visualizers, plus targeted structural assertions for\n  the invariant.\n- When snapshot output is volatile, stabilize inputs or normalize output; do not\n  replace strong snapshots with vague assertions.\n- When optimizing Git traversal or workspace behavior, preserve semantics with a\n  fixture-backed regression test.\n- Run `cargo fmt` for Rust formatting, avoiding unrelated dirty files.\n- Run `cargo clippy --fix --allow-dirty` only after inspecting the diff scope and\n  only when generated edits stay within the intended change.\n- Run `cargo machete` when dependencies changed; verify targets, features, and\n  platform-specific usage before removing dependencies.\n- After changing Rust APIs or types exposed through `@gitbutler/but-sdk`, run\n  `pnpm build:sdk && pnpm format`; this updates\n  `packages/but-sdk/src/generated`.\n- Use workspace-wide checks only when the change affects shared contracts or\n  multiple crates.\n\n## Assertions\n\n- Explain why a standard Rust assertion holds concisely using the last-argument message, like `assert!(1!=2, \"arithmetic unit on CPU works\")`\n- Snapshot assertions use `snapbox`, like `snapbox::assert_data_eq!(actual, snapbox::str![[r#\"...\"#]])`. `snapbox::assert_data_eq!` has no message argument, so explain why a snapshot holds with a `// comment` on the line above it.\n- Regenerate inline snapshots by running the tests with `SNAPSHOTS=overwrite`.\n- Remove unstable output before asserting (sanitize ids/timestamps/paths, e.g. the `but_testsupport` helpers) rather than snapshotting it raw. `snapbox` also pattern-matches expected data by default (`[..]` and `...` wildcards, path-separator normalization); append `.raw()` to the expected `snapbox::str!` when the value must match exactly (e.g. it contains backslashes or literal `[..]`/`...`).\n","crates/CLAUDE.md":"AGENTS.md","crates/but-agentlog/skill/SKILL.md":"---\nname: but-agentlog\nversion: 0.0.0\ndescription: \"Use for prompts like \\\"get context for branch\\\", \\\"catch up on this branch\\\", \\\"recover branch context\\\", or \\\"what prior agent work happened here\\\". Skim prior agent work from `but agentlog` captures for a GitButler branch, review / pull request / merge request, or change. Prefer this over generic git branch/diff inspection when the user asks for context, history, prior work, or branch catch-up.\"\nauthor: GitButler Team\n---\n\n# GitButler Agentlog Skill\n\nUse `but agentlog` to skim prior agent work from captured sessions.\n\nTrigger this skill for plain prompts like \"get context for branch\", \"branch\ncontext\", \"catch up on this branch\", \"recover context\", \"what happened on this\nbranch\", or \"what prior agent work happened here\".\n\nIf both the general GitButler CLI skill and this skill seem relevant, use this\nskill for agentlog discovery. Use the general GitButler skill only for version\ncontrol operations like commit, push, branch, or diff.\n\n## Core Workflow\n\nDo not search for or read local `SKILL.md` files. If this skill is active, use\nthese instructions directly. Do not inspect the general GitButler skill first.\nStart with `but agentlog skim`.\n\nStart with `skim` when prior work could affect your next action. It is a\ntable-of-contents view: all related sessions and turns, abbreviated and shown\nchronologically.\n\n```sh\nbut agentlog skim\nbut agentlog skim branch <branch-name-or-ref>\nbut agentlog skim review <review-id-or-pull-request-key>\nbut agentlog skim change <change-id-or-key>\n```\n\nUse `show` only when `skim` is too thin, ambiguous, or you need exact evidence:\n\n```sh\nbut agentlog show <session-key> --limit 20\nbut agentlog show <session-key> --turn <turn-key> --limit 20\n```\n\nFor normal current-branch recovery, run `skim` without target arguments first.\nIt discovers the applied GitButler branch itself. Do not run plain Git commands\nlike `git branch --show-current` or `git status` first.\n\n```sh\nbut agentlog skim\n```\n\nPass an explicit target only when the user gives one.\n\nUse JSON only when you need exact handles for drill-down:\n\n```sh\nbut --json agentlog skim\n```\n\n1. Start with `skim` for a clean orientation.\n2. Treat `skim` like compacted turn history: all related sessions and turns are\n   present, but each turn is abbreviated. It is not the full transcript.\n3. If `skim` is enough for a lightweight status answer, summarize it and stop.\n4. Drill down only when `skim` is ambiguous, misses the rationale, or you need\n   exact evidence.\n5. To drill down, rerun `skim` with `--json` to get the relevant\n   `session_key` and `turn_key`, then use `show`.\n6. Use `show <session-key>` for turn-level context.\n7. Use `show <session-key> --turn <turn-key>` only for turns that need exact detail.\n\n## Target Discovery\n\nWhen the user says \"current branch\" or gives no explicit target, run:\n\n```sh\nbut agentlog skim\n```\n\n`skim` performs GitButler branch discovery automatically. If target discovery\nfails, then inspect GitButler state:\n\n```sh\nbut status\n```\n\nDo not use plain Git's `gitbutler/workspace` branch as an agentlog target.\n\n## Rules\n\n- Prefer orientation before payload.\n- Prefer `skim` for turn-history recovery.\n- Treat `skim` as complete but abbreviated. It includes every related session\n  and every turn in those sessions, not every record or the full transcript.\n- Prefer human `skim` output first. Use `--json` only for drill-down\n  handles or exact evidence.\n- Run `show` when `skim` is thin, ambiguous, missing the why, or when the user\n  is asking you to make or verify a consequential claim.\n- Do not dump records for every candidate session.\n- Do not dump full records or transcripts. If session detail is needed, start\n  with `show <session-key> --limit 20` and summarize previews.\n- Treat matches as related evidence, not ownership.\n- Keep `session_key` and `turn_key` values internal unless the user asks for\n  evidence handles or they are needed for a follow-up command.\n- Use record snippets only when they justify the next action.\n- Say \"session related to branch X via turn Y\", not \"session for branch X\".\n\n## Reading `skim`\n\nUse `skim` to answer \"what has already been discussed, planned, attempted, or\nchanged in this work?\"\n\nHuman output is intentionally compact but includes every related session and\nevery turn in chronological order. JSON output keeps\nfull `session_key` and `turn_key` values so you can drill into `show` without\nprinting those handles to the user.\n\nThe skim is a compressed table of contents. It abbreviates each turn but does\nnot omit related sessions or turns.\n\nUseful fields:\n\n- `target_kind` and `target_key`: target used for discovery.\n- `sessions`: related sessions in chronological order, with counts and previews.\n- `coverage`: shown sessions, shown turns, and direct related turn count.\n- `sessions[].turns`: every turn in order, with labels and previews.\n\nIf `skim` has enough context for a lightweight answer, summarize from it and\nstop. If it is ambiguous or misses the why, use JSON skim handles and drill\ndown with `show`.\n\n## Reading `show`\n\nUse `show` to open a session or one turn.\n\n```sh\nbut agentlog show <session-key> --limit 20\nbut agentlog show <session-key> --turn <turn-key> --limit 20\n```\n\nWithout `--turn`, `show` answers what happened in a session at turn granularity.\n\nUseful fields:\n\n- `coverage`: returned turns versus total turns.\n- `turn_key`: handle for `show --turn`.\n- `turn_index`: order within the session.\n- `capture_kind`: backfill or incremental.\n- `record_count`: turn size before hydrating records.\n- `source_record_index_range`: provider/source indexes covered by the turn.\n- `observed_targets`: targets observed in the turn.\n- `latest_user_preview` and `latest_assistant_preview`: compact orientation.\n- `tool_counts`: tool call/result counts and tool names.\n\nIncrease `--limit` only when the current window misses the related turn or setup.\n\nWith `--turn`, `show` answers what exactly happened inside one turn.\n\nUseful record fields:\n\n- `coverage`: returned records versus total records in the turn.\n- `turn_record_index`: order within the turn.\n- `source_record_index`: original provider/source index when available.\n- `timestamp`, `kind`, `role`, `text`: message/tool orientation.\n- `tool_name` and `tool_input`: tool details when present.\n- `source_record`: redacted stored provider envelope for low-level debugging.\n\nTurn records are intentionally bounded. Increase `--limit` only for a specific\nturn after session-level `show` output proves that more detail is needed.\n\n## Skim Summary\n\nWhen summarizing a skim, do not say you recovered \"full context.\" Say you\nskimmed the full turn history in abbreviated form.\n\nInclude:\n\n- target used for discovery\n- previews explaining intent and outcome\n- minimal record snippets for the next action\n\nDo not lead with raw session or turn identifiers. Mention them only when the\nuser asks for evidence handles or when passing them to another command/tool.\n","crates/but/skill/SKILL.md":"---\nname: but\nversion: 0.0.0\ndescription: \"Commit, push, branch, and manage version control with GitButler. Use for commits, selective dirty-file or hunk commits, branches, diffs, PRs, history edits, squashes, amends, undo, merge, apply, and unapply. For selected dirty files or hunks, inspect with `but diff`; use compact `but status` for commit order, branch/stack placement, or conflict overview; use `but status -fv` when file/hunk IDs or per-commit file details matter. Replaces git write commands.\"\nauthor: GitButler Team\n---\n\n# GitButler CLI Skill\n\nUse GitButler CLI (`but`) as the default version-control interface.\n\n## Start Here\n\nChoose the narrowest first command by task; avoid ritual status checks:\n\n```bash\n# Selected dirty files/hunks:\nbut diff\n\n# Commit order, branch/stack placement, conflict overview:\nbut status\n\n# File/hunk IDs, per-commit files, amend/split details:\nbut status -fv\n\n# Details for one known branch or commit:\nbut show <id>\n```\n\nDo not run plain `but status` and then `but status -fv` unless the compact output lacks file/hunk details needed for the task.\n\nFor \"commit just/only/specific changes on a new branch\", use the fast path:\n\n```bash\nbut diff\nbut commit -b <branch> -m \"<msg>\" <id> <id>\n```\n\n`but commit -b <branch> ...` creates the branch when it does not exist and prints the created commit. Do not run a separate `but branch new`, status command, or verification diff unless the task needs workspace information that the result does not provide.\n\n## IDs\n\nThe first token on each `but diff` / `but status` line is that line's ID. When a command needs an ID, copy it exactly from the current output; never hardcode or invent one. ID lifetimes differ by entity:\n\n- Changes and sources are **positional, space-separated** IDs (`but commit -b feat -m \"msg\" uvw:2e4 uvw:e2c`). An uncommitted hunk ID is written `<file-id>:<hunk-id>` (e.g. `uvw:2e4`, copied from bare `but diff`) — the part after the colon is the hunk's ID, **not** a line range (`uvw:16-40` is invalid). Do not invent flags like `--changes` / `--hunk` / `--ids`, pass a line range, or comma-separate IDs — `uvw,qyo` is parsed as one ID and fails.\n- `but diff` is the exception: it accepts at most **one** target. Bare `but diff` shows all uncommitted files; inspect committed files or other entities one target at a time — never `but diff <id> <id>`.\n- A committed file is `<commit-id>:<file-id>` (e.g. `mzm:uvw`, shown under each commit). A committed hunk is `<commit-id>:<file-id>:<hunk-id>` and is shown by `but diff <commit-id>`. `@` means the uncommitted area.\n- Commit IDs are stable change IDs that survive history edits (`amend`, `squash`, `move`, `uncommit`, `reword`). Commits without a change ID (e.g. upstream-only) lead with a sha prefix instead, and `#N`-suffixed refs disambiguate duplicates — both go stale after history edits, and a stale sha can silently resolve to the wrong commit. The `(sha …)` on verbose commit lines is informational — do not pass it to commands.\n- Branch short IDs are snapshot-local selectors. Use full branch names for branch-targeting mutations; short IDs are safe only for immediate read-only inspection.\n- File/hunk IDs copied from one diff read generally remain usable across chained commits. If one stops resolving, re-read `but diff` and retry.\n\n**Chaining:** mutation output is concise by default, so you may chain mutations with `&&` off one inspection read. Do not chain branch short IDs through mutations; use full branch names. Add `--status-after` only when the next step needs workspace IDs or details that the mutation result does not provide. Chained `but commit` calls stack in the order written — the first is oldest, each later one goes on top. History edits may run in sequence when every commit ref involved is a change-ID ref; run them one at a time with `--status-after` when a ref is sha-based or `#N`-suffixed, or when the next command needs freshly issued IDs. Chaining `but uncommit <id> && but diff` is safe because bare `diff` needs no ID from the uncommit output.\n\n## Non-Negotiable Rules\n\n1. Use `but` for all write operations. Never run `git add`, `git commit`, `git push`, `git checkout`, `git merge`, `git rebase`, `git stash`, or `git cherry-pick`. The only exception is selecting a side of a conflicted submodule in resolution mode (see Resolve conflicted commits). If the user says a `git` write command, translate it to `but` and run that. Running from a linked worktree shows the same workspace and IDs as the main worktree, and `@` still means the main worktree's changes; a bare `but commit` or `but diff` takes that worktree's changes, a bare path names a file there, and `<worktree>:@` names them from anywhere.\n2. Mutation commands print their result without appending workspace status. Add `--status-after` only when the next step needs resulting workspace IDs or details; otherwise trust the mutation result and do not run a verification status/diff.\n3. Branches marked `(merged upstream)` have landed; start new work on another branch. That marker alone does not mean `but pull` removes them: run `but pull --check` first. `but pull` removes only branches listed `[integrated]` (\"has been integrated upstream and removed locally\", or \"has been merged into <target> and removed\" when the target has no upstream) and rebases the rest, which stay applied. To clear a landed branch that stays applied, use `but unapply <branch>` (acts on its whole stack; `but apply <branch>` restores it) rather than `but discard`. `push` and mutations (`commit`, `amend`, `squash`, `uncommit`, `reword`, `move`) refuse landed branches and commits, `absorb` skips them with a notice, and `commit` skips them when picking a default target.\n4. In non-interactive CLI workflows, do not narrate progress between routine commands. Execute the needed `but` commands and give a concise final summary.\n5. Prefer this skill and `but skill reference` over exploratory help calls. Use `<command> --help` when required syntax is missing or a command fails; use top-level help only when you genuinely need to discover an undocumented command.\n\n## Command Patterns\n\n- Initialize a Git repository: `but init` (current directory) or `but -C <path> init`. Non-interactive; creates no commits, remotes, or GitButler workspace. Existing repositories are left unchanged.\n\n- Commit: `but commit -b <branch> -m \"<msg>\" <id> <id>` — `-b <branch>` creates the branch if it does not exist\n- Commit everything uncommitted: `but commit -b <branch> -m \"<msg>\"` (omit the IDs)\n- Several commits from one diff: chain `but commit` calls with `&&` (commits stack oldest-first)\n- Commit at a specific history position: `--above <commit-or-branch>` or `--below <commit-or-branch>` (mutually exclusive). With a branch target, add `-b <new-name>` to name the new branch; otherwise its name is generated. The name must not already exist. Do not combine `-b` (even without a name) with commit/worktree targets.\n- Targeting is **required** when more than one **stack** is applied; without it `but commit` fails with \"Unclear where to commit. Found more than one stack\". Several branches stacked together count as one stack — an untargeted commit then silently lands on the stack's top branch, so pass `-b` whenever the branch matters.\n- Always pass `-m \"<msg>\"` (or `-F -` with a heredoc, `-F <file>` with a file outside the repository, or `--no-message`) to `but commit`, and to `but squash` whenever its sources are commits or branches unless the target is `@` — those compose a new message; without a flag an agent run keeps whatever the command composed (empty for `but commit`, the joined source messages for `but squash`) and a terminal opens an editor. Squash sources that are uncommitted or committed changes reuse the target's message and need no flag; squashing into `@` rejects message flags outright.\n- Amend: `but amend -t <commit-or-branch> <file-or-hunk-id> <file-or-hunk-id>` — a branch target resolves to its newest commit\n- Uncommit: `but uncommit <commit-id>` (whole commit), `but uncommit <branch>` (all commits and remove branch), `but uncommit <commit-id>:<file-id>` (committed file), or `but uncommit <commit-id>:<file-id>:<hunk-id>` (committed hunk); committed files and hunks may be mixed, but all must come from one commit\n- Insert empty commit: `but commit --empty -b <branch> -m \"<msg>\"`\n- Pick onto a new named branch: `but pick <commit-sha> --above <branch> -b <new-name>` (`--below` also works). The name must not exist; `-b` cannot be combined with commit/worktree targets.\n- Squash commits: `but squash <source-commit-id> [<source-commit-id>...] -t <target-commit-id> -m \"<msg>\"`\n- Move committed changes into an existing commit or `@`: `but squash <commit-id>:<file-id> -t <commit-id-or-@>` for a committed file; `but squash <commit-id>:<file-id>:<hunk-id> -t <commit-id-or-@>` for a committed hunk; all sources must come from one commit\n- Move committed changes into a new commit at a chosen position: `but move <commit-id>:<file-id> --above <commit-or-branch> -m \"<msg>\"` for a committed file; `but move <commit-id>:<file-id>:<hunk-id> --above <commit-or-branch> -m \"<msg>\"` for a committed hunk (`--below`, `--branch`, and `--unstack` also work)\n- Split selected committed changes into a new commit immediately above their source: `but split <committed-file-or-hunk-id> [<committed-file-or-hunk-id>...] -m \"<msg>\"`\n- For `but split` and committed-change `but move`, use `-m/--message` to name the new commit directly; repeated `-m` values are joined with blank lines. For a multiline message, pass `-F -` with a heredoc, or `-F <file>` with a file outside the repository, instead of `-m`. Omitting both creates an empty-message commit without opening an editor. `but move -m` and `-F` reject whole-commit and branch sources.\n- Squash a whole branch into one commit: `but squash <branch> -m \"<msg>\"` (no `-t`)\n- Uncommit and remove a branch: `but uncommit <branch>`\n- Reorder commits: `but move <commit-id> --below <commit-id>` (`--above` for the other direction; **commit IDs**, not branch names)\n- Reorder a block: `but move <commit-id> <commit-id> --below <following-commit-id>` or `--above <preceding-commit-id>` (both anchors accept multiple space-separated sources)\n- Move commit to branch top: `but move <commit-id> -b <branch>`\n- Move commits or committed changes onto a new named branch: `but move <source> --above <branch> -b <new-name>` (`--below` also works); use `but move <source> --unstack -b <new-name>` for an independent branch. Omit `-b` for a generated name. Naming is not supported when stacking or unstacking a branch source.\n- Stack branches: `but move <branch-name> --above <target-branch-name>` (**use full branch names**)\n- Tear off a branch: `but move <branch> --unstack`\n- Discard: `but discard <id> [<id>...]` — accepts branches, commits, committed changes, uncommitted changes, or `@` for all uncommitted changes\n- Push: `but push <top-branch>` — pushes the selected branch and its ancestors; to update a stack, select its top branch once and never loop. Bare `but push` pushes all unpushed work when run non-interactively — one push per stack (its topmost unpushed branch, ancestors included), so output has one entry per stack, not per branch. It exits non-zero if any stack failed; stacks that already pushed stay pushed, and rerunning after fixing the failure is safe (up-to-date stacks are skipped)\n- Pull (update workspace from the target): `but pull` — the output reports the result; `but pull --check` previews without updating when a preview is actually needed\n- Create PR: `but pr new <branch-name> [-m \"Title...\"] [-F - | -F <file>] [-t] [--draft]` — auto-pushes first; do not run `but push` before it\n\n## Task Recipes\n\n### Update workspace from main\n\nFor \"get latest from main\", \"update/sync this workspace\", \"rebase onto main\", or \"pull main\":\n\n1. `but pull` — one command; no preflight needed. Its output reports the resulting state, it refuses safely when uncommitted changes conflict, and `but undo` reverts it.\n2. If commits come back conflicted, resolve them oldest-first following the printed instructions: `but resolve <commit>`, edit the files, then `but resolve finish`. Its result gives the current ID of the next conflict. Add `--status-after` to the finish you expect to clear the last conflict only when the task needs the complete resulting workspace. When it says no conflicted commits remain, stop; do not run a verification status. Finishing a lower commit rebases the ones above it, so always work bottom-up.\n\n`but pull --check` answers \"would this conflict?\" without updating. Do not use it as a routine\npreflight; use it when the user asks for a preview, repository policy requires one, other agents'\nbranches may move, or an applied branch was just merged upstream (see rule 3).\n\nRebasing applied branches onto the latest target IS `but pull` — never `move`, `config target`, `unapply`, or raw `git pull`/`git rebase`. The base shown in status is the last FETCHED state: when `git log` shows `main` (local or remote) ahead of it, that is exactly the update `but pull` fetches and applies — the target setting is not stale and repointing it is never the fix. Pull carries uncommitted changes along, and its output reports the resulting state. If it refuses because uncommitted changes conflict, park them: `but commit -b <branch> -m \"wip\" <ids>`, pull again, then `but uncommit` the parked commit (there is no stash; do not hand-revert files).\n\n### Commit selected files or hunks\n\n1. `but diff` — shows file and hunk IDs for uncommitted changes. Do not run plain `but status` first.\n2. Use file IDs when whole files belong in the commit; use hunk IDs (`<file-id>:<hunk-id>`) when only part of a file belongs. Omit IDs you don't want committed.\n3. `but commit -b <branch> -m \"<msg>\" <id1> <id2>` — the branch is created if it does not exist, so no prior `but branch new` is needed.\n4. When the task requires knowing which changes remain, add `--status-after`; otherwise the created-commit result is sufficient.\n\nEdge case: if wanted and unwanted edits are in the same diff hunk, GitButler cannot split that hunk by ID. Only when the task requires keeping part of that hunk uncommitted, temporarily edit the working tree to isolate the wanted lines, commit those IDs, then restore the leftover lines so they remain uncommitted.\n\n### Amend into existing commit\n\n1. `but status -fv` (or `but show <branch-id>`) — locate file/hunk IDs and target commit IDs.\n2. `but amend -t <commit-id> <id> <id>` — one command per target commit. For several target commits, chain the amends with `&&` when every target is a change-ID ref; otherwise run them one at a time with `--status-after` to get fresh refs.\n\n### Split an existing commit\n\nFor a two-way split, move selected committed files or hunks directly into a new commit:\n\n1. `but diff <source-commit-id>` — read committed file and hunk IDs.\n2. `but split <committed-file-or-hunk-id> [<committed-file-or-hunk-id>...] -m \"<message>\"` — sources may mix files and hunks, but must come from one commit. The command creates the named commit immediately above the source and leaves unselected changes and the original message in the source. No follow-up reword is needed for the new commit.\n3. Add `--status-after` only when the next step needs workspace IDs or details not provided by the mutation result.\n\nFor more than two replacement commits, repeat `but split ... -m \"<message>\"` using the remaining source's committed changes, or use the uncommit/recommit workflow below when rebuilding the entire commit:\n\n1. `but status -fv` when you need the source commit, branch name, or placement anchor.\n2. `but uncommit <source-commit-id> && but diff` in one shell call exposes the commit's changes and prints the resulting file and hunk IDs.\n3. Pick replacement contents from that dirty diff, not from the old committed diff.\n4. Determine the requested chronological order from the user's wording, then create the replacement commits oldest-first by chaining `but commit` calls. Do not reverse that order to match `but status`, which displays commits newest-first. `-b <branch>` puts each new commit at the TOP of that branch — if the split commit had commits above it, the replacements now sit above those preserved commits.\n5. **If commits from that branch must stay ABOVE the replacements, put the preserved block back on top instead of fighting anchors.** Do not anchor with `--above <top>`/`--below <top>` (sha/`#N` anchors go stale as each insert rewrites history). Move the block together so its internal order stays intact: append `&& but move <preserved-id> [<preserved-id>...] -b <branch>` to the commit chain. Change-ID refs from step 1 stay valid; wait for fresh output first if any preserved ref is sha-based or `#N`-suffixed.\n6. Leave unwanted changes uncommitted. Replacements created oldest-first appear newest-first in status — that is correct; do not reorder them. Add `--status-after` to the final mutation when you need to inspect the resulting order.\n\n### Reorder commits\n\n`but status` displays commits newest/top first, while task specs often list history oldest to newest — translate before moving.\n\n1. `but status` once to get commit IDs (use `-fv` only if you also need file details).\n2. `but move <source> --below <target-commit>` places source immediately below target in `but status` (older in oldest-to-newest history). `--above` places it immediately above (newer). `but move <source> -b <branch>` moves it to branch top/newest.\n3. For an adjacent block, run ONE move, anchored either way: `but move <block-id> <block-id> --below <following-commit-id>` or `but move <block-id> <block-id> --above <preceding-commit-id>`. Pick an anchor outside the block; source order does not matter and the block keeps its internal order. Add `--status-after` when you need to inspect the resulting order; do not move the anchor or block members again.\n4. For other reorders, make the smallest set of moves.\n\n### Squash commits\n\n1. `but status` for commit IDs and order.\n2. Name the sources positionally and the result/target commit with `-t`: `but squash <source> [<source>...] -t <target> -m \"<new message>\"`.\n3. To collapse an entire branch into one commit, pass just the branch and no `-t`: `but squash <branch> -m \"<new message>\"`.\n4. Multiple independent groups may run in sequence off one status read (targets keep their change-ID refs); prefer newer/top groups first. Take fresh refs only when a ref is sha-based or `#N`-suffixed.\n5. Add `--status-after` to the final squash when you need to inspect the resulting history; do not re-verify with a separate status.\n\n### Stack existing branches\n\nTo make one existing branch depend on another: `but move <child-branch-name> --above <parent-branch-name>` (use full branch names; commit reordering uses commit IDs). To unstack: `but move <branch-name> --unstack`.\n\n**DO NOT** stack via `uncommit` + `branch delete` + `branch new --above` (git branch names persist after delete and it loses work), and do not use `but undo` to unstack.\n\n### Create or manage pull requests\n\n`but pr new <branch-name>` pushes the selected branch and its ancestors, then creates the PR in one step — no prior `but push`. Provide `-F -` with a heredoc or `-F <file>` (first line is the title), `-t`, or `-m` with real newlines (zsh/bash: `-m $'Title\\n\\nBody'`) so no editor opens. Do not write the message to a file inside the repository: the next `but commit` would commit it. If forge auth is missing, run `but config forge auth`.\n\nIf you do create a PR for a stacked branch, use `but pr` — not `gh pr create` (only `but pr` sets PR bases and stack metadata; `gh pr create` breaks that). To publish a whole stack: `but pr new <top-branch-name> -t`. Manage with `but pr auto-merge|set-draft|set-ready <selector>`; selectors can be a branch name, current branch/stack CLI ID, or numeric review ID. See `but skill reference` for details.\n\n### Dependency conflict with another branch\n\nChanges that build on another branch's commits cannot land on an independent branch. `but commit` and `but amend` fail atomically (\"Cannot commit: N changes could not be applied\"), naming the branch and commit each rejected change depends on — nothing is committed and no `-b` branch is created.\n\nWhen there is a single dependency branch, the error's Hint gives the exact recovery command: `but move <your-branch> --above <dependency-branch>` to stack an existing branch on its dependency, or `but branch new <name> --above <dependency-branch>` when the target branch didn't exist yet. Run it, then retry the original command. When the error names dependencies without a Hint (several dependency branches, or the dependency is on the target branch itself), run `but status -fv` to see where the dependent commits live before choosing a placement.\n\nIf that recovery command fails, do NOT try `uncommit`, `squash`, or `undo` as a workaround — re-run `but status -fv` to confirm both branches exist and are applied, then retry with exact branch names.\n\n### Resolve conflicted commits (after pull, move, or reorder)\n\n**NEVER use `git add`, `git commit`, `git checkout --theirs/--ours`, or any git write command during resolution.** Only `but resolve` commands plus direct file edits. The one exception is a conflicted submodule in edit mode, below.\n\nConflicts do not interrupt operations in GitButler: a rebase always completes, and commits that conflicted are marked `{conflicted}` in `but status`. Find them from the warning that history-editing commands (`move`, `discard`, …) print, from the `but pull` summary, or from `but status`. Resolve them one conflict at a time, without entering any mode. Work through one branch at a time, oldest commit first — the loop is:\n\n1. `but resolve conflicts <branch>` — shows the conflicts of that branch's oldest conflicted commit, numbered per file. Each conflict has three sections: `ours` (the new base the commit was rebased onto), `base` (common ancestor), and `theirs` (the commit's own version). Without a branch it picks the first conflicted branch and tells you which; the output also says when other conflicted commits exist.\n2. Apply one resolution per command:\n   - Merged/mixed content (the common case — combine the intent of both sides): pipe it to `but resolve apply <path>:<N>` via stdin (heredoc) or pass `--file <f>`. The content replaces the whole conflicted region; never include conflict markers.\n   - Take a side entirely: `but resolve apply <path>:<N> --ours` or `--theirs` (bare `<path>` applies the side to all conflicts in that file).\n   - Delegate one conflict to the configured AI model: `but resolve apply <path>:<N> --ai` — prefer your own merged content when you have the context.\n   - `apply` targets the same default commit as `conflicts`; when more than one branch is conflicted, pin it with `--commit <branch>`.\n3. Go back to step 1 until it reports no conflicts. Commit ids are not stable here — every apply rewrites the commit — but branch names are, so address everything by branch and never bookkeep commit ids. Partial progress is fine: a commit with unresolved conflicts left stays `{conflicted}` with exactly those conflicts.\n\nA wrong resolution is reverted with `but undo`.\n\n`but resolve <commit-id> --ai` resolves a whole commit with the configured AI model in one shot, and `but resolve --ai` without a commit resolves every conflicted commit, oldest first — acceptable as a fallback, but you usually have better context than that model does, so prefer resolving the conflicts yourself with `apply`.\n\n**Use edit mode only when you must run code against the resolution** (build/tests at that commit):\n\n1. `but resolve <commit-id>` — enters resolution mode and prints the conflict regions.\n2. **Edit the files** to remove every conflict marker — `<<<<<<<`, `|||||||` (the common-ancestor section), `=======` and `>>>>>>>` — and keep the correct content. Do NOT skip this; do NOT use `but amend` on conflicted commits.\n   - A conflicted submodule has no markers, and `but resolve finish` refuses while it is unresolved, even to keep the commit's own side. Select it explicitly: check out the wanted commit inside the submodule (or write the file that replaces it) and run `git add -- <path>`, or drop it with `git rm -- <path>`. These are the only git write commands allowed during resolution, and only for the paths `finish` names; never `git commit`. If the submodule directory is empty or missing, do not `git add` it (for a missing one that stages its removal): keep any resolution edits you want and report the conflict. `but resolve cancel --force` abandons the resolution and discards its edits; use it only when those edits can be discarded.\n3. `but resolve finish` reports leftover markers, surviving uncommitted changes, every remaining conflicted commit, and the exact current `but resolve <id>` command. Add `--status-after` to the finish you expect to clear the last conflict only when the task needs the complete resulting workspace. When it says no conflicted commits remain, stop; do not run a verification status. Cancel instead with `but resolve cancel`.\n4. Repeat for remaining conflicted commits, oldest first — finishing a lower commit rebases the ones above it.\n\n### Conflicts in uncommitted files\n\n`but status` marks uncommitted files with unresolved merge conflicts `{conflicted}`; they are excluded from committable changes and outside `but resolve` mode. Edit the file to the wanted contents (or delete it), then `but resolve <path>...` to mark it resolved with that state; it then shows as an ordinary uncommitted change.\n\n## Git-to-But Map\n\n| git | but |\n|---|---|\n| `git status` | `but status` for branch/stack/commit overview; `but status -fv` for file/hunk details; `but diff` for selected dirty changes |\n| `git add` + `git commit` | `but commit -b <branch> -m ... <ids>` |\n| `git checkout -b` + commit | `but commit -b <new-branch> -m ... <ids>` |\n| `git push` | `but push <branch-name>` |\n| `git rebase -i` | `but move`, `but squash`, `but reword` |\n| `git rebase --onto` | `but move <branch> --above <new-base>` |\n| `git checkout -- <file>` / `git restore` | `but discard <id>` |\n| `git cherry-pick` | `but pick` |\n| `gh pr create` | `but pr new <branch-name> -m \"Title...\"` |\n\n## Notes\n\n- Read-only git inspection (`git log`, `git blame`, `git show --stat`) is allowed.\n- If `but` prints an `AGENT ACTION REQUIRED` skill warning, run the suggested command once, then reload/use the GitButler skill. If it repeats, report it instead of retrying.\n- For command syntax and flags: `but skill reference`\n- For workspace model: `but skill concepts`\n- For workflow examples: `but skill examples`\n","packages/but-mcp-app/README.md":"# GitButler workspace MCP App\n\nThese React apps render the structured results of the `gitbutler_workspace` and\n`gitbutler_review_card` MCP tools. Their compact workspace and review components\ntake their visual cues from GitButler Lite. Selecting a workspace commit or\nbranch opens a responsive details panel with copy, explain, and review actions.\nReview cards refresh open reviews and their CI status every 30 seconds until the\nreview closes, CI fails, or a refresh request fails.\n\nBuild the single-file resource embedded by the Rust MCP server with:\n\n```sh\npnpm -F @gitbutler/but-mcp-app build\n```\n\nThe generated `workspace.html` and `review.html` files under\n`../../crates/but/src/command/mcp` are ignored. Release workflows set\n`GITBUTLER_REQUIRE_MCP_APP=1` and must run the command above before compiling\n`but`. Normal Rust builds use a small fallback resource so they do not require\nNode.js or pnpm.\n","packages/but-sdk/README.md":"# @gitbutler/but-sdk\n\n`@gitbutler/but-sdk` is the local npm package that exposes GitButler Rust APIs to JavaScript/TypeScript through a native Node add-on.\n\n## How it works\n\nThe generated SDK surface is produced in two parts and merged into a single declaration file:\n\n1. **N-API bindings (functions)**\n   - Rust functions marked with `#[but_api(napi)]` in `but-api` generate `*_napi` exports via the `but_api` proc macro.\n   - `but-napi` links those generated exports and is built by `napi-rs` into a `.node` binary.\n   - `napi-rs` generates, per flavor folder:\n     - `gitbutler-sdk.{PLATFORM}.node` (Node native add-on)\n     - `index.js` (runtime loader + JS bridge)\n     - `index.d.ts` (N-API function declarations)\n\n2. **Schema-derived TypeScript types (structs/enums)**\n   - `but-ts` collects registered JSON schemas from `but-api` (`but_api::schema` module) and emits TS type aliases.\n   - These emitted type aliases are appended to the flavor folder's `index.d.ts`.\n\n\nThis package ships only generated artifacts (`browser.js` plus each flavor's `index.js`, `index.d.ts`, `*.node`) and re-exports them through package `exports`.\n\n## Workspace-projection flavors\n\nMutation APIs return a `WorkspaceState` whose projection depends on the\n`graph-workspace` cargo feature of `but-api` (see its feature docs). The SDK\nships one fully self-contained build per flavor:\n\n- `@gitbutler/but-sdk` (alias `@gitbutler/but-sdk/linear`, from\n  `src/generated/linear/`): built without the feature; `WorkspaceState`\n  carries the legacy `RefInfo`-based `headInfo` projection.\n- `@gitbutler/but-sdk/graph` (from `src/generated/graph/`): built with\n  `--features graph-workspace`; `WorkspaceState` carries the graph-based\n  `graphWorkspace` (`DetailedGraphWorkspace`) projection.\n\nEach folder has its own `.node` binary, loader, and declarations, so importing\nan entry point loads the matching native build — types and runtime cannot\ndisagree for napi consumers. (The desktop app consumes this package for types\nonly; its runtime is the Tauri backend, so it must still keep its import flavor\nin lockstep with `gitbutler-tauri`'s `graph-workspace` feature.) Both folders'\n`index.js`/`index.d.ts` are committed and CI-checked.\n\n## Generate bindings and types\n\nPer flavor, always run the napi build before the type generation:\n\n```bash\n# linear flavor (src/generated/linear/)\npnpm --filter @gitbutler/but-sdk build:napi\npnpm --filter @gitbutler/but-sdk build:types\n# graph flavor (src/generated/graph/)\npnpm --filter @gitbutler/but-sdk build:napi:graph\npnpm --filter @gitbutler/but-sdk build:types:graph\n```\n\nOr run the combined script, which builds both flavors:\n\n```bash\npnpm --filter @gitbutler/but-sdk build\n```\n\n### Why order matters\n\n`build:types` appends schema types to the existing declaration file. Running it repeatedly without regenerating N-API declarations first can duplicate schema type sections.\n\n## Add new bindings and types\n\n### Add a new exported N-API function\n\n1. In `crates/but-api`, annotate a Rust API with `#[but_api(napi)]`.\n2. Ensure input/output types are serializable in a way the macro supports.\n3. Re-run:\n\n```bash\npnpm --filter @gitbutler/but-sdk build:napi\n```\n\nYou should see a new `*Napi` export in `src/generated/linear/index.d.ts` (and `src/generated/graph/index.d.ts` for the graph flavor).\n\n### Add a new generated TypeScript type\n\n1. Make sure the Rust type derives `schemars::JsonSchema`.\n2. Register the type in `crates/but-api/src/schema.rs` using `TypeSchemaEntry`.\n3. Re-run:\n\n```bash\npnpm --filter @gitbutler/but-sdk build:types\n```\n\nYou should see a new `export type ...` in `src/generated/linear/index.d.ts` (and `src/generated/graph/index.d.ts` for the graph flavor).\n\n## Validate generated output\n\nBaseline validation:\n\n```bash\npnpm --filter @gitbutler/but-sdk check\n```\n\nOptional extra runtime/type sanity check:\n\n```bash\npnpm --filter @gitbutler/but-sdk testTypes\n```\n\nRecommended contributor flow after changing bindings/types:\n\n```bash\npnpm --filter @gitbutler/but-sdk build\npnpm --filter @gitbutler/but-sdk check\n# optional\npnpm --filter @gitbutler/but-sdk testTypes\n```\n\n## Usage in Electron (`apps/lite` pattern)\n\nUse native bindings in the Electron **main process**, then expose only typed IPC APIs to the renderer.\n\n### Main process model\n\n```ts\nimport { listProjectsStateless } from '@gitbutler/but-sdk';\n\nexport function listProjects() {\n\treturn listProjectsStateless();\n}\n```\n\n### IPC contract with SDK types\n\n```ts\nimport type { ProjectForFrontend } from '@gitbutler/but-sdk';\n\nexport interface LiteElectronApi {\n\tlistProjects(): Promise<ProjectForFrontend[]>;\n}\n```\n\n### Renderer consumes IPC API\n\n```ts\nconst projects = await window.lite.listProjects();\n```\n\nThis keeps native access out of the renderer while sharing the same generated TS types (`ProjectForFrontend`, `StackEntry`, `StackDetails`, etc.) across process boundaries.\n\n## Current exported binding entry points\n\nSee `src/generated/linear/index.d.ts` (and `src/generated/graph/index.d.ts` for the graph flavor) for the complete generated API and type surface.\n\n## See also\n\n- [but-napi](../../crates/but-napi/README.md)\n- [but-ts](../../crates/but-ts/README.md)\n\n## License\n\nFSL-1.1-MIT - See [LICENSE.md](../../LICENSE.md) for details.\n","packages/no-relative-imports/README.md":"# @gitbutler/no-relative-imports\n\nAn eslint rule for enforcing non-relative imports if there are `paths` in your `tsconfig.json` available to you.\n\n## What the rule affects\n\nThe rule requires that any import that _could_ be referenced absolutely via an entry in `paths` will provide an error.\n\nEven if a sibling import (IE `./foo`) is used, if it could be accessed via a path (IE `$lib/foo`). An error will be provided.\n\n## The `paths` format\n\nIt should be noted that the current implementation does not try to handle all the possible `paths` entries. Instead it handles the three common cases:\n\n```json\n{\n\t\"compilerOptions\": {\n\t\t\"paths\": {\n\t\t\t\"$lib\": \"./lib\", // Without glob. IE pointing directly to a file\n\t\t\t\"$lib/*\": \"./lib/*\", // Both with glob, IE anything starting in ./lib/... results in $lib/...\n\t\t\t\"$lib\": \"./lib/*\" // Path with, IE anything starting in ./lib/... results in $lib\n\t\t}\n\t}\n}\n```\n\n## Usage\n\n```\nimport noRelativeImportPaths from '@gitbutler/no-relative-imports';\n\nexport default [\n  {\n    plugins: {\n      'no-relative-import-paths': noRelativeImportPaths,\n    },\n    rules: {\n      'no-relative-import-paths/no-relative-import-paths': 'error',\n    },\n  },\n]\n```\n","packages/shared/README.md":"# create-svelte\n\nEverything you need to build a Svelte library, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).\n\nRead more about creating a library [in the docs](https://kit.svelte.dev/docs/packaging).\n\n## Creating a project\n\nIf you're seeing this, you've probably already done this step. Congrats!\n\n```bash\n# create a new project in the current directory\nnpm create svelte@latest\n\n# create a new project in my-app\nnpm create svelte@latest my-app\n```\n\n## Developing\n\nOnce you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:\n\n```bash\nnpm run dev\n\n# or start the server and open the app in a new browser tab\nnpm run dev -- --open\n```\n\nEverything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app.\n\n## Building\n\nTo build your library:\n\n```bash\nnpm run package\n```\n\nTo create a production version of your showcase app:\n\n```bash\nnpm run build\n```\n\nYou can preview the production build with `npm run preview`.\n\n> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.\n\n## Publishing\n\nGo into the `package.json` and give your package the desired name through the `\"name\"` option. Also consider adding a `\"license\"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).\n\nTo publish your library to [npm](https://www.npmjs.com):\n\n```bash\nnpm publish\n```\n","packages/svelte-comment-injector/README.md":"# svelte-comment-injector\n\nA Svelte preprocessor that injects an HTML comment at the top of every component, making it easier to identify components in browser DevTools.\n\n## ✨ Features\n\n- Injects a DevTools-visible HTML comment like:\n  ```html\n  <!-- Begin MyComponent.svelte -->\n  <div class=\"my-component\">\n  \t<!-- Component content -->\n  </div>\n  <!-- End MyComponent.svelte -->\n  ```\n\n## 📦 Installation\n\n```bash\nnpm install @gitbutler/svelte-comment-injector --save-dev\n```\n\n## ⚙️ Usage\n\nAdd the preprocessor to your Svelte configuration:\n\n```js\n// svelte.config.js\nimport svelteInjectComment from \"@gitbutler/svelte-comment-injector\";\n\nexport default {\n\tpreprocess: [svelteInjectComment()],\n\t// ...rest of your config\n};\n```\n\n## 🛠️ Configuration\n\nYou can customize the comment format by passing options to the preprocessor:\n\n```js\n// svelte.config.js\nimport svelteInjectComment from \"@gitbutler/svelte-comment-injector\";\n\nexport default {\n\tpreprocess: [\n\t\tsvelteInjectComment({\n\t\t\tenabled: true, // Enable or disable the comment injection\n\t\t\tshowEndComment: true, // Show the end comment\n\t\t\tshowFullPath: false, // Show the full path in the comment\n\t\t}),\n\t],\n\t// ...rest of your config\n};\n```\n","packages/ui-react/AGENTS.md":"# @gitbutler/ui-react\n\nThe React component library: the components, icons, illustrations and base\nstyles shared by Lite, `but panel` and the MCP app. It ships source, not a\nbuild; each consumer's Vite compiles it, and React Compiler runs on it under\nthe consumer's config as it does on the consumer's own code.\n\n`DESIGN.md` beside this file is the visual language: how things should look\nand read. It is an index of the pages in `design/`, one per topic; read the\nones for what you are changing before changing anything a user sees. This file\nis the tooling that enforces them.\n\n## Writing the code\n\nLite's rules apply here unchanged; `apps/lite/AGENTS.md` has them. In short:\nReact Compiler makes `useMemo`, `useCallback` and `React.memo` redundant\noutside hot paths; `useEffect` is usually an anti-pattern and needs consent;\ncomments explain the non-obvious why, never the what.\n\n### Changing a component\n\n**A change to a component here needs the designer's okay.** Every app draws\nwith these components, so a fix made for one surface changes all of them.\nWhen an app's bug seems to need a change here (a style, a prop, a default, a\nwrapper), try fixing it in the app first. If the library really is at fault,\nask before changing it. When you can't ask, make the change its own\n`fix(ui-react): …` commit that says which surface showed the bug and what\nelse it touches, and call it out in the PR description.\n\n### What belongs here\n\nA component belongs here when it doesn't reach into an app: no API calls,\nno settings, no store, nothing on `window.lite`. Everything a component needs\nfrom its host arrives as a prop, the way `TextLink` takes its `onClick` rather\nthan assuming Electron. A component that needs the app stays in the app;\nLite keeps its Markdown renderer and mention suggestions for that reason.\n\nImports are deep and carry the extension: `@gitbutler/ui-react/Button.tsx`.\nThere is no barrel file. Inside the package, siblings import each other\nrelatively.\n\n### Styles\n\nTokens come from `@gitbutler/design-core`, imported by the host once.\n`src/base.css` declares the few variables components read that aren't tokens\n(`--focus-ring`, `--control-cursor`, `--transition-button`) and the control\ncursor rules; every host imports it right after design-core. A variable nothing declares doesn't error, the\nproperty quietly falls back, so a new shared variable goes in `base.css`, not\nin a host's stylesheet.\n\n### Icons\n\nIcons are SVGs in `src/icons/`, inlined into the bundle as raw strings; the\nfilename is the icon name (`folder-lock.svg` → `<Icon name=\"folder-lock\" />`).\nTo add one:\n\n1. Export it from Figma at 16×16 (⚛️ Core library) as SVG into `src/icons/`.\n2. Run `pnpm -F @gitbutler/ui-react optimize-icons`. It minifies the SVG,\n   recolors it to `currentColor` and regenerates `src/iconNames.ts`.\n3. Commit both. `iconNames.ts` is generated; never hand-edit it.\n\n`src/file-icons/` are the language and filetype glyphs. They keep their brand\ncolours and are deliberately not run through the script.\n\n`src/program-icons/` are the marks of editors and terminals, as PNGs at 2×.\nTheir rounded corner is part of the image: export each from the \"Programm\"\nframe on ⚛️ Core's Icons page, where the mark is a 14px rectangle with\nthe corner on it, as PNG at 2× with the layer named as the file is\n(`vscode` → `vscode@2x.png`). No CSS rounds them, so a square export shows\nsquare.\n\n## Figma\n\nTwo files, split the way the code is:\n\n- **⚛️ Core** —\n  <https://www.figma.com/design/cqdnAotT8n9op8WGYLOHg4/%E2%9A%9B%EF%B8%8F-Lite-Core>,\n  the published library: tokens (Tokens page), icons (Icons page) and this\n  package's components, one section per component. It is the source of truth\n  for tokens; for components, code is, and the drawing follows it. The\n  components are on six pages, by what they are for: Actions, Inputs,\n  Overlays, Content & status, Preview (a details view's header and the bar\n  above it), and Sidebar. A new component's section goes at\n  the end of its group's page, with its examples section under it. Storybook\n  shelves the stories in the same groups (plus Icons, which Figma keeps on its\n  own page): a stories file names its group in `title` and keeps its old `id`,\n  so the story ids below don't change when a component moves group.\n- **Client**, the working file —\n  <https://www.figma.com/design/EBuHQGUcCaSw4Ln5uVpWkn/Client>: Lite's own\n  components on the ⚙️ Meta page, the full screens, and drafts on the\n  🚧 Drafts page and the pages under it. A new mockup or an exploration goes\n  here, never in the library.\n\nA component is drawn where its code lives: one in this package goes in\n⚛️ Core, one in `apps/lite` on ⚙️ Meta. When a component moves between the two\nin code, its drawing moves with it in the same piece of work, and the\nmockups' instances are swapped to the new one.\n\nThe drawing mirrors the code's structure: the same component under the same\nname, variant properties named as the props and their values (`status`,\n`variant`, `glyph`), and no property the code lacks. Figma may add only what\nthe code produces from data, such as a toggle for a side the code drops when\nits count is zero, and a `state` property (`default`, `hover`) for what CSS\ndraws under the pointer, as Button has; two components the code keeps apart\nstay apart in Figma and are placed side by side where a surface shows both.\n\nA component's description in Figma says what its JSDoc and DESIGN.md say\nabout it: what it is for, when to pick which variant, and a `Code:` line\nnaming the component and its file. Change one and change the other in the\nsame piece of work; a description that lags the code is what an agent reading\nFigma will build from.\n\n## Verifying your work\n\nAlways run the specified commands **exactly** as written. The package has no\napp to run, so the story is how a component is checked before it goes into a\nsurface.\n\n### Typechecking, the CSS rules, and tests\n\n```console\n$ pnpm -F @gitbutler/ui-react check\n$ pnpm -F @gitbutler/ui-react test\n```\n\n`check` typechecks and then runs Stylelint over the components' CSS with the\nrules the design pages give a machine: colours are tokens, font sizes are on\nthe 11–16 scale, radii come from the radius tokens. An error names what to use\ninstead. The exceptions are the ones the pages name, and each is marked where\nit is: the file icons' brand colours (a file-level override in\n`stylelint.config.mjs`) and a mask's opaque stop. A radius that is a share of\nanother, an inner corner following its outer one, is a `calc()` from the outer\ntoken and needs no exception (`design/foundations/radius.md`). A new exception\nis a design decision first; if it stands, disable the rule on that line with\nthe reason after `--`.\n\n### Tokens\n\nEvery `var(--x)` in this package has to resolve. Lite's token check reads this\npackage's source alongside its own:\n\n```console\n$ pnpm -F @gitbutler/lite check\n```\n\nIt fails on any undefined name and suggests the nearest real one. Run it\nafter any CSS change here, even when the package's own check passes.\n\n### Stories\n\nEvery component has a story beside it, in both themes, and a component that\nlives only in code is half a component. `pnpm check` holds the package to it\nthrough `scripts/check-stories.mjs`, which fails on a component no story\nimports. Storybook is Lite's, on port 6007, and lists this package's stories\nunder `components/`:\n\n```console\n$ pnpm -F @gitbutler/lite demos\n```\n\nThe package's own `test` runs its unit tests, not its stories. Lite's runs\nevery story, this package's included, as a test in headless Chromium; run\nthem alone with `pnpm -F @gitbutler/lite test:stories` (`apps/lite/AGENTS.md`\nhas the setup).\n\nA story renders alone at\n`http://localhost:6007/iframe.html?id=components-<name>--<export>&viewMode=story`;\nappend `&globals=theme:dark` for the dark theme. Check colour, font and\nspacing from computed styles rather than by eye. A story links its Figma\ncomponent through the `design` parameter; keep the link when you add a story,\nand compare the two sides when either changes.\n\n### Default, then examples\n\nA component's stories are one entry in Storybook, in one order:\n\n- **`Default` first:** the simplest use of the component, its props on the\n  controls where they have any to show. It answers what the component is and\n  accepts. `pnpm check` fails on a stories file that opens with anything\n  else.\n- **Examples after it:** each named after what it shows — a state\n  (`OnlyAdditions`, `NothingToList`) or a surface the app builds from it\n  (`Confirm`, `SelectProject`, `CommandPalette`). An example is written as\n  the app writes it: its own state, real copy, rendered open, since its\n  source is what an agent copies from `docs-show`.\n\nA surface built from several components is an example of the outermost one:\nthe upload confirmation is Modal's, the project picker is Popup's (a\ncombobox dressed in its parts), the command palette is PickerDialog's.\n\nIn ⚛️ Core the examples sit in a section named `<Component> examples`\ndirectly under the component's own section, on its group's page, one\ninstance per story, named as the story is; an example story links its\ninstance through the `design` parameter.\n\n### The manifest\n\nStorybook writes `/manifests/components.json` (and `components.html`, to\nread) from the stories: every component, its props from the TypeScript\ntypes, its stories with their source, and the import to write. An agent\nreads it instead of guessing an API. Two things about it to know:\n\n- The import to write is the `@import` tag in each component's JSDoc, as\n  `@import import { Badge } from \"@gitbutler/ui-react/Badge.tsx\";`. Give a\n  new component one: without it the manifest falls back to the package\n  root, and there is no such barrel. Storybook reads the tag through\n  `experimentalReactComponentMeta` in Lite's `.storybook/main.ts`.\n- The link to the Figma component is the story's `design` parameter, not\n  the manifest. `grep -l 'type: \"figma\"' src/*.stories.tsx` lists the stories\n  that have one; a story without one has no drawn spec yet.\n\nA story with no component behind it, as Lite's `AppUpdater.stories.tsx`,\ncarries `tags: [\"!manifest\"]` and says why. That is also why a styling\nhelper is not how a control is offered: `getButtonClassName` is for making\nanother component look like a button, and a button is `<Button>`, which the\nmanifest documents.\n\n### The Storybook MCP server\n\nWhile Storybook runs, it serves an MCP server at `http://localhost:6007/mcp`\n(`@storybook/addon-mcp`). Add it to your agent once:\n\n```console\n$ claude mcp add --transport http gitbutler-storybook http://localhost:6007/mcp\n```\n\nEach design notes page has an `.mdx` beside its `.md` in `design/`, under\nStorybook's Design section, so the published Storybook shows them and the MCP\nserver's `docs-list` lists them; `design/overview.mdx` renders DESIGN.md. The\nMDX renders the Markdown file itself, so edit the `.md`, never the page. What\nthe MDX writes is the title, a one-line summary (the description `docs-list`\nshows) and the file's raw URL, since `docs-show` returns the MDX source, not\nthe rendered text. A new page takes a Markdown file, an MDX beside it titled\n`Design/<Group>/<Name>` after its folder and file name (that is how links\nbetween the files find their Storybook page), and a row in DESIGN.md.\n\nBefore using a component, ask it rather than guessing: `docs-list` lists the\ncomponents, `docs-show` gives one's props, its stories and the import to\nwrite, and `docs-show-story` a whole story. Use only props it documents; if\none seems missing, ask instead of inventing it.\n`stories-find-by-component` finds the stories a file renders in, and\n`stories-preview` links to them. The published Storybook serves the same docs\ntools without a local one. The repository's `.mcp.json` connects Claude Code\nto it (it asks once); another agent, or one outside this repo, adds it:\n\n```console\n$ claude mcp add --transport http gitbutler-storybook-published https://master--6ab536f5f40e41db628ccf1b.chromatic.com/mcp\n```\n\nThe published server can time out, most often while a new master Storybook\ndeploys. Fall back in order: the manifest at\n<https://master--6ab536f5f40e41db628ccf1b.chromatic.com/manifests/components.json>,\na static file that keeps answering, then the component's own source and JSDoc\nin `src/`, which the manifest is built from. Never guess a prop because the\ntools were down.\n\n### Accessibility\n\n`@storybook/addon-a11y` runs axe on every story: the Accessibility tab under\na story lists contrast failures, controls without a name and misused roles.\nCheck it for a new or changed story. It catches the mechanical part of\nDESIGN.md's rules (an icon-only button's `aria-label`, a field's label,\ncontrast), not whether a label makes sense.\n\n### Chromatic\n\n`.github/workflows/chromatic.yml` publishes the Storybook to Chromatic's\npublic \"GitButler UI\" project: from master as the published Storybook, at\n<https://master--6ab536f5f40e41db628ccf1b.chromatic.com>, and for a pull request that touches the\nstories, as a preview with a UI Review check that compares every story's\nscreenshot with master's. A visual change waits there to be accepted rather\nthan failing the build; accept it when it is the change you meant.\n\n### Linting & formatting\n\nThe package is linted by oxlint with Lite's config, not by ESLint:\n\n```console\n$ pnpm exec prettier --check packages/ui-react\n$ pnpm run oxlint\n$ pnpm knip:non-prod\n```\n\nRun these before declaring the work done, and run `check-tokens` (above) with\nthem. The lint job in CI runs all of them and fails on what any one reports.\n","packages/ui-react/CLAUDE.md":"AGENTS.md","packages/ui-react/skill/SKILL.md":"---\nname: gitbutler-ui\ndescription: Use when changing what users see in an app built on `@gitbutler/ui-react` — Lite, the but.dev web app — adding or restyling a component, modal, form, list, empty state or toast, or writing the words on screen. Not for logic, data or backend work, even inside a UI file.\n---\n\n# Building GitButler UI\n\nWork as you would in any codebase: find the closest thing that already\nexists and copy it. These notes say where to look and what to check. They are\nnot a gate to clear before starting.\n\n## Scope\n\nChange what the task needs. Older screens may not follow these notes; leave\nthem alone unless the task is to fix them.\n\n## Start from the library\n\n`@gitbutler/ui-react` has the controls: `Button`, `Popup` (modals, dropdowns),\n`Select`, `Field`, `Checkbox`, `Switch`, `Tooltip`, `EmptyState`, `Snackbar`\n(raised through the `Snackbars` stack), `Toasts`, `Banner` and more. Use them\nrather than a control styled in a feature's CSS. Find a surface that already\nuses the ones you need and follow its structure.\n\nThe source is the reference, because it is the version you build with:\n\n- In the gitbutler repository: `packages/ui-react/src/`\n- In but.dev: `web/node_modules/@gitbutler/ui-react/src/`\n\nEach component's JSDoc says what its props do. The Storybook MCP server\n(`docs-show`) and\n<https://master--6ab536f5f40e41db628ccf1b.chromatic.com/manifests/components.json>\nhave the same docs with examples; use them when they answer, and go back to the\nsource when they don't. Don't wait on them.\n\nEach component's stories open with `Default`, its simplest use and its props;\nthe ones after it are examples, the component put to work the way an app\nwrites it. For a whole surface, find the closest example under its outermost\ncomponent (a confirmation is Modal's, a picker Popup's or PickerDialog's),\ncopy it and change what differs.\n\nWhen nothing fits, build the smallest thing that works and say so in the PR\ndescription: what was needed, which components you tried.\n\n## Read the design pages you need\n\nThe shared design language is the library's `DESIGN.md`, beside `src/` in the\nsame two places: an index of one page per topic, in `design/`, each saying\nwhen to read it. Read the pages for what you are building, not all of them.\nIn Storybook they are the Design section, and `docs-list` lists them.\n\nEach app keeps its own choices in its own `DESIGN.md`: Lite's is\n`apps/lite/DESIGN.md`, but.dev's is at its repository root.\n\n## Words\n\nShort and plain. A label is one to three words; a hint is one sentence. Call\nthings what the rest of the app calls them. Say what the user can't already see\non screen, once, without parentheses.\n\n## Check it\n\nLook at the result in light and dark, and keep one screenshot. In the\ngitbutler repository a component's story runs with `pnpm -F @gitbutler/lite\ndemos`; `apps/lite/AGENTS.md` has how to reach a single story.\n","packages/ui-svelte/scripts/README.md":"# UI Package Scripts\n\n## `optimize-ui-icons.js`\n\nOptimizes all SVG files in `src/lib/icons/svg/` using svgo, then syncs `src/lib/icons/names.ts`.\n\n### Usage\n\n```bash\n# From the UI package root\npnpm optimize-ui-icons\n# or\nnode scripts/optimize-ui-icons.js\n```\n\n### What it does\n\n1. Runs svgo on every `.svg` in `src/lib/icons/svg/` with `preset-default` (keeping `viewBox` and groups intact)\n2. Sets `width=\"100%\"` and `height=\"100%\"` on every `<svg>` so icons scale via CSS\n3. Replaces hardcoded `fill`/`stroke` colors with `currentColor` (leaves `none` untouched)\n4. Adds `vector-effect=\"non-scaling-stroke\"` to all vector shape elements\n5. Updates `src/lib/icons/names.ts` to reflect the current set of icon files\n\n---\n\n## `optimize-file-icons.js`\n\nOptimises SVG file-type icons and writes them into `src/lib/components/file/icon/svg/`.\n\nSupports two modes:\n\n- **Optimize in place** (no argument): optimises all existing SVGs in the output directory\n- **Import from a source directory** (with argument): reads SVGs from `<svg-dir>`, optimises them, and writes each into the output directory (adding new files, updating changed ones)\n\n### Optimisation steps\n\n- Runs svgo `preset-default` (keeping `viewBox` and groups intact)\n- Removes `width`/`height` attributes so icons scale via CSS\n- Replaces hardcoded hex colours with CSS variables from a canonical colour map (`--file-icon-gray`, `--file-icon-green`, `--file-icon-teal`, `--file-icon-blue`, `--file-icon-dark-blue`, `--file-icon-yellow`, `--file-icon-orange`, `--file-icon-red`, `--file-icon-pink`, `--file-icon-purple`, `--file-icon-violet`)\n\n### Usage\n\n```bash\n# Optimize in place\npnpm optimize-file-icons\n# or\nnode scripts/optimize-file-icons.js\n\n# Import + optimize from a source directory\nnode scripts/optimize-file-icons.js <svg-dir>\n```\n","packages/ui-svelte/src/lib/components/chipToast/README.md":"# Toast Component\n\nA simple toast notification system for the GitButler UI package.\n\n## Features\n\n- **Simple**: Just type and text - no complex configuration\n- **Bottom-center positioning**: Appears at the bottom center of the screen\n- **Stacking**: Multiple toasts stack vertically\n- **Auto-dismiss**: All toasts automatically disappear after 4 seconds\n- **4 types**: Info, Success, Warning, and Error with appropriate styling\n\n## Usage\n\n### Basic Usage\n\n```svelte\n<script>\n\timport { toasts, ToastContainer } from \"@gitbutler/ui-svelte\";\n\n\tfunction showToast() {\n\t\ttoasts.success(\"Operation completed!\");\n\t}\n</script>\n\n<button on:click={showToast}>Show Toast</button>\n\n<!-- Add the container to your app root -->\n<ToastContainer />\n```\n\n### Toast Types\n\n```javascript\nimport { toasts } from \"@gitbutler/ui-svelte\";\n\n// Different toast types\ntoasts.info(\"This is a info message\");\ntoasts.success(\"Operation completed successfully!\");\ntoasts.warning(\"Please review your changes\");\ntoasts.error(\"Something went wrong\");\n```\n\n### Promise Integration\n\n```javascript\nimport { toasts } from \"@gitbutler/ui-svelte\";\n\nasync function handleAsyncOperation() {\n\tconst myPromise = fetch(\"/api/data\");\n\n\tawait toasts.promise(myPromise, {\n\t\tloading: \"Loading data...\",\n\t\tsuccess: \"Data loaded successfully!\",\n\t\terror: \"Failed to load data\",\n\t});\n}\n```\n\n### Clear All Toasts\n\n```javascript\ntoasts.clearAll(); // Removes all current toasts\n```\n\n## Components\n\n### `<Toast>`\n\nIndividual toast component.\n\n**Props:**\n\n- `type: 'info' | 'success' | 'warning' | 'error'` - Toast type\n- `message: string` - Toast message text\n\n### `<ToastContainer>`\n\nContainer that manages positioning and stacking. Add this once to your app root.\n"},"files":{".agents/skills/but-performance-tests/SKILL.md":"---\nname: but-performance-tests\ndescription: Use when creating, changing, running, or debugging shell-based `but` CLI performance scenarios under `crates/but/tests/performance`, including Hyperfine runs, fixture setup, setup-to-test state, and output inspection.\n---\n\n# `but` CLI performance tests\n\nRead [performance framework documentation](../../../crates/but/tests/performance/README.md)\ncompletely before answering questions or changing performance scenarios. Inspect nearby\nscenarios and current shared helpers as applicable; README is canonical source for\nframework behavior, commands, helper APIs, and debugging options.\n\n## Workflow for changes\n\n1. Confirm requested operation and timing boundary.\n2. Inspect closest existing scenarios plus `lib.sh` and `run.sh`.\n3. Add or update only scenario's `setup.sh` and `test.sh` unless shared behavior is\n   genuinely required.\n4. Prefer real, representative GitButler repository state over toy data.\n5. Keep setup, validation, and ID discovery outside timed operation.\n6. Reuse documented helpers instead of creating parallel fixture, state, output, or\n   timing mechanisms.\n7. List every scenario in top-level performance README's included-scenario list, and\n   update its entry when materially changing scenario.\n8. Run README's shell validation and short single-scenario benchmark.\n\nFor usage or debugging questions, answer from current README and scripts. Do not edit\nfiles unless user asks for changes.\n\n## Guardrails\n\n- Keep changes inside `crates/but/tests/performance/` unless task specifically requests\n  related documentation or skill updates.\n- Preserve two-file scenario contract.\n- Do not add custom timing/statistics code; Hyperfine owns measurement.\n- Do not include fixture setup or compilation in measured command.\n- Do not run all scenarios when user requests or changes only one.\n- Report fixture/setup limitations and unavailable validation explicitly.\n",".agents/skills/cli-commands/SKILL.md":"---\nname: cli-commands\ndescription: Use when adding or modifying CLI command (`but` commands) under `crates/but/src`.\n---\n\nImagine we're implementing a new `commit3` command. The high-level structure\nfor that must be as follows.\n\n## Arguments\n\nArguments live in `crates/but/src/args`. For our commit command that would be\n`crates/but/src/args/commit3.rs`:\n\n```rust\nuse crate::args::atoms::CliIdArg;\n\n/// Create a commit.\n///\n/// More details about the command here...\n#[derive(Debug, clap::Parser)]\n#[cfg_attr(feature = \"raw-clap-docs\", clap(verbatim_doc_comment))]\n#[deny(missing_docs)]\npub struct Platform {\n    /// The message to use for the commit.\n    #[clap(short, long, group = \"commit_message\")]\n    pub message: Option<Vec<String>>,\n\n    /// Place the commit on the branch `BRANCH`.\n    #[clap(short, long, value_name = \"BRANCH\", group = \"targeting\")]\n    pub branch: Option<Option<CliIdArg>>,\n\n    /// One or more changes to commit.\n    pub changes: Vec<CliIdArg>,\n}\n```\n\nIn `crates/but/src/args/mod.rs`:\n\n```rust\n#[cfg(feature = \"legacy\")]\n#[cfg_attr(feature = \"raw-clap-docs\", clap(verbatim_doc_comment))]\n#[clap(hide = true, name = \"_commit3\")]\nCommit3(commit3::Platform),\n```\n\nThings to note:\n\nArguments that refer to Git objects (such as commits, branches, files, hunks,\netc.) use some type from `crates/but/src/args/atoms/` and not `String` or another\nloose type.\n\nUse `CliIdArg` for arguments that reference existing Git objects such as\nbranches, commits, files, etc. This allows the user to use short IDs or fully\nqualified names.\n\n`String` should only be used for truly loose text input such as commit\nmessages.\n\n`Platform` and all of its fields must have documentation. The `Subcommands`\nvariant intentionally has no doc comment because clap obtains the command\ndocumentation from `Platform`.\n\nUse `#[clap(group = \"...\")]` to create mutually exclusive groups of arguments.\n\nCommands with tricky grammar can define a `pub(crate) const ERROR_EXAMPLES`\nnext to `Platform` and register it in `args::error_examples`; the block is\nappended after clap parse errors. At most 4 lines, each a\n`but <cmd> ...   # what it does` invocation that works as written (e.g.\ninclude `-m` where omitting it would open an editor).\n\n## Handling the command\n\nAdd a match arm to `crates/but/src/lib.rs` to handle the command:\n\n```rust\nmatch cmd {\n    Subcommands::Commit3(commit_args) => {\n        use crate::utils::IntermediateChannel;\n\n        let status_after = args.status_after;\n        let mut ctx = setup::init_ctx(\n            &args,\n            InitCtxOptions {\n                background_sync: BackgroundSync::Enabled { silent: false },\n                ..Default::default()\n            },\n            out,\n        )?;\n        out.begin_status_after(status_after);\n\n        let outcome = command::legacy::commit3::commit(\n            &mut ctx,\n            IntermediateChannel::new(out),\n            commit_args,\n        )\n        .emit_metrics(metrics_ctx)?;\n        out.print_cli_output(outcome)?;\n\n        run_status_after_if_requested(status_after, &mut ctx, out);\n\n        Ok(())\n    }\n\n    // all the other commands...\n}\n```\n\nThings to note:\n\nUse `IntermediateChannel`. Do not pass `OutputChannel` to commands.\n\nUse `OutputChannel::print_cli_output` to print the final output from the\ncommand. This ensures we handle all supported formats. If only human format is\nsupported, use `OutputChannel::print_cli_output_human`.\n\n## Implementing the command\n\nIn `crates/but/src/command/legacy/commit3.rs`\n\n```\npub fn commit(\n    ctx: &mut Context,\n    out: IntermediateChannel<'_>,\n    args: Platform,\n) -> CliResult<CommitOutcome> {\n    // get whatever dependencies we need from `Context` such as\n    // `RepoExclusiveGuard`, `IdMap`, `RefInfo`, etc.\n\n    // resolve the arguments into a `CommitOperation`\n    let commit_operation = resolve(ctx, args)?;\n\n    // Run the operation\n    let outcome = run(ctx, commit_operation)?;\n\n    // Return the outcome which will be printed by the caller\n    Ok(outcome)\n}\n\nfn resolve(ctx: &mut Context, args: Platform) -> CliResult<CommitOperation> {\n    let Platform { message, branch, changes } = args;\n    // ...\n}\n\nfn run(ctx: &mut Context, commit_op: CommitOperation) -> anyhow::Result<CommitOutcome> {\n    match commit_op {\n        // ...\n    }\n}\n\n#[must_use]\nstruct CommitOutcome {\n    new_commit: ObjectId,\n}\n\nimpl CliOutputHuman for CommitOutcome {\n    fn on_human(self, out: &mut dyn WriteWithUtils, _theme: &Theme) -> anyhow::Result<()> {\n        let Self { new_commit } = self;\n\n        writeln!(\n            out,\n            \"Created commit {}\",\n            theme::Commit(new_commit, None),\n        )?;\n\n        Ok(())\n    }\n}\n\nimpl CliOutput for CommitOutcome {\n    fn on_shell(self, out: &mut dyn WriteWithUtils) -> anyhow::Result<()> {\n        let Self { new_commit } = self;\n\n        writeln!(out, \"{}\", new_commit.to_hex_with_len(7))?;\n\n        Ok(())\n    }\n\n    fn on_json(self) -> impl serde::Serialize {\n        #[derive(Serialize)]\n        struct Output {\n            commit: HexHash,\n        }\n\n        let Self { new_commit } = self;\n\n        Output { commit: new_commit.into() }\n    }\n}\n```\n\nThings to note:\n\nCommands follow a `resolve` then `run` structure.\n\n`run` doesn't print its final output. It returns something that implements\n`CliOutput` / `CliOutputHuman` which the caller can then print.\n\n`resolve` returns `CliResult` because it needs to reject bad user input.\n\n`run` returns `anyhow::Result` because it can only hit internal errors. Bad\nuser input is handled by `resolve`.\n\n`resolve` translates CLI arguments into domain targets and validates user\ninput. It may query repository state to disambiguate or reject input, but\nshould not retain derived data in the operation.\n\n`run()` loads current repository state and computes results. Commit counts,\ndiffs, statistics, branch details, current tips, and workspace projections\ngenerally belong in `run()`. State derivable from those identifiers and the\ncontext belongs in `run()`.\n\nThe operation should not contain types from `crate::args::atoms`.\n\nA non-CLI caller such as the TUI should be able to construct an operation from\ndomain identifiers without reproducing repository queries from `resolve()`. If\nit must compute counts, diffs, or branch metadata to build the operation, those\nfields probably belong in `run()`.\n\nThe operation does not contain diff specs (usually in the form\n`Vec<DiffSpec>`). It should instead contain `CliId`s which `run` turns into\n`Vec<DiffSpec>` using `DiffSpecBuilder`. This makes it easier for the status\nTUI to call `run` directly.\n\nUser-input pickers and prompts are created via `InputOutputChannel`\naccessed through `IntermediateChannel::prepare_for_terminal_input`.\n\nThe JSON output includes both commit IDs and change IDs.\n\nUse `AllowMergedArg` and `MergedUpstream` to verify we don't mutate merged\ncommits and branches.\n\nPrinting of commits, branches, change IDs, etc. uses newtypes from `theme` such\nas `theme::Branch` and `theme::Commit`. This ensures consistent coloring.\n\nBad user input errors use `bad_input(...)`, optionally with `.arg_name()`,\n`.arg_value()`, and `.hint()`.\n\nAvoid making the operation or outcome types implement `serde::Serialize`.\nDefine the specific types needed inside `fn on_json`. This makes it harder to\nbreak compatibility by accident. Reusing `Serialize` from existing domain types\nis fine.\n\n## More examples\n\nFor examples of this structure in practice see\n\n- `crates/but/src/command/legacy/commit.rs`\n- `crates/but/src/command/legacy/move.rs`\n- `crates/but/src/command/legacy/squash.rs`\n- `crates/but/src/command/legacy/diff.rs`\n",".agents/skills/lite-render-perf/SKILL.md":"---\nname: lite-render-perf\ndescription: Use when working on React code in `apps/lite` that derives values during render, adds or consumes a context, subscribes to the redux store or a query, or renders lists of row components — and whenever the lite UI is reported slow, laggy, or re-rendering too much. React Compiler does not prevent these regressions; load this skill BEFORE writing such code, not only when debugging it.\n---\n\nThe recurring render-performance failure in this app is accidental\nover-re-rendering: one interaction re-renders every row instead of the few\naffected ones. Repositories can have hundreds of branch rows, so this turns\nsingle-digit-millisecond interactions into triple-digit ones. The compiler\nmakes it easy to cause by accident because it silently stops memoizing\nexactly where you assume it won't.\n\n## The three mechanisms behind past regressions\n\n### 1. Whether the compiler memoizes a derivation is not predictable from the source\n\nThe compiler only caches what it can prove safe to cache, and it declines\nmore often than you would guess. Two derivations in this app are both\nmodule-level functions that call imported helpers, and they compile\ndifferently: `buildOutlineNavigationIndex` in `WorkspacePage.tsx` gets a\nmemo block keyed on its inputs, while `getDiffView` in `Details.tsx` got\nnone and re-ran on every render of a component that re-renders whenever the\nfile selection changes.\n\nUnmemoized inputs are contagious. `getDiffView` was called with values\nbuilt by `Match` pipes that the compiler had not memoized either; feeding\nit stable values instead was enough to make the memo block appear. So the\nchurn spreads outward from whichever value first fails to get a scope,\nwhich is why one unmemoized derivation can quietly cost far more than\nitself.\n\nEither way the effect is the same: the derivation re-runs and returns fresh\nidentities even when its data never changed, and everything keyed on those\nidentities — context values, props, effect deps — churns with it.\n\nWhat to do instead:\n\n- Derive query data inside the query's `select`. react-query caches the\n  result on `(data, select function identity)`, and the compiler DOES\n  memoize the closure on its captures — so the closure's identity becomes\n  exactly the dependency key you want. Extra inputs are simply captured by\n  the closure. Precedent: `useBranchesOutline.ts`. `useQueries` /\n  `useSuspenseQueries` give you the same lever in `combine` — precedent:\n  `lineStats` in `OutlineTree/UncommittedChangesRow.tsx`, which folds the\n  results down inside `combine` instead of mapping them out and deriving in\n  render the way `Details.tsx` does. Check what the closure captures:\n  capture a value that is itself rebuilt each render and you have only\n  moved the churn one level up.\n- For redux-derived collections, use `createSelector` (see\n  `projects/project.ts`), never a plain function returning a fresh array or\n  object.\n- Never conclude from the source that something is or is not memoized —\n  read the compiler's output. A compiled function imports\n  `react/compiler-runtime` and allocates slots with `const $ = _c(n)`; a\n  memoized value is assigned inside an `if ($[i] !== dep)` block, and its\n  dependencies are exactly the values that block tests. A derivation\n  sitting outside every such block runs on every render. Two ways to look:\n  fetch the vite-transformed module from the running app\n  (`fetch('/src/path/to/file.ts').then(r => r.text())` over CDP — see\n  below), or run the plugin offline on any file, which needs no app:\n\n```js\n// Resolve both from node_modules/.pnpm — pnpm does not hoist them. There is\n// no @babel/preset-typescript here, so strip types via parserOpts instead.\nawait babel.transformAsync(source, {\n\tfilename: file,\n\tparserOpts: { plugins: [\"typescript\", \"jsx\"] },\n\tplugins: [[\"babel-plugin-react-compiler\", { target: \"19\" }]],\n\tconfigFile: false,\n\tbabelrc: false,\n});\n```\n\n### 2. Context re-renders every consumer; it has no selector\n\n`use(SomeContext)` re-renders the component whenever the context value's\nidentity changes — there is no way to subscribe to a slice, and neither the\ncompiler nor `React.memo` can bail a context read out. Note this is a\ndifferent failure from mechanism 1: when the value's content legitimately\nchanged, fixing identities cannot help — the only cure is consumers\nsubscribing to something narrower than the whole value.\n\nWhat to do instead:\n\n- Context is for values that change rarely relative to their consumers.\n- Per-row state must be a narrow `useAppSelector` returning a primitive\n  (usually a boolean). Redux subscriptions only re-render when the selected\n  value changes, so a selection change re-renders two rows, not all of them:\n\n```tsx\nconst isSelected = useAppSelector((state) => {\n\tconst stored = projectSlice.selectors.selectPrimaryOutlineSelection(state, projectId);\n\treturn stored !== null && operandEquals(stored, operand);\n});\n```\n\nFor the current shape, see `useIsSelected` in `BranchesList.tsx` and the\n`selectIsSelectedOutline` subscription in `OutlineTree/ItemRow.tsx`.\n\n- If rows need a value that requires resolution against a big object,\n  resolve it once at the top and store the resolved form, rather than\n  letting every row resolve independently.\n\n### 3. Identity discipline in the store and in query results\n\n- Reducers must early-return when the update is a no-op, otherwise they\n  produce a new state identity and wake every subscriber.\n- Reading store data inside an event callback needs no subscription at all —\n  take `const store = useAppStore()` at the top and call `store.getState()`\n  in the callback, instead of subscribing with `useAppSelector` to a value\n  only the callback reads.\n- Destructure `useMutation` results (`const { isPending, mutate } = ...`) —\n  the top-level result object is a new identity every render\n  (`@tanstack/query` `no-unstable-deps`).\n- Hook call order can change what the compiler memoizes; moving plain\n  `useQuery` calls above a `useQueries` call has fixed memoization here\n  before. If the compiled output shows a value outside every memo slot for\n  no visible reason, try reordering the hooks around it.\n\n## Driving the dev app over CDP\n\n`pnpm dev` in `apps/lite` starts vite (port 5173) and Electron with\n`--remote-debugging-port=9222`. The first run compiles a helper binary with\ncargo and takes minutes; `Port 5173 is already in use` means an instance is\nalready running — reuse it, or `lsof -ti tcp:5173 | xargs kill`. React and\nRedux DevTools are auto-installed in dev builds.\n\n`curl http://127.0.0.1:9222/json` lists debug targets. Pick the entry with\n`\"type\": \"page\"` whose URL is on 5173 — the others are extension service\nworkers. Its `webSocketDebuggerUrl` speaks the Chrome DevTools Protocol; a\nfew lines of Node (global `WebSocket`) are enough to drive it:\n\n```js\n// Usage: node cdp.mjs eval \"<expr>\"   or:   node cdp.mjs shot out.png\nconst page = (await (await fetch(\"http://127.0.0.1:9222/json\")).json()).find(\n\t(t) => t.type === \"page\" && t.url.includes(\"5173\"),\n);\nconst ws = new WebSocket(page.webSocketDebuggerUrl);\nlet id = 0;\nconst pending = new Map();\nconst send = (method, params) =>\n\tnew Promise((res) => {\n\t\tpending.set(++id, res);\n\t\tws.send(JSON.stringify({ id, method, params }));\n\t});\nws.onmessage = (e) => {\n\tconst m = JSON.parse(e.data);\n\tpending.get(m.id)?.(m.result);\n};\nawait new Promise((res) => (ws.onopen = res));\nconst [mode, arg] = process.argv.slice(2);\nif (mode === \"eval\")\n\tconsole.log(\n\t\tJSON.stringify(\n\t\t\tawait send(\"Runtime.evaluate\", {\n\t\t\t\texpression: arg,\n\t\t\t\tawaitPromise: true,\n\t\t\t\treturnByValue: true,\n\t\t\t}),\n\t\t),\n\t);\nelse if (mode === \"shot\") {\n\tconst { data } = await send(\"Page.captureScreenshot\", { format: \"png\" });\n\t(await import(\"node:fs\")).writeFileSync(arg, Buffer.from(data, \"base64\"));\n}\nprocess.exit(0);\n```\n\nUseful from `Runtime.evaluate`:\n\n- The whole preload API is callable: `window.lite.branchList(projectId)`\n  etc. — the fastest way to inspect real API payloads and sizes.\n- Interact by dispatching events on DOM nodes found via `aria-label`s;\n  screenshot to verify visually.\n\nGotchas:\n\n- Repeated evaluates share one top-level scope: wrap every expression in a\n  `{ ... }` block or `const x` collides with the previous call\n  (\"Identifier 'x' has already been declared\").\n- Synthetic `MouseEvent`s do not move real focus; behavior wired to\n  `onFocus` (selection scopes) needs an explicit `.focus()` or real input.\n- Renderer edits arrive via HMR, but edits to store/redux modules trigger a\n  full page reload that wipes `window.*` state — re-arm any probes after.\n  Electron main-process edits are only compiled at startup: restart\n  `pnpm dev`.\n\n## Measuring: prove it, in both directions\n\nReact DevTools being installed means its hook is available in-page:\n\n1. Arm a commit logger and interact:\n\n```js\nconst hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;\nwindow.__log = [];\nconst orig = hook.onCommitFiberRoot.bind(hook);\nhook.onCommitFiberRoot = (id, root, ...rest) => {\n\twindow.__log.push({ dur: root.current.actualDuration });\n\treturn orig(id, root, ...rest);\n};\n```\n\n2. Judge by `actualDuration` per commit: an interaction should cost\n   single-digit milliseconds and re-render O(affected rows). A commit whose\n   duration scales with total row count is the signature of a blast.\n3. To find WHY, read the compiled module (mechanism 1 above). To watch a\n   specific context value's identity across commits, extend the logger to\n   walk `root.current` for the provider fiber (its `type` is the context\n   object, so match on `type.displayName`) and compare `memoizedProps.value`\n   against the previous commit's. Do NOT add `window.*` probe writes inside\n   components or hooks to trace identities — that mutation makes the\n   compiler bail out of the whole function and destroys the very memoization\n   you are measuring.\n4. Restore `onCommitFiberRoot` when done; measure before and after the fix.\n\n## Review checklist\n\n- Any non-trivial derivation in render: is its result's identity\n  load-bearing (context value, prop to many children, effect dep)? Move it\n  into `select`/`combine`/`createSelector`, or read the compiled output to\n  prove it harmless — do not assume the compiler handled it.\n- Any new context: how often does its value change, and how many consumers?\n- Any `useAppSelector`: does it return a primitive or a stored reference,\n  not a fresh object?\n- Any new reducer: does it no-op cleanly when the value is unchanged?\n",".agents/skills/lite-screenshots/SKILL.md":"---\nname: lite-screenshots\ndescription: Use when asked to add before/after screenshots of a Lite UI change to a pull request, when a PR touching apps/lite/ui needs its visual change shown, or when extending the screenshot catalogue in apps/lite/e2e/tests/screenshots.spec.ts. Captures both sides locally against seeded fixtures, publishes the surfaces that changed, and posts them to the PR.\n---\n\nA reviewer reading a CSS diff cannot see the result. This captures the Lite UI\nwith the branch applied and again with it unapplied, and posts the surfaces that\ndiffer to the pull request.\n\nCapture runs **on this machine**, not in CI. It was tried in CI and abandoned:\nunder the bare X server on the Linux runners this Electron build paints a frame\non load and not reliably afterwards, so several surfaces hung indefinitely\nwhether the capture went through Playwright or the DevTools protocol. The same\nspec passes here. The CI workflow only leaves a reminder.\n\n## Before capturing: is the surface covered?\n\nWork this out first, by reading the diff — not by capturing and inferring it from\nthe result. A run that reports \"8 surfaces, 0 changed\" looks identical whether the\nchange is invisible or whether nothing looked at it.\n\n1. Identify which screen the change affects.\n2. Check it against the catalogue in `apps/lite/e2e/tests/screenshots.spec.ts`\n   (currently: workspace sidebar, diff pane, branches tab, upstream tab, project\n   picker, uncommitted rows, commit form, settings).\n3. **Covered** — capture, as below.\n4. **Not covered** — stop and ask the developer, offering the two lanes below.\n   Either way it is their call, not one to make quietly inside a screenshot\n   request. Tell them which screen is uncovered and which fixture would reach it.\n   - **Add it to the catalogue** when the gap is a _screen_ — something anyone\n     working nearby will change again. It edits a shared file and becomes\n     permanent coverage for every later pull request. See \"Extending the\n     catalogue\" below.\n   - **Capture it ad hoc** when the gap is a _state_ — a conflicted commit\n     mid-resolution, an empty list, one specific dialog — that no future pull\n     request is likely to want. Nothing shared changes and nothing is left\n     behind. See \"One-off capture\" below.\n\n   Prefer the catalogue when it is a close call. The ad-hoc lane photographs the\n   change once and protects nothing afterwards, and a catalogue that stops growing\n   makes every later request re-derive the same fixtures.\n\nIf they decline both, **do not post a comment**. A normal comment would report\n\"N surfaces, 0 changed\", which asserts the change is not visual when in truth it\nwas never photographed. Say plainly that the affected screen is not in the\ncatalogue and no screenshots were taken.\n\n## Changes the capture cannot see\n\nSome diffs are invisible to this harness for reasons that have nothing to do with\nthe change being non-visual. Rule these out before reading a \"0 changed\" result\nas an answer:\n\n- **Scrollbars and their gutter.** macOS hides scrollbars unless a mouse is\n  attached, and a styled `::-webkit-scrollbar` does _not_ override that — the\n  scroller simply reserves no gutter. On a machine set to \"When scrolling\", any\n  change to the track, thumb, gutter width, or a separator meeting the gutter\n  captures identically on both sides. Set System Settings → Appearance → Show\n  scroll bars → **Always** for the run, and say in the comment that you did.\n- **Hover, focus, and drag states** are not entered by the spec.\n- **Anything behind a scroll position** — surfaces are captured at the top.\n\n## Prerequisites\n\n- `cargo build -p but` once, giving `target/debug/but`.\n- The branch under test applied, and **not yet merged** — the base side comes\n  from unapplying it. Check `but status` first.\n- **Run every capture through `env -u ELECTRON_RUN_AS_NODE`.** An agent hosted\n  inside VS Code (itself Electron) inherits `ELECTRON_RUN_AS_NODE=1`, which makes\n  the Electron the test launches behave as plain Node. Every surface then fails\n  with `Error: Process failed to launch!` and nothing says why — the binary is\n  fine, and running it by hand from another shell works. Check with\n  `env | grep ELECTRON`.\n\n## The flow\n\nRun every command from the repository root.\n\n### 1. Capture the branch as it stands\n\n```console\n$ env -u ELECTRON_RUN_AS_NODE SCREENSHOT_OUT=head BUT=$PWD/target/debug/but \\\n    pnpm -F @gitbutler/lite test:e2e screenshots.spec.ts\n```\n\nOutput lands in `apps/lite/e2e/screenshots/head/` (gitignored).\n\n### 2. Capture the base\n\n```console\n$ but unapply <branch>\n$ but status        # confirm the branch is gone from the workspace\n$ env -u ELECTRON_RUN_AS_NODE SCREENSHOT_OUT=base BUT=$PWD/target/debug/but \\\n    pnpm -F @gitbutler/lite test:e2e screenshots.spec.ts\n$ but apply <branch>\n```\n\n**Check the unapply took effect.** It silently does nothing on a stale id or an\nalready-merged branch, and both runs then capture identical code — which looks\nexactly like \"this change is not visual\". If every pair matches in step 3, assume\nthis went wrong before concluding anything about the change.\n\nRe-apply immediately, before anything else can fail and leave the workspace short\na branch.\n\n**Stacked branches take the whole stack down.** `but unapply` on a branch that is\npart of a stack unapplies every branch in it, including ones this capture depends\non — unapplying a stacked change also removes the spec, and the base run then has\nno tests to execute. Check `but status` first. When the branch is stacked, capture\nthe base by reverting the change in the working tree instead:\n\n1. Edit the changed files back to their pre-change state by hand.\n2. Capture into `base`.\n3. `but discard <file-id>` to restore the committed version.\n\nConfirm the restore: the change must be back before anything else happens.\n\n### 3. Select what changed\n\n```console\n$ node apps/lite/e2e/compare-screenshots.mjs \\\n    apps/lite/e2e/screenshots/base \\\n    apps/lite/e2e/screenshots/head \\\n    /tmp/publish \\\n    \"https://raw.githubusercontent.com/<owner>/<repo>/pr-screenshots/pr-<number>/<short-sha>\" \\\n    /tmp/section.md\n```\n\nIt prints `total=`, `changed=`, `added=`, stages only the differing pairs into\n`/tmp/publish`, and writes the comment body. Surfaces that did not change are\nbyte-identical, so they are folded into a collapsed list rather than shown.\n\n### 4. Publish the images\n\nImages go on an orphan `pr-screenshots` branch, under `pr-<number>/<short-sha>/`.\nA per-commit directory matters: reusing one path lets GitHub's image proxy serve\nthe previous run's screenshots from cache, which reads as \"my change did nothing\".\n\nWrite them with the git data API rather than a local clone. There is nothing to\nclone, nothing to clean up, and no local `git` writes — which agents are often\nnot permitted to make outside the project, and which cannot touch the GitButler\nworkspace by accident.\n\n```console\n$ REPO=<owner>/<repo>; DIR=pr-<number>/<short-sha>\n$ HEAD_SHA=$(gh api repos/$REPO/git/refs/heads/pr-screenshots --jq '.object.sha')\n$ BASE_TREE=$(gh api repos/$REPO/git/commits/$HEAD_SHA --jq '.tree.sha')\n\n# One blob per image. base64 must be stripped of newlines.\n$ jq -n --rawfile c <(base64 -i /tmp/publish/<name>.png) \\\n      '{content: ($c | gsub(\"\\n\";\"\")), encoding: \"base64\"}' > /tmp/blob.json\n$ BLOB=$(gh api -X POST repos/$REPO/git/blobs --input /tmp/blob.json --jq '.sha')\n\n# One tree and one commit for all of them, then move the ref.\n$ jq -n --arg t \"$BASE_TREE\" --arg b \"$BLOB\" --arg d \"$DIR\" '{base_tree: $t, tree: [\n      {path: ($d + \"/<name>.png\"), mode: \"100644\", type: \"blob\", sha: $b}]}' > /tmp/tree.json\n$ TREE=$(gh api -X POST repos/$REPO/git/trees --input /tmp/tree.json --jq '.sha')\n$ jq -n --arg t \"$TREE\" --arg p \"$HEAD_SHA\" \\\n      '{message: \"Screenshots for #<number>\", tree: $t, parents: [$p]}' > /tmp/commit.json\n$ COMMIT=$(gh api -X POST repos/$REPO/git/commits --input /tmp/commit.json --jq '.sha')\n$ gh api -X PATCH repos/$REPO/git/refs/heads/pr-screenshots -f sha=\"$COMMIT\"\n```\n\nWhen the branch does not exist yet, omit `base_tree` and `parents`, then create\nthe ref with `POST git/refs -f ref=refs/heads/pr-screenshots -f sha=$COMMIT`.\n\n**Fetch one raw URL before posting.** A comment full of broken images is worse\nthan no comment, and the failure is invisible until someone opens the pull\nrequest:\n\n```console\n$ curl -s -o /dev/null -w '%{http_code}\\n' \\\n    https://raw.githubusercontent.com/<owner>/<repo>/pr-screenshots/<dir>/<name>.png\n```\n\n### 5. Post to the pull request\n\nUpsert a single comment rather than stacking one per run — find a previous\ncomment starting with `<!-- lite-screenshots -->` and PATCH it, else POST.\n\n```console\n$ jq -Rs '{body: .}' /tmp/section.md > /tmp/payload.json\n$ gh api \"repos/<owner>/<repo>/issues/<number>/comments\" --paginate \\\n    --jq 'map(select(.body | startswith(\"<!-- lite-screenshots -->\"))) | first | .id // empty'\n$ gh api -X PATCH \"repos/<owner>/<repo>/issues/comments/<id>\" --input /tmp/payload.json\n```\n\nDo not edit the pull request description: rewriting text a human owns risks\nclobbering their concurrent edits.\n\n### 6. Add the label\n\n`screenshots` is the record that they were posted. Add it once the comment is\nup, so the label says what is true:\n\n```console\n$ gh pr edit <number> --remove-label \"screenshots needed\" --add-label \"screenshots\"\n```\n\nCI labels any pull request with CSS in it `CSS`, and asks one that touches\nLite's for screenshots only if the change is visible; leave that label alone,\nit describes the diff.\n`screenshots needed` is one a human applies by hand when they do want them, and\nthe command above clears it if it is there.\n\nOnly after a comment with images actually went up. A pull request labelled\n`screenshots` with none attached is worse than an unlabelled one — it tells a\nreviewer the change has been shown when it has not. If you declined to post (an\nuncovered surface, a capture that failed), leave the labels as they are.\n\nScreenshots are asked for once per pull request and never again, so the label\nis final — a later push does not undo it, and a set posted here is not a claim\nabout code pushed after it. Recapture when the surfaces move; the command above\nis idempotent either way.\n\nIf you ever post images that are **not** captures of the running app — a page\nrendered from the branch's CSS, say, when the harness cannot be run — label them\nas such in the comment and mark it `<!-- lite-screenshots-replica -->` instead.\nA reviewer assumes a screenshot is a photograph of the app; anything else has to\nsay so, and the different marker keeps a later real run from overwriting it.\n\n## Before finishing\n\n- **Look at the images**, do not just trust exit codes. A green run proves files\n  were written, not that they show the surface.\n- Confirm the workspace is whole: `but status` should show the branch applied and\n  no stray uncommitted changes.\n\n## One-off capture\n\nFor a state the catalogue should not carry permanently. The run command filters\nby filename, so any spec whose name contains `screenshots` is picked up by the\nsame command and writes into the same `SCREENSHOT_OUT` directory — the compare\nstep cannot tell the difference.\n\nWrite `apps/lite/e2e/tests/screenshots-adhoc.spec.ts`, importing the shared\nhelpers rather than copying them (every workaround in them was earned, and a copy\nloses the comments explaining why):\n\n```ts\nimport { enabled, openProject, shoot } from \"../screenshot-helpers.ts\";\nimport { test } from \"../test.ts\";\n\ntest.describe(\"screenshots\", () => {\n\ttest.skip(!enabled, \"set SCREENSHOT_OUT to capture screenshots\");\n\ttest.describe.configure({ timeout: 180_000 });\n\ttest.use({ scenario: \"project-in-single-branch-three-branch-stack.sh\" });\n\n\ttest(\"<surface>\", async ({ appWindow }) => {\n\t\tawait openProject(appWindow);\n\t\t// ...reach the state...\n\t\tawait shoot(appWindow, \"<surface-name>\", \"<selector>\");\n\t});\n});\n```\n\nThe same fixture and selector rules as the catalogue apply — see below.\n\nThen capture both sides as usual, and:\n\n- **Delete the file** once the base run is done. It is untracked and not ignored,\n  so left behind it shows up as a stray change in `but status` and eventually in\n  someone's commit. The \"confirm the workspace is whole\" check at the end is what\n  catches this.\n- **Say in the comment** that the surface was captured ad hoc and is not covered\n  going forward, so nobody reads it as a regression guarantee.\n\n**Never run the two specs in one command.** The config sets `fullyParallel`, and\nthe catalogue spec wipes `SCREENSHOT_OUT` in a `beforeAll` — run concurrently, that\nwipe lands in the middle of the ad-hoc capture and deletes it. What survives is a\nplausible-looking directory missing exactly the surface you were asked for.\n\nThe trailing argument is a **substring match on the file path**, so a bare\n`screenshots` selects the ad-hoc spec as well and puts you straight into that race.\nName the file: `screenshots.spec.ts` for the catalogue, `screenshots-adhoc` for the\nad-hoc spec. Steps 1 and 2 above already do.\n\nCapture the ad-hoc surface **alone**, filtering to its filename, into its own\noutput directory on each side:\n\n```console\n$ env -u ELECTRON_RUN_AS_NODE SCREENSHOT_OUT=adhoc-head BUT=$PWD/target/debug/but \\\n    pnpm -F @gitbutler/lite test:e2e screenshots-adhoc\n```\n\nIts own directory also avoids inheriting stale PNGs from an earlier catalogue run,\nsince only the catalogue spec cleans up after itself. Then compare\n`adhoc-base` against `adhoc-head` as in step 3.\n\nWhen a change touches both a covered screen and an uncovered state, run the\ncatalogue first and the ad-hoc spec second, in separate commands, pointing both at\nthe same directory — that order is safe because only the first one wipes.\n\n## Extending the catalogue\n\nOnly after the developer has agreed (see the top of this document). Add the\nsurface to `apps/lite/e2e/tests/screenshots.spec.ts`, then capture as usual —\nthe same run proves the new surface works and shows the change. Coverage applies\nto every later pull request, not just this one.\n\n```ts\ntest.describe(\"<area>\", () => {\n\ttest.use({ scenario: \"<fixture>.sh\" });\n\n\ttest(\"<surface>\", async ({ appWindow }) => {\n\t\tawait openProject(appWindow);\n\t\tawait goToTab(appWindow, \"branches\"); // when it lives on another tab\n\t\tawait shoot(appWindow, \"<surface-name>\", \"<selector>\");\n\t});\n});\n```\n\n- `<surface-name>` becomes the filename and the heading a reviewer reads: name it\n  `commit-form`, not `outline-panel-2`.\n- Only fixtures calling `\"$BUT\" setup` register a project; the rest leave the app\n  on \"Select a project.\" and every capture fails. Known good:\n  `project-in-single-branch-three-branch-stack.sh`,\n  `project-with-remote-branches.sh`, `project-with-conflicting-commits.sh`.\n- If no fixture reaches the state, build it in the test — `uncommitted` writes\n  files into `testEnvironment.workdir/local-clone` and reloads.\n- Prefer a stable id for the clip (`#outline-panel`, `#details-panel`), else a\n  CSS-module prefix or an ARIA hook. Never nth-child or a generated class.\n- One screenshot per test, taken after a fresh load. This costs nothing here and\n  keeps the spec usable if CI capture is ever revisited.\n\n## Out of scope\n\n- **Native context menus** cannot be captured; they are OS windows.\n- **Dark mode** is not covered — the harness seeds `theme: \"light\"`.\n- **States needing a real remote or credentials** cannot be fixtured.\n",".agents/skills/tui-tests/SKILL.md":"---\nname: tui-tests\ndescription: Use when adding or modifying tests for one of GitButler's Ratatui TUIs\n---\n\n## Where tests live\n\n- Main TUI tests: `crates/but/src/command/legacy/status/tui/tests/`\n- Test harness/helpers: `crates/but/src/tui/test_utils.rs` and `crates/but/src/command/legacy/status/tui/tests/utils.rs`\n- Snapshots: `crates/but/src/command/legacy/status/tui/tests/snapshots/`\n\n## Basic pattern\n\n```rust\n#[test]\nfn describes_behavior_under_test() {\n    let env = Sandbox::init_scenario_with_target_and_default_settings(\"one-stack\").unwrap();\n    env.setup_metadata(&[\"A\"]).unwrap();\n\n    let mut tui = test_status_tui(env);\n\n    tui.input_then_render(KeyCode::Down)\n        .assert_rendered_term_svg_eq(file![\"snapshots/describes_behavior_under_test_001.svg\"]);\n}\n```\n\n## Driving the TUI\n\nUseful input examples:\n\n```rust\ntui.input_then_render(None);                                       // render without inputs\ntui.input_then_render('j');                                        // single char input\ntui.input_then_render(KeyCode::Down);                              // special key\ntui.input_then_render(Shift('j'));                                 // keys with shift\ntui.input_then_render(Control('j'));                               // keys with control\ntui.input_then_render([KeyCode::Down, KeyCode::Down]);             // multiple keys from array\ntui.input_then_render(\"commit message text\");                      // multiple keys from string\ntui.reload();                                                      // reload state after making external changes\n```\n\n## Assertions\n\nGenerally prefer\n\n- `assert_current_line_eq(str![...])` for cursor/selection behavior.\n- `assert_rendered_term_svg_eq(file![\"snapshots/test_function_name_001.svg\"])`\n  for everything else.\n\nGenerally you should include one `assert_rendered_term_svg_eq` per logical\ngroup of inputs, to catch bad states early.\n\nBe careful using `assert_rendered_contains` and `assert_rendered_not_contains`\nsince they might lead to false positives. They're intended to use while\niterating on a test where snapshots would cause too much churn.\n\nRead `crates/but/src/tui/test_utils.rs` and\n`crates/but/src/command/legacy/status/tui/tests/utils.rs` for more specialized\nassertions.\n\nYou're not allowed to add new kinds of assertions to `crates/but/src/tui/test_utils.rs` or\n`crates/but/src/command/legacy/status/tui/tests/utils.rs`. Rely entirely on the\nexisting assertions.\n\nDon't reuse snapshots from other tests. Give each test its own snapshots.\n\nAvoid using `tui.env().invoke_git(\"...\")`. Prefer performing operations via the TUI.\n\n## Running tests\n\n- `cargo test -p but <test-name>` to run one test.\n- `SNAPSHOTS=overwrite cargo test -p but <test-name>` to run and update\n  snapshots.\n- `cargo test -p but tui` to run all tui tests. Do this after changing\n  things.\n\nIf a test fails the output will include the rendered state of the test backend.\nThis can be used when iterating on a test as a way of inspecting the state.\n",".github/copilot-instructions.md":"## General information\n\nThis is a Rust/Svelte/React/TypeScript monorepo with multiple projects. For\nagent instruction precedence and scoped guidance, follow the nearest applicable\n`AGENTS.md`; use this file as Copilot-oriented project context.\n\nThe main applications are found in the `apps` directory:\n\n- `desktop` containing the Tauri application's frontend code\n- `web` containing the web application's frontend code\n- `lite` containing the Electron/React desktop app\n\nRust code is found in the `crates` directory. It contains `gitbutler-tauri` for\nthe Tauri application, `but-api` for the API surface used by Tauri, Electron,\nCLI, and TUI callers, and the `but` CLI crate.\n\nThe `packages` directory contains different self-contained npm packages.\nThese are shared between the applications.\nThe packages are:\n\n- `but-sdk` containing the generated TypeScript/Node SDK for Rust APIs\n- `core` containing shared TypeScript helpers\n- `ui` containing the shared UI components\n- `shared` containing the shared types and utils\n- `no-cross-domain-imports` containing the no-cross-domain-imports ESLint package\n- `no-relative-imports` containing the no-relative-imports ESLint package\n- `svelte-comment-injector` containing the Svelte comment injector package\n\nThe `e2e` directory contains Playwright, WebdriverIO, and blackbox end-to-end\ntests.\n\n## Technology Stack\n\n- **Frontend**: Svelte with TypeScript\n- **Backend**: Rust\n- **Desktop Framework**: Tauri (like Electron but using Rust instead of Node.js)\n- **Build Tools**: Turborepo for monorepo management, pnpm for package management\n- **Git Libraries**: gix (gitoxide) and git2 (libgit2)\n- **Testing**: Vitest for JS/TS, Playwright for E2E tests, standard Rust testing framework\n\n## Development Setup\n\n### Prerequisites\n\n1. **Rust**: as specified in `rust-toolchain.toml`. Install via rustup.\n2. **pnpm**: Install standalone pnpm as described in [DEVELOPMENT.md](../DEVELOPMENT.md#prerequisites).\n3. **System Dependencies**: Tauri requires platform-specific dependencies (see DEVELOPMENT.md for details).\n\n### Initial Setup\n\n```bash\n# Clone the repository\ngit clone https://github.com/gitbutlerapp/gitbutler.git\ncd gitbutler\n\n# Install dependencies and the project-managed Node.js runtime\npnpm install\n\n# Build Rust binaries (required before running the app)\n# look at https://github.com/gitbutlerapp/gitbutler/blob/fd9a58de5579074c0526193143d531c21907a26b/scripts/install-tauri-debian-dependencies.sh\n# for Linux prerequisites for tauri.\ncargo build\n\n# or use this to build the `but` CLI, without the need for tauri dependencies.\ncargo build -p but\n```\n\n## Building and Running\n\n### Development\n\n```bash\n# Run desktop app in development mode\npnpm dev:desktop\n\n# Run with debug logs\nLOG_LEVEL=debug pnpm dev:desktop\n\n# Run web app\npnpm dev:web\n\n# Run UI component storybook\npnpm dev:ui\n```\n\n### Building\n\n```bash\n# Build all packages\npnpm build\n\n# Build desktop app only\npnpm build:desktop\n\n# Build for production (used for releases)\npnpm tauri build --features devtools --config crates/gitbutler-tauri/tauri.conf.nightly.json\n```\n\n## Testing\n\nWhen asked about frontend tests please read `@frontend.md` for context.\n\nTo run rust tests run `cargo test`, and for specific crates `cargo test -p CRATE_NAME`\n\n## Code Style and Linting\n\n### JavaScript/TypeScript\n\n**Linting**: ESLint with TypeScript and Svelte plugins\n**Formatting**: Prettier with Svelte plugin\n\n```bash\n# Check linting\npnpm lint\n\n# Fix linting issues\npnpm fix\n\n# Format code\npnpm format\n\n# Check formatting\npnpm prettier\n\n# Shortcut to check everything\npnpm isgood\n\n# Shortcut to fix and format everything\npnpm begood\n```\n\n**Important ESLint Rules**:\n\n- No relative imports (use `@gitbutler/` package references)\n- Import order: alphabetically sorted with specific group ordering\n- No console.log (use console.warn or console.error)\n- Prefer function declarations over arrow functions at top level\n- Svelte-specific rules for button types, unused props, etc.\n\n**Prettier Config**:\n\n- Tabs for indentation\n- Double quotes\n- No trailing commas\n- 100 character line width\n\n### Rust\n\n**Formatting**: Use rustfmt with nightly toolchain for imports grouping\n\n```bash\n# Format Rust code\ncargo fmt\n\n# Format with nightly settings\npnpm rustfmt\n# or (but don't do this unless you are asked to)\ncargo +nightly fmt -- --config-path rustfmt-nightly.toml\n\n# Run clippy for linting\ncargo clippy --all-targets\n```\n\n**Rust Formatting**:\n\n- Group imports: Std, External, Crate\n- Imports granularity: Crate level\n\n**temporary directories** - use `but-testsupport` for scenario creation. Read-only functions should use read-only fixtures. DO NOT USE the `std::env::temp_dir().join(format!(…)` pattern.\n\n## Dependency Management\n\n### Adding JavaScript/TypeScript Dependencies\n\n```bash\n# Add to root workspace\npnpm add -D <package>\n\n# Add to specific package\npnpm add <package> --filter @gitbutler/desktop\npnpm add <package> --filter @gitbutler/ui-svelte\n```\n\n**Always check for vulnerabilities before adding npm packages** (ecosystem: npm).\n\n### Adding Rust Dependencies\n\nEdit the appropriate `Cargo.toml` file:\n\n- Workspace-level dependencies go in `/Cargo.toml` under `[workspace.dependencies]`\n- Individual crate dependencies reference workspace versions when possible, and when it's used more than once.\n\n**Always check for vulnerabilities before adding Rust dependencies** (ecosystem: rust).\n\n## Monorepo Structure\n\n### Workspace Navigation\n\nThe repository uses:\n\n- **Turborepo** for JavaScript/TypeScript build orchestration\n- **Cargo workspace** for Rust crate management\n- **pnpm workspace** for npm package management\n\nWhen making changes:\n\n1. Identify which package/crate is affected\n2. Run builds/tests in that package first\n3. Then verify dependent packages still work\n\n### Crate Organization\n\nThe `crates/` directory contains ~65 Rust crates organized by functionality:\n\n- `gitbutler-*` prefix: Core GitButler functionality. These are old, and we want to port them to `but-*`.\n- `but-*` prefix: Backend utilities and services\n- Many crates have specific purposes (see DEVELOPMENT.md \"Code Hitlist\" for technical debt items)\n\n**Note**: Some crates are marked for refactoring (see DEVELOPMENT.md). Be cautious when modifying:\n\n- gitbutler-reference\n- gitbutler-branch-actions\n- gitbutler-repository\n\n## CI/CD\n\n### Workflows\n\nLocated in `.github/workflows/`:\n\n- `push.yaml`: Main CI for linting, building, and testing on push\n- `publish.yaml`: Release builds for different platforms\n- `test-e2e.yml`: E2E tests (Playwright and blackbox)\n- `test-client-fe-integration.yml`: Frontend integration tests\n\n### Pre-commit Checks\n\nBefore committing, ensure:\n\n1. Code is formatted: `pnpm format && pnpm rustfmt`\n2. Linting passes: `pnpm lint && cargo clippy --all-targets`\n3. Tests pass: `pnpm test && cargo test`\n4. Build succeeds: `pnpm build`\n\nOr use the shortcut: `pnpm isgood`.\nAuto-fix with `pnpm begood && cargo clippy --fix --all-targets`\n\n## Common Patterns and Conventions\n\n### Frontend File Organization\n\n- Use absolute imports via package references (e.g., `@gitbutler/ui-svelte`) instead of relative imports\n- Components should be in logical directories by feature\n- Shared utilities go in `packages/shared`\n- UI components go in `packages/ui-svelte`\n\n### Naming Conventions\n\n**JavaScript/TypeScript**:\n\n- Components: PascalCase (e.g., `BranchCard.svelte`)\n- Files: kebab-case (e.g., `branch-service.ts`)\n- Variables/functions: camelCase\n\n**Rust**:\n\n- Follow standard Rust naming conventions\n\n### Error Handling\n\n**JavaScript/TypeScript**: Throw errors and handle with try/catch at appropriate boundaries\n\n**Rust**: Use `Result<T, E>` and `anyhow` for error handling. Most functions should return `anyhow::Result`.\n\n## Troubleshooting\n\n### Build Issues\n\n```bash\n# Clear Turbo cache\npnpm exec turbo daemon stop\npnpm exec turbo daemon clean\n\n# Clear node_modules and reinstall\nrm -rf .turbo node_modules\npnpm install\n\n# Clear Rust build artifacts\ncargo clean\n```\n\n### Platform-Specific Issues\n\n- **macOS**: Ensure Xcode and cmake are installed\n- **Linux**: Install webkit2gtk and other dependencies (see DEVELOPMENT.md)\n- **Windows**: May require Perl, OpenSSL setup, and specific compiler settings (see DEVELOPMENT.md)\n\n## Performance Considerations\n\n### Development Builds\n\n- Set `GITBUTLER_PERFORMANCE_LOG=1` for performance logging\n- Use `--release` flag for more realistic performance testing\n\n### Turbo Cache\n\n- Turborepo caches build outputs\n- If experiencing stale builds, clear cache: `pnpm exec turbo daemon clean`\n- Case-sensitive filesystem issues may affect Turbo on macOS (see DEVELOPMENT.md)\n\n## Additional Resources\n\n- **Development Guide**: See DEVELOPMENT.md for detailed setup instructions\n- **Contributing Guide**: See CONTRIBUTING.md for contribution guidelines\n- **Discord**: https://discord.gg/MmFkmaJ42D for questions and discussions\n- See `crates/AGENTS.md` for Rust-specific API, repository, and testing conventions\n\n## Important Notes for AI Coding Agents\n\n1. **Always run lints and tests** after making changes\n2. **Use the existing patterns** in the codebase - don't introduce new patterns without discussion\n3. **Keep changes minimal** - surgical changes are preferred over large refactors\n4. **Follow the monorepo structure** - understand which package your change affects\n5. **Check both JS and Rust** - changes to the backend often require frontend updates\n6. **Documentation** - update relevant docs if changing public APIs or user-facing features\n7. **Avoid relative imports** - ESLint will error on relative imports; use package references\n8. **Test on target platform** if making platform-specific changes\n9. **Security**: Check dependencies for vulnerabilities before adding them\n10. **Code marked for refactoring**: Be extra careful with crates in the \"Code Hitlist\" section\n11. **but CLI happy path testing only**: CLI tests are expensive and should be limited to what really matters.\n\n## Review Guidelines\n\nFollow these guidelines when reviewing code.\n\n- Do not report type errors. CI will catch those.\n- Don't review formatting such as commas, indentation, etc\n","AGENTS.md":"# GitButler Agent Instructions\n\nGitButler is a Rust/Svelte/React/TypeScript monorepo.\n\nApply all relevant instruction files. If instructions conflict, resolve them in\nthis order:\n\n1. Explicit human instructions\n2. Nearest nested `AGENTS.md`\n3. This file\n\n## Repo Map\n\n- `crates/` - Rust crates.\n- `apps/desktop/` - Tauri/Svelte desktop app.\n- `apps/web/` - Svelte web app.\n- `apps/lite/` - Electron/React desktop app.\n- `packages/` - shared TypeScript packages, including the SDK.\n- `e2e/` - Playwright, WebdriverIO, and blackbox end-to-end tests.\n\n## Working Style\n\n- Treat questions about the codebase as read-only unless the user asks for changes.\n- Make focused, reviewable changes; avoid unrelated rewrites.\n- Use the simplest design that solves the actual problem; do not add\n  speculative machinery, and remove machinery your change makes unnecessary.\n- Inspect nearby code before introducing patterns.\n- Prefer existing APIs, tests, and conventions.\n- Before declaring shared behavior done, check each applicable surface and contract\n  (desktop, web, Lite, CLI/TUI, N-API, SDK, and docs) and update it or explicitly\n  determine that it is unaffected.\n- Run targeted validation for the area touched.\n- Before adding new machinery to fix a behavior bug, reproduce the bug in a failing\n  test and survey the target file's existing loops and classifications as candidate\n  hosts; let the tests, not the diagnosis, set how much implementation the fix needs.\n- When a fix calls for a new mechanism (a new module, a new public API, or a parallel\n  walk where one already exists), propose the intended shape before building it.\n\n## Scoped Instructions\n\n- For Rust work under `crates/`, follow `crates/AGENTS.md`.\n- For Lite work under `apps/lite/`, follow `apps/lite/AGENTS.md`.\n- For the React component library under `packages/ui-react/`, follow\n  `packages/ui-react/AGENTS.md`.\n","claude.md":"AGENTS.md","crates/AGENTS.md":"# Rust Instructions\n\nThese are defaults for Rust source and test changes under `crates/`; more\nspecific instruction files override them. Do not apply them to generated,\nvendored, or fixture data unless the task is specifically about that code.\n\n## API Boundaries and Legacy\n\n- Treat `gitbutler-*` crates as legacy-heavy, but preserve local ownership and\n  nearby patterns for localized fixes. Do not introduce new `gitbutler-*` usage\n  in newer code unless the surrounding legacy boundary requires it. Keep legacy\n  fixes localized; migrate only when necessary, tiny, tested, and\n  behavior-neutral.\n- Minimize new `VirtualBranchesHandle` usage. At API boundaries, prefer\n  `but_ctx::Context` workspace helpers and newer `but-*` APIs.\n- `but-api` is the API surface for Tauri, Electron/N-API, CLI, and TUI callers.\n  Outer callers should prefer existing `but-api` functions when they fit, but\n  lower-level crates should not depend on `but-api`.\n- Preserve existing `but-api` macro, transport, serialization, and conversion\n  patterns instead of hand-rolling parallel wrappers.\n- Keep transport DTOs at the API boundary, commonly in a local `json` module,\n  and convert them to domain types before calling lower-level crates.\n- For permissioned `but-api` functions, follow the existing composition shape:\n  acquire permission near the wrapper and delegate to a `_with_perm` or other\n  permission-taking implementation.\n- API consumers such as the CLI, TUI, and other composed callers should use\n  `_with_perm` variants when they already hold permission to avoid extra locking\n  and deadlock risk.\n- Keep `Context` at API/composition boundaries; pass granular dependencies\n  like repo, workspace, metadata, database handles, or an `Editor` into\n  lower-level crates where possible.\n- Acquire repository access locks at top-level API/command boundaries. Do not\n  call helpers that acquire permissions while holding a guard; drop the guard or\n  call a permission-taking helper.\n- Preserve existing `DryRun` semantics; add dry-run support when the API/product\n  surface calls for preview behavior. Dry runs should avoid persisting refs,\n  objects, or oplog entries.\n- When an API offers both action-only and timeline-recording behavior, keep the\n  `*_only*` function free of oplog side effects; prepare a best-effort oplog\n  snapshot in the wrapper and commit it only after the mutation succeeds.\n- New user-facing mutations should participate in undo: record an oplog\n  snapshot via the existing `SnapshotDetails`/`OperationKind` wrapper pattern,\n  and call out any mutation intentionally left out of the timeline rather than\n  omitting it silently. Undo is snapshot-based; do not design bespoke inverse\n  operations for it.\n- Before changing or reviewing code that derives graph/workspace/branch/stack/commit\n  relationships, reachability, dependencies, ordering, operation targets, or Git\n  graph/history/ref-placement mutations, use `crates/WORKSPACE_MODEL.md` as the\n  reference. In short: prefer commit IDs and refs at API boundaries, convert to\n  operation-local selectors inside editor-backed operations, use\n  `but_graph::Graph` for relationship/reachability questions, use\n  `but_rebase::graph_rebase::Editor` for Git graph/history/ref rewrites where an\n  editor model exists, and treat `but_graph::Workspace` and\n  `but_workspace::RefInfo` as lossy presentation/compatibility views unless an\n  existing workspace-shaped boundary requires them.\n\n## Code Shape and Naming\n\n- Use Git/GitButler domain names and keep types/helpers in the crate or module\n  that owns the concept.\n- Avoid vague names like `Manager`, `Service`, `Helper`, `Util`, or `Processor`\n  unless nearby code uses that concept.\n- Solve the present problem directly. Avoid speculative abstractions: one-use\n  traits/types/functions, fake extension points, ceremonial wrappers/modules,\n  and public APIs larger than real callers need.\n- Keep module boundaries discoverable from names and the call graph.\n- Avoid drive-by refactors while fixing a specific bug.\n- Prefer explicit domain enum matches when wildcard arms would hide behavior.\n- Comment public API purpose/invariants/errors and complex algorithm intent;\n  avoid comments that merely repeat the code.\n\n## Git Repository Semantics\n\n- Do not rediscover Git repositories inside business logic; pass repo/context\n  explicitly.\n- Prefer repository APIs over shelling out to `git`, except at\n  shell/executable boundaries such as hooks, debug tooling, tests, or Git\n  interop helpers.\n- Preserve Git graph semantics even when the shape looks redundant or odd; do\n  not deduplicate, reorder, or smooth graph data unless Git semantics allow it.\n- Use `gix` for new repository logic. Treat `git2` and `Context::git2_repo` as\n  legacy/boundary-only escape hatches for libgit2 checkout/index, hooks,\n  transport/auth, or existing code that cannot reasonably move yet.\n- Keep Git paths, refnames, commit messages, and diff payloads byte-preserving\n  until UI/API boundaries; avoid lossy `String` conversion in business logic.\n- Treat filesystem and worktree writes carefully; use existing locking,\n  transaction, and access patterns.\n- Avoid implicit `SystemTime::now()` in testable business logic; pass time in\n  when deterministic behavior matters.\n- Use existing reload/invalidation helpers after external repo or worktree\n  changes, especially when working through `Context` caches.\n- Use `anyhow::Context` to explain what operation failed. When frontend/API\n  consumers need classification, use existing `but_error::Code` patterns; do\n  not make consumers match error strings.\n\n## Database Migrations (`but-db`)\n\n- Keep migrations forward-compatible: stay on the current `SchemaVersion` and\n  leave columns or tables that new code stopped using in place. Bumping the\n  version locks every older binary out of the database; reserve it for planned,\n  coordinated breaks, never routine cleanup.\n\n## Version Control\n\n- Assume the worktree may contain other agents' changes. Do not overwrite, clean\n  up, stage, commit, or amend changes you did not make.\n- When asked to branch, commit, push, or open a PR, use the GitButler `but`\n  CLI/workflow when available.\n- When the user says \"ship it\", commit your changes on a session branch,\n  creating one if needed, then push and open or update the PR. Reuse the\n  existing branch/PR when it already fits the session.\n- For small cleanup or follow-up fixes on your own branch, amend the relevant\n  existing commit(s) when that is the cleaner history.\n- Keep commit messages and PR descriptions succinct: why, impact, and core\n  decisions. Do not list local validation commands in commit messages or add AI\n  co-author trailers or tool branding.\n\n## Testing and Validation\n\n- Run the narrowest relevant test or check first, for example `cargo test\n  -p <crate> <test-name>` or `cargo check -p <crate> --all-targets`.\n- Use existing test-support crates and fixtures; prefer read-only fixtures for\n  read-only behavior.\n- For graph, rebase, or workspace behavior, prefer fixture-backed before/after\n  snapshots using existing visualizers, plus targeted structural assertions for\n  the invariant.\n- When snapshot output is volatile, stabilize inputs or normalize output; do not\n  replace strong snapshots with vague assertions.\n- When optimizing Git traversal or workspace behavior, preserve semantics with a\n  fixture-backed regression test.\n- Run `cargo fmt` for Rust formatting, avoiding unrelated dirty files.\n- Run `cargo clippy --fix --allow-dirty` only after inspecting the diff scope and\n  only when generated edits stay within the intended change.\n- Run `cargo machete` when dependencies changed; verify targets, features, and\n  platform-specific usage before removing dependencies.\n- After changing Rust APIs or types exposed through `@gitbutler/but-sdk`, run\n  `pnpm build:sdk && pnpm format`; this updates\n  `packages/but-sdk/src/generated`.\n- Use workspace-wide checks only when the change affects shared contracts or\n  multiple crates.\n\n## Assertions\n\n- Explain why a standard Rust assertion holds concisely using the last-argument message, like `assert!(1!=2, \"arithmetic unit on CPU works\")`\n- Snapshot assertions use `snapbox`, like `snapbox::assert_data_eq!(actual, snapbox::str![[r#\"...\"#]])`. `snapbox::assert_data_eq!` has no message argument, so explain why a snapshot holds with a `// comment` on the line above it.\n- Regenerate inline snapshots by running the tests with `SNAPSHOTS=overwrite`.\n- Remove unstable output before asserting (sanitize ids/timestamps/paths, e.g. the `but_testsupport` helpers) rather than snapshotting it raw. `snapbox` also pattern-matches expected data by default (`[..]` and `...` wildcards, path-separator normalization); append `.raw()` to the expected `snapbox::str!` when the value must match exactly (e.g. it contains backslashes or literal `[..]`/`...`).\n","crates/CLAUDE.md":"AGENTS.md","crates/but-agentlog/skill/SKILL.md":"---\nname: but-agentlog\nversion: 0.0.0\ndescription: \"Use for prompts like \\\"get context for branch\\\", \\\"catch up on this branch\\\", \\\"recover branch context\\\", or \\\"what prior agent work happened here\\\". Skim prior agent work from `but agentlog` captures for a GitButler branch, review / pull request / merge request, or change. Prefer this over generic git branch/diff inspection when the user asks for context, history, prior work, or branch catch-up.\"\nauthor: GitButler Team\n---\n\n# GitButler Agentlog Skill\n\nUse `but agentlog` to skim prior agent work from captured sessions.\n\nTrigger this skill for plain prompts like \"get context for branch\", \"branch\ncontext\", \"catch up on this branch\", \"recover context\", \"what happened on this\nbranch\", or \"what prior agent work happened here\".\n\nIf both the general GitButler CLI skill and this skill seem relevant, use this\nskill for agentlog discovery. Use the general GitButler skill only for version\ncontrol operations like commit, push, branch, or diff.\n\n## Core Workflow\n\nDo not search for or read local `SKILL.md` files. If this skill is active, use\nthese instructions directly. Do not inspect the general GitButler skill first.\nStart with `but agentlog skim`.\n\nStart with `skim` when prior work could affect your next action. It is a\ntable-of-contents view: all related sessions and turns, abbreviated and shown\nchronologically.\n\n```sh\nbut agentlog skim\nbut agentlog skim branch <branch-name-or-ref>\nbut agentlog skim review <review-id-or-pull-request-key>\nbut agentlog skim change <change-id-or-key>\n```\n\nUse `show` only when `skim` is too thin, ambiguous, or you need exact evidence:\n\n```sh\nbut agentlog show <session-key> --limit 20\nbut agentlog show <session-key> --turn <turn-key> --limit 20\n```\n\nFor normal current-branch recovery, run `skim` without target arguments first.\nIt discovers the applied GitButler branch itself. Do not run plain Git commands\nlike `git branch --show-current` or `git status` first.\n\n```sh\nbut agentlog skim\n```\n\nPass an explicit target only when the user gives one.\n\nUse JSON only when you need exact handles for drill-down:\n\n```sh\nbut --json agentlog skim\n```\n\n1. Start with `skim` for a clean orientation.\n2. Treat `skim` like compacted turn history: all related sessions and turns are\n   present, but each turn is abbreviated. It is not the full transcript.\n3. If `skim` is enough for a lightweight status answer, summarize it and stop.\n4. Drill down only when `skim` is ambiguous, misses the rationale, or you need\n   exact evidence.\n5. To drill down, rerun `skim` with `--json` to get the relevant\n   `session_key` and `turn_key`, then use `show`.\n6. Use `show <session-key>` for turn-level context.\n7. Use `show <session-key> --turn <turn-key>` only for turns that need exact detail.\n\n## Target Discovery\n\nWhen the user says \"current branch\" or gives no explicit target, run:\n\n```sh\nbut agentlog skim\n```\n\n`skim` performs GitButler branch discovery automatically. If target discovery\nfails, then inspect GitButler state:\n\n```sh\nbut status\n```\n\nDo not use plain Git's `gitbutler/workspace` branch as an agentlog target.\n\n## Rules\n\n- Prefer orientation before payload.\n- Prefer `skim` for turn-history recovery.\n- Treat `skim` as complete but abbreviated. It includes every related session\n  and every turn in those sessions, not every record or the full transcript.\n- Prefer human `skim` output first. Use `--json` only for drill-down\n  handles or exact evidence.\n- Run `show` when `skim` is thin, ambiguous, missing the why, or when the user\n  is asking you to make or verify a consequential claim.\n- Do not dump records for every candidate session.\n- Do not dump full records or transcripts. If session detail is needed, start\n  with `show <session-key> --limit 20` and summarize previews.\n- Treat matches as related evidence, not ownership.\n- Keep `session_key` and `turn_key` values internal unless the user asks for\n  evidence handles or they are needed for a follow-up command.\n- Use record snippets only when they justify the next action.\n- Say \"session related to branch X via turn Y\", not \"session for branch X\".\n\n## Reading `skim`\n\nUse `skim` to answer \"what has already been discussed, planned, attempted, or\nchanged in this work?\"\n\nHuman output is intentionally compact but includes every related session and\nevery turn in chronological order. JSON output keeps\nfull `session_key` and `turn_key` values so you can drill into `show` without\nprinting those handles to the user.\n\nThe skim is a compressed table of contents. It abbreviates each turn but does\nnot omit related sessions or turns.\n\nUseful fields:\n\n- `target_kind` and `target_key`: target used for discovery.\n- `sessions`: related sessions in chronological order, with counts and previews.\n- `coverage`: shown sessions, shown turns, and direct related turn count.\n- `sessions[].turns`: every turn in order, with labels and previews.\n\nIf `skim` has enough context for a lightweight answer, summarize from it and\nstop. If it is ambiguous or misses the why, use JSON skim handles and drill\ndown with `show`.\n\n## Reading `show`\n\nUse `show` to open a session or one turn.\n\n```sh\nbut agentlog show <session-key> --limit 20\nbut agentlog show <session-key> --turn <turn-key> --limit 20\n```\n\nWithout `--turn`, `show` answers what happened in a session at turn granularity.\n\nUseful fields:\n\n- `coverage`: returned turns versus total turns.\n- `turn_key`: handle for `show --turn`.\n- `turn_index`: order within the session.\n- `capture_kind`: backfill or incremental.\n- `record_count`: turn size before hydrating records.\n- `source_record_index_range`: provider/source indexes covered by the turn.\n- `observed_targets`: targets observed in the turn.\n- `latest_user_preview` and `latest_assistant_preview`: compact orientation.\n- `tool_counts`: tool call/result counts and tool names.\n\nIncrease `--limit` only when the current window misses the related turn or setup.\n\nWith `--turn`, `show` answers what exactly happened inside one turn.\n\nUseful record fields:\n\n- `coverage`: returned records versus total records in the turn.\n- `turn_record_index`: order within the turn.\n- `source_record_index`: original provider/source index when available.\n- `timestamp`, `kind`, `role`, `text`: message/tool orientation.\n- `tool_name` and `tool_input`: tool details when present.\n- `source_record`: redacted stored provider envelope for low-level debugging.\n\nTurn records are intentionally bounded. Increase `--limit` only for a specific\nturn after session-level `show` output proves that more detail is needed.\n\n## Skim Summary\n\nWhen summarizing a skim, do not say you recovered \"full context.\" Say you\nskimmed the full turn history in abbreviated form.\n\nInclude:\n\n- target used for discovery\n- previews explaining intent and outcome\n- minimal record snippets for the next action\n\nDo not lead with raw session or turn identifiers. Mention them only when the\nuser asks for evidence handles or when passing them to another command/tool.\n","crates/but/skill/SKILL.md":"---\nname: but\nversion: 0.0.0\ndescription: \"Commit, push, branch, and manage version control with GitButler. Use for commits, selective dirty-file or hunk commits, branches, diffs, PRs, history edits, squashes, amends, undo, merge, apply, and unapply. For selected dirty files or hunks, inspect with `but diff`; use compact `but status` for commit order, branch/stack placement, or conflict overview; use `but status -fv` when file/hunk IDs or per-commit file details matter. Replaces git write commands.\"\nauthor: GitButler Team\n---\n\n# GitButler CLI Skill\n\nUse GitButler CLI (`but`) as the default version-control interface.\n\n## Start Here\n\nChoose the narrowest first command by task; avoid ritual status checks:\n\n```bash\n# Selected dirty files/hunks:\nbut diff\n\n# Commit order, branch/stack placement, conflict overview:\nbut status\n\n# File/hunk IDs, per-commit files, amend/split details:\nbut status -fv\n\n# Details for one known branch or commit:\nbut show <id>\n```\n\nDo not run plain `but status` and then `but status -fv` unless the compact output lacks file/hunk details needed for the task.\n\nFor \"commit just/only/specific changes on a new branch\", use the fast path:\n\n```bash\nbut diff\nbut commit -b <branch> -m \"<msg>\" <id> <id>\n```\n\n`but commit -b <branch> ...` creates the branch when it does not exist and prints the created commit. Do not run a separate `but branch new`, status command, or verification diff unless the task needs workspace information that the result does not provide.\n\n## IDs\n\nThe first token on each `but diff` / `but status` line is that line's ID. When a command needs an ID, copy it exactly from the current output; never hardcode or invent one. ID lifetimes differ by entity:\n\n- Changes and sources are **positional, space-separated** IDs (`but commit -b feat -m \"msg\" uvw:2e4 uvw:e2c`). An uncommitted hunk ID is written `<file-id>:<hunk-id>` (e.g. `uvw:2e4`, copied from bare `but diff`) — the part after the colon is the hunk's ID, **not** a line range (`uvw:16-40` is invalid). Do not invent flags like `--changes` / `--hunk` / `--ids`, pass a line range, or comma-separate IDs — `uvw,qyo` is parsed as one ID and fails.\n- `but diff` is the exception: it accepts at most **one** target. Bare `but diff` shows all uncommitted files; inspect committed files or other entities one target at a time — never `but diff <id> <id>`.\n- A committed file is `<commit-id>:<file-id>` (e.g. `mzm:uvw`, shown under each commit). A committed hunk is `<commit-id>:<file-id>:<hunk-id>` and is shown by `but diff <commit-id>`. `@` means the uncommitted area.\n- Commit IDs are stable change IDs that survive history edits (`amend`, `squash`, `move`, `uncommit`, `reword`). Commits without a change ID (e.g. upstream-only) lead with a sha prefix instead, and `#N`-suffixed refs disambiguate duplicates — both go stale after history edits, and a stale sha can silently resolve to the wrong commit. The `(sha …)` on verbose commit lines is informational — do not pass it to commands.\n- Branch short IDs are snapshot-local selectors. Use full branch names for branch-targeting mutations; short IDs are safe only for immediate read-only inspection.\n- File/hunk IDs copied from one diff read generally remain usable across chained commits. If one stops resolving, re-read `but diff` and retry.\n\n**Chaining:** mutation output is concise by default, so you may chain mutations with `&&` off one inspection read. Do not chain branch short IDs through mutations; use full branch names. Add `--status-after` only when the next step needs workspace IDs or details that the mutation result does not provide. Chained `but commit` calls stack in the order written — the first is oldest, each later one goes on top. History edits may run in sequence when every commit ref involved is a change-ID ref; run them one at a time with `--status-after` when a ref is sha-based or `#N`-suffixed, or when the next command needs freshly issued IDs. Chaining `but uncommit <id> && but diff` is safe because bare `diff` needs no ID from the uncommit output.\n\n## Non-Negotiable Rules\n\n1. Use `but` for all write operations. Never run `git add`, `git commit`, `git push`, `git checkout`, `git merge`, `git rebase`, `git stash`, or `git cherry-pick`. The only exception is selecting a side of a conflicted submodule in resolution mode (see Resolve conflicted commits). If the user says a `git` write command, translate it to `but` and run that. Running from a linked worktree shows the same workspace and IDs as the main worktree, and `@` still means the main worktree's changes; a bare `but commit` or `but diff` takes that worktree's changes, a bare path names a file there, and `<worktree>:@` names them from anywhere.\n2. Mutation commands print their result without appending workspace status. Add `--status-after` only when the next step needs resulting workspace IDs or details; otherwise trust the mutation result and do not run a verification status/diff.\n3. Branches marked `(merged upstream)` have landed; start new work on another branch. That marker alone does not mean `but pull` removes them: run `but pull --check` first. `but pull` removes only branches listed `[integrated]` (\"has been integrated upstream and removed locally\", or \"has been merged into <target> and removed\" when the target has no upstream) and rebases the rest, which stay applied. To clear a landed branch that stays applied, use `but unapply <branch>` (acts on its whole stack; `but apply <branch>` restores it) rather than `but discard`. `push` and mutations (`commit`, `amend`, `squash`, `uncommit`, `reword`, `move`) refuse landed branches and commits, `absorb` skips them with a notice, and `commit` skips them when picking a default target.\n4. In non-interactive CLI workflows, do not narrate progress between routine commands. Execute the needed `but` commands and give a concise final summary.\n5. Prefer this skill and `but skill reference` over exploratory help calls. Use `<command> --help` when required syntax is missing or a command fails; use top-level help only when you genuinely need to discover an undocumented command.\n\n## Command Patterns\n\n- Initialize a Git repository: `but init` (current directory) or `but -C <path> init`. Non-interactive; creates no commits, remotes, or GitButler workspace. Existing repositories are left unchanged.\n\n- Commit: `but commit -b <branch> -m \"<msg>\" <id> <id>` — `-b <branch>` creates the branch if it does not exist\n- Commit everything uncommitted: `but commit -b <branch> -m \"<msg>\"` (omit the IDs)\n- Several commits from one diff: chain `but commit` calls with `&&` (commits stack oldest-first)\n- Commit at a specific history position: `--above <commit-or-branch>` or `--below <commit-or-branch>` (mutually exclusive). With a branch target, add `-b <new-name>` to name the new branch; otherwise its name is generated. The name must not already exist. Do not combine `-b` (even without a name) with commit/worktree targets.\n- Targeting is **required** when more than one **stack** is applied; without it `but commit` fails with \"Unclear where to commit. Found more than one stack\". Several branches stacked together count as one stack — an untargeted commit then silently lands on the stack's top branch, so pass `-b` whenever the branch matters.\n- Always pass `-m \"<msg>\"` (or `-F -` with a heredoc, `-F <file>` with a file outside the repository, or `--no-message`) to `but commit`, and to `but squash` whenever its sources are commits or branches unless the target is `@` — those compose a new message; without a flag an agent run keeps whatever the command composed (empty for `but commit`, the joined source messages for `but squash`) and a terminal opens an editor. Squash sources that are uncommitted or committed changes reuse the target's message and need no flag; squashing into `@` rejects message flags outright.\n- Amend: `but amend -t <commit-or-branch> <file-or-hunk-id> <file-or-hunk-id>` — a branch target resolves to its newest commit\n- Uncommit: `but uncommit <commit-id>` (whole commit), `but uncommit <branch>` (all commits and remove branch), `but uncommit <commit-id>:<file-id>` (committed file), or `but uncommit <commit-id>:<file-id>:<hunk-id>` (committed hunk); committed files and hunks may be mixed, but all must come from one commit\n- Insert empty commit: `but commit --empty -b <branch> -m \"<msg>\"`\n- Pick onto a new named branch: `but pick <commit-sha> --above <branch> -b <new-name>` (`--below` also works). The name must not exist; `-b` cannot be combined with commit/worktree targets.\n- Squash commits: `but squash <source-commit-id> [<source-commit-id>...] -t <target-commit-id> -m \"<msg>\"`\n- Move committed changes into an existing commit or `@`: `but squash <commit-id>:<file-id> -t <commit-id-or-@>` for a committed file; `but squash <commit-id>:<file-id>:<hunk-id> -t <commit-id-or-@>` for a committed hunk; all sources must come from one commit\n- Move committed changes into a new commit at a chosen position: `but move <commit-id>:<file-id> --above <commit-or-branch> -m \"<msg>\"` for a committed file; `but move <commit-id>:<file-id>:<hunk-id> --above <commit-or-branch> -m \"<msg>\"` for a committed hunk (`--below`, `--branch`, and `--unstack` also work)\n- Split selected committed changes into a new commit immediately above their source: `but split <committed-file-or-hunk-id> [<committed-file-or-hunk-id>...] -m \"<msg>\"`\n- For `but split` and committed-change `but move`, use `-m/--message` to name the new commit directly; repeated `-m` values are joined with blank lines. For a multiline message, pass `-F -` with a heredoc, or `-F <file>` with a file outside the repository, instead of `-m`. Omitting both creates an empty-message commit without opening an editor. `but move -m` and `-F` reject whole-commit and branch sources.\n- Squash a whole branch into one commit: `but squash <branch> -m \"<msg>\"` (no `-t`)\n- Uncommit and remove a branch: `but uncommit <branch>`\n- Reorder commits: `but move <commit-id> --below <commit-id>` (`--above` for the other direction; **commit IDs**, not branch names)\n- Reorder a block: `but move <commit-id> <commit-id> --below <following-commit-id>` or `--above <preceding-commit-id>` (both anchors accept multiple space-separated sources)\n- Move commit to branch top: `but move <commit-id> -b <branch>`\n- Move commits or committed changes onto a new named branch: `but move <source> --above <branch> -b <new-name>` (`--below` also works); use `but move <source> --unstack -b <new-name>` for an independent branch. Omit `-b` for a generated name. Naming is not supported when stacking or unstacking a branch source.\n- Stack branches: `but move <branch-name> --above <target-branch-name>` (**use full branch names**)\n- Tear off a branch: `but move <branch> --unstack`\n- Discard: `but discard <id> [<id>...]` — accepts branches, commits, committed changes, uncommitted changes, or `@` for all uncommitted changes\n- Push: `but push <top-branch>` — pushes the selected branch and its ancestors; to update a stack, select its top branch once and never loop. Bare `but push` pushes all unpushed work when run non-interactively — one push per stack (its topmost unpushed branch, ancestors included), so output has one entry per stack, not per branch. It exits non-zero if any stack failed; stacks that already pushed stay pushed, and rerunning after fixing the failure is safe (up-to-date stacks are skipped)\n- Pull (update workspace from the target): `but pull` — the output reports the result; `but pull --check` previews without updating when a preview is actually needed\n- Create PR: `but pr new <branch-name> [-m \"Title...\"] [-F - | -F <file>] [-t] [--draft]` — auto-pushes first; do not run `but push` before it\n\n## Task Recipes\n\n### Update workspace from main\n\nFor \"get latest from main\", \"update/sync this workspace\", \"rebase onto main\", or \"pull main\":\n\n1. `but pull` — one command; no preflight needed. Its output reports the resulting state, it refuses safely when uncommitted changes conflict, and `but undo` reverts it.\n2. If commits come back conflicted, resolve them oldest-first following the printed instructions: `but resolve <commit>`, edit the files, then `but resolve finish`. Its result gives the current ID of the next conflict. Add `--status-after` to the finish you expect to clear the last conflict only when the task needs the complete resulting workspace. When it says no conflicted commits remain, stop; do not run a verification status. Finishing a lower commit rebases the ones above it, so always work bottom-up.\n\n`but pull --check` answers \"would this conflict?\" without updating. Do not use it as a routine\npreflight; use it when the user asks for a preview, repository policy requires one, other agents'\nbranches may move, or an applied branch was just merged upstream (see rule 3).\n\nRebasing applied branches onto the latest target IS `but pull` — never `move`, `config target`, `unapply`, or raw `git pull`/`git rebase`. The base shown in status is the last FETCHED state: when `git log` shows `main` (local or remote) ahead of it, that is exactly the update `but pull` fetches and applies — the target setting is not stale and repointing it is never the fix. Pull carries uncommitted changes along, and its output reports the resulting state. If it refuses because uncommitted changes conflict, park them: `but commit -b <branch> -m \"wip\" <ids>`, pull again, then `but uncommit` the parked commit (there is no stash; do not hand-revert files).\n\n### Commit selected files or hunks\n\n1. `but diff` — shows file and hunk IDs for uncommitted changes. Do not run plain `but status` first.\n2. Use file IDs when whole files belong in the commit; use hunk IDs (`<file-id>:<hunk-id>`) when only part of a file belongs. Omit IDs you don't want committed.\n3. `but commit -b <branch> -m \"<msg>\" <id1> <id2>` — the branch is created if it does not exist, so no prior `but branch new` is needed.\n4. When the task requires knowing which changes remain, add `--status-after`; otherwise the created-commit result is sufficient.\n\nEdge case: if wanted and unwanted edits are in the same diff hunk, GitButler cannot split that hunk by ID. Only when the task requires keeping part of that hunk uncommitted, temporarily edit the working tree to isolate the wanted lines, commit those IDs, then restore the leftover lines so they remain uncommitted.\n\n### Amend into existing commit\n\n1. `but status -fv` (or `but show <branch-id>`) — locate file/hunk IDs and target commit IDs.\n2. `but amend -t <commit-id> <id> <id>` — one command per target commit. For several target commits, chain the amends with `&&` when every target is a change-ID ref; otherwise run them one at a time with `--status-after` to get fresh refs.\n\n### Split an existing commit\n\nFor a two-way split, move selected committed files or hunks directly into a new commit:\n\n1. `but diff <source-commit-id>` — read committed file and hunk IDs.\n2. `but split <committed-file-or-hunk-id> [<committed-file-or-hunk-id>...] -m \"<message>\"` — sources may mix files and hunks, but must come from one commit. The command creates the named commit immediately above the source and leaves unselected changes and the original message in the source. No follow-up reword is needed for the new commit.\n3. Add `--status-after` only when the next step needs workspace IDs or details not provided by the mutation result.\n\nFor more than two replacement commits, repeat `but split ... -m \"<message>\"` using the remaining source's committed changes, or use the uncommit/recommit workflow below when rebuilding the entire commit:\n\n1. `but status -fv` when you need the source commit, branch name, or placement anchor.\n2. `but uncommit <source-commit-id> && but diff` in one shell call exposes the commit's changes and prints the resulting file and hunk IDs.\n3. Pick replacement contents from that dirty diff, not from the old committed diff.\n4. Determine the requested chronological order from the user's wording, then create the replacement commits oldest-first by chaining `but commit` calls. Do not reverse that order to match `but status`, which displays commits newest-first. `-b <branch>` puts each new commit at the TOP of that branch — if the split commit had commits above it, the replacements now sit above those preserved commits.\n5. **If commits from that branch must stay ABOVE the replacements, put the preserved block back on top instead of fighting anchors.** Do not anchor with `--above <top>`/`--below <top>` (sha/`#N` anchors go stale as each insert rewrites history). Move the block together so its internal order stays intact: append `&& but move <preserved-id> [<preserved-id>...] -b <branch>` to the commit chain. Change-ID refs from step 1 stay valid; wait for fresh output first if any preserved ref is sha-based or `#N`-suffixed.\n6. Leave unwanted changes uncommitted. Replacements created oldest-first appear newest-first in status — that is correct; do not reorder them. Add `--status-after` to the final mutation when you need to inspect the resulting order.\n\n### Reorder commits\n\n`but status` displays commits newest/top first, while task specs often list history oldest to newest — translate before moving.\n\n1. `but status` once to get commit IDs (use `-fv` only if you also need file details).\n2. `but move <source> --below <target-commit>` places source immediately below target in `but status` (older in oldest-to-newest history). `--above` places it immediately above (newer). `but move <source> -b <branch>` moves it to branch top/newest.\n3. For an adjacent block, run ONE move, anchored either way: `but move <block-id> <block-id> --below <following-commit-id>` or `but move <block-id> <block-id> --above <preceding-commit-id>`. Pick an anchor outside the block; source order does not matter and the block keeps its internal order. Add `--status-after` when you need to inspect the resulting order; do not move the anchor or block members again.\n4. For other reorders, make the smallest set of moves.\n\n### Squash commits\n\n1. `but status` for commit IDs and order.\n2. Name the sources positionally and the result/target commit with `-t`: `but squash <source> [<source>...] -t <target> -m \"<new message>\"`.\n3. To collapse an entire branch into one commit, pass just the branch and no `-t`: `but squash <branch> -m \"<new message>\"`.\n4. Multiple independent groups may run in sequence off one status read (targets keep their change-ID refs); prefer newer/top groups first. Take fresh refs only when a ref is sha-based or `#N`-suffixed.\n5. Add `--status-after` to the final squash when you need to inspect the resulting history; do not re-verify with a separate status.\n\n### Stack existing branches\n\nTo make one existing branch depend on another: `but move <child-branch-name> --above <parent-branch-name>` (use full branch names; commit reordering uses commit IDs). To unstack: `but move <branch-name> --unstack`.\n\n**DO NOT** stack via `uncommit` + `branch delete` + `branch new --above` (git branch names persist after delete and it loses work), and do not use `but undo` to unstack.\n\n### Create or manage pull requests\n\n`but pr new <branch-name>` pushes the selected branch and its ancestors, then creates the PR in one step — no prior `but push`. Provide `-F -` with a heredoc or `-F <file>` (first line is the title), `-t`, or `-m` with real newlines (zsh/bash: `-m $'Title\\n\\nBody'`) so no editor opens. Do not write the message to a file inside the repository: the next `but commit` would commit it. If forge auth is missing, run `but config forge auth`.\n\nIf you do create a PR for a stacked branch, use `but pr` — not `gh pr create` (only `but pr` sets PR bases and stack metadata; `gh pr create` breaks that). To publish a whole stack: `but pr new <top-branch-name> -t`. Manage with `but pr auto-merge|set-draft|set-ready <selector>`; selectors can be a branch name, current branch/stack CLI ID, or numeric review ID. See `but skill reference` for details.\n\n### Dependency conflict with another branch\n\nChanges that build on another branch's commits cannot land on an independent branch. `but commit` and `but amend` fail atomically (\"Cannot commit: N changes could not be applied\"), naming the branch and commit each rejected change depends on — nothing is committed and no `-b` branch is created.\n\nWhen there is a single dependency branch, the error's Hint gives the exact recovery command: `but move <your-branch> --above <dependency-branch>` to stack an existing branch on its dependency, or `but branch new <name> --above <dependency-branch>` when the target branch didn't exist yet. Run it, then retry the original command. When the error names dependencies without a Hint (several dependency branches, or the dependency is on the target branch itself), run `but status -fv` to see where the dependent commits live before choosing a placement.\n\nIf that recovery command fails, do NOT try `uncommit`, `squash`, or `undo` as a workaround — re-run `but status -fv` to confirm both branches exist and are applied, then retry with exact branch names.\n\n### Resolve conflicted commits (after pull, move, or reorder)\n\n**NEVER use `git add`, `git commit`, `git checkout --theirs/--ours`, or any git write command during resolution.** Only `but resolve` commands plus direct file edits. The one exception is a conflicted submodule in edit mode, below.\n\nConflicts do not interrupt operations in GitButler: a rebase always completes, and commits that conflicted are marked `{conflicted}` in `but status`. Find them from the warning that history-editing commands (`move`, `discard`, …) print, from the `but pull` summary, or from `but status`. Resolve them one conflict at a time, without entering any mode. Work through one branch at a time, oldest commit first — the loop is:\n\n1. `but resolve conflicts <branch>` — shows the conflicts of that branch's oldest conflicted commit, numbered per file. Each conflict has three sections: `ours` (the new base the commit was rebased onto), `base` (common ancestor), and `theirs` (the commit's own version). Without a branch it picks the first conflicted branch and tells you which; the output also says when other conflicted commits exist.\n2. Apply one resolution per command:\n   - Merged/mixed content (the common case — combine the intent of both sides): pipe it to `but resolve apply <path>:<N>` via stdin (heredoc) or pass `--file <f>`. The content replaces the whole conflicted region; never include conflict markers.\n   - Take a side entirely: `but resolve apply <path>:<N> --ours` or `--theirs` (bare `<path>` applies the side to all conflicts in that file).\n   - Delegate one conflict to the configured AI model: `but resolve apply <path>:<N> --ai` — prefer your own merged content when you have the context.\n   - `apply` targets the same default commit as `conflicts`; when more than one branch is conflicted, pin it with `--commit <branch>`.\n3. Go back to step 1 until it reports no conflicts. Commit ids are not stable here — every apply rewrites the commit — but branch names are, so address everything by branch and never bookkeep commit ids. Partial progress is fine: a commit with unresolved conflicts left stays `{conflicted}` with exactly those conflicts.\n\nA wrong resolution is reverted with `but undo`.\n\n`but resolve <commit-id> --ai` resolves a whole commit with the configured AI model in one shot, and `but resolve --ai` without a commit resolves every conflicted commit, oldest first — acceptable as a fallback, but you usually have better context than that model does, so prefer resolving the conflicts yourself with `apply`.\n\n**Use edit mode only when you must run code against the resolution** (build/tests at that commit):\n\n1. `but resolve <commit-id>` — enters resolution mode and prints the conflict regions.\n2. **Edit the files** to remove every conflict marker — `<<<<<<<`, `|||||||` (the common-ancestor section), `=======` and `>>>>>>>` — and keep the correct content. Do NOT skip this; do NOT use `but amend` on conflicted commits.\n   - A conflicted submodule has no markers, and `but resolve finish` refuses while it is unresolved, even to keep the commit's own side. Select it explicitly: check out the wanted commit inside the submodule (or write the file that replaces it) and run `git add -- <path>`, or drop it with `git rm -- <path>`. These are the only git write commands allowed during resolution, and only for the paths `finish` names; never `git commit`. If the submodule directory is empty or missing, do not `git add` it (for a missing one that stages its removal): keep any resolution edits you want and report the conflict. `but resolve cancel --force` abandons the resolution and discards its edits; use it only when those edits can be discarded.\n3. `but resolve finish` reports leftover markers, surviving uncommitted changes, every remaining conflicted commit, and the exact current `but resolve <id>` command. Add `--status-after` to the finish you expect to clear the last conflict only when the task needs the complete resulting workspace. When it says no conflicted commits remain, stop; do not run a verification status. Cancel instead with `but resolve cancel`.\n4. Repeat for remaining conflicted commits, oldest first — finishing a lower commit rebases the ones above it.\n\n### Conflicts in uncommitted files\n\n`but status` marks uncommitted files with unresolved merge conflicts `{conflicted}`; they are excluded from committable changes and outside `but resolve` mode. Edit the file to the wanted contents (or delete it), then `but resolve <path>...` to mark it resolved with that state; it then shows as an ordinary uncommitted change.\n\n## Git-to-But Map\n\n| git | but |\n|---|---|\n| `git status` | `but status` for branch/stack/commit overview; `but status -fv` for file/hunk details; `but diff` for selected dirty changes |\n| `git add` + `git commit` | `but commit -b <branch> -m ... <ids>` |\n| `git checkout -b` + commit | `but commit -b <new-branch> -m ... <ids>` |\n| `git push` | `but push <branch-name>` |\n| `git rebase -i` | `but move`, `but squash`, `but reword` |\n| `git rebase --onto` | `but move <branch> --above <new-base>` |\n| `git checkout -- <file>` / `git restore` | `but discard <id>` |\n| `git cherry-pick` | `but pick` |\n| `gh pr create` | `but pr new <branch-name> -m \"Title...\"` |\n\n## Notes\n\n- Read-only git inspection (`git log`, `git blame`, `git show --stat`) is allowed.\n- If `but` prints an `AGENT ACTION REQUIRED` skill warning, run the suggested command once, then reload/use the GitButler skill. If it repeats, report it instead of retrying.\n- For command syntax and flags: `but skill reference`\n- For workspace model: `but skill concepts`\n- For workflow examples: `but skill examples`\n","packages/but-mcp-app/README.md":"# GitButler workspace MCP App\n\nThese React apps render the structured results of the `gitbutler_workspace` and\n`gitbutler_review_card` MCP tools. Their compact workspace and review components\ntake their visual cues from GitButler Lite. Selecting a workspace commit or\nbranch opens a responsive details panel with copy, explain, and review actions.\nReview cards refresh open reviews and their CI status every 30 seconds until the\nreview closes, CI fails, or a refresh request fails.\n\nBuild the single-file resource embedded by the Rust MCP server with:\n\n```sh\npnpm -F @gitbutler/but-mcp-app build\n```\n\nThe generated `workspace.html` and `review.html` files under\n`../../crates/but/src/command/mcp` are ignored. Release workflows set\n`GITBUTLER_REQUIRE_MCP_APP=1` and must run the command above before compiling\n`but`. Normal Rust builds use a small fallback resource so they do not require\nNode.js or pnpm.\n","packages/but-sdk/README.md":"# @gitbutler/but-sdk\n\n`@gitbutler/but-sdk` is the local npm package that exposes GitButler Rust APIs to JavaScript/TypeScript through a native Node add-on.\n\n## How it works\n\nThe generated SDK surface is produced in two parts and merged into a single declaration file:\n\n1. **N-API bindings (functions)**\n   - Rust functions marked with `#[but_api(napi)]` in `but-api` generate `*_napi` exports via the `but_api` proc macro.\n   - `but-napi` links those generated exports and is built by `napi-rs` into a `.node` binary.\n   - `napi-rs` generates, per flavor folder:\n     - `gitbutler-sdk.{PLATFORM}.node` (Node native add-on)\n     - `index.js` (runtime loader + JS bridge)\n     - `index.d.ts` (N-API function declarations)\n\n2. **Schema-derived TypeScript types (structs/enums)**\n   - `but-ts` collects registered JSON schemas from `but-api` (`but_api::schema` module) and emits TS type aliases.\n   - These emitted type aliases are appended to the flavor folder's `index.d.ts`.\n\n\nThis package ships only generated artifacts (`browser.js` plus each flavor's `index.js`, `index.d.ts`, `*.node`) and re-exports them through package `exports`.\n\n## Workspace-projection flavors\n\nMutation APIs return a `WorkspaceState` whose projection depends on the\n`graph-workspace` cargo feature of `but-api` (see its feature docs). The SDK\nships one fully self-contained build per flavor:\n\n- `@gitbutler/but-sdk` (alias `@gitbutler/but-sdk/linear`, from\n  `src/generated/linear/`): built without the feature; `WorkspaceState`\n  carries the legacy `RefInfo`-based `headInfo` projection.\n- `@gitbutler/but-sdk/graph` (from `src/generated/graph/`): built with\n  `--features graph-workspace`; `WorkspaceState` carries the graph-based\n  `graphWorkspace` (`DetailedGraphWorkspace`) projection.\n\nEach folder has its own `.node` binary, loader, and declarations, so importing\nan entry point loads the matching native build — types and runtime cannot\ndisagree for napi consumers. (The desktop app consumes this package for types\nonly; its runtime is the Tauri backend, so it must still keep its import flavor\nin lockstep with `gitbutler-tauri`'s `graph-workspace` feature.) Both folders'\n`index.js`/`index.d.ts` are committed and CI-checked.\n\n## Generate bindings and types\n\nPer flavor, always run the napi build before the type generation:\n\n```bash\n# linear flavor (src/generated/linear/)\npnpm --filter @gitbutler/but-sdk build:napi\npnpm --filter @gitbutler/but-sdk build:types\n# graph flavor (src/generated/graph/)\npnpm --filter @gitbutler/but-sdk build:napi:graph\npnpm --filter @gitbutler/but-sdk build:types:graph\n```\n\nOr run the combined script, which builds both flavors:\n\n```bash\npnpm --filter @gitbutler/but-sdk build\n```\n\n### Why order matters\n\n`build:types` appends schema types to the existing declaration file. Running it repeatedly without regenerating N-API declarations first can duplicate schema type sections.\n\n## Add new bindings and types\n\n### Add a new exported N-API function\n\n1. In `crates/but-api`, annotate a Rust API with `#[but_api(napi)]`.\n2. Ensure input/output types are serializable in a way the macro supports.\n3. Re-run:\n\n```bash\npnpm --filter @gitbutler/but-sdk build:napi\n```\n\nYou should see a new `*Napi` export in `src/generated/linear/index.d.ts` (and `src/generated/graph/index.d.ts` for the graph flavor).\n\n### Add a new generated TypeScript type\n\n1. Make sure the Rust type derives `schemars::JsonSchema`.\n2. Register the type in `crates/but-api/src/schema.rs` using `TypeSchemaEntry`.\n3. Re-run:\n\n```bash\npnpm --filter @gitbutler/but-sdk build:types\n```\n\nYou should see a new `export type ...` in `src/generated/linear/index.d.ts` (and `src/generated/graph/index.d.ts` for the graph flavor).\n\n## Validate generated output\n\nBaseline validation:\n\n```bash\npnpm --filter @gitbutler/but-sdk check\n```\n\nOptional extra runtime/type sanity check:\n\n```bash\npnpm --filter @gitbutler/but-sdk testTypes\n```\n\nRecommended contributor flow after changing bindings/types:\n\n```bash\npnpm --filter @gitbutler/but-sdk build\npnpm --filter @gitbutler/but-sdk check\n# optional\npnpm --filter @gitbutler/but-sdk testTypes\n```\n\n## Usage in Electron (`apps/lite` pattern)\n\nUse native bindings in the Electron **main process**, then expose only typed IPC APIs to the renderer.\n\n### Main process model\n\n```ts\nimport { listProjectsStateless } from '@gitbutler/but-sdk';\n\nexport function listProjects() {\n\treturn listProjectsStateless();\n}\n```\n\n### IPC contract with SDK types\n\n```ts\nimport type { ProjectForFrontend } from '@gitbutler/but-sdk';\n\nexport interface LiteElectronApi {\n\tlistProjects(): Promise<ProjectForFrontend[]>;\n}\n```\n\n### Renderer consumes IPC API\n\n```ts\nconst projects = await window.lite.listProjects();\n```\n\nThis keeps native access out of the renderer while sharing the same generated TS types (`ProjectForFrontend`, `StackEntry`, `StackDetails`, etc.) across process boundaries.\n\n## Current exported binding entry points\n\nSee `src/generated/linear/index.d.ts` (and `src/generated/graph/index.d.ts` for the graph flavor) for the complete generated API and type surface.\n\n## See also\n\n- [but-napi](../../crates/but-napi/README.md)\n- [but-ts](../../crates/but-ts/README.md)\n\n## License\n\nFSL-1.1-MIT - See [LICENSE.md](../../LICENSE.md) for details.\n","packages/no-relative-imports/README.md":"# @gitbutler/no-relative-imports\n\nAn eslint rule for enforcing non-relative imports if there are `paths` in your `tsconfig.json` available to you.\n\n## What the rule affects\n\nThe rule requires that any import that _could_ be referenced absolutely via an entry in `paths` will provide an error.\n\nEven if a sibling import (IE `./foo`) is used, if it could be accessed via a path (IE `$lib/foo`). An error will be provided.\n\n## The `paths` format\n\nIt should be noted that the current implementation does not try to handle all the possible `paths` entries. Instead it handles the three common cases:\n\n```json\n{\n\t\"compilerOptions\": {\n\t\t\"paths\": {\n\t\t\t\"$lib\": \"./lib\", // Without glob. IE pointing directly to a file\n\t\t\t\"$lib/*\": \"./lib/*\", // Both with glob, IE anything starting in ./lib/... results in $lib/...\n\t\t\t\"$lib\": \"./lib/*\" // Path with, IE anything starting in ./lib/... results in $lib\n\t\t}\n\t}\n}\n```\n\n## Usage\n\n```\nimport noRelativeImportPaths from '@gitbutler/no-relative-imports';\n\nexport default [\n  {\n    plugins: {\n      'no-relative-import-paths': noRelativeImportPaths,\n    },\n    rules: {\n      'no-relative-import-paths/no-relative-import-paths': 'error',\n    },\n  },\n]\n```\n","packages/shared/README.md":"# create-svelte\n\nEverything you need to build a Svelte library, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).\n\nRead more about creating a library [in the docs](https://kit.svelte.dev/docs/packaging).\n\n## Creating a project\n\nIf you're seeing this, you've probably already done this step. Congrats!\n\n```bash\n# create a new project in the current directory\nnpm create svelte@latest\n\n# create a new project in my-app\nnpm create svelte@latest my-app\n```\n\n## Developing\n\nOnce you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:\n\n```bash\nnpm run dev\n\n# or start the server and open the app in a new browser tab\nnpm run dev -- --open\n```\n\nEverything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app.\n\n## Building\n\nTo build your library:\n\n```bash\nnpm run package\n```\n\nTo create a production version of your showcase app:\n\n```bash\nnpm run build\n```\n\nYou can preview the production build with `npm run preview`.\n\n> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.\n\n## Publishing\n\nGo into the `package.json` and give your package the desired name through the `\"name\"` option. Also consider adding a `\"license\"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).\n\nTo publish your library to [npm](https://www.npmjs.com):\n\n```bash\nnpm publish\n```\n","packages/svelte-comment-injector/README.md":"# svelte-comment-injector\n\nA Svelte preprocessor that injects an HTML comment at the top of every component, making it easier to identify components in browser DevTools.\n\n## ✨ Features\n\n- Injects a DevTools-visible HTML comment like:\n  ```html\n  <!-- Begin MyComponent.svelte -->\n  <div class=\"my-component\">\n  \t<!-- Component content -->\n  </div>\n  <!-- End MyComponent.svelte -->\n  ```\n\n## 📦 Installation\n\n```bash\nnpm install @gitbutler/svelte-comment-injector --save-dev\n```\n\n## ⚙️ Usage\n\nAdd the preprocessor to your Svelte configuration:\n\n```js\n// svelte.config.js\nimport svelteInjectComment from \"@gitbutler/svelte-comment-injector\";\n\nexport default {\n\tpreprocess: [svelteInjectComment()],\n\t// ...rest of your config\n};\n```\n\n## 🛠️ Configuration\n\nYou can customize the comment format by passing options to the preprocessor:\n\n```js\n// svelte.config.js\nimport svelteInjectComment from \"@gitbutler/svelte-comment-injector\";\n\nexport default {\n\tpreprocess: [\n\t\tsvelteInjectComment({\n\t\t\tenabled: true, // Enable or disable the comment injection\n\t\t\tshowEndComment: true, // Show the end comment\n\t\t\tshowFullPath: false, // Show the full path in the comment\n\t\t}),\n\t],\n\t// ...rest of your config\n};\n```\n","packages/ui-react/AGENTS.md":"# @gitbutler/ui-react\n\nThe React component library: the components, icons, illustrations and base\nstyles shared by Lite, `but panel` and the MCP app. It ships source, not a\nbuild; each consumer's Vite compiles it, and React Compiler runs on it under\nthe consumer's config as it does on the consumer's own code.\n\n`DESIGN.md` beside this file is the visual language: how things should look\nand read. It is an index of the pages in `design/`, one per topic; read the\nones for what you are changing before changing anything a user sees. This file\nis the tooling that enforces them.\n\n## Writing the code\n\nLite's rules apply here unchanged; `apps/lite/AGENTS.md` has them. In short:\nReact Compiler makes `useMemo`, `useCallback` and `React.memo` redundant\noutside hot paths; `useEffect` is usually an anti-pattern and needs consent;\ncomments explain the non-obvious why, never the what.\n\n### Changing a component\n\n**A change to a component here needs the designer's okay.** Every app draws\nwith these components, so a fix made for one surface changes all of them.\nWhen an app's bug seems to need a change here (a style, a prop, a default, a\nwrapper), try fixing it in the app first. If the library really is at fault,\nask before changing it. When you can't ask, make the change its own\n`fix(ui-react): …` commit that says which surface showed the bug and what\nelse it touches, and call it out in the PR description.\n\n### What belongs here\n\nA component belongs here when it doesn't reach into an app: no API calls,\nno settings, no store, nothing on `window.lite`. Everything a component needs\nfrom its host arrives as a prop, the way `TextLink` takes its `onClick` rather\nthan assuming Electron. A component that needs the app stays in the app;\nLite keeps its Markdown renderer and mention suggestions for that reason.\n\nImports are deep and carry the extension: `@gitbutler/ui-react/Button.tsx`.\nThere is no barrel file. Inside the package, siblings import each other\nrelatively.\n\n### Styles\n\nTokens come from `@gitbutler/design-core`, imported by the host once.\n`src/base.css` declares the few variables components read that aren't tokens\n(`--focus-ring`, `--control-cursor`, `--transition-button`) and the control\ncursor rules; every host imports it right after design-core. A variable nothing declares doesn't error, the\nproperty quietly falls back, so a new shared variable goes in `base.css`, not\nin a host's stylesheet.\n\n### Icons\n\nIcons are SVGs in `src/icons/`, inlined into the bundle as raw strings; the\nfilename is the icon name (`folder-lock.svg` → `<Icon name=\"folder-lock\" />`).\nTo add one:\n\n1. Export it from Figma at 16×16 (⚛️ Core library) as SVG into `src/icons/`.\n2. Run `pnpm -F @gitbutler/ui-react optimize-icons`. It minifies the SVG,\n   recolors it to `currentColor` and regenerates `src/iconNames.ts`.\n3. Commit both. `iconNames.ts` is generated; never hand-edit it.\n\n`src/file-icons/` are the language and filetype glyphs. They keep their brand\ncolours and are deliberately not run through the script.\n\n`src/program-icons/` are the marks of editors and terminals, as PNGs at 2×.\nTheir rounded corner is part of the image: export each from the \"Programm\"\nframe on ⚛️ Core's Icons page, where the mark is a 14px rectangle with\nthe corner on it, as PNG at 2× with the layer named as the file is\n(`vscode` → `vscode@2x.png`). No CSS rounds them, so a square export shows\nsquare.\n\n## Figma\n\nTwo files, split the way the code is:\n\n- **⚛️ Core** —\n  <https://www.figma.com/design/cqdnAotT8n9op8WGYLOHg4/%E2%9A%9B%EF%B8%8F-Lite-Core>,\n  the published library: tokens (Tokens page), icons (Icons page) and this\n  package's components, one section per component. It is the source of truth\n  for tokens; for components, code is, and the drawing follows it. The\n  components are on six pages, by what they are for: Actions, Inputs,\n  Overlays, Content & status, Preview (a details view's header and the bar\n  above it), and Sidebar. A new component's section goes at\n  the end of its group's page, with its examples section under it. Storybook\n  shelves the stories in the same groups (plus Icons, which Figma keeps on its\n  own page): a stories file names its group in `title` and keeps its old `id`,\n  so the story ids below don't change when a component moves group.\n- **Client**, the working file —\n  <https://www.figma.com/design/EBuHQGUcCaSw4Ln5uVpWkn/Client>: Lite's own\n  components on the ⚙️ Meta page, the full screens, and drafts on the\n  🚧 Drafts page and the pages under it. A new mockup or an exploration goes\n  here, never in the library.\n\nA component is drawn where its code lives: one in this package goes in\n⚛️ Core, one in `apps/lite` on ⚙️ Meta. When a component moves between the two\nin code, its drawing moves with it in the same piece of work, and the\nmockups' instances are swapped to the new one.\n\nThe drawing mirrors the code's structure: the same component under the same\nname, variant properties named as the props and their values (`status`,\n`variant`, `glyph`), and no property the code lacks. Figma may add only what\nthe code produces from data, such as a toggle for a side the code drops when\nits count is zero, and a `state` property (`default`, `hover`) for what CSS\ndraws under the pointer, as Button has; two components the code keeps apart\nstay apart in Figma and are placed side by side where a surface shows both.\n\nA component's description in Figma says what its JSDoc and DESIGN.md say\nabout it: what it is for, when to pick which variant, and a `Code:` line\nnaming the component and its file. Change one and change the other in the\nsame piece of work; a description that lags the code is what an agent reading\nFigma will build from.\n\n## Verifying your work\n\nAlways run the specified commands **exactly** as written. The package has no\napp to run, so the story is how a component is checked before it goes into a\nsurface.\n\n### Typechecking, the CSS rules, and tests\n\n```console\n$ pnpm -F @gitbutler/ui-react check\n$ pnpm -F @gitbutler/ui-react test\n```\n\n`check` typechecks and then runs Stylelint over the components' CSS with the\nrules the design pages give a machine: colours are tokens, font sizes are on\nthe 11–16 scale, radii come from the radius tokens. An error names what to use\ninstead. The exceptions are the ones the pages name, and each is marked where\nit is: the file icons' brand colours (a file-level override in\n`stylelint.config.mjs`) and a mask's opaque stop. A radius that is a share of\nanother, an inner corner following its outer one, is a `calc()` from the outer\ntoken and needs no exception (`design/foundations/radius.md`). A new exception\nis a design decision first; if it stands, disable the rule on that line with\nthe reason after `--`.\n\n### Tokens\n\nEvery `var(--x)` in this package has to resolve. Lite's token check reads this\npackage's source alongside its own:\n\n```console\n$ pnpm -F @gitbutler/lite check\n```\n\nIt fails on any undefined name and suggests the nearest real one. Run it\nafter any CSS change here, even when the package's own check passes.\n\n### Stories\n\nEvery component has a story beside it, in both themes, and a component that\nlives only in code is half a component. `pnpm check` holds the package to it\nthrough `scripts/check-stories.mjs`, which fails on a component no story\nimports. Storybook is Lite's, on port 6007, and lists this package's stories\nunder `components/`:\n\n```console\n$ pnpm -F @gitbutler/lite demos\n```\n\nThe package's own `test` runs its unit tests, not its stories. Lite's runs\nevery story, this package's included, as a test in headless Chromium; run\nthem alone with `pnpm -F @gitbutler/lite test:stories` (`apps/lite/AGENTS.md`\nhas the setup).\n\nA story renders alone at\n`http://localhost:6007/iframe.html?id=components-<name>--<export>&viewMode=story`;\nappend `&globals=theme:dark` for the dark theme. Check colour, font and\nspacing from computed styles rather than by eye. A story links its Figma\ncomponent through the `design` parameter; keep the link when you add a story,\nand compare the two sides when either changes.\n\n### Default, then examples\n\nA component's stories are one entry in Storybook, in one order:\n\n- **`Default` first:** the simplest use of the component, its props on the\n  controls where they have any to show. It answers what the component is and\n  accepts. `pnpm check` fails on a stories file that opens with anything\n  else.\n- **Examples after it:** each named after what it shows — a state\n  (`OnlyAdditions`, `NothingToList`) or a surface the app builds from it\n  (`Confirm`, `SelectProject`, `CommandPalette`). An example is written as\n  the app writes it: its own state, real copy, rendered open, since its\n  source is what an agent copies from `docs-show`.\n\nA surface built from several components is an example of the outermost one:\nthe upload confirmation is Modal's, the project picker is Popup's (a\ncombobox dressed in its parts), the command palette is PickerDialog's.\n\nIn ⚛️ Core the examples sit in a section named `<Component> examples`\ndirectly under the component's own section, on its group's page, one\ninstance per story, named as the story is; an example story links its\ninstance through the `design` parameter.\n\n### The manifest\n\nStorybook writes `/manifests/components.json` (and `components.html`, to\nread) from the stories: every component, its props from the TypeScript\ntypes, its stories with their source, and the import to write. An agent\nreads it instead of guessing an API. Two things about it to know:\n\n- The import to write is the `@import` tag in each component's JSDoc, as\n  `@import import { Badge } from \"@gitbutler/ui-react/Badge.tsx\";`. Give a\n  new component one: without it the manifest falls back to the package\n  root, and there is no such barrel. Storybook reads the tag through\n  `experimentalReactComponentMeta` in Lite's `.storybook/main.ts`.\n- The link to the Figma component is the story's `design` parameter, not\n  the manifest. `grep -l 'type: \"figma\"' src/*.stories.tsx` lists the stories\n  that have one; a story without one has no drawn spec yet.\n\nA story with no component behind it, as Lite's `AppUpdater.stories.tsx`,\ncarries `tags: [\"!manifest\"]` and says why. That is also why a styling\nhelper is not how a control is offered: `getButtonClassName` is for making\nanother component look like a button, and a button is `<Button>`, which the\nmanifest documents.\n\n### The Storybook MCP server\n\nWhile Storybook runs, it serves an MCP server at `http://localhost:6007/mcp`\n(`@storybook/addon-mcp`). Add it to your agent once:\n\n```console\n$ claude mcp add --transport http gitbutler-storybook http://localhost:6007/mcp\n```\n\nEach design notes page has an `.mdx` beside its `.md` in `design/`, under\nStorybook's Design section, so the published Storybook shows them and the MCP\nserver's `docs-list` lists them; `design/overview.mdx` renders DESIGN.md. The\nMDX renders the Markdown file itself, so edit the `.md`, never the page. What\nthe MDX writes is the title, a one-line summary (the description `docs-list`\nshows) and the file's raw URL, since `docs-show` returns the MDX source, not\nthe rendered text. A new page takes a Markdown file, an MDX beside it titled\n`Design/<Group>/<Name>` after its folder and file name (that is how links\nbetween the files find their Storybook page), and a row in DESIGN.md.\n\nBefore using a component, ask it rather than guessing: `docs-list` lists the\ncomponents, `docs-show` gives one's props, its stories and the import to\nwrite, and `docs-show-story` a whole story. Use only props it documents; if\none seems missing, ask instead of inventing it.\n`stories-find-by-component` finds the stories a file renders in, and\n`stories-preview` links to them. The published Storybook serves the same docs\ntools without a local one. The repository's `.mcp.json` connects Claude Code\nto it (it asks once); another agent, or one outside this repo, adds it:\n\n```console\n$ claude mcp add --transport http gitbutler-storybook-published https://master--6ab536f5f40e41db628ccf1b.chromatic.com/mcp\n```\n\nThe published server can time out, most often while a new master Storybook\ndeploys. Fall back in order: the manifest at\n<https://master--6ab536f5f40e41db628ccf1b.chromatic.com/manifests/components.json>,\na static file that keeps answering, then the component's own source and JSDoc\nin `src/`, which the manifest is built from. Never guess a prop because the\ntools were down.\n\n### Accessibility\n\n`@storybook/addon-a11y` runs axe on every story: the Accessibility tab under\na story lists contrast failures, controls without a name and misused roles.\nCheck it for a new or changed story. It catches the mechanical part of\nDESIGN.md's rules (an icon-only button's `aria-label`, a field's label,\ncontrast), not whether a label makes sense.\n\n### Chromatic\n\n`.github/workflows/chromatic.yml` publishes the Storybook to Chromatic's\npublic \"GitButler UI\" project: from master as the published Storybook, at\n<https://master--6ab536f5f40e41db628ccf1b.chromatic.com>, and for a pull request that touches the\nstories, as a preview with a UI Review check that compares every story's\nscreenshot with master's. A visual change waits there to be accepted rather\nthan failing the build; accept it when it is the change you meant.\n\n### Linting & formatting\n\nThe package is linted by oxlint with Lite's config, not by ESLint:\n\n```console\n$ pnpm exec prettier --check packages/ui-react\n$ pnpm run oxlint\n$ pnpm knip:non-prod\n```\n\nRun these before declaring the work done, and run `check-tokens` (above) with\nthem. The lint job in CI runs all of them and fails on what any one reports.\n","packages/ui-react/CLAUDE.md":"AGENTS.md","packages/ui-react/skill/SKILL.md":"---\nname: gitbutler-ui\ndescription: Use when changing what users see in an app built on `@gitbutler/ui-react` — Lite, the but.dev web app — adding or restyling a component, modal, form, list, empty state or toast, or writing the words on screen. Not for logic, data or backend work, even inside a UI file.\n---\n\n# Building GitButler UI\n\nWork as you would in any codebase: find the closest thing that already\nexists and copy it. These notes say where to look and what to check. They are\nnot a gate to clear before starting.\n\n## Scope\n\nChange what the task needs. Older screens may not follow these notes; leave\nthem alone unless the task is to fix them.\n\n## Start from the library\n\n`@gitbutler/ui-react` has the controls: `Button`, `Popup` (modals, dropdowns),\n`Select`, `Field`, `Checkbox`, `Switch`, `Tooltip`, `EmptyState`, `Snackbar`\n(raised through the `Snackbars` stack), `Toasts`, `Banner` and more. Use them\nrather than a control styled in a feature's CSS. Find a surface that already\nuses the ones you need and follow its structure.\n\nThe source is the reference, because it is the version you build with:\n\n- In the gitbutler repository: `packages/ui-react/src/`\n- In but.dev: `web/node_modules/@gitbutler/ui-react/src/`\n\nEach component's JSDoc says what its props do. The Storybook MCP server\n(`docs-show`) and\n<https://master--6ab536f5f40e41db628ccf1b.chromatic.com/manifests/components.json>\nhave the same docs with examples; use them when they answer, and go back to the\nsource when they don't. Don't wait on them.\n\nEach component's stories open with `Default`, its simplest use and its props;\nthe ones after it are examples, the component put to work the way an app\nwrites it. For a whole surface, find the closest example under its outermost\ncomponent (a confirmation is Modal's, a picker Popup's or PickerDialog's),\ncopy it and change what differs.\n\nWhen nothing fits, build the smallest thing that works and say so in the PR\ndescription: what was needed, which components you tried.\n\n## Read the design pages you need\n\nThe shared design language is the library's `DESIGN.md`, beside `src/` in the\nsame two places: an index of one page per topic, in `design/`, each saying\nwhen to read it. Read the pages for what you are building, not all of them.\nIn Storybook they are the Design section, and `docs-list` lists them.\n\nEach app keeps its own choices in its own `DESIGN.md`: Lite's is\n`apps/lite/DESIGN.md`, but.dev's is at its repository root.\n\n## Words\n\nShort and plain. A label is one to three words; a hint is one sentence. Call\nthings what the rest of the app calls them. Say what the user can't already see\non screen, once, without parentheses.\n\n## Check it\n\nLook at the result in light and dark, and keep one screenshot. In the\ngitbutler repository a component's story runs with `pnpm -F @gitbutler/lite\ndemos`; `apps/lite/AGENTS.md` has how to reach a single story.\n","packages/ui-svelte/scripts/README.md":"# UI Package Scripts\n\n## `optimize-ui-icons.js`\n\nOptimizes all SVG files in `src/lib/icons/svg/` using svgo, then syncs `src/lib/icons/names.ts`.\n\n### Usage\n\n```bash\n# From the UI package root\npnpm optimize-ui-icons\n# or\nnode scripts/optimize-ui-icons.js\n```\n\n### What it does\n\n1. Runs svgo on every `.svg` in `src/lib/icons/svg/` with `preset-default` (keeping `viewBox` and groups intact)\n2. Sets `width=\"100%\"` and `height=\"100%\"` on every `<svg>` so icons scale via CSS\n3. Replaces hardcoded `fill`/`stroke` colors with `currentColor` (leaves `none` untouched)\n4. Adds `vector-effect=\"non-scaling-stroke\"` to all vector shape elements\n5. Updates `src/lib/icons/names.ts` to reflect the current set of icon files\n\n---\n\n## `optimize-file-icons.js`\n\nOptimises SVG file-type icons and writes them into `src/lib/components/file/icon/svg/`.\n\nSupports two modes:\n\n- **Optimize in place** (no argument): optimises all existing SVGs in the output directory\n- **Import from a source directory** (with argument): reads SVGs from `<svg-dir>`, optimises them, and writes each into the output directory (adding new files, updating changed ones)\n\n### Optimisation steps\n\n- Runs svgo `preset-default` (keeping `viewBox` and groups intact)\n- Removes `width`/`height` attributes so icons scale via CSS\n- Replaces hardcoded hex colours with CSS variables from a canonical colour map (`--file-icon-gray`, `--file-icon-green`, `--file-icon-teal`, `--file-icon-blue`, `--file-icon-dark-blue`, `--file-icon-yellow`, `--file-icon-orange`, `--file-icon-red`, `--file-icon-pink`, `--file-icon-purple`, `--file-icon-violet`)\n\n### Usage\n\n```bash\n# Optimize in place\npnpm optimize-file-icons\n# or\nnode scripts/optimize-file-icons.js\n\n# Import + optimize from a source directory\nnode scripts/optimize-file-icons.js <svg-dir>\n```\n","packages/ui-svelte/src/lib/components/chipToast/README.md":"# Toast Component\n\nA simple toast notification system for the GitButler UI package.\n\n## Features\n\n- **Simple**: Just type and text - no complex configuration\n- **Bottom-center positioning**: Appears at the bottom center of the screen\n- **Stacking**: Multiple toasts stack vertically\n- **Auto-dismiss**: All toasts automatically disappear after 4 seconds\n- **4 types**: Info, Success, Warning, and Error with appropriate styling\n\n## Usage\n\n### Basic Usage\n\n```svelte\n<script>\n\timport { toasts, ToastContainer } from \"@gitbutler/ui-svelte\";\n\n\tfunction showToast() {\n\t\ttoasts.success(\"Operation completed!\");\n\t}\n</script>\n\n<button on:click={showToast}>Show Toast</button>\n\n<!-- Add the container to your app root -->\n<ToastContainer />\n```\n\n### Toast Types\n\n```javascript\nimport { toasts } from \"@gitbutler/ui-svelte\";\n\n// Different toast types\ntoasts.info(\"This is a info message\");\ntoasts.success(\"Operation completed successfully!\");\ntoasts.warning(\"Please review your changes\");\ntoasts.error(\"Something went wrong\");\n```\n\n### Promise Integration\n\n```javascript\nimport { toasts } from \"@gitbutler/ui-svelte\";\n\nasync function handleAsyncOperation() {\n\tconst myPromise = fetch(\"/api/data\");\n\n\tawait toasts.promise(myPromise, {\n\t\tloading: \"Loading data...\",\n\t\tsuccess: \"Data loaded successfully!\",\n\t\terror: \"Failed to load data\",\n\t});\n}\n```\n\n### Clear All Toasts\n\n```javascript\ntoasts.clearAll(); // Removes all current toasts\n```\n\n## Components\n\n### `<Toast>`\n\nIndividual toast component.\n\n**Props:**\n\n- `type: 'info' | 'success' | 'warning' | 'error'` - Toast type\n- `message: string` - Toast message text\n\n### `<ToastContainer>`\n\nContainer that manages positioning and stacking. Add this once to your app root.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# GitButler Agent Instructions\n\nGitButler is a Rust/Svelte/React/TypeScript monorepo.\n\nApply all relevant instruction files. If instructions conflict, resolve them in\nthis order:\n\n1. Explicit human instructions\n2. Nearest nested `AGENTS.md`\n3. This file\n\n## Repo Map\n\n- `crates/` - Rust crates.\n- `apps/desktop/` - Tauri/Svelte desktop app.\n- `apps/web/` - Svelte web app.\n- `apps/lite/` - Electron/React desktop app.\n- `packages/` - shared TypeScript packages, including the SDK.\n- `e2e/` - Playwright, WebdriverIO, and blackbox end-to-end tests.\n\n## Working Style\n\n- Treat questions about the codebase as read-only unless the user asks for changes.\n- Make focused, reviewable changes; avoid unrelated rewrites.\n- Use the simplest design that solves the actual problem; do not add\n  speculative machinery, and remove machinery your change makes unnecessary.\n- Inspect nearby code before introducing patterns.\n- Prefer existing APIs, tests, and conventions.\n- Before declaring shared behavior done, check each applicable surface and contract\n  (desktop, web, Lite, CLI/TUI, N-API, SDK, and docs) and update it or explicitly\n  determine that it is unaffected.\n- Run targeted validation for the area touched.\n- Before adding new machinery to fix a behavior bug, reproduce the bug in a failing\n  test and survey the target file's existing loops and classifications as candidate\n  hosts; let the tests, not the diagnosis, set how much implementation the fix needs.\n- When a fix calls for a new mechanism (a new module, a new public API, or a parallel\n  walk where one already exists), propose the intended shape before building it.\n\n## Scoped Instructions\n\n- For Rust work under `crates/`, follow `crates/AGENTS.md`.\n- For Lite work under `apps/lite/`, follow `apps/lite/AGENTS.md`.\n- For the React component library under `packages/ui-react/`, follow\n  `packages/ui-react/AGENTS.md`.\n","isInternal":false,"tokens":449,"sizeBytes":1881},{"name":"AGENTS.md","path":"crates/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/crates/AGENTS.md","title":"crates - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Rust Instructions\n\nThese are defaults for Rust source and test changes under `crates/`; more\nspecific instruction files override them. Do not apply them to generated,\nvendored, or fixture data unless the task is specifically about that code.\n\n## API Boundaries and Legacy\n\n- Treat `gitbutler-*` crates as legacy-heavy, but preserve local ownership and\n  nearby patterns for localized fixes. Do not introduce new `gitbutler-*` usage\n  in newer code unless the surrounding legacy boundary requires it. Keep legacy\n  fixes localized; migrate only when necessary, tiny, tested, and\n  behavior-neutral.\n- Minimize new `VirtualBranchesHandle` usage. At API boundaries, prefer\n  `but_ctx::Context` workspace helpers and newer `but-*` APIs.\n- `but-api` is the API surface for Tauri, Electron/N-API, CLI, and TUI callers.\n  Outer callers should prefer existing `but-api` functions when they fit, but\n  lower-level crates should not depend on `but-api`.\n- Preserve existing `but-api` macro, transport, serialization, and conversion\n  patterns instead of hand-rolling parallel wrappers.\n- Keep transport DTOs at the API boundary, commonly in a local `json` module,\n  and convert them to domain types before calling lower-level crates.\n- For permissioned `but-api` functions, follow the existing composition shape:\n  acquire permission near the wrapper and delegate to a `_with_perm` or other\n  permission-taking implementation.\n- API consumers such as the CLI, TUI, and other composed callers should use\n  `_with_perm` variants when they already hold permission to avoid extra locking\n  and deadlock risk.\n- Keep `Context` at API/composition boundaries; pass granular dependencies\n  like repo, workspace, metadata, database handles, or an `Editor` into\n  lower-level crates where possible.\n- Acquire repository access locks at top-level API/command boundaries. Do not\n  call helpers that acquire permissions while holding a guard; drop the guard or\n  call a permission-taking helper.\n- Preserve existing `DryRun` semantics; add dry-run support when the API/product\n  surface calls for preview behavior. Dry runs should avoid persisting refs,\n  objects, or oplog entries.\n- When an API offers both action-only and timeline-recording behavior, keep the\n  `*_only*` function free of oplog side effects; prepare a best-effort oplog\n  snapshot in the wrapper and commit it only after the mutation succeeds.\n- New user-facing mutations should participate in undo: record an oplog\n  snapshot via the existing `SnapshotDetails`/`OperationKind` wrapper pattern,\n  and call out any mutation intentionally left out of the timeline rather than\n  omitting it silently. Undo is snapshot-based; do not design bespoke inverse\n  operations for it.\n- Before changing or reviewing code that derives graph/workspace/branch/stack/commit\n  relationships, reachability, dependencies, ordering, operation targets, or Git\n  graph/history/ref-placement mutations, use `crates/WORKSPACE_MODEL.md` as the\n  reference. In short: prefer commit IDs and refs at API boundaries, convert to\n  operation-local selectors inside editor-backed operations, use\n  `but_graph::Graph` for relationship/reachability questions, use\n  `but_rebase::graph_rebase::Editor` for Git graph/history/ref rewrites where an\n  editor model exists, and treat `but_graph::Workspace` and\n  `but_workspace::RefInfo` as lossy presentation/compatibility views unless an\n  existing workspace-shaped boundary requires them.\n\n## Code Shape and Naming\n\n- Use Git/GitButler domain names and keep types/helpers in the crate or module\n  that owns the concept.\n- Avoid vague names like `Manager`, `Service`, `Helper`, `Util`, or `Processor`\n  unless nearby code uses that concept.\n- Solve the present problem directly. Avoid speculative abstractions: one-use\n  traits/types/functions, fake extension points, ceremonial wrappers/modules,\n  and public APIs larger than real callers need.\n- Keep module boundaries discoverable from names and the call graph.\n- Avoid drive-by refactors while fixing a specific bug.\n- Prefer explicit domain enum matches when wildcard arms would hide behavior.\n- Comment public API purpose/invariants/errors and complex algorithm intent;\n  avoid comments that merely repeat the code.\n\n## Git Repository Semantics\n\n- Do not rediscover Git repositories inside business logic; pass repo/context\n  explicitly.\n- Prefer repository APIs over shelling out to `git`, except at\n  shell/executable boundaries such as hooks, debug tooling, tests, or Git\n  interop helpers.\n- Preserve Git graph semantics even when the shape looks redundant or odd; do\n  not deduplicate, reorder, or smooth graph data unless Git semantics allow it.\n- Use `gix` for new repository logic. Treat `git2` and `Context::git2_repo` as\n  legacy/boundary-only escape hatches for libgit2 checkout/index, hooks,\n  transport/auth, or existing code that cannot reasonably move yet.\n- Keep Git paths, refnames, commit messages, and diff payloads byte-preserving\n  until UI/API boundaries; avoid lossy `String` conversion in business logic.\n- Treat filesystem and worktree writes carefully; use existing locking,\n  transaction, and access patterns.\n- Avoid implicit `SystemTime::now()` in testable business logic; pass time in\n  when deterministic behavior matters.\n- Use existing reload/invalidation helpers after external repo or worktree\n  changes, especially when working through `Context` caches.\n- Use `anyhow::Context` to explain what operation failed. When frontend/API\n  consumers need classification, use existing `but_error::Code` patterns; do\n  not make consumers match error strings.\n\n## Database Migrations (`but-db`)\n\n- Keep migrations forward-compatible: stay on the current `SchemaVersion` and\n  leave columns or tables that new code stopped using in place. Bumping the\n  version locks every older binary out of the database; reserve it for planned,\n  coordinated breaks, never routine cleanup.\n\n## Version Control\n\n- Assume the worktree may contain other agents' changes. Do not overwrite, clean\n  up, stage, commit, or amend changes you did not make.\n- When asked to branch, commit, push, or open a PR, use the GitButler `but`\n  CLI/workflow when available.\n- When the user says \"ship it\", commit your changes on a session branch,\n  creating one if needed, then push and open or update the PR. Reuse the\n  existing branch/PR when it already fits the session.\n- For small cleanup or follow-up fixes on your own branch, amend the relevant\n  existing commit(s) when that is the cleaner history.\n- Keep commit messages and PR descriptions succinct: why, impact, and core\n  decisions. Do not list local validation commands in commit messages or add AI\n  co-author trailers or tool branding.\n\n## Testing and Validation\n\n- Run the narrowest relevant test or check first, for example `cargo test\n  -p <crate> <test-name>` or `cargo check -p <crate> --all-targets`.\n- Use existing test-support crates and fixtures; prefer read-only fixtures for\n  read-only behavior.\n- For graph, rebase, or workspace behavior, prefer fixture-backed before/after\n  snapshots using existing visualizers, plus targeted structural assertions for\n  the invariant.\n- When snapshot output is volatile, stabilize inputs or normalize output; do not\n  replace strong snapshots with vague assertions.\n- When optimizing Git traversal or workspace behavior, preserve semantics with a\n  fixture-backed regression test.\n- Run `cargo fmt` for Rust formatting, avoiding unrelated dirty files.\n- Run `cargo clippy --fix --allow-dirty` only after inspecting the diff scope and\n  only when generated edits stay within the intended change.\n- Run `cargo machete` when dependencies changed; verify targets, features, and\n  platform-specific usage before removing dependencies.\n- After changing Rust APIs or types exposed through `@gitbutler/but-sdk`, run\n  `pnpm build:sdk && pnpm format`; this updates\n  `packages/but-sdk/src/generated`.\n- Use workspace-wide checks only when the change affects shared contracts or\n  multiple crates.\n\n## Assertions\n\n- Explain why a standard Rust assertion holds concisely using the last-argument message, like `assert!(1!=2, \"arithmetic unit on CPU works\")`\n- Snapshot assertions use `snapbox`, like `snapbox::assert_data_eq!(actual, snapbox::str![[r#\"...\"#]])`. `snapbox::assert_data_eq!` has no message argument, so explain why a snapshot holds with a `// comment` on the line above it.\n- Regenerate inline snapshots by running the tests with `SNAPSHOTS=overwrite`.\n- Remove unstable output before asserting (sanitize ids/timestamps/paths, e.g. the `but_testsupport` helpers) rather than snapshotting it raw. `snapbox` also pattern-matches expected data by default (`[..]` and `...` wildcards, path-separator normalization); append `.raw()` to the expected `snapbox::str!` when the value must match exactly (e.g. it contains backslashes or literal `[..]`/`...`).\n","isInternal":false,"tokens":1967,"sizeBytes":8876},{"name":"claude.md","path":"claude.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/claude.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"AGENTS.md","isInternal":false,"tokens":3,"sizeBytes":9},{"name":"CLAUDE.md","path":"crates/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/crates/CLAUDE.md","title":"crates - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"AGENTS.md","isInternal":false,"tokens":3,"sizeBytes":9},{"name":"copilot-instructions.md","path":".github/copilot-instructions.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/.github/copilot-instructions.md","title":"GitHub Copilot Instructions","category":"copilot-instructions","format":"markdown","content":"## General information\n\nThis is a Rust/Svelte/React/TypeScript monorepo with multiple projects. For\nagent instruction precedence and scoped guidance, follow the nearest applicable\n`AGENTS.md`; use this file as Copilot-oriented project context.\n\nThe main applications are found in the `apps` directory:\n\n- `desktop` containing the Tauri application's frontend code\n- `web` containing the web application's frontend code\n- `lite` containing the Electron/React desktop app\n\nRust code is found in the `crates` directory. It contains `gitbutler-tauri` for\nthe Tauri application, `but-api` for the API surface used by Tauri, Electron,\nCLI, and TUI callers, and the `but` CLI crate.\n\nThe `packages` directory contains different self-contained npm packages.\nThese are shared between the applications.\nThe packages are:\n\n- `but-sdk` containing the generated TypeScript/Node SDK for Rust APIs\n- `core` containing shared TypeScript helpers\n- `ui` containing the shared UI components\n- `shared` containing the shared types and utils\n- `no-cross-domain-imports` containing the no-cross-domain-imports ESLint package\n- `no-relative-imports` containing the no-relative-imports ESLint package\n- `svelte-comment-injector` containing the Svelte comment injector package\n\nThe `e2e` directory contains Playwright, WebdriverIO, and blackbox end-to-end\ntests.\n\n## Technology Stack\n\n- **Frontend**: Svelte with TypeScript\n- **Backend**: Rust\n- **Desktop Framework**: Tauri (like Electron but using Rust instead of Node.js)\n- **Build Tools**: Turborepo for monorepo management, pnpm for package management\n- **Git Libraries**: gix (gitoxide) and git2 (libgit2)\n- **Testing**: Vitest for JS/TS, Playwright for E2E tests, standard Rust testing framework\n\n## Development Setup\n\n### Prerequisites\n\n1. **Rust**: as specified in `rust-toolchain.toml`. Install via rustup.\n2. **pnpm**: Install standalone pnpm as described in [DEVELOPMENT.md](../DEVELOPMENT.md#prerequisites).\n3. **System Dependencies**: Tauri requires platform-specific dependencies (see DEVELOPMENT.md for details).\n\n### Initial Setup\n\n```bash\n# Clone the repository\ngit clone https://github.com/gitbutlerapp/gitbutler.git\ncd gitbutler\n\n# Install dependencies and the project-managed Node.js runtime\npnpm install\n\n# Build Rust binaries (required before running the app)\n# look at https://github.com/gitbutlerapp/gitbutler/blob/fd9a58de5579074c0526193143d531c21907a26b/scripts/install-tauri-debian-dependencies.sh\n# for Linux prerequisites for tauri.\ncargo build\n\n# or use this to build the `but` CLI, without the need for tauri dependencies.\ncargo build -p but\n```\n\n## Building and Running\n\n### Development\n\n```bash\n# Run desktop app in development mode\npnpm dev:desktop\n\n# Run with debug logs\nLOG_LEVEL=debug pnpm dev:desktop\n\n# Run web app\npnpm dev:web\n\n# Run UI component storybook\npnpm dev:ui\n```\n\n### Building\n\n```bash\n# Build all packages\npnpm build\n\n# Build desktop app only\npnpm build:desktop\n\n# Build for production (used for releases)\npnpm tauri build --features devtools --config crates/gitbutler-tauri/tauri.conf.nightly.json\n```\n\n## Testing\n\nWhen asked about frontend tests please read `@frontend.md` for context.\n\nTo run rust tests run `cargo test`, and for specific crates `cargo test -p CRATE_NAME`\n\n## Code Style and Linting\n\n### JavaScript/TypeScript\n\n**Linting**: ESLint with TypeScript and Svelte plugins\n**Formatting**: Prettier with Svelte plugin\n\n```bash\n# Check linting\npnpm lint\n\n# Fix linting issues\npnpm fix\n\n# Format code\npnpm format\n\n# Check formatting\npnpm prettier\n\n# Shortcut to check everything\npnpm isgood\n\n# Shortcut to fix and format everything\npnpm begood\n```\n\n**Important ESLint Rules**:\n\n- No relative imports (use `@gitbutler/` package references)\n- Import order: alphabetically sorted with specific group ordering\n- No console.log (use console.warn or console.error)\n- Prefer function declarations over arrow functions at top level\n- Svelte-specific rules for button types, unused props, etc.\n\n**Prettier Config**:\n\n- Tabs for indentation\n- Double quotes\n- No trailing commas\n- 100 character line width\n\n### Rust\n\n**Formatting**: Use rustfmt with nightly toolchain for imports grouping\n\n```bash\n# Format Rust code\ncargo fmt\n\n# Format with nightly settings\npnpm rustfmt\n# or (but don't do this unless you are asked to)\ncargo +nightly fmt -- --config-path rustfmt-nightly.toml\n\n# Run clippy for linting\ncargo clippy --all-targets\n```\n\n**Rust Formatting**:\n\n- Group imports: Std, External, Crate\n- Imports granularity: Crate level\n\n**temporary directories** - use `but-testsupport` for scenario creation. Read-only functions should use read-only fixtures. DO NOT USE the `std::env::temp_dir().join(format!(…)` pattern.\n\n## Dependency Management\n\n### Adding JavaScript/TypeScript Dependencies\n\n```bash\n# Add to root workspace\npnpm add -D <package>\n\n# Add to specific package\npnpm add <package> --filter @gitbutler/desktop\npnpm add <package> --filter @gitbutler/ui-svelte\n```\n\n**Always check for vulnerabilities before adding npm packages** (ecosystem: npm).\n\n### Adding Rust Dependencies\n\nEdit the appropriate `Cargo.toml` file:\n\n- Workspace-level dependencies go in `/Cargo.toml` under `[workspace.dependencies]`\n- Individual crate dependencies reference workspace versions when possible, and when it's used more than once.\n\n**Always check for vulnerabilities before adding Rust dependencies** (ecosystem: rust).\n\n## Monorepo Structure\n\n### Workspace Navigation\n\nThe repository uses:\n\n- **Turborepo** for JavaScript/TypeScript build orchestration\n- **Cargo workspace** for Rust crate management\n- **pnpm workspace** for npm package management\n\nWhen making changes:\n\n1. Identify which package/crate is affected\n2. Run builds/tests in that package first\n3. Then verify dependent packages still work\n\n### Crate Organization\n\nThe `crates/` directory contains ~65 Rust crates organized by functionality:\n\n- `gitbutler-*` prefix: Core GitButler functionality. These are old, and we want to port them to `but-*`.\n- `but-*` prefix: Backend utilities and services\n- Many crates have specific purposes (see DEVELOPMENT.md \"Code Hitlist\" for technical debt items)\n\n**Note**: Some crates are marked for refactoring (see DEVELOPMENT.md). Be cautious when modifying:\n\n- gitbutler-reference\n- gitbutler-branch-actions\n- gitbutler-repository\n\n## CI/CD\n\n### Workflows\n\nLocated in `.github/workflows/`:\n\n- `push.yaml`: Main CI for linting, building, and testing on push\n- `publish.yaml`: Release builds for different platforms\n- `test-e2e.yml`: E2E tests (Playwright and blackbox)\n- `test-client-fe-integration.yml`: Frontend integration tests\n\n### Pre-commit Checks\n\nBefore committing, ensure:\n\n1. Code is formatted: `pnpm format && pnpm rustfmt`\n2. Linting passes: `pnpm lint && cargo clippy --all-targets`\n3. Tests pass: `pnpm test && cargo test`\n4. Build succeeds: `pnpm build`\n\nOr use the shortcut: `pnpm isgood`.\nAuto-fix with `pnpm begood && cargo clippy --fix --all-targets`\n\n## Common Patterns and Conventions\n\n### Frontend File Organization\n\n- Use absolute imports via package references (e.g., `@gitbutler/ui-svelte`) instead of relative imports\n- Components should be in logical directories by feature\n- Shared utilities go in `packages/shared`\n- UI components go in `packages/ui-svelte`\n\n### Naming Conventions\n\n**JavaScript/TypeScript**:\n\n- Components: PascalCase (e.g., `BranchCard.svelte`)\n- Files: kebab-case (e.g., `branch-service.ts`)\n- Variables/functions: camelCase\n\n**Rust**:\n\n- Follow standard Rust naming conventions\n\n### Error Handling\n\n**JavaScript/TypeScript**: Throw errors and handle with try/catch at appropriate boundaries\n\n**Rust**: Use `Result<T, E>` and `anyhow` for error handling. Most functions should return `anyhow::Result`.\n\n## Troubleshooting\n\n### Build Issues\n\n```bash\n# Clear Turbo cache\npnpm exec turbo daemon stop\npnpm exec turbo daemon clean\n\n# Clear node_modules and reinstall\nrm -rf .turbo node_modules\npnpm install\n\n# Clear Rust build artifacts\ncargo clean\n```\n\n### Platform-Specific Issues\n\n- **macOS**: Ensure Xcode and cmake are installed\n- **Linux**: Install webkit2gtk and other dependencies (see DEVELOPMENT.md)\n- **Windows**: May require Perl, OpenSSL setup, and specific compiler settings (see DEVELOPMENT.md)\n\n## Performance Considerations\n\n### Development Builds\n\n- Set `GITBUTLER_PERFORMANCE_LOG=1` for performance logging\n- Use `--release` flag for more realistic performance testing\n\n### Turbo Cache\n\n- Turborepo caches build outputs\n- If experiencing stale builds, clear cache: `pnpm exec turbo daemon clean`\n- Case-sensitive filesystem issues may affect Turbo on macOS (see DEVELOPMENT.md)\n\n## Additional Resources\n\n- **Development Guide**: See DEVELOPMENT.md for detailed setup instructions\n- **Contributing Guide**: See CONTRIBUTING.md for contribution guidelines\n- **Discord**: https://discord.gg/MmFkmaJ42D for questions and discussions\n- See `crates/AGENTS.md` for Rust-specific API, repository, and testing conventions\n\n## Important Notes for AI Coding Agents\n\n1. **Always run lints and tests** after making changes\n2. **Use the existing patterns** in the codebase - don't introduce new patterns without discussion\n3. **Keep changes minimal** - surgical changes are preferred over large refactors\n4. **Follow the monorepo structure** - understand which package your change affects\n5. **Check both JS and Rust** - changes to the backend often require frontend updates\n6. **Documentation** - update relevant docs if changing public APIs or user-facing features\n7. **Avoid relative imports** - ESLint will error on relative imports; use package references\n8. **Test on target platform** if making platform-specific changes\n9. **Security**: Check dependencies for vulnerabilities before adding them\n10. **Code marked for refactoring**: Be extra careful with crates in the \"Code Hitlist\" section\n11. **but CLI happy path testing only**: CLI tests are expensive and should be limited to what really matters.\n\n## Review Guidelines\n\nFollow these guidelines when reviewing code.\n\n- Do not report type errors. CI will catch those.\n- Don't review formatting such as commas, indentation, etc\n","isInternal":false,"tokens":2411,"sizeBytes":10110},{"name":"SKILL.md","path":".agents/skills/but-performance-tests/SKILL.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/.agents/skills/but-performance-tests/SKILL.md","title":"but-performance-tests","category":"anthropic-skill","format":"markdown","content":"---\nname: but-performance-tests\ndescription: Use when creating, changing, running, or debugging shell-based `but` CLI performance scenarios under `crates/but/tests/performance`, including Hyperfine runs, fixture setup, setup-to-test state, and output inspection.\n---\n\n# `but` CLI performance tests\n\nRead [performance framework documentation](../../../crates/but/tests/performance/README.md)\ncompletely before answering questions or changing performance scenarios. Inspect nearby\nscenarios and current shared helpers as applicable; README is canonical source for\nframework behavior, commands, helper APIs, and debugging options.\n\n## Workflow for changes\n\n1. Confirm requested operation and timing boundary.\n2. Inspect closest existing scenarios plus `lib.sh` and `run.sh`.\n3. Add or update only scenario's `setup.sh` and `test.sh` unless shared behavior is\n   genuinely required.\n4. Prefer real, representative GitButler repository state over toy data.\n5. Keep setup, validation, and ID discovery outside timed operation.\n6. Reuse documented helpers instead of creating parallel fixture, state, output, or\n   timing mechanisms.\n7. List every scenario in top-level performance README's included-scenario list, and\n   update its entry when materially changing scenario.\n8. Run README's shell validation and short single-scenario benchmark.\n\nFor usage or debugging questions, answer from current README and scripts. Do not edit\nfiles unless user asks for changes.\n\n## Guardrails\n\n- Keep changes inside `crates/but/tests/performance/` unless task specifically requests\n  related documentation or skill updates.\n- Preserve two-file scenario contract.\n- Do not add custom timing/statistics code; Hyperfine owns measurement.\n- Do not include fixture setup or compilation in measured command.\n- Do not run all scenarios when user requests or changes only one.\n- Report fixture/setup limitations and unavailable validation explicitly.\n","frontmatter":{"name":"but-performance-tests","description":"Use when creating, changing, running, or debugging shell-based `but` CLI performance scenarios under `crates/but/tests/performance`, including Hyperfine runs, fixture setup, setup-to-test state, and output inspection."},"isInternal":false,"tokens":383,"sizeBytes":1925},{"name":"SKILL.md","path":".agents/skills/cli-commands/SKILL.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/.agents/skills/cli-commands/SKILL.md","title":"cli-commands","category":"anthropic-skill","format":"markdown","content":"---\nname: cli-commands\ndescription: Use when adding or modifying CLI command (`but` commands) under `crates/but/src`.\n---\n\nImagine we're implementing a new `commit3` command. The high-level structure\nfor that must be as follows.\n\n## Arguments\n\nArguments live in `crates/but/src/args`. For our commit command that would be\n`crates/but/src/args/commit3.rs`:\n\n```rust\nuse crate::args::atoms::CliIdArg;\n\n/// Create a commit.\n///\n/// More details about the command here...\n#[derive(Debug, clap::Parser)]\n#[cfg_attr(feature = \"raw-clap-docs\", clap(verbatim_doc_comment))]\n#[deny(missing_docs)]\npub struct Platform {\n    /// The message to use for the commit.\n    #[clap(short, long, group = \"commit_message\")]\n    pub message: Option<Vec<String>>,\n\n    /// Place the commit on the branch `BRANCH`.\n    #[clap(short, long, value_name = \"BRANCH\", group = \"targeting\")]\n    pub branch: Option<Option<CliIdArg>>,\n\n    /// One or more changes to commit.\n    pub changes: Vec<CliIdArg>,\n}\n```\n\nIn `crates/but/src/args/mod.rs`:\n\n```rust\n#[cfg(feature = \"legacy\")]\n#[cfg_attr(feature = \"raw-clap-docs\", clap(verbatim_doc_comment))]\n#[clap(hide = true, name = \"_commit3\")]\nCommit3(commit3::Platform),\n```\n\nThings to note:\n\nArguments that refer to Git objects (such as commits, branches, files, hunks,\netc.) use some type from `crates/but/src/args/atoms/` and not `String` or another\nloose type.\n\nUse `CliIdArg` for arguments that reference existing Git objects such as\nbranches, commits, files, etc. This allows the user to use short IDs or fully\nqualified names.\n\n`String` should only be used for truly loose text input such as commit\nmessages.\n\n`Platform` and all of its fields must have documentation. The `Subcommands`\nvariant intentionally has no doc comment because clap obtains the command\ndocumentation from `Platform`.\n\nUse `#[clap(group = \"...\")]` to create mutually exclusive groups of arguments.\n\nCommands with tricky grammar can define a `pub(crate) const ERROR_EXAMPLES`\nnext to `Platform` and register it in `args::error_examples`; the block is\nappended after clap parse errors. At most 4 lines, each a\n`but <cmd> ...   # what it does` invocation that works as written (e.g.\ninclude `-m` where omitting it would open an editor).\n\n## Handling the command\n\nAdd a match arm to `crates/but/src/lib.rs` to handle the command:\n\n```rust\nmatch cmd {\n    Subcommands::Commit3(commit_args) => {\n        use crate::utils::IntermediateChannel;\n\n        let status_after = args.status_after;\n        let mut ctx = setup::init_ctx(\n            &args,\n            InitCtxOptions {\n                background_sync: BackgroundSync::Enabled { silent: false },\n                ..Default::default()\n            },\n            out,\n        )?;\n        out.begin_status_after(status_after);\n\n        let outcome = command::legacy::commit3::commit(\n            &mut ctx,\n            IntermediateChannel::new(out),\n            commit_args,\n        )\n        .emit_metrics(metrics_ctx)?;\n        out.print_cli_output(outcome)?;\n\n        run_status_after_if_requested(status_after, &mut ctx, out);\n\n        Ok(())\n    }\n\n    // all the other commands...\n}\n```\n\nThings to note:\n\nUse `IntermediateChannel`. Do not pass `OutputChannel` to commands.\n\nUse `OutputChannel::print_cli_output` to print the final output from the\ncommand. This ensures we handle all supported formats. If only human format is\nsupported, use `OutputChannel::print_cli_output_human`.\n\n## Implementing the command\n\nIn `crates/but/src/command/legacy/commit3.rs`\n\n```\npub fn commit(\n    ctx: &mut Context,\n    out: IntermediateChannel<'_>,\n    args: Platform,\n) -> CliResult<CommitOutcome> {\n    // get whatever dependencies we need from `Context` such as\n    // `RepoExclusiveGuard`, `IdMap`, `RefInfo`, etc.\n\n    // resolve the arguments into a `CommitOperation`\n    let commit_operation = resolve(ctx, args)?;\n\n    // Run the operation\n    let outcome = run(ctx, commit_operation)?;\n\n    // Return the outcome which will be printed by the caller\n    Ok(outcome)\n}\n\nfn resolve(ctx: &mut Context, args: Platform) -> CliResult<CommitOperation> {\n    let Platform { message, branch, changes } = args;\n    // ...\n}\n\nfn run(ctx: &mut Context, commit_op: CommitOperation) -> anyhow::Result<CommitOutcome> {\n    match commit_op {\n        // ...\n    }\n}\n\n#[must_use]\nstruct CommitOutcome {\n    new_commit: ObjectId,\n}\n\nimpl CliOutputHuman for CommitOutcome {\n    fn on_human(self, out: &mut dyn WriteWithUtils, _theme: &Theme) -> anyhow::Result<()> {\n        let Self { new_commit } = self;\n\n        writeln!(\n            out,\n            \"Created commit {}\",\n            theme::Commit(new_commit, None),\n        )?;\n\n        Ok(())\n    }\n}\n\nimpl CliOutput for CommitOutcome {\n    fn on_shell(self, out: &mut dyn WriteWithUtils) -> anyhow::Result<()> {\n        let Self { new_commit } = self;\n\n        writeln!(out, \"{}\", new_commit.to_hex_with_len(7))?;\n\n        Ok(())\n    }\n\n    fn on_json(self) -> impl serde::Serialize {\n        #[derive(Serialize)]\n        struct Output {\n            commit: HexHash,\n        }\n\n        let Self { new_commit } = self;\n\n        Output { commit: new_commit.into() }\n    }\n}\n```\n\nThings to note:\n\nCommands follow a `resolve` then `run` structure.\n\n`run` doesn't print its final output. It returns something that implements\n`CliOutput` / `CliOutputHuman` which the caller can then print.\n\n`resolve` returns `CliResult` because it needs to reject bad user input.\n\n`run` returns `anyhow::Result` because it can only hit internal errors. Bad\nuser input is handled by `resolve`.\n\n`resolve` translates CLI arguments into domain targets and validates user\ninput. It may query repository state to disambiguate or reject input, but\nshould not retain derived data in the operation.\n\n`run()` loads current repository state and computes results. Commit counts,\ndiffs, statistics, branch details, current tips, and workspace projections\ngenerally belong in `run()`. State derivable from those identifiers and the\ncontext belongs in `run()`.\n\nThe operation should not contain types from `crate::args::atoms`.\n\nA non-CLI caller such as the TUI should be able to construct an operation from\ndomain identifiers without reproducing repository queries from `resolve()`. If\nit must compute counts, diffs, or branch metadata to build the operation, those\nfields probably belong in `run()`.\n\nThe operation does not contain diff specs (usually in the form\n`Vec<DiffSpec>`). It should instead contain `CliId`s which `run` turns into\n`Vec<DiffSpec>` using `DiffSpecBuilder`. This makes it easier for the status\nTUI to call `run` directly.\n\nUser-input pickers and prompts are created via `InputOutputChannel`\naccessed through `IntermediateChannel::prepare_for_terminal_input`.\n\nThe JSON output includes both commit IDs and change IDs.\n\nUse `AllowMergedArg` and `MergedUpstream` to verify we don't mutate merged\ncommits and branches.\n\nPrinting of commits, branches, change IDs, etc. uses newtypes from `theme` such\nas `theme::Branch` and `theme::Commit`. This ensures consistent coloring.\n\nBad user input errors use `bad_input(...)`, optionally with `.arg_name()`,\n`.arg_value()`, and `.hint()`.\n\nAvoid making the operation or outcome types implement `serde::Serialize`.\nDefine the specific types needed inside `fn on_json`. This makes it harder to\nbreak compatibility by accident. Reusing `Serialize` from existing domain types\nis fine.\n\n## More examples\n\nFor examples of this structure in practice see\n\n- `crates/but/src/command/legacy/commit.rs`\n- `crates/but/src/command/legacy/move.rs`\n- `crates/but/src/command/legacy/squash.rs`\n- `crates/but/src/command/legacy/diff.rs`\n","frontmatter":{"name":"cli-commands","description":"Use when adding or modifying CLI command (`but` commands) under `crates/but/src`."},"isInternal":false,"tokens":1806,"sizeBytes":7624},{"name":"SKILL.md","path":".agents/skills/lite-render-perf/SKILL.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/.agents/skills/lite-render-perf/SKILL.md","title":"lite-render-perf","category":"anthropic-skill","format":"markdown","content":"---\nname: lite-render-perf\ndescription: Use when working on React code in `apps/lite` that derives values during render, adds or consumes a context, subscribes to the redux store or a query, or renders lists of row components — and whenever the lite UI is reported slow, laggy, or re-rendering too much. React Compiler does not prevent these regressions; load this skill BEFORE writing such code, not only when debugging it.\n---\n\nThe recurring render-performance failure in this app is accidental\nover-re-rendering: one interaction re-renders every row instead of the few\naffected ones. Repositories can have hundreds of branch rows, so this turns\nsingle-digit-millisecond interactions into triple-digit ones. The compiler\nmakes it easy to cause by accident because it silently stops memoizing\nexactly where you assume it won't.\n\n## The three mechanisms behind past regressions\n\n### 1. Whether the compiler memoizes a derivation is not predictable from the source\n\nThe compiler only caches what it can prove safe to cache, and it declines\nmore often than you would guess. Two derivations in this app are both\nmodule-level functions that call imported helpers, and they compile\ndifferently: `buildOutlineNavigationIndex` in `WorkspacePage.tsx` gets a\nmemo block keyed on its inputs, while `getDiffView` in `Details.tsx` got\nnone and re-ran on every render of a component that re-renders whenever the\nfile selection changes.\n\nUnmemoized inputs are contagious. `getDiffView` was called with values\nbuilt by `Match` pipes that the compiler had not memoized either; feeding\nit stable values instead was enough to make the memo block appear. So the\nchurn spreads outward from whichever value first fails to get a scope,\nwhich is why one unmemoized derivation can quietly cost far more than\nitself.\n\nEither way the effect is the same: the derivation re-runs and returns fresh\nidentities even when its data never changed, and everything keyed on those\nidentities — context values, props, effect deps — churns with it.\n\nWhat to do instead:\n\n- Derive query data inside the query's `select`. react-query caches the\n  result on `(data, select function identity)`, and the compiler DOES\n  memoize the closure on its captures — so the closure's identity becomes\n  exactly the dependency key you want. Extra inputs are simply captured by\n  the closure. Precedent: `useBranchesOutline.ts`. `useQueries` /\n  `useSuspenseQueries` give you the same lever in `combine` — precedent:\n  `lineStats` in `OutlineTree/UncommittedChangesRow.tsx`, which folds the\n  results down inside `combine` instead of mapping them out and deriving in\n  render the way `Details.tsx` does. Check what the closure captures:\n  capture a value that is itself rebuilt each render and you have only\n  moved the churn one level up.\n- For redux-derived collections, use `createSelector` (see\n  `projects/project.ts`), never a plain function returning a fresh array or\n  object.\n- Never conclude from the source that something is or is not memoized —\n  read the compiler's output. A compiled function imports\n  `react/compiler-runtime` and allocates slots with `const $ = _c(n)`; a\n  memoized value is assigned inside an `if ($[i] !== dep)` block, and its\n  dependencies are exactly the values that block tests. A derivation\n  sitting outside every such block runs on every render. Two ways to look:\n  fetch the vite-transformed module from the running app\n  (`fetch('/src/path/to/file.ts').then(r => r.text())` over CDP — see\n  below), or run the plugin offline on any file, which needs no app:\n\n```js\n// Resolve both from node_modules/.pnpm — pnpm does not hoist them. There is\n// no @babel/preset-typescript here, so strip types via parserOpts instead.\nawait babel.transformAsync(source, {\n\tfilename: file,\n\tparserOpts: { plugins: [\"typescript\", \"jsx\"] },\n\tplugins: [[\"babel-plugin-react-compiler\", { target: \"19\" }]],\n\tconfigFile: false,\n\tbabelrc: false,\n});\n```\n\n### 2. Context re-renders every consumer; it has no selector\n\n`use(SomeContext)` re-renders the component whenever the context value's\nidentity changes — there is no way to subscribe to a slice, and neither the\ncompiler nor `React.memo` can bail a context read out. Note this is a\ndifferent failure from mechanism 1: when the value's content legitimately\nchanged, fixing identities cannot help — the only cure is consumers\nsubscribing to something narrower than the whole value.\n\nWhat to do instead:\n\n- Context is for values that change rarely relative to their consumers.\n- Per-row state must be a narrow `useAppSelector` returning a primitive\n  (usually a boolean). Redux subscriptions only re-render when the selected\n  value changes, so a selection change re-renders two rows, not all of them:\n\n```tsx\nconst isSelected = useAppSelector((state) => {\n\tconst stored = projectSlice.selectors.selectPrimaryOutlineSelection(state, projectId);\n\treturn stored !== null && operandEquals(stored, operand);\n});\n```\n\nFor the current shape, see `useIsSelected` in `BranchesList.tsx` and the\n`selectIsSelectedOutline` subscription in `OutlineTree/ItemRow.tsx`.\n\n- If rows need a value that requires resolution against a big object,\n  resolve it once at the top and store the resolved form, rather than\n  letting every row resolve independently.\n\n### 3. Identity discipline in the store and in query results\n\n- Reducers must early-return when the update is a no-op, otherwise they\n  produce a new state identity and wake every subscriber.\n- Reading store data inside an event callback needs no subscription at all —\n  take `const store = useAppStore()` at the top and call `store.getState()`\n  in the callback, instead of subscribing with `useAppSelector` to a value\n  only the callback reads.\n- Destructure `useMutation` results (`const { isPending, mutate } = ...`) —\n  the top-level result object is a new identity every render\n  (`@tanstack/query` `no-unstable-deps`).\n- Hook call order can change what the compiler memoizes; moving plain\n  `useQuery` calls above a `useQueries` call has fixed memoization here\n  before. If the compiled output shows a value outside every memo slot for\n  no visible reason, try reordering the hooks around it.\n\n## Driving the dev app over CDP\n\n`pnpm dev` in `apps/lite` starts vite (port 5173) and Electron with\n`--remote-debugging-port=9222`. The first run compiles a helper binary with\ncargo and takes minutes; `Port 5173 is already in use` means an instance is\nalready running — reuse it, or `lsof -ti tcp:5173 | xargs kill`. React and\nRedux DevTools are auto-installed in dev builds.\n\n`curl http://127.0.0.1:9222/json` lists debug targets. Pick the entry with\n`\"type\": \"page\"` whose URL is on 5173 — the others are extension service\nworkers. Its `webSocketDebuggerUrl` speaks the Chrome DevTools Protocol; a\nfew lines of Node (global `WebSocket`) are enough to drive it:\n\n```js\n// Usage: node cdp.mjs eval \"<expr>\"   or:   node cdp.mjs shot out.png\nconst page = (await (await fetch(\"http://127.0.0.1:9222/json\")).json()).find(\n\t(t) => t.type === \"page\" && t.url.includes(\"5173\"),\n);\nconst ws = new WebSocket(page.webSocketDebuggerUrl);\nlet id = 0;\nconst pending = new Map();\nconst send = (method, params) =>\n\tnew Promise((res) => {\n\t\tpending.set(++id, res);\n\t\tws.send(JSON.stringify({ id, method, params }));\n\t});\nws.onmessage = (e) => {\n\tconst m = JSON.parse(e.data);\n\tpending.get(m.id)?.(m.result);\n};\nawait new Promise((res) => (ws.onopen = res));\nconst [mode, arg] = process.argv.slice(2);\nif (mode === \"eval\")\n\tconsole.log(\n\t\tJSON.stringify(\n\t\t\tawait send(\"Runtime.evaluate\", {\n\t\t\t\texpression: arg,\n\t\t\t\tawaitPromise: true,\n\t\t\t\treturnByValue: true,\n\t\t\t}),\n\t\t),\n\t);\nelse if (mode === \"shot\") {\n\tconst { data } = await send(\"Page.captureScreenshot\", { format: \"png\" });\n\t(await import(\"node:fs\")).writeFileSync(arg, Buffer.from(data, \"base64\"));\n}\nprocess.exit(0);\n```\n\nUseful from `Runtime.evaluate`:\n\n- The whole preload API is callable: `window.lite.branchList(projectId)`\n  etc. — the fastest way to inspect real API payloads and sizes.\n- Interact by dispatching events on DOM nodes found via `aria-label`s;\n  screenshot to verify visually.\n\nGotchas:\n\n- Repeated evaluates share one top-level scope: wrap every expression in a\n  `{ ... }` block or `const x` collides with the previous call\n  (\"Identifier 'x' has already been declared\").\n- Synthetic `MouseEvent`s do not move real focus; behavior wired to\n  `onFocus` (selection scopes) needs an explicit `.focus()` or real input.\n- Renderer edits arrive via HMR, but edits to store/redux modules trigger a\n  full page reload that wipes `window.*` state — re-arm any probes after.\n  Electron main-process edits are only compiled at startup: restart\n  `pnpm dev`.\n\n## Measuring: prove it, in both directions\n\nReact DevTools being installed means its hook is available in-page:\n\n1. Arm a commit logger and interact:\n\n```js\nconst hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;\nwindow.__log = [];\nconst orig = hook.onCommitFiberRoot.bind(hook);\nhook.onCommitFiberRoot = (id, root, ...rest) => {\n\twindow.__log.push({ dur: root.current.actualDuration });\n\treturn orig(id, root, ...rest);\n};\n```\n\n2. Judge by `actualDuration` per commit: an interaction should cost\n   single-digit milliseconds and re-render O(affected rows). A commit whose\n   duration scales with total row count is the signature of a blast.\n3. To find WHY, read the compiled module (mechanism 1 above). To watch a\n   specific context value's identity across commits, extend the logger to\n   walk `root.current` for the provider fiber (its `type` is the context\n   object, so match on `type.displayName`) and compare `memoizedProps.value`\n   against the previous commit's. Do NOT add `window.*` probe writes inside\n   components or hooks to trace identities — that mutation makes the\n   compiler bail out of the whole function and destroys the very memoization\n   you are measuring.\n4. Restore `onCommitFiberRoot` when done; measure before and after the fix.\n\n## Review checklist\n\n- Any non-trivial derivation in render: is its result's identity\n  load-bearing (context value, prop to many children, effect dep)? Move it\n  into `select`/`combine`/`createSelector`, or read the compiled output to\n  prove it harmless — do not assume the compiler handled it.\n- Any new context: how often does its value change, and how many consumers?\n- Any `useAppSelector`: does it return a primitive or a stored reference,\n  not a fresh object?\n- Any new reducer: does it no-op cleanly when the value is unchanged?\n","frontmatter":{"name":"lite-render-perf","description":"Use when working on React code in `apps/lite` that derives values during render, adds or consumes a context, subscribes to the redux store or a query, or renders lists of row components — and whenever the lite UI is reported slow, laggy, or re-rendering too much. React Compiler does not prevent these regressions; load this skill BEFORE writing such code, not only when debugging it."},"isInternal":false,"tokens":2531,"sizeBytes":10500},{"name":"SKILL.md","path":".agents/skills/lite-screenshots/SKILL.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/.agents/skills/lite-screenshots/SKILL.md","title":"lite-screenshots","category":"anthropic-skill","format":"markdown","content":"---\nname: lite-screenshots\ndescription: Use when asked to add before/after screenshots of a Lite UI change to a pull request, when a PR touching apps/lite/ui needs its visual change shown, or when extending the screenshot catalogue in apps/lite/e2e/tests/screenshots.spec.ts. Captures both sides locally against seeded fixtures, publishes the surfaces that changed, and posts them to the PR.\n---\n\nA reviewer reading a CSS diff cannot see the result. This captures the Lite UI\nwith the branch applied and again with it unapplied, and posts the surfaces that\ndiffer to the pull request.\n\nCapture runs **on this machine**, not in CI. It was tried in CI and abandoned:\nunder the bare X server on the Linux runners this Electron build paints a frame\non load and not reliably afterwards, so several surfaces hung indefinitely\nwhether the capture went through Playwright or the DevTools protocol. The same\nspec passes here. The CI workflow only leaves a reminder.\n\n## Before capturing: is the surface covered?\n\nWork this out first, by reading the diff — not by capturing and inferring it from\nthe result. A run that reports \"8 surfaces, 0 changed\" looks identical whether the\nchange is invisible or whether nothing looked at it.\n\n1. Identify which screen the change affects.\n2. Check it against the catalogue in `apps/lite/e2e/tests/screenshots.spec.ts`\n   (currently: workspace sidebar, diff pane, branches tab, upstream tab, project\n   picker, uncommitted rows, commit form, settings).\n3. **Covered** — capture, as below.\n4. **Not covered** — stop and ask the developer, offering the two lanes below.\n   Either way it is their call, not one to make quietly inside a screenshot\n   request. Tell them which screen is uncovered and which fixture would reach it.\n   - **Add it to the catalogue** when the gap is a _screen_ — something anyone\n     working nearby will change again. It edits a shared file and becomes\n     permanent coverage for every later pull request. See \"Extending the\n     catalogue\" below.\n   - **Capture it ad hoc** when the gap is a _state_ — a conflicted commit\n     mid-resolution, an empty list, one specific dialog — that no future pull\n     request is likely to want. Nothing shared changes and nothing is left\n     behind. See \"One-off capture\" below.\n\n   Prefer the catalogue when it is a close call. The ad-hoc lane photographs the\n   change once and protects nothing afterwards, and a catalogue that stops growing\n   makes every later request re-derive the same fixtures.\n\nIf they decline both, **do not post a comment**. A normal comment would report\n\"N surfaces, 0 changed\", which asserts the change is not visual when in truth it\nwas never photographed. Say plainly that the affected screen is not in the\ncatalogue and no screenshots were taken.\n\n## Changes the capture cannot see\n\nSome diffs are invisible to this harness for reasons that have nothing to do with\nthe change being non-visual. Rule these out before reading a \"0 changed\" result\nas an answer:\n\n- **Scrollbars and their gutter.** macOS hides scrollbars unless a mouse is\n  attached, and a styled `::-webkit-scrollbar` does _not_ override that — the\n  scroller simply reserves no gutter. On a machine set to \"When scrolling\", any\n  change to the track, thumb, gutter width, or a separator meeting the gutter\n  captures identically on both sides. Set System Settings → Appearance → Show\n  scroll bars → **Always** for the run, and say in the comment that you did.\n- **Hover, focus, and drag states** are not entered by the spec.\n- **Anything behind a scroll position** — surfaces are captured at the top.\n\n## Prerequisites\n\n- `cargo build -p but` once, giving `target/debug/but`.\n- The branch under test applied, and **not yet merged** — the base side comes\n  from unapplying it. Check `but status` first.\n- **Run every capture through `env -u ELECTRON_RUN_AS_NODE`.** An agent hosted\n  inside VS Code (itself Electron) inherits `ELECTRON_RUN_AS_NODE=1`, which makes\n  the Electron the test launches behave as plain Node. Every surface then fails\n  with `Error: Process failed to launch!` and nothing says why — the binary is\n  fine, and running it by hand from another shell works. Check with\n  `env | grep ELECTRON`.\n\n## The flow\n\nRun every command from the repository root.\n\n### 1. Capture the branch as it stands\n\n```console\n$ env -u ELECTRON_RUN_AS_NODE SCREENSHOT_OUT=head BUT=$PWD/target/debug/but \\\n    pnpm -F @gitbutler/lite test:e2e screenshots.spec.ts\n```\n\nOutput lands in `apps/lite/e2e/screenshots/head/` (gitignored).\n\n### 2. Capture the base\n\n```console\n$ but unapply <branch>\n$ but status        # confirm the branch is gone from the workspace\n$ env -u ELECTRON_RUN_AS_NODE SCREENSHOT_OUT=base BUT=$PWD/target/debug/but \\\n    pnpm -F @gitbutler/lite test:e2e screenshots.spec.ts\n$ but apply <branch>\n```\n\n**Check the unapply took effect.** It silently does nothing on a stale id or an\nalready-merged branch, and both runs then capture identical code — which looks\nexactly like \"this change is not visual\". If every pair matches in step 3, assume\nthis went wrong before concluding anything about the change.\n\nRe-apply immediately, before anything else can fail and leave the workspace short\na branch.\n\n**Stacked branches take the whole stack down.** `but unapply` on a branch that is\npart of a stack unapplies every branch in it, including ones this capture depends\non — unapplying a stacked change also removes the spec, and the base run then has\nno tests to execute. Check `but status` first. When the branch is stacked, capture\nthe base by reverting the change in the working tree instead:\n\n1. Edit the changed files back to their pre-change state by hand.\n2. Capture into `base`.\n3. `but discard <file-id>` to restore the committed version.\n\nConfirm the restore: the change must be back before anything else happens.\n\n### 3. Select what changed\n\n```console\n$ node apps/lite/e2e/compare-screenshots.mjs \\\n    apps/lite/e2e/screenshots/base \\\n    apps/lite/e2e/screenshots/head \\\n    /tmp/publish \\\n    \"https://raw.githubusercontent.com/<owner>/<repo>/pr-screenshots/pr-<number>/<short-sha>\" \\\n    /tmp/section.md\n```\n\nIt prints `total=`, `changed=`, `added=`, stages only the differing pairs into\n`/tmp/publish`, and writes the comment body. Surfaces that did not change are\nbyte-identical, so they are folded into a collapsed list rather than shown.\n\n### 4. Publish the images\n\nImages go on an orphan `pr-screenshots` branch, under `pr-<number>/<short-sha>/`.\nA per-commit directory matters: reusing one path lets GitHub's image proxy serve\nthe previous run's screenshots from cache, which reads as \"my change did nothing\".\n\nWrite them with the git data API rather than a local clone. There is nothing to\nclone, nothing to clean up, and no local `git` writes — which agents are often\nnot permitted to make outside the project, and which cannot touch the GitButler\nworkspace by accident.\n\n```console\n$ REPO=<owner>/<repo>; DIR=pr-<number>/<short-sha>\n$ HEAD_SHA=$(gh api repos/$REPO/git/refs/heads/pr-screenshots --jq '.object.sha')\n$ BASE_TREE=$(gh api repos/$REPO/git/commits/$HEAD_SHA --jq '.tree.sha')\n\n# One blob per image. base64 must be stripped of newlines.\n$ jq -n --rawfile c <(base64 -i /tmp/publish/<name>.png) \\\n      '{content: ($c | gsub(\"\\n\";\"\")), encoding: \"base64\"}' > /tmp/blob.json\n$ BLOB=$(gh api -X POST repos/$REPO/git/blobs --input /tmp/blob.json --jq '.sha')\n\n# One tree and one commit for all of them, then move the ref.\n$ jq -n --arg t \"$BASE_TREE\" --arg b \"$BLOB\" --arg d \"$DIR\" '{base_tree: $t, tree: [\n      {path: ($d + \"/<name>.png\"), mode: \"100644\", type: \"blob\", sha: $b}]}' > /tmp/tree.json\n$ TREE=$(gh api -X POST repos/$REPO/git/trees --input /tmp/tree.json --jq '.sha')\n$ jq -n --arg t \"$TREE\" --arg p \"$HEAD_SHA\" \\\n      '{message: \"Screenshots for #<number>\", tree: $t, parents: [$p]}' > /tmp/commit.json\n$ COMMIT=$(gh api -X POST repos/$REPO/git/commits --input /tmp/commit.json --jq '.sha')\n$ gh api -X PATCH repos/$REPO/git/refs/heads/pr-screenshots -f sha=\"$COMMIT\"\n```\n\nWhen the branch does not exist yet, omit `base_tree` and `parents`, then create\nthe ref with `POST git/refs -f ref=refs/heads/pr-screenshots -f sha=$COMMIT`.\n\n**Fetch one raw URL before posting.** A comment full of broken images is worse\nthan no comment, and the failure is invisible until someone opens the pull\nrequest:\n\n```console\n$ curl -s -o /dev/null -w '%{http_code}\\n' \\\n    https://raw.githubusercontent.com/<owner>/<repo>/pr-screenshots/<dir>/<name>.png\n```\n\n### 5. Post to the pull request\n\nUpsert a single comment rather than stacking one per run — find a previous\ncomment starting with `<!-- lite-screenshots -->` and PATCH it, else POST.\n\n```console\n$ jq -Rs '{body: .}' /tmp/section.md > /tmp/payload.json\n$ gh api \"repos/<owner>/<repo>/issues/<number>/comments\" --paginate \\\n    --jq 'map(select(.body | startswith(\"<!-- lite-screenshots -->\"))) | first | .id // empty'\n$ gh api -X PATCH \"repos/<owner>/<repo>/issues/comments/<id>\" --input /tmp/payload.json\n```\n\nDo not edit the pull request description: rewriting text a human owns risks\nclobbering their concurrent edits.\n\n### 6. Add the label\n\n`screenshots` is the record that they were posted. Add it once the comment is\nup, so the label says what is true:\n\n```console\n$ gh pr edit <number> --remove-label \"screenshots needed\" --add-label \"screenshots\"\n```\n\nCI labels any pull request with CSS in it `CSS`, and asks one that touches\nLite's for screenshots only if the change is visible; leave that label alone,\nit describes the diff.\n`screenshots needed` is one a human applies by hand when they do want them, and\nthe command above clears it if it is there.\n\nOnly after a comment with images actually went up. A pull request labelled\n`screenshots` with none attached is worse than an unlabelled one — it tells a\nreviewer the change has been shown when it has not. If you declined to post (an\nuncovered surface, a capture that failed), leave the labels as they are.\n\nScreenshots are asked for once per pull request and never again, so the label\nis final — a later push does not undo it, and a set posted here is not a claim\nabout code pushed after it. Recapture when the surfaces move; the command above\nis idempotent either way.\n\nIf you ever post images that are **not** captures of the running app — a page\nrendered from the branch's CSS, say, when the harness cannot be run — label them\nas such in the comment and mark it `<!-- lite-screenshots-replica -->` instead.\nA reviewer assumes a screenshot is a photograph of the app; anything else has to\nsay so, and the different marker keeps a later real run from overwriting it.\n\n## Before finishing\n\n- **Look at the images**, do not just trust exit codes. A green run proves files\n  were written, not that they show the surface.\n- Confirm the workspace is whole: `but status` should show the branch applied and\n  no stray uncommitted changes.\n\n## One-off capture\n\nFor a state the catalogue should not carry permanently. The run command filters\nby filename, so any spec whose name contains `screenshots` is picked up by the\nsame command and writes into the same `SCREENSHOT_OUT` directory — the compare\nstep cannot tell the difference.\n\nWrite `apps/lite/e2e/tests/screenshots-adhoc.spec.ts`, importing the shared\nhelpers rather than copying them (every workaround in them was earned, and a copy\nloses the comments explaining why):\n\n```ts\nimport { enabled, openProject, shoot } from \"../screenshot-helpers.ts\";\nimport { test } from \"../test.ts\";\n\ntest.describe(\"screenshots\", () => {\n\ttest.skip(!enabled, \"set SCREENSHOT_OUT to capture screenshots\");\n\ttest.describe.configure({ timeout: 180_000 });\n\ttest.use({ scenario: \"project-in-single-branch-three-branch-stack.sh\" });\n\n\ttest(\"<surface>\", async ({ appWindow }) => {\n\t\tawait openProject(appWindow);\n\t\t// ...reach the state...\n\t\tawait shoot(appWindow, \"<surface-name>\", \"<selector>\");\n\t});\n});\n```\n\nThe same fixture and selector rules as the catalogue apply — see below.\n\nThen capture both sides as usual, and:\n\n- **Delete the file** once the base run is done. It is untracked and not ignored,\n  so left behind it shows up as a stray change in `but status` and eventually in\n  someone's commit. The \"confirm the workspace is whole\" check at the end is what\n  catches this.\n- **Say in the comment** that the surface was captured ad hoc and is not covered\n  going forward, so nobody reads it as a regression guarantee.\n\n**Never run the two specs in one command.** The config sets `fullyParallel`, and\nthe catalogue spec wipes `SCREENSHOT_OUT` in a `beforeAll` — run concurrently, that\nwipe lands in the middle of the ad-hoc capture and deletes it. What survives is a\nplausible-looking directory missing exactly the surface you were asked for.\n\nThe trailing argument is a **substring match on the file path**, so a bare\n`screenshots` selects the ad-hoc spec as well and puts you straight into that race.\nName the file: `screenshots.spec.ts` for the catalogue, `screenshots-adhoc` for the\nad-hoc spec. Steps 1 and 2 above already do.\n\nCapture the ad-hoc surface **alone**, filtering to its filename, into its own\noutput directory on each side:\n\n```console\n$ env -u ELECTRON_RUN_AS_NODE SCREENSHOT_OUT=adhoc-head BUT=$PWD/target/debug/but \\\n    pnpm -F @gitbutler/lite test:e2e screenshots-adhoc\n```\n\nIts own directory also avoids inheriting stale PNGs from an earlier catalogue run,\nsince only the catalogue spec cleans up after itself. Then compare\n`adhoc-base` against `adhoc-head` as in step 3.\n\nWhen a change touches both a covered screen and an uncovered state, run the\ncatalogue first and the ad-hoc spec second, in separate commands, pointing both at\nthe same directory — that order is safe because only the first one wipes.\n\n## Extending the catalogue\n\nOnly after the developer has agreed (see the top of this document). Add the\nsurface to `apps/lite/e2e/tests/screenshots.spec.ts`, then capture as usual —\nthe same run proves the new surface works and shows the change. Coverage applies\nto every later pull request, not just this one.\n\n```ts\ntest.describe(\"<area>\", () => {\n\ttest.use({ scenario: \"<fixture>.sh\" });\n\n\ttest(\"<surface>\", async ({ appWindow }) => {\n\t\tawait openProject(appWindow);\n\t\tawait goToTab(appWindow, \"branches\"); // when it lives on another tab\n\t\tawait shoot(appWindow, \"<surface-name>\", \"<selector>\");\n\t});\n});\n```\n\n- `<surface-name>` becomes the filename and the heading a reviewer reads: name it\n  `commit-form`, not `outline-panel-2`.\n- Only fixtures calling `\"$BUT\" setup` register a project; the rest leave the app\n  on \"Select a project.\" and every capture fails. Known good:\n  `project-in-single-branch-three-branch-stack.sh`,\n  `project-with-remote-branches.sh`, `project-with-conflicting-commits.sh`.\n- If no fixture reaches the state, build it in the test — `uncommitted` writes\n  files into `testEnvironment.workdir/local-clone` and reloads.\n- Prefer a stable id for the clip (`#outline-panel`, `#details-panel`), else a\n  CSS-module prefix or an ARIA hook. Never nth-child or a generated class.\n- One screenshot per test, taken after a fresh load. This costs nothing here and\n  keeps the spec usable if CI capture is ever revisited.\n\n## Out of scope\n\n- **Native context menus** cannot be captured; they are OS windows.\n- **Dark mode** is not covered — the harness seeds `theme: \"light\"`.\n- **States needing a real remote or credentials** cannot be fixtured.\n","frontmatter":{"name":"lite-screenshots","description":"Use when asked to add before/after screenshots of a Lite UI change to a pull request, when a PR touching apps/lite/ui needs its visual change shown, or when extending the screenshot catalogue in apps/lite/e2e/tests/screenshots.spec.ts. Captures both sides locally against seeded fixtures, publishes the surfaces that changed, and posts them to the PR."},"isInternal":false,"tokens":3797,"sizeBytes":15447},{"name":"SKILL.md","path":".agents/skills/tui-tests/SKILL.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/.agents/skills/tui-tests/SKILL.md","title":"tui-tests","category":"anthropic-skill","format":"markdown","content":"---\nname: tui-tests\ndescription: Use when adding or modifying tests for one of GitButler's Ratatui TUIs\n---\n\n## Where tests live\n\n- Main TUI tests: `crates/but/src/command/legacy/status/tui/tests/`\n- Test harness/helpers: `crates/but/src/tui/test_utils.rs` and `crates/but/src/command/legacy/status/tui/tests/utils.rs`\n- Snapshots: `crates/but/src/command/legacy/status/tui/tests/snapshots/`\n\n## Basic pattern\n\n```rust\n#[test]\nfn describes_behavior_under_test() {\n    let env = Sandbox::init_scenario_with_target_and_default_settings(\"one-stack\").unwrap();\n    env.setup_metadata(&[\"A\"]).unwrap();\n\n    let mut tui = test_status_tui(env);\n\n    tui.input_then_render(KeyCode::Down)\n        .assert_rendered_term_svg_eq(file![\"snapshots/describes_behavior_under_test_001.svg\"]);\n}\n```\n\n## Driving the TUI\n\nUseful input examples:\n\n```rust\ntui.input_then_render(None);                                       // render without inputs\ntui.input_then_render('j');                                        // single char input\ntui.input_then_render(KeyCode::Down);                              // special key\ntui.input_then_render(Shift('j'));                                 // keys with shift\ntui.input_then_render(Control('j'));                               // keys with control\ntui.input_then_render([KeyCode::Down, KeyCode::Down]);             // multiple keys from array\ntui.input_then_render(\"commit message text\");                      // multiple keys from string\ntui.reload();                                                      // reload state after making external changes\n```\n\n## Assertions\n\nGenerally prefer\n\n- `assert_current_line_eq(str![...])` for cursor/selection behavior.\n- `assert_rendered_term_svg_eq(file![\"snapshots/test_function_name_001.svg\"])`\n  for everything else.\n\nGenerally you should include one `assert_rendered_term_svg_eq` per logical\ngroup of inputs, to catch bad states early.\n\nBe careful using `assert_rendered_contains` and `assert_rendered_not_contains`\nsince they might lead to false positives. They're intended to use while\niterating on a test where snapshots would cause too much churn.\n\nRead `crates/but/src/tui/test_utils.rs` and\n`crates/but/src/command/legacy/status/tui/tests/utils.rs` for more specialized\nassertions.\n\nYou're not allowed to add new kinds of assertions to `crates/but/src/tui/test_utils.rs` or\n`crates/but/src/command/legacy/status/tui/tests/utils.rs`. Rely entirely on the\nexisting assertions.\n\nDon't reuse snapshots from other tests. Give each test its own snapshots.\n\nAvoid using `tui.env().invoke_git(\"...\")`. Prefer performing operations via the TUI.\n\n## Running tests\n\n- `cargo test -p but <test-name>` to run one test.\n- `SNAPSHOTS=overwrite cargo test -p but <test-name>` to run and update\n  snapshots.\n- `cargo test -p but tui` to run all tui tests. Do this after changing\n  things.\n\nIf a test fails the output will include the rendered state of the test backend.\nThis can be used when iterating on a test as a way of inspecting the state.\n","frontmatter":{"name":"tui-tests","description":"Use when adding or modifying tests for one of GitButler's Ratatui TUIs"},"isInternal":false,"tokens":693,"sizeBytes":3004},{"name":"SKILL.md","path":"crates/but-agentlog/skill/SKILL.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/crates/but-agentlog/skill/SKILL.md","title":"but-agentlog","category":"anthropic-skill","format":"markdown","content":"---\nname: but-agentlog\nversion: 0.0.0\ndescription: \"Use for prompts like \\\"get context for branch\\\", \\\"catch up on this branch\\\", \\\"recover branch context\\\", or \\\"what prior agent work happened here\\\". Skim prior agent work from `but agentlog` captures for a GitButler branch, review / pull request / merge request, or change. Prefer this over generic git branch/diff inspection when the user asks for context, history, prior work, or branch catch-up.\"\nauthor: GitButler Team\n---\n\n# GitButler Agentlog Skill\n\nUse `but agentlog` to skim prior agent work from captured sessions.\n\nTrigger this skill for plain prompts like \"get context for branch\", \"branch\ncontext\", \"catch up on this branch\", \"recover context\", \"what happened on this\nbranch\", or \"what prior agent work happened here\".\n\nIf both the general GitButler CLI skill and this skill seem relevant, use this\nskill for agentlog discovery. Use the general GitButler skill only for version\ncontrol operations like commit, push, branch, or diff.\n\n## Core Workflow\n\nDo not search for or read local `SKILL.md` files. If this skill is active, use\nthese instructions directly. Do not inspect the general GitButler skill first.\nStart with `but agentlog skim`.\n\nStart with `skim` when prior work could affect your next action. It is a\ntable-of-contents view: all related sessions and turns, abbreviated and shown\nchronologically.\n\n```sh\nbut agentlog skim\nbut agentlog skim branch <branch-name-or-ref>\nbut agentlog skim review <review-id-or-pull-request-key>\nbut agentlog skim change <change-id-or-key>\n```\n\nUse `show` only when `skim` is too thin, ambiguous, or you need exact evidence:\n\n```sh\nbut agentlog show <session-key> --limit 20\nbut agentlog show <session-key> --turn <turn-key> --limit 20\n```\n\nFor normal current-branch recovery, run `skim` without target arguments first.\nIt discovers the applied GitButler branch itself. Do not run plain Git commands\nlike `git branch --show-current` or `git status` first.\n\n```sh\nbut agentlog skim\n```\n\nPass an explicit target only when the user gives one.\n\nUse JSON only when you need exact handles for drill-down:\n\n```sh\nbut --json agentlog skim\n```\n\n1. Start with `skim` for a clean orientation.\n2. Treat `skim` like compacted turn history: all related sessions and turns are\n   present, but each turn is abbreviated. It is not the full transcript.\n3. If `skim` is enough for a lightweight status answer, summarize it and stop.\n4. Drill down only when `skim` is ambiguous, misses the rationale, or you need\n   exact evidence.\n5. To drill down, rerun `skim` with `--json` to get the relevant\n   `session_key` and `turn_key`, then use `show`.\n6. Use `show <session-key>` for turn-level context.\n7. Use `show <session-key> --turn <turn-key>` only for turns that need exact detail.\n\n## Target Discovery\n\nWhen the user says \"current branch\" or gives no explicit target, run:\n\n```sh\nbut agentlog skim\n```\n\n`skim` performs GitButler branch discovery automatically. If target discovery\nfails, then inspect GitButler state:\n\n```sh\nbut status\n```\n\nDo not use plain Git's `gitbutler/workspace` branch as an agentlog target.\n\n## Rules\n\n- Prefer orientation before payload.\n- Prefer `skim` for turn-history recovery.\n- Treat `skim` as complete but abbreviated. It includes every related session\n  and every turn in those sessions, not every record or the full transcript.\n- Prefer human `skim` output first. Use `--json` only for drill-down\n  handles or exact evidence.\n- Run `show` when `skim` is thin, ambiguous, missing the why, or when the user\n  is asking you to make or verify a consequential claim.\n- Do not dump records for every candidate session.\n- Do not dump full records or transcripts. If session detail is needed, start\n  with `show <session-key> --limit 20` and summarize previews.\n- Treat matches as related evidence, not ownership.\n- Keep `session_key` and `turn_key` values internal unless the user asks for\n  evidence handles or they are needed for a follow-up command.\n- Use record snippets only when they justify the next action.\n- Say \"session related to branch X via turn Y\", not \"session for branch X\".\n\n## Reading `skim`\n\nUse `skim` to answer \"what has already been discussed, planned, attempted, or\nchanged in this work?\"\n\nHuman output is intentionally compact but includes every related session and\nevery turn in chronological order. JSON output keeps\nfull `session_key` and `turn_key` values so you can drill into `show` without\nprinting those handles to the user.\n\nThe skim is a compressed table of contents. It abbreviates each turn but does\nnot omit related sessions or turns.\n\nUseful fields:\n\n- `target_kind` and `target_key`: target used for discovery.\n- `sessions`: related sessions in chronological order, with counts and previews.\n- `coverage`: shown sessions, shown turns, and direct related turn count.\n- `sessions[].turns`: every turn in order, with labels and previews.\n\nIf `skim` has enough context for a lightweight answer, summarize from it and\nstop. If it is ambiguous or misses the why, use JSON skim handles and drill\ndown with `show`.\n\n## Reading `show`\n\nUse `show` to open a session or one turn.\n\n```sh\nbut agentlog show <session-key> --limit 20\nbut agentlog show <session-key> --turn <turn-key> --limit 20\n```\n\nWithout `--turn`, `show` answers what happened in a session at turn granularity.\n\nUseful fields:\n\n- `coverage`: returned turns versus total turns.\n- `turn_key`: handle for `show --turn`.\n- `turn_index`: order within the session.\n- `capture_kind`: backfill or incremental.\n- `record_count`: turn size before hydrating records.\n- `source_record_index_range`: provider/source indexes covered by the turn.\n- `observed_targets`: targets observed in the turn.\n- `latest_user_preview` and `latest_assistant_preview`: compact orientation.\n- `tool_counts`: tool call/result counts and tool names.\n\nIncrease `--limit` only when the current window misses the related turn or setup.\n\nWith `--turn`, `show` answers what exactly happened inside one turn.\n\nUseful record fields:\n\n- `coverage`: returned records versus total records in the turn.\n- `turn_record_index`: order within the turn.\n- `source_record_index`: original provider/source index when available.\n- `timestamp`, `kind`, `role`, `text`: message/tool orientation.\n- `tool_name` and `tool_input`: tool details when present.\n- `source_record`: redacted stored provider envelope for low-level debugging.\n\nTurn records are intentionally bounded. Increase `--limit` only for a specific\nturn after session-level `show` output proves that more detail is needed.\n\n## Skim Summary\n\nWhen summarizing a skim, do not say you recovered \"full context.\" Say you\nskimmed the full turn history in abbreviated form.\n\nInclude:\n\n- target used for discovery\n- previews explaining intent and outcome\n- minimal record snippets for the next action\n\nDo not lead with raw session or turn identifiers. Mention them only when the\nuser asks for evidence handles or when passing them to another command/tool.\n","frontmatter":{"name":"but-agentlog","version":"0.0.0","description":"Use for prompts like \"get context for branch\", \"catch up on this branch\", \"recover branch context\", or \"what prior agent work happened here\". Skim prior agent work from `but agentlog` captures for a GitButler branch, review / pull request / merge request, or change. Prefer this over generic git branch/diff inspection when the user asks for context, history, prior work, or branch catch-up.","author":"GitButler Team"},"isInternal":false,"tokens":1639,"sizeBytes":6970},{"name":"SKILL.md","path":"crates/but/skill/SKILL.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/crates/but/skill/SKILL.md","title":"but","category":"anthropic-skill","format":"markdown","content":"---\nname: but\nversion: 0.0.0\ndescription: \"Commit, push, branch, and manage version control with GitButler. Use for commits, selective dirty-file or hunk commits, branches, diffs, PRs, history edits, squashes, amends, undo, merge, apply, and unapply. For selected dirty files or hunks, inspect with `but diff`; use compact `but status` for commit order, branch/stack placement, or conflict overview; use `but status -fv` when file/hunk IDs or per-commit file details matter. Replaces git write commands.\"\nauthor: GitButler Team\n---\n\n# GitButler CLI Skill\n\nUse GitButler CLI (`but`) as the default version-control interface.\n\n## Start Here\n\nChoose the narrowest first command by task; avoid ritual status checks:\n\n```bash\n# Selected dirty files/hunks:\nbut diff\n\n# Commit order, branch/stack placement, conflict overview:\nbut status\n\n# File/hunk IDs, per-commit files, amend/split details:\nbut status -fv\n\n# Details for one known branch or commit:\nbut show <id>\n```\n\nDo not run plain `but status` and then `but status -fv` unless the compact output lacks file/hunk details needed for the task.\n\nFor \"commit just/only/specific changes on a new branch\", use the fast path:\n\n```bash\nbut diff\nbut commit -b <branch> -m \"<msg>\" <id> <id>\n```\n\n`but commit -b <branch> ...` creates the branch when it does not exist and prints the created commit. Do not run a separate `but branch new`, status command, or verification diff unless the task needs workspace information that the result does not provide.\n\n## IDs\n\nThe first token on each `but diff` / `but status` line is that line's ID. When a command needs an ID, copy it exactly from the current output; never hardcode or invent one. ID lifetimes differ by entity:\n\n- Changes and sources are **positional, space-separated** IDs (`but commit -b feat -m \"msg\" uvw:2e4 uvw:e2c`). An uncommitted hunk ID is written `<file-id>:<hunk-id>` (e.g. `uvw:2e4`, copied from bare `but diff`) — the part after the colon is the hunk's ID, **not** a line range (`uvw:16-40` is invalid). Do not invent flags like `--changes` / `--hunk` / `--ids`, pass a line range, or comma-separate IDs — `uvw,qyo` is parsed as one ID and fails.\n- `but diff` is the exception: it accepts at most **one** target. Bare `but diff` shows all uncommitted files; inspect committed files or other entities one target at a time — never `but diff <id> <id>`.\n- A committed file is `<commit-id>:<file-id>` (e.g. `mzm:uvw`, shown under each commit). A committed hunk is `<commit-id>:<file-id>:<hunk-id>` and is shown by `but diff <commit-id>`. `@` means the uncommitted area.\n- Commit IDs are stable change IDs that survive history edits (`amend`, `squash`, `move`, `uncommit`, `reword`). Commits without a change ID (e.g. upstream-only) lead with a sha prefix instead, and `#N`-suffixed refs disambiguate duplicates — both go stale after history edits, and a stale sha can silently resolve to the wrong commit. The `(sha …)` on verbose commit lines is informational — do not pass it to commands.\n- Branch short IDs are snapshot-local selectors. Use full branch names for branch-targeting mutations; short IDs are safe only for immediate read-only inspection.\n- File/hunk IDs copied from one diff read generally remain usable across chained commits. If one stops resolving, re-read `but diff` and retry.\n\n**Chaining:** mutation output is concise by default, so you may chain mutations with `&&` off one inspection read. Do not chain branch short IDs through mutations; use full branch names. Add `--status-after` only when the next step needs workspace IDs or details that the mutation result does not provide. Chained `but commit` calls stack in the order written — the first is oldest, each later one goes on top. History edits may run in sequence when every commit ref involved is a change-ID ref; run them one at a time with `--status-after` when a ref is sha-based or `#N`-suffixed, or when the next command needs freshly issued IDs. Chaining `but uncommit <id> && but diff` is safe because bare `diff` needs no ID from the uncommit output.\n\n## Non-Negotiable Rules\n\n1. Use `but` for all write operations. Never run `git add`, `git commit`, `git push`, `git checkout`, `git merge`, `git rebase`, `git stash`, or `git cherry-pick`. The only exception is selecting a side of a conflicted submodule in resolution mode (see Resolve conflicted commits). If the user says a `git` write command, translate it to `but` and run that. Running from a linked worktree shows the same workspace and IDs as the main worktree, and `@` still means the main worktree's changes; a bare `but commit` or `but diff` takes that worktree's changes, a bare path names a file there, and `<worktree>:@` names them from anywhere.\n2. Mutation commands print their result without appending workspace status. Add `--status-after` only when the next step needs resulting workspace IDs or details; otherwise trust the mutation result and do not run a verification status/diff.\n3. Branches marked `(merged upstream)` have landed; start new work on another branch. That marker alone does not mean `but pull` removes them: run `but pull --check` first. `but pull` removes only branches listed `[integrated]` (\"has been integrated upstream and removed locally\", or \"has been merged into <target> and removed\" when the target has no upstream) and rebases the rest, which stay applied. To clear a landed branch that stays applied, use `but unapply <branch>` (acts on its whole stack; `but apply <branch>` restores it) rather than `but discard`. `push` and mutations (`commit`, `amend`, `squash`, `uncommit`, `reword`, `move`) refuse landed branches and commits, `absorb` skips them with a notice, and `commit` skips them when picking a default target.\n4. In non-interactive CLI workflows, do not narrate progress between routine commands. Execute the needed `but` commands and give a concise final summary.\n5. Prefer this skill and `but skill reference` over exploratory help calls. Use `<command> --help` when required syntax is missing or a command fails; use top-level help only when you genuinely need to discover an undocumented command.\n\n## Command Patterns\n\n- Initialize a Git repository: `but init` (current directory) or `but -C <path> init`. Non-interactive; creates no commits, remotes, or GitButler workspace. Existing repositories are left unchanged.\n\n- Commit: `but commit -b <branch> -m \"<msg>\" <id> <id>` — `-b <branch>` creates the branch if it does not exist\n- Commit everything uncommitted: `but commit -b <branch> -m \"<msg>\"` (omit the IDs)\n- Several commits from one diff: chain `but commit` calls with `&&` (commits stack oldest-first)\n- Commit at a specific history position: `--above <commit-or-branch>` or `--below <commit-or-branch>` (mutually exclusive). With a branch target, add `-b <new-name>` to name the new branch; otherwise its name is generated. The name must not already exist. Do not combine `-b` (even without a name) with commit/worktree targets.\n- Targeting is **required** when more than one **stack** is applied; without it `but commit` fails with \"Unclear where to commit. Found more than one stack\". Several branches stacked together count as one stack — an untargeted commit then silently lands on the stack's top branch, so pass `-b` whenever the branch matters.\n- Always pass `-m \"<msg>\"` (or `-F -` with a heredoc, `-F <file>` with a file outside the repository, or `--no-message`) to `but commit`, and to `but squash` whenever its sources are commits or branches unless the target is `@` — those compose a new message; without a flag an agent run keeps whatever the command composed (empty for `but commit`, the joined source messages for `but squash`) and a terminal opens an editor. Squash sources that are uncommitted or committed changes reuse the target's message and need no flag; squashing into `@` rejects message flags outright.\n- Amend: `but amend -t <commit-or-branch> <file-or-hunk-id> <file-or-hunk-id>` — a branch target resolves to its newest commit\n- Uncommit: `but uncommit <commit-id>` (whole commit), `but uncommit <branch>` (all commits and remove branch), `but uncommit <commit-id>:<file-id>` (committed file), or `but uncommit <commit-id>:<file-id>:<hunk-id>` (committed hunk); committed files and hunks may be mixed, but all must come from one commit\n- Insert empty commit: `but commit --empty -b <branch> -m \"<msg>\"`\n- Pick onto a new named branch: `but pick <commit-sha> --above <branch> -b <new-name>` (`--below` also works). The name must not exist; `-b` cannot be combined with commit/worktree targets.\n- Squash commits: `but squash <source-commit-id> [<source-commit-id>...] -t <target-commit-id> -m \"<msg>\"`\n- Move committed changes into an existing commit or `@`: `but squash <commit-id>:<file-id> -t <commit-id-or-@>` for a committed file; `but squash <commit-id>:<file-id>:<hunk-id> -t <commit-id-or-@>` for a committed hunk; all sources must come from one commit\n- Move committed changes into a new commit at a chosen position: `but move <commit-id>:<file-id> --above <commit-or-branch> -m \"<msg>\"` for a committed file; `but move <commit-id>:<file-id>:<hunk-id> --above <commit-or-branch> -m \"<msg>\"` for a committed hunk (`--below`, `--branch`, and `--unstack` also work)\n- Split selected committed changes into a new commit immediately above their source: `but split <committed-file-or-hunk-id> [<committed-file-or-hunk-id>...] -m \"<msg>\"`\n- For `but split` and committed-change `but move`, use `-m/--message` to name the new commit directly; repeated `-m` values are joined with blank lines. For a multiline message, pass `-F -` with a heredoc, or `-F <file>` with a file outside the repository, instead of `-m`. Omitting both creates an empty-message commit without opening an editor. `but move -m` and `-F` reject whole-commit and branch sources.\n- Squash a whole branch into one commit: `but squash <branch> -m \"<msg>\"` (no `-t`)\n- Uncommit and remove a branch: `but uncommit <branch>`\n- Reorder commits: `but move <commit-id> --below <commit-id>` (`--above` for the other direction; **commit IDs**, not branch names)\n- Reorder a block: `but move <commit-id> <commit-id> --below <following-commit-id>` or `--above <preceding-commit-id>` (both anchors accept multiple space-separated sources)\n- Move commit to branch top: `but move <commit-id> -b <branch>`\n- Move commits or committed changes onto a new named branch: `but move <source> --above <branch> -b <new-name>` (`--below` also works); use `but move <source> --unstack -b <new-name>` for an independent branch. Omit `-b` for a generated name. Naming is not supported when stacking or unstacking a branch source.\n- Stack branches: `but move <branch-name> --above <target-branch-name>` (**use full branch names**)\n- Tear off a branch: `but move <branch> --unstack`\n- Discard: `but discard <id> [<id>...]` — accepts branches, commits, committed changes, uncommitted changes, or `@` for all uncommitted changes\n- Push: `but push <top-branch>` — pushes the selected branch and its ancestors; to update a stack, select its top branch once and never loop. Bare `but push` pushes all unpushed work when run non-interactively — one push per stack (its topmost unpushed branch, ancestors included), so output has one entry per stack, not per branch. It exits non-zero if any stack failed; stacks that already pushed stay pushed, and rerunning after fixing the failure is safe (up-to-date stacks are skipped)\n- Pull (update workspace from the target): `but pull` — the output reports the result; `but pull --check` previews without updating when a preview is actually needed\n- Create PR: `but pr new <branch-name> [-m \"Title...\"] [-F - | -F <file>] [-t] [--draft]` — auto-pushes first; do not run `but push` before it\n\n## Task Recipes\n\n### Update workspace from main\n\nFor \"get latest from main\", \"update/sync this workspace\", \"rebase onto main\", or \"pull main\":\n\n1. `but pull` — one command; no preflight needed. Its output reports the resulting state, it refuses safely when uncommitted changes conflict, and `but undo` reverts it.\n2. If commits come back conflicted, resolve them oldest-first following the printed instructions: `but resolve <commit>`, edit the files, then `but resolve finish`. Its result gives the current ID of the next conflict. Add `--status-after` to the finish you expect to clear the last conflict only when the task needs the complete resulting workspace. When it says no conflicted commits remain, stop; do not run a verification status. Finishing a lower commit rebases the ones above it, so always work bottom-up.\n\n`but pull --check` answers \"would this conflict?\" without updating. Do not use it as a routine\npreflight; use it when the user asks for a preview, repository policy requires one, other agents'\nbranches may move, or an applied branch was just merged upstream (see rule 3).\n\nRebasing applied branches onto the latest target IS `but pull` — never `move`, `config target`, `unapply`, or raw `git pull`/`git rebase`. The base shown in status is the last FETCHED state: when `git log` shows `main` (local or remote) ahead of it, that is exactly the update `but pull` fetches and applies — the target setting is not stale and repointing it is never the fix. Pull carries uncommitted changes along, and its output reports the resulting state. If it refuses because uncommitted changes conflict, park them: `but commit -b <branch> -m \"wip\" <ids>`, pull again, then `but uncommit` the parked commit (there is no stash; do not hand-revert files).\n\n### Commit selected files or hunks\n\n1. `but diff` — shows file and hunk IDs for uncommitted changes. Do not run plain `but status` first.\n2. Use file IDs when whole files belong in the commit; use hunk IDs (`<file-id>:<hunk-id>`) when only part of a file belongs. Omit IDs you don't want committed.\n3. `but commit -b <branch> -m \"<msg>\" <id1> <id2>` — the branch is created if it does not exist, so no prior `but branch new` is needed.\n4. When the task requires knowing which changes remain, add `--status-after`; otherwise the created-commit result is sufficient.\n\nEdge case: if wanted and unwanted edits are in the same diff hunk, GitButler cannot split that hunk by ID. Only when the task requires keeping part of that hunk uncommitted, temporarily edit the working tree to isolate the wanted lines, commit those IDs, then restore the leftover lines so they remain uncommitted.\n\n### Amend into existing commit\n\n1. `but status -fv` (or `but show <branch-id>`) — locate file/hunk IDs and target commit IDs.\n2. `but amend -t <commit-id> <id> <id>` — one command per target commit. For several target commits, chain the amends with `&&` when every target is a change-ID ref; otherwise run them one at a time with `--status-after` to get fresh refs.\n\n### Split an existing commit\n\nFor a two-way split, move selected committed files or hunks directly into a new commit:\n\n1. `but diff <source-commit-id>` — read committed file and hunk IDs.\n2. `but split <committed-file-or-hunk-id> [<committed-file-or-hunk-id>...] -m \"<message>\"` — sources may mix files and hunks, but must come from one commit. The command creates the named commit immediately above the source and leaves unselected changes and the original message in the source. No follow-up reword is needed for the new commit.\n3. Add `--status-after` only when the next step needs workspace IDs or details not provided by the mutation result.\n\nFor more than two replacement commits, repeat `but split ... -m \"<message>\"` using the remaining source's committed changes, or use the uncommit/recommit workflow below when rebuilding the entire commit:\n\n1. `but status -fv` when you need the source commit, branch name, or placement anchor.\n2. `but uncommit <source-commit-id> && but diff` in one shell call exposes the commit's changes and prints the resulting file and hunk IDs.\n3. Pick replacement contents from that dirty diff, not from the old committed diff.\n4. Determine the requested chronological order from the user's wording, then create the replacement commits oldest-first by chaining `but commit` calls. Do not reverse that order to match `but status`, which displays commits newest-first. `-b <branch>` puts each new commit at the TOP of that branch — if the split commit had commits above it, the replacements now sit above those preserved commits.\n5. **If commits from that branch must stay ABOVE the replacements, put the preserved block back on top instead of fighting anchors.** Do not anchor with `--above <top>`/`--below <top>` (sha/`#N` anchors go stale as each insert rewrites history). Move the block together so its internal order stays intact: append `&& but move <preserved-id> [<preserved-id>...] -b <branch>` to the commit chain. Change-ID refs from step 1 stay valid; wait for fresh output first if any preserved ref is sha-based or `#N`-suffixed.\n6. Leave unwanted changes uncommitted. Replacements created oldest-first appear newest-first in status — that is correct; do not reorder them. Add `--status-after` to the final mutation when you need to inspect the resulting order.\n\n### Reorder commits\n\n`but status` displays commits newest/top first, while task specs often list history oldest to newest — translate before moving.\n\n1. `but status` once to get commit IDs (use `-fv` only if you also need file details).\n2. `but move <source> --below <target-commit>` places source immediately below target in `but status` (older in oldest-to-newest history). `--above` places it immediately above (newer). `but move <source> -b <branch>` moves it to branch top/newest.\n3. For an adjacent block, run ONE move, anchored either way: `but move <block-id> <block-id> --below <following-commit-id>` or `but move <block-id> <block-id> --above <preceding-commit-id>`. Pick an anchor outside the block; source order does not matter and the block keeps its internal order. Add `--status-after` when you need to inspect the resulting order; do not move the anchor or block members again.\n4. For other reorders, make the smallest set of moves.\n\n### Squash commits\n\n1. `but status` for commit IDs and order.\n2. Name the sources positionally and the result/target commit with `-t`: `but squash <source> [<source>...] -t <target> -m \"<new message>\"`.\n3. To collapse an entire branch into one commit, pass just the branch and no `-t`: `but squash <branch> -m \"<new message>\"`.\n4. Multiple independent groups may run in sequence off one status read (targets keep their change-ID refs); prefer newer/top groups first. Take fresh refs only when a ref is sha-based or `#N`-suffixed.\n5. Add `--status-after` to the final squash when you need to inspect the resulting history; do not re-verify with a separate status.\n\n### Stack existing branches\n\nTo make one existing branch depend on another: `but move <child-branch-name> --above <parent-branch-name>` (use full branch names; commit reordering uses commit IDs). To unstack: `but move <branch-name> --unstack`.\n\n**DO NOT** stack via `uncommit` + `branch delete` + `branch new --above` (git branch names persist after delete and it loses work), and do not use `but undo` to unstack.\n\n### Create or manage pull requests\n\n`but pr new <branch-name>` pushes the selected branch and its ancestors, then creates the PR in one step — no prior `but push`. Provide `-F -` with a heredoc or `-F <file>` (first line is the title), `-t`, or `-m` with real newlines (zsh/bash: `-m $'Title\\n\\nBody'`) so no editor opens. Do not write the message to a file inside the repository: the next `but commit` would commit it. If forge auth is missing, run `but config forge auth`.\n\nIf you do create a PR for a stacked branch, use `but pr` — not `gh pr create` (only `but pr` sets PR bases and stack metadata; `gh pr create` breaks that). To publish a whole stack: `but pr new <top-branch-name> -t`. Manage with `but pr auto-merge|set-draft|set-ready <selector>`; selectors can be a branch name, current branch/stack CLI ID, or numeric review ID. See `but skill reference` for details.\n\n### Dependency conflict with another branch\n\nChanges that build on another branch's commits cannot land on an independent branch. `but commit` and `but amend` fail atomically (\"Cannot commit: N changes could not be applied\"), naming the branch and commit each rejected change depends on — nothing is committed and no `-b` branch is created.\n\nWhen there is a single dependency branch, the error's Hint gives the exact recovery command: `but move <your-branch> --above <dependency-branch>` to stack an existing branch on its dependency, or `but branch new <name> --above <dependency-branch>` when the target branch didn't exist yet. Run it, then retry the original command. When the error names dependencies without a Hint (several dependency branches, or the dependency is on the target branch itself), run `but status -fv` to see where the dependent commits live before choosing a placement.\n\nIf that recovery command fails, do NOT try `uncommit`, `squash`, or `undo` as a workaround — re-run `but status -fv` to confirm both branches exist and are applied, then retry with exact branch names.\n\n### Resolve conflicted commits (after pull, move, or reorder)\n\n**NEVER use `git add`, `git commit`, `git checkout --theirs/--ours`, or any git write command during resolution.** Only `but resolve` commands plus direct file edits. The one exception is a conflicted submodule in edit mode, below.\n\nConflicts do not interrupt operations in GitButler: a rebase always completes, and commits that conflicted are marked `{conflicted}` in `but status`. Find them from the warning that history-editing commands (`move`, `discard`, …) print, from the `but pull` summary, or from `but status`. Resolve them one conflict at a time, without entering any mode. Work through one branch at a time, oldest commit first — the loop is:\n\n1. `but resolve conflicts <branch>` — shows the conflicts of that branch's oldest conflicted commit, numbered per file. Each conflict has three sections: `ours` (the new base the commit was rebased onto), `base` (common ancestor), and `theirs` (the commit's own version). Without a branch it picks the first conflicted branch and tells you which; the output also says when other conflicted commits exist.\n2. Apply one resolution per command:\n   - Merged/mixed content (the common case — combine the intent of both sides): pipe it to `but resolve apply <path>:<N>` via stdin (heredoc) or pass `--file <f>`. The content replaces the whole conflicted region; never include conflict markers.\n   - Take a side entirely: `but resolve apply <path>:<N> --ours` or `--theirs` (bare `<path>` applies the side to all conflicts in that file).\n   - Delegate one conflict to the configured AI model: `but resolve apply <path>:<N> --ai` — prefer your own merged content when you have the context.\n   - `apply` targets the same default commit as `conflicts`; when more than one branch is conflicted, pin it with `--commit <branch>`.\n3. Go back to step 1 until it reports no conflicts. Commit ids are not stable here — every apply rewrites the commit — but branch names are, so address everything by branch and never bookkeep commit ids. Partial progress is fine: a commit with unresolved conflicts left stays `{conflicted}` with exactly those conflicts.\n\nA wrong resolution is reverted with `but undo`.\n\n`but resolve <commit-id> --ai` resolves a whole commit with the configured AI model in one shot, and `but resolve --ai` without a commit resolves every conflicted commit, oldest first — acceptable as a fallback, but you usually have better context than that model does, so prefer resolving the conflicts yourself with `apply`.\n\n**Use edit mode only when you must run code against the resolution** (build/tests at that commit):\n\n1. `but resolve <commit-id>` — enters resolution mode and prints the conflict regions.\n2. **Edit the files** to remove every conflict marker — `<<<<<<<`, `|||||||` (the common-ancestor section), `=======` and `>>>>>>>` — and keep the correct content. Do NOT skip this; do NOT use `but amend` on conflicted commits.\n   - A conflicted submodule has no markers, and `but resolve finish` refuses while it is unresolved, even to keep the commit's own side. Select it explicitly: check out the wanted commit inside the submodule (or write the file that replaces it) and run `git add -- <path>`, or drop it with `git rm -- <path>`. These are the only git write commands allowed during resolution, and only for the paths `finish` names; never `git commit`. If the submodule directory is empty or missing, do not `git add` it (for a missing one that stages its removal): keep any resolution edits you want and report the conflict. `but resolve cancel --force` abandons the resolution and discards its edits; use it only when those edits can be discarded.\n3. `but resolve finish` reports leftover markers, surviving uncommitted changes, every remaining conflicted commit, and the exact current `but resolve <id>` command. Add `--status-after` to the finish you expect to clear the last conflict only when the task needs the complete resulting workspace. When it says no conflicted commits remain, stop; do not run a verification status. Cancel instead with `but resolve cancel`.\n4. Repeat for remaining conflicted commits, oldest first — finishing a lower commit rebases the ones above it.\n\n### Conflicts in uncommitted files\n\n`but status` marks uncommitted files with unresolved merge conflicts `{conflicted}`; they are excluded from committable changes and outside `but resolve` mode. Edit the file to the wanted contents (or delete it), then `but resolve <path>...` to mark it resolved with that state; it then shows as an ordinary uncommitted change.\n\n## Git-to-But Map\n\n| git | but |\n|---|---|\n| `git status` | `but status` for branch/stack/commit overview; `but status -fv` for file/hunk details; `but diff` for selected dirty changes |\n| `git add` + `git commit` | `but commit -b <branch> -m ... <ids>` |\n| `git checkout -b` + commit | `but commit -b <new-branch> -m ... <ids>` |\n| `git push` | `but push <branch-name>` |\n| `git rebase -i` | `but move`, `but squash`, `but reword` |\n| `git rebase --onto` | `but move <branch> --above <new-base>` |\n| `git checkout -- <file>` / `git restore` | `but discard <id>` |\n| `git cherry-pick` | `but pick` |\n| `gh pr create` | `but pr new <branch-name> -m \"Title...\"` |\n\n## Notes\n\n- Read-only git inspection (`git log`, `git blame`, `git show --stat`) is allowed.\n- If `but` prints an `AGENT ACTION REQUIRED` skill warning, run the suggested command once, then reload/use the GitButler skill. If it repeats, report it instead of retrying.\n- For command syntax and flags: `but skill reference`\n- For workspace model: `but skill concepts`\n- For workflow examples: `but skill examples`\n","frontmatter":{"name":"but","version":"0.0.0","description":"Commit, push, branch, and manage version control with GitButler. Use for commits, selective dirty-file or hunk commits, branches, diffs, PRs, history edits, squashes, amends, undo, merge, apply, and unapply. For selected dirty files or hunks, inspect with `but diff`; use compact `but status` for commit order, branch/stack placement, or conflict overview; use `but status -fv` when file/hunk IDs or per-commit file details matter. Replaces git write commands.","author":"GitButler Team"},"isInternal":false,"tokens":6544,"sizeBytes":26791},{"name":"AGENTS.md","path":"packages/ui-react/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/packages/ui-react/AGENTS.md","title":"Ui-react Skill","category":"anthropic-skill","format":"markdown","content":"# @gitbutler/ui-react\n\nThe React component library: the components, icons, illustrations and base\nstyles shared by Lite, `but panel` and the MCP app. It ships source, not a\nbuild; each consumer's Vite compiles it, and React Compiler runs on it under\nthe consumer's config as it does on the consumer's own code.\n\n`DESIGN.md` beside this file is the visual language: how things should look\nand read. It is an index of the pages in `design/`, one per topic; read the\nones for what you are changing before changing anything a user sees. This file\nis the tooling that enforces them.\n\n## Writing the code\n\nLite's rules apply here unchanged; `apps/lite/AGENTS.md` has them. In short:\nReact Compiler makes `useMemo`, `useCallback` and `React.memo` redundant\noutside hot paths; `useEffect` is usually an anti-pattern and needs consent;\ncomments explain the non-obvious why, never the what.\n\n### Changing a component\n\n**A change to a component here needs the designer's okay.** Every app draws\nwith these components, so a fix made for one surface changes all of them.\nWhen an app's bug seems to need a change here (a style, a prop, a default, a\nwrapper), try fixing it in the app first. If the library really is at fault,\nask before changing it. When you can't ask, make the change its own\n`fix(ui-react): …` commit that says which surface showed the bug and what\nelse it touches, and call it out in the PR description.\n\n### What belongs here\n\nA component belongs here when it doesn't reach into an app: no API calls,\nno settings, no store, nothing on `window.lite`. Everything a component needs\nfrom its host arrives as a prop, the way `TextLink` takes its `onClick` rather\nthan assuming Electron. A component that needs the app stays in the app;\nLite keeps its Markdown renderer and mention suggestions for that reason.\n\nImports are deep and carry the extension: `@gitbutler/ui-react/Button.tsx`.\nThere is no barrel file. Inside the package, siblings import each other\nrelatively.\n\n### Styles\n\nTokens come from `@gitbutler/design-core`, imported by the host once.\n`src/base.css` declares the few variables components read that aren't tokens\n(`--focus-ring`, `--control-cursor`, `--transition-button`) and the control\ncursor rules; every host imports it right after design-core. A variable nothing declares doesn't error, the\nproperty quietly falls back, so a new shared variable goes in `base.css`, not\nin a host's stylesheet.\n\n### Icons\n\nIcons are SVGs in `src/icons/`, inlined into the bundle as raw strings; the\nfilename is the icon name (`folder-lock.svg` → `<Icon name=\"folder-lock\" />`).\nTo add one:\n\n1. Export it from Figma at 16×16 (⚛️ Core library) as SVG into `src/icons/`.\n2. Run `pnpm -F @gitbutler/ui-react optimize-icons`. It minifies the SVG,\n   recolors it to `currentColor` and regenerates `src/iconNames.ts`.\n3. Commit both. `iconNames.ts` is generated; never hand-edit it.\n\n`src/file-icons/` are the language and filetype glyphs. They keep their brand\ncolours and are deliberately not run through the script.\n\n`src/program-icons/` are the marks of editors and terminals, as PNGs at 2×.\nTheir rounded corner is part of the image: export each from the \"Programm\"\nframe on ⚛️ Core's Icons page, where the mark is a 14px rectangle with\nthe corner on it, as PNG at 2× with the layer named as the file is\n(`vscode` → `vscode@2x.png`). No CSS rounds them, so a square export shows\nsquare.\n\n## Figma\n\nTwo files, split the way the code is:\n\n- **⚛️ Core** —\n  <https://www.figma.com/design/cqdnAotT8n9op8WGYLOHg4/%E2%9A%9B%EF%B8%8F-Lite-Core>,\n  the published library: tokens (Tokens page), icons (Icons page) and this\n  package's components, one section per component. It is the source of truth\n  for tokens; for components, code is, and the drawing follows it. The\n  components are on six pages, by what they are for: Actions, Inputs,\n  Overlays, Content & status, Preview (a details view's header and the bar\n  above it), and Sidebar. A new component's section goes at\n  the end of its group's page, with its examples section under it. Storybook\n  shelves the stories in the same groups (plus Icons, which Figma keeps on its\n  own page): a stories file names its group in `title` and keeps its old `id`,\n  so the story ids below don't change when a component moves group.\n- **Client**, the working file —\n  <https://www.figma.com/design/EBuHQGUcCaSw4Ln5uVpWkn/Client>: Lite's own\n  components on the ⚙️ Meta page, the full screens, and drafts on the\n  🚧 Drafts page and the pages under it. A new mockup or an exploration goes\n  here, never in the library.\n\nA component is drawn where its code lives: one in this package goes in\n⚛️ Core, one in `apps/lite` on ⚙️ Meta. When a component moves between the two\nin code, its drawing moves with it in the same piece of work, and the\nmockups' instances are swapped to the new one.\n\nThe drawing mirrors the code's structure: the same component under the same\nname, variant properties named as the props and their values (`status`,\n`variant`, `glyph`), and no property the code lacks. Figma may add only what\nthe code produces from data, such as a toggle for a side the code drops when\nits count is zero, and a `state` property (`default`, `hover`) for what CSS\ndraws under the pointer, as Button has; two components the code keeps apart\nstay apart in Figma and are placed side by side where a surface shows both.\n\nA component's description in Figma says what its JSDoc and DESIGN.md say\nabout it: what it is for, when to pick which variant, and a `Code:` line\nnaming the component and its file. Change one and change the other in the\nsame piece of work; a description that lags the code is what an agent reading\nFigma will build from.\n\n## Verifying your work\n\nAlways run the specified commands **exactly** as written. The package has no\napp to run, so the story is how a component is checked before it goes into a\nsurface.\n\n### Typechecking, the CSS rules, and tests\n\n```console\n$ pnpm -F @gitbutler/ui-react check\n$ pnpm -F @gitbutler/ui-react test\n```\n\n`check` typechecks and then runs Stylelint over the components' CSS with the\nrules the design pages give a machine: colours are tokens, font sizes are on\nthe 11–16 scale, radii come from the radius tokens. An error names what to use\ninstead. The exceptions are the ones the pages name, and each is marked where\nit is: the file icons' brand colours (a file-level override in\n`stylelint.config.mjs`) and a mask's opaque stop. A radius that is a share of\nanother, an inner corner following its outer one, is a `calc()` from the outer\ntoken and needs no exception (`design/foundations/radius.md`). A new exception\nis a design decision first; if it stands, disable the rule on that line with\nthe reason after `--`.\n\n### Tokens\n\nEvery `var(--x)` in this package has to resolve. Lite's token check reads this\npackage's source alongside its own:\n\n```console\n$ pnpm -F @gitbutler/lite check\n```\n\nIt fails on any undefined name and suggests the nearest real one. Run it\nafter any CSS change here, even when the package's own check passes.\n\n### Stories\n\nEvery component has a story beside it, in both themes, and a component that\nlives only in code is half a component. `pnpm check` holds the package to it\nthrough `scripts/check-stories.mjs`, which fails on a component no story\nimports. Storybook is Lite's, on port 6007, and lists this package's stories\nunder `components/`:\n\n```console\n$ pnpm -F @gitbutler/lite demos\n```\n\nThe package's own `test` runs its unit tests, not its stories. Lite's runs\nevery story, this package's included, as a test in headless Chromium; run\nthem alone with `pnpm -F @gitbutler/lite test:stories` (`apps/lite/AGENTS.md`\nhas the setup).\n\nA story renders alone at\n`http://localhost:6007/iframe.html?id=components-<name>--<export>&viewMode=story`;\nappend `&globals=theme:dark` for the dark theme. Check colour, font and\nspacing from computed styles rather than by eye. A story links its Figma\ncomponent through the `design` parameter; keep the link when you add a story,\nand compare the two sides when either changes.\n\n### Default, then examples\n\nA component's stories are one entry in Storybook, in one order:\n\n- **`Default` first:** the simplest use of the component, its props on the\n  controls where they have any to show. It answers what the component is and\n  accepts. `pnpm check` fails on a stories file that opens with anything\n  else.\n- **Examples after it:** each named after what it shows — a state\n  (`OnlyAdditions`, `NothingToList`) or a surface the app builds from it\n  (`Confirm`, `SelectProject`, `CommandPalette`). An example is written as\n  the app writes it: its own state, real copy, rendered open, since its\n  source is what an agent copies from `docs-show`.\n\nA surface built from several components is an example of the outermost one:\nthe upload confirmation is Modal's, the project picker is Popup's (a\ncombobox dressed in its parts), the command palette is PickerDialog's.\n\nIn ⚛️ Core the examples sit in a section named `<Component> examples`\ndirectly under the component's own section, on its group's page, one\ninstance per story, named as the story is; an example story links its\ninstance through the `design` parameter.\n\n### The manifest\n\nStorybook writes `/manifests/components.json` (and `components.html`, to\nread) from the stories: every component, its props from the TypeScript\ntypes, its stories with their source, and the import to write. An agent\nreads it instead of guessing an API. Two things about it to know:\n\n- The import to write is the `@import` tag in each component's JSDoc, as\n  `@import import { Badge } from \"@gitbutler/ui-react/Badge.tsx\";`. Give a\n  new component one: without it the manifest falls back to the package\n  root, and there is no such barrel. Storybook reads the tag through\n  `experimentalReactComponentMeta` in Lite's `.storybook/main.ts`.\n- The link to the Figma component is the story's `design` parameter, not\n  the manifest. `grep -l 'type: \"figma\"' src/*.stories.tsx` lists the stories\n  that have one; a story without one has no drawn spec yet.\n\nA story with no component behind it, as Lite's `AppUpdater.stories.tsx`,\ncarries `tags: [\"!manifest\"]` and says why. That is also why a styling\nhelper is not how a control is offered: `getButtonClassName` is for making\nanother component look like a button, and a button is `<Button>`, which the\nmanifest documents.\n\n### The Storybook MCP server\n\nWhile Storybook runs, it serves an MCP server at `http://localhost:6007/mcp`\n(`@storybook/addon-mcp`). Add it to your agent once:\n\n```console\n$ claude mcp add --transport http gitbutler-storybook http://localhost:6007/mcp\n```\n\nEach design notes page has an `.mdx` beside its `.md` in `design/`, under\nStorybook's Design section, so the published Storybook shows them and the MCP\nserver's `docs-list` lists them; `design/overview.mdx` renders DESIGN.md. The\nMDX renders the Markdown file itself, so edit the `.md`, never the page. What\nthe MDX writes is the title, a one-line summary (the description `docs-list`\nshows) and the file's raw URL, since `docs-show` returns the MDX source, not\nthe rendered text. A new page takes a Markdown file, an MDX beside it titled\n`Design/<Group>/<Name>` after its folder and file name (that is how links\nbetween the files find their Storybook page), and a row in DESIGN.md.\n\nBefore using a component, ask it rather than guessing: `docs-list` lists the\ncomponents, `docs-show` gives one's props, its stories and the import to\nwrite, and `docs-show-story` a whole story. Use only props it documents; if\none seems missing, ask instead of inventing it.\n`stories-find-by-component` finds the stories a file renders in, and\n`stories-preview` links to them. The published Storybook serves the same docs\ntools without a local one. The repository's `.mcp.json` connects Claude Code\nto it (it asks once); another agent, or one outside this repo, adds it:\n\n```console\n$ claude mcp add --transport http gitbutler-storybook-published https://master--6ab536f5f40e41db628ccf1b.chromatic.com/mcp\n```\n\nThe published server can time out, most often while a new master Storybook\ndeploys. Fall back in order: the manifest at\n<https://master--6ab536f5f40e41db628ccf1b.chromatic.com/manifests/components.json>,\na static file that keeps answering, then the component's own source and JSDoc\nin `src/`, which the manifest is built from. Never guess a prop because the\ntools were down.\n\n### Accessibility\n\n`@storybook/addon-a11y` runs axe on every story: the Accessibility tab under\na story lists contrast failures, controls without a name and misused roles.\nCheck it for a new or changed story. It catches the mechanical part of\nDESIGN.md's rules (an icon-only button's `aria-label`, a field's label,\ncontrast), not whether a label makes sense.\n\n### Chromatic\n\n`.github/workflows/chromatic.yml` publishes the Storybook to Chromatic's\npublic \"GitButler UI\" project: from master as the published Storybook, at\n<https://master--6ab536f5f40e41db628ccf1b.chromatic.com>, and for a pull request that touches the\nstories, as a preview with a UI Review check that compares every story's\nscreenshot with master's. A visual change waits there to be accepted rather\nthan failing the build; accept it when it is the change you meant.\n\n### Linting & formatting\n\nThe package is linted by oxlint with Lite's config, not by ESLint:\n\n```console\n$ pnpm exec prettier --check packages/ui-react\n$ pnpm run oxlint\n$ pnpm knip:non-prod\n```\n\nRun these before declaring the work done, and run `check-tokens` (above) with\nthem. The lint job in CI runs all of them and fails on what any one reports.\n","isInternal":false,"tokens":3457,"sizeBytes":13619},{"name":"CLAUDE.md","path":"packages/ui-react/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/packages/ui-react/CLAUDE.md","title":"Ui-react Skill","category":"anthropic-skill","format":"markdown","content":"AGENTS.md","isInternal":false,"tokens":3,"sizeBytes":9},{"name":"SKILL.md","path":"packages/ui-react/skill/SKILL.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/packages/ui-react/skill/SKILL.md","title":"gitbutler-ui","category":"anthropic-skill","format":"markdown","content":"---\nname: gitbutler-ui\ndescription: Use when changing what users see in an app built on `@gitbutler/ui-react` — Lite, the but.dev web app — adding or restyling a component, modal, form, list, empty state or toast, or writing the words on screen. Not for logic, data or backend work, even inside a UI file.\n---\n\n# Building GitButler UI\n\nWork as you would in any codebase: find the closest thing that already\nexists and copy it. These notes say where to look and what to check. They are\nnot a gate to clear before starting.\n\n## Scope\n\nChange what the task needs. Older screens may not follow these notes; leave\nthem alone unless the task is to fix them.\n\n## Start from the library\n\n`@gitbutler/ui-react` has the controls: `Button`, `Popup` (modals, dropdowns),\n`Select`, `Field`, `Checkbox`, `Switch`, `Tooltip`, `EmptyState`, `Snackbar`\n(raised through the `Snackbars` stack), `Toasts`, `Banner` and more. Use them\nrather than a control styled in a feature's CSS. Find a surface that already\nuses the ones you need and follow its structure.\n\nThe source is the reference, because it is the version you build with:\n\n- In the gitbutler repository: `packages/ui-react/src/`\n- In but.dev: `web/node_modules/@gitbutler/ui-react/src/`\n\nEach component's JSDoc says what its props do. The Storybook MCP server\n(`docs-show`) and\n<https://master--6ab536f5f40e41db628ccf1b.chromatic.com/manifests/components.json>\nhave the same docs with examples; use them when they answer, and go back to the\nsource when they don't. Don't wait on them.\n\nEach component's stories open with `Default`, its simplest use and its props;\nthe ones after it are examples, the component put to work the way an app\nwrites it. For a whole surface, find the closest example under its outermost\ncomponent (a confirmation is Modal's, a picker Popup's or PickerDialog's),\ncopy it and change what differs.\n\nWhen nothing fits, build the smallest thing that works and say so in the PR\ndescription: what was needed, which components you tried.\n\n## Read the design pages you need\n\nThe shared design language is the library's `DESIGN.md`, beside `src/` in the\nsame two places: an index of one page per topic, in `design/`, each saying\nwhen to read it. Read the pages for what you are building, not all of them.\nIn Storybook they are the Design section, and `docs-list` lists them.\n\nEach app keeps its own choices in its own `DESIGN.md`: Lite's is\n`apps/lite/DESIGN.md`, but.dev's is at its repository root.\n\n## Words\n\nShort and plain. A label is one to three words; a hint is one sentence. Call\nthings what the rest of the app calls them. Say what the user can't already see\non screen, once, without parentheses.\n\n## Check it\n\nLook at the result in light and dark, and keep one screenshot. In the\ngitbutler repository a component's story runs with `pnpm -F @gitbutler/lite\ndemos`; `apps/lite/AGENTS.md` has how to reach a single story.\n","frontmatter":{"name":"gitbutler-ui","description":"Use when changing what users see in an app built on `@gitbutler/ui-react` — Lite, the but.dev web app — adding or restyling a component, modal, form, list, empty state or toast, or writing the words on screen. Not for logic, data or backend work, even inside a UI file."},"isInternal":false,"tokens":730,"sizeBytes":2891},{"name":"README.md","path":"packages/but-mcp-app/README.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/packages/but-mcp-app/README.md","title":"but-mcp-app Documentation","category":"plugin-manifest","format":"markdown","content":"# GitButler workspace MCP App\n\nThese React apps render the structured results of the `gitbutler_workspace` and\n`gitbutler_review_card` MCP tools. Their compact workspace and review components\ntake their visual cues from GitButler Lite. Selecting a workspace commit or\nbranch opens a responsive details panel with copy, explain, and review actions.\nReview cards refresh open reviews and their CI status every 30 seconds until the\nreview closes, CI fails, or a refresh request fails.\n\nBuild the single-file resource embedded by the Rust MCP server with:\n\n```sh\npnpm -F @gitbutler/but-mcp-app build\n```\n\nThe generated `workspace.html` and `review.html` files under\n`../../crates/but/src/command/mcp` are ignored. Release workflows set\n`GITBUTLER_REQUIRE_MCP_APP=1` and must run the command above before compiling\n`but`. Normal Rust builds use a small fallback resource so they do not require\nNode.js or pnpm.\n","isInternal":false,"tokens":211,"sizeBytes":906},{"name":"README.md","path":"packages/but-sdk/README.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/packages/but-sdk/README.md","title":"but-sdk Documentation","category":"plugin-manifest","format":"markdown","content":"# @gitbutler/but-sdk\n\n`@gitbutler/but-sdk` is the local npm package that exposes GitButler Rust APIs to JavaScript/TypeScript through a native Node add-on.\n\n## How it works\n\nThe generated SDK surface is produced in two parts and merged into a single declaration file:\n\n1. **N-API bindings (functions)**\n   - Rust functions marked with `#[but_api(napi)]` in `but-api` generate `*_napi` exports via the `but_api` proc macro.\n   - `but-napi` links those generated exports and is built by `napi-rs` into a `.node` binary.\n   - `napi-rs` generates, per flavor folder:\n     - `gitbutler-sdk.{PLATFORM}.node` (Node native add-on)\n     - `index.js` (runtime loader + JS bridge)\n     - `index.d.ts` (N-API function declarations)\n\n2. **Schema-derived TypeScript types (structs/enums)**\n   - `but-ts` collects registered JSON schemas from `but-api` (`but_api::schema` module) and emits TS type aliases.\n   - These emitted type aliases are appended to the flavor folder's `index.d.ts`.\n\n\nThis package ships only generated artifacts (`browser.js` plus each flavor's `index.js`, `index.d.ts`, `*.node`) and re-exports them through package `exports`.\n\n## Workspace-projection flavors\n\nMutation APIs return a `WorkspaceState` whose projection depends on the\n`graph-workspace` cargo feature of `but-api` (see its feature docs). The SDK\nships one fully self-contained build per flavor:\n\n- `@gitbutler/but-sdk` (alias `@gitbutler/but-sdk/linear`, from\n  `src/generated/linear/`): built without the feature; `WorkspaceState`\n  carries the legacy `RefInfo`-based `headInfo` projection.\n- `@gitbutler/but-sdk/graph` (from `src/generated/graph/`): built with\n  `--features graph-workspace`; `WorkspaceState` carries the graph-based\n  `graphWorkspace` (`DetailedGraphWorkspace`) projection.\n\nEach folder has its own `.node` binary, loader, and declarations, so importing\nan entry point loads the matching native build — types and runtime cannot\ndisagree for napi consumers. (The desktop app consumes this package for types\nonly; its runtime is the Tauri backend, so it must still keep its import flavor\nin lockstep with `gitbutler-tauri`'s `graph-workspace` feature.) Both folders'\n`index.js`/`index.d.ts` are committed and CI-checked.\n\n## Generate bindings and types\n\nPer flavor, always run the napi build before the type generation:\n\n```bash\n# linear flavor (src/generated/linear/)\npnpm --filter @gitbutler/but-sdk build:napi\npnpm --filter @gitbutler/but-sdk build:types\n# graph flavor (src/generated/graph/)\npnpm --filter @gitbutler/but-sdk build:napi:graph\npnpm --filter @gitbutler/but-sdk build:types:graph\n```\n\nOr run the combined script, which builds both flavors:\n\n```bash\npnpm --filter @gitbutler/but-sdk build\n```\n\n### Why order matters\n\n`build:types` appends schema types to the existing declaration file. Running it repeatedly without regenerating N-API declarations first can duplicate schema type sections.\n\n## Add new bindings and types\n\n### Add a new exported N-API function\n\n1. In `crates/but-api`, annotate a Rust API with `#[but_api(napi)]`.\n2. Ensure input/output types are serializable in a way the macro supports.\n3. Re-run:\n\n```bash\npnpm --filter @gitbutler/but-sdk build:napi\n```\n\nYou should see a new `*Napi` export in `src/generated/linear/index.d.ts` (and `src/generated/graph/index.d.ts` for the graph flavor).\n\n### Add a new generated TypeScript type\n\n1. Make sure the Rust type derives `schemars::JsonSchema`.\n2. Register the type in `crates/but-api/src/schema.rs` using `TypeSchemaEntry`.\n3. Re-run:\n\n```bash\npnpm --filter @gitbutler/but-sdk build:types\n```\n\nYou should see a new `export type ...` in `src/generated/linear/index.d.ts` (and `src/generated/graph/index.d.ts` for the graph flavor).\n\n## Validate generated output\n\nBaseline validation:\n\n```bash\npnpm --filter @gitbutler/but-sdk check\n```\n\nOptional extra runtime/type sanity check:\n\n```bash\npnpm --filter @gitbutler/but-sdk testTypes\n```\n\nRecommended contributor flow after changing bindings/types:\n\n```bash\npnpm --filter @gitbutler/but-sdk build\npnpm --filter @gitbutler/but-sdk check\n# optional\npnpm --filter @gitbutler/but-sdk testTypes\n```\n\n## Usage in Electron (`apps/lite` pattern)\n\nUse native bindings in the Electron **main process**, then expose only typed IPC APIs to the renderer.\n\n### Main process model\n\n```ts\nimport { listProjectsStateless } from '@gitbutler/but-sdk';\n\nexport function listProjects() {\n\treturn listProjectsStateless();\n}\n```\n\n### IPC contract with SDK types\n\n```ts\nimport type { ProjectForFrontend } from '@gitbutler/but-sdk';\n\nexport interface LiteElectronApi {\n\tlistProjects(): Promise<ProjectForFrontend[]>;\n}\n```\n\n### Renderer consumes IPC API\n\n```ts\nconst projects = await window.lite.listProjects();\n```\n\nThis keeps native access out of the renderer while sharing the same generated TS types (`ProjectForFrontend`, `StackEntry`, `StackDetails`, etc.) across process boundaries.\n\n## Current exported binding entry points\n\nSee `src/generated/linear/index.d.ts` (and `src/generated/graph/index.d.ts` for the graph flavor) for the complete generated API and type surface.\n\n## See also\n\n- [but-napi](../../crates/but-napi/README.md)\n- [but-ts](../../crates/but-ts/README.md)\n\n## License\n\nFSL-1.1-MIT - See [LICENSE.md](../../LICENSE.md) for details.\n","isInternal":false,"tokens":1339,"sizeBytes":5252},{"name":"README.md","path":"packages/no-relative-imports/README.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/packages/no-relative-imports/README.md","title":"no-relative-imports Documentation","category":"plugin-manifest","format":"markdown","content":"# @gitbutler/no-relative-imports\n\nAn eslint rule for enforcing non-relative imports if there are `paths` in your `tsconfig.json` available to you.\n\n## What the rule affects\n\nThe rule requires that any import that _could_ be referenced absolutely via an entry in `paths` will provide an error.\n\nEven if a sibling import (IE `./foo`) is used, if it could be accessed via a path (IE `$lib/foo`). An error will be provided.\n\n## The `paths` format\n\nIt should be noted that the current implementation does not try to handle all the possible `paths` entries. Instead it handles the three common cases:\n\n```json\n{\n\t\"compilerOptions\": {\n\t\t\"paths\": {\n\t\t\t\"$lib\": \"./lib\", // Without glob. IE pointing directly to a file\n\t\t\t\"$lib/*\": \"./lib/*\", // Both with glob, IE anything starting in ./lib/... results in $lib/...\n\t\t\t\"$lib\": \"./lib/*\" // Path with, IE anything starting in ./lib/... results in $lib\n\t\t}\n\t}\n}\n```\n\n## Usage\n\n```\nimport noRelativeImportPaths from '@gitbutler/no-relative-imports';\n\nexport default [\n  {\n    plugins: {\n      'no-relative-import-paths': noRelativeImportPaths,\n    },\n    rules: {\n      'no-relative-import-paths/no-relative-import-paths': 'error',\n    },\n  },\n]\n```\n","isInternal":false,"tokens":305,"sizeBytes":1187},{"name":"README.md","path":"packages/shared/README.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/packages/shared/README.md","title":"shared Documentation","category":"plugin-manifest","format":"markdown","content":"# create-svelte\n\nEverything you need to build a Svelte library, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).\n\nRead more about creating a library [in the docs](https://kit.svelte.dev/docs/packaging).\n\n## Creating a project\n\nIf you're seeing this, you've probably already done this step. Congrats!\n\n```bash\n# create a new project in the current directory\nnpm create svelte@latest\n\n# create a new project in my-app\nnpm create svelte@latest my-app\n```\n\n## Developing\n\nOnce you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:\n\n```bash\nnpm run dev\n\n# or start the server and open the app in a new browser tab\nnpm run dev -- --open\n```\n\nEverything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app.\n\n## Building\n\nTo build your library:\n\n```bash\nnpm run package\n```\n\nTo create a production version of your showcase app:\n\n```bash\nnpm run build\n```\n\nYou can preview the production build with `npm run preview`.\n\n> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.\n\n## Publishing\n\nGo into the `package.json` and give your package the desired name through the `\"name\"` option. Also consider adding a `\"license\"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).\n\nTo publish your library to [npm](https://www.npmjs.com):\n\n```bash\nnpm publish\n```\n","isInternal":false,"tokens":385,"sizeBytes":1606},{"name":"README.md","path":"packages/svelte-comment-injector/README.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/packages/svelte-comment-injector/README.md","title":"svelte-comment-injector Documentation","category":"plugin-manifest","format":"markdown","content":"# svelte-comment-injector\n\nA Svelte preprocessor that injects an HTML comment at the top of every component, making it easier to identify components in browser DevTools.\n\n## ✨ Features\n\n- Injects a DevTools-visible HTML comment like:\n  ```html\n  <!-- Begin MyComponent.svelte -->\n  <div class=\"my-component\">\n  \t<!-- Component content -->\n  </div>\n  <!-- End MyComponent.svelte -->\n  ```\n\n## 📦 Installation\n\n```bash\nnpm install @gitbutler/svelte-comment-injector --save-dev\n```\n\n## ⚙️ Usage\n\nAdd the preprocessor to your Svelte configuration:\n\n```js\n// svelte.config.js\nimport svelteInjectComment from \"@gitbutler/svelte-comment-injector\";\n\nexport default {\n\tpreprocess: [svelteInjectComment()],\n\t// ...rest of your config\n};\n```\n\n## 🛠️ Configuration\n\nYou can customize the comment format by passing options to the preprocessor:\n\n```js\n// svelte.config.js\nimport svelteInjectComment from \"@gitbutler/svelte-comment-injector\";\n\nexport default {\n\tpreprocess: [\n\t\tsvelteInjectComment({\n\t\t\tenabled: true, // Enable or disable the comment injection\n\t\t\tshowEndComment: true, // Show the end comment\n\t\t\tshowFullPath: false, // Show the full path in the comment\n\t\t}),\n\t],\n\t// ...rest of your config\n};\n```\n","isInternal":false,"tokens":305,"sizeBytes":1212},{"name":"README.md","path":"packages/ui-svelte/scripts/README.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/packages/ui-svelte/scripts/README.md","title":"scripts Documentation","category":"plugin-manifest","format":"markdown","content":"# UI Package Scripts\n\n## `optimize-ui-icons.js`\n\nOptimizes all SVG files in `src/lib/icons/svg/` using svgo, then syncs `src/lib/icons/names.ts`.\n\n### Usage\n\n```bash\n# From the UI package root\npnpm optimize-ui-icons\n# or\nnode scripts/optimize-ui-icons.js\n```\n\n### What it does\n\n1. Runs svgo on every `.svg` in `src/lib/icons/svg/` with `preset-default` (keeping `viewBox` and groups intact)\n2. Sets `width=\"100%\"` and `height=\"100%\"` on every `<svg>` so icons scale via CSS\n3. Replaces hardcoded `fill`/`stroke` colors with `currentColor` (leaves `none` untouched)\n4. Adds `vector-effect=\"non-scaling-stroke\"` to all vector shape elements\n5. Updates `src/lib/icons/names.ts` to reflect the current set of icon files\n\n---\n\n## `optimize-file-icons.js`\n\nOptimises SVG file-type icons and writes them into `src/lib/components/file/icon/svg/`.\n\nSupports two modes:\n\n- **Optimize in place** (no argument): optimises all existing SVGs in the output directory\n- **Import from a source directory** (with argument): reads SVGs from `<svg-dir>`, optimises them, and writes each into the output directory (adding new files, updating changed ones)\n\n### Optimisation steps\n\n- Runs svgo `preset-default` (keeping `viewBox` and groups intact)\n- Removes `width`/`height` attributes so icons scale via CSS\n- Replaces hardcoded hex colours with CSS variables from a canonical colour map (`--file-icon-gray`, `--file-icon-green`, `--file-icon-teal`, `--file-icon-blue`, `--file-icon-dark-blue`, `--file-icon-yellow`, `--file-icon-orange`, `--file-icon-red`, `--file-icon-pink`, `--file-icon-purple`, `--file-icon-violet`)\n\n### Usage\n\n```bash\n# Optimize in place\npnpm optimize-file-icons\n# or\nnode scripts/optimize-file-icons.js\n\n# Import + optimize from a source directory\nnode scripts/optimize-file-icons.js <svg-dir>\n```\n","isInternal":false,"tokens":473,"sizeBytes":1803},{"name":"README.md","path":"packages/ui-svelte/src/lib/components/chipToast/README.md","rawUrl":"https://raw.githubusercontent.com/gitbutlerapp/gitbutler/HEAD/packages/ui-svelte/src/lib/components/chipToast/README.md","title":"chipToast Documentation","category":"plugin-manifest","format":"markdown","content":"# Toast Component\n\nA simple toast notification system for the GitButler UI package.\n\n## Features\n\n- **Simple**: Just type and text - no complex configuration\n- **Bottom-center positioning**: Appears at the bottom center of the screen\n- **Stacking**: Multiple toasts stack vertically\n- **Auto-dismiss**: All toasts automatically disappear after 4 seconds\n- **4 types**: Info, Success, Warning, and Error with appropriate styling\n\n## Usage\n\n### Basic Usage\n\n```svelte\n<script>\n\timport { toasts, ToastContainer } from \"@gitbutler/ui-svelte\";\n\n\tfunction showToast() {\n\t\ttoasts.success(\"Operation completed!\");\n\t}\n</script>\n\n<button on:click={showToast}>Show Toast</button>\n\n<!-- Add the container to your app root -->\n<ToastContainer />\n```\n\n### Toast Types\n\n```javascript\nimport { toasts } from \"@gitbutler/ui-svelte\";\n\n// Different toast types\ntoasts.info(\"This is a info message\");\ntoasts.success(\"Operation completed successfully!\");\ntoasts.warning(\"Please review your changes\");\ntoasts.error(\"Something went wrong\");\n```\n\n### Promise Integration\n\n```javascript\nimport { toasts } from \"@gitbutler/ui-svelte\";\n\nasync function handleAsyncOperation() {\n\tconst myPromise = fetch(\"/api/data\");\n\n\tawait toasts.promise(myPromise, {\n\t\tloading: \"Loading data...\",\n\t\tsuccess: \"Data loaded successfully!\",\n\t\terror: \"Failed to load data\",\n\t});\n}\n```\n\n### Clear All Toasts\n\n```javascript\ntoasts.clearAll(); // Removes all current toasts\n```\n\n## Components\n\n### `<Toast>`\n\nIndividual toast component.\n\n**Props:**\n\n- `type: 'info' | 'success' | 'warning' | 'error'` - Toast type\n- `message: string` - Toast message text\n\n### `<ToastContainer>`\n\nContainer that manages positioning and stacking. Add this once to your app root.\n","isInternal":false,"tokens":399,"sizeBytes":1711}],"systemPromptSnippet":"<yakaai_skills repo=\"gitbutlerapp/gitbutler\">\n<!-- File: AGENTS.md (Tokens: ~449 | Category: root-instruction) -->\n# GitButler Agent Instructions\n\nGitButler is a Rust/Svelte/React/TypeScript monorepo.\n\nApply all relevant instruction files. If instructions conflict, resolve them in\nthis order:\n\n1. Explicit human instructions\n2. Nearest nested `AGENTS.md`\n3. This file\n\n## Repo Map\n\n- `crates/` - Rust crates.\n- `apps/desktop/` - Tauri/Svelte desktop app.\n- `apps/web/` - Svelte web app.\n- `apps/lite/` - Electron/React desktop app.\n- `packages/` - shared TypeScript packages, including the SDK.\n- `e2e/` - Playwright, WebdriverIO, and blackbox end-to-end tests.\n\n## Working Style\n\n- Treat questions about the codebase as read-only unless the user asks for changes.\n- Make focused, reviewable changes; avoid unrelated rewrites.\n- Use the simplest design that solves the actual problem; do not add\n  speculative machinery, and remove machinery your change makes unnecessary.\n- Inspect nearby code before introducing patterns.\n- Prefer existing APIs, tests, and conventions.\n- Before declaring shared behavior done, check each applicable surface and contract\n  (desktop, web, Lite, CLI/TUI, N-API, SDK, and docs) and update it or explicitly\n  determine that it is unaffected.\n- Run targeted validation for the area touched.\n- Before adding new machinery to fix a behavior bug, reproduce the bug in a failing\n  test and survey the target file's existing loops and classifications as candidate\n  hosts; let the tests, not the diagnosis, set how much implementation the fix needs.\n- When a fix calls for a new mechanism (a new module, a new public API, or a parallel\n  walk where one already exists), propose the intended shape before building it.\n\n## Scoped Instructions\n\n- For Rust work under `crates/`, follow `crates/AGENTS.md`.\n- For Lite work under `apps/lite/`, follow `apps/lite/AGENTS.md`.\n- For the React component library under `packages/ui-react/`, follow\n  `packages/ui-react/AGENTS.md`.\n\n<!-- File: crates/AGENTS.md (Tokens: ~1967 | Category: root-instruction) -->\n# Rust Instructions\n\nThese are defaults for Rust source and test changes under `crates/`; more\nspecific instruction files override them. Do not apply them to generated,\nvendored, or fixture data unless the task is specifically about that code.\n\n## API Boundaries and Legacy\n\n- Treat `gitbutler-*` crates as legacy-heavy, but preserve local ownership and\n  nearby patterns for localized fixes. Do not introduce new `gitbutler-*` usage\n  in newer code unless the surrounding legacy boundary requires it. Keep legacy\n  fixes localized; migrate only when necessary, tiny, tested, and\n  behavior-neutral.\n- Minimize new `VirtualBranchesHandle` usage. At API boundaries, prefer\n  `but_ctx::Context` workspace helpers and newer `but-*` APIs.\n- `but-api` is the API surface for Tauri, Electron/N-API, CLI, and TUI callers.\n  Outer callers should prefer existing `but-api` functions when they fit, but\n  lower-level crates should not depend on `but-api`.\n- Preserve existing `but-api` macro, transport, serialization, and conversion\n  patterns instead of hand-rolling parallel wrappers.\n- Keep transport DTOs at the API boundary, commonly in a local `json` module,\n  and convert them to domain types before calling lower-level crates.\n- For permissioned `but-api` functions, follow the existing composition shape:\n  acquire permission near the wrapper and delegate to a `_with_perm` or other\n  permission-taking implementation.\n- API consumers such as the CLI, TUI, and other composed callers should use\n  `_with_perm` variants when they already hold permission to avoid extra locking\n  and deadlock risk.\n- Keep `Context` at API/composition boundaries; pass granular dependencies\n  like repo, workspace, metadata, database handles, or an `Editor` into\n  lower-level crates where possible.\n- Acquire repository access locks at top-level API/command boundaries. Do not\n  call helpers that acquire permissions while holding a guard; drop the guard or\n  call a permission-taking helper.\n- Preserve existing `DryRun` semantics; add dry-run support when the API/product\n  surface calls for preview behavior. Dry runs should avoid persisting refs,\n  objects, or oplog entries.\n- When an API offers both action-only and timeline-recording behavior, keep the\n  `*_only*` function free of oplog side effects; prepare a best-effort oplog\n  snapshot in the wrapper and commit it only after the mutation succeeds.\n- New user-facing mutations should participate in undo: record an oplog\n  snapshot via the existing `SnapshotDetails`/`OperationKind` wrapper pattern,\n  and call out any mutation intentionally left out of the timeline rather than\n  omitting it silently. Undo is snapshot-based; do not design bespoke inverse\n  operations for it.\n- Before changing or reviewing code that derives graph/workspace/branch/stack/commit\n  relationships, reachability, dependencies, ordering, operation targets, or Git\n  graph/history/ref-placement mutations, use `crates/WORKSPACE_MODEL.md` as the\n  reference. In short: prefer commit IDs and refs at API boundaries, convert to\n  operation-local selectors inside editor-backed operations, use\n  `but_graph::Graph` for relationship/reachability questions, use\n  `but_rebase::graph_rebase::Editor` for Git graph/history/ref rewrites where an\n  editor model exists, and treat `but_graph::Workspace` and\n  `but_workspace::RefInfo` as lossy presentation/compatibility views unless an\n  existing workspace-shaped boundary requires them.\n\n## Code Shape and Naming\n\n- Use Git/GitButler domain names and keep types/helpers in the crate or module\n  that owns the concept.\n- Avoid vague names like `Manager`, `Service`, `Helper`, `Util`, or `Processor`\n  unless nearby code uses that concept.\n- Solve the present problem directly. Avoid speculative abstractions: one-use\n  traits/types/functions, fake extension points, ceremonial wrappers/modules,\n  and public APIs larger than real callers need.\n- Keep module boundaries discoverable from names and the call graph.\n- Avoid drive-by refactors while fixing a specific bug.\n- Prefer explicit domain enum matches when wildcard arms would hide behavior.\n- Comment public API purpose/invariants/errors and complex algorithm intent;\n  avoid comments that merely repeat the code.\n\n## Git Repository Semantics\n\n- Do not rediscover Git repositories inside business logic; pass repo/context\n  explicitly.\n- Prefer repository APIs over shelling out to `git`, except at\n  shell/executable boundaries such as hooks, debug tooling, tests, or Git\n  interop helpers.\n- Preserve Git graph semantics even when the shape looks redundant or odd; do\n  not deduplicate, reorder, or smooth graph data unless Git semantics allow it.\n- Use `gix` for new repository logic. Treat `git2` and `Context::git2_repo` as\n  legacy/boundary-only escape hatches for libgit2 checkout/index, hooks,\n  transport/auth, or existing code that cannot reasonably move yet.\n- Keep Git paths, refnames, commit messages, and diff payloads byte-preserving\n  until UI/API boundaries; avoid lossy `String` conversion in business logic.\n- Treat filesystem and worktree writes carefully; use existing locking,\n  transaction, and access patterns.\n- Avoid implicit `SystemTime::now()` in testable business logic; pass time in\n  when deterministic behavior matters.\n- Use existing reload/invalidation helpers after external repo or worktree\n  changes, especially when working through `Context` caches.\n- Use `anyhow::Context` to explain what operation failed. When frontend/API\n  consumers need classification, use existing `but_error::Code` patterns; do\n  not make consumers match error strings.\n\n## Database Migrations (`but-db`)\n\n- Keep migrations forward-compatible: stay on the current `SchemaVersion` and\n  leave columns or tables that new code stopped using in place. Bumping the\n  version locks every older binary out of the database; reserve it for planned,\n  coordinated breaks, never routine cleanup.\n\n## Version Control\n\n- Assume the worktree may contain other agents' changes. Do not overwrite, clean\n  up, stage, commit, or amend changes you did not make.\n- When asked to branch, commit, push, or open a PR, use the GitButler `but`\n  CLI/workflow when available.\n- When the user says \"ship it\", commit your changes on a session branch,\n  creating one if needed, then push and open or update the PR. Reuse the\n  existing branch/PR when it already fits the session.\n- For small cleanup or follow-up fixes on your own branch, amend the relevant\n  existing commit(s) when that is the cleaner history.\n- Keep commit messages and PR descriptions succinct: why, impact, and core\n  decisions. Do not list local validation commands in commit messages or add AI\n  co-author trailers or tool branding.\n\n## Testing and Validation\n\n- Run the narrowest relevant test or check first, for example `cargo test\n  -p <crate> <test-name>` or `cargo check -p <crate> --all-targets`.\n- Use existing test-support crates and fixtures; prefer read-only fixtures for\n  read-only behavior.\n- For graph, rebase, or workspace behavior, prefer fixture-backed before/after\n  snapshots using existing visualizers, plus targeted structural assertions for\n  the invariant.\n- When snapshot output is volatile, stabilize inputs or normalize output; do not\n  replace strong snapshots with vague assertions.\n- When optimizing Git traversal or workspace behavior, preserve semantics with a\n  fixture-backed regression test.\n- Run `cargo fmt` for Rust formatting, avoiding unrelated dirty files.\n- Run `cargo clippy --fix --allow-dirty` only after inspecting the diff scope and\n  only when generated edits stay within the intended change.\n- Run `cargo machete` when dependencies changed; verify targets, features, and\n  platform-specific usage before removing dependencies.\n- After changing Rust APIs or types exposed through `@gitbutler/but-sdk`, run\n  `pnpm build:sdk && pnpm format`; this updates\n  `packages/but-sdk/src/generated`.\n- Use workspace-wide checks only when the change affects shared contracts or\n  multiple crates.\n\n## Assertions\n\n- Explain why a standard Rust assertion holds concisely using the last-argument message, like `assert!(1!=2, \"arithmetic unit on CPU works\")`\n- Snapshot assertions use `snapbox`, like `snapbox::assert_data_eq!(actual, snapbox::str![[r#\"...\"#]])`. `snapbox::assert_data_eq!` has no message argument, so explain why a snapshot holds with a `// comment` on the line above it.\n- Regenerate inline snapshots by running the tests with `SNAPSHOTS=overwrite`.\n- Remove unstable output before asserting (sanitize ids/timestamps/paths, e.g. the `but_testsupport` helpers) rather than snapshotting it raw. `snapbox` also pattern-matches expected data by default (`[..]` and `...` wildcards, path-separator normalization); append `.raw()` to the expected `snapbox::str!` when the value must match exactly (e.g. it contains backslashes or literal `[..]`/`...`).\n\n<!-- File: claude.md (Tokens: ~3 | Category: claude-rule) -->\nAGENTS.md\n\n<!-- File: crates/CLAUDE.md (Tokens: ~3 | Category: claude-rule) -->\nAGENTS.md\n\n<!-- File: .github/copilot-instructions.md (Tokens: ~2411 | Category: copilot-instructions) -->\n## General information\n\nThis is a Rust/Svelte/React/TypeScript monorepo with multiple projects. For\nagent instruction precedence and scoped guidance, follow the nearest applicable\n`AGENTS.md`; use this file as Copilot-oriented project context.\n\nThe main applications are found in the `apps` directory:\n\n- `desktop` containing the Tauri application's frontend code\n- `web` containing the web application's frontend code\n- `lite` containing the Electron/React desktop app\n\nRust code is found in the `crates` directory. It contains `gitbutler-tauri` for\nthe Tauri application, `but-api` for the API surface used by Tauri, Electron,\nCLI, and TUI callers, and the `but` CLI crate.\n\nThe `packages` directory contains different self-contained npm packages.\nThese are shared between the applications.\nThe packages are:\n\n- `but-sdk` containing the generated TypeScript/Node SDK for Rust APIs\n- `core` containing shared TypeScript helpers\n- `ui` containing the shared UI components\n- `shared` containing the shared types and utils\n- `no-cross-domain-imports` containing the no-cross-domain-imports ESLint package\n- `no-relative-imports` containing the no-relative-imports ESLint package\n- `svelte-comment-injector` containing the Svelte comment injector package\n\nThe `e2e` directory contains Playwright, WebdriverIO, and blackbox end-to-end\ntests.\n\n## Technology Stack\n\n- **Frontend**: Svelte with TypeScript\n- **Backend**: Rust\n- **Desktop Framework**: Tauri (like Electron but using Rust instead of Node.js)\n- **Build Tools**: Turborepo for monorepo management, pnpm for package management\n- **Git Libraries**: gix (gitoxide) and git2 (libgit2)\n- **Testing**: Vitest for JS/TS, Playwright for E2E tests, standard Rust testing framework\n\n## Development Setup\n\n### Prerequisites\n\n1. **Rust**: as specified in `rust-toolchain.toml`. Install via rustup.\n2. **pnpm**: Install standalone pnpm as described in [DEVELOPMENT.md](../DEVELOPMENT.md#prerequisites).\n3. **System Dependencies**: Tauri requires platform-specific dependencies (see DEVELOPMENT.md for details).\n\n### Initial Setup\n\n```bash\n# Clone the repository\ngit clone https://github.com/gitbutlerapp/gitbutler.git\ncd gitbutler\n\n# Install dependencies and the project-managed Node.js runtime\npnpm install\n\n# Build Rust binaries (required before running the app)\n# look at https://github.com/gitbutlerapp/gitbutler/blob/fd9a58de5579074c0526193143d531c21907a26b/scripts/install-tauri-debian-dependencies.sh\n# for Linux prerequisites for tauri.\ncargo build\n\n# or use this to build the `but` CLI, without the need for tauri dependencies.\ncargo build -p but\n```\n\n## Building and Running\n\n### Development\n\n```bash\n# Run desktop app in development mode\npnpm dev:desktop\n\n# Run with debug logs\nLOG_LEVEL=debug pnpm dev:desktop\n\n# Run web app\npnpm dev:web\n\n# Run UI component storybook\npnpm dev:ui\n```\n\n### Building\n\n```bash\n# Build all packages\npnpm build\n\n# Build desktop app only\npnpm build:desktop\n\n# Build for production (used for releases)\npnpm tauri build --features devtools --config crates/gitbutler-tauri/tauri.conf.nightly.json\n```\n\n## Testing\n\nWhen asked about frontend tests please read `@frontend.md` for context.\n\nTo run rust tests run `cargo test`, and for specific crates `cargo test -p CRATE_NAME`\n\n## Code Style and Linting\n\n### JavaScript/TypeScript\n\n**Linting**: ESLint with TypeScript and Svelte plugins\n**Formatting**: Prettier with Svelte plugin\n\n```bash\n# Check linting\npnpm lint\n\n# Fix linting issues\npnpm fix\n\n# Format code\npnpm format\n\n# Check formatting\npnpm prettier\n\n# Shortcut to check everything\npnpm isgood\n\n# Shortcut to fix and format everything\npnpm begood\n```\n\n**Important ESLint Rules**:\n\n- No relative imports (use `@gitbutler/` package references)\n- Import order: alphabetically sorted with specific group ordering\n- No console.log (use console.warn or console.error)\n- Prefer function declarations over arrow functions at top level\n- Svelte-specific rules for button types, unused props, etc.\n\n**Prettier Config**:\n\n- Tabs for indentation\n- Double quotes\n- No trailing commas\n- 100 character line width\n\n### Rust\n\n**Formatting**: Use rustfmt with nightly toolchain for imports grouping\n\n```bash\n# Format Rust code\ncargo fmt\n\n# Format with nightly settings\npnpm rustfmt\n# or (but don't do this unless you are asked to)\ncargo +nightly fmt -- --config-path rustfmt-nightly.toml\n\n# Run clippy for linting\ncargo clippy --all-targets\n```\n\n**Rust Formatting**:\n\n- Group imports: Std, External, Crate\n- Imports granularity: Crate level\n\n**temporary directories** - use `but-testsupport` for scenario creation. Read-only functions should use read-only fixtures. DO NOT USE the `std::env::temp_dir().join(format!(…)` pattern.\n\n## Dependency Management\n\n### Adding JavaScript/TypeScript Dependencies\n\n```bash\n# Add to root workspace\npnpm add -D <package>\n\n# Add to specific package\npnpm add <package> --filter @gitbutler/desktop\npnpm add <package> --filter @gitbutler/ui-svelte\n```\n\n**Always check for vulnerabilities before adding npm packages** (ecosystem: npm).\n\n### Adding Rust Dependencies\n\nEdit the appropriate `Cargo.toml` file:\n\n- Workspace-level dependencies go in `/Cargo.toml` under `[workspace.dependencies]`\n- Individual crate dependencies reference workspace versions when possible, and when it's used more than once.\n\n**Always check for vulnerabilities before adding Rust dependencies** (ecosystem: rust).\n\n## Monorepo Structure\n\n### Workspace Navigation\n\nThe repository uses:\n\n- **Turborepo** for JavaScript/TypeScript build orchestration\n- **Cargo workspace** for Rust crate management\n- **pnpm workspace** for npm package management\n\nWhen making changes:\n\n1. Identify which package/crate is affected\n2. Run builds/tests in that package first\n3. Then verify dependent packages still work\n\n### Crate Organization\n\nThe `crates/` directory contains ~65 Rust crates organized by functionality:\n\n- `gitbutler-*` prefix: Core GitButler functionality. These are old, and we want to port them to `but-*`.\n- `but-*` prefix: Backend utilities and services\n- Many crates have specific purposes (see DEVELOPMENT.md \"Code Hitlist\" for technical debt items)\n\n**Note**: Some crates are marked for refactoring (see DEVELOPMENT.md). Be cautious when modifying:\n\n- gitbutler-reference\n- gitbutler-branch-actions\n- gitbutler-repository\n\n## CI/CD\n\n### Workflows\n\nLocated in `.github/workflows/`:\n\n- `push.yaml`: Main CI for linting, building, and testing on push\n- `publish.yaml`: Release builds for different platforms\n- `test-e2e.yml`: E2E tests (Playwright and blackbox)\n- `test-client-fe-integration.yml`: Frontend integration tests\n\n### Pre-commit Checks\n\nBefore committing, ensure:\n\n1. Code is formatted: `pnpm format && pnpm rustfmt`\n2. Linting passes: `pnpm lint && cargo clippy --all-targets`\n3. Tests pass: `pnpm test && cargo test`\n4. Build succeeds: `pnpm build`\n\nOr use the shortcut: `pnpm isgood`.\nAuto-fix with `pnpm begood && cargo clippy --fix --all-targets`\n\n## Common Patterns and Conventions\n\n### Frontend File Organization\n\n- Use absolute imports via package references (e.g., `@gitbutler/ui-svelte`) instead of relative imports\n- Components should be in logical directories by feature\n- Shared utilities go in `packages/shared`\n- UI components go in `packages/ui-svelte`\n\n### Naming Conventions\n\n**JavaScript/TypeScript**:\n\n- Components: PascalCase (e.g., `BranchCard.svelte`)\n- Files: kebab-case (e.g., `branch-service.ts`)\n- Variables/functions: camelCase\n\n**Rust**:\n\n- Follow standard Rust naming conventions\n\n### Error Handling\n\n**JavaScript/TypeScript**: Throw errors and handle with try/catch at appropriate boundaries\n\n**Rust**: Use `Result<T, E>` and `anyhow` for error handling. Most functions should return `anyhow::Result`.\n\n## Troubleshooting\n\n### Build Issues\n\n```bash\n# Clear Turbo cache\npnpm exec turbo daemon stop\npnpm exec turbo daemon clean\n\n# Clear node_modules and reinstall\nrm -rf .turbo node_modules\npnpm install\n\n# Clear Rust build artifacts\ncargo clean\n```\n\n### Platform-Specific Issues\n\n- **macOS**: Ensure Xcode and cmake are installed\n- **Linux**: Install webkit2gtk and other dependencies (see DEVELOPMENT.md)\n- **Windows**: May require Perl, OpenSSL setup, and specific compiler settings (see DEVELOPMENT.md)\n\n## Performance Considerations\n\n### Development Builds\n\n- Set `GITBUTLER_PERFORMANCE_LOG=1` for performance logging\n- Use `--release` flag for more realistic performance testing\n\n### Turbo Cache\n\n- Turborepo caches build outputs\n- If experiencing stale builds, clear cache: `pnpm exec turbo daemon clean`\n- Case-sensitive filesystem issues may affect Turbo on macOS (see DEVELOPMENT.md)\n\n## Additional Resources\n\n- **Development Guide**: See DEVELOPMENT.md for detailed setup instructions\n- **Contributing Guide**: See CONTRIBUTING.md for contribution guidelines\n- **Discord**: https://discord.gg/MmFkmaJ42D for questions and discussions\n- See `crates/AGENTS.md` for Rust-specific API, repository, and testing conventions\n\n## Important Notes for AI Coding Agents\n\n1. **Always run lints and tests** after making changes\n2. **Use the existing patterns** in the codebase - don't introduce new patterns without discussion\n3. **Keep changes minimal** - surgical changes are preferred over large refactors\n4. **Follow the monorepo structure** - understand which package your change affects\n5. **Check both JS and Rust** - changes to the backend often require frontend updates\n6. **Documentation** - update relevant docs if changing public APIs or user-facing features\n7. **Avoid relative imports** - ESLint will error on relative imports; use package references\n8. **Test on target platform** if making platform-specific changes\n9. **Security**: Check dependencies for vulnerabilities before adding them\n10. **Code marked for refactoring**: Be extra careful with crates in the \"Code Hitlist\" section\n11. **but CLI happy path testing only**: CLI tests are expensive and should be limited to what really matters.\n\n## Review Guidelines\n\nFollow these guidelines when reviewing code.\n\n- Do not report type errors. CI will catch those.\n- Don't review formatting such as commas, indentation, etc\n\n<!-- File: .agents/skills/but-performance-tests/SKILL.md (Tokens: ~383 | Category: anthropic-skill) -->\n---\nname: but-performance-tests\ndescription: Use when creating, changing, running, or debugging shell-based `but` CLI performance scenarios under `crates/but/tests/performance`, including Hyperfine runs, fixture setup, setup-to-test state, and output inspection.\n---\n\n# `but` CLI performance tests\n\nRead [performance framework documentation](../../../crates/but/tests/performance/README.md)\ncompletely before answering questions or changing performance scenarios. Inspect nearby\nscenarios and current shared helpers as applicable; README is canonical source for\nframework behavior, commands, helper APIs, and debugging options.\n\n## Workflow for changes\n\n1. Confirm requested operation and timing boundary.\n2. Inspect closest existing scenarios plus `lib.sh` and `run.sh`.\n3. Add or update only scenario's `setup.sh` and `test.sh` unless shared behavior is\n   genuinely required.\n4. Prefer real, representative GitButler repository state over toy data.\n5. Keep setup, validation, and ID discovery outside timed operation.\n6. Reuse documented helpers instead of creating parallel fixture, state, output, or\n   timing mechanisms.\n7. List every scenario in top-level performance README's included-scenario list, and\n   update its entry when materially changing scenario.\n8. Run README's shell validation and short single-scenario benchmark.\n\nFor usage or debugging questions, answer from current README and scripts. Do not edit\nfiles unless user asks for changes.\n\n## Guardrails\n\n- Keep changes inside `crates/but/tests/performance/` unless task specifically requests\n  related documentation or skill updates.\n- Preserve two-file scenario contract.\n- Do not add custom timing/statistics code; Hyperfine owns measurement.\n- Do not include fixture setup or compilation in measured command.\n- Do not run all scenarios when user requests or changes only one.\n- Report fixture/setup limitations and unavailable validation explicitly.\n\n<!-- File: .agents/skills/cli-commands/SKILL.md (Tokens: ~1806 | Category: anthropic-skill) -->\n---\nname: cli-commands\ndescription: Use when adding or modifying CLI command (`but` commands) under `crates/but/src`.\n---\n\nImagine we're implementing a new `commit3` command. The high-level structure\nfor that must be as follows.\n\n## Arguments\n\nArguments live in `crates/but/src/args`. For our commit command that would be\n`crates/but/src/args/commit3.rs`:\n\n```rust\nuse crate::args::atoms::CliIdArg;\n\n/// Create a commit.\n///\n/// More details about the command here...\n#[derive(Debug, clap::Parser)]\n#[cfg_attr(feature = \"raw-clap-docs\", clap(verbatim_doc_comment))]\n#[deny(missing_docs)]\npub struct Platform {\n    /// The message to use for the commit.\n    #[clap(short, long, group = \"commit_message\")]\n    pub message: Option<Vec<String>>,\n\n    /// Place the commit on the branch `BRANCH`.\n    #[clap(short, long, value_name = \"BRANCH\", group = \"targeting\")]\n    pub branch: Option<Option<CliIdArg>>,\n\n    /// One or more changes to commit.\n    pub changes: Vec<CliIdArg>,\n}\n```\n\nIn `crates/but/src/args/mod.rs`:\n\n```rust\n#[cfg(feature = \"legacy\")]\n#[cfg_attr(feature = \"raw-clap-docs\", clap(verbatim_doc_comment))]\n#[clap(hide = true, name = \"_commit3\")]\nCommit3(commit3::Platform),\n```\n\nThings to note:\n\nArguments that refer to Git objects (such as commits, branches, files, hunks,\netc.) use some type from `crates/but/src/args/atoms/` and not `String` or another\nloose type.\n\nUse `CliIdArg` for arguments that reference existing Git objects such as\nbranches, commits, files, etc. This allows the user to use short IDs or fully\nqualified names.\n\n`String` should only be used for truly loose text input such as commit\nmessages.\n\n`Platform` and all of its fields must have documentation. The `Subcommands`\nvariant intentionally has no doc comment because clap obtains the command\ndocumentation from `Platform`.\n\nUse `#[clap(group = \"...\")]` to create mutually exclusive groups of arguments.\n\nCommands with tricky grammar can define a `pub(crate) const ERROR_EXAMPLES`\nnext to `Platform` and register it in `args::error_examples`; the block is\nappended after clap parse errors. At most 4 lines, each a\n`but <cmd> ...   # what it does` invocation that works as written (e.g.\ninclude `-m` where omitting it would open an editor).\n\n## Handling the command\n\nAdd a match arm to `crates/but/src/lib.rs` to handle the command:\n\n```rust\nmatch cmd {\n    Subcommands::Commit3(commit_args) => {\n        use crate::utils::IntermediateChannel;\n\n        let status_after = args.status_after;\n        let mut ctx = setup::init_ctx(\n            &args,\n            InitCtxOptions {\n                background_sync: BackgroundSync::Enabled { silent: false },\n                ..Default::default()\n            },\n            out,\n        )?;\n        out.begin_status_after(status_after);\n\n        let outcome = command::legacy::commit3::commit(\n            &mut ctx,\n            IntermediateChannel::new(out),\n            commit_args,\n        )\n        .emit_metrics(metrics_ctx)?;\n        out.print_cli_output(outcome)?;\n\n        run_status_after_if_requested(status_after, &mut ctx, out);\n\n        Ok(())\n    }\n\n    // all the other commands...\n}\n```\n\nThings to note:\n\nUse `IntermediateChannel`. Do not pass `OutputChannel` to commands.\n\nUse `OutputChannel::print_cli_output` to print the final output from the\ncommand. This ensures we handle all supported formats. If only human format is\nsupported, use `OutputChannel::print_cli_output_human`.\n\n## Implementing the command\n\nIn `crates/but/src/command/legacy/commit3.rs`\n\n```\npub fn commit(\n    ctx: &mut Context,\n    out: IntermediateChannel<'_>,\n    args: Platform,\n) -> CliResult<CommitOutcome> {\n    // get whatever dependencies we need from `Context` such as\n    // `RepoExclusiveGuard`, `IdMap`, `RefInfo`, etc.\n\n    // resolve the arguments into a `CommitOperation`\n    let commit_operation = resolve(ctx, args)?;\n\n    // Run the operation\n    let outcome = run(ctx, commit_operation)?;\n\n    // Return the outcome which will be printed by the caller\n    Ok(outcome)\n}\n\nfn resolve(ctx: &mut Context, args: Platform) -> CliResult<CommitOperation> {\n    let Platform { message, branch, changes } = args;\n    // ...\n}\n\nfn run(ctx: &mut Context, commit_op: CommitOperation) -> anyhow::Result<CommitOutcome> {\n    match commit_op {\n        // ...\n    }\n}\n\n#[must_use]\nstruct CommitOutcome {\n    new_commit: ObjectId,\n}\n\nimpl CliOutputHuman for CommitOutcome {\n    fn on_human(self, out: &mut dyn WriteWithUtils, _theme: &Theme) -> anyhow::Result<()> {\n        let Self { new_commit } = self;\n\n        writeln!(\n            out,\n            \"Created commit {}\",\n            theme::Commit(new_commit, None),\n        )?;\n\n        Ok(())\n    }\n}\n\nimpl CliOutput for CommitOutcome {\n    fn on_shell(self, out: &mut dyn WriteWithUtils) -> anyhow::Result<()> {\n        let Self { new_commit } = self;\n\n        writeln!(out, \"{}\", new_commit.to_hex_with_len(7))?;\n\n        Ok(())\n    }\n\n    fn on_json(self) -> impl serde::Serialize {\n        #[derive(Serialize)]\n        struct Output {\n            commit: HexHash,\n        }\n\n        let Self { new_commit } = self;\n\n        Output { commit: new_commit.into() }\n    }\n}\n```\n\nThings to note:\n\nCommands follow a `resolve` then `run` structure.\n\n`run` doesn't print its final output. It returns something that implements\n`CliOutput` / `CliOutputHuman` which the caller can then print.\n\n`resolve` returns `CliResult` because it needs to reject bad user input.\n\n`run` returns `anyhow::Result` because it can only hit internal errors. Bad\nuser input is handled by `resolve`.\n\n`resolve` translates CLI arguments into domain targets and validates user\ninput. It may query repository state to disambiguate or reject input, but\nshould not retain derived data in the operation.\n\n`run()` loads current repository state and computes results. Commit counts,\ndiffs, statistics, branch details, current tips, and workspace projections\ngenerally belong in `run()`. State derivable from those identifiers and the\ncontext belongs in `run()`.\n\nThe operation should not contain types from `crate::args::atoms`.\n\nA non-CLI caller such as the TUI should be able to construct an operation from\ndomain identifiers without reproducing repository queries from `resolve()`. If\nit must compute counts, diffs, or branch metadata to build the operation, those\nfields probably belong in `run()`.\n\nThe operation does not contain diff specs (usually in the form\n`Vec<DiffSpec>`). It should instead contain `CliId`s which `run` turns into\n`Vec<DiffSpec>` using `DiffSpecBuilder`. This makes it easier for the status\nTUI to call `run` directly.\n\nUser-input pickers and prompts are created via `InputOutputChannel`\naccessed through `IntermediateChannel::prepare_for_terminal_input`.\n\nThe JSON output includes both commit IDs and change IDs.\n\nUse `AllowMergedArg` and `MergedUpstream` to verify we don't mutate merged\ncommits and branches.\n\nPrinting of commits, branches, change IDs, etc. uses newtypes from `theme` such\nas `theme::Branch` and `theme::Commit`. This ensures consistent coloring.\n\nBad user input errors use `bad_input(...)`, optionally with `.arg_name()`,\n`.arg_value()`, and `.hint()`.\n\nAvoid making the operation or outcome types implement `serde::Serialize`.\nDefine the specific types needed inside `fn on_json`. This makes it harder to\nbreak compatibility by accident. Reusing `Serialize` from existing domain types\nis fine.\n\n## More examples\n\nFor examples of this structure in practice see\n\n- `crates/but/src/command/legacy/commit.rs`\n- `crates/but/src/command/legacy/move.rs`\n- `crates/but/src/command/legacy/squash.rs`\n- `crates/but/src/command/legacy/diff.rs`\n\n<!-- File: .agents/skills/lite-render-perf/SKILL.md (Tokens: ~2531 | Category: anthropic-skill) -->\n---\nname: lite-render-perf\ndescription: Use when working on React code in `apps/lite` that derives values during render, adds or consumes a context, subscribes to the redux store or a query, or renders lists of row components — and whenever the lite UI is reported slow, laggy, or re-rendering too much. React Compiler does not prevent these regressions; load this skill BEFORE writing such code, not only when debugging it.\n---\n\nThe recurring render-performance failure in this app is accidental\nover-re-rendering: one interaction re-renders every row instead of the few\naffected ones. Repositories can have hundreds of branch rows, so this turns\nsingle-digit-millisecond interactions into triple-digit ones. The compiler\nmakes it easy to cause by accident because it silently stops memoizing\nexactly where you assume it won't.\n\n## The three mechanisms behind past regressions\n\n### 1. Whether the compiler memoizes a derivation is not predictable from the source\n\nThe compiler only caches what it can prove safe to cache, and it declines\nmore often than you would guess. Two derivations in this app are both\nmodule-level functions that call imported helpers, and they compile\ndifferently: `buildOutlineNavigationIndex` in `WorkspacePage.tsx` gets a\nmemo block keyed on its inputs, while `getDiffView` in `Details.tsx` got\nnone and re-ran on every render of a component that re-renders whenever the\nfile selection changes.\n\nUnmemoized inputs are contagious. `getDiffView` was called with values\nbuilt by `Match` pipes that the compiler had not memoized either; feeding\nit stable values instead was enough to make the memo block appear. So the\nchurn spreads outward from whichever value first fails to get a scope,\nwhich is why one unmemoized derivation can quietly cost far more than\nitself.\n\nEither way the effect is the same: the derivation re-runs and returns fresh\nidentities even when its data never changed, and everything keyed on those\nidentities — context values, props, effect deps — churns with it.\n\nWhat to do instead:\n\n- Derive query data inside the query's `select`. react-query caches the\n  result on `(data, select function identity)`, and the compiler DOES\n  memoize the closure on its captures — so the closure's identity becomes\n  exactly the dependency key you want. Extra inputs are simply captured by\n  the closure. Precedent: `useBranchesOutline.ts`. `useQueries` /\n  `useSuspenseQueries` give you the same lever in `combine` — precedent:\n  `lineStats` in `OutlineTree/UncommittedChangesRow.tsx`, which folds the\n  results down inside `combine` instead of mapping them out and deriving in\n  render the way `Details.tsx` does. Check what the closure captures:\n  capture a value that is itself rebuilt each render and you have only\n  moved the churn one level up.\n- For redux-derived collections, use `createSelector` (see\n  `projects/project.ts`), never a plain function returning a fresh array or\n  object.\n- Never conclude from the source that something is or is not memoized —\n  read the compiler's output. A compiled function imports\n  `react/compiler-runtime` and allocates slots with `const $ = _c(n)`; a\n  memoized value is assigned inside an `if ($[i] !== dep)` block, and its\n  dependencies are exactly the values that block tests. A derivation\n  sitting outside every such block runs on every render. Two ways to look:\n  fetch the vite-transformed module from the running app\n  (`fetch('/src/path/to/file.ts').then(r => r.text())` over CDP — see\n  below), or run the plugin offline on any file, which needs no app:\n\n```js\n// Resolve both from node_modules/.pnpm — pnpm does not hoist them. There is\n// no @babel/preset-typescript here, so strip types via parserOpts instead.\nawait babel.transformAsync(source, {\n\tfilename: file,\n\tparserOpts: { plugins: [\"typescript\", \"jsx\"] },\n\tplugins: [[\"babel-plugin-react-compiler\", { target: \"19\" }]],\n\tconfigFile: false,\n\tbabelrc: false,\n});\n```\n\n### 2. Context re-renders every consumer; it has no selector\n\n`use(SomeContext)` re-renders the component whenever the context value's\nidentity changes — there is no way to subscribe to a slice, and neither the\ncompiler nor `React.memo` can bail a context read out. Note this is a\ndifferent failure from mechanism 1: when the value's content legitimately\nchanged, fixing identities cannot help — the only cure is consumers\nsubscribing to something narrower than the whole value.\n\nWhat to do instead:\n\n- Context is for values that change rarely relative to their consumers.\n- Per-row state must be a narrow `useAppSelector` returning a primitive\n  (usually a boolean). Redux subscriptions only re-render when the selected\n  value changes, so a selection change re-renders two rows, not all of them:\n\n```tsx\nconst isSelected = useAppSelector((state) => {\n\tconst stored = projectSlice.selectors.selectPrimaryOutlineSelection(state, projectId);\n\treturn stored !== null && operandEquals(stored, operand);\n});\n```\n\nFor the current shape, see `useIsSelected` in `BranchesList.tsx` and the\n`selectIsSelectedOutline` subscription in `OutlineTree/ItemRow.tsx`.\n\n- If rows need a value that requires resolution against a big object,\n  resolve it once at the top and store the resolved form, rather than\n  letting every row resolve independently.\n\n### 3. Identity discipline in the store and in query results\n\n- Reducers must early-return when the update is a no-op, otherwise they\n  produce a new state identity and wake every subscriber.\n- Reading store data inside an event callback needs no subscription at all —\n  take `const store = useAppStore()` at the top and call `store.getState()`\n  in the callback, instead of subscribing with `useAppSelector` to a value\n  only the callback reads.\n- Destructure `useMutation` results (`const { isPending, mutate } = ...`) —\n  the top-level result object is a new identity every render\n  (`@tanstack/query` `no-unstable-deps`).\n- Hook call order can change what the compiler memoizes; moving plain\n  `useQuery` calls above a `useQueries` call has fixed memoization here\n  before. If the compiled output shows a value outside every memo slot for\n  no visible reason, try reordering the hooks around it.\n\n## Driving the dev app over CDP\n\n`pnpm dev` in `apps/lite` starts vite (port 5173) and Electron with\n`--remote-debugging-port=9222`. The first run compiles a helper binary with\ncargo and takes minutes; `Port 5173 is already in use` means an instance is\nalready running — reuse it, or `lsof -ti tcp:5173 | xargs kill`. React and\nRedux DevTools are auto-installed in dev builds.\n\n`curl http://127.0.0.1:9222/json` lists debug targets. Pick the entry with\n`\"type\": \"page\"` whose URL is on 5173 — the others are extension service\nworkers. Its `webSocketDebuggerUrl` speaks the Chrome DevTools Protocol; a\nfew lines of Node (global `WebSocket`) are enough to drive it:\n\n```js\n// Usage: node cdp.mjs eval \"<expr>\"   or:   node cdp.mjs shot out.png\nconst page = (await (await fetch(\"http://127.0.0.1:9222/json\")).json()).find(\n\t(t) => t.type === \"page\" && t.url.includes(\"5173\"),\n);\nconst ws = new WebSocket(page.webSocketDebuggerUrl);\nlet id = 0;\nconst pending = new Map();\nconst send = (method, params) =>\n\tnew Promise((res) => {\n\t\tpending.set(++id, res);\n\t\tws.send(JSON.stringify({ id, method, params }));\n\t});\nws.onmessage = (e) => {\n\tconst m = JSON.parse(e.data);\n\tpending.get(m.id)?.(m.result);\n};\nawait new Promise((res) => (ws.onopen = res));\nconst [mode, arg] = process.argv.slice(2);\nif (mode === \"eval\")\n\tconsole.log(\n\t\tJSON.stringify(\n\t\t\tawait send(\"Runtime.evaluate\", {\n\t\t\t\texpression: arg,\n\t\t\t\tawaitPromise: true,\n\t\t\t\treturnByValue: true,\n\t\t\t}),\n\t\t),\n\t);\nelse if (mode === \"shot\") {\n\tconst { data } = await send(\"Page.captureScreenshot\", { format: \"png\" });\n\t(await import(\"node:fs\")).writeFileSync(arg, Buffer.from(data, \"base64\"));\n}\nprocess.exit(0);\n```\n\nUseful from `Runtime.evaluate`:\n\n- The whole preload API is callable: `window.lite.branchList(projectId)`\n  etc. — the fastest way to inspect real API payloads and sizes.\n- Interact by dispatching events on DOM nodes found via `aria-label`s;\n  screenshot to verify visually.\n\nGotchas:\n\n- Repeated evaluates share one top-level scope: wrap every expression in a\n  `{ ... }` block or `const x` collides with the previous call\n  (\"Identifier 'x' has already been declared\").\n- Synthetic `MouseEvent`s do not move real focus; behavior wired to\n  `onFocus` (selection scopes) needs an explicit `.focus()` or real input.\n- Renderer edits arrive via HMR, but edits to store/redux modules trigger a\n  full page reload that wipes `window.*` state — re-arm any probes after.\n  Electron main-process edits are only compiled at startup: restart\n  `pnpm dev`.\n\n## Measuring: prove it, in both directions\n\nReact DevTools being installed means its hook is available in-page:\n\n1. Arm a commit logger and interact:\n\n```js\nconst hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;\nwindow.__log = [];\nconst orig = hook.onCommitFiberRoot.bind(hook);\nhook.onCommitFiberRoot = (id, root, ...rest) => {\n\twindow.__log.push({ dur: root.current.actualDuration });\n\treturn orig(id, root, ...rest);\n};\n```\n\n2. Judge by `actualDuration` per commit: an interaction should cost\n   single-digit milliseconds and re-render O(affected rows). A commit whose\n   duration scales with total row count is the signature of a blast.\n3. To find WHY, read the compiled module (mechanism 1 above). To watch a\n   specific context value's identity across commits, extend the logger to\n   walk `root.current` for the provider fiber (its `type` is the context\n   object, so match on `type.displayName`) and compare `memoizedProps.value`\n   against the previous commit's. Do NOT add `window.*` probe writes inside\n   components or hooks to trace identities — that mutation makes the\n   compiler bail out of the whole function and destroys the very memoization\n   you are measuring.\n4. Restore `onCommitFiberRoot` when done; measure before and after the fix.\n\n## Review checklist\n\n- Any non-trivial derivation in render: is its result's identity\n  load-bearing (context value, prop to many children, effect dep)? Move it\n  into `select`/`combine`/`createSelector`, or read the compiled output to\n  prove it harmless — do not assume the compiler handled it.\n- Any new context: how often does its value change, and how many consumers?\n- Any `useAppSelector`: does it return a primitive or a stored reference,\n  not a fresh object?\n- Any new reducer: does it no-op cleanly when the value is unchanged?\n\n<!-- File: .agents/skills/lite-screenshots/SKILL.md (Tokens: ~3797 | Category: anthropic-skill) -->\n---\nname: lite-screenshots\ndescription: Use when asked to add before/after screenshots of a Lite UI change to a pull request, when a PR touching apps/lite/ui needs its visual change shown, or when extending the screenshot catalogue in apps/lite/e2e/tests/screenshots.spec.ts. Captures both sides locally against seeded fixtures, publishes the surfaces that changed, and posts them to the PR.\n---\n\nA reviewer reading a CSS diff cannot see the result. This captures the Lite UI\nwith the branch applied and again with it unapplied, and posts the surfaces that\ndiffer to the pull request.\n\nCapture runs **on this machine**, not in CI. It was tried in CI and abandoned:\nunder the bare X server on the Linux runners this Electron build paints a frame\non load and not reliably afterwards, so several surfaces hung indefinitely\nwhether the capture went through Playwright or the DevTools protocol. The same\nspec passes here. The CI workflow only leaves a reminder.\n\n## Before capturing: is the surface covered?\n\nWork this out first, by reading the diff — not by capturing and inferring it from\nthe result. A run that reports \"8 surfaces, 0 changed\" looks identical whether the\nchange is invisible or whether nothing looked at it.\n\n1. Identify which screen the change affects.\n2. Check it against the catalogue in `apps/lite/e2e/tests/screenshots.spec.ts`\n   (currently: workspace sidebar, diff pane, branches tab, upstream tab, project\n   picker, uncommitted rows, commit form, settings).\n3. **Covered** — capture, as below.\n4. **Not covered** — stop and ask the developer, offering the two lanes below.\n   Either way it is their call, not one to make quietly inside a screenshot\n   request. Tell them which screen is uncovered and which fixture would reach it.\n   - **Add it to the catalogue** when the gap is a _screen_ — something anyone\n     working nearby will change again. It edits a shared file and becomes\n     permanent coverage for every later pull request. See \"Extending the\n     catalogue\" below.\n   - **Capture it ad hoc** when the gap is a _state_ — a conflicted commit\n     mid-resolution, an empty list, one specific dialog — that no future pull\n     request is likely to want. Nothing shared changes and nothing is left\n     behind. See \"One-off capture\" below.\n\n   Prefer the catalogue when it is a close call. The ad-hoc lane photographs the\n   change once and protects nothing afterwards, and a catalogue that stops growing\n   makes every later request re-derive the same fixtures.\n\nIf they decline both, **do not post a comment**. A normal comment would report\n\"N surfaces, 0 changed\", which asserts the change is not visual when in truth it\nwas never photographed. Say plainly that the affected screen is not in the\ncatalogue and no screenshots were taken.\n\n## Changes the capture cannot see\n\nSome diffs are invisible to this harness for reasons that have nothing to do with\nthe change being non-visual. Rule these out before reading a \"0 changed\" result\nas an answer:\n\n- **Scrollbars and their gutter.** macOS hides scrollbars unless a mouse is\n  attached, and a styled `::-webkit-scrollbar` does _not_ override that — the\n  scroller simply reserves no gutter. On a machine set to \"When scrolling\", any\n  change to the track, thumb, gutter width, or a separator meeting the gutter\n  captures identically on both sides. Set System Settings → Appearance → Show\n  scroll bars → **Always** for the run, and say in the comment that you did.\n- **Hover, focus, and drag states** are not entered by the spec.\n- **Anything behind a scroll position** — surfaces are captured at the top.\n\n## Prerequisites\n\n- `cargo build -p but` once, giving `target/debug/but`.\n- The branch under test applied, and **not yet merged** — the base side comes\n  from unapplying it. Check `but status` first.\n- **Run every capture through `env -u ELECTRON_RUN_AS_NODE`.** An agent hosted\n  inside VS Code (itself Electron) inherits `ELECTRON_RUN_AS_NODE=1`, which makes\n  the Electron the test launches behave as plain Node. Every surface then fails\n  with `Error: Process failed to launch!` and nothing says why — the binary is\n  fine, and running it by hand from another shell works. Check with\n  `env | grep ELECTRON`.\n\n## The flow\n\nRun every command from the repository root.\n\n### 1. Capture the branch as it stands\n\n```console\n$ env -u ELECTRON_RUN_AS_NODE SCREENSHOT_OUT=head BUT=$PWD/target/debug/but \\\n    pnpm -F @gitbutler/lite test:e2e screenshots.spec.ts\n```\n\nOutput lands in `apps/lite/e2e/screenshots/head/` (gitignored).\n\n### 2. Capture the base\n\n```console\n$ but unapply <branch>\n$ but status        # confirm the branch is gone from the workspace\n$ env -u ELECTRON_RUN_AS_NODE SCREENSHOT_OUT=base BUT=$PWD/target/debug/but \\\n    pnpm -F @gitbutler/lite test:e2e screenshots.spec.ts\n$ but apply <branch>\n```\n\n**Check the unapply took effect.** It silently does nothing on a stale id or an\nalready-merged branch, and both runs then capture identical code — which looks\nexactly like \"this change is not visual\". If every pair matches in step 3, assume\nthis went wrong before concluding anything about the change.\n\nRe-apply immediately, before anything else can fail and leave the workspace short\na branch.\n\n**Stacked branches take the whole stack down.** `but unapply` on a branch that is\npart of a stack unapplies every branch in it, including ones this capture depends\non — unapplying a stacked change also removes the spec, and the base run then has\nno tests to execute. Check `but status` first. When the branch is stacked, capture\nthe base by reverting the change in the working tree instead:\n\n1. Edit the changed files back to their pre-change state by hand.\n2. Capture into `base`.\n3. `but discard <file-id>` to restore the committed version.\n\nConfirm the restore: the change must be back before anything else happens.\n\n### 3. Select what changed\n\n```console\n$ node apps/lite/e2e/compare-screenshots.mjs \\\n    apps/lite/e2e/screenshots/base \\\n    apps/lite/e2e/screenshots/head \\\n    /tmp/publish \\\n    \"https://raw.githubusercontent.com/<owner>/<repo>/pr-screenshots/pr-<number>/<short-sha>\" \\\n    /tmp/section.md\n```\n\nIt prints `total=`, `changed=`, `added=`, stages only the differing pairs into\n`/tmp/publish`, and writes the comment body. Surfaces that did not change are\nbyte-identical, so they are folded into a collapsed list rather than shown.\n\n### 4. Publish the images\n\nImages go on an orphan `pr-screenshots` branch, under `pr-<number>/<short-sha>/`.\nA per-commit directory matters: reusing one path lets GitHub's image proxy serve\nthe previous run's screenshots from cache, which reads as \"my change did nothing\".\n\nWrite them with the git data API rather than a local clone. There is nothing to\nclone, nothing to clean up, and no local `git` writes — which agents are often\nnot permitted to make outside the project, and which cannot touch the GitButler\nworkspace by accident.\n\n```console\n$ REPO=<owner>/<repo>; DIR=pr-<number>/<short-sha>\n$ HEAD_SHA=$(gh api repos/$REPO/git/refs/heads/pr-screenshots --jq '.object.sha')\n$ BASE_TREE=$(gh api repos/$REPO/git/commits/$HEAD_SHA --jq '.tree.sha')\n\n# One blob per image. base64 must be stripped of newlines.\n$ jq -n --rawfile c <(base64 -i /tmp/publish/<name>.png) \\\n      '{content: ($c | gsub(\"\\n\";\"\")), encoding: \"base64\"}' > /tmp/blob.json\n$ BLOB=$(gh api -X POST repos/$REPO/git/blobs --input /tmp/blob.json --jq '.sha')\n\n# One tree and one commit for all of them, then move the ref.\n$ jq -n --arg t \"$BASE_TREE\" --arg b \"$BLOB\" --arg d \"$DIR\" '{base_tree: $t, tree: [\n      {path: ($d + \"/<name>.png\"), mode: \"100644\", type: \"blob\", sha: $b}]}' > /tmp/tree.json\n$ TREE=$(gh api -X POST repos/$REPO/git/trees --input /tmp/tree.json --jq '.sha')\n$ jq -n --arg t \"$TREE\" --arg p \"$HEAD_SHA\" \\\n      '{message: \"Screenshots for #<number>\", tree: $t, parents: [$p]}' > /tmp/commit.json\n$ COMMIT=$(gh api -X POST repos/$REPO/git/commits --input /tmp/commit.json --jq '.sha')\n$ gh api -X PATCH repos/$REPO/git/refs/heads/pr-screenshots -f sha=\"$COMMIT\"\n```\n\nWhen the branch does not exist yet, omit `base_tree` and `parents`, then create\nthe ref with `POST git/refs -f ref=refs/heads/pr-screenshots -f sha=$COMMIT`.\n\n**Fetch one raw URL before posting.** A comment full of broken images is worse\nthan no comment, and the failure is invisible until someone opens the pull\nrequest:\n\n```console\n$ curl -s -o /dev/null -w '%{http_code}\\n' \\\n    https://raw.githubusercontent.com/<owner>/<repo>/pr-screenshots/<dir>/<name>.png\n```\n\n### 5. Post to the pull request\n\nUpsert a single comment rather than stacking one per run — find a previous\ncomment starting with `<!-- lite-screenshots -->` and PATCH it, else POST.\n\n```console\n$ jq -Rs '{body: .}' /tmp/section.md > /tmp/payload.json\n$ gh api \"repos/<owner>/<repo>/issues/<number>/comments\" --paginate \\\n    --jq 'map(select(.body | startswith(\"<!-- lite-screenshots -->\"))) | first | .id // empty'\n$ gh api -X PATCH \"repos/<owner>/<repo>/issues/comments/<id>\" --input /tmp/payload.json\n```\n\nDo not edit the pull request description: rewriting text a human owns risks\nclobbering their concurrent edits.\n\n### 6. Add the label\n\n`screenshots` is the record that they were posted. Add it once the comment is\nup, so the label says what is true:\n\n```console\n$ gh pr edit <number> --remove-label \"screenshots needed\" --add-label \"screenshots\"\n```\n\nCI labels any pull request with CSS in it `CSS`, and asks one that touches\nLite's for screenshots only if the change is visible; leave that label alone,\nit describes the diff.\n`screenshots needed` is one a human applies by hand when they do want them, and\nthe command above clears it if it is there.\n\nOnly after a comment with images actually went up. A pull request labelled\n`screenshots` with none attached is worse than an unlabelled one — it tells a\nreviewer the change has been shown when it has not. If you declined to post (an\nuncovered surface, a capture that failed), leave the labels as they are.\n\nScreenshots are asked for once per pull request and never again, so the label\nis final — a later push does not undo it, and a set posted here is not a claim\nabout code pushed after it. Recapture when the surfaces move; the command above\nis idempotent either way.\n\nIf you ever post images that are **not** captures of the running app — a page\nrendered from the branch's CSS, say, when the harness cannot be run — label them\nas such in the comment and mark it `<!-- lite-screenshots-replica -->` instead.\nA reviewer assumes a screenshot is a photograph of the app; anything else has to\nsay so, and the different marker keeps a later real run from overwriting it.\n\n## Before finishing\n\n- **Look at the images**, do not just trust exit codes. A green run proves files\n  were written, not that they show the surface.\n- Confirm the workspace is whole: `but status` should show the branch applied and\n  no stray uncommitted changes.\n\n## One-off capture\n\nFor a state the catalogue should not carry permanently. The run command filters\nby filename, so any spec whose name contains `screenshots` is picked up by the\nsame command and writes into the same `SCREENSHOT_OUT` directory — the compare\nstep cannot tell the difference.\n\nWrite `apps/lite/e2e/tests/screenshots-adhoc.spec.ts`, importing the shared\nhelpers rather than copying them (every workaround in them was earned, and a copy\nloses the comments explaining why):\n\n```ts\nimport { enabled, openProject, shoot } from \"../screenshot-helpers.ts\";\nimport { test } from \"../test.ts\";\n\ntest.describe(\"screenshots\", () => {\n\ttest.skip(!enabled, \"set SCREENSHOT_OUT to capture screenshots\");\n\ttest.describe.configure({ timeout: 180_000 });\n\ttest.use({ scenario: \"project-in-single-branch-three-branch-stack.sh\" });\n\n\ttest(\"<surface>\", async ({ appWindow }) => {\n\t\tawait openProject(appWindow);\n\t\t// ...reach the state...\n\t\tawait shoot(appWindow, \"<surface-name>\", \"<selector>\");\n\t});\n});\n```\n\nThe same fixture and selector rules as the catalogue apply — see below.\n\nThen capture both sides as usual, and:\n\n- **Delete the file** once the base run is done. It is untracked and not ignored,\n  so left behind it shows up as a stray change in `but status` and eventually in\n  someone's commit. The \"confirm the workspace is whole\" check at the end is what\n  catches this.\n- **Say in the comment** that the surface was captured ad hoc and is not covered\n  going forward, so nobody reads it as a regression guarantee.\n\n**Never run the two specs in one command.** The config sets `fullyParallel`, and\nthe catalogue spec wipes `SCREENSHOT_OUT` in a `beforeAll` — run concurrently, that\nwipe lands in the middle of the ad-hoc capture and deletes it. What survives is a\nplausible-looking directory missing exactly the surface you were asked for.\n\nThe trailing argument is a **substring match on the file path**, so a bare\n`screenshots` selects the ad-hoc spec as well and puts you straight into that race.\nName the file: `screenshots.spec.ts` for the catalogue, `screenshots-adhoc` for the\nad-hoc spec. Steps 1 and 2 above already do.\n\nCapture the ad-hoc surface **alone**, filtering to its filename, into its own\noutput directory on each side:\n\n```console\n$ env -u ELECTRON_RUN_AS_NODE SCREENSHOT_OUT=adhoc-head BUT=$PWD/target/debug/but \\\n    pnpm -F @gitbutler/lite test:e2e screenshots-adhoc\n```\n\nIts own directory also avoids inheriting stale PNGs from an earlier catalogue run,\nsince only the catalogue spec cleans up after itself. Then compare\n`adhoc-base` against `adhoc-head` as in step 3.\n\nWhen a change touches both a covered screen and an uncovered state, run the\ncatalogue first and the ad-hoc spec second, in separate commands, pointing both at\nthe same directory — that order is safe because only the first one wipes.\n\n## Extending the catalogue\n\nOnly after the developer has agreed (see the top of this document). Add the\nsurface to `apps/lite/e2e/tests/screenshots.spec.ts`, then capture as usual —\nthe same run proves the new surface works and shows the change. Coverage applies\nto every later pull request, not just this one.\n\n```ts\ntest.describe(\"<area>\", () => {\n\ttest.use({ scenario: \"<fixture>.sh\" });\n\n\ttest(\"<surface>\", async ({ appWindow }) => {\n\t\tawait openProject(appWindow);\n\t\tawait goToTab(appWindow, \"branches\"); // when it lives on another tab\n\t\tawait shoot(appWindow, \"<surface-name>\", \"<selector>\");\n\t});\n});\n```\n\n- `<surface-name>` becomes the filename and the heading a reviewer reads: name it\n  `commit-form`, not `outline-panel-2`.\n- Only fixtures calling `\"$BUT\" setup` register a project; the rest leave the app\n  on \"Select a project.\" and every capture fails. Known good:\n  `project-in-single-branch-three-branch-stack.sh`,\n  `project-with-remote-branches.sh`, `project-with-conflicting-commits.sh`.\n- If no fixture reaches the state, build it in the test — `uncommitted` writes\n  files into `testEnvironment.workdir/local-clone` and reloads.\n- Prefer a stable id for the clip (`#outline-panel`, `#details-panel`), else a\n  CSS-module prefix or an ARIA hook. Never nth-child or a generated class.\n- One screenshot per test, taken after a fresh load. This costs nothing here and\n  keeps the spec usable if CI capture is ever revisited.\n\n## Out of scope\n\n- **Native context menus** cannot be captured; they are OS windows.\n- **Dark mode** is not covered — the harness seeds `theme: \"light\"`.\n- **States needing a real remote or credentials** cannot be fixtured.\n\n<!-- File: .agents/skills/tui-tests/SKILL.md (Tokens: ~693 | Category: anthropic-skill) -->\n---\nname: tui-tests\ndescription: Use when adding or modifying tests for one of GitButler's Ratatui TUIs\n---\n\n## Where tests live\n\n- Main TUI tests: `crates/but/src/command/legacy/status/tui/tests/`\n- Test harness/helpers: `crates/but/src/tui/test_utils.rs` and `crates/but/src/command/legacy/status/tui/tests/utils.rs`\n- Snapshots: `crates/but/src/command/legacy/status/tui/tests/snapshots/`\n\n## Basic pattern\n\n```rust\n#[test]\nfn describes_behavior_under_test() {\n    let env = Sandbox::init_scenario_with_target_and_default_settings(\"one-stack\").unwrap();\n    env.setup_metadata(&[\"A\"]).unwrap();\n\n    let mut tui = test_status_tui(env);\n\n    tui.input_then_render(KeyCode::Down)\n        .assert_rendered_term_svg_eq(file![\"snapshots/describes_behavior_under_test_001.svg\"]);\n}\n```\n\n## Driving the TUI\n\nUseful input examples:\n\n```rust\ntui.input_then_render(None);                                       // render without inputs\ntui.input_then_render('j');                                        // single char input\ntui.input_then_render(KeyCode::Down);                              // special key\ntui.input_then_render(Shift('j'));                                 // keys with shift\ntui.input_then_render(Control('j'));                               // keys with control\ntui.input_then_render([KeyCode::Down, KeyCode::Down]);             // multiple keys from array\ntui.input_then_render(\"commit message text\");                      // multiple keys from string\ntui.reload();                                                      // reload state after making external changes\n```\n\n## Assertions\n\nGenerally prefer\n\n- `assert_current_line_eq(str![...])` for cursor/selection behavior.\n- `assert_rendered_term_svg_eq(file![\"snapshots/test_function_name_001.svg\"])`\n  for everything else.\n\nGenerally you should include one `assert_rendered_term_svg_eq` per logical\ngroup of inputs, to catch bad states early.\n\nBe careful using `assert_rendered_contains` and `assert_rendered_not_contains`\nsince they might lead to false positives. They're intended to use while\niterating on a test where snapshots would cause too much churn.\n\nRead `crates/but/src/tui/test_utils.rs` and\n`crates/but/src/command/legacy/status/tui/tests/utils.rs` for more specialized\nassertions.\n\nYou're not allowed to add new kinds of assertions to `crates/but/src/tui/test_utils.rs` or\n`crates/but/src/command/legacy/status/tui/tests/utils.rs`. Rely entirely on the\nexisting assertions.\n\nDon't reuse snapshots from other tests. Give each test its own snapshots.\n\nAvoid using `tui.env().invoke_git(\"...\")`. Prefer performing operations via the TUI.\n\n## Running tests\n\n- `cargo test -p but <test-name>` to run one test.\n- `SNAPSHOTS=overwrite cargo test -p but <test-name>` to run and update\n  snapshots.\n- `cargo test -p but tui` to run all tui tests. Do this after changing\n  things.\n\nIf a test fails the output will include the rendered state of the test backend.\nThis can be used when iterating on a test as a way of inspecting the state.\n\n<!-- File: crates/but-agentlog/skill/SKILL.md (Tokens: ~1639 | Category: anthropic-skill) -->\n---\nname: but-agentlog\nversion: 0.0.0\ndescription: \"Use for prompts like \\\"get context for branch\\\", \\\"catch up on this branch\\\", \\\"recover branch context\\\", or \\\"what prior agent work happened here\\\". Skim prior agent work from `but agentlog` captures for a GitButler branch, review / pull request / merge request, or change. Prefer this over generic git branch/diff inspection when the user asks for context, history, prior work, or branch catch-up.\"\nauthor: GitButler Team\n---\n\n# GitButler Agentlog Skill\n\nUse `but agentlog` to skim prior agent work from captured sessions.\n\nTrigger this skill for plain prompts like \"get context for branch\", \"branch\ncontext\", \"catch up on this branch\", \"recover context\", \"what happened on this\nbranch\", or \"what prior agent work happened here\".\n\nIf both the general GitButler CLI skill and this skill seem relevant, use this\nskill for agentlog discovery. Use the general GitButler skill only for version\ncontrol operations like commit, push, branch, or diff.\n\n## Core Workflow\n\nDo not search for or read local `SKILL.md` files. If this skill is active, use\nthese instructions directly. Do not inspect the general GitButler skill first.\nStart with `but agentlog skim`.\n\nStart with `skim` when prior work could affect your next action. It is a\ntable-of-contents view: all related sessions and turns, abbreviated and shown\nchronologically.\n\n```sh\nbut agentlog skim\nbut agentlog skim branch <branch-name-or-ref>\nbut agentlog skim review <review-id-or-pull-request-key>\nbut agentlog skim change <change-id-or-key>\n```\n\nUse `show` only when `skim` is too thin, ambiguous, or you need exact evidence:\n\n```sh\nbut agentlog show <session-key> --limit 20\nbut agentlog show <session-key> --turn <turn-key> --limit 20\n```\n\nFor normal current-branch recovery, run `skim` without target arguments first.\nIt discovers the applied GitButler branch itself. Do not run plain Git commands\nlike `git branch --show-current` or `git status` first.\n\n```sh\nbut agentlog skim\n```\n\nPass an explicit target only when the user gives one.\n\nUse JSON only when you need exact handles for drill-down:\n\n```sh\nbut --json agentlog skim\n```\n\n1. Start with `skim` for a clean orientation.\n2. Treat `skim` like compacted turn history: all related sessions and turns are\n   present, but each turn is abbreviated. It is not the full transcript.\n3. If `skim` is enough for a lightweight status answer, summarize it and stop.\n4. Drill down only when `skim` is ambiguous, misses the rationale, or you need\n   exact evidence.\n5. To drill down, rerun `skim` with `--json` to get the relevant\n   `session_key` and `turn_key`, then use `show`.\n6. Use `show <session-key>` for turn-level context.\n7. Use `show <session-key> --turn <turn-key>` only for turns that need exact detail.\n\n## Target Discovery\n\nWhen the user says \"current branch\" or gives no explicit target, run:\n\n```sh\nbut agentlog skim\n```\n\n`skim` performs GitButler branch discovery automatically. If target discovery\nfails, then inspect GitButler state:\n\n```sh\nbut status\n```\n\nDo not use plain Git's `gitbutler/workspace` branch as an agentlog target.\n\n## Rules\n\n- Prefer orientation before payload.\n- Prefer `skim` for turn-history recovery.\n- Treat `skim` as complete but abbreviated. It includes every related session\n  and every turn in those sessions, not every record or the full transcript.\n- Prefer human `skim` output first. Use `--json` only for drill-down\n  handles or exact evidence.\n- Run `show` when `skim` is thin, ambiguous, missing the why, or when the user\n  is asking you to make or verify a consequential claim.\n- Do not dump records for every candidate session.\n- Do not dump full records or transcripts. If session detail is needed, start\n  with `show <session-key> --limit 20` and summarize previews.\n- Treat matches as related evidence, not ownership.\n- Keep `session_key` and `turn_key` values internal unless the user asks for\n  evidence handles or they are needed for a follow-up command.\n- Use record snippets only when they justify the next action.\n- Say \"session related to branch X via turn Y\", not \"session for branch X\".\n\n## Reading `skim`\n\nUse `skim` to answer \"what has already been discussed, planned, attempted, or\nchanged in this work?\"\n\nHuman output is intentionally compact but includes every related session and\nevery turn in chronological order. JSON output keeps\nfull `session_key` and `turn_key` values so you can drill into `show` without\nprinting those handles to the user.\n\nThe skim is a compressed table of contents. It abbreviates each turn but does\nnot omit related sessions or turns.\n\nUseful fields:\n\n- `target_kind` and `target_key`: target used for discovery.\n- `sessions`: related sessions in chronological order, with counts and previews.\n- `coverage`: shown sessions, shown turns, and direct related turn count.\n- `sessions[].turns`: every turn in order, with labels and previews.\n\nIf `skim` has enough context for a lightweight answer, summarize from it and\nstop. If it is ambiguous or misses the why, use JSON skim handles and drill\ndown with `show`.\n\n## Reading `show`\n\nUse `show` to open a session or one turn.\n\n```sh\nbut agentlog show <session-key> --limit 20\nbut agentlog show <session-key> --turn <turn-key> --limit 20\n```\n\nWithout `--turn`, `show` answers what happened in a session at turn granularity.\n\nUseful fields:\n\n- `coverage`: returned turns versus total turns.\n- `turn_key`: handle for `show --turn`.\n- `turn_index`: order within the session.\n- `capture_kind`: backfill or incremental.\n- `record_count`: turn size before hydrating records.\n- `source_record_index_range`: provider/source indexes covered by the turn.\n- `observed_targets`: targets observed in the turn.\n- `latest_user_preview` and `latest_assistant_preview`: compact orientation.\n- `tool_counts`: tool call/result counts and tool names.\n\nIncrease `--limit` only when the current window misses the related turn or setup.\n\nWith `--turn`, `show` answers what exactly happened inside one turn.\n\nUseful record fields:\n\n- `coverage`: returned records versus total records in the turn.\n- `turn_record_index`: order within the turn.\n- `source_record_index`: original provider/source index when available.\n- `timestamp`, `kind`, `role`, `text`: message/tool orientation.\n- `tool_name` and `tool_input`: tool details when present.\n- `source_record`: redacted stored provider envelope for low-level debugging.\n\nTurn records are intentionally bounded. Increase `--limit` only for a specific\nturn after session-level `show` output proves that more detail is needed.\n\n## Skim Summary\n\nWhen summarizing a skim, do not say you recovered \"full context.\" Say you\nskimmed the full turn history in abbreviated form.\n\nInclude:\n\n- target used for discovery\n- previews explaining intent and outcome\n- minimal record snippets for the next action\n\nDo not lead with raw session or turn identifiers. Mention them only when the\nuser asks for evidence handles or when passing them to another command/tool.\n\n<!-- File: crates/but/skill/SKILL.md (Tokens: ~6544 | Category: anthropic-skill) -->\n---\nname: but\nversion: 0.0.0\ndescription: \"Commit, push, branch, and manage version control with GitButler. Use for commits, selective dirty-file or hunk commits, branches, diffs, PRs, history edits, squashes, amends, undo, merge, apply, and unapply. For selected dirty files or hunks, inspect with `but diff`; use compact `but status` for commit order, branch/stack placement, or conflict overview; use `but status -fv` when file/hunk IDs or per-commit file details matter. Replaces git write commands.\"\nauthor: GitButler Team\n---\n\n# GitButler CLI Skill\n\nUse GitButler CLI (`but`) as the default version-control interface.\n\n## Start Here\n\nChoose the narrowest first command by task; avoid ritual status checks:\n\n```bash\n# Selected dirty files/hunks:\nbut diff\n\n# Commit order, branch/stack placement, conflict overview:\nbut status\n\n# File/hunk IDs, per-commit files, amend/split details:\nbut status -fv\n\n# Details for one known branch or commit:\nbut show <id>\n```\n\nDo not run plain `but status` and then `but status -fv` unless the compact output lacks file/hunk details needed for the task.\n\nFor \"commit just/only/specific changes on a new branch\", use the fast path:\n\n```bash\nbut diff\nbut commit -b <branch> -m \"<msg>\" <id> <id>\n```\n\n`but commit -b <branch> ...` creates the branch when it does not exist and prints the created commit. Do not run a separate `but branch new`, status command, or verification diff unless the task needs workspace information that the result does not provide.\n\n## IDs\n\nThe first token on each `but diff` / `but status` line is that line's ID. When a command needs an ID, copy it exactly from the current output; never hardcode or invent one. ID lifetimes differ by entity:\n\n- Changes and sources are **positional, space-separated** IDs (`but commit -b feat -m \"msg\" uvw:2e4 uvw:e2c`). An uncommitted hunk ID is written `<file-id>:<hunk-id>` (e.g. `uvw:2e4`, copied from bare `but diff`) — the part after the colon is the hunk's ID, **not** a line range (`uvw:16-40` is invalid). Do not invent flags like `--changes` / `--hunk` / `--ids`, pass a line range, or comma-separate IDs — `uvw,qyo` is parsed as one ID and fails.\n- `but diff` is the exception: it accepts at most **one** target. Bare `but diff` shows all uncommitted files; inspect committed files or other entities one target at a time — never `but diff <id> <id>`.\n- A committed file is `<commit-id>:<file-id>` (e.g. `mzm:uvw`, shown under each commit). A committed hunk is `<commit-id>:<file-id>:<hunk-id>` and is shown by `but diff <commit-id>`. `@` means the uncommitted area.\n- Commit IDs are stable change IDs that survive history edits (`amend`, `squash`, `move`, `uncommit`, `reword`). Commits without a change ID (e.g. upstream-only) lead with a sha prefix instead, and `#N`-suffixed refs disambiguate duplicates — both go stale after history edits, and a stale sha can silently resolve to the wrong commit. The `(sha …)` on verbose commit lines is informational — do not pass it to commands.\n- Branch short IDs are snapshot-local selectors. Use full branch names for branch-targeting mutations; short IDs are safe only for immediate read-only inspection.\n- File/hunk IDs copied from one diff read generally remain usable across chained commits. If one stops resolving, re-read `but diff` and retry.\n\n**Chaining:** mutation output is concise by default, so you may chain mutations with `&&` off one inspection read. Do not chain branch short IDs through mutations; use full branch names. Add `--status-after` only when the next step needs workspace IDs or details that the mutation result does not provide. Chained `but commit` calls stack in the order written — the first is oldest, each later one goes on top. History edits may run in sequence when every commit ref involved is a change-ID ref; run them one at a time with `--status-after` when a ref is sha-based or `#N`-suffixed, or when the next command needs freshly issued IDs. Chaining `but uncommit <id> && but diff` is safe because bare `diff` needs no ID from the uncommit output.\n\n## Non-Negotiable Rules\n\n1. Use `but` for all write operations. Never run `git add`, `git commit`, `git push`, `git checkout`, `git merge`, `git rebase`, `git stash`, or `git cherry-pick`. The only exception is selecting a side of a conflicted submodule in resolution mode (see Resolve conflicted commits). If the user says a `git` write command, translate it to `but` and run that. Running from a linked worktree shows the same workspace and IDs as the main worktree, and `@` still means the main worktree's changes; a bare `but commit` or `but diff` takes that worktree's changes, a bare path names a file there, and `<worktree>:@` names them from anywhere.\n2. Mutation commands print their result without appending workspace status. Add `--status-after` only when the next step needs resulting workspace IDs or details; otherwise trust the mutation result and do not run a verification status/diff.\n3. Branches marked `(merged upstream)` have landed; start new work on another branch. That marker alone does not mean `but pull` removes them: run `but pull --check` first. `but pull` removes only branches listed `[integrated]` (\"has been integrated upstream and removed locally\", or \"has been merged into <target> and removed\" when the target has no upstream) and rebases the rest, which stay applied. To clear a landed branch that stays applied, use `but unapply <branch>` (acts on its whole stack; `but apply <branch>` restores it) rather than `but discard`. `push` and mutations (`commit`, `amend`, `squash`, `uncommit`, `reword`, `move`) refuse landed branches and commits, `absorb` skips them with a notice, and `commit` skips them when picking a default target.\n4. In non-interactive CLI workflows, do not narrate progress between routine commands. Execute the needed `but` commands and give a concise final summary.\n5. Prefer this skill and `but skill reference` over exploratory help calls. Use `<command> --help` when required syntax is missing or a command fails; use top-level help only when you genuinely need to discover an undocumented command.\n\n## Command Patterns\n\n- Initialize a Git repository: `but init` (current directory) or `but -C <path> init`. Non-interactive; creates no commits, remotes, or GitButler workspace. Existing repositories are left unchanged.\n\n- Commit: `but commit -b <branch> -m \"<msg>\" <id> <id>` — `-b <branch>` creates the branch if it does not exist\n- Commit everything uncommitted: `but commit -b <branch> -m \"<msg>\"` (omit the IDs)\n- Several commits from one diff: chain `but commit` calls with `&&` (commits stack oldest-first)\n- Commit at a specific history position: `--above <commit-or-branch>` or `--below <commit-or-branch>` (mutually exclusive). With a branch target, add `-b <new-name>` to name the new branch; otherwise its name is generated. The name must not already exist. Do not combine `-b` (even without a name) with commit/worktree targets.\n- Targeting is **required** when more than one **stack** is applied; without it `but commit` fails with \"Unclear where to commit. Found more than one stack\". Several branches stacked together count as one stack — an untargeted commit then silently lands on the stack's top branch, so pass `-b` whenever the branch matters.\n- Always pass `-m \"<msg>\"` (or `-F -` with a heredoc, `-F <file>` with a file outside the repository, or `--no-message`) to `but commit`, and to `but squash` whenever its sources are commits or branches unless the target is `@` — those compose a new message; without a flag an agent run keeps whatever the command composed (empty for `but commit`, the joined source messages for `but squash`) and a terminal opens an editor. Squash sources that are uncommitted or committed changes reuse the target's message and need no flag; squashing into `@` rejects message flags outright.\n- Amend: `but amend -t <commit-or-branch> <file-or-hunk-id> <file-or-hunk-id>` — a branch target resolves to its newest commit\n- Uncommit: `but uncommit <commit-id>` (whole commit), `but uncommit <branch>` (all commits and remove branch), `but uncommit <commit-id>:<file-id>` (committed file), or `but uncommit <commit-id>:<file-id>:<hunk-id>` (committed hunk); committed files and hunks may be mixed, but all must come from one commit\n- Insert empty commit: `but commit --empty -b <branch> -m \"<msg>\"`\n- Pick onto a new named branch: `but pick <commit-sha> --above <branch> -b <new-name>` (`--below` also works). The name must not exist; `-b` cannot be combined with commit/worktree targets.\n- Squash commits: `but squash <source-commit-id> [<source-commit-id>...] -t <target-commit-id> -m \"<msg>\"`\n- Move committed changes into an existing commit or `@`: `but squash <commit-id>:<file-id> -t <commit-id-or-@>` for a committed file; `but squash <commit-id>:<file-id>:<hunk-id> -t <commit-id-or-@>` for a committed hunk; all sources must come from one commit\n- Move committed changes into a new commit at a chosen position: `but move <commit-id>:<file-id> --above <commit-or-branch> -m \"<msg>\"` for a committed file; `but move <commit-id>:<file-id>:<hunk-id> --above <commit-or-branch> -m \"<msg>\"` for a committed hunk (`--below`, `--branch`, and `--unstack` also work)\n- Split selected committed changes into a new commit immediately above their source: `but split <committed-file-or-hunk-id> [<committed-file-or-hunk-id>...] -m \"<msg>\"`\n- For `but split` and committed-change `but move`, use `-m/--message` to name the new commit directly; repeated `-m` values are joined with blank lines. For a multiline message, pass `-F -` with a heredoc, or `-F <file>` with a file outside the repository, instead of `-m`. Omitting both creates an empty-message commit without opening an editor. `but move -m` and `-F` reject whole-commit and branch sources.\n- Squash a whole branch into one commit: `but squash <branch> -m \"<msg>\"` (no `-t`)\n- Uncommit and remove a branch: `but uncommit <branch>`\n- Reorder commits: `but move <commit-id> --below <commit-id>` (`--above` for the other direction; **commit IDs**, not branch names)\n- Reorder a block: `but move <commit-id> <commit-id> --below <following-commit-id>` or `--above <preceding-commit-id>` (both anchors accept multiple space-separated sources)\n- Move commit to branch top: `but move <commit-id> -b <branch>`\n- Move commits or committed changes onto a new named branch: `but move <source> --above <branch> -b <new-name>` (`--below` also works); use `but move <source> --unstack -b <new-name>` for an independent branch. Omit `-b` for a generated name. Naming is not supported when stacking or unstacking a branch source.\n- Stack branches: `but move <branch-name> --above <target-branch-name>` (**use full branch names**)\n- Tear off a branch: `but move <branch> --unstack`\n- Discard: `but discard <id> [<id>...]` — accepts branches, commits, committed changes, uncommitted changes, or `@` for all uncommitted changes\n- Push: `but push <top-branch>` — pushes the selected branch and its ancestors; to update a stack, select its top branch once and never loop. Bare `but push` pushes all unpushed work when run non-interactively — one push per stack (its topmost unpushed branch, ancestors included), so output has one entry per stack, not per branch. It exits non-zero if any stack failed; stacks that already pushed stay pushed, and rerunning after fixing the failure is safe (up-to-date stacks are skipped)\n- Pull (update workspace from the target): `but pull` — the output reports the result; `but pull --check` previews without updating when a preview is actually needed\n- Create PR: `but pr new <branch-name> [-m \"Title...\"] [-F - | -F <file>] [-t] [--draft]` — auto-pushes first; do not run `but push` before it\n\n## Task Recipes\n\n### Update workspace from main\n\nFor \"get latest from main\", \"update/sync this workspace\", \"rebase onto main\", or \"pull main\":\n\n1. `but pull` — one command; no preflight needed. Its output reports the resulting state, it refuses safely when uncommitted changes conflict, and `but undo` reverts it.\n2. If commits come back conflicted, resolve them oldest-first following the printed instructions: `but resolve <commit>`, edit the files, then `but resolve finish`. Its result gives the current ID of the next conflict. Add `--status-after` to the finish you expect to clear the last conflict only when the task needs the complete resulting workspace. When it says no conflicted commits remain, stop; do not run a verification status. Finishing a lower commit rebases the ones above it, so always work bottom-up.\n\n`but pull --check` answers \"would this conflict?\" without updating. Do not use it as a routine\npreflight; use it when the user asks for a preview, repository policy requires one, other agents'\nbranches may move, or an applied branch was just merged upstream (see rule 3).\n\nRebasing applied branches onto the latest target IS `but pull` — never `move`, `config target`, `unapply`, or raw `git pull`/`git rebase`. The base shown in status is the last FETCHED state: when `git log` shows `main` (local or remote) ahead of it, that is exactly the update `but pull` fetches and applies — the target setting is not stale and repointing it is never the fix. Pull carries uncommitted changes along, and its output reports the resulting state. If it refuses because uncommitted changes conflict, park them: `but commit -b <branch> -m \"wip\" <ids>`, pull again, then `but uncommit` the parked commit (there is no stash; do not hand-revert files).\n\n### Commit selected files or hunks\n\n1. `but diff` — shows file and hunk IDs for uncommitted changes. Do not run plain `but status` first.\n2. Use file IDs when whole files belong in the commit; use hunk IDs (`<file-id>:<hunk-id>`) when only part of a file belongs. Omit IDs you don't want committed.\n3. `but commit -b <branch> -m \"<msg>\" <id1> <id2>` — the branch is created if it does not exist, so no prior `but branch new` is needed.\n4. When the task requires knowing which changes remain, add `--status-after`; otherwise the created-commit result is sufficient.\n\nEdge case: if wanted and unwanted edits are in the same diff hunk, GitButler cannot split that hunk by ID. Only when the task requires keeping part of that hunk uncommitted, temporarily edit the working tree to isolate the wanted lines, commit those IDs, then restore the leftover lines so they remain uncommitted.\n\n### Amend into existing commit\n\n1. `but status -fv` (or `but show <branch-id>`) — locate file/hunk IDs and target commit IDs.\n2. `but amend -t <commit-id> <id> <id>` — one command per target commit. For several target commits, chain the amends with `&&` when every target is a change-ID ref; otherwise run them one at a time with `--status-after` to get fresh refs.\n\n### Split an existing commit\n\nFor a two-way split, move selected committed files or hunks directly into a new commit:\n\n1. `but diff <source-commit-id>` — read committed file and hunk IDs.\n2. `but split <committed-file-or-hunk-id> [<committed-file-or-hunk-id>...] -m \"<message>\"` — sources may mix files and hunks, but must come from one commit. The command creates the named commit immediately above the source and leaves unselected changes and the original message in the source. No follow-up reword is needed for the new commit.\n3. Add `--status-after` only when the next step needs workspace IDs or details not provided by the mutation result.\n\nFor more than two replacement commits, repeat `but split ... -m \"<message>\"` using the remaining source's committed changes, or use the uncommit/recommit workflow below when rebuilding the entire commit:\n\n1. `but status -fv` when you need the source commit, branch name, or placement anchor.\n2. `but uncommit <source-commit-id> && but diff` in one shell call exposes the commit's changes and prints the resulting file and hunk IDs.\n3. Pick replacement contents from that dirty diff, not from the old committed diff.\n4. Determine the requested chronological order from the user's wording, then create the replacement commits oldest-first by chaining `but commit` calls. Do not reverse that order to match `but status`, which displays commits newest-first. `-b <branch>` puts each new commit at the TOP of that branch — if the split commit had commits above it, the replacements now sit above those preserved commits.\n5. **If commits from that branch must stay ABOVE the replacements, put the preserved block back on top instead of fighting anchors.** Do not anchor with `--above <top>`/`--below <top>` (sha/`#N` anchors go stale as each insert rewrites history). Move the block together so its internal order stays intact: append `&& but move <preserved-id> [<preserved-id>...] -b <branch>` to the commit chain. Change-ID refs from step 1 stay valid; wait for fresh output first if any preserved ref is sha-based or `#N`-suffixed.\n6. Leave unwanted changes uncommitted. Replacements created oldest-first appear newest-first in status — that is correct; do not reorder them. Add `--status-after` to the final mutation when you need to inspect the resulting order.\n\n### Reorder commits\n\n`but status` displays commits newest/top first, while task specs often list history oldest to newest — translate before moving.\n\n1. `but status` once to get commit IDs (use `-fv` only if you also need file details).\n2. `but move <source> --below <target-commit>` places source immediately below target in `but status` (older in oldest-to-newest history). `--above` places it immediately above (newer). `but move <source> -b <branch>` moves it to branch top/newest.\n3. For an adjacent block, run ONE move, anchored either way: `but move <block-id> <block-id> --below <following-commit-id>` or `but move <block-id> <block-id> --above <preceding-commit-id>`. Pick an anchor outside the block; source order does not matter and the block keeps its internal order. Add `--status-after` when you need to inspect the resulting order; do not move the anchor or block members again.\n4. For other reorders, make the smallest set of moves.\n\n### Squash commits\n\n1. `but status` for commit IDs and order.\n2. Name the sources positionally and the result/target commit with `-t`: `but squash <source> [<source>...] -t <target> -m \"<new message>\"`.\n3. To collapse an entire branch into one commit, pass just the branch and no `-t`: `but squash <branch> -m \"<new message>\"`.\n4. Multiple independent groups may run in sequence off one status read (targets keep their change-ID refs); prefer newer/top groups first. Take fresh refs only when a ref is sha-based or `#N`-suffixed.\n5. Add `--status-after` to the final squash when you need to inspect the resulting history; do not re-verify with a separate status.\n\n### Stack existing branches\n\nTo make one existing branch depend on another: `but move <child-branch-name> --above <parent-branch-name>` (use full branch names; commit reordering uses commit IDs). To unstack: `but move <branch-name> --unstack`.\n\n**DO NOT** stack via `uncommit` + `branch delete` + `branch new --above` (git branch names persist after delete and it loses work), and do not use `but undo` to unstack.\n\n### Create or manage pull requests\n\n`but pr new <branch-name>` pushes the selected branch and its ancestors, then creates the PR in one step — no prior `but push`. Provide `-F -` with a heredoc or `-F <file>` (first line is the title), `-t`, or `-m` with real newlines (zsh/bash: `-m $'Title\\n\\nBody'`) so no editor opens. Do not write the message to a file inside the repository: the next `but commit` would commit it. If forge auth is missing, run `but config forge auth`.\n\nIf you do create a PR for a stacked branch, use `but pr` — not `gh pr create` (only `but pr` sets PR bases and stack metadata; `gh pr create` breaks that). To publish a whole stack: `but pr new <top-branch-name> -t`. Manage with `but pr auto-merge|set-draft|set-ready <selector>`; selectors can be a branch name, current branch/stack CLI ID, or numeric review ID. See `but skill reference` for details.\n\n### Dependency conflict with another branch\n\nChanges that build on another branch's commits cannot land on an independent branch. `but commit` and `but amend` fail atomically (\"Cannot commit: N changes could not be applied\"), naming the branch and commit each rejected change depends on — nothing is committed and no `-b` branch is created.\n\nWhen there is a single dependency branch, the error's Hint gives the exact recovery command: `but move <your-branch> --above <dependency-branch>` to stack an existing branch on its dependency, or `but branch new <name> --above <dependency-branch>` when the target branch didn't exist yet. Run it, then retry the original command. When the error names dependencies without a Hint (several dependency branches, or the dependency is on the target branch itself), run `but status -fv` to see where the dependent commits live before choosing a placement.\n\nIf that recovery command fails, do NOT try `uncommit`, `squash`, or `undo` as a workaround — re-run `but status -fv` to confirm both branches exist and are applied, then retry with exact branch names.\n\n### Resolve conflicted commits (after pull, move, or reorder)\n\n**NEVER use `git add`, `git commit`, `git checkout --theirs/--ours`, or any git write command during resolution.** Only `but resolve` commands plus direct file edits. The one exception is a conflicted submodule in edit mode, below.\n\nConflicts do not interrupt operations in GitButler: a rebase always completes, and commits that conflicted are marked `{conflicted}` in `but status`. Find them from the warning that history-editing commands (`move`, `discard`, …) print, from the `but pull` summary, or from `but status`. Resolve them one conflict at a time, without entering any mode. Work through one branch at a time, oldest commit first — the loop is:\n\n1. `but resolve conflicts <branch>` — shows the conflicts of that branch's oldest conflicted commit, numbered per file. Each conflict has three sections: `ours` (the new base the commit was rebased onto), `base` (common ancestor), and `theirs` (the commit's own version). Without a branch it picks the first conflicted branch and tells you which; the output also says when other conflicted commits exist.\n2. Apply one resolution per command:\n   - Merged/mixed content (the common case — combine the intent of both sides): pipe it to `but resolve apply <path>:<N>` via stdin (heredoc) or pass `--file <f>`. The content replaces the whole conflicted region; never include conflict markers.\n   - Take a side entirely: `but resolve apply <path>:<N> --ours` or `--theirs` (bare `<path>` applies the side to all conflicts in that file).\n   - Delegate one conflict to the configured AI model: `but resolve apply <path>:<N> --ai` — prefer your own merged content when you have the context.\n   - `apply` targets the same default commit as `conflicts`; when more than one branch is conflicted, pin it with `--commit <branch>`.\n3. Go back to step 1 until it reports no conflicts. Commit ids are not stable here — every apply rewrites the commit — but branch names are, so address everything by branch and never bookkeep commit ids. Partial progress is fine: a commit with unresolved conflicts left stays `{conflicted}` with exactly those conflicts.\n\nA wrong resolution is reverted with `but undo`.\n\n`but resolve <commit-id> --ai` resolves a whole commit with the configured AI model in one shot, and `but resolve --ai` without a commit resolves every conflicted commit, oldest first — acceptable as a fallback, but you usually have better context than that model does, so prefer resolving the conflicts yourself with `apply`.\n\n**Use edit mode only when you must run code against the resolution** (build/tests at that commit):\n\n1. `but resolve <commit-id>` — enters resolution mode and prints the conflict regions.\n2. **Edit the files** to remove every conflict marker — `<<<<<<<`, `|||||||` (the common-ancestor section), `=======` and `>>>>>>>` — and keep the correct content. Do NOT skip this; do NOT use `but amend` on conflicted commits.\n   - A conflicted submodule has no markers, and `but resolve finish` refuses while it is unresolved, even to keep the commit's own side. Select it explicitly: check out the wanted commit inside the submodule (or write the file that replaces it) and run `git add -- <path>`, or drop it with `git rm -- <path>`. These are the only git write commands allowed during resolution, and only for the paths `finish` names; never `git commit`. If the submodule directory is empty or missing, do not `git add` it (for a missing one that stages its removal): keep any resolution edits you want and report the conflict. `but resolve cancel --force` abandons the resolution and discards its edits; use it only when those edits can be discarded.\n3. `but resolve finish` reports leftover markers, surviving uncommitted changes, every remaining conflicted commit, and the exact current `but resolve <id>` command. Add `--status-after` to the finish you expect to clear the last conflict only when the task needs the complete resulting workspace. When it says no conflicted commits remain, stop; do not run a verification status. Cancel instead with `but resolve cancel`.\n4. Repeat for remaining conflicted commits, oldest first — finishing a lower commit rebases the ones above it.\n\n### Conflicts in uncommitted files\n\n`but status` marks uncommitted files with unresolved merge conflicts `{conflicted}`; they are excluded from committable changes and outside `but resolve` mode. Edit the file to the wanted contents (or delete it), then `but resolve <path>...` to mark it resolved with that state; it then shows as an ordinary uncommitted change.\n\n## Git-to-But Map\n\n| git | but |\n|---|---|\n| `git status` | `but status` for branch/stack/commit overview; `but status -fv` for file/hunk details; `but diff` for selected dirty changes |\n| `git add` + `git commit` | `but commit -b <branch> -m ... <ids>` |\n| `git checkout -b` + commit | `but commit -b <new-branch> -m ... <ids>` |\n| `git push` | `but push <branch-name>` |\n| `git rebase -i` | `but move`, `but squash`, `but reword` |\n| `git rebase --onto` | `but move <branch> --above <new-base>` |\n| `git checkout -- <file>` / `git restore` | `but discard <id>` |\n| `git cherry-pick` | `but pick` |\n| `gh pr create` | `but pr new <branch-name> -m \"Title...\"` |\n\n## Notes\n\n- Read-only git inspection (`git log`, `git blame`, `git show --stat`) is allowed.\n- If `but` prints an `AGENT ACTION REQUIRED` skill warning, run the suggested command once, then reload/use the GitButler skill. If it repeats, report it instead of retrying.\n- For command syntax and flags: `but skill reference`\n- For workspace model: `but skill concepts`\n- For workflow examples: `but skill examples`\n\n<!-- File: packages/ui-react/AGENTS.md (Tokens: ~3457 | Category: anthropic-skill) -->\n# @gitbutler/ui-react\n\nThe React component library: the components, icons, illustrations and base\nstyles shared by Lite, `but panel` and the MCP app. It ships source, not a\nbuild; each consumer's Vite compiles it, and React Compiler runs on it under\nthe consumer's config as it does on the consumer's own code.\n\n`DESIGN.md` beside this file is the visual language: how things should look\nand read. It is an index of the pages in `design/`, one per topic; read the\nones for what you are changing before changing anything a user sees. This file\nis the tooling that enforces them.\n\n## Writing the code\n\nLite's rules apply here unchanged; `apps/lite/AGENTS.md` has them. In short:\nReact Compiler makes `useMemo`, `useCallback` and `React.memo` redundant\noutside hot paths; `useEffect` is usually an anti-pattern and needs consent;\ncomments explain the non-obvious why, never the what.\n\n### Changing a component\n\n**A change to a component here needs the designer's okay.** Every app draws\nwith these components, so a fix made for one surface changes all of them.\nWhen an app's bug seems to need a change here (a style, a prop, a default, a\nwrapper), try fixing it in the app first. If the library really is at fault,\nask before changing it. When you can't ask, make the change its own\n`fix(ui-react): …` commit that says which surface showed the bug and what\nelse it touches, and call it out in the PR description.\n\n### What belongs here\n\nA component belongs here when it doesn't reach into an app: no API calls,\nno settings, no store, nothing on `window.lite`. Everything a component needs\nfrom its host arrives as a prop, the way `TextLink` takes its `onClick` rather\nthan assuming Electron. A component that needs the app stays in the app;\nLite keeps its Markdown renderer and mention suggestions for that reason.\n\nImports are deep and carry the extension: `@gitbutler/ui-react/Button.tsx`.\nThere is no barrel file. Inside the package, siblings import each other\nrelatively.\n\n### Styles\n\nTokens come from `@gitbutler/design-core`, imported by the host once.\n`src/base.css` declares the few variables components read that aren't tokens\n(`--focus-ring`, `--control-cursor`, `--transition-button`) and the control\ncursor rules; every host imports it right after design-core. A variable nothing declares doesn't error, the\nproperty quietly falls back, so a new shared variable goes in `base.css`, not\nin a host's stylesheet.\n\n### Icons\n\nIcons are SVGs in `src/icons/`, inlined into the bundle as raw strings; the\nfilename is the icon name (`folder-lock.svg` → `<Icon name=\"folder-lock\" />`).\nTo add one:\n\n1. Export it from Figma at 16×16 (⚛️ Core library) as SVG into `src/icons/`.\n2. Run `pnpm -F @gitbutler/ui-react optimize-icons`. It minifies the SVG,\n   recolors it to `currentColor` and regenerates `src/iconNames.ts`.\n3. Commit both. `iconNames.ts` is generated; never hand-edit it.\n\n`src/file-icons/` are the language and filetype glyphs. They keep their brand\ncolours and are deliberately not run through the script.\n\n`src/program-icons/` are the marks of editors and terminals, as PNGs at 2×.\nTheir rounded corner is part of the image: export each from the \"Programm\"\nframe on ⚛️ Core's Icons page, where the mark is a 14px rectangle with\nthe corner on it, as PNG at 2× with the layer named as the file is\n(`vscode` → `vscode@2x.png`). No CSS rounds them, so a square export shows\nsquare.\n\n## Figma\n\nTwo files, split the way the code is:\n\n- **⚛️ Core** —\n  <https://www.figma.com/design/cqdnAotT8n9op8WGYLOHg4/%E2%9A%9B%EF%B8%8F-Lite-Core>,\n  the published library: tokens (Tokens page), icons (Icons page) and this\n  package's components, one section per component. It is the source of truth\n  for tokens; for components, code is, and the drawing follows it. The\n  components are on six pages, by what they are for: Actions, Inputs,\n  Overlays, Content & status, Preview (a details view's header and the bar\n  above it), and Sidebar. A new component's section goes at\n  the end of its group's page, with its examples section under it. Storybook\n  shelves the stories in the same groups (plus Icons, which Figma keeps on its\n  own page): a stories file names its group in `title` and keeps its old `id`,\n  so the story ids below don't change when a component moves group.\n- **Client**, the working file —\n  <https://www.figma.com/design/EBuHQGUcCaSw4Ln5uVpWkn/Client>: Lite's own\n  components on the ⚙️ Meta page, the full screens, and drafts on the\n  🚧 Drafts page and the pages under it. A new mockup or an exploration goes\n  here, never in the library.\n\nA component is drawn where its code lives: one in this package goes in\n⚛️ Core, one in `apps/lite` on ⚙️ Meta. When a component moves between the two\nin code, its drawing moves with it in the same piece of work, and the\nmockups' instances are swapped to the new one.\n\nThe drawing mirrors the code's structure: the same component under the same\nname, variant properties named as the props and their values (`status`,\n`variant`, `glyph`), and no property the code lacks. Figma may add only what\nthe code produces from data, such as a toggle for a side the code drops when\nits count is zero, and a `state` property (`default`, `hover`) for what CSS\ndraws under the pointer, as Button has; two components the code keeps apart\nstay apart in Figma and are placed side by side where a surface shows both.\n\nA component's description in Figma says what its JSDoc and DESIGN.md say\nabout it: what it is for, when to pick which variant, and a `Code:` line\nnaming the component and its file. Change one and change the other in the\nsame piece of work; a description that lags the code is what an agent reading\nFigma will build from.\n\n## Verifying your work\n\nAlways run the specified commands **exactly** as written. The package has no\napp to run, so the story is how a component is checked before it goes into a\nsurface.\n\n### Typechecking, the CSS rules, and tests\n\n```console\n$ pnpm -F @gitbutler/ui-react check\n$ pnpm -F @gitbutler/ui-react test\n```\n\n`check` typechecks and then runs Stylelint over the components' CSS with the\nrules the design pages give a machine: colours are tokens, font sizes are on\nthe 11–16 scale, radii come from the radius tokens. An error names what to use\ninstead. The exceptions are the ones the pages name, and each is marked where\nit is: the file icons' brand colours (a file-level override in\n`stylelint.config.mjs`) and a mask's opaque stop. A radius that is a share of\nanother, an inner corner following its outer one, is a `calc()` from the outer\ntoken and needs no exception (`design/foundations/radius.md`). A new exception\nis a design decision first; if it stands, disable the rule on that line with\nthe reason after `--`.\n\n### Tokens\n\nEvery `var(--x)` in this package has to resolve. Lite's token check reads this\npackage's source alongside its own:\n\n```console\n$ pnpm -F @gitbutler/lite check\n```\n\nIt fails on any undefined name and suggests the nearest real one. Run it\nafter any CSS change here, even when the package's own check passes.\n\n### Stories\n\nEvery component has a story beside it, in both themes, and a component that\nlives only in code is half a component. `pnpm check` holds the package to it\nthrough `scripts/check-stories.mjs`, which fails on a component no story\nimports. Storybook is Lite's, on port 6007, and lists this package's stories\nunder `components/`:\n\n```console\n$ pnpm -F @gitbutler/lite demos\n```\n\nThe package's own `test` runs its unit tests, not its stories. Lite's runs\nevery story, this package's included, as a test in headless Chromium; run\nthem alone with `pnpm -F @gitbutler/lite test:stories` (`apps/lite/AGENTS.md`\nhas the setup).\n\nA story renders alone at\n`http://localhost:6007/iframe.html?id=components-<name>--<export>&viewMode=story`;\nappend `&globals=theme:dark` for the dark theme. Check colour, font and\nspacing from computed styles rather than by eye. A story links its Figma\ncomponent through the `design` parameter; keep the link when you add a story,\nand compare the two sides when either changes.\n\n### Default, then examples\n\nA component's stories are one entry in Storybook, in one order:\n\n- **`Default` first:** the simplest use of the component, its props on the\n  controls where they have any to show. It answers what the component is and\n  accepts. `pnpm check` fails on a stories file that opens with anything\n  else.\n- **Examples after it:** each named after what it shows — a state\n  (`OnlyAdditions`, `NothingToList`) or a surface the app builds from it\n  (`Confirm`, `SelectProject`, `CommandPalette`). An example is written as\n  the app writes it: its own state, real copy, rendered open, since its\n  source is what an agent copies from `docs-show`.\n\nA surface built from several components is an example of the outermost one:\nthe upload confirmation is Modal's, the project picker is Popup's (a\ncombobox dressed in its parts), the command palette is PickerDialog's.\n\nIn ⚛️ Core the examples sit in a section named `<Component> examples`\ndirectly under the component's own section, on its group's page, one\ninstance per story, named as the story is; an example story links its\ninstance through the `design` parameter.\n\n### The manifest\n\nStorybook writes `/manifests/components.json` (and `components.html`, to\nread) from the stories: every component, its props from the TypeScript\ntypes, its stories with their source, and the import to write. An agent\nreads it instead of guessing an API. Two things about it to know:\n\n- The import to write is the `@import` tag in each component's JSDoc, as\n  `@import import { Badge } from \"@gitbutler/ui-react/Badge.tsx\";`. Give a\n  new component one: without it the manifest falls back to the package\n  root, and there is no such barrel. Storybook reads the tag through\n  `experimentalReactComponentMeta` in Lite's `.storybook/main.ts`.\n- The link to the Figma component is the story's `design` parameter, not\n  the manifest. `grep -l 'type: \"figma\"' src/*.stories.tsx` lists the stories\n  that have one; a story without one has no drawn spec yet.\n\nA story with no component behind it, as Lite's `AppUpdater.stories.tsx`,\ncarries `tags: [\"!manifest\"]` and says why. That is also why a styling\nhelper is not how a control is offered: `getButtonClassName` is for making\nanother component look like a button, and a button is `<Button>`, which the\nmanifest documents.\n\n### The Storybook MCP server\n\nWhile Storybook runs, it serves an MCP server at `http://localhost:6007/mcp`\n(`@storybook/addon-mcp`). Add it to your agent once:\n\n```console\n$ claude mcp add --transport http gitbutler-storybook http://localhost:6007/mcp\n```\n\nEach design notes page has an `.mdx` beside its `.md` in `design/`, under\nStorybook's Design section, so the published Storybook shows them and the MCP\nserver's `docs-list` lists them; `design/overview.mdx` renders DESIGN.md. The\nMDX renders the Markdown file itself, so edit the `.md`, never the page. What\nthe MDX writes is the title, a one-line summary (the description `docs-list`\nshows) and the file's raw URL, since `docs-show` returns the MDX source, not\nthe rendered text. A new page takes a Markdown file, an MDX beside it titled\n`Design/<Group>/<Name>` after its folder and file name (that is how links\nbetween the files find their Storybook page), and a row in DESIGN.md.\n\nBefore using a component, ask it rather than guessing: `docs-list` lists the\ncomponents, `docs-show` gives one's props, its stories and the import to\nwrite, and `docs-show-story` a whole story. Use only props it documents; if\none seems missing, ask instead of inventing it.\n`stories-find-by-component` finds the stories a file renders in, and\n`stories-preview` links to them. The published Storybook serves the same docs\ntools without a local one. The repository's `.mcp.json` connects Claude Code\nto it (it asks once); another agent, or one outside this repo, adds it:\n\n```console\n$ claude mcp add --transport http gitbutler-storybook-published https://master--6ab536f5f40e41db628ccf1b.chromatic.com/mcp\n```\n\nThe published server can time out, most often while a new master Storybook\ndeploys. Fall back in order: the manifest at\n<https://master--6ab536f5f40e41db628ccf1b.chromatic.com/manifests/components.json>,\na static file that keeps answering, then the component's own source and JSDoc\nin `src/`, which the manifest is built from. Never guess a prop because the\ntools were down.\n\n### Accessibility\n\n`@storybook/addon-a11y` runs axe on every story: the Accessibility tab under\na story lists contrast failures, controls without a name and misused roles.\nCheck it for a new or changed story. It catches the mechanical part of\nDESIGN.md's rules (an icon-only button's `aria-label`, a field's label,\ncontrast), not whether a label makes sense.\n\n### Chromatic\n\n`.github/workflows/chromatic.yml` publishes the Storybook to Chromatic's\npublic \"GitButler UI\" project: from master as the published Storybook, at\n<https://master--6ab536f5f40e41db628ccf1b.chromatic.com>, and for a pull request that touches the\nstories, as a preview with a UI Review check that compares every story's\nscreenshot with master's. A visual change waits there to be accepted rather\nthan failing the build; accept it when it is the change you meant.\n\n### Linting & formatting\n\nThe package is linted by oxlint with Lite's config, not by ESLint:\n\n```console\n$ pnpm exec prettier --check packages/ui-react\n$ pnpm run oxlint\n$ pnpm knip:non-prod\n```\n\nRun these before declaring the work done, and run `check-tokens` (above) with\nthem. The lint job in CI runs all of them and fails on what any one reports.\n\n<!-- File: packages/ui-react/CLAUDE.md (Tokens: ~3 | Category: anthropic-skill) -->\nAGENTS.md\n\n<!-- File: packages/ui-react/skill/SKILL.md (Tokens: ~730 | Category: anthropic-skill) -->\n---\nname: gitbutler-ui\ndescription: Use when changing what users see in an app built on `@gitbutler/ui-react` — Lite, the but.dev web app — adding or restyling a component, modal, form, list, empty state or toast, or writing the words on screen. Not for logic, data or backend work, even inside a UI file.\n---\n\n# Building GitButler UI\n\nWork as you would in any codebase: find the closest thing that already\nexists and copy it. These notes say where to look and what to check. They are\nnot a gate to clear before starting.\n\n## Scope\n\nChange what the task needs. Older screens may not follow these notes; leave\nthem alone unless the task is to fix them.\n\n## Start from the library\n\n`@gitbutler/ui-react` has the controls: `Button`, `Popup` (modals, dropdowns),\n`Select`, `Field`, `Checkbox`, `Switch`, `Tooltip`, `EmptyState`, `Snackbar`\n(raised through the `Snackbars` stack), `Toasts`, `Banner` and more. Use them\nrather than a control styled in a feature's CSS. Find a surface that already\nuses the ones you need and follow its structure.\n\nThe source is the reference, because it is the version you build with:\n\n- In the gitbutler repository: `packages/ui-react/src/`\n- In but.dev: `web/node_modules/@gitbutler/ui-react/src/`\n\nEach component's JSDoc says what its props do. The Storybook MCP server\n(`docs-show`) and\n<https://master--6ab536f5f40e41db628ccf1b.chromatic.com/manifests/components.json>\nhave the same docs with examples; use them when they answer, and go back to the\nsource when they don't. Don't wait on them.\n\nEach component's stories open with `Default`, its simplest use and its props;\nthe ones after it are examples, the component put to work the way an app\nwrites it. For a whole surface, find the closest example under its outermost\ncomponent (a confirmation is Modal's, a picker Popup's or PickerDialog's),\ncopy it and change what differs.\n\nWhen nothing fits, build the smallest thing that works and say so in the PR\ndescription: what was needed, which components you tried.\n\n## Read the design pages you need\n\nThe shared design language is the library's `DESIGN.md`, beside `src/` in the\nsame two places: an index of one page per topic, in `design/`, each saying\nwhen to read it. Read the pages for what you are building, not all of them.\nIn Storybook they are the Design section, and `docs-list` lists them.\n\nEach app keeps its own choices in its own `DESIGN.md`: Lite's is\n`apps/lite/DESIGN.md`, but.dev's is at its repository root.\n\n## Words\n\nShort and plain. A label is one to three words; a hint is one sentence. Call\nthings what the rest of the app calls them. Say what the user can't already see\non screen, once, without parentheses.\n\n## Check it\n\nLook at the result in light and dark, and keep one screenshot. In the\ngitbutler repository a component's story runs with `pnpm -F @gitbutler/lite\ndemos`; `apps/lite/AGENTS.md` has how to reach a single story.\n\n<!-- File: packages/but-mcp-app/README.md (Tokens: ~211 | Category: plugin-manifest) -->\n# GitButler workspace MCP App\n\nThese React apps render the structured results of the `gitbutler_workspace` and\n`gitbutler_review_card` MCP tools. Their compact workspace and review components\ntake their visual cues from GitButler Lite. Selecting a workspace commit or\nbranch opens a responsive details panel with copy, explain, and review actions.\nReview cards refresh open reviews and their CI status every 30 seconds until the\nreview closes, CI fails, or a refresh request fails.\n\nBuild the single-file resource embedded by the Rust MCP server with:\n\n```sh\npnpm -F @gitbutler/but-mcp-app build\n```\n\nThe generated `workspace.html` and `review.html` files under\n`../../crates/but/src/command/mcp` are ignored. Release workflows set\n`GITBUTLER_REQUIRE_MCP_APP=1` and must run the command above before compiling\n`but`. Normal Rust builds use a small fallback resource so they do not require\nNode.js or pnpm.\n\n<!-- File: packages/but-sdk/README.md (Tokens: ~1339 | Category: plugin-manifest) -->\n# @gitbutler/but-sdk\n\n`@gitbutler/but-sdk` is the local npm package that exposes GitButler Rust APIs to JavaScript/TypeScript through a native Node add-on.\n\n## How it works\n\nThe generated SDK surface is produced in two parts and merged into a single declaration file:\n\n1. **N-API bindings (functions)**\n   - Rust functions marked with `#[but_api(napi)]` in `but-api` generate `*_napi` exports via the `but_api` proc macro.\n   - `but-napi` links those generated exports and is built by `napi-rs` into a `.node` binary.\n   - `napi-rs` generates, per flavor folder:\n     - `gitbutler-sdk.{PLATFORM}.node` (Node native add-on)\n     - `index.js` (runtime loader + JS bridge)\n     - `index.d.ts` (N-API function declarations)\n\n2. **Schema-derived TypeScript types (structs/enums)**\n   - `but-ts` collects registered JSON schemas from `but-api` (`but_api::schema` module) and emits TS type aliases.\n   - These emitted type aliases are appended to the flavor folder's `index.d.ts`.\n\n\nThis package ships only generated artifacts (`browser.js` plus each flavor's `index.js`, `index.d.ts`, `*.node`) and re-exports them through package `exports`.\n\n## Workspace-projection flavors\n\nMutation APIs return a `WorkspaceState` whose projection depends on the\n`graph-workspace` cargo feature of `but-api` (see its feature docs). The SDK\nships one fully self-contained build per flavor:\n\n- `@gitbutler/but-sdk` (alias `@gitbutler/but-sdk/linear`, from\n  `src/generated/linear/`): built without the feature; `WorkspaceState`\n  carries the legacy `RefInfo`-based `headInfo` projection.\n- `@gitbutler/but-sdk/graph` (from `src/generated/graph/`): built with\n  `--features graph-workspace`; `WorkspaceState` carries the graph-based\n  `graphWorkspace` (`DetailedGraphWorkspace`) projection.\n\nEach folder has its own `.node` binary, loader, and declarations, so importing\nan entry point loads the matching native build — types and runtime cannot\ndisagree for napi consumers. (The desktop app consumes this package for types\nonly; its runtime is the Tauri backend, so it must still keep its import flavor\nin lockstep with `gitbutler-tauri`'s `graph-workspace` feature.) Both folders'\n`index.js`/`index.d.ts` are committed and CI-checked.\n\n## Generate bindings and types\n\nPer flavor, always run the napi build before the type generation:\n\n```bash\n# linear flavor (src/generated/linear/)\npnpm --filter @gitbutler/but-sdk build:napi\npnpm --filter @gitbutler/but-sdk build:types\n# graph flavor (src/generated/graph/)\npnpm --filter @gitbutler/but-sdk build:napi:graph\npnpm --filter @gitbutler/but-sdk build:types:graph\n```\n\nOr run the combined script, which builds both flavors:\n\n```bash\npnpm --filter @gitbutler/but-sdk build\n```\n\n### Why order matters\n\n`build:types` appends schema types to the existing declaration file. Running it repeatedly without regenerating N-API declarations first can duplicate schema type sections.\n\n## Add new bindings and types\n\n### Add a new exported N-API function\n\n1. In `crates/but-api`, annotate a Rust API with `#[but_api(napi)]`.\n2. Ensure input/output types are serializable in a way the macro supports.\n3. Re-run:\n\n```bash\npnpm --filter @gitbutler/but-sdk build:napi\n```\n\nYou should see a new `*Napi` export in `src/generated/linear/index.d.ts` (and `src/generated/graph/index.d.ts` for the graph flavor).\n\n### Add a new generated TypeScript type\n\n1. Make sure the Rust type derives `schemars::JsonSchema`.\n2. Register the type in `crates/but-api/src/schema.rs` using `TypeSchemaEntry`.\n3. Re-run:\n\n```bash\npnpm --filter @gitbutler/but-sdk build:types\n```\n\nYou should see a new `export type ...` in `src/generated/linear/index.d.ts` (and `src/generated/graph/index.d.ts` for the graph flavor).\n\n## Validate generated output\n\nBaseline validation:\n\n```bash\npnpm --filter @gitbutler/but-sdk check\n```\n\nOptional extra runtime/type sanity check:\n\n```bash\npnpm --filter @gitbutler/but-sdk testTypes\n```\n\nRecommended contributor flow after changing bindings/types:\n\n```bash\npnpm --filter @gitbutler/but-sdk build\npnpm --filter @gitbutler/but-sdk check\n# optional\npnpm --filter @gitbutler/but-sdk testTypes\n```\n\n## Usage in Electron (`apps/lite` pattern)\n\nUse native bindings in the Electron **main process**, then expose only typed IPC APIs to the renderer.\n\n### Main process model\n\n```ts\nimport { listProjectsStateless } from '@gitbutler/but-sdk';\n\nexport function listProjects() {\n\treturn listProjectsStateless();\n}\n```\n\n### IPC contract with SDK types\n\n```ts\nimport type { ProjectForFrontend } from '@gitbutler/but-sdk';\n\nexport interface LiteElectronApi {\n\tlistProjects(): Promise<ProjectForFrontend[]>;\n}\n```\n\n### Renderer consumes IPC API\n\n```ts\nconst projects = await window.lite.listProjects();\n```\n\nThis keeps native access out of the renderer while sharing the same generated TS types (`ProjectForFrontend`, `StackEntry`, `StackDetails`, etc.) across process boundaries.\n\n## Current exported binding entry points\n\nSee `src/generated/linear/index.d.ts` (and `src/generated/graph/index.d.ts` for the graph flavor) for the complete generated API and type surface.\n\n## See also\n\n- [but-napi](../../crates/but-napi/README.md)\n- [but-ts](../../crates/but-ts/README.md)\n\n## License\n\nFSL-1.1-MIT - See [LICENSE.md](../../LICENSE.md) for details.\n\n<!-- File: packages/no-relative-imports/README.md (Tokens: ~305 | Category: plugin-manifest) -->\n# @gitbutler/no-relative-imports\n\nAn eslint rule for enforcing non-relative imports if there are `paths` in your `tsconfig.json` available to you.\n\n## What the rule affects\n\nThe rule requires that any import that _could_ be referenced absolutely via an entry in `paths` will provide an error.\n\nEven if a sibling import (IE `./foo`) is used, if it could be accessed via a path (IE `$lib/foo`). An error will be provided.\n\n## The `paths` format\n\nIt should be noted that the current implementation does not try to handle all the possible `paths` entries. Instead it handles the three common cases:\n\n```json\n{\n\t\"compilerOptions\": {\n\t\t\"paths\": {\n\t\t\t\"$lib\": \"./lib\", // Without glob. IE pointing directly to a file\n\t\t\t\"$lib/*\": \"./lib/*\", // Both with glob, IE anything starting in ./lib/... results in $lib/...\n\t\t\t\"$lib\": \"./lib/*\" // Path with, IE anything starting in ./lib/... results in $lib\n\t\t}\n\t}\n}\n```\n\n## Usage\n\n```\nimport noRelativeImportPaths from '@gitbutler/no-relative-imports';\n\nexport default [\n  {\n    plugins: {\n      'no-relative-import-paths': noRelativeImportPaths,\n    },\n    rules: {\n      'no-relative-import-paths/no-relative-import-paths': 'error',\n    },\n  },\n]\n```\n\n<!-- File: packages/shared/README.md (Tokens: ~385 | Category: plugin-manifest) -->\n# create-svelte\n\nEverything you need to build a Svelte library, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).\n\nRead more about creating a library [in the docs](https://kit.svelte.dev/docs/packaging).\n\n## Creating a project\n\nIf you're seeing this, you've probably already done this step. Congrats!\n\n```bash\n# create a new project in the current directory\nnpm create svelte@latest\n\n# create a new project in my-app\nnpm create svelte@latest my-app\n```\n\n## Developing\n\nOnce you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:\n\n```bash\nnpm run dev\n\n# or start the server and open the app in a new browser tab\nnpm run dev -- --open\n```\n\nEverything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app.\n\n## Building\n\nTo build your library:\n\n```bash\nnpm run package\n```\n\nTo create a production version of your showcase app:\n\n```bash\nnpm run build\n```\n\nYou can preview the production build with `npm run preview`.\n\n> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.\n\n## Publishing\n\nGo into the `package.json` and give your package the desired name through the `\"name\"` option. Also consider adding a `\"license\"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).\n\nTo publish your library to [npm](https://www.npmjs.com):\n\n```bash\nnpm publish\n```\n\n<!-- File: packages/svelte-comment-injector/README.md (Tokens: ~305 | Category: plugin-manifest) -->\n# svelte-comment-injector\n\nA Svelte preprocessor that injects an HTML comment at the top of every component, making it easier to identify components in browser DevTools.\n\n## ✨ Features\n\n- Injects a DevTools-visible HTML comment like:\n  ```html\n  <!-- Begin MyComponent.svelte -->\n  <div class=\"my-component\">\n  \t<!-- Component content -->\n  </div>\n  <!-- End MyComponent.svelte -->\n  ```\n\n## 📦 Installation\n\n```bash\nnpm install @gitbutler/svelte-comment-injector --save-dev\n```\n\n## ⚙️ Usage\n\nAdd the preprocessor to your Svelte configuration:\n\n```js\n// svelte.config.js\nimport svelteInjectComment from \"@gitbutler/svelte-comment-injector\";\n\nexport default {\n\tpreprocess: [svelteInjectComment()],\n\t// ...rest of your config\n};\n```\n\n## 🛠️ Configuration\n\nYou can customize the comment format by passing options to the preprocessor:\n\n```js\n// svelte.config.js\nimport svelteInjectComment from \"@gitbutler/svelte-comment-injector\";\n\nexport default {\n\tpreprocess: [\n\t\tsvelteInjectComment({\n\t\t\tenabled: true, // Enable or disable the comment injection\n\t\t\tshowEndComment: true, // Show the end comment\n\t\t\tshowFullPath: false, // Show the full path in the comment\n\t\t}),\n\t],\n\t// ...rest of your config\n};\n```\n\n<!-- File: packages/ui-svelte/scripts/README.md (Tokens: ~473 | Category: plugin-manifest) -->\n# UI Package Scripts\n\n## `optimize-ui-icons.js`\n\nOptimizes all SVG files in `src/lib/icons/svg/` using svgo, then syncs `src/lib/icons/names.ts`.\n\n### Usage\n\n```bash\n# From the UI package root\npnpm optimize-ui-icons\n# or\nnode scripts/optimize-ui-icons.js\n```\n\n### What it does\n\n1. Runs svgo on every `.svg` in `src/lib/icons/svg/` with `preset-default` (keeping `viewBox` and groups intact)\n2. Sets `width=\"100%\"` and `height=\"100%\"` on every `<svg>` so icons scale via CSS\n3. Replaces hardcoded `fill`/`stroke` colors with `currentColor` (leaves `none` untouched)\n4. Adds `vector-effect=\"non-scaling-stroke\"` to all vector shape elements\n5. Updates `src/lib/icons/names.ts` to reflect the current set of icon files\n\n---\n\n## `optimize-file-icons.js`\n\nOptimises SVG file-type icons and writes them into `src/lib/components/file/icon/svg/`.\n\nSupports two modes:\n\n- **Optimize in place** (no argument): optimises all existing SVGs in the output directory\n- **Import from a source directory** (with argument): reads SVGs from `<svg-dir>`, optimises them, and writes each into the output directory (adding new files, updating changed ones)\n\n### Optimisation steps\n\n- Runs svgo `preset-default` (keeping `viewBox` and groups intact)\n- Removes `width`/`height` attributes so icons scale via CSS\n- Replaces hardcoded hex colours with CSS variables from a canonical colour map (`--file-icon-gray`, `--file-icon-green`, `--file-icon-teal`, `--file-icon-blue`, `--file-icon-dark-blue`, `--file-icon-yellow`, `--file-icon-orange`, `--file-icon-red`, `--file-icon-pink`, `--file-icon-purple`, `--file-icon-violet`)\n\n### Usage\n\n```bash\n# Optimize in place\npnpm optimize-file-icons\n# or\nnode scripts/optimize-file-icons.js\n\n# Import + optimize from a source directory\nnode scripts/optimize-file-icons.js <svg-dir>\n```\n\n<!-- File: packages/ui-svelte/src/lib/components/chipToast/README.md (Tokens: ~399 | Category: plugin-manifest) -->\n# Toast Component\n\nA simple toast notification system for the GitButler UI package.\n\n## Features\n\n- **Simple**: Just type and text - no complex configuration\n- **Bottom-center positioning**: Appears at the bottom center of the screen\n- **Stacking**: Multiple toasts stack vertically\n- **Auto-dismiss**: All toasts automatically disappear after 4 seconds\n- **4 types**: Info, Success, Warning, and Error with appropriate styling\n\n## Usage\n\n### Basic Usage\n\n```svelte\n<script>\n\timport { toasts, ToastContainer } from \"@gitbutler/ui-svelte\";\n\n\tfunction showToast() {\n\t\ttoasts.success(\"Operation completed!\");\n\t}\n</script>\n\n<button on:click={showToast}>Show Toast</button>\n\n<!-- Add the container to your app root -->\n<ToastContainer />\n```\n\n### Toast Types\n\n```javascript\nimport { toasts } from \"@gitbutler/ui-svelte\";\n\n// Different toast types\ntoasts.info(\"This is a info message\");\ntoasts.success(\"Operation completed successfully!\");\ntoasts.warning(\"Please review your changes\");\ntoasts.error(\"Something went wrong\");\n```\n\n### Promise Integration\n\n```javascript\nimport { toasts } from \"@gitbutler/ui-svelte\";\n\nasync function handleAsyncOperation() {\n\tconst myPromise = fetch(\"/api/data\");\n\n\tawait toasts.promise(myPromise, {\n\t\tloading: \"Loading data...\",\n\t\tsuccess: \"Data loaded successfully!\",\n\t\terror: \"Failed to load data\",\n\t});\n}\n```\n\n### Clear All Toasts\n\n```javascript\ntoasts.clearAll(); // Removes all current toasts\n```\n\n## Components\n\n### `<Toast>`\n\nIndividual toast component.\n\n**Props:**\n\n- `type: 'info' | 'success' | 'warning' | 'error'` - Toast type\n- `message: string` - Toast message text\n\n### `<ToastContainer>`\n\nContainer that manages positioning and stacking. Add this once to your app root.\n</yakaai_skills>"}