**Title:** http3 client caches dial errors forever, preventing retries after transient failures

**Background**
The HTTP/3 `RoundTripper` maintains a per-host cache of active QUIC connections. Once a connection to a host is established (or its handshake is in progress), that cache entry is reused for all subsequent requests to the same host.

**Problem**
When the initial dial to a host fails (e.g., a transient network issue, a TLS handshake failure), the error is stored inside the cached client. All future requests to that host receive the same cached error immediately, without attempting a new dial. This effectively “poisons” the host entry permanently, even long after the original failure reason may have healed.

**Expected behavior**
A dial error should not permanently blacklist the host. After a dial failure, the `RoundTripper` should discard the stale cache entry so that the next request to the same host triggers a fresh dial attempt. The second dial may of course fail again, but it must be attempted, not short-circuited.

**Scope / constraints**
- The fix should ensure that subsequent requests to a host that previously experienced a dial error can succeed if the new dial succeeds.
- The cache eviction should happen regardless of whether the error is observed during the initial dial or after waiting for an in-progress handshake to complete.
- Existing semantics for connection reuse after a successful dial must remain unchanged.
- The change should maintain thread-safety (the `RoundTripper`'s client map is accessed concurrently).

http3: allow re-dialing of connection after a dial error (#4573)


When constructing a coalesced QUIC datagram (multiple encryption-level packets inside a single UDP payload), any ACK frames that end up being large can push the total size beyond the path MTU if they are placed in the second or later sub‑packet. This leads to dropped datagrams and poor performance.

The desired behavior: within a coalesced packet, ACK frames should be included only in the first sub‑packet, never in subsequent sub‑packets created for the same datagram. This applies to both Handshake and 1‑RTT encryption levels when they appear as later parts of a coalesced datagram.

Additionally, the existing TODO about not sending ACK frames in 0‑RTT packets should be addressed: 0‑RTT packets should not carry ACK frames at all.

A correct implementation must:
- still allow ACK frames in the initial (first) sub‑packet of a coalesced datagram for both Handshake and 1‑RTT levels;
- avoid placing ACK frames in the second (or later) sub‑packet of a coalesced datagram, even if ACK‑eliciting data was received before that sub‑packet is assembled;
- ensure that non‑coalesced single‑packet sends are unaffected and continue to include ACKs normally;
- not introduce any new data‑dependent allocations or significant performance regressions for the common fast‑path cases.

don't pack ACK frames in the second part of a coalesced packet


We need to support URL schemes other than `https` for HTTP/3 requests, specifically `masque://…` for MASQUE proxying. Currently, `http3.RoundTripper` unconditionally returns `unsupported protocol scheme` when the request’s URL scheme is not `https`.

Add a mechanism on `RoundTripOpt` (the options struct for `RoundTripOpt`) to skip the scheme check, so that callers can opt-in to issuing requests with a non-`https` scheme. The default behavior must remain unchanged: without this option, any non-`https` scheme must still produce an error and close the request body as it does today. When the new option is active, the scheme check is bypassed; other validation (method, body, etc.) and connection logic should proceed as if the scheme were valid.

Acceptance:
- `RoundTripper.RoundTrip` continues to reject non-`https` schemes.
- `RoundTripper.RoundTripOpt` accepts a new boolean option on the existing `RoundTripOpt` struct that, when true, allows schemes like `masque://example.org`.
- The fix must not break any existing constraints around connection caching or the `OnlyCachedConn` logic.

add a http3.RoundTripOpt to skip the request scheme check


We need a new `quic.Config` option that lets the application dynamically control connection-level receive window increases. Currently the connection flow control window is automatically increased by the system according to received data and limits, with no way for the application to veto an increase (for example to bound memory usage). We want an optional callback `AllowConnectionWindowIncrease` that is invoked each time the connection flow controller attempts to enlarge the window. The callback receives the `Session` and the increase delta, and must return a `bool`. If the callback returns `false`, the window must not be increased. If the callback is `nil`, the behavior must be equivalent to always returning `true` (no change from today). This new field must be properly handled when cloning a `Config` and when populating defaults in `populateConfig` (i.e. the user-provided value must be preserved through server and client config pathways).

add a AllowConnectionWindowIncrease config option


Below is an implementation request that describes the problem and acceptance criteria without revealing the patch internals.

---

**Summary**: The receive stream notifies its completion callback too early when a RESET_STREAM arrives or when the application cancels reading. Completion should be deferred until the application has actually consumed the terminal error, and the final byte offset is known.

**Background & observable issues**:

- When a remote `RESET_STREAM` frame is received, the corresponding `ReceiveStream` immediately considers itself completed and fires `onStreamCompleted`, even if the application hasn't read the reset error yet.
- Similarly, if the application calls `CancelRead`, the stream may be treated as completed before the user's next `Read` call returns the cancellation error.
- If a remote reset arrives _after_ `CancelRead`, the code may send a `STOP_SENDING` frame and may overwrite the error that the reader will see. The user should see the locally initiated cancellation error (`Remote: false`), and no `STOP_SENDING` should be sent in that case.
- The final byte offset (received via `FIN` or `RESET_STREAM.FinalSize`) must be known before flow‑control resources can be released; completing the stream earlier may violate flow‑control invariants.

**Desired behavior**:

1. **Deferred completion**:
   The receive stream must fire `onStreamCompleted` (via the `streamSender`) only after **both** of the following hold:
   - The application has actually read the terminal error (`io.EOF` for a clean FIN, or the `*StreamError` for a cancellation / remote reset).
   - The final byte offset is known (i.e., a `FIN` bit or a `RESET_STREAM.FinalSize` has been received). If the final offset is not yet known, completion is not allowed even if the stream is locally cancelled – it must wait until the final offset arrives.

2. **Error precedence and no duplicate `STOP_SENDING`**:
   - When the application calls `CancelRead` **after** a remote `RESET_STREAM` has already been received, no `STOP_SENDING` frame shall be queued, and the error returned to the reader must be the remote reset error (`Remote: true`).
   - When `CancelRead` is called **first**, and a remote `RESET_STREAM` arrives later, the reader must see the local cancellation error (`Remote: false`). The arrival of the reset must not change the stored error, and no additional `STOP_SENDING` shall be sent.
   - Duplicate `RESET_STREAM` frames must not trigger multiple completion calls.

3. **Shutdown path**:
   If the stream is closed for shutdown (`closeForShutdownErr` is set), this takes precedence and the stream can be marked completed immediately (existing behavior should be preserved).

**Implementation constraints**:
- All changes must be made inside the `receiveStream` type (defined in `receive_stream.go`).
- The `Read` method is the natural point at which the terminal error becomes observable; completion should be evaluated after that read returns.
- Flow‑control bookkeeping (`flowController.AddBytesRead`, `flowController.Abandon`) and sender callback (`sender.onStreamCompleted`) must only be called when the stream is truly completed according to the rules above.

**Acceptance criteria** (externally observable):
- A `Read` call that returns `io.EOF` or a `*StreamError` also marks the stream as completed; subsequent operations (like `CancelRead`) are no-ops and do not trigger extra `STOP_SENDING` frames or completion callbacks.
- Reading after a remote reset or local cancellation yields exactly one error, with the correct `Remote` flag.
- The flow‑control final offset is always known at the moment `onStreamCompleted` is called; no completion happens before that offset is registered.

delay completion of the receive stream until the reset error was read (#4460)


```
Transport.Close panics when the Transport hasn't been initialized

If you construct a Transport directly (with a user-supplied PacketConn and createdConn = true) and call Close without ever calling Listen/Dial, Close panics with a nil pointer dereference. The same happens for any Transport that hasn't been fully set up.

We need Close (and the helper close) to be safe to call on a Transport that hasn't gone through init. Specifically:

- When createdConn is true we still own the connection and must close it, but the internal raw conn (t.conn) isn't set until init runs. Close should use the public Conn (net.PacketConn) field instead.
- Several other internal fields (handlerMap, listening channel, and, in the !createdConn case, t.conn itself) may be nil. Guard them before using, so that a Transport that was created and immediately closed doesn't panic.
- The overall contract: Close must be callable at any point after a Transport struct exists, and it must clean up any resources the Transport actually acquired (or was given) without assuming init has completed.

Acceptance criteria:
- No panics when closing a freshly allocated Transport{Conn: pc, createdConn: true}.
- If createdConn is true, the underlying Conn is closed.
- If createdConn is false and no init happened, close is a no-op (other than setting the close error).
```

check for uninitialized fields when closing the Transport (#3908)


**Scoring**
- supervision_value: 3 — This is a solid ordinary bug fix: the agent must investigate the HTTP/0.9 server and interop launcher, compare the setup with the HTTP/3 path, identify that the ALPN protocol is missing and that the `http.Server` embedding is unnecessary, then make clean localised edits across a few files to pass configuration correctly and simplify the struct. It teaches a realistic mini‑process (symptom → inspect server/launcher → infer contract → align with sibling code) without being trivial.
- difficulty: 3 — The fix spans a few files and requires connecting multiple facts: the TLS config’s `NextProtos` field, the `http09.Server`’s unnecessary embedding, and how `runHTTP09Server` / `runHTTP3Server` get their TLS config. There are some plausible wrong local fixes, but the necessary changes are still bounded and inferable from the repo context.

**Implementation Request**
```
Title: HTTP/0.9 interop server setup broken: missing ALPN negotiation and awkward Server construction

When running the interop server with the “versionnegotiation” or “transfer” testcase (i.e., the HTTP/0.9 path),
the server fails to accept QUIC connections because the TLS configuration does not include the required ALPN
protocol token “hq-interop”. Additionally, the `http09.Server` type forces callers to embed a `*http.Server`
and checks for its presence in `ServeListener`, even though the HTTP/0.9 implementation never uses it.

Steps to reproduce (against current main):
1. Build and run the interop server container with testcase set to any HTTP/0.9 testcase.
2. Observe that the QUIC listener starts, but clients cannot complete a handshake because the server does not
   advertise “hq-interop” in the ALPN extension.
3. Try to create an `http09.Server` without embedding an `http.Server` – it is currently required even
   though the server code only needs its own response handling (and embedding it forces unnecessary
   construction).

Expected behavior:
- The interop server should correctly configure `tls.Config.NextProtos` to include `http09.NextProto`
  (“hq-interop”) when the testcase uses HTTP/0.9, and should use `http3.NextProtoH3` (“h3”) for the HTTP/3
  testcase.
- The `http09.Server` struct should be self-contained: no forced embedding of `http.Server`, and
  `ServeListener` should not require the caller to supply a dummy `http.Server`.
- The interop server’s `runHTTP09Server` function should receive and use the TLS config so the QUIC listener
  is created with proper ALPN settings.

This affects all HTTP/0.9 interop testcases (e.g., versionnegotiation, handshake, transfer, etc.) and makes
the interop testing impossible for those scenarios.
```

interop: fix server setup for the HTTP/0.9 server (#4658)


```
Title: Flow controller AddBytesRead double-locks the shared mutex, causing deadlock

In the flowcontrol package, the `baseFlowController` struct contains a `sync.Mutex` that is documented as being used by both the stream and connection flow controllers. When a stream or connection receives data, `AddBytesRead` is called to update the read accounting and potentially queue a window update.

Currently the call path is:
1. `baseFlowController.AddBytesRead` acquires `c.mutex`, updates counters, releases it.
2. The caller (`connectionFlowController.AddBytesRead` or `streamFlowController.AddBytesRead`) then calls its own `maybeQueueWindowUpdate` method, which acquires the **same** `c.mutex` again.

Because `sync.Mutex` is not reentrant, this leads to a deadlock whenever a window update becomes necessary. The same goroutine tries to lock the mutex it already holds, and the program hangs.

Expected behavior: `AddBytesRead` should perform all work under a single lock-acquisition, evaluating any window-update conditions inside that critical section, and only enqueue the update after releasing the lock.

This will require:
- The base helper that updates `bytesRead` and auto-tuning state **must not** acquire the mutex itself; callers should be responsible for holding the lock.
- Both `connectionFlowController.AddBytesRead` and `streamFlowController.AddBytesRead` should acquire the mutex once, call the helper, evaluate window-update eligibility (including the `receivedFinalOffset` gate for streams), release the mutex, and then queue the update if needed.
- No deadlock should occur even when a window update is triggered.
```

avoid duplicate mutex locking when reading data


```
Title: qlog event export must output events ordered by timestamp

Currently, when serializing qlog events to JSON (via `events.MarshalJSONArray`), the events are written in whatever order they were recorded. However, due to concurrency — for example, a packet’s receive timestamp is set on a different goroutine from the one that records an event — the internal insertion order can end up slightly out of chronological order. This results in exported JSON where event timestamps are not monotonic, making log analysis harder.

Acceptance criteria:
- All exported qlog event sequences must be sorted by their timestamp in ascending order.
- Sorting must reflect the timestamp that actually appears in the serialized output. Do **not** rely on `time.Before()`; it uses monotonic time which may not correspond to the exported wall-clock time. Instead, compare the values that `time.UnixNano()` returns for each event’s `Time` field.
- Because events are typically only slightly out of order, the implementation should prefer a stable sort to minimize wasted work on nearly-sorted data.

The fix should touch only the qlog package, specifically the serialization path for event arrays.
```


The `connIDManager` in `quic-go` keeps a queue of unused connection IDs received from the peer, processing `NEW_CONNECTION_ID` frames and retiring old IDs as needed. Internally, this queue is currently a linked list, which shows up in profiles as an allocation and cache‑miss hotspot, especially when frames arrive out of order and insertions happen in the middle.

The queue is naturally bounded by `protocol.MaxActiveConnectionIDs`, entries must stay sorted by sequence number, and operations include iteration from front, removal from front, ordered insertion, and bulk removal of entries below a retire‑prior‑to threshold. A contiguous slice can serve all these operations with better cache locality and fewer allocations.

Refactor the internal queue representation to use a Go slice instead of a linked list. No external behavior may change: the public API of the connection ID manager must remain identical, the same invariants around ordering, retirement, duplicate detection, and conflict reporting must hold, and the manager must never exceed the maximum active IDs.

This is a pure performance improvement. All existing unit tests should continue to pass without modification.

refactor connection ID manager to use a slice instead of a linked list (#5022)


**Title**: Trace dropped packets in the packet handler map on header parse failures

**Request**:
When a QUIC packet arrives that cannot be parsed due to an invalid header (e.g., unparseable connection ID), the packet handler silently discards the packet. Users who have registered a tracer on the packet handler map want to observe these drops. Update the handling of unparseable packets so that, when a `wire.ParseConnectionID` error occurs and the packet handler map has a non-nil tracer, it calls `DroppedPacket` on the tracer with:

- the remote address from which the packet was received
- `logging.PacketTypeNotDetermined` as the packet type
- the raw packet length as the byte count
- `logging.PacketDropHeaderParseError` as the drop reason

This does not change existing logging; it only adds a new tracing callback. The tracer must be checked for nil before invoking the method. The rest of the packet handling (the existing debug log and early return) should be preserved.

trace dropped packets in the packet handler map


**Title:** Enforce RFC 9114 pseudo-header ordering in HTTP/3 requests

**Description**
Per RFC 9114 Section 4.3, all pseudo-header fields (e.g. `:method`, `:path`, `:authority`) in an HTTP/3 request MUST appear before any regular header fields. Currently quic-go’s HTTP/3 server does not reject requests that violate this ordering—pseudo-headers that appear after a regular header are silently processed, which is non‑compliant.

**Acceptance Criteria**
- When parsing an incoming HTTP/3 request, once the first regular (non‑pseudo) header field is encountered any subsequent pseudo-header field MUST cause the request to be rejected.
- The rejection SHOULD return a clear error (e.g. a stream or request error) indicating that a pseudo-header was received after a regular header field.
- This check MUST NOT break existing header validations (case‑sensitivity of field names, field‑value validity, valid pseudo‑header names, etc.).
- Requests that contain only regular headers, or all pseudo-headers before any regular headers, MUST continue to be accepted and handled as before.

http3: enforce ordering requirement between pseudo and regular headers (#3968)


**Request: Minimize UDP socket usage in default `http3.RoundTripper` dial path**

Currently, when an `http3.RoundTripper` is used without a custom `Dial` function, each QUIC connection creates its own ephemeral UDP socket. This can cause unnecessarily high socket usage, especially when the same transport is used to connect to many servers (e.g., in a proxy or gateway). Environments with limited file descriptors or strict socket-tracking policies are particularly affected.

We want the default dial path to create a single UDP socket for the lifetime of the `RoundTripper` and reuse it across all QUIC connections (even to different remote addresses). A custom `Dial` function, if set, should continue to have full control and not be forced into socket reuse.

**Acceptance criteria:**

1. When `RoundTripper.Dial` is `nil` (the default), the first outgoing request causes the transport to open a single UDP socket (bound to a random port). All subsequent requests made through that same `RoundTripper` instance share that same socket, regardless of the destination server address.
2. Calling `RoundTripper.Close()` must close the underlying shared socket, in addition to closing any open QUIC connections. After close, a subsequent use is undefined (matching existing semantics).
3. If a custom `Dial` function is provided, the transport’s internal socket should not be created and the custom dial is used for every connection, preserving backward compatibility.
4. The single-socket behavior must not leak sockets on error paths: a failure to create the socket on first use should be surfaced to the caller without leaving any open resources, and normal close/cleanup should be correct.
5. The socket must be bound to a random local address (e.g., `:0`), not a fixed port, to avoid conflicts and keep the current dynamic behavior.

No changes are required to the QUIC-level dial API or to the `client` struct; the intent is only to change how `RoundTripper` supplies its default dialer.

http3: use a single UDPConn in RoundTripper (#3720)


# Bug: HTTP/3 response writer does not panic on invalid status codes

The `WriteHeader` method on the HTTP/3 response writer (`http3`) should behave like the standard library’s `http.ResponseWriter` and panic when an invalid HTTP status code is passed. Currently, codes outside the three-digit range—for example `99` or `1000`—are accepted without any error or panic, which violates the expected contract.

**Observed behavior:**
Calling `WriteHeader` with a non‑three‑digit integer silently succeeds and continues processing.

**Expected behavior:**
`WriteHeader` must panic with an informative message (e.g., `"invalid WriteHeader code N"`) if the status code is not a valid HTTP status number (i.e., less than 100 or greater than 999).

**Additional constraints:**
- 1xx informational status codes must still be handled correctly: they should not trigger the header‑written state or the addition of the `Date` header (as is standard for HTTP).
- The panic must be immediate—before any side effects such as writing headers—when an invalid code is detected.

**Impact:**
This discrepancy breaks the common assumption that HTTP response writers in Go panic on invalid codes, potentially hiding bugs in handlers that accidentally pass malformed status values.

http3: panic in ResponseWriter.WriteHeader for invalid status codes (#3984)


We need a way to test that the handshake succeeds when the ClientHello is larger than the MTU (e.g., post-quantum ClientHellos requiring multi-packet fragmentation). Add a testing-only hook that can inject arbitrary transport parameters into the client's transport parameters during marshaling. The hook must only affect the client perspective; it should not alter server transport parameters. Then use that hook in the handshake drop integration test (`integrationtests/self/handshake_drop_test.go`) to inflate the ClientHello to ~2500 bytes and verify that the handshake completes when incoming packets are randomly dropped.

add a handshake test for post-quantum-size ClientHellos (#3759)


**Bug: Panic when path probe packet is acknowledged after loss detection**

When connection migration probing is active, a path probe packet can be declared lost by a loss detection timeout before its acknowledgment arrives. The current handler panics in `detectAndRemoveAckedPackets` with `"path probe doesn't exist: <pn>"` because the probe was already removed from the probe-specific history during loss handling, but the ACK path still expects it to exist.

In `detectLostPathProbes`, the code also performs an unnecessary removal from the regular packet history (`Remove(p.PacketNumber)`) in addition to the probe history removal. This extra call can further confuse the tracking state.

**Expected behavior:**
1. When an ACK arrives for a path probe packet that has already been declared lost (and thus removed from the probe history), the handler should not panic. It should simply skip adding that probe to the acknowledged packets list and continue.
2. In the loss detection path for probes, only the probe-specific history (`RemovePathProbe`) should be updated; the redundant removal from the regular packet history must be removed.

ackhandler: fix panic in probe packet tracking logic (#4998)


In the packet packer, when we’ve sent `MaxNonAckElicitingAcks` consecutive non‑ack‑eliciting packets and append a PING frame to the next ACK‑only packet to make it ack‑eliciting, that PING frame currently participates in loss retransmission. There is no need to retransmit it: if the packet is lost, a fresh PING will be added automatically after another batch of non‑ack‑eliciting packets. Change the packer so that these automatically‑inserted PING frames are **not** retransmitted on loss.

don't retransmit PING frames added to ACK-only packets


**Request**

### Summary
On Go 1.27+, leverage the new `tls.QUICConfig.ClientHelloInfoConn` field to avoid cloning the TLS configuration and injecting a fake `net.Conn` into `GetCertificate` / `GetConfigForClient` callbacks. Also stop cloning the client-side `tls.Config` when it is not needed. The old behaviour must be preserved when building with older Go versions.

### Acceptance criteria
1. **Server callbacks still see correct addresses regardless of Go version**
   When a QUIC server is created, any `GetCertificate` or `GetConfigForClient` callback supplied in the `tls.Config` must receive a `tls.ClientHelloInfo` whose `Conn` field exposes the correct local and remote addresses (as `net.Addr`).
2. **With Go 1.27+: no cloning for server addresses**
   The addresses are provided by setting `tls.QUICConfig.ClientHelloInfoConn` when constructing the QUIC server, instead of cloning the user's `tls.Config` and wrapping the callbacks. The original `tls.Config` is not altered.
3. **With Go 1.27+: client-side `tls.Config` is not cloned**
   When creating a client crypto setup, the provided `tls.Config` is used directly; no clone is made and `MinVersion` is not forced to TLS 1.3 (the standard library handles that internally).
4. **With Go <1.27: existing behaviour is unchanged**
   The server-side approach of cloning the config, setting `MinVersion` to TLS 1.3, and injecting a fake `conn` into the callback wrappers remains in place. Client-side cloning and `MinVersion` forcing are also kept.
5. **Recursive configurations still handled**
   If a `GetConfigForClient` callback returns a new `tls.Config`, addresses must be correctly passed to any callbacks inside the returned config, without modification of the returned config itself.

### Constraints
- The code must compile on Go 1.26 (the minimum Go version currently supported) as well as future 1.27+ releases. Use build tags (`!go1.27` / `go1.27`) to select the appropriate implementation.
- All existing tests must pass on both old and new Go toolchains, without changing test logic (tests may need to be reorganised into version-specific files, but their assertions and intent must remain identical).
- The `internal/handshake` package’s existing helper that creates the fake `net.Conn` (`conn` struct) must still be present and used for the pre-1.27 path.

handshake: use new tls.QUICConfig.ClientHelloInfoConn API, avoid cloning tls.Config (#5571)


## Bug: Long Header packet length field uses variable-length encoding instead of fixed 2-byte encoding

The Length field in QUIC Long Header packets is being serialized with a variable-length integer encoding (`VarInt`), but the IETF QUIC transport drafts (draft-29 and draft-32, both targeted by this repo) require the Length field in all Long Header packets to use a fixed 2-byte encoding.

**Observed behavior:**
When a Long Header packet is written, the packet length field can be 1, 2, 4, or 8 bytes depending on the actual length value, because it is written as a variable-length integer. Similarly, the header length reported by `GetLength` varies based on the magnitude of the length value.

**Expected behavior:**
The Length field in Long Header packets should always occupy exactly 2 bytes on the wire, regardless of the actual packet length value. The `GetLength` method should always account for exactly 2 bytes for this field.

**Impact:**
- Interoperability: other QUIC implementations that strictly follow the spec will fail to parse packets from this library when the length value is small enough to be encoded in 1 byte, or large enough to require 4/8 bytes, because they expect exactly 2 bytes at that position.
- The incorrect header size calculation can cause framing errors downstream.

**Edge cases / notes:**
- This applies to all Long Header packet types (Initial, Handshake, 0-RTT, Retry).
- The Token length field within Initial packets is intentionally still a variable-length integer — that behavior is correct and should not change.
- The length value may exceed what fits in 2 bytes (e.g., values near protocol.MaxPacketSizeIPv4); in such cases the value should still be written in exactly 2 bytes, and if it overflows 16 bits, the encoding should handle that the same way other 2-byte encodings in the codebase do.

always use a 2-byte encoding for the packet length of Long Header packets


Move the Content-Length enforcement from the stream wrapper (`lengthLimitedStream`) into the body types themselves.  Currently `ReadResponse` and `handleRequest` optionally wrap the HTTP stream with a length-limited stream, but this enforcement should live on the body because it's a property of the request/response body.

The body (and by extension `requestBody` and `hijackableBody`) should accept the content length (use -1 for no limit), limit reads to that many bytes, and if the peer sends more data than declared, cancel the underlying stream with `ErrCodeMessageError` and return a distinct error.  The `hijackableBody`'s `Read` should call the embedded body's `Read` to pick up this enforcement.

Update `newRequestBody` and `newResponseBody` to accept a `contentLength int64` argument, and update the callers accordingly.  Remove the `lengthLimitedStream` type and its constructor.

http3: move length limiting logic to the body (#4439)


Title: Proxy packet queue performs unnecessary full sorts on out-of-order insertion

When the proxy’s packet queue receives packets whose timestamp is earlier than the last enqueued packet, it currently calls `sort.Stable` on the entire packet slice. This is an O(n log n) operation every time reordering occurs, which happens frequently in realistic network conditions. Since the slice is already sorted except for the newly appended element, we can achieve the same correctness with an O(n) insertion.

Please change the queue’s insertion logic to keep the `Packets` slice sorted at all times by inserting new entries directly at the correct position without ever performing a full sort.

The queue must satisfy these invariants:
- `Packets` is always sorted by monotonically non‑decreasing `Time`.
- The associated timer (`timer`) must be reset **only** when the newly added packet becomes the earliest packet in the queue (i.e., it is inserted at index 0). If the queue was empty before insertion, the timer should also be started for the first packet.
- If the new packet’s timestamp is equal to or later than all existing entries, it should simply be appended; nothing extra needs to happen to the timer unless the queue was previously empty.

The change should be confined to the packet queue’s `Add` method and may remove the custom `sort.Interface` wrapper (`packetEntries`) since it would no longer be needed after eliminating the full‑sort path.

proxy: optimize packet sorting logic (#4923)


As a user of the quic-go qlog tracing, I noticed that the logs do not include the `connection_closed` event that the standard QUIC qlog schema expects. Instead, I currently see a `transport:connection_state_updated` event with just `"new": "closed"` and a trigger string, and only for timeouts. Stateless resets appear as a separate `transport:packet_received` event, and connection closes due to application errors or transport errors are not logged at all.

This makes it hard to debug connection closures with a single, structured event that captures the real reason. The goal is to replace these inconsistent approaches with a single `transport:connection_closed` event that is emitted whenever a connection is closed, carrying an `owner` field (`local`/`remote`) and the relevant details depending on the `logging.CloseReason`:

- For timeouts, include `trigger` with `"idle_timeout"` or `"handshake_timeout"` and `owner: "local"`.
- For stateless resets, include `trigger: "stateless_reset"`, `owner: "remote"`, and the `stateless_reset_token` hex string.
- For application errors, include `owner` (`local`/`remote` based on whether the error source was remote) and an `application_code` integer.
- For transport errors, include `owner` (`local`/`remote`) and a `connection_code` string describing the transport error (e.g., `"aead_limit_reached"`).

The `logging` package already exposes a `CloseReason` interface with methods `Timeout()`, `StatelessReset()`, `ApplicationError()`, and `TransportError()` that can be used to distinguish the reason. The qlog tracer (`connectionTracer.ClosedConnection`) should simply pass the full `CloseReason` to an `eventConnectionClosed` struct that knows how to serialize the appropriate fields.

The old `eventStatelessResetReceived` event type and the separate paths for timeout vs. stateless reset should be removed in favor of the unified event.

add support for the connection_closed qlog event


```
Simplify CRYPTO frame handling: separate frame reassembly from handshake message dispatch

Currently, the crypto stream manager both reassembles CRYPTO frames and immediately delivers assembled bytes to a crypto data handler interface. This couples frame-level and handshake-level concerns, and the extra interface indirection adds unnecessary complexity.

We want the manager to focus solely on buffering and ordering CRYPTO data per encryption level. The connection’s handleCryptoFrame should ingest the frame via the manager, then explicitly ask the manager for any newly assembled data and deliver it directly to the crypto stream handler.

Implementation requirements:

- Remove the `cryptoDataHandler` interface. Remove its mock file and the mockgen directive for it.
- Add a method to the manager that returns the next assembled data for a given encryption level (analogous to `GetCryptoData()` on a single stream, but selecting the correct stream by level). The connection's `handleCryptoFrame` must use this to drain assembled data after each frame ingestion.
- Move the loop that calls `HandleMessage` into `handleCryptoFrame`, placed after the manager’s `HandleCryptoFrame` call. The manager’s `HandleCryptoFrame` should no longer call `HandleMessage` itself.
- Remove the `cryptoDataHandler` parameter from `newCryptoStreamManager`.
- Add `HandleMessage([]byte, protocol.EncryptionLevel) error` to the `cryptoStreamHandler` interface so the connection can invoke it directly.
- Update unit tests so that manager tests no longer expect `HandleMessage` calls but instead verify the new data-retrieval method. The connection-level test for crypto frame handling should verify the new message-dispatch loop.
```

simplify handling of CRYPTO frames, remove cryptoDataHandler interface (#4588)


The `responseWriter` embeds a `headerWriter` that holds stream, header, status, and logger state. This indirection is unnecessary. Merge those fields directly into `responseWriter` and remove the `headerWriter` type.

Additionally, the `responseWriter` manually constructs HTTP/3 HEADERS and DATA frames and writes them to the stream. The `stream` wrapper used for HTTP/3 adds its own DATA framing in its `Write` method, so calling `stream.Write` from the response writer would produce double-framed output. Bypass the stream wrapper's framing layer and write raw bytes directly to the underlying QUIC stream for both headers and data.

Update the server's request handling code to create the `stream` wrapper once and pass it to both the request body and the response writer, rather than passing the raw QUIC stream.

http3: simplify response header writing (#4441)


**Title:** Concurrent `http3.Server.Serve` and `Close` can deadlock and prevent shutdown

**Description:**

I’m running an `http3.Server` by calling `Serve` (or `ServeListener`) in a background goroutine and later call `Close` from another goroutine when I want to stop the server. Under concurrent scheduling, the server sometimes never shuts down cleanly: the `Close` call returns, but the goroutine that called `Serve` remains blocked and never returns. The server resources (the UDP socket / QUIC listener) also remain open, causing resource leaks and preventing a graceful restart.

Steps to reproduce:
1. Create an `http3.Server` and assign a `*http.Server` to its embedded field.
2. Call `Serve` with a `net.PacketConn` (or `ServeListener` with a QUIC listener) in a separate goroutine.
3. Shortly after, call `Close` from the main goroutine (e.g., after a short delay or triggered by a signal).
4. Observed: the program sometimes hangs, with `Serve` never returning.

Expected behavior:
- `Close` should cause all active listeners to be closed immediately.
- Any `Serve`/`ServeListener` call that is still in progress should return (with an appropriate error, ideally `http.ErrServerClosed`) after the listener has been closed.
- No goroutine remains stuck, and all network sockets are released.

Additional notes:
- The problem seems to be a race between the point where `Serve` registers a new listener and when `Close` iterates over existing listeners to close them. If `Close` observes an empty or incomplete listener set before `Serve` adds the listener, the new listener never gets closed, leaving the accept loop in `Serve` waiting forever.
- The issue also affects `ServeListener` and any path that ends up calling the internal helper that creates or registers a listener.

fix deadlock on concurrent http3.Server.Serve and Close calls (#3387)


**Title:** Pacer budget calculation overflows and returns negative values under high bandwidth

**Background:**
The pacer implements a token-bucket algorithm. The `Budget(now time.Time) protocol.ByteCount` method computes the available bytes as:

```
budgetAtLastSent + (adjustedBandwidth * elapsedNanoseconds) / 1e9
```

where `protocol.ByteCount` is `int64`.

**Problem:**
When the adjusted bandwidth is extremely high (e.g., in testing with “infinite” bandwidth or in extreme configurations) or the elapsed time is very large, the product `adjustedBandwidth * elapsedNanoseconds` can overflow the signed 64-bit integer, wrapping to a negative value. The resulting budget becomes negative, which violates the invariant that the pacing budget should always be non‑negative. A negative budget can cause incorrect pacing decisions and may lead to panics downstream.

**Desired fix:**
Add overflow protection to the budget calculation so that it never returns a negative value. If the computed budget is negative (indicating overflow), saturate it to `protocol.MaxByteCount`. This is safe because a negative result can only arise from overflow, and in that situation the real budget is effectively infinite.

**Scope:**
The primary change is in the `Budget` method. The same overflow pattern might also appear in `maxBurstSize` and `TimeUntilSend`, but those use `uint64` arithmetic that does not produce a recognisable negative rollover in the same way. A review of those locations is welcome, but the immediate issue is the negative budget returned by `Budget`.

---
*Resulting behavior: `Budget` always returns a non‑negative value, clamped to `protocol.MaxByteCount` on overflow.*

congestion: fix overflow when calculating the pacing budget (#3796)


**Title**: Enforce TLS 1.3 minimum version on Config returned by user-provided `GetConfigForClient` callbacks

**Problem**
QUIC requires TLS 1.3, but users can supply a `tls.Config` that includes a `GetConfigForClient` callback. Our QUIC setup wraps that callback to inject metadata (local/remote addresses) into `ClientHelloInfo`. If a user's callback returns a `*tls.Config` that does not explicitly set `MinVersion` to `tls.VersionTLS13`, the QUIC handshake may attempt to negotiate an older TLS version, resulting in handshake failure or protocol errors.

**Expected behavior**
After cloning the config returned by the user's `GetConfigForClient` (to avoid mutating the original), we should force `MinVersion = tls.VersionTLS13`. This ensures the TLS handshake only considers TLS 1.3, even when the callback-returned config omits the version constraint.

**Constraints & edge cases**
- The enforcement must happen **after** cloning the returned config, preserving the user's original config unchanged.
- The enforcement should apply **recursively**: if the returned config itself contains a `GetConfigForClient` callback (our wrapper already chains those calls), the inner callback's result must also have `MinVersion` set to `tls.VersionTLS13`.
- Do **not** overwrite unrelated version settings such as `MaxVersion` or other fields beyond `MinVersion`.
- This is a workaround for <https://github.com/golang/go/issues/63722>; document the intent with a comment referencing that issue.

handshake: set MinVersion on the Config returned by GetConfigForClient (#4134)


```
Make logging.PacketType a distinct type — not an alias for protocol.PacketType.
protocol.PacketType only defines long-header packet types (Initial, Handshake, Retry, 0‑RTT).
Logging needs to represent *all* QUIC packet types, including short‑header packets
(1‑RTT, Version Negotiation, Stateless Reset) and a “NotDetermined” sentinel.

Define a new named `PacketType` in the logging package with its own iota constants covering
the full set of packet types. Then update every logging interface, mock (tracer and connection tracer),
and any call site that currently uses `protocol.PacketType` to accept/produce the new logging type.
Finally remove the unnecessary import of the protocol package from the logging types file.
```

avoid type confusion between protocol.PacketType and logging.PacketType


**Title:** HTTP/3 client silently discards response trailers (trailing HEADERS frames)

**Description:**
When an HTTP/3 server sends trailers (a `HEADERS` frame after the initial response `HEADERS` and any `DATA` frames), the quic-go client does not expose them to the caller. The trailers are silently consumed and discarded, and `http.Response.Trailer` remains empty. This makes it impossible to use protocols that rely on response trailers, such as gRPC status reporting.

**Steps to reproduce:**
1. Establish an HTTP/3 connection to a server that emits a trailer (a `HEADERS` frame with pseudo-header fields after the response body).
2. Call `RoundTrip` and read the response body completely with `io.ReadAll`.
3. Inspect `rsp.Trailer` — it is `nil`.

**Expected behavior:**
- After the response body is fully consumed, `http.Response.Trailer` should contain the header fields from any trailing `HEADERS` frame.
- The trailers must be parsed only on the client side; the server should continue to ignore any `HEADERS` frames it receives on request streams (current behavior).
- If the server sends multiple trailing `HEADERS` frames (a protocol violation), reading the body should return an error indicating that additional `HEADERS` frames are not permitted after the trailers have already been parsed.
- If a `DATA` frame appears after a trailing `HEADERS` frame has already been received (also a protocol violation), reading the body should return a corresponding error.
- A `HEADERS` frame that exceeds the maximum header byte size should also produce an error.

**Impact:**
- Any HTTP/3 client that needs to inspect trailers (e.g., gRPC status, custom metadata after streaming responses) cannot do so.
- The current behavior silently violates the HTTP/3 contract for clients, leading to subtle protocol incompatibilities.

http3: add client-side HTTP Trailer support (#4581)


When a Retry packet is received and processed successfully, that event is not recorded in the qlog trace. The trace should include a transport‑level “packet_received” event for Retry packets, with `packet_type` set to `"retry"`. The event must contain the header from the Retry packet (source and destination connection IDs, payload length), but it must not include a `"frames"` array or any coalescing metadata, since Retry packets carry no frames and are never coalesced.

The qlog Tracer interface (`qlog.Tracer`) currently has no way to accept a Retry packet header — the existing `ReceivedPacket` method requires a `*wire.ExtendedHeader`, while Retry processing only has a `*wire.Header`. The interface needs to be extended so that a Retry packet reception can be logged, and the session’s Retry-handling path must call into it when a valid Retry is received (after the Retry integrity check passes, but before the destination connection ID is switched).


We need to add support for parsing and writing QUIC v2 long header packets. In QUIC v1, the two-bit packet type field in the first byte of a long header maps as follows:

- 0b00 = Initial
- 0b01 = 0-RTT
- 0b10 = Handshake
- 0b11 = Retry

In QUIC v2, this mapping is changed to:

- 0b00 = Retry
- 0b01 = Initial
- 0b10 = 0-RTT
- 0b11 = Handshake

Currently our header parsing and writing code only implements the v1 mapping. This means that when we receive a QUIC v2 packet, we will misinterpret the packet type — for example, a v2 Retry will be parsed as Initial. Similarly, when writing headers, we always use the v1 bit patterns, which will produce invalid v2 packets.

We also have a helper `Is0RTTPacket` that checks the packet type bits but does not distinguish between v1 and v2; it needs to handle the remapped 0-RTT bit pattern for v2.

The fix should:
1. In the long-header parsing logic, use the v2 bit-to-type mapping when the version is `protocol.Version2`.
2. In the long-header writing logic, select the correct two-bit value based on the packet type and the target version (v1 or v2).
3. Update `Is0RTTPacket` to apply the correct bit check depending on the version of the packet being inspected.
4. Ensure existing v1 behavior is unchanged.

This affects only long header packets; short headers are not version-dependent and should remain untouched.

implement parsing and writing of QUIC v2 packet headers


The QUIC framer’s `streamQueue` is currently a `[]protocol.StreamID` used as a FIFO queue.  Removing the front via `f.streamQueue = f.streamQueue[1:]` causes per-operation allocations and copies, and clearing the queue only resets the length without freeing the backing array.  Replace the slice with a ring-buffer to eliminate these allocations and provide a true `Clear` that releases stored elements.

Implement a generic ring-buffer in `internal/utils/ringbuffer` with at least the following methods:
- `PushBack` – add an element, growing the buffer when full.
- `PopFront` – remove and return the front element (panic when empty).
- `Empty`, `Len`, `Clear` – `Clear` must reset the buffer to an empty state and zero out stored elements to avoid dangling references.

Then update `framer.go` to use the ring-buffer for `streamQueue`.  The framer’s existing semantics must be preserved:
- `AddActiveStream` adds an ID only if it is not already tracked.
- `HasData` reports true when the queue is non-empty.
- `AppendStreamFrames` pops `numActiveStreams` IDs in order, fetches the stream, generates a STREAM frame, and re‑pushes the ID at the back if the stream still has data.
- `Handle0RTTRejection` empties the queue completely.


When a `Transport` dials with a host string that contains a port (e.g., `"example.com:443"`) and the `tls.Config.ServerName` is left unset, the TLS server name is currently set to the full `host:port` string instead of just the host part. That produces an invalid SNI and causes TLS handshake / certificate validation failures.

Strip the port from the host string before inferring `tls.Config.ServerName`. Preserve the existing behavior for the empty-host case (fallback to the IP string from the `net.UDPAddr`) and for the case where `ServerName` is already explicitly set. Use `net.SplitHostPort` so that IPv6 addresses with ports are handled correctly.

remove the port from the hostname used for tls.Config.ServerName (#4046)


The HTTP/3 RoundTripper's connection cache mishandles failures: once a cached QUIC connection becomes dead (e.g., idle timeout), all subsequent requests to that host reuse the dead connection and fail.

Fix the per-host connection reuse so that:

- Any cached client is evicted immediately when a request on it fails.
- If the failure is a timeout error (idle timeout) and the client had already completed a handshake on a previous request (i.e., it was being reused), automatically retry the request with a fresh dial after eviction.
- Do not retry for non-timeout errors, or for clients that were never successfully reused, to avoid infinite loops.
- Multiple concurrent requests that encounter the same failing cached client must result in at most one new connection being established, not one per request.

http3: correctly handle closed clients (#3684)


**Summary:**
The`getMetadata` method in `CoursierAlg` currently wraps its result in `Option`. Observing its usage shows that callers never treat the `None` case differently from an empty metadata record; they always process the result as metadata. This causes unnecessary unwrapping and makes the API harder to use.

**Desired behavior:**

- `getMetadata` should return `F[DependencyMetadata]` directly, not `F[Option[DependencyMetadata]]`.
- When no resolvers are available or no metadata fields can be discovered, the method should return an empty `DependencyMetadata` (all optional fields `None`) rather than a missing result.
- Callers that previously checked for the presence of metadata via `Option.isDefined` should instead inspect the relevant field (e.g., `repoUrl.isDefined`) on the returned metadata instance.
- The internal accumulation logic that walks parent POMs should use a non‑optional accumulator; the existing enrichment behaviour (`enrichWith`) must continue to work correctly when starting from an empty metadata base.

**Constraints/Safety:**

- The `None`-to-empty behaviour change must be consistent for all edge cases, including when no resolvers can be converted and when the dependency has no POM metadata (minimal POM).
- All public callers of `getMetadata` must be updated to compile and behave correctly with the new return type.
- The `empty` metadata instance should be a single well‑known value so callers can reference it if needed.

Change return type of getMetadata to F[DependencyMetadata]


**Bug report: Per-repository branch specification ignored when fetching and cloning**

When a repository is added to the repos file with an explicit branch (e.g., `owner/repo:develop`), Scala Steward should work on that specific branch. Instead, it appears to always use the global `--default-branch` setting (or the platform default) for operations such as:

- determining which branch to use as the "default branch" when creating/retrieving the fork or repo metadata,
- checking out the branch after cloning the repository locally.

This causes the bot to inspect and update the wrong branch for repos that have a custom branch configured.

**Expected behavior**

- The branch specified per repository (the part after the colon in a repos-file line) must be used when identifying the default branch and when checking out the working copy.
- The global `--default-branch` option should apply _only_ to repositories that do not specify a branch of their own.
- This behavior should be consistent across the metadata retrieval step (`createForkOrGetRepoWithDefaultBranch`) and the local clone step.

**Impact**

Repositories that explicitly request a non-default branch via the repos list are silently ignored or updated on the wrong branch, making the per-repo branch feature unusable for any purpose. This defeats the intent of the repos-file syntax that allows a branch suffix.

Use branch from `Repo` class instead of configuration


Bug: Scala Steward silently ignores repository entries when the branch name contains a slash (e.g., `series/3.x`).

**Steps to reproduce**
1. Add a line like `- owner/repo:series/3.x` to the `repos.md` file.
2. Run Scala Steward.
3. Observe that no update PRs are created for that repository, even though the repo exists and the branch is correct.

**Expected behavior**
Lines of the form `- owner/repo:branch` must accept branch names that include slashes. The `:` separates the repository identifier from the optional branch name, and everything after the colon is the branch. The parsing should handle any branch name that can appear in a Git ref, including slashes, dots, and hyphens.

**Acceptance criteria**
- A line like `- typelevel/cats-effect:series/3.x` is recognised and results in `Repo("typelevel", "cats-effect", Some(Branch("series/3.x")))`.
- Existing formats without a branch or with a branch without a slash continue to work exactly as before (e.g., `- owner/repo`, `- owner/repo:main`).
- Lines that do not start with a dash and space are still ignored.
- Lines missing the `/` between owner and repo are still ignored.
- Leading/trailing whitespace around the owner, repo, and branch is trimmed as before.

Allow slashes in branches in the repos.md file (#2297)


**Title:** Switch branch-update flow from merge/revert to `git reset --hard`

**Background**
When an existing update PR branch is out of date (the base branch has moved), the bot currently tries to merge the base into the update branch using `--strategy-option=theirs` and then reverts the previous update commits before re-applying the new update. This logic is fragile: it causes unnecessary merge conflicts, requires special handling for modify/delete conflicts, and produces merge/revert artifacts in the branch history.

**Desired Behavior**
Instead of merging and reverting, the update flow should:

1. Hard-reset the update branch to the tip of the base branch (`git reset --hard <base>`).
2. Re-apply the update edits on top of that clean base, exactly as it would for a new branch.
3. Force-push the rebuilt branch (the existing force-push behavior remains unchanged).

**Scope & Constraints**
- Remove the `mergeTheirs` and `revertChanges` operations from the Git abstraction and their file-backed implementations – they are no longer needed.
- Add a simple `resetHard` operation that performs `git reset --hard <ref>`.
- The existing decision logic (whether an existing PR needs to be updated) and the edit-application/push logic should remain the same; only the branch-rebuild step changes.
- The conflict-detection check (`hasConflicts`) that runs before the update should be hardened so that if a trial merge fails in an unexpected way, the abort cleanup still succeeds without propagating an error into the main flow (no partial merge state left behind).
- No change to the public API contract of update PR creation/management; only the internal reconstruction of the branch is affected.

**Rationale**
A reset-based rebuild gives a clean linear history on the update branch, completely avoids merge conflicts during updates, and eliminates the complex revert/merge-strategy workarounds.

Reset instead of merge to update branches


```
Title: Pull-request head computation not defined on ForgeType trait, risking omission for new forges

Description:
Currently, forming the `head`/`source_branch` used when listing and creating pull requests relies on a separate utility function that matches over `ForgeType` values. When a new forge type is added (e.g., a new Bitbucket variant or a future host), that function must be manually updated. Forgetting to add a case can lead to an incorrect head value—for instance, GitHub needs the fork owner prepended, while other forges need only the branch name. A missing case may silently produce a wrong PR head, causing pull-request discovery and creation to fail in confusing ways.

Expected behavior:
The logic for the pull-request head should live on the `ForgeType` trait itself. A default implementation that returns just the branch name would work for most forges, while specific forges like GitHub can override it to prepend the fork owner. This way, the compile-time check ensures every forge type is handled, and the intent is documented right alongside the forge’s other configuration.

Impact:
Improves maintainability and prevents subtle bugs when extending forge support. Without this change, adding a new forge requires hunting down scattered conditional logic, with a high risk of incomplete updates.
```

Refactor pull request head: replace conditionals with polymorphism


As a Mill user, I recently upgraded my project from Mill 0.11 to Mill 1.0. After the upgrade, Scala Steward stopped opening dependency update pull requests for my repository. I tracked it down to two issues.

1. **Mill version detection no longer works.**
   In Mill 0.x, the build tool version was declared in a `.mill-version` file, or sometimes in `.config/mill-version`. Scala Steward currently reads those files to determine the Mill version. Mill 1 removed that mechanism. Instead, the Mill version is declared as a comment inside the build file itself (either `build.mill` or `build.mill.scala`) in the form:

   ```
   //| mill-version: 1.0.5
   ```

   The version value may be optionally surrounded by double (`"`) or single (`'`) quotes (e.g., `"1.0.5"` or `'1.0.5'`), or be unquoted. Whitespace around the value may vary.

   Scala Steward currently does not look for the version in these build files, so it cannot determine the Mill version for Mill 1 projects and fails to run.

2. **The Mill plugin artifact has changed for Mill 1.**
   The artifact `scala-steward-mill-plugin` now needs to use the Mill 1 classifier (e.g., `_mill1_3` instead of the old `_mill0.11_2.13`). Scala Steward currently ships with the Mill 0.11 version of the plugin, which is incompatible with Mill 1.

**Expected behavior**:
Scala Steward should be able to work with Mill 1 projects by:

- Detecting the Mill version from `build.mill` or `build.mill.scala` using the `//| mill-version:` comment. The search for the version should first try these build files, falling back to the legacy `.mill-version`/`.config/mill-version` files for older projects. The parsing should strip optional surrounding quotes and trim whitespace.
- Using the Mill 1 variant of the Scala Steward Mill plugin artifact when the project uses Mill 1 (and respectfully, update the bundled plugin dependency so it's available).

**Impact**: Without this, any project that migrates to Mill 1 loses automatic updates from Scala Steward. Mill 1 is the current stable release of Mill, and more projects will adopt it.


**Title:** Prevent version replacement from accidentally matching a substring inside another version string

When the version replacement heuristic searches for the current version in a build file, it may match a substring that is part of a different, longer version string. For example, when updating `"2.2.0"` to `"2.3.0"`, the heuristic might replace the substring `2.2.0` inside `"3.2.2.0"` even though the quotes around `"3.2.2.0"` do not enclose just `2.2.0`.

Make the replacement only apply when the matched version text is either **unquoted** or **fully enclosed in matching double quotes** (i.e., `"` immediately before and after the matched substring). Specifically:

- If a `"` appears immediately before the match, a `"` must also appear immediately after it – otherwise do not replace.
- If a `"` appears immediately after the match but there is no `"` immediately before it, do not replace.
- If no `"` appears on either side, replacement is allowed.

This ensures that a version string that is truly a standalone version (or an unquoted reference) is updated, while a substring that is only part of a different quoted version is left untouched.

Do not replace a substring in another version


Title: Grouped PR description incorrectly lists updates that were ignored via `// scala-steward:off`

**Description**
When two or more dependencies are grouped into a single pull request, and one of them is excluded with a `// scala-steward:off` comment, the generated PR body still shows the ignored dependency. This happens in at least three places:

* The “Updates” list at the top of the PR description.
* The “⚙ Adjust future updates” section, which suggests `updates.ignore` and `dependencyOverrides` rules for the ignored dependency.
* The embedded metadata at the bottom of the PR.

As a result, a reviewer looking at the PR sees a dependency listed as updated even though no version change was applied to it, and the proposed ignore snippet is for a dependency that was already excluded.

**Expected behavior**
The PR description should only mention dependencies for which an actual edit was made. A dependency that was skipped (e.g., because of a `// scala-steward:off` comment) should not appear in the “Updates” list, the “Adjust future updates” suggestions, or the metadata.

The grouping and the PR title should be unaffected – only the list of individual updates rendered inside the body should be filtered to those that were not ignored.

**Steps to reproduce**
1. Have a project with two dependencies that are grouped together in Scala Steward’s configuration (or eligible for automatic grouping).
2. Add a `// scala-steward:off` comment next to one of the dependencies in the build file.
3. Let Scala Steward run and open a grouped PR for the two dependencies.

Observe that the PR description still lists both dependencies as updated, and the “Adjust future updates” section suggests ignore rules for the dependency that was already excluded.

**Impact**
This is misleading for project maintainers. It makes it look like an update was intended but not actually performed, and it encourages adding redundant ignore rules for dependencies that are already guarded with `scala-steward:off`.

fix: Grouped PR description must not include updates ignored via // scala-steward:off


```
Feature Request: Make run summary accessible outside GitHub Actions

Currently, Scala Steward only outputs a markdown run summary when the
`GITHUB_STEP_SUMMARY` environment variable is set (i.e., when running
inside a GitHub Actions workflow). In any other CI environment or when
running locally, the summary is silently discarded and unavailable.

Please change the behavior so that when a run completes, Scala Steward
always writes a file named `run-summary.md` in the root of its
workspace directory. The file should contain the same markdown summary
that was previously appended to the `GITHUB_STEP_SUMMARY` path.

Key requirements:

- The file must be written regardless of the forge or CI environment.
- The file must be placed in the workspace root directory (the
  directory accessible via `WorkspaceAlg.rootDir`).
- Writing should use the existing `FileAlg.writeFile` semantics, so
  the file is overwritten with the final summary for each run (no
  appending needed across multiple runs).
- The previous `GitHubStepSummaryAlg` mechanism that depended on the
  `GITHUB_STEP_SUMMARY` environment variable is no longer needed and
  should be removed along with any associated append-to-file helpers
  if they are no longer used elsewhere.

This change will allow any CI system (or ad‑hoc local runs) to consume
the run summary from a predictable file location.
```

Make run-summary accessible to non-GitHub forges


```
Bitbucket Server integration currently assumes the default branch is always named 'master'. Many repositories use a different default branch (e.g., 'main'). The bot must stop hardcoding 'master' and instead use the repository's actual default branch, obtained from the Bitbucket Server API.

Acceptance criteria:
- When retrieving repository information from Bitbucket Server, the bot determines the repository's default branch by querying the Bitbucket Server API.
- The returned default branch is used as the repo's primary branch, replacing the hardcoded 'master'.
- The change applies only to the Bitbucket Server VCS backend; other backends are out of scope.
- The fix works for any repository whose default branch is not named 'master'.
```

BitbucketServer: do not hardcode default branch (#1866)


**Feature request: Respect `defaultBranch` configuration when creating forks and accessing repos**

**Background**
Scala Steward supports a `defaultBranch` configuration option to allow users to specify an alternative branch to treat as the default for a repository. This is useful when a project uses a non-`master` default branch, or when the bot should target a maintenance branch. Currently, however, this setting is silently ignored – the bot always uses the repository’s actual default branch (e.g. `master`) regardless of the configuration.

**Acceptance criteria**

* If `defaultBranch` is set for a repository, Scala Steward must use that branch as the *effective* default for all subsequent operations:
  * The `RepoOut` returned after creating a fork or fetching a repo (with or without forking) must have its `defaultBranch` field set to the configured branch, not the original branch.
  * The branch metadata (branch commit SHA, etc.) must be fetched for the configured branch, so that the rest of the pipeline (cache checks, update discovery, PR creation) operates against that branch.
* If `defaultBranch` is `None`, behavior must remain unchanged – the original default branch is used.
* The behavior must be provider-agnostic: GitHub, Bitbucket, and any other supported VCS should all be handled identically through the common VCS API layer.
* The fix must be contained in the `VCSApiAlg` trait and the call site in `RepoCacheAlg`, without per‑provider duplication.

**Relevant existing code**

* `VCSApiAlg.applyDefaultBranch` already exists and provides the logic to override a `RepoOut`’s default branch; it is simply not called in the right place.
* `VCSApiAlg.createForkOrGetRepoWithDefaultBranch` does not currently accept or use a `defaultBranch` parameter, and does not call `applyDefaultBranch`.
* `RepoCacheAlg.checkCache` has access to `config.defaultBranch` but does not pass it to `createForkOrGetRepoWithDefaultBranch`.

**Expected outcome**

After the change, a repository configured with `defaultBranch = custom` should cause Scala Steward to treat `custom` as the default branch for that repository, with the bot checking the `custom` branch’s latest commit and targeting subsequent updates on that branch, regardless of what the repository’s actual default branch is.


**Request: Avoid repeated reads of the same key in the pull_requests store**

During pruning, Scala Steward checks whether pull requests already exist for a given repository by repeatedly querying the `pull_requests` key-value store with the same repository key. This causes the same JSON file to be read and decoded from disk multiple times in quick succession, which is wasteful and can slow down processing for repositories that have not changed.

Introduce a caching layer for the `pull_requests` store that retains the value of the most recently accessed key. When a `get` request uses the same key as the currently cached entry, return the cached value without touching the underlying storage. When a `set` is performed, update the underlying storage and also update the cache with the new key-value pair to keep it consistent.

The caching should be transparent to existing callers and should only apply to the `pull_requests` store, not to other key-value stores in the system.

Add caching layer to pull requests store (#1842)


**Title:** Prefer GitHub release notes over custom release notes when deduplicating update-info URLs

**Description**

When Scala Steward prepares an update pull request, it collects one or more update-info URLs (release notes, changelogs, version diffs) and includes the best available ones in the PR body. These URLs are represented by variants of `UpdateInfoUrl`: `GitHubReleaseNotes`, `CustomReleaseNotes`, `CustomChangelog`, and `VersionDiff`. The same underlying URI may appear wrapped in multiple `UpdateInfoUrl` variants—for example, a GitHub release note URL might also be recorded as a custom release notes URL.

Currently, when that happens the deduplication logic (`distinctBy(_.url)`) keeps whichever variant happens to appear first, which is non-deterministic and may discard the higher-quality variant. This leads to PRs that sometimes omit the richer GitHub release notes in favor of a generic custom release notes link.

**Desired behavior**

1. Define a priority order among `UpdateInfoUrl` variants:
   - `GitHubReleaseNotes` (highest preference)
   - `CustomReleaseNotes`
   - `CustomChangelog`
   - `VersionDiff` (lowest preference)

2. When the update-info URL finder produces a list of candidate URLs, it must sort them according to the above order **before** removing duplicates by URI. After deduplication, every distinct URI must be represented by the highest-priority variant present in the original candidate list.

3. This ordering must be stable and deterministic; no other aspect of the finder’s behavior (especially the URI-existence check) should change.

**Non-obvious edge cases / safety constraints**

- The priority ordering applies **only** when two or more candidates share the exact same URI. If the URIs differ, both should be kept regardless of priority.
- The existence check (network lookups) must still be performed on the deduplicated URLs, not on the full unsorted list, so no extra HTTP requests are introduced.
- The change must not alter which URIs are ultimately considered “reachable”; it only affects which variant survives when multiple variants point to the same reachable URI.
- The behavior when only a single variant exists for a given URI must remain unchanged.

Prefer GitHubReleaseNotes over CustomReleaseNotes


**Bug: `process.slurp` ignores its timeout parameter**

`process.slurp` is documented to accept a `timeout: FiniteDuration` that should limit how long we wait for a spawned external command. In practice, the timeout is not enforced when the launched process does not exit quickly. For example, `sleep 1` with a timeout of `500.milliseconds` will cause `slurp` to block for the full second (or until the process terminates) instead of timing out after the given duration.

**Expected behaviour**

- When the spawned command has not terminated within `timeout`, `slurp` must complete (fail) promptly after that timeout elapses. The elapsed wall-clock time should stay close to the supplied timeout value, never reaching the full run-time of a slow or blocking process.
- The error returned on timeout should be a `TimeoutException` (or a subtype), and the error message should include any output that had been captured up to that point (the existing fallback message).
- The spawned OS process should be terminated when the timeout fires, so that we avoid leaving behind orphan processes or leaking resources.
- Any logging of the process output must still happen up to the point of cancellation.

**Observed problem**

The timeout argument appears to have no effect. When a process takes longer than the timeout, `slurp` simply waits until that process exits on its own, effectively bypassing the timeout entirely. This means users who rely on the timeout to guard against hung commands experience indefinite blocking behaviour.

**Impact**

This makes the timeout capability unreliable for any use case that needs a hard deadline on external commands (e.g., running `sbt`, `mill`, or other build tools that might stall). In automated dependency-update bots, this can lead to pipeline hangs, wasted CI minutes, and orphan processes that accumulate over time.

Fix: `process.slurp` ignores the timeout (#2385)


**Bug: Scala Steward misses version updates in multi-line declarations (e.g. pom.xml, multiline sbt vals)**

### Observed behavior
Scala Steward does not update version numbers when the artifact declaration or the version string itself spans multiple lines. This causes missed updates for projects that use a common formatting style, such as Maven’s `pom.xml` (where `groupId`, `artifactId`, and `version` are each on separate lines) or sbt build files where a `val` version definition is split across lines, e.g.:
```scala
val scalajsJqueryVersion =
  "0.9.3"
```

### Expected behavior
Version replacement should work regardless of whether the relevant text straddles line boundaries. A dependency update that is otherwise unambiguous (same groupId/artifactId/current version) should be applied even when newline characters separate the searched terms and the version literal.

### Additional improvement: treat “sbt” as a common word
In the heuristic that filters out overly generic search terms, the word `"scala"` is already recognized as a common (less informative) token. The word `"sbt"` should be treated similarly, so that artifact search matches are not influenced by this recurring non-distinctive substring.

### Impact
This affects any repository that uses multi-line dependency notations (e.g., standard Maven POM layout, or line-broken sbt definitions). Fixing this will make Scala Steward update those dependencies as reliable as it does for single-line declarations.


The RemoteFileSystem `read_file` and `read_range` methods return empty content for all files.
They use `request_blocking`, which only returns the final `AgentResponse`. The agent sends file data as streaming data chunks (base64-encoded, up to 65536 bytes per chunk), not in the final result payload. The current path discards those chunks, producing an empty `Vec<u8>`.

**What needs to happen**
`read_file` and `read_range` must collect all `data` responses for the request before interpreting the final result. Each `data` chunk carries a `"data"` field with a base64 string; these must be decoded and concatenated in order to reconstruct the file content.

**Edge cases**
- Large files will produce many chunks; the reader must aggregate all of them without dropping any.
- Empty files may produce zero data chunks; the result should correctly be an empty vector.
- The base64 decoding must handle chunk boundaries—each chunk is decoded independently, not the concatenation of base64 strings.

**Non-goals**
- Other streaming methods (if any) don’t need to be fixed immediately, but the same pattern should be applied if they exist.
- The channel API will need a variant that retains streaming data alongside the final result, because the existing `request_blocking` discards them.

fix(remote): Fix file reading and add integration tests


**Title:** Viewport fails to scroll immediately when pressing Enter at bottom of file with trailing newline

**Background**
`apply_scroll_limit` enforces the invariant that the last line of the buffer is never above the viewport bottom. This works for files where the line count is ≤ `viewport_height`, but fails when the buffer ends with a newline because an extra implicit empty line is created at the end of the file. The editor allows the cursor to move onto this invisible “phantom” line, yet the scroll limit calculation ignores it, treating the file as having exactly `line_count` lines.

**Reproduction**
1. Open a file whose content contains exactly `(viewport_height)` lines and a trailing newline (e.g., 22 lines with a newline at the end in a 24-row terminal where the editable area is 22 lines).
2. Move the cursor to the last visible line.
3. Press Enter.
4. **Expected:** The viewport scrolls up by one line immediately. The cursor stays on the new (now visible) phantom line at the bottom of the viewport.
5. **Actual:** The viewport does **not** scroll. The new empty line is off-screen; only after typing a character does the viewport adjust.

**Root cause**
In `apply_scroll_limit`, the comparison `line_count <= viewport_height` early-returns with `top_byte = 0` when line count equals the viewport height, even though a trailing newline adds an extra line that must be visible. After the early return, the cursor’s new position causes no scroll because the scroll limit incorrectly thinks all lines already fit.

**Required outcome**
- When the buffer ends with a newline, the “phantom” empty line must be counted in the scroll limit calculations.
- Pressing Enter at the bottom of a file that has exactly `viewport_height` lines and a trailing newline must **immediately** scroll the viewport up by one line, keeping the new empty line visible and maintaining the bottom-line-pinned invariant.
- Upward scrolling must remain unaffected (i.e., `top_byte` should only be clamped to prevent overshooting the bottom, not forced unconditionally).

**Acceptance criteria**
- In the reproduction scenario, after pressing Enter at the bottom, `top_byte` increases and the cursor stays at the bottom row of the viewport.
- The invariant that the last line of the buffer (including the phantom line) never sits above the bottom of the viewport is preserved in all cases.
- No regressions in existing scroll behavior for files without trailing newlines or with line counts far exceeding the viewport height.

Fix Enter key scrolling when phantom empty line is present


## Title
Display LSP diagnostics via overlay underlines

## Context
We have overlays (Phase 3.5) that support colored underlines, backgrounds, and priority layering. The LSP client already receives and stores `PublishDiagnostics` notifications per-URI. We need to close the loop: when the LSP server publishes diagnostics for a file, the editor should visually underline the affected ranges so the user sees errors and warnings inline in the buffer.

## Desired behavior

1. **Severity-to-style mapping**
   - Error → red wavy underline (highest priority, so it renders above other decorations)
   - Warning → yellow wavy underline
   - Information → blue wavy underline
   - Hint → gray dotted underline (lowest priority)

2. **Rendering layering** (in `render_content`)
   - Base text style → syntax highlighting → overlays → selection
   - Overlays are applied in priority order: lower-priority overlays are applied first, then higher-priority ones override, so errors always paint on top of hints when ranges overlap.
   - Selection highlighting remains the final top layer (overrides everything).

3. **API surface on Editor**
   - A method to clear all existing diagnostic overlays (identified by an `"lsp-diag-"` ID prefix) and re-populate them from the LSP client’s stored diagnostics for the active buffer.
   - The method must be callable when a `PublishDiagnostics` notification arrives.
   - Must handle buffers with no file path gracefully (no-op).

4. **Coordinate conversion**
   - LSP positions are 0-indexed `(line, character)`, where character counts UTF-16 code units per the LSP spec. For now, treat `character` as a byte offset within the line (acknowledging that proper UTF-16 → UTF-8 conversion is a deferred TODO).
   - Invalid positions (e.g., line past end of buffer) must not panic; the diagnostic is simply skipped.

5. **Integration with the event system**
   - Adding and removing diagnostic overlays must go through the event log so undo/redo correctly restores or clears diagnostic decorations.

## Non-obvious edge cases
- Overlapping diagnostics (e.g., an error and a warning on the same span) must render with the higher-priority face winning.
- Multi-line diagnostic ranges (spanning `start_line` to `end_line`) must correctly compute the byte range across line boundaries.
- Clearing diagnostics before re-populating must only remove overlays tagged with the diagnostic prefix; any user- or plugin-added overlays must be preserved.

Implement LSP diagnostics display via overlay system


**Bug report: Popup text selection copies wrong text when lines wrap**

**Summary**
When a popup is narrower than its content and text wraps across multiple visual lines, selecting text via mouse and copying with `Ctrl+C` copies the wrong text. The copied text often comes from a different logical line than the one that appears visually selected.

**Steps to reproduce**
1. Open any text popup (hover documentation, custom `Popup::text`, etc.) with a `width` that forces at least one long line to wrap onto multiple visual rows.
2. Use the mouse to select a word that appears on a later visual row (for example, the second or third wrapped line).
3. Press `Ctrl+C` to copy the selection.
4. Paste the clipboard content elsewhere.

**Actual behavior**
The clipboard contains text from a different logical line — often the line before or after the one the user actually selected. The visual highlight correctly covers the intended word, but the copied content does not match.

**Expected behavior**
The copied text must exactly match the text that is visually selected, regardless of whether the popup content has wrapped lines. Any selection that spans multiple visual rows (because of wrapping) should also copy the correct, contiguous text as it appears on screen.

**Root cause (high-level)**
Mouse-selection coordinates are computed in *wrapped-line space* (visual row indices), but the text extraction for copying (`get_selected_text` / `get_text_lines`) uses the original *unwrapped* logical lines. When wrapping occurs, the visual row index no longer corresponds to the logical line index, causing the wrong content to be copied.

**Acceptance criteria**
- In a popup with fixed width that triggers line wrapping, selecting any word on any visual line and copying (`Ctrl+C`) yields exactly that word, not text from a different logical line.
- The fix must apply to both plain-text popups (`PopupContent::Text`) and markdown popups (`PopupContent::Markdown`).
- The wrapping logic used for selection must be identical to the one used for rendering (i.e., identical `wrap_text_lines` / `wrap_styled_lines` behavior) so that visual line indices always match.
- The behavior must be correct even when the popup also has a scrollbar (the presence/absence of a scrollbar may affect the inner width used for wrapping).
- No regression for popups wider than their content (no wrapping): selection should continue to work exactly as before.

Fix popup text selection copying wrong text when lines wrap


**Request: Cancel stale LSP requests on user action + show LSP progress on cursor**

When the user triggers LSP completion (Ctrl‑Space) or go‑to‑definition (Ctrl‑Click / equivalent), a request is sent asynchronously. Currently, if the user moves the cursor or edits the buffer before the response arrives, the stale results still pop up, often in the wrong location.

Desired behavior:

1. **Cancel pending LSP requests on any user action except the LSP-request actions themselves.**
   - Exempted actions: `LspCompletion`, `LspGotoDefinition`, and `None` (the no‑op).
   - For every other action (cursor movement, text insertion/deletion, opening dialogs, buffer switches, etc.), both types of pending LSP requests (completion and go‑to‑definition) must be immediately canceled.
   - When a request is canceled, clear the LSP‑status indicator (the “LSP: completion...”, “LSP: goto‑definition...” message in the status area). The user should never see a stale status after they’ve moved on.

2. **Visual cursor feedback while waiting for LSP.**
   - While any LSP request is pending, the character under the cursor should be visually replaced with `⋯` (U+22EF, MIDLINE HORIZONTAL ELLIPSIS) **only in the active split pane**.
   - This animation must be purely graphical – it does not modify the buffer content. It disappears as soon as the request completes or is canceled.
   - The replacement must not interfere with selection highlighting; the styled span should use the same background/foreground as the normal cursor would.

The cancellation logic must ensure that a late‑arriving response for a canceled request is ignored (existing request‑ID matching should already handle that, but the cancellation must clear the pending request ID so that the response is dropped when it arrives later). No new commands or keybindings are needed – this is purely a responsiveness and UX improvement.

Cancel LSP requests on user action and add cursor animation


**devcontainer: single panel slot + surface lifecycle command output**

Two issues from real-world use.

### 1. Lifecycle command output is invisible

When the user triggers a lifecycle command (e.g. `postCreateCommand` that fails), the plugin captures stdout/stderr but only displays a short status line like `postCreateCommand failed (exit 1)`. The actual error message is discarded. The user has no way to see what went wrong.

**Required behavior:** After any lifecycle command completes, open the captured stdout/stderr in the same panel slot used by the "Show Info/Logs" commands (see issue 2). Show a header line with the command name, the command that ran, then stdout (if any) then stderr (if any), or `(no output)` when both are empty. Always show the output even on success so the user can review what ran.

### 2. Every "Show *" command opens a new horizontal split

`Show Container Info`, `Show Container Logs`, `Show Container Build Logs`, `Show Forwarded Ports` each open a fresh horizontal split every time they are invoked. After a few invocations the layout is a column of tiny stacked panes.

**Required behavior:** Introduce a single shared panel slot for all devcontainer-owned panels. The first time any of these commands is used, open the panel in the currently focused split (do **not** create a new split). Subsequent invocations of any "Show *" or lifecycle-output panel must replace the content of that same slot without spawning additional splits.

Edge cases and safety constraints:

- If the user closes the panel slot manually (e.g. with `q`), the next panel command must reuse whatever split is currently focused — not create a new one and not reuse a now-dead split.
- Switching between different panel types (Info → Logs → Ports → Lifecycle output) must cycle within the same slot. The previous panel buffer should be closed when replaced so the tab/buffer list does not accumulate stale entries.
- The `Show Container Build Logs` panel is file-backed (log file under `.fresh-cache/devcontainer-logs/`). When a new build log is shown, close the previous build-log buffer if it was in the same slot, but never close user-owned buffers.
- The lifecycle object-form path (multiple sub-commands) should batch all sub-command outputs into a single panel update rather than flickering through N intermediate panels.

feat(devcontainer): single panel slot + capture lifecycle output


Fix adjacent tab arrow duplication in view pipeline

When `show_whitespace_tabs` is enabled, two adjacent tab characters render
with a doubled indicator arrow (`→→`) instead of one arrow per tab. The
expected output for tab size 4 is `→   →   ` (arrow, three spaces, arrow,
three spaces) — each tab contributes exactly one `→` at its starting
column.

The underlying cause is an off-by-one in the visual column mapping for
expanded tab spaces inside `ViewLineIterator` (view_pipeline.rs). The i‑th
space of a tab (i starts at 1 for the first space after the initial
indicator column) should map to visual column `col_before_tab + i`, but the
current formula uses `char_source_bytes.len()` after pushing the space,
adding an extra column for every space beyond the first. This skews the
`char_visual_cols` entries so that the second space of each tab lands one
column too far right, which in turn tricks the renderer into emitting an
extra arrow for the last expansion space of the previous tab when the next
tab immediately follows.

Fix the calculation so that the visual column for the i‑th expansion space
is `col_before_tab + i`. The arrow should only appear at the first
expansion space of each tab; all subsequent expansion spaces must be
ordinary spaces with no indicator.

fix(view): adjacent tab indicators no longer double up (#1997)


We need to polish the search/replace mini-bar that shows next to the prompt mini-buffer. Currently it is styled with the status-bar colors, which can be hard to read, and the keyboard shortcuts are dim. Additionally, the bar should appear consistently in both find and replace contexts, and we need to verify the toggles work correctly. A minor related cleanup: the status bar should be hidden when the suggestions popup is visible so it doesn’t clash with the mini-bar or prompt.

**Desired behavior & constraints**

- **Search options bar appearance**
  The bar that displays `Case Sensitive` and `Whole Word` checkboxes plus their shortcuts (`Alt+C`, `Alt+W`) must be visible whenever the find prompt *or* the replace prompt is active.
  After the prompt is dismissed (Esc), the bar should disappear.

- **Visual styling**
  Background: use the theme’s `menu_dropdown_bg` (dark gray) instead of `status_bar_bg`.
  Text: use `menu_dropdown_fg` for the option labels and checkbox markers.
  Active (checked) options: highlight with `menu_highlight_fg` on the dropdown background.
  Keyboard shortcut hints (`Alt+C`, `Alt+W`): use a lighter gray (around RGB 140,140,140) so they are visible against the dark background, not `DarkGray`.

- **Toggle actions**
  - `Alt+C` must toggle the `Case Sensitive` checkbox between `[x]` and `[ ]`.
  - `Alt+W` must toggle the `Whole Word` checkbox between `[x]` and `[ ]`.
  The actual search behavior should respect these states when navigating matches or performing replace.

- **Status bar hiding during suggestions**
  When the command palette (or any popup that shows suggestions) is open, the status bar should be hidden so it does not overlap with the mini-bar or prompt. The canvas area used by the mini-bar/prompt should remain intact.

**Scope**
The implementation should touch the status-bar rendering (the search-options helper) and the logic that decides when to hide the status bar. Use the existing theme fields; do not introduce new theme keys. All existing search and replace workflows must continue to work.

Add e2e tests for search options bar and improve colors


**Title:** Crash when collapsing or deleting a large expanded folder while scrolled deep into its children

**Context**
File explorer (`src/view/ui/file_explorer.rs`) renders visible nodes from a `display_nodes` list using a manual scroll offset. A panic occurs when the tree shrinks (e.g., via collapse or deletion) while the scroll offset points beyond the new, smaller list.

**Steps to reproduce**
1. Open a workspace containing a directory with many children (e.g., 100 files in a single folder).
2. Focus the file explorer and expand that folder so all its children appear.
3. Scroll down into the deep children so that `scroll_offset` becomes large (e.g., >70).
4. Collapse the folder (e.g., via `Enter`) or delete it.

**Observed behaviour**
The editor panics with a slice index out-of-range error:
`"range start index X out of range for slice of length Y"`
This happens during rendering when the code slices `display_nodes[scroll_offset..visible_end]`.

**Expected behaviour**
The editor must not panic. After the tree mutation, scrolling must remain within the valid bounds of the new `display_nodes` list. The visible area should gracefully show whatever portion of the list remains, without crashing or hanging.

**Acceptance criteria**
- Collapsing a deeply-expanded directory while scrolled far down does not produce a panic, regardless of how large the pre-collapse tree was.
- Deleting a directory with many children while scrolled into those children does not produce a panic.
- The visible file-explorer content after the operation is a valid, non-empty slice of the current items; no out-of-bounds access occurs.
- The fix must be defensive: the invariant `scroll_offset <= display_nodes.len()` must hold before rendering the visible slice, for any possible tree state change (collapse, delete, filter, etc.).

fix: Prevent crash when file explorer scroll_offset exceeds tree size


**Title:** Settings search result clicks are ignored

**Problem**
In the settings modal, when a search query is active and a list of matching results is displayed, clicking any of those results with the mouse has no effect. The user must use the keyboard (arrow keys + Enter) to navigate to a result.

**Expected behavior**
Clicking a search result should produce exactly the same outcome as selecting it with the keyboard and pressing Enter:
- The settings view navigates to the corresponding setting (selecting it and scrolling it into view if necessary).
- The search bar is dismissed and normal settings navigation resumes.
- No other actions (e.g., toggle controls, open sub-dialogs, modify values) are triggered by the click.

**Additional constraints**
- The click must be position-based, relying on the same areas that are used to display the search result entries.
- Clicking outside the search result list (but still inside the settings modal) should not accidentally trigger a result selection.
- The behavior must work regardless of the target setting’s category, page position, or control type.

fix: clicking search result in settings navigates to setting


Support CRLF line endings with transparent handling

Fresh currently assumes all files use LF line endings. When opening a CRLF (Windows-style) file, CR characters are shown as control glyphs in the buffer, and saving converts everything to LF. We need proper line-ending detection and preservation.

**Behavior:**

- Detect the file's line ending format on open (LF, CRLF, or CR) by sampling the first 8KB and using majority voting
- Normalize all endings to LF internally so editing, cursor movement, and rendering work uniformly — no special cases in the editor core
- Never render CR characters as visible glyphs (`<0D>`, `^M`) in the main view. The editor should behave identically regardless of line ending format
- On save, convert modified regions back to the original line ending format
- Unmodified regions in large files (loaded lazily from disk) already contain the correct endings, so stream them directly without re-encoding
- When the user presses Enter to insert a new line, use the file's detected line ending format
- Changing line endings via any future UI toggle should mark the buffer as modified

**Edge cases:**

- Mixed-ending files: majority voting picks the dominant format. The editor still normalizes and renders transparently
- Empty files: default to LF
- Large files: line ending detection must not force a full read — reuse the same 8KB sample already read for binary detection
- Old Mac format (`\r` only): treat identically to CRLF for normalization and re-encoding

Implement CRLF line ending support with transparent handling


```
title: diff_nav n/p should jump through live-diff hunks, and live-diff must render added/modified/removed glyphs with old-content virtual lines

labels: [plugin, live-diff, diff-nav, ux]

body:

## Summary

Two related gaps exist between the `live_diff` and `diff_nav` plugins:

1.  **diff_nav ignores live-diff hunks.**
    The “next diff” / “previous diff” commands already visit hunks from `git_gutter` and from unsaved buffer diffs.
    When the `live_diff` plugin is active it publishes its hunks as `live_diff_hunks` view state, but `diff_nav` never reads that source.
    Users pressing n/p expect to land on *every* hunk visible on screen, including those rendered by live-diff.

2.  **live-diff rendering behaviour is not documented with acceptance criteria.**
    The plugin is expected to render a unified-diff overlay inside the live editable buffer.
    The observable behaviour that users see must follow these rules:

    - **Added line** (exists on disk but not in `HEAD`): gutter column shows `+`.
    - **Modified line** (exists in both but differs): gutter column shows `~`, and a *virtual line* is rendered immediately above the modified line containing the old `HEAD` text prefixed with `- `.
    - **Removed line** (exists in `HEAD` but not on disk): gutter column shows `-` (if this case is handled).
    - The diff overlay must **recompute** whenever the buffer content changes through an edit (`after_insert`), so the glyphs stay accurate while the user types—this is the same path that covers the agent-watching use-case when Fresh reloads a file from disk.

## Acceptance criteria (observable)

### A. diff_nav integration

1.  Open a git-tracked file that has live-diff hunks (i.e. the on-disk content differs from `HEAD`), with both `live_diff` and `diff_nav` plugins enabled.
2.  Trigger the **next-diff** action repeatedly.
    The cursor must visit every hunk that is visible, including those reported by `live_diff_hunks`, in addition to `git_gutter_hunks` and unsaved-diff ranges.
3.  The same holds for **previous-diff**.
4.  The UX must not change for users who *don't* have `live_diff` enabled: when `live_diff_hunks` view state is absent or empty, navigation behaviour is identical to before.

### B. live-diff rendering

1.  Open a git-tracked file where one line has been **added** relative to `HEAD`.
    The gutter next to that line must display a `+` glyph.
2.  Open a git-tracked file where one line has been **modified** relative to `HEAD`.
    - The gutter next to that line must display a `~` glyph.
    - Immediately above the modified line a virtual line must appear, containing the *old* content from `HEAD` prefixed with `- ` (so the user can read what changed).
3.  The virtual old-content line must *not* be editable—it is a rendered overlay, not part of the real buffer.
4.  While the buffer is open, type a new line anywhere.
    The diff overlay must recompute (triggered by the edit) and the glyphs must reflect the current state relative to `HEAD` (newly-added lines get `+`, etc.).

### Constraints / non-goals

- Tests must observe the screen output (`screen_to_string`) rather than inspect internal state structures.
  Assertions on gutter glyphs and virtual-line text belong in the e2e plugin test suite alongside the existing `gutter.rs` conventions.
- The `live_diff` plugin obtains the `HEAD` reference by running `git show HEAD:<path>`; behaviour for untracked files or files not in `HEAD` is out of scope for this request.
- Only the gutter glyphs and virtual-line rendering are being specified here; command-palette or menu integration for live-diff is a separate concern.
```

test(plugins): add live-diff e2e tests and wire diff_nav.ts