TUI: live right-column (Plan + Code cards) + network/sandbox/provider fixes - #228
TUI: live right-column (Plan + Code cards) + network/sandbox/provider fixes#228gnanam1990 wants to merge 9 commits into
Conversation
…blocking `rm -rf <subdir>` was classified "destructive" by the AST analyzer and hard-denied by the sandbox before any permission prompt — so deleting a directory the agent had created was impossible, forcing an ask_user workaround. The deny also fired before the unsafe-mode allow, so bypass mode couldn't run it either. Split destructive into two tiers: - "destructive_catastrophic" — the irrecoverable system-level forms the catastrophic regex already isolates (rm -rf / or $HOME/~/*, mkfs, dd to a raw device, fork bomb, chmod 777 on a system tree, chown -R). These stay a hard block, even in unsafe mode. - everything else flagged destructive (e.g. rm -rf <subdir>, shred <file>) is now a Prompt in ask mode — surfaced to the (clickable) permission popup so the user can approve — and an Allow in unsafe mode. risk.go tags the catastrophic set with the new category; engine.go branches deny / fall-through-to-allow / prompt accordingly. Adds tests pinning the split (scoped → prompt/allow, catastrophic → deny even in unsafe). Existing sandbox/agent/cli/tools tests stay green; gofmt/vet/build(host+linux+windows)/ staticcheck clean.
Long tasks looked stuck — the agent would say "I'll build…" then work silently. Dock a panel on the right of the chat (alt-screen) that shows the update_plan steps with status glyphs (○ pending, ◐ in-progress, ✓ done) and an animated header with the current activity — Planning / Building / Scanning / Running / Responding / Thinking — plus the spinner and elapsed time. The activity is derived from the running tool (or streaming state), so the panel is a live "it's not stuck" signal. Width is a single source of truth: m.chatAreaWidth() = chatWidth - the panel column when docked, else chatWidth, so every renderer AND mouse hit-test uses the same narrowed width and clicks never desync from what's drawn. The panel shows only when alt-screen + not setup + wide enough (>= 52-col chat) + a plan or run is live; otherwise chatAreaWidth == chatWidth and every path is byte- for-byte unchanged. composeWithPlanPanel docks the panel row-by-row in View(). Adds runStartedAt (for elapsed) and plan_panel.go; ~20 chatWidth(m.width) call-sites moved to m.chatAreaWidth(). Tests cover activation/width-narrowing, hidden cases (narrow/setup/inline), side-by-side compose, the activity label (incl. from a running tool), glyphs, and elapsed formatting. gofmt/vet/build (host+linux+windows)/full -race/staticcheck green.
…ream A one-off TLS handshake / dial timeout (or connection reset) reaching the provider killed the whole turn — the user saw a red "provider stream error: … TLS handshake timeout" and had to resend. These are connection-level blips where the request never reached the model, so they're safe to retry. After collecting a turn's stream, if it errored with NO output produced and the error looks transient (TLS handshake / i/o / dial timeout, connection reset/refused, unexpected EOF, transient DNS), re-send the SAME request after an exponential backoff (1s, 2s; up to 2 retries). A failure that already produced text/tool calls is NOT retried (would duplicate output); a cancellation, an HTTP status error, or a context-length error is left to the normal handling (the latter still triggers reactive compaction). The backoff respects context cancellation. internal/agent/stream_retry.go classifies the error and backs off; Options.OnNetworkRetry surfaces each retry, which the TUI shows as a "network issue reaching the provider — retrying (attempt N)…" row so the pause isn't silent. Tests: classification (transient vs auth/rate-limit/context), backoff, retry-to-success, no-retry-on-terminal, no-retry-after-partial-output. gofmt/vet/build(host+linux+windows)/full -race/staticcheck green.
…nned run The panel was a full-height "long box" that also showed a STALE plan for trivial follow-ups (a plain "hi" still displayed the previous task's plan), and long steps were cut mid-word. - It now FLOATS over the top-right corner (composeWithPlanPanel overlays only its own rows), so the transcript stays full width — no reserved column, no full-height box. chatAreaWidth is now just the full chat width. - It shows ONLY while a run is in flight AND that run actually called update_plan (currentRunHasPlan), so "hi" / a delete task never shows a stale plan, and it disappears the moment the run finishes. - Steps truncate with an ellipsis (cutRunesEllipsis) instead of a hard break, and the box is wider (40 cols). Tests rewritten for the gate + overlay (shows only during a planned run; hidden for trivial/narrow/setup/inline; floats top-right full width). Gate green.
…retries A network failure that survives every retry (e.g. a provider whose host is unreachable from the user's network) surfaced as a cryptic "TLS handshake timeout". Annotate it once retries are exhausted: it's a network/connectivity problem, not the model — check the connection/VPN or switch providers with /provider. Test covers the annotation after the retries fail.
The agent re-dumped the entire "Current Plan: 1. [completed] …" on every update_plan call, cluttering the transcript — and the plan is already shown live in the right-side plan panel (and on demand via /plan). Skip update_plan's tool call and result rows from the chat so the transcript stays focused on the actual work (reads, edits, answers). Test added.
Adds the bottom half of the IDE-style right column: a "Code" card stacked under the Plan/Progress card, showing the live unified diff of the file the active run is editing — green additions / red removals, the same palette as the inline diff cards. The card carries the path and a +adds/−dels tally and tracks the most recent edit in the run. While the Code card is showing a run's diff, the inline edit cards in the chat collapse to a one-line record (full diff is one click away, and returns inline once the run ends), so the change lives in the card instead of being duplicated in the middle of the conversation. This is live-region only — the detailed view (Ctrl+O) and inline scrollback keep the full diffs. The right column is gated to an active run that actually planned or edited (never a trivial "hi"), is content-sized, and reserves the bottom chat rows so a tall plan+diff column can never bury the conversation or the composer. - code_panel.go: currentEditDiff/diffPath/diffCounts, renderCodeCard, codeDiffLines/codeBand, codePanelActive - plan_panel.go: rightColumnBase gate, renderRightColumn stacks Plan+Code, composeWithPlanPanel overlays the column with a bottom reserve - rendering.go/render_cache.go: compactEdit option collapses edit cards in the live region, with a cache key + stability fix so it never serves stale - tests for the diff parsing, gating, stacking order, collapse, and reserve
…ng endpoint Removes two built-in openAI-compatible presets whose endpoints no longer resolve, so a fresh open-source setup only lists providers that actually work. Curated model lists and the registry mapping are pruned to match.
Zero automated PR reviewVerdict: No blockers found Blockers
Validation
ScopeHead: This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (9)
🚧 Files skipped from review as they are similar to previous changes (6)
WalkthroughAdds transient network retry logic with exponential backoff to the agent loop ( ChangesAgent Transient Network Retry
Sandbox Destructive Command Hardening
TUI Floating Plan/Code Panels and Layout Refactor
Provider Catalog Cleanup
Sequence Diagram(s)sequenceDiagram
rect rgba(255, 200, 100, 0.5)
Note over AgentLoop,Provider: Transient Network Retry Loop
end
participant AgentLoop
participant isTransientNetworkError
participant sleepWithContext
participant Provider
participant OnNetworkRetry
AgentLoop->>Provider: StreamCompletion (attempt 0)
Provider-->>AgentLoop: error (no text/tool calls/reasoning)
AgentLoop->>isTransientNetworkError: classify reason
isTransientNetworkError-->>AgentLoop: true (if transient signal match)
loop up to maxNetworkRetries
AgentLoop->>OnNetworkRetry: attempt N, redacted reason
AgentLoop->>sleepWithContext: exponential backoff (1s→2s→4s→8s cap)
AgentLoop->>Provider: StreamCompletion (retry N)
Provider-->>AgentLoop: success or error
end
AgentLoop->>AgentLoop: annotateUnreachableProvider if still transient/no-output
sequenceDiagram
rect rgba(100, 180, 255, 0.5)
Note over TUIView,renderRightColumn: Plan/Code Floating Panel Composition
end
participant TUIView as View()
participant composeWithPlanPanel
participant renderRightColumn
participant renderPlanPanel
participant renderCodeCard
participant rendering as renderRowMode
TUIView->>composeWithPlanPanel: chat content string
composeWithPlanPanel->>renderRightColumn: model state
renderRightColumn->>renderPlanPanel: currentPlanItems + activity label
renderPlanPanel-->>renderRightColumn: plan card lines
renderRightColumn->>renderCodeCard: currentEditDiff path+text
renderCodeCard-->>renderRightColumn: code card (styled diff)
renderRightColumn-->>composeWithPlanPanel: stacked column
composeWithPlanPanel-->>TUIView: chat with top-right overlay (reserved rows protected)
TUIView->>rendering: renderRowMode (live region, compactEdit=true)
rendering-->>TUIView: update_plan rows suppressed, diff cards collapsed
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
internal/sandbox/destructive_test.go (1)
45-53: ⚡ Quick winAdd a regression case for
rm -rf *to lock the scoped-vs-catastrophic boundary.Given this PR’s behavior split, add an assertion that
rm -rf *in ask mode prompts (and in unsafe mode allows), unless your policy intentionally treats it as catastrophic.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/sandbox/destructive_test.go` around lines 45 - 53, The test file does not have regression coverage for the `rm -rf *` command to verify the boundary between scoped-vs-catastrophic command handling. Add test assertions after the catastrophic commands list that verify `rm -rf *` behaves as a scoped command: it should prompt the user when run in ask mode and be allowed when run in unsafe mode. This ensures the command is not incorrectly treated as catastrophic like the other destructive commands in the catastrophic slice.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/agent/loop.go`:
- Around line 162-165: The retry condition in the for loop at the "No output"
retry guard is incomplete and allows retries even when reasoning or dropped
tool-call output has been emitted, which can duplicate surfaced output. Extend
the retry condition to also exclude collected.Reasoning and
collected.DroppedToolCalls (or equivalent field names) by adding checks that
these are empty/zero-valued, similar to how collected.Text and
collected.ToolCalls are checked. This same fix must be applied at both the
primary location (lines 162-165 in the for loop condition) and the sibling
location (lines 182-183, also applies to: 182-183) to ensure the "no output"
contract is properly enforced across all retry guards.
- Around line 150-178: The initial provider.StreamCompletion(ctx, request) call
that produces the stream variable is not protected by retry logic, so transient
network errors during that first call will return immediately without retrying.
Refactor the code to wrap the initial StreamCompletion call and the subsequent
retry loop into a unified retry mechanism that handles transient network
failures consistently for both the initial attempt and any retries, ensuring
that all transient network errors (those with no collected text or tool calls)
are subject to the maxNetworkRetries limit and backoff behavior.
In `@internal/sandbox/risk.go`:
- Around line 105-110: The current code in the `add("destructive_catastrophic",
RiskCritical)` assignment is reusing the broad `matchesDestructive` matcher,
which causes all destructive command matches (including scoped ones like `rm -rf
<subdir>`) to be treated as system-level catastrophic actions that get
hard-denied, rather than being promptable scoped-destructive actions. Create a
separate, stricter matcher function that only matches truly catastrophic
system-level operations (such as `rm -rf /`, `$HOME/~/*`, `mkfs`, `dd` to raw
device, fork bomb, `chmod 777` on system tree, and `chown -R`) and use this new
matcher exclusively for the `destructive_catastrophic` risk assignment, while
keeping the existing broader `matchesDestructive` matcher for the scoped
destructive detection that gets downgraded to prompts in the engine.
In `@internal/tui/plan_panel.go`:
- Around line 35-45: The rightColumnBase() function currently returns true
without verifying whether there is sufficient drawable height available for the
plan panel. Add a height-capacity check to this function to gate activation,
similar to the condition `height - planPanelReserveRows <= 0` that is used
downstream. This check should be added alongside the existing guards to ensure
that rightColumnBase() only returns true when the panel can actually be rendered
on short terminals. The same height-capacity gate should also be applied to any
related activation logic that currently lacks this check.
In `@internal/tui/rendering.go`:
- Around line 966-968: The condition for setting collapsedFooter in the
rendering logic around line 966-968 does not properly force collapsing of edit
tool cards in compact mode when they are short. Currently, the OR condition with
opts.compactEdit only partially addresses this. You need to modify the logic so
that when opts.compactEdit is enabled, edit tools (identified by
toolCardAlwaysExpands(name) returning true) always receive a collapsed footer
via collapsedToolFooter(row.detail), regardless of the body length. Also check
the similar logic around lines 996-1006 and apply the same fix there to ensure
consistent behavior across all locations where edit tools are rendered in
compact mode.
---
Nitpick comments:
In `@internal/sandbox/destructive_test.go`:
- Around line 45-53: The test file does not have regression coverage for the `rm
-rf *` command to verify the boundary between scoped-vs-catastrophic command
handling. Add test assertions after the catastrophic commands list that verify
`rm -rf *` behaves as a scoped command: it should prompt the user when run in
ask mode and be allowed when run in unsafe mode. This ensures the command is not
incorrectly treated as catastrophic like the other destructive commands in the
catastrophic slice.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 6e5b7d74-38c0-4f4b-9a14-0d31a7da812e
📒 Files selected for processing (27)
internal/agent/loop.gointernal/agent/stream_retry.gointernal/agent/stream_retry_test.gointernal/agent/types.gointernal/providercatalog/catalog.gointernal/providercatalog/catalog_test.gointernal/providermodelcatalog/catalog.gointernal/providermodelcatalog/remote.gointernal/providermodelcatalog/remote_test.gointernal/sandbox/destructive_test.gointernal/sandbox/engine.gointernal/sandbox/risk.gointernal/tui/code_panel.gointernal/tui/code_panel_test.gointernal/tui/command_views.gointernal/tui/composer.gointernal/tui/flush.gointernal/tui/model.gointernal/tui/model_test.gointernal/tui/onboarding.gointernal/tui/plan_panel.gointernal/tui/plan_panel_test.gointernal/tui/plan_suppress_test.gointernal/tui/render_cache.gointernal/tui/rendering.gointernal/tui/spec_mode.gointernal/tui/transcript_view.go
💤 Files with no reviewable changes (4)
- internal/providercatalog/catalog.go
- internal/providermodelcatalog/remote.go
- internal/providermodelcatalog/remote_test.go
- internal/providermodelcatalog/catalog.go
Vasanthdev2004
left a comment
There was a problem hiding this comment.
One P1 finding: the scoped-destructive allow path in unsafe mode can be bypassed with path-traversal targets (
m -rf ../../) because destructive_catastrophic is only tagged by matchesDestructive (regex-based), which does not recognize ../ as a catastrophic target. The network retry, provider catalog cleanup, and TUI plan/code panels look clean.
Vasanthdev2004
left a comment
There was a problem hiding this comment.
Requesting changes: one P1 sandbox-escape regression. The scoped-destructive allow path in unsafe mode can be bypassed with path-traversal targets (
m -rf ../../) because destructive_catastrophic is only tagged by matchesDestructive (regex-based), which does not recognize ../ as a catastrophic target. The network retry, provider catalog cleanup, and TUI plan/code panels look clean.
|
Parity check against the reference Droid (D), supplementing the P1 sandbox finding in the review above. Sandbox (destructive split): D's DroidSandboxManager does NOT split destructive into scoped vs catastrophic tiers. It has a single deny path for destructive commands with an allowAlways persistence layer. Zero's two-tier split (catastrophic = hard block, scoped = prompt/allow) is a Zero-specific enhancement over D. The P1 path-traversal escape (rm -rf ../../ misclassified as scoped) is the gap to fix. D avoids this class of issue because it denies ALL destructive commands uniformly. Network retry: D's retry lives at the LLM client layer (src/utils/retryPolicy.ts -> getRetryConfig) with error-type-specific delays: throttling (5s-30s with server retry-after), capacity/503 (500ms + provider rotation), timeout (3s base, 1.5x, 15s cap), and default jittered backoff. Zero's stream_retry.go retries at the agent-loop level (wrapping the provider stream). Key difference: D retries at the HTTP client level (before any streaming output is produced), so there is no risk of duplicating partial output. Zero retries only when no output was produced (confirmed correct in review), which avoids the same risk. However, D's per-error-type backoff is more sophisticated than a single backoff curve. Consider adding throttling-specific delays with Retry-After header support. Plan panel: D uses PinnedTodoDisplay (a left-bordered box showing todo items with scroll anchoring) rendered inline in the chat, not a docked right column. Zero's right-docked panel is a different UX choice. D's approach keeps the chat width stable; Zero's reserves columns from the chat area. Both are valid. Code panel: D does not have a live diff panel. It renders diffs inline in the chat via DiffRenderer/UnifiedDiffRenderer. Zero's floating Code card that collapses inline edits is a Zero innovation not present in D. |
Fix guide: path-traversal escape in the scoped-destructive splitThe Fix (two parts)1. Tag path-escape After the existing AST block where // In classifyWithScope, after the `if analysis.Destructive { add("destructive", RiskCritical) }` block:
// A recursive rm targeting a parent path (../, absolute path outside the
// workspace) is catastrophic — it escapes the workspace boundary.
if analysis.Destructive && commandIsRecursiveRmEscape(command, request.WorkspaceRoot) {
add("destructive_catastrophic", RiskCritical)
}Add a helper that parses the // commandIsRecursiveRmEscape reports whether command is a recursive rm whose
// target resolves above the workspace root (e.g. rm -rf ../../, rm -rf /etc).
// It re-uses the AST walk so glob/expansion doesn't blind-side the check.
func commandIsRecursiveRmEscape(command string, workspaceRoot string) bool {
parsed, err := syntax.Parse(strings.NewReader(command), "", syntax.PosixStrict)
if err != nil {
return false // unparseable — let the unparseable_command category handle it
}
for _, stmt := range parsed.Stmts {
call, ok := stmt.Call.(*syntax.Call)
if !ok || len(call.Args) == 0 {
continue
}
prog := wordText(call.Args[0])
if prog != "rm" {
continue
}
if !hasRecursiveForce(call.Args[1:]) {
continue
}
for _, arg := range call.Args[1:] {
text := wordText(arg)
if strings.HasPrefix(text, "-") || text == "--" {
continue
}
// Target is a relative parent path or an absolute path outside root.
cleaned := filepath.ToSlash(filepath.Clean(text))
if cleaned == ".." || strings.HasPrefix(cleaned, "../") {
return true
}
if filepath.IsAbs(cleaned) && workspaceRoot != "" {
if abs, err := filepath.Abs(workspaceRoot); err == nil {
if !strings.HasPrefix(cleaned, abs+"/") && cleaned != abs {
return true
}
}
}
}
}
return false
}
2. Add test cases to Append to the // Path-traversal targets are catastrophic — never allowed even in unsafe mode.
escapeCases := []string{
"rm -rf ../../",
"rm -rf ..",
"rm -rf ../../../etc",
"rm -rf /etc",
}
for _, command := range escapeCases {
d := engine.Evaluate(context.Background(), shellReq(command, PermissionUnsafe))
if d.Action != ActionDeny {
t.Fatalf("escape %q in unsafe = %#v, want ActionDeny", command, d)
}
if !HasRiskCategory(d.Risk, "destructive_catastrophic") {
t.Fatalf("escape %q categories = %v, want destructive_catastrophic", command, d.Risk.Categories)
}
}Why this approach
Verifygofmt -w internal/sandbox/risk.go internal/sandbox/destructive_test.go
go vet ./internal/sandbox/
go test ./internal/sandbox/ -run TestScopedDestructive -race -v
go test ./internal/sandbox/ -race
go build ./... |
|
Improvement notes for future work, supplementing the P1 sandbox finding in the review above. Sandbox (destructive split): A simpler sandbox model is to deny ALL destructive commands uniformly and let the user allow them via a persistent settings layer (allow-always list). That single-deny approach avoids the need for a scoped-vs-catastrophic tier and the path-traversal gap that comes with it. The two-tier split in this PR is more nuanced, but it introduces the misclassification risk that the P1 finding calls out. Network retry: A more sophisticated retry layer uses per-error-type backoff: throttling errors (5s-30s, respect server Retry-After header), provider capacity/503 errors (500ms + provider rotation), timeout errors (3s base, 1.5x growth, 15s cap), and a default jittered backoff for everything else. Right now stream_retry.go uses a single backoff curve. The good news is the loop-level retry correctly only fires when no output was produced, so partial-output duplication isn't a risk. Consider adding Retry-After support for throttling responses and shorter delays on capacity errors so provider rotation kicks in faster. Plan panel: An alternative UI is to keep the plan inline in the chat as a left-bordered box with scroll anchoring (using update_plan steps). That keeps the chat width stable — no column reservation, no chat-width narrowing. Right now the right-docked panel takes columns from the chat area, which is visible at narrow terminal widths. Both approaches are valid; the inline approach is simpler to maintain and degrades more gracefully on small terminals. Code panel: A live floating diff card that collapses inline edits is an interesting UX choice not commonly seen. Worth keeping, but verify it works well on long-running sessions where the diff card might grow large. |
…els) Agent loop (internal/agent): - Route INITIAL StreamCompletion transient failures through the network-retry flow too — a TLS/dial timeout on the call itself was returned immediately, skipping retries (only the collect phase was covered). Annotate after retries. - Exclude reasoning blocks and dropped tool-call signals from the "no output" retry/annotation guard via a shared streamProducedNoOutput helper, so a stream that surfaced reasoning is not re-sent (which would duplicate output). Sandbox risk/engine (internal/sandbox): - Split catastrophic detection from the broad destructive matcher. `rm -rf *` (and other workspace-local deletes) are now destructive-but-promptable instead of hard-denied; only system-level targets (/, $HOME, ~), mkfs/dd/fork-bomb/ chmod-system/chown-R stay catastrophic. - Close the path-traversal escape: a destructive command containing a `..` component (e.g. `rm -rf ../../`) is tagged destructive_catastrophic via both a regex target and an AST + traversal backstop, so it stays denied even in unsafe mode rather than escaping the workspace. TUI (internal/tui): - Gate rightColumnBase by drawable height (height > planPanelReserveRows) so activation matches composeWithPlanPanel, which overlays zero rows on a short terminal. - Force-collapse short edit diffs in compact-edit mode so a live Code card does not duplicate the diff inline. Tests cover each: initial-call retry + exhaustion, reasoning/dropped no-retry, the scoped/catastrophic/traversal split, the height gate, and the short-diff compact collapse.
Summary
Adds an IDE-style right column to the full-screen chat and bundles several independent fixes that were sitting on local branches. Fully gated (gofmt, vet, host+linux+windows build,
go test -race, staticcheck).Right-column panel (Plan + Code cards)
During an active run, a compact column floats over the top-right of the chat:
update_plansteps with a spinner, activity word (Planning / Building / Scanning…) and elapsed time. Shown only when the run actually produced a plan, so a trivial "hi" never shows a stale one.+adds/−delstally.While the Code card is showing a run's diff, inline edit cards in the chat collapse to a one-line record (full diff one click away, and back inline once the run ends) so the change lives in the card, not the middle of the chat. The column is content-sized and reserves the bottom rows, so it can never bury the conversation or the composer.
update_plan's tool call/result are kept out of the chat (they live in the panel and/plan).Network resilience
Sandbox
rm -rf <subdir>) now prompt for approval instead of being hard-blocked, while catastrophic ones stay blocked.Providers
Testing
gofmt -lclean;go vetcleango buildhost + linux/amd64 + windows/amd64go test ./internal/tui/ ./internal/agent/ ./internal/sandbox/ ./internal/providercatalog/ ./internal/providermodelcatalog/ -race— all passmainnits unchanged)Notes
main, so it is intentionally not included here.mainrefactored the mouse/selection layer beneath them, so they need a proper re-port (plus interactive testing) rather than a blind merge.Summary by CodeRabbit
Release Notes
New Features
Improvements
Chores