**Title:** Connection send window not reset after 0-RTT handshake rejection

**Description**

When a QUIC client’s 0-RTT handshake is rejected and the server’s new `initial_max_data` transport parameter is smaller than the one the client originally cached, the client can send more connection‑level data than the server is willing to accept. This occurs because the connection‑level send window is not cleared when the connection flow controller is reset during 0‑RTT rejection – other send‑side state (like `bytesSent`) is reset, but the send window retains its old value.

The observable symptom is that after a 0‑RTT rejection with a reduced connection‑level window, the client keeps sending data up to the original (larger) window and may trigger flow‑control errors on the server, or deadlock waiting for a credit update that it does not receive.

**Expected behaviour**

* After 0‑RTT is rejected, the client must treat its connection‑level send window as zero until it receives a fresh `MAX_DATA` frame from the server.
* The `Reset()` method of the connection flow controller should also reset the send window (`sendWindow`).
* No other code path that relies on the connection flow controller’s reset should be adversely affected (e.g., normal re‑negotiation of flow control credits should still work correctly).
* The fix should be verifiable with an integration test that:
  1. Originates a client connection with a high `initial_max_data`.
  2. Rejects 0‑RTT on the server with a lower `initial_max_data`.
  3. Observes that the client, after the rejection, does **not** send more than the new limit until the server explicitly increases it with a `MAX_DATA` frame.

**Context**

* The relevant code lives in the flow‑control package (`internal/flowcontrol`), specifically the connection flow controller that implements `ConnectionFlowController`.
* The 0‑RTT rejection path is exercised in the zero‑RTT integration tests (`integrationtests/self/zero_rtt_test.go`), which currently cover stream‑limit decreases and ALPN changes but not connection‑level window reductions.

flowcontrol: reset the connection send window when 0-RTT is rejected (#4764)


```
Disable GSO and ECN on Linux kernels older than version 5.

On some older Linux kernels (major < 5), GSO and/or ECN cause hangs or
panics despite the socket option appearing to succeed.  We currently
decide GSO support only by probing the socket option and ECN is on by
default unless QUIC_GO_DISABLE_ECN is set, so users on these kernels
must know to set environment variables to avoid crashes.

Add an explicit kernel-version guard: on Linux, do not enable GSO or
ECN when the kernel major version is less than 5, even if the socket
option succeeds or the env var is unset.  The existing
QUIC_GO_DISABLE_GSO and QUIC_GO_DISABLE_ECN environment variables
should still override this check (i.e., they can force disable on
newer kernels too, but should not force enable on old kernels).

On non-Linux platforms (Darwin, FreeBSD) the existing behaviour must
not change: GSO remains off, ECN remains on unless disabled by env var.
```

disable GSO and ECN on kernels older than version 5 (#4456)


```
Title: Server does not pad ack-eliciting Initial packets, violating QUIC anti-amplification requirement

The QUIC transport draft requires servers to pad datagrams that contain ack-eliciting Initial packets to at least the minimum datagram size (MinInitialPacketSize / 1200 bytes) as an anti-amplification measure. ACK-only Initial packets from the server are exempt from this requirement. Clients, by contrast, are expected to always pad all Initial packets to the minimum size, regardless of whether they contain ack-eliciting frames.

Currently the implementation seems to unconditionally skip padding for all server Initial packets. Additionally, it appears that ACK-only Initial packets from the client may also escape padding because the ACK-only packing path does not trigger the padding logic at all.

The expected behavior is:

- For the **client**, all Initial packets (including ACK-only) are padded to the minimum datagram size.
- For the **server**, only Initial packets that contain at least one ack-eliciting frame are padded to the minimum datagram size. Initial packets that are purely ACK-only must not be padded.

Please fix the packet packing so that padding is applied according to these rules, respecting both the perspective (client vs. server) and whether the Initial packet contains ack-eliciting frames.
```

pad datagrams containing ack-eliciting Initial packets from the server


We need a way for application-layer protocols (like WebTransport over HTTP/3) to perform their own flow-control accounting on the receive side. For that, they must know the final, total number of bytes the peer will send on a QUIC stream, as soon as that information becomes available. Today, the library has no public API that yields this final size independently from actually reading the stream data.

Please add a method on a receive stream (and a corresponding convenience method on the bidirectional `Stream` type) that satisfies these acceptance criteria:

* **Signature and behavior.** The method should take a `context.Context` and return `(int64, error)`. It blocks the caller until one of the following is true, then returns the final size (non-zero, zero, or error):
  1. The final size of the receive stream becomes known. The final size is established when the peer communicates it through a STREAM frame with the FIN bit set, or through a RESET_STREAM frame carrying a `FinalSize`. In this case the call returns the size and `nil` error.
  2. The receive stream is closed for shutdown (i.e., `closeForShutdown` is called) before the final size is known. Return `0` and the shutdown error.
  3. The supplied `context` is cancelled or exceeds its deadline. Return `0` and the context’s cancellation cause (`context.Cause(ctx)`).
* **Idempotent / eager return.** If the final size is *already* known when the method is called (e.g., because a FIN or RESET_STREAM arrived earlier), the call must return immediately with the known size and no error.
* **No early unblocking on local cancel.** The application might call `CancelRead` on the same stream before the final size is known. That must *not* cause the new method to return; the method must continue waiting until the peer actually communicates the final size (typically via a subsequent RESET_STREAM). In other words, local cancellation does not guarantee that the final size is known, and the caller explicitly wants the peer’s final size.
* **Convenience accessor on `Stream`.** The bidirectional `Stream` type should expose a method (e.g. `WaitForReceiveFinalSize`) that forwards to the underlying receive-stream’s final-size waiting method, so that callers holding a `Stream` do not need to reach into its internals.
* **Concurrency safety.** The method must be safe to call concurrently with other stream operations, including `Read`, `CancelRead`, and shutdown.

Edge-case reminders:
- The “final size” is the peer’s final offset, i.e., the total number of bytes the peer will ever send on this stream (which can be zero). It is *not* the amount of data already read by the application.
- If the stream has already been closed for shutdown and the final size was never received, the method must return the shutdown error.
- If the context is already expired when the call is made, it should return immediately with the context error.
- Repeated calls after the final size is known must all return the same size without blocking.

The implementation should exist in the receive-stream layer, with minimal surface exposed up to `Stream`. This is a niche API; it can be documented as such, but it must behave correctly with the existing stream lifecycles.

add a method to query for the final size of a receive stream (#5693)


**Implementation request: Fix premature dropping of Initial packet number space for clients**

When a client sends a coalesced Initial+Handshake packet, the crypto layer calls `DropPackets(EncryptionInitial)` *before* `SentPacket()` is called for the Initial portion. This causes the Initial packet number space to be torn down too early, breaking bytes-in-flight accounting and potentially losing data needed for retransmission.

The desired behavior:
- For the **client** perspective, `DropPackets(EncryptionInitial)` should be a no‑op — the caller is signalling that Handshake keys are ready, but the actual drop must be deferred until the first Handshake packet is actually sent.
- As soon as a `SentPacket` call with `EncryptionLevel == EncryptionHandshake` occurs and there are outstanding Initial packets (`initialPackets != nil`), the handler should drop the Initial packet number space (the same work that `DropPackets(EncryptionInitial)` would normally do).
- The **server** perspective must remain unchanged: `DropPackets(EncryptionInitial)` should continue to drop the space immediately, as it does today.

Edge cases / safety:
- If multiple Handshake packets are sent, only the first one should trigger the drop.
- After the drop, `bytesInFlight` must reflect only the Handshake (and later) packets; no frames from the discarded Initial packets should be queued for retransmission.
- The existing `DropPackets` logic (removing packets from bytes_in_flight, clearing history, resetting the packet number space pointer) should be reused, not duplicated.

Implement this fix in `internal/ackhandler/sent_packet_handler.go`.

fix dropping of the Initial packet number space for clients


**Title:** `protocol.KeyPhaseBit` needs an undefined/absent state; qlog workaround is fragile

**Description**
The key phase bit exists only in 1-RTT (short header) QUIC packets. Long header packets (Initial, Handshake, 0-RTT) do not carry a key phase bit at all.
Currently `protocol.KeyPhaseBit` is defined as `bool`, so it only has two states: zero (`false`) and one (`true`). There is no value that means “not applicable / absent”.

Because of this, any code handling a `KeyPhaseBit` that might originate from a long‑header context has to carry an extra `hasKeyPhase` boolean to decide whether the value is meaningful. The most prominent example is the qlog packet‑header serialization, where `packetHeader` has a separate `hasKeyPhase` field used to guard the `key_phase_bit` JSON key. That duplication is error‑prone and makes it easy to accidentally emit `"key_phase_bit":"0"` for packets that should not have the field at all.

**Expected behavior**
1. `protocol.KeyPhaseBit` should become a type that can represent three values: *undefined*, *0*, and *1*.
2. Its zero value should be the undefined state (so simply zero‑initializing a `KeyPhaseBit` means “no key phase”).
3. `KeyPhaseBit.String()` should return `"undefined"` for the undefined state, and `"0"` / `"1"` for the two valid phases.
4. `KeyPhase.Bit()` should return `KeyPhaseZero` or `KeyPhaseOne` (not a `bool`).
5. The logging alias `logging.KeyPhaseBit` must pick up the new type automatically (it is a type alias).
6. In `qlog`:
   - Remove the `hasKeyPhase` field from `packetHeader`. The `KeyPhaseBit` field alone should indicate whether the bit is present.
   - `transformExtendedHeader` should always copy the key phase from the wire extended header (for long headers the zero value will naturally be undefined).
   - The JSON marshaler should emit `"key_phase_bit"` *only* when the value is `KeyPhaseZero` or `KeyPhaseOne`.

**Impact**
- Eliminates the need for a separate presence boolean across the codebase.
- Prevents accidental logging of a key phase bit for long‑header packets.
- Makes the protocol type represent the domain more faithfully, reducing the chance of future bugs in any code that inspects the key phase.

**Acceptance criteria**
- `KeyPhaseBit` is no longer a `bool`.
- A zero‑valued `KeyPhaseBit` evaluates to “undefined”.
- qlog output contains `key_phase_bit` only for short‑header packets, and is absent for long‑header packets.
- Tests cover the new undefined state, the string representation, and the qlog serialization behaviour.

use a uint8 to represent the key phase bit


**Title:** qlog: add `preferred_address` to transport parameter events

**Description**
When transport parameters are emitted as a qlog `transport:parameters_set` event, the `PreferredAddress` field (if present) is currently not included; the code only has a TODO comment. This makes it impossible to trace server-preferred addresses through qlog.

We need to implement serialization of the preferred address in the transport parameters event.

**Requirements**
- When `TransportParameters.PreferredAddress` is non-nil, the event JSON must contain a `preferred_address` sub-object with fields:
  - `ip_v4` (string, e.g. `"12.34.56.78"`)
  - `port_v4` (uint16)
  - `ip_v6` (string, e.g. `"102:304:506:708:90a:b0c:d0e:f10"`)
  - `port_v6` (uint16)
  - `connection_id` (string, hex-encoded)
  - `stateless_reset_token` (string, hex-encoded)
- When `PreferredAddress` is nil, the `preferred_address` key must be omitted entirely from the event JSON.
- The logging types already alias `wire.PreferredAddress` in `logging/interface.go` so the qlog layer can receive a `*logging.PreferredAddress` value; use that.
- The change must follow existing patterns for qlog event marshaling (i.e., implement the `gojay.MarshalerJSONObject` interface for the address sub-object).

**Context**
This affects the `transport:parameters_set` event emitted in qlog when local or remote transport parameters are recorded. The wire structure `PreferredAddress` contains both IPv4 and IPv6 addresses, the respective ports, a connection ID, and a stateless reset token. The current event struct (`eventTransportParameters`) has the TODO comment `// TODO: add the preferred_address`.

implement qlogging of the preferred address in the transport parameters


**Title:** HTTP/3 server sends corrupt frames on the stream when a handler panics after writing part of a response

**Description:**
When an application-defined HTTP handler panics during request processing, the quic‑go HTTP/3 server currently catches the panic and unconditionally writes an `HTTP 500 Internal Server Error` response onto the stream. If the handler had already started sending a response (e.g., called `WriteHeader` or `Write` before the panic), the stream ends up with *two* independent HTTP responses—the partial original response followed by a new set of HEADERS carrying `:status 500`. This violates the HTTP/3 framing requirements of RFC 9114 and causes decoding failures or stream resets on the client side.

**Expected behavior:**
After recovering from a handler panic, the server must **not** send any additional HTTP response on that stream. The stream’s state is unknown once the handler panics; trying to inject a second response is unsafe and protocol‑invalid. Instead, the server should suppress the outgoing response entirely for that request and simply tear down the stream / connection (logging the panic if necessary). This is analogous to how Go’s standard `net/http` server behaves when a handler panics after headers have been written: it does not attempt to send a second status line.

In short: the server should only produce an explicit `200 OK` response (or the handler’s chosen status) when the handler completes normally. On panic, **no** HTTP response frames should be emitted for the request.

**Impact:**
Clients receive corrupt HTTP/3 streams and experience connection failures, especially when using middleware or libraries that write response headers early and may later panic. This is a correctness bug that breaks interoperability with spec‑compliant HTTP/3 clients.

http3: don't write response headers if the handler panicked (#3950)


The HTTP/3 stream wrapper and request stream currently carry their own `closeConnection` callbacks, leading to duplication and potential inconsistency: the base stream’s frame parser and the request stream’s response reader both need to close the QUIC connection on protocol errors, but they use separate callbacks that callers must supply. Simplify the composition so that the stream wrapper directly holds the `quic.Connection` and calls `CloseWithError` on it when it encounters an unexpected frame or a decoding error. Remove the `closeConnection` callbacks and the extra field from the request stream wrapper, and update all call sites (client `SingleDestinationRoundTripper`, server `handleRequest`) to pass the connection instead of a callback. The tests must be adjusted to provide a mock connection in place of the old error callbacks.

http3: simplify composition of the HTTP stream and request stream (#4433)


### Simplify retransmission queue API to reduce per-encryption-level duplication

The current retransmission queue API exposes a separate set of methods for each encryption level:
- `HasInitialData()`, `HasHandshakeData()`, `HasAppData()`
- `GetInitialFrame(maxLen, v)`, `GetHandshakeFrame(maxLen, v)`, `GetAppDataFrame(maxLen, v)`
- `InitialAckHandler()`, `HandshakeAckHandler()`, `AppDataAckHandler()`

This forces callers (notably in `packet_packer.go`) to manually switch on encryption level and call the corresponding method, leading to repetitive, verbose code that is easy to get wrong or miss a case. It also creates an explosion of test cases that mechanically repeat the same logic for Initial, Handshake, and AppData.

**Request:** Unify the API into three methods parameterized by `EncryptionLevel`:
- `HasData(encLevel) bool`
- `GetFrame(encLevel, maxLen protocol.ByteCount, v protocol.Version) wire.Frame`
- `AckHandler(encLevel) ackhandler.FrameHandler`

The old per-level methods should be removed (or made internal/private helpers). The new `AckHandler` method must correctly handle `Encryption0RTT` by returning the same app-data ack handler used for `Encryption1RTT`, since 0‑RTT retransmissions are ultimately sent on 1‑RTT packets.

Update all internal callers (e.g., the packet packer) to use the new unified methods, eliminating the existing `switch encLevel` blocks. Tests should be updated accordingly to use the new API, preserving all existing behavioral coverage (frame splitting, order, ack handler assignment, etc.).

simplify the API of the retransmission queue (#4793)


Add an `AcknowledgedPacket` method to the `ConnectionTracer` interface.
It should be invoked whenever a sent packet is confirmed acknowledged by the remote.
The method receives the encryption level and the packet number of the acknowledged packet.

The callback must **not** be called for "skipped" packets – those that were never fully transmitted as part of normal sending flow.

All existing implementations of the interface (`connTracerMultiplexer`, the qlog tracer, integration-test tracers, mock tracers) must be updated to satisfy the interface. The multiplexer should forward the call to each sub-tracer; the qlog and test tracers should provide a no-op implementation.

add a function to trace acknowledged packets


Title: Store grouped updates in PullRequestData and adapt repository queries

Description:
`PullRequestData` and the `PullRequestRepository.Entry` currently hold an `Update`. We recently added `GroupedUpdate` (a subtype of `AnUpdate`), and `UpdateData` now carries an `AnUpdate` instead of a plain `Update`. `NurtureAlg` should pass that `AnUpdate` through to `PullRequestData`. Consequently, `PullRequestData` and the repository entry need to store `AnUpdate`.

The repository has several query methods that rely on `Update`-specific fields (`nextVersion`, `withNewerVersions`, `groupId`, `mainArtifactId`). These must be adapted so they only apply their logic when the stored value is an actual `Update`, and safely ignore `GroupedUpdate` entries where those operations do not apply. The affected methods are:

- `getObsoleteOpenPullRequests` – should match only entries that are `Update` and have a lower `nextVersion`.
- `findLatestPullRequest` – should consider only `Update` entries that match the given cross-dependency and version.
- `lastPullRequestCreatedAtByArtifact` – should group by artifact identity only for `Update` entries; grouped updates have no single artifact and should be excluded from this map.
- `lastPullRequestCreatedAt` – should still take all entries (including grouped updates) into account when reporting the most recent PR timestamp, because grouped updates correspond to real pull requests.

The stored data uses the existing `AnUpdate` codec, so no migration is required. Implement the necessary type widening and the per-method filtering so that both plain updates and grouped updates are handled correctly.

Update `PullRequestData` to store `AnUpdate` values instead of just `Update`


We have two separate representations for the result of reading a `.scala-steward.conf` file: one used by `RepoConfigAlg` (an `Option[Either[io.circe.Error, RepoConfig]]`) and another used by `ValidateRepoConfigAlg` (a sealed trait `ConfigValidationResult` with cases `FileDoesNotExist`, `ConfigIsInvalid`, and `Ok`). They describe exactly the same three outcomes—file missing, invalid content with an error, and successfully parsed config—but they are separate types and lead to duplicated parsing logic.

Unify them so that:
1. There is a single result type covering the three possibilities: file not found, config parse error (with the `io.circe.Error`), and valid config (with the `RepoConfig`).
2. Both the repo-config reading path (`RepoConfigAlg.readRepoConfig`) and the validation command (`ValidateRepoConfigAlg.validateAndReport`) use the same result type.
3. The validation command delegates to the same file-reading and parsing logic that the repo-config reader uses, rather than reimplementing it. The externally visible messages from validation must remain identical: "Configuration file at X does not exist!" for missing files, position-specific error details for invalid content, and "Configuration file at X is valid." for valid files.
4. The repo cache computation (`RepoCacheAlg`) must still extract the optional parsed config and the optional parsing error from the unified result, and the logging of parsing errors must not change (the message "Failed to parse .scala-steward.conf: ..." must still appear in the same circumstances).
5. The global-config merging (`mergeWithGlobal`) must continue to receive the optional `RepoConfig` derived from the result.

No observable behavior should change—only the internal types and the sharing of parsing logic should be adjusted. The existing tests for default config, valid config, and invalid config (with error message extraction) should still pass.

Merge ConfigParsingResult and ConfigValidationResult


Add an optional `authors` field to `ScalafixMigration`. It should be a list of `Author` objects (each with `name` and `email`), decoded from the `scalafix-migrations.conf` HOCON. When a Scalafix migration commit is created, include a `Co-authored-by: Name <email>` line for each author in the commit message. The co-author lines must appear after any existing “See … for details” line that comes from the `doc` field. If the `authors` field is absent, no co-author lines should be added. Ensure the `Author` case class can be decoded via Circe and can format itself as `Name <email>`. Existing migration definitions in the config and any code that constructs commit messages must be adjusted to honor the new field, without breaking backward compatibility.


**Feature Request: Make `validate-repo-config` a standalone subcommand**

The current `scala-steward` CLI includes a `--validate-repo-config <file>` option. This works, but it has an annoying requirement: the user must also provide every mandatory argument for the main scrape loop (`--workspace`, `--repos-file`, `--git-author-email`, `--git-ask-pass`, `--vcs-api-host`, `--vcs-login`, etc.) even though validation doesn't use them.

The wanted behaviour:

1. **New usage**: `scala-steward validate-repo-config <config-file>`
   - The old `--validate-repo-config <file>` option is removed from the main command's option set.
   - The new form does not require any of the normal scrape-related options; it only needs the path to the config file.
   - The tool reads the file, validates it against the repo-config schema, and reports whether it is valid.

2. **Output and exit codes**:
   - If the file is valid, print an informational message (to stdout / standard logging channels) and exit with code 0.
   - If the file is invalid, print one or more error messages describing the validation failures (preferably to stderr / error log) and exit with a non-zero code.
   - If the file does not exist, treat this as a validation error (exit non-zero, appropriate message).

3. **Help text**:
   - `scala-steward --help` should show both usage lines: the normal `scala-steward [options]` form and the `scala-steward validate-repo-config <file>` form.
   - The option listing for the main command must no longer mention `--validate-repo-config`. The subcommand should be listed separately (e.g., under a `Subcommands:` section) with a short description.

4. **Backwards compatibility**: The old `--validate-repo-config` flag should be completely removed; there is no need to support both forms simultaneously. Any script relying on the old flag can be updated to use the new subcommand.


The `string.longestCommonPrefixGreater` utility uses `refined` (Refined + MinSize) and `shapeless` (Witness.Aux) to guarantee at the type level that the returned common prefix has length at least `N`. This makes the code incompatible with Scala 3.

Replace that method with a plain Scala method that takes the minimum desired length as an `Int` argument and returns an `Option[String]`: `Some(prefix)` if the longest common prefix length is >= the given threshold, otherwise `None`.

Update the two call sites to use the new method:
- `Update.ForGroupId.artifactIdsPrefix` currently calls `longestCommonPrefixGreater[3]` and exposes an `Option[MinLengthString[3]]`; it should become `Option[String]`, still requiring a prefix of at least 3 characters.
- `Selector.heuristic1SearchTerms` accesses `prefix.value`; that `.value` call must be removed after the type change.

The core behavior and the minimum-length contract must stay the same; only the type-level encoding is being dropped. Remove the `MinLengthString` type alias and the `refined`/`shapeless` imports from the string utility.

Rewrite `longestCommonPrefixGreater` withtout refined


Scala Steward’s automatic PR labels are incomplete when a PR groups multiple dependency updates together. The labels need to represent the full set of changes across all grouped updates, not just one of them.

## Current behavior

When a PR combines several dependency updates (e.g., via pull request grouping configuration), the labels applied to the PR reflect only a single update’s type and version-change information. This means labels like `test-library-update`, `sbt-plugin-update`, `early-semver-major`, or `semver-spec-minor` may be missing for some of the updates included in the group.

For example, if a grouped PR updates a regular library from `1.0` to `2.0` and a test library from `1.1.0` to `1.2.0`, the PR should carry both `library-update` and `test-library-update`, as well as `early-semver-major` and `early-semver-minor`. Currently only a subset of these labels appears.

## Expected behavior

1. **Union of labels across grouped updates**
   For any PR that represents a group of updates (`GroupedUpdate`), the computed label set must be the union (with duplicates removed) of the labels that would be generated for each individual update in the group. This applies to:
   - Update type labels (`library-update`, `test-library-update`, `sbt-plugin-update`, `scalafix-rule-update`)
   - Semantic versioning labels (`early-semver-*`, `semver-spec-*`)

2. **Single-update PRs remain unchanged**
   Labels for PRs that contain a single update must not be affected.

3. **Use of the correct update representation**
   The label generation logic should work with `AnUpdate` (which can be either a single `Update` or a `GroupedUpdate`) so it can handle both cases. The call site that invokes the label computation must pass the overall update from the `UpdateData` (`data.update`) rather than a previous single-update field.

4. **Non-goal**
   This does not change the behavior of other label sources (e.g., `scalafix-migrations`, `old-version-remains`, commit count) except as necessary to combine them correctly with the new per-update labels.

The fix should ensure that PR labels accurately reflect every dependency change included in the pull request, making it easier to understand the scope of the update at a glance.

Update `NewPullRequestData.labelsFor` to work with `AnUpdate``


**Task: Thread `RepoCache` from `RepoCacheAlg` to `PruningAlg` instead of reloading it**

Currently, during the main processing flow, `RepoCacheAlg.checkCache` is responsible for fetching (or refreshing) the repository cache, but it only returns the `RepoOut` fork information. Later, `PruningAlg.needsAttention` independently loads the same `RepoCache` from the persistent store again, causing redundant deserialization and an extra database read.

We should change the data flow so that the already-available `RepoCache` is obtained in `checkCache` and passed directly into `needsAttention`, eliminating the redundant fetch.

**Requirements:**

- `checkCache` should return both the `RepoCache` object and the `RepoOut` fork information to the caller. The internal methods that compute or update the cache (`cloneAndRefreshCache`, `refreshCache`) must be adjusted to propagate the `RepoCache` up instead of just `Unit`.
- `PruningAlg.needsAttention` should be modified to accept the `RepoCache` as a parameter and use it directly, rather than loading it itself. The `RepoCacheRepository` dependency and the related `findCache` call inside `PruningAlg` can then be removed.
- The now-unused `findSha1` method on `RepoCacheRepository` (which was only used by `checkCache` to get the cached SHA) should be removed, since `checkCache` will instead fetch the full `RepoCache` and extract the SHA from it.
- All callers (e.g., the main steward algorithm) must be updated to destructure the new return type from `checkCache` and pass the cache to `needsAttention`.
- The behavioural invariants should remain identical: if the cache is stale or missing, it is recomputed and persisted as before, but now returned; if cached, the existing cache is reused exactly once.

This refactoring removes unnecessary work and makes the cache flow more explicit.

Pass RepoCache from RepoCacheAlg to PruningAlg


**Title:** Use `git commit --trailer` for co-authored-by trailers, not a body paragraph

**Background**
When Scala Steward creates commits with co-authors (e.g., from Scalafix migrations or hooks), the `Co-authored-by: …` lines are currently inserted as the last paragraph of the commit message body via a regular `-m` argument. This means Git treats them as ordinary body text, not as structured trailers.

**Problem**
- Trailers formatted as a body paragraph aren’t recognized by Git’s trailer machinery.
- It doesn’t compose with other `git commit` options that add trailers (e.g., `--signoff`).

**Goal**
Make Scala Steward produce commits where the co-authors are passed to `git commit` using the `--trailer` option, so that Git handles proper formatting and separation of trailers from the body.

**Concrete requirements**

1. **CommitMsg data model**
   - The current `toNel` method returns a non‑empty list (`Nel`) that combines the title, body, and a final “paragraph” containing the co-authored‑by lines.
   - Instead, expose two separate pieces:
     a) the title and body paragraphs (as they are now, without the trailer lines) – currently returned by `toNel`, suggest renaming this to something like `paragraphs`.
     b) the co‑author trailers as a list of key‑value pairs (key `"Co-authored-by"`, value the author string as produced by `Author.show`).
   - Rename `withParagraph` to a name that better expresses it appends a body paragraph (e.g., `appendParagraph`).

2. **Git invocation (FileGitAlg.commitAll)**
   - Build the `git commit` command so that:
     - title and each body paragraph are still passed via `-m`.
     - each co‑author trailer is passed via `--trailer` in the form `--trailer "Co-authored-by=Name <email>"`.
   - Do **not** include the co‑author lines in the `-m` arguments.

3. **Callers**
   - The hook executor uses `withParagraph` to add a command‑executed note. Update to use the new name.
   - Any other place that constructs commit messages using the old `toNel`/`withParagraph` signatures must be adjusted accordingly.

4. **Tests**
   - Update the Scalafix migration test that currently checks `toNel` output. It should now assert directly against the title, body paragraphs, and co‑authors of the `CommitMsg` object, rather than a single combined list.
   - Ensure an existing or new integration‑style test (e.g., in `FileGitAlgTest`) exercises a commit with a co‑author, so we are confident the `--trailer` option is used and the commit is created successfully.

**Non‑functional notes**
- The change must not alter the content or order of title/body/co‑authors in the final commit message as seen by humans; only the mechanism of passing them to Git changes.
- The file `CommitMsg.scala` likely holds the data class; `FileGitAlg.scala` holds the concrete `git` command.

Use Git's `--trailer` for commit message trailers


**Title:** Support Mill 0.11+ plugin loading via `--import`

**Problem**
Scala Steward fails to extract dependencies from projects that use Mill 0.11 or later. Mill 0.11 changed its plugin loading mechanism: the old `-p <predef-file>` approach no longer works; instead plugins must be loaded with the new `--import` flag and an Ivy coordinate.

**Desired behavior**
Scala Steward should automatically detect the Mill version and load its dependency-extraction plugin using the appropriate method for that version, without any change to the projects it processes.

**Acceptance criteria**

1. **Mill ≥ 0.11**
   When the Mill version is 0.11.0 or higher, Scala Steward must execute the dependency extraction without creating or referencing a predef file. It must pass the plugin Ivy coordinate directly via the `--import` flag, e.g.
   `mill -i --import ivy:<org>::<artifact>::<version> show …`
   The coordinate format and contents must be derived from the same plugin coordinates currently used in the predef imports.

2. **Mill < 0.11 (including unknown versions)**
   For all Mill versions below 0.11, and for cases where the version cannot be determined or its minor component cannot be parsed, the existing predef‑based flow must be used unchanged:
   `mill -i -p <predef-file> show …`

3. **Version detection**
   The version is read from the usual locations (`.mill-version`, `.config/mill-version`) — this logic already exists and should not be duplicated.

4. **Backward compatibility**
   The behavior for Mill versions `< 0.11` must be identical to the current implementation. The predef content generation and its handling of specific Mill minor versions (0.6, 0.7/0.8, 0.9, and the `$MILL_BIN_PLATFORM` fallback) must remain unchanged.

5. **Scope**
   The change must be confined to the component responsible for executing Mill commands and extracting dependencies. No modifications to the Mill plugin itself are required.


We need to refactor the optional update-limit logic in the dependency-update nurturing process to make it more composable and to inline a helper that currently lives in a companion object.

**Context**
- The `NurtureAlg` companion object defines a method `processUpdates` that applies a function to each update in a list, but stops early if a `limit` (count of non-ignored results) is configured. It uses `fs2.Stream` and `util.takeUntil`.
- This method is only used from `NurtureAlg.updateDependencies`, which passes a grouping and the limit from configuration.
- Having the logic in a separate companion object method adds indirection and mixes list/stream handling.

**Desired behavior**
1. Move the optional limit behavior into a reusable `Pipe` combinator in the `util` package object. The combinator should accept an optional limit:
   - When `Some(limit)`, behave exactly like `util.takeUntil(init, limit)(weight)`.
   - When `None`, behave as the identity pipe (pass through all elements unchanged).
   Name the combinator `takeUntilMaybe`.
2. Inline the stream processing directly into `NurtureAlg.updateDependencies`, eliminating the companion object method `processUpdates`.
   - Group the updates by groupId before passing them in (this grouped list is the stream source).
   - For each update, create the branch and `UpdateData`, then apply `processUpdate`.
   - Apply `takeUntilMaybe` with the configured optional limit and the existing weight logic (Ignored weighs 0, Updated/Created weigh 1).
3. Delete the `NurtureAlg` companion object completely (the `processUpdates` method is its only member). Remove any unused imports that become dead as a result.

**Acceptance criteria**
- When `config.updates.limit` is not set, all updates are processed regardless of their outcome.
- When `config.updates.limit` is set to `N`, processing stops after `N` updates that result in `Updated` or `Created`; `Ignored` results do not count toward the limit.
- The external behavior of `NurtureAlg.nurture` (the caller) does not change.
- The new `takeUntilMaybe` utility is available in `org.scalasteward.core.util` and can be used by any future stream pipeline that needs an optional limit.

Add `takeUntilMaybe` and inline `NurtureAlg.processUpdates` (#2421)


Title: Simplify `KeyValueStore` trait to avoid duplicated write logic across implementations

Description:
The `KeyValueStore` trait (in `org.scalasteward.core.persistence`) currently declares `get`, `put`, and `modifyF` as abstract methods. This forces every implementation to supply write logic in two places. For example, `JsonKeyValueStore` has a private `write` helper that is called by both `put` and `modifyF`. This duplication would be repeated in any other store implementation.

We should refactor the abstraction so that a single abstract method `set(key: K, value: Option[V]): F[Unit]` replaces `put` and `modifyF` as the write entry point. Then `put` and `modifyF` can be default-implemented in the trait in terms of `get` and `set`:

- `put` simply calls `set(key, Some(value))`.
- `modifyF` performs a `get`, applies the function `f`, and writes the result via `set` (using `flatTap` or equivalent).

The `update` method should also be removed; its functionality is a special case of `modify` and can be expressed directly with `modifyF`.

Expected outcome:

- `KeyValueStore` declares only `get` and `set` as abstract methods.
- `put` and `modifyF` are final, concretely implemented in the trait.
- `update` is deleted from the trait.
- `JsonKeyValueStore` implements `set` instead of `put` and `modifyF`, and its private `write` helper can be removed.
- The behavior of `put`, `modifyF`, and `get` remains identical to before, but all writes now flow through `set`.

Implement KVStore.{put, modifyF} in terms of set


# Clean up VCSRepoAlg: move `doNotFork` guard to `cloneAndSync` and separate logging

## Problem

`VCSRepoAlg.syncFork` internally checks `config.doNotFork` and becomes a silent no‑op when forking is disabled. At the same time, `cloneAndSync` logs a single message that tries to cover both clone and sync in one conditional (either `"Clone …"` or `"Clone and synchronize …"`). This interleaving obscures the actual control flow and can hide logic errors.

## What we want

1. **Decision at the top level** – `cloneAndSync` should be the single place that decides whether to attempt a fork synchronization. The sync step should be skipped entirely when `doNotFork` is `true`.

2. **`syncFork` with clear semantics** – `syncFork` must **not** inspect `doNotFork` in any way. It should always attempt to synchronize with the fork’s parent. If the provided `RepoOut` has no parent, it should fail immediately (the existing `parentOrRaise` behaviour). This makes the intent explicit and avoids silently masking a missing parent.

3. **Logging per step** – Log a `"Clone <repo>"` message unconditionally when cloning starts. When a fork synchronization is performed, log a `"Synchronize with <parent>"` message. Remove the combined `"Clone and synchronize …"` log.

4. **General tidy up** – As part of this refactoring, extract the sub‑module initialization into its own small helper to keep `cloneAndSync` clean and improve readability (no change in behaviour).

## Background

The overall sequence today is: clone the repo, synchronize the fork (if applicable), then initialise sub‑modules. The restructuring should preserve this order exactly. The only change is who owns the `doNotFork` decision and what is logged.


**Feature request: Show release notes URL from POM in update PR body**

When Scala Steward opens a pull request or updates an existing one to bump a dependency, the PR body normally includes helpful links such as a link to the library's repository, a changelog, or a comparison of versions. Many libraries now embed a `releaseNotesUrl` property in their published POM metadata (in the `<properties>` section) pointing to the project's release notes. Steward should pick up this information and reflect it in the PR body as an explicit "Release notes" link.

**Acceptance criteria**

* **Extract the property:**   When retrieving metadata about the *new* version of a dependency (from its POM, using the same coursier-based machinery that already finds the homepage and SCM URL), also extract any `releaseNotesUrl` property value.
* **Valid URL requirement:**   Only treat a candidate URL as valid when it is an absolute URL that includes a scheme (e.g. `http`/`https`). Malformed or scheme-less strings should be ignored.
* **Existence check:**   Before including the URL in the PR body, send a `HEAD` request to verify that the endpoint is reachable. This is the same existence check already applied to other release-related URLs (changelogs, version diffs, etc.).
* **Appearance in the PR:**   If a reachable `releaseNotesUrl` is found, include it in the PR body as a "Release notes" link, alongside any other release-related links that would normally be generated (e.g. GitHub release, custom changelog, version comparison).
* **No regression:**   Dependencies whose POMs do **not** define a `releaseNotesUrl` property, or whose property value is not a valid/reachable URL, must produce the same set of release-related links as before (no new links and no missing existing links).
* **Parent POM inheritance:**   If the immediate POM does not contain the property, but a parent POM (as declared in the POM) does, the parent's `releaseNotesUrl` should be used, consistent with how homepage/scm fallback works today.
* **Concurrent updates:**   When a single PR updates multiple dependencies, the release notes link for each dependency should be discovered and displayed independently (i.e. one per dependency, if present).

Show release notes URL from POM in PR body


RefreshErrorAlg.failedRecently currently writes to the key-value store on every call, even when the stored entry has not expired. This causes unnecessary I/O for a read-only check. Change the implementation so that:

* when no entry exists, return false without writing;
* when an entry exists and has not expired, return true without writing;
* when an entry exists and has expired, clear the entry (write None) and return false.

Separately, the KeyValueStore.modify convenience method (which is only used by failedRecently) unconditionally calls set after every modification, making it too easy to introduce wasteful writes. Remove that method from the KeyValueStore trait. Ensure any callers that need its functionality switch to modifyF or a direct get/set pattern, preserving correct behavior with minimal writes.

Do not unnecessarily write to KVStore in RefreshErrorAlg


As a maintainer, I want the bot to parse each repository's `.scala-steward.conf` configuration file exactly once per run, rather than twice, to reduce wasted I/O and eliminate any risk of inconsistencies between the parsed values. Currently, the configuration is read and merged with defaults in two separate places: once when building the dependency cache (`RepoCacheAlg`) and again when the build tool dispatcher (`BuildToolDispatcher`) determines build roots and gathers dependencies or runs migrations. Both reads happen during the same processing of a repository, so the second read is unnecessary.

The configuration should be loaded and merged only in the cache-building phase (where it is already read), and the resulting merged `RepoConfig` should be passed downstream to consumers such as the build tool dispatcher, the edit/migration logic, and any other code that currently reads the repo config independently. After this change, the `BuildToolDispatcher` and migration-related operations should receive the already-merged config from the cache rather than triggering their own file reads, and the repo processing should use a single source of truth for the configuration throughout the run.


Long paths in the Open File prompt overflow and cause layout issues. On macOS, temp directory paths like `/private/var/folders/…/T/.tmpXXX/project/` easily exceed the prompt width, pushing the cursor and input text off-screen.

Truncate the displayed path when the combined length of `"Open: "` + directory path + input text exceeds 90% of the prompt width. The truncated form should show the first path component, a visually distinct `[...]` ellipsis segment, and as many trailing components as fit. For example:

```
/private/var/folders/.../T/.tmpXXX/project/  →  /private/[...]/project/
```

Apply the same truncation to the file browser popup's title bar when the path is wider than the popup.

- The ellipsis marker `[...]` must use a distinct, more visible style (use the existing `menu_highlight_fg` theme color) so it doesn't blend with the dimmed directory text.
- The non-ellipsis parts of the truncated path use the existing dimmed directory style.
- The truncation must be length-aware: only truncate when needed, and ensure the truncated result never exceeds the target width.
- The cursor position in the prompt must account for the truncated display length, not the original path length.
- Edge cases: single-component paths (root `/`), paths that already fit, and paths where even the suffix alone is too long should all degrade gracefully without panicking or producing empty output.

Truncate long paths in Open File prompt with styled [...]


**Title:** Deleting a custom keybinding recorded with `Shift` + letter (e.g., `Shift+N`) does not persist across saves

**Behavioral description**
When a user creates a custom keybinding in the keybinding editor by pressing a `Shift`-letter combination (e.g., `Shift+N`) and later deletes that binding through the editor, the deletion is lost after saving and reopening the keybinding editor. The binding reappears in the list as if it was never deleted. This problem only affects bindings that involve `Shift` plus a letter key; plain letter bindings or non-letter keys work correctly.

**Steps to observe the problem**

1. Open the keybinding editor (for example, via the command palette).
2. Add a new binding: press `a`, then record the key combination `Shift+N`, tab to the action field, enter a valid action (e.g., `duplicate_line`), accept the autocomplete, press `Enter` to save the dialog.
3. Save and close the keybinding editor (e.g., `Ctrl+S`).
4. Reopen the keybinding editor and verify that the `Shift+N` binding is listed, coming from the “custom” source.
5. Select that binding row and press `d` to delete it. Verify the row disappears from the table.
6. Save and close the keybinding editor again.
7. Reopen the keybinding editor.

**Current (buggy) behavior**
After step 7, the `Shift+N` binding reappears in the table. The deletion was not persisted in the configuration. The binding continues to reappear every time the editor is saved after a deletion attempt.

**Expected behavior**
After step 6 the custom binding must be permanently removed from the underlying configuration. Reopening the keybinding editor (step 7) must show no trace of that binding—it should not be present in the table and its source should not appear. The same behaviour is required for any `Shift`+letter combination (e.g., `Shift+A`, `Shift+Z`).

**Additional acceptance criteria**

- Deletion of an existing `Shift`+letter binding must work end‑to‑end: the binding disappears from the UI immediately after deleting, stays absent after saving, and remains absent after a fresh reopen.
- The user‑visible key representation (e.g., `Shift+N`) must remain consistent between creation, display after reload, and at deletion time.
- Existing custom bindings that do not involve `Shift`+letter must not be affected. No regressions in add, edit, or delete of other key combinations are acceptable.
- The issue is not limited to a specific letter; it applies to all letters when combined with the `Shift` modifier.

Fix Shift+letter keybinding deletion not persisting


## LSP auto_start flag ignored when multiple servers share a language

**Severity:** Bug
**Affects:** LSP manager auto-start logic (`force_spawn` / `try_spawn`)
**Reported by:** user report — Discord + issue tracker

### Summary

When a language has more than one LSP server configured, opening a buffer for that language spawns *every* enabled server, ignoring each server's own `auto_start` flag. Users expect that setting `auto_start: false` on an individual server will keep it dormant until manually started, but instead it gets dragged in whenever *any* other server for the same language is eligible to auto-start.

### Minimal reproduction

User config (`config.json` excerpt, real scenario from report):

```json
{
  "lsp": {
    "rust": [
      { "name": "rust-analyzer", "command": "rust-analyzer", "enabled": true, "auto_start": true },
      { "name": "typos-lsp",      "command": "typos-lsp",      "enabled": true, "auto_start": false }
    ]
  }
}
```

*Expected:* Opening `main.rs` spawns only `rust-analyzer`. `typos-lsp` stays dormant — the user intends to start it manually via the command palette only when proofreading.

*Actual:* Opening `main.rs` spawns both servers.

The same bug manifests with `universal_lsp` entries. A universal server with `enabled: true, auto_start: false` gets spawned whenever *any* per-language auto-start server fires up for an opened buffer.

### Root cause analysis

`try_spawn()` checks whether *any* config for the language has `auto_start=true && enabled=true`. If so, it delegates to `force_spawn()`. But `force_spawn()` today spawns every configured server whose `enabled=true`, without inspecting the per-server `auto_start` flag.

The manual-start paths (command palette "Start LSP" / "Restart LSP", the LSP confirmation popup after `allow_language`) need to keep their current behavior: they should still spawn every configured server regardless of `auto_start`. Only the automatic path (buffer open, crash recovery) should filter by per-server `auto_start`.

### Required behavior

1. **On buffer open (auto-start path):** Only servers with *both* `enabled: true` AND `auto_start: true` for the buffer's language should spawn. Servers with `auto_start: false`, or belonging to a different language, must not spawn.

2. **Manual start (command palette / popup):** All configured servers for the language should spawn, honoring `enabled` but ignoring `auto_start`. The existing "start a disabled server" override (via `allowed_languages`) should continue to bypass even `enabled: false`.

3. **Crash recovery** of an already-running server should follow the auto-start rules (respect `auto_start`), since this path is not a user-initiated manual action.

4. **No language confusion:** A server configured for language X must never auto-spawn when a buffer of language Y is opened, even if Y happens to share configuration lookup paths. (This should already hold but must remain true after the fix.)

### Non-goals

- Changing the semantics of `allowed_languages` or `disabled_languages`.
- Changing how the command palette "Start LSP" / "Restart LSP" / "Start LSP Server…" commands populate the `allowed_languages` set.
- Altering `universal_lsp` merging logic — those configs are correctly appended to per-language lists at startup.

### Edge cases to preserve

- A server with `enabled: false, auto_start: true` must not auto-spawn (both flags required).
- A server with `enabled: true, auto_start: false` must not auto-spawn, but must be spawnable manually.
- An empty `command` field should still skip spawning (existing guard).
- The `manual_restart` and `manual_restart_server` paths must remain unaffected — they set up `allowed_languages` and call into `force_spawn`.

### Acceptance criteria

- On buffer open, only `auto_start=true && enabled=true` servers spawn.
- Manual LSP start/restart commands still spawn all configured servers.
- Crash recovery behaves like auto-start, not manual start.
- Opening a buffer for language X does not trigger any server configured only for language Y.

fix(lsp): honor per-server auto_start when multiple servers configured


**Title:** Replace-toolbar checked options have heavy highlight background in Dracula (follow‑up to visibility fix)

**Report**

After the recent fix that made checked search options (Case Sensitive, Whole Word, Regex) visible on the Dracula theme, the checked‑state visual is now a solid, opaque highlight block (`[x]` drawn in `menu_highlight` foreground on `menu_highlight` background). This block stands out harshly from the rest of the toolbar’s uniform background and looks heavy and out of place.

**Expected behaviour**

1. Checked options in the find/replace toolbar should keep the toolbar’s normal background (`menu_dropdown` background) and signal the checked state purely through an **accent foreground colour** that contrasts with that background.
2. The same accent colour should be used for other toggle‑indicator text in the editor (e.g. the checked glyph in the plugin search panel’s toggle) so the two search UIs feel consistent.
3. The checked option must be visibly distinct from the unchecked option on every built‑in theme; on Dracula the checked status should **not** require a heavy background block to be legible.

**Context / reproduction**

* Theme: Dracula
* Steps: open any file, trigger find/replace, toggle one of the search options (e.g. Whole Word).
* Observed: the `[x]` appears with a solid highlight‑coloured rectangle that visually breaks the toolbar.
* Expected: the `[x]` uses the toolbar’s own background and an accent foreground that is clearly different from the normal unchecked foreground, without a full‑block highlight.

Use accent fg (not a highlight bg block) for checked toolbar options


After using Alt+N or F3 to cycle through all search matches and wrap back to the first, the viewport recenters around the match.  Pressing Down to read further keeps the cursor moving but the viewport stays stuck for many keypresses – the cursor travels below the visible area and only snaps into view after it’s far past the bottom margin.  This only happens if the search wrapped around; normal Down-arrow scrolling works fine otherwise.

The viewport must immediately follow the cursor after a recenter, scrolling as soon as the cursor leaves the visible region.  The stall appears because an internal scroll-up fine-tuning flag (`scrolled_up_in_wrap`) that was set during the byte-level `ensure_cursor_visible` is not cleared when the subsequent recenter overrides the viewport offset and sets `skip_ensure_visible`.  The flag survives and on the next unskipped render `ensure_visible_in_layout` applies a stale `top_view_line_offset` that pushes the viewport away from the recentered position, hanging the scroll until the cursor’s downward drift eventually re-triggers a full scroll.

In `ensure_active_cursor_visible_for_navigation` (or whatever navigation path forces a recenter), explicitly reset `scrolled_up_in_wrap` alongside the top-byte/offset override so the fine-tune doesn’t fire on stale data.

Clear stale scrolled_up_in_wrap when navigation recenters


**Title:** Ctrl+F3 (find_selection_next) hijacks the search with a multi‑word query when the cursor is on a non‑word character

**Environment**
Fresh latest build (including the official 0.x releases). Reproduced on Linux, macOS, and Windows.

**Steps to reproduce**

1. Create a file with this content:
   ```
   sub one
   sub two { (
   sub three
   }
   sub four
   ```
2. Press **Ctrl+F** and type `sub`, then press **Enter** to start searching for “sub”. Four matches are highlighted.
3. Move the cursor to the lone `}` (e.g. by pressing **Ctrl+]** (`goto_matching_bracket`) or by clicking on it). The cursor is now on a non‑word character, and it is *not* on any match of the active search.
4. Press **Ctrl+F3** (`find_selection_next`).

**Expected behavior**
Because the cursor is sitting on a non‑word character (punctuation/whitespace), **no new search query should be extracted**.
- If an existing search is active (the “sub” search), **Ctrl+F3 should simply jump to the next match** of that search.
- If there is no active search, a “No text to search for” status message should appear (or nothing should happen).

**Actual behavior**
Fresh synthesises a *new* search query by taking the “word” around the `}`. Internally, `find_word_start` / `find_word_end` (used heavily by Ctrl+Arrow) skip over the non‑word character and extend into adjacent words, producing a query like `three\n}\nsub` (or `}\nsub`). This unexpected multi‑line term replaces the original “sub” search, the highlight jumps to whatever matches that unintended string, and the user’s search context is lost.

**Additional details**

- The same bug affects **Ctrl+Shift+F3** (`find_selection_previous`).
- It also happens when the cursor is on **whitespace** (e.g. a space between two words).
- The problem only occurs when there is **no selection**; if text is selected, Ctrl+F3 correctly uses the selection.
- The underlying word‑boundary helpers (`find_word_start` / `find_word_end`) are designed for *movement* (Ctrl+arrows), which intentionally steps across punctuation/whitespace to reach the next word. That semantics is wrong for “word under cursor” extraction – when the cursor is not on a word character, the extraction should fail, never silently synthesise a query that spans surrounding tokens.

**Impact**
The bug breaks the common workflow of jumping to a matching bracket (`Ctrl+]`) and then continuing to search for a previously entered term. It also makes Ctrl+F3 behave unpredictably on punctuation and spaces. A fix should check whether the cursor is on a word character before attempting to extract a “word under cursor”, and should fall back to navigating the existing search (if any) rather than clearing it.

fix(search): don't hijack search query when find_selection_(next|previous) is invoked on a non-word character


**Title:** Audit mode (review diff): hunk navigation centering, Alt-key conflicts, and discard confirmation

**Background**
The `audit_mode.ts` plugin (`review-mode`) uses Alt+key combos for hunk actions (Alt+c comment, Alt+a approve, Alt+x reject, Alt+r refresh, Alt+e export, Alt+s stage, Alt+u unstage, Alt+d discard).  In the side-by-side diff drill-down, hunk navigation (next/prev) scrolls the view so the hunk header sits exactly at the top.

**Issues and requirements**

1. **Hunk navigation centering & cursor tracking**
   - **Current:** `next/prev hunk` sets `scroll_row` to the hunk header row, placing it at the very top of the viewport. The virtual cursor is not moved. Searching for the next hunk uses `scroll_row`, so if the scroll is offset (e.g., from centering) the same hunk may be re-found.
   - **Desired:**
     - On hunk navigation, center the hunk header ~⅓ of the viewport height from the top, so users see context lines above.
     - Move the composite-buffer cursor (`cursor_row`) to the hunk header row.  This cursor row (not `scroll_row`) should be used as the starting point for subsequent next/prev hunk searches, preventing re-detection of the same hunk.
     - The centering must use the actual viewport height (accounts for splits/terminal size) to compute `scroll_row = hunk_row - viewport_height/3`.

2. **Keybinding conflicts with menu mnemonics**
   - **Current:** `Alt+S`, `Alt+E`, `Alt+X`, etc. match global menu mnemonics (File, Edit, Selection), so the mode bindings are frequently overridden and don’t work.
   - **Desired:**
     - Change all review-mode action keys from `M-` (Alt) prefix to plain letter keys: `c`, `a`, `r`, `e`, `s`, `u`, `d`.  This is safe because review-mode uses a read-only buffer with hidden cursors, so plain letters will never interfere with text input.
     - Update the toolbar/status text (and all locales in `audit_mode.i18n.json`) to list the new plain-key shortcuts instead of the Alt combos.

3. **Discard confirmation**
   - **Current:** Pressing `d` (or `Alt+d`) immediately runs `rm` for untracked files or `git checkout --` for tracked files, with no confirmation.  This can cause permanent data loss by accident.
   - **Desired:**
     - Before executing the destructive operation, show a confirmation prompt via the existing prompt API (`editor.startPrompt` with a custom prompt type and suggestions).
     - For **untracked files:** prompt “Delete "<file>"? This cannot be undone.” with choices: `Delete file` (value “discard”) and `Cancel`.
     - For **tracked files:** prompt “Discard changes in "<file>"? This cannot be undone.” with choices: `Discard file` and `Cancel`.
     - Only perform the deletion/checkout if the user explicitly confirms by selecting the discard choice or typing “discard”.  On cancel, do nothing and optionally show a status message.
     - After a successful discard, refresh the magit data and set an appropriate status.

**Accepts**

- Hunk navigation shows several lines of context above the hunk header (not just the first line).
- Repeated `next` / `prev` does not get stuck on the same hunk.
- Plain-letter keybindings trigger the correct actions in review-mode.
- The discard action always asks for confirmation before removing data.
- The toolbar hint reflects the actual keybindings.
- Internationalization strings are updated to match.

Fix hunk nav centering/cursor, menu conflicts, discard confirmation


### Ticket: Ctrl+D with no selection should select the current word (VSCode-like behavior)

**Area:** Multi-cursor / Add Cursor at Next Match (Ctrl+D)
**Related:** #781

**Current behavior:**
Pressing Ctrl+D with no active selection shows "No selection to match" and does nothing.

**Desired behavior:**
When there is no selection, Ctrl+D should first select the whole word under or adjacent to the cursor. This word then becomes the match scope for subsequent Ctrl+D presses, which add cursors at the next occurrences of the same word.

**Exact semantics:**

- If the cursor is **inside** a word (at any position between the first and last character), select the entire word (anchor at word start, cursor at word end).
- If the cursor is at the **beginning** of a word, select that whole word.
- If the cursor is at the **end** of a word (i.e., on a non-word character immediately after a word), select the preceding word.
- If the cursor is on a **non-word character** (whitespace, punctuation) with no adjacent word on either side, do nothing and display an informational message such as `"No word at cursor position"`.
- **No additional cursor** is created on this first press. Only the primary cursor’s selection is expanded.
- After the word is selected, subsequent Ctrl+D presses follow the existing multi-cursor logic: they add a new cursor at the next occurrence of the selected text.

**Examples:**

| Cursor position (‸) | Text                  | Expected selection | New cursor count |
|---------------------|-----------------------|---------------------|------------------|
| Inside word         | `he‸llo`             | `hello`             | 1 (primary)      |
| Word start          | `‸hello`             | `hello`             | 1                |
| After word          | `hello‸ world`       | `hello`             | 1                |
| Isolated space      | `hello ‸ world`      | none → message      | 1                |

**Acceptance criteria:**

- The operation is self-contained: a single call to the existing multi-cursor routine should handle both the new word-selection path and the existing `Success`/`Failed` paths.
- The new behavior must not break any existing selection/multi-cursor workflows; pressing Ctrl+D again after the word is selected must continue to add cursors at subsequent matches as before.

fix: Ctrl+D selects entire word when no selection exists


```
Title: OSC 7 path parsing rejects remote-shell paths from a different OS

Fresh’s embedded terminal uses OSC 7 escape sequences to keep track of the
current working directory. The parser in `parse_osc7_path` currently uses
`Path::is_absolute()` for validation and applies a Windows-specific
leading-slash strip (for `file:///C:/dir`) only on Windows builds. Both of
these are host-OS-specific.

When Fresh runs on one OS but the terminal emulates a remote shell on
another (e.g. an SSH session from Windows to Linux, or from Linux to a
Windows machine) the OSC 7 payload’s path flavour follows the remote
shell’s OS, not the host OS. For example, a Linux shell reports
`/home/user/project`, while a Windows shell might report `C:\Users\me` or a
`file:///C:/Users/me` URI. Currently:

- On Windows, `Path::is_absolute()` rejects POSIX-style paths
  (`/home/user`) as “not absolute,” so they are silently discarded.
- On Linux/macOS, the leading-slash strip for `file:///C:/...` never runs,
  leaving `/C:/...` which is not a usable absolute path on any platform and
  is also discarded.

This breaks Ctrl+Click file resolution in the embedded terminal whenever
the remote directory is of the “other” OS.

Desired behaviour:

- Accept the path if it is absolute in *either* POSIX or Windows convention,
  regardless of the host OS. Recognise:
  - Leading `/` (POSIX absolute)
  - Leading `\\` (UNC)
  - A drive letter followed by `:` (e.g. `C:\`, `C:/`, or the `/C:` form
    that appears after percent-decoding a `file:///C:/dir` URI)
- Apply the `file:///C:/dir` → `C:/dir` leading-slash strip unconditionally,
  not only on Windows builds.
- Continue to reject relative paths (e.g. `projects/fresh`).
- Preserve percent-decoding and all existing edge-case handling.

The fix should be limited to `parse_osc7_path` (and possibly a small
internal helper) in `crates/fresh-editor/src/services/terminal/term.rs`.
No public API change is required; only the internal path-validation logic
needs to be corrected.

Acceptance criteria:
- `cwd()` is correctly updated for POSIX absolute paths (`/home/user/proj`)
  on a Windows build of Fresh.
- `cwd()` is correctly updated for Windows drive-absolute paths
  (`C:\Users\me` or `/C:/Users/me`) on a Unix build, as long as the
  payload contains a `file://` URI or a bare path.
- Relative paths are still ignored.
```

fix(terminal): make OSC 7 path parsing OS-independent (Windows CI)


Title: `.cargo/config.toml` and other user-editable files under `.cargo` are opened as read-only

Fresh incorrectly treats every file path that contains `/.cargo/` (or `\.cargo\` on Windows) as a library file, making it read-only. This prevents editing of legitimate user configuration files such as:

- `.cargo/config.toml` (the main Cargo configuration file)
- `.cargo/credentials.toml` (registry credentials)
- `.cargo/env` (environment variables)

### Steps to reproduce
1. Open a project (or home directory) that contains a `.cargo/` directory.
2. From the file explorer or command line, open `.cargo/config.toml`.
3. Attempt to type – the status bar shows “Editing disabled” and input is rejected.

### Expected behavior
- **Editable**: Files directly inside `.cargo/` (i.e., `.cargo/<file>`) that are not downloaded sources should be treated as normal, user-editable files.
- **Read-only**: Only files under **`.cargo/registry/`** and **`.cargo/git/`** should be considered library files, since these directories contain downloaded crate sources that should not be modified by hand.

### Impact
This makes routine Cargo configuration impossible inside Fresh. Every Rust developer who places a `.cargo/config.toml` in their project (or uses a global one) hits the problem. The overly broad path check also catches any `.cargo` subdirectory under a project, not just the user’s home directory.

### Desired fix
Narrow the library-path detection so that it only matches `.cargo/registry/` and `.cargo/git/` (and their Windows equivalents) rather than any path containing `/.cargo/`. This preserves the read-only protection for vendor/crate sources while allowing all other cargo configuration files to be edited normally.

See issue #1970.

fix(buffer): treat only .cargo/registry and .cargo/git as library paths


**Bug: Multi-cursor editing produces garbled output when cursors share identical lines**

### Summary
When multiple cursors are placed on lines with the same content (e.g., via Ctrl+D to add cursors at matching occurrences), inserting or deleting characters corrupts the buffer. Instead of each cursor acting independently, some inserts appear in the wrong positions, leading to misaligned or missing characters.

### Steps to reproduce
1. Open a file containing identical lines, e.g.:
   ```
   abc
   abc
   abc
   abc
   ```
2. Place a cursor on the first line, then press Ctrl+D three times to add cursors on each subsequent occurrence of `abc`. The four cursors should now be on four separate lines, all at column 0.
3. Type `X` (or any character).

### Observed behavior
The buffer becomes garbled. For the four-line example above, the output might look like:
```
Xabc
XabcX
abXc
abc
```
Only three `X` appear, and the last line remains unchanged. Deletions exhibit similar misplacement.

### Expected behavior
Each cursor should insert or delete at its own position as if the operations were applied to the original buffer simultaneously. The correct result for the input above should be:
```
Xabc
Xabc
Xabc
Xabc
```
Four `X` characters, one per line, at the original cursor positions.

### Affected operations
All multi-cursor editing commands are affected:
- `InsertChar`
- `InsertNewline`
- `InsertTab`
- `DeleteBackward`
- `DeleteForward`

### Root cause (suspected)
When generating events for multiple cursors, the iteration order over the cursor collection is not guaranteed to be sorted by position. If an earlier (in the buffer) cursor inserts a character first, it shifts the positions of all later cursors, causing subsequent insertions to target incorrect offsets. The same issue occurs for deletions.

### Desired fix
Ensure that events for multi-cursor operations are emitted in **reverse position order** (rightmost cursor first). This way, each insertion or deletion does not affect the positions of cursors earlier in the buffer. The behavior should preserve the invariant that after the operation, the number of characters inserted/deleted equals the number of cursors times the operation length, and each cursor’s original offset appears to have been modified in isolation.

### Impact
- Prevents practical use of multi-cursor editing on any file where repeated patterns exist (very common in source code).
- Makes multi-cursor edits unpredictable and requires manual workarounds.

Fix multi-cursor insertion/deletion position bug


Title: Intermittent data loss in remote file streaming reads (#1059)

**Summary**
When loading large files through the remote‑editing channel (`AgentChannel`), the resulting buffer content is sometimes truncated or missing chunks. The problem is non‑deterministic and appears most often on macOS, but can affect any platform under the right scheduling conditions.

**Root Cause**
`AgentChannel::handle_response` uses a bounded `mpsc` channel (capacity 64) with `try_send` to deliver streaming data chunks to the caller. If the consumer is slower than the producer, the channel fills up and `try_send` silently drops the chunk, causing permanent data loss. The bursty arrival of many chunks (e.g., multi‑megabyte reads) easily exceeds the 64‑slot buffer when OS thread scheduling delays the consumer loop even briefly.

**Expected behavior**
Streaming reads must be lossless under all scheduling timings. A fully‑connected channel should apply backpressure rather than discard data.

**Proposed fix**
Replace the `try_send` in `handle_response` with an `mpsc::send().await` so the producer yields until the consumer drains a slot. This guarantees no data is lost.

**Constraints / edge cases**

- `send().await` may yield, so the `data_tx` sender must be extracted from the pending‑request map *without* holding the mutex across the await point; otherwise we risk deadlocks.
- If the receiver is dropped mid‑stream, `send().await` will return an error. The pending entry must be cleaned up to prevent leaks, and a warning should be logged (this is an unexpected condition since callers are expected to hold `data_rx` until the stream ends).
- The `result_tx` path may still complete even if the data channel receiver is gone; handle that gracefully.
- Non‑streaming request flows (single‑shot `oneshot` only) must remain unaffected.

**Nice to have (for verification)**
Make the data‑channel capacity configurable (or at least overridable via a constant) so that integration tests can deliberately trigger backpressure with a tiny buffer and confirm correctness.

**Affected area**
`crates/fresh-editor/src/services/remote/channel.rs` – `AgentChannel::handle_response` and surrounding initialization.

fix: use backpressure instead of try_send for streaming data (#1059)


Fix auto-dedent pattern fallback to skip already-closed inner blocks

When typing a closing delimiter (}, ], )) and tree-sitter parsing is unavailable, the pattern-based auto-dedent scans backwards for an opening delimiter. Currently it stops at the first opening delimiter found, ignoring any inner blocks that have already been closed. This causes the closing delimiter to be indented to the level of an inner block rather than the matching outer block.

Expected behavior: the fallback should track nesting depth while scanning backwards. For each closing delimiter encountered, increment a depth counter. For each opening delimiter, if depth > 0, decrement the counter and continue (the opening is already matched by a previous closing delimiter). If depth == 0, that's the matching opening delimiter—dedent to its line’s indent. This ensures that typing a closing delimiter for an outer block after inner blocks have been closed correctly dedents to the outer block’s column, not the inner block’s column. The behavior must handle mixed bracket types independently (parentheses, braces, brackets are distinct).

Fix auto-dedent pattern fallback to track nesting depth


## Sessions: warm-swap utility-dock panel occupancy

The utility dock (search/replace, references, diagnostics, etc.) is currently global across sessions. When the user switches the active session, the dock remains unchanged — the incoming session sees whatever panels the outgoing session had open. This is confusing and can lead to accidental loss of state:

- Alpha has the Search panel docked.
- User dives to Beta, but Beta still shows Search (which may be meaningless or misleading).
- User closes the panel in Beta; diving back to Alpha loses Alpha’s search state.

We want per-session dock occupancy, following the same pattern already established for the file explorer (the `file_explorer_stash` warm-swap in `Session`).

### Desired behavior

1. Each session independently remembers the set of utility panels it had open (the `panel_ids` map: panel key → buffer id).
2. When the user switches away from a session, its entire current `panel_ids` map is stashed into the session.
3. The incoming session starts with either its own previously-stashed dock occupancy, or with an empty dock if it has never opened any panel.
4. The swap must be atomic — there should be no intermediate tick where a panel is detached from any session’s map.
5. Closing/opening panels while in a session should only affect that session’s live occupancy; the stashed maps of other sessions must not be mutated.

### Acceptance criteria

- Given base session A with Search panel open in the dock, and a second session B with no panels:
  - Switch from A → B → dock is empty.
  - Switch back to A → dock shows the same Search panel with the same buffer.
- If B opens References and then the user switches back to A, A’s Search is restored; switching again to B restores References.
- The behavior should be consistent with the existing file-explorer warm-swap: both stashes are handled together in `setActiveSession`.

### Non-goals

- This ticket does not change how panels themselves are created or destroyed, nor does it introduce any new lifecycle for panel buffers. Only the ownership of the dock occupancy during session switches is affected.

### Implementation notes

The `Session` struct already carries [`file_explorer_stash`](https://github.com/sinelaw/fresh/blob/master/crates/fresh-editor/src/app/session.rs) for the file explorer. Add a symmetric `panel_ids_stash: HashMap<String, BufferId>` to `Session`. In `setActiveSession`, before updating `active_session`, move `self.panel_ids` into the outgoing session’s stash; after the switch, replace `self.panel_ids` with the incoming session’s stash (or an empty map if none). Keep the existing file-explorer swap lines right next to this new swap so they stay in sync.

The test-only accessors for injecting and inspecting `panel_ids` should be added in the `editor_accessors` module, following the pattern used by other test-only helpers (e.g., `buffer_count_for_tests`). These are only used for integration tests that assert the dock-swap invariants without going through the async plugin command path.

feat(core): warm-swap utility-dock panel_ids across sessions


**Title:** Config editor plugin crashes due to `pathJoin` misuse, leaks its commands globally, and may produce unloadable config files

**Describe the bug**
1. The config editor plugin immediately fails at startup because all calls to `editor.pathJoin` pass a single array argument (e.g. `editor.pathJoin(["plugins", "config-schema.json"])`) instead of multiple string arguments. The runtime error prevents the plugin from locating the schema or the config file, so the editor never opens.
2. Even after working around the above, commands specific to the config editor (save, toggle, navigate, etc.) are always globally registered. They appear in the command palette and can be invoked even when the config editor buffer is closed, causing spurious errors or unwanted side effects.
3. There is no guarantee that config files written by the plugin will be accepted by Fresh on the next launch. If the editor serialises values in a way that deviates from the `Config` struct’s `Deserialize` implementation, the application will fail to load the file.

**To Reproduce**
1. Place a `config.json` and a `config-schema.json` in the project root, and load Fresh.
2. Open the command palette and run “Edit Configuration”.
3. Observe the plugin fails silently or the console shows an error about `editor.pathJoin` expecting string arguments.
4. If you patch the `pathJoin` calls, open and close the config editor. Then open the command palette – commands like “Save Configuration”, “Toggle Or Edit”, “Expand Section” are still listed and can be executed.
5. After saving a config, quit and restart Fresh – depending on the schema and serialisation, the config file may fail to deserialise, crashing or falling back to defaults.

**Expected behavior**
- The config editor plugin must locate its schema and config files correctly and start without errors.
- Commands that only make sense within the config editor (save, toggle, edit field, navigation, etc.) must be **available only when a config editor buffer is focused**, and **removed from the command palette and keyboard handling when the user switches away or closes the config editor**. The “Edit Configuration” command that opens the editor should remain globally available.
- Saving through the config editor must always produce a `config.json` that can be deserialised into a valid `Config` by the main application. Any deviation from the expected structure (e.g. missing required fields, wrong types) should be prevented by the plugin itself.
- The command lifecycle must handle edge cases: closing the config editor via its own command, closing the buffer directly, switching to another buffer and back, and re-opening the editor after it has been closed. In every case the set of registered commands must exactly match the presence of an active config editor buffer.

**Impact**
- Without the fix the config editor plugin is entirely unusable, removing a key productivity feature.
- If commands leak, the global command palette becomes cluttered and keybindings can clash with other modes.
- If invalid config is saved, users may find Fresh has silently reverted to defaults, or fails to start, leading to a broken configuration experience.

Improve config editor commands and add e2e tests


**Title:** Devcontainer: attach popup re-displays after editor restart post-attach

**Severity:** Medium (user-facing annoyance, breaks expected flow)

**Context**
After a user clicks “Attach” in the devcontainer popup, the plugin calls `setAuthority`, which triggers an editor restart. Main constructs a fresh `Editor` (with `Authority::local()` and an empty `display_label`), then calls `set_boot_authority(real_container_authority)`, and finally fires the `plugins_loaded` hook. The devcontainer plugin decides whether to show the attach prompt by checking `editor.getAuthorityLabel()`.

**Observed behavior**
On the restart immediately following a successful attach, the attach popup appears again – even though the status bar correctly reflects the container connection.

**Expected behavior**
Once the user has attached, the popup must not re-appear on subsequent restarts until the authority is explicitly cleared (detach). The prompt should only require a fresh “Attach” or “Not now” decision when there is no active container authority, i.e. when `getAuthorityLabel()` returns an empty string.

**Root cause**
Two problems conspire:
1. `set_boot_authority` updates `self.authority` but does **not** refresh the plugin state snapshot that backs `editor.getAuthorityLabel()`. Because the snapshot was seeded with the construction-time `Authority::local()` (empty label), the stale empty string is returned even after the real authority is installed.
2. The devcontainer plugin’s attach-prompt check runs at plugin **top-level** – that is, during `Editor` construction, **before** main calls `set_boot_authority`. So even if the snapshot were refreshed inside `set_boot_authority`, the check would have already read the empty label and concluded “not attached”.

**Proposed fix**
- In `set_boot_authority` (crates/fresh-editor/src/app/editor_accessors.rs), ensure the plugin state snapshot is refreshed after setting `self.authority`, so that any subsequent reads reflect the installed authority label.
- In `devcontainer.ts`, defer the “should I show the attach prompt?” decision from the top-level body to a `plugins_loaded` handler. Main fires that hook *after* `set_boot_authority`, guaranteeing the snapshot already carries the correct label. Keep per-session “Not now” dismissal working as before.

**Edge cases / invariants**
- When authority is cleared (`set_boot_authority(Authority::local())`), the snapshot must return to an empty label so that a detached state triggers the prompt again on the next cold start.
- The prompt must still appear normally on first launch when no devcontainer is attached.
- The plugin’s one-shot “Not now” dismissal per editor process must be preserved: if the user dismissed earlier in the same session, do not re-prompt even if the authority is empty.

fix(devcontainer): don't re-prompt after post-attach restart


Fix dropdown menu border inconsistencies caused by active tab modifier bleed.

When a dropdown menu overlaps the tab bar, the border cells inherit the active tab's BOLD modifier because the dropdown's ratatui Block only sets background color without clearing prior cell attributes. On terminals that map BOLD to bright color variants, this makes the border appear in a different color where it overlaps the active tab.

Expected behavior: The dropdown border should render uniformly, fully independent of the visual state of the tab bar underneath.

To achieve this, the dropdown rendering must clear all cell modifiers (bold, reversed, etc.) before applying the menu background. Replace `Style::default()` with `Style::reset()` for the dropdown Block's style in `render_dropdown_level`, ensuring every cell in the dropdown area starts from a clean state.

Fix active tab styling bleeding through dropdown menu border


Ticket: **Line wrapping test hardening and temporary style simplification**

### Problem
- The main cursor-positioning test (`test_wrapped_line_cursor_positioning`) hardcodes wrapping parameters derived from terminal width, gutter, and scrollbar widths. These calculations are fragile and break when layout constants change. The test should instead verify the required observable behavior independent of exact pixel counts.
- Wrapped segment rendering currently preserves syntax-highlighting styles using `apply_styles_to_segment`. This extra complexity is causing instability and visual glitches. The styling can be simplified to plain text for now, with a TODO to reintroduce proper style handling later—prompt highlighting on wrapped lines must not block core wrapping correctness.
- A known bug exists where the cursor can move into empty space past the visible end of a wrapped line. The test for this (`test_wrapped_line_cursor_no_empty_space`) fails reliably and should be ignored until the root cause is investigated.

### Desired behavior / Acceptance criteria
1. **Dynamic wrapping detection in tests**
   - The cursor positioning test must stop predicting exact wrap positions.
   - Instead, it should move the cursor character-by-character and **detect wrap points by observing screen y‑coordinate changes**.
   - On each wrap event, verify that:
     - The cursor x‑position is exactly `GUTTER_WIDTH` (start of continuation line).
     - The y‑coordinate increases by 1 per wrap.
   - After navigation, confirm that no horizontal scroll occurred (start of text remains visible).
   - Verify that moving left across a wrap boundary causes the cursor to jump back up one visual line.
   - Home/End keys must still place the cursor at the logical start/end of the line correctly.

2. **Temporary style removal for wrapped lines**
   - In `split_rendering.rs`, replace the call to `apply_styles_to_segment` with a plain `Span::raw(segment_text)`.
   - Add a `TODO` comment explaining that syntax highlighting on wrapped lines is temporarily disabled and will be restored in a follow-up.
   - Basic line wrapping (text content, visual breaks, gutter) must continue to work as before.

3. **Documentation fix**
   - Update the docstring for `WrapConfig::new` to clarify that the first parameter is `content_area_width` (not total terminal width).

4. **Disable failing empty-space test**
   - Mark `test_wrapped_line_cursor_no_empty_space` with `#[ignore]` and a short note that it’s waiting on a rendering investigation.

### Scope / Constraints
- All other wrapping tests must remain passing.
- No changes to the underlying line-wrapping algorithm (`wrap_line`, `char_position_to_segment`, `WrapConfig` widths) other than the documentation fix.
- The styling simplification is a **temporary** workaround; the final restoration of syntax highlighting on wrapped lines will be tracked separately.

Fix line wrapping tests and improve test expectations


Bug: after a rename or file creation inside a directory that is already expanded in the file tree, the new entry does **not** appear in the explorer. The old name may remain visible, arrow-key navigation cannot reach the new entry, and the explorer is out of sync with the filesystem until the directory is collapsed and re-expanded.

This occurs because `FileTree::reload_expanded_node` correctly inserts new entries into `self.nodes` and `self.path_to_node` via `add_node`, but never appends their ids to the parent node’s `children` list. As a result, `get_visible_nodes` (and therefore the explorer UI) omits them.

**Required fix:**
In `reload_expanded_node`, after detecting which on-disk paths are new, collect the `NodeId`s returned by `add_node`. Rebuild the parent’s `children` list from the surviving old children **plus** the newly added ids, sorted exactly as `expand_node` sorts them: directories first, then case-insensitive by name. This ensures all three data structures (`self.nodes`, `self.path_to_node`, and parent’s `children`) stay consistent, and the visible tree immediately reflects the current on-disk state after a reload.

**Additional note:**
The Explorer context menu has gained Cut/Copy items (inserted between Delete and Refresh), which shifts the position of “Show Hidden Files” downward in the menu. Any hard-coded indexes for menu navigation (e.g., pressing Down N times to reach that item) must be bumped accordingly so that tests and interactions land on the correct item.

fix(explorer): link new children in reload_expanded_node; sync menu tests


Fix cursor overlapping the vertical scrollbar when line wrapping is enabled.

- The vertical scrollbar occupies the column immediately to the right of the content area.
- The end-of-line cursor sits one column past the last rendered character.
- When line wrapping is on and a wrapped visual row fills exactly `content_width` columns, the EOL cursor lands on the scrollbar track, obscuring it.
- Reserve one column from the effective wrap width whenever line wrapping is enabled, so no visual row can reach the full content width. The EOL cursor must stay on the last content column at most, one column short of the scrollbar.
- This applies both to automatic wrapping at `content_width` and to user-configured `viewport.wrap_column`.
- At very small widths (content_width ≤ 2) the reserved width must not drop below 1; existing short-circuit logic for widths < 2 in the wrapping transform already handles those cases safely.

fix(view/wrap): reserve one column so EOL cursor doesn't overlap scrollbar


Fix the command palette so that pressing **Enter** executes the currently highlighted suggestion instead of the raw typed text. Right now, typing a partial command like `new` and pressing Enter tries to run `"new"` and shows `"Unknown command: new"`, even when `"New File"` is visibly selected.

**Acceptance criteria:**
- When the command palette is open and exactly one suggestion is highlighted, Enter must run that suggestion’s command, not the literal characters typed so far.
- If no suggestion is highlighted (selection index is empty), fall back to the raw input as before.
- If the highlighted suggestion is disabled (greyed out), show a status message like “Command '<name>' is not available in current context” and do **not** execute any command.
- Arrow-key navigation works as usual: moving the highlight with Up/Down and then pressing Enter executes the newly highlighted suggestion, even when the typed text is a partial match (e.g., type `sav`, arrow to `Save File As`, Enter → `Save File As` is executed, not `"sav"`).
- This new selection-aware behaviour must apply **only** to the command palette prompt; other prompts (e.g., file open/save) should keep their existing raw-input semantics.
- The fix must not break existing command palette functionality like filtering, cancellation, or Tab completion.

Fix Enter key in command palette to execute selected suggestion


Add a visible "☐ Hidden" / "☑ Hidden" checkbox to the right side of the navigation bar in the Open File dialog, and remove the Ctrl+H keybinding.

**Problem**
Ctrl+H is intercepted by terminals as Backspace in raw mode, making the hidden-files toggle unusable for many users.

**Expected behavior**
- The file browser navigation bar (the top row showing parent/root/home shortcuts) gains a checkbox on its right side.
- The checkbox shows "☐ Hidden" when hidden files are not shown, "☑ Hidden" when they are shown.
- Left-clicking the checkbox toggles hidden files (same effect as the old Ctrl+H action).
- Hovering the checkbox highlights it using the standard hover styling.
- The checkbox does not interfere with the existing navigation shortcuts; it sits in the remaining space on the right.
- The Ctrl+H keybinding is removed entirely; pressing Ctrl+H no longer toggles hidden files.

**Constraints / edge cases**
- The checkbox must be discoverable and visually distinct from the navigation shortcuts.
- The toggle should work reliably when the file list is populated, both when hidden files are currently visible and when they are not.

Replace Ctrl+H with clickable checkbox for hidden files toggle


The settings UI renders `theme` as a free-text field instead of a dropdown. The config schema defines `theme` as `"type": "string"` with a comment listing valid values (`dark`, `light`, `high-contrast`, `nostalgia`). Because no `enum` constraint is present in the schema, the settings parser does not recognize it as an enum type, so no dropdown is shown. The user can type any arbitrary string, and there is no affordance for discovering valid themes.

Make two changes so the settings UI surfaces themes as a selectable dropdown:

1. **Schema definitions**: Move the theme options into a proper `$defs` entry with a JSON Schema `enum` so the settings parser can detect it. Reference it from the `theme` property via `$ref`. The display name shown in the dropdown should be human-readable (e.g., "High Contrast" for value `high-contrast`); provide a way to associate display names with enum values cleanly.

2. **Schema parsing**: When resolving `$ref` for a property, check the resolved definition for `enum` values and expose them as a dropdown. Currently the parser only checks inline `enum` on the property itself, not on the referenced definition.

3. **Dropdown rendering**: The dropdown state currently treats each option string as both the displayed label and the stored value. This is insufficient when display names differ from values. Extend the dropdown to support separate label/value tuples so the UI can show "High Contrast" while the config stores `high-contrast`. Ensure `selected_option()` still returns the display name for rendering, and add a way to retrieve the current *value* for committing changes back to config. The dropdown cycling and selection-by-value (used when settings are loaded from an existing config file) must resolve against the value set, not the display names.

Generalize the enum-from-`$ref` resolution so any property that references a definition containing an `enum` is automatically treated as a dropdown, without requiring changes across multiple files for each new setting.

Fix theme dropdown with enum support from $ref definitions


Entry dialog keyboard navigation must descend into composite controls (`Map`, `ObjectArray`, `TextList`) rather than skipping them. When focus reaches a composite control, **Down** should first move through the control’s internal entries and then reach its `[+] Add new` row before advancing to the next dialog item. **Up** should traverse backwards through internal entries and then exit the control when at its first entry. This replaces the old behaviour where pressing Down on a composite control immediately jumped to the next top-level item or to the button row, making it impossible to add rows (e.g., a second LSP server for a language) without the mouse.

**Boundary rules:**
- **Down at `[+] Add new`**: exit the composite control and advance to the next dialog item.
- **Up at the first internal entry** (or on an empty composite’s `[+]`): exit the composite and move to the previous dialog item.
- **When entering a composite from above** (i.e., Down from the previous item), focus the first internal entry if any, otherwise the `[+]`.
- **When entering a composite from below** (i.e., Up from the next item), focus the `[+]` (or last entry for consistency).
- The navigation should be uniform across `Map`, `ObjectArray`, and `TextList`; the existing internal `focus_next`/`focus_prev` methods on those types already return a boundary flag, so the entry dialog can delegate and react accordingly.

This is a correctness fix: keyboard-only workflows must be able to reach and activate every actionable UI element, including add-new rows inside arrays and maps. No existing keyboard shortcuts or tab behaviour should change; only the per-item Up/Down sequence within the entries region is affected.

Fix entry dialog keyboard navigation for composite controls


Fix CSI u and xterm modifyOtherKeys sequences in session mode

In session attach mode, terminal input is forwarded to the server-side `InputParser`. Two families of escape sequences are currently unhandled and appear as literal bytes inserted into the buffer:

- CSI u sequences (kitty/fixterms protocol): `CSI codepoint ; modifiers u`
- xterm modifyOtherKeys mode 2 sequences: `CSI 27 ; modifier ; codepoint ~`

Both must be parsed into the corresponding `KeyEvent` with the correct `KeyCode` and `KeyModifiers`.

Requirements:

- Add handling for the `u` final byte in `parse_csi_final`. Extract the Unicode codepoint (first parameter) and modifier parameter (second, default 1). Map codepoints: 9→Tab, 13→Enter, 27→Esc, 127→Backspace, otherwise convert to `KeyCode::Char` via `char::from_u32`.
- Extend `parse_tilde_sequence` to detect the `27` prefix: when parameters contain exactly three parts and the first is `"27"`, treat the second part as the modifier parameter and the third as the codepoint, with the same special-key mapping as above.
- Use the existing `modifiers_from_param` to decode the modifier parameter in both cases.
- Unrecognised codepoints that cannot be converted to a `char` (i.e., `char::from_u32` returns `None`) must be treated as invalid input and not emitted as text.

The fix must not affect non‑session (local) parsing paths.

Fix #1113: parse CSI u and xterm modifyOtherKeys sequences in session mode


In `nvme_pcie_admin_qpair_destroy()`, all outstanding admin trackers (not only Asynchronous Event Requests) must be aborted to invoke their completion callbacks. Currently only AERs are aborted via `nvme_pcie_admin_qpair_abort_aers()`, leaving other admin commands (e.g., Get Log Page) without a completion, which leaks memory. Call `nvme_pcie_qpair_abort_trackers()` with the qpair’s `abort_dnr` after aborting AERs, so every remaining tracker is completed with an abort status.

nvme/pcie: abort outstanding admin trackers on qpair destroy