on b72f9aa help sign in
.github/cla-signatures.json−32
since splitsnapshots
unifiedsplit
@@ -2063,38 +2063,6 @@
2063 "created_at": "2026-06-27T14:15:50Z",
2063 "created_at": "2026-06-27T14:15:50Z",
2064 "repoId": 987670088,
2064 "repoId": 987670088,
2065 "pullRequestNo": 3133
2065 "pullRequestNo": 3133
2066− },
2067− {
2068− "name": "sablea",
2069− "id": 52388727,
2070− "comment_id": 4851979164,
2071− "created_at": "2026-07-01T08:13:03Z",
2072− "repoId": 987670088,
2073− "pullRequestNo": 3235
2074− },
2075− {
2076− "name": "Osamaali313",
2077− "id": 86572800,
2078− "comment_id": 4870561813,
2079− "created_at": "2026-07-02T21:19:15Z",
2080− "repoId": 987670088,
2081− "pullRequestNo": 3244
2082− },
2083− {
2084− "name": "lockp111",
2085− "id": 28383475,
2086− "comment_id": 4878248994,
2087− "created_at": "2026-07-03T17:12:26Z",
2088− "repoId": 987670088,
2089− "pullRequestNo": 3247
2090− },
2091− {
2092− "name": "TheRAWagent",
2093− "id": 77039476,
2094− "comment_id": 4879276761,
2095− "created_at": "2026-07-03T20:52:00Z",
2096− "repoId": 987670088,
2097− "pullRequestNo": 3251
2098 }
2066 }
2099 ]
2067 ]
2100}
2068}
.github/workflows/security.yml+5−5
@@ -30,11 +30,11 @@ jobs:
30 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
30 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
31 with:
31 with:
32 persist-credentials: false
32 persist-credentials: false
33− - uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
33+ - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
34 with:
34 with:
35 languages: ${{ matrix.language }}
35 languages: ${{ matrix.language }}
36− - uses: github/codeql-action/autobuild@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
36+ - uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
37− - uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
37+ - uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
38
38
39 grype:
39 grype:
40 runs-on: ubuntu-latest
40 runs-on: ubuntu-latest
@@ -52,7 +52,7 @@ jobs:
52 path: "."
52 path: "."
53 fail-build: true
53 fail-build: true
54 severity-cutoff: critical
54 severity-cutoff: critical
55− - uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
55+ - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
56 with:
56 with:
57 sarif_file: ${{ steps.scan.outputs.sarif }}
57 sarif_file: ${{ steps.scan.outputs.sarif }}
58
58
@@ -73,7 +73,7 @@ jobs:
73 - name: Run govulncheck
73 - name: Run govulncheck
74 run: |
74 run: |
75 govulncheck -C . -format sarif ./... > results.sarif
75 govulncheck -C . -format sarif ./... > results.sarif
76− - uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
76+ - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
77 with:
77 with:
78 sarif_file: results.sarif
78 sarif_file: results.sarif
79
79
.github/workflows/snapshot.yml+1−1
@@ -25,7 +25,7 @@ jobs:
25 - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
25 - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
26 with:
26 with:
27 go-version-file: go.mod
27 go-version-file: go.mod
28− - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
28+ - uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
29 with:
29 with:
30 version: "nightly"
30 version: "nightly"
31 distribution: goreleaser-pro
31 distribution: goreleaser-pro
.gitignore−1
@@ -44,7 +44,6 @@ Thumbs.db
44
44
45# Nix/direnv
45# Nix/direnv
46.direnv/
46.direnv/
47−/result
48
47
49**/.crush/**
48**/.crush/**
50
49
.golangci.yml+4−12
@@ -2,7 +2,6 @@ version: "2"
2linters:
2linters:
3 enable:
3 enable:
4 - bodyclose
4 - bodyclose
5− - errcheck
6 # - exhaustive
5 # - exhaustive
7 # - goconst
6 # - goconst
8 # - godot
7 # - godot
@@ -10,7 +9,6 @@ linters:
10 # - gomoddirectives
9 # - gomoddirectives
11 - goprintffuncname
10 - goprintffuncname
12 # - gosec
11 # - gosec
13− - ineffassign
14 - misspell
12 - misspell
15 # - nakedret
13 # - nakedret
16 # - nestif
14 # - nestif
@@ -25,23 +23,17 @@ linters:
25 - tparallel
23 - tparallel
26 # - unconvert
24 # - unconvert
27 # - unparam
25 # - unparam
28− - unused
29 - whitespace
26 - whitespace
30 # - wrapcheck
27 # - wrapcheck
28+ disable:
29+ - errcheck
30+ - ineffassign
31+ - unused
31 exclusions:
32 exclusions:
32 generated: lax
33 generated: lax
33 presets:
34 presets:
34 - common-false-positives
35 - common-false-positives
35− # Ignores the idiomatic-to-drop error family (*Close, *Flush,
36− # Fprint*, os.Remove/RemoveAll, os.Setenv, stdout/stderr writes) so
37− # errcheck only flags genuinely meaningful dropped errors.
38− - std-error-handling
39 rules:
36 rules:
40− # Dropped errors in tests (setup writes, httptest encoders) are
41− # low-signal — test failures surface them anyway.
42− - path: _test\.go
43− linters:
44− - errcheck
45 - text: '(slog|log)\.\w+'
37 - text: '(slog|log)\.\w+'
46 linters:
38 linters:
47 - noctx
39 - noctx
README.md−5
@@ -1,8 +1,5 @@
1# Crush
1# Crush
2
2
3−> [!NOTE]
4−> This is a personal fork of [charmbracelet/crush](https://github.com/charmbracelet/crush) with UI/UX tweaks and performance improvements.
5−
6<p align="center">
3<p align="center">
7 <a href="https://stuff.charm.sh/crush/charm-crush.png"><img width="450" alt="Charm Crush Logo" src="https://github.com/user-attachments/assets/cf8ca3ce-8b02-43f0-9d0f-5a331488da4b" /></a><br />
4 <a href="https://stuff.charm.sh/crush/charm-crush.png"><img width="450" alt="Charm Crush Logo" src="https://github.com/user-attachments/assets/cf8ca3ce-8b02-43f0-9d0f-5a331488da4b" /></a><br />
8 <a href="https://github.com/charmbracelet/crush/releases"><img src="https://img.shields.io/github/release/charmbracelet/crush" alt="Latest Release"></a>
5 <a href="https://github.com/charmbracelet/crush/releases"><img src="https://img.shields.io/github/release/charmbracelet/crush" alt="Latest Release"></a>
@@ -197,7 +194,6 @@ That said, you can also set environment variables for preferred providers.
197| `OPENROUTER_API_KEY` | OpenRouter |
194| `OPENROUTER_API_KEY` | OpenRouter |
198| `IONET_API_KEY` | io.net |
195| `IONET_API_KEY` | io.net |
199| `ALIBABA_SINGAPORE_API_KEY` | Alibaba (Singapore) |
196| `ALIBABA_SINGAPORE_API_KEY` | Alibaba (Singapore) |
200−| `ALIBABA_US_API_KEY` | Alibaba (United States) |
201| `GROQ_API_KEY` | Groq |
197| `GROQ_API_KEY` | Groq |
202| `AVIAN_API_KEY` | Avian |
198| `AVIAN_API_KEY` | Avian |
203| `OPENCODE_API_KEY` | OpenCode Zen & Go |
199| `OPENCODE_API_KEY` | OpenCode Zen & Go |
@@ -211,7 +207,6 @@ That said, you can also set environment variables for preferred providers.
211| `AZURE_OPENAI_API_ENDPOINT` | Azure OpenAI models |
207| `AZURE_OPENAI_API_ENDPOINT` | Azure OpenAI models |
212| `AZURE_OPENAI_API_KEY` | Azure OpenAI models (optional when using Entra ID) |
208| `AZURE_OPENAI_API_KEY` | Azure OpenAI models (optional when using Entra ID) |
213| `AZURE_OPENAI_API_VERSION` | Azure OpenAI models |
209| `AZURE_OPENAI_API_VERSION` | Azure OpenAI models |
214−| `MOONSHOT_API_KEY` | Moonshot |
215
210
216### Subscriptions
211### Subscriptions
217
212
flake.lock+4−3
@@ -20,15 +20,16 @@
20 },
20 },
21 "nixpkgs": {
21 "nixpkgs": {
22 "locked": {
22 "locked": {
23− "lastModified": 1782964654,
23+ "lastModified": 1775423009,
24− "narHash": "sha256-oqyac50OyRdni5ZK9H7ZuAJoR3LzgsOdUcbBlUsk9lg=",
24+ "narHash": "sha256-vPKLpjhIVWdDrfiUM8atW6YkIggCEKdSAlJPzzhkQlw=",
25 "owner": "NixOS",
25 "owner": "NixOS",
26 "repo": "nixpkgs",
26 "repo": "nixpkgs",
27− "rev": "3130a1cf8f7c794ea5a4760c3fa4c745d64ec269",
27+ "rev": "68d8aa3d661f0e6bd5862291b5bb263b2a6595c9",
28 "type": "github"
28 "type": "github"
29 },
29 },
30 "original": {
30 "original": {
31 "owner": "NixOS",
31 "owner": "NixOS",
32+ "ref": "nixos-unstable",
32 "repo": "nixpkgs",
33 "repo": "nixpkgs",
33 "type": "github"
34 "type": "github"
34 }
35 }
flake.nix+1−32
@@ -2,7 +2,7 @@
2 description = "Crush development environment";
2 description = "Crush development environment";
3
3
4 inputs = {
4 inputs = {
5− nixpkgs.url = "github:NixOS/nixpkgs";
5+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6 flake-utils.url = "github:numtide/flake-utils";
6 flake-utils.url = "github:numtide/flake-utils";
7 };
7 };
8
8
@@ -14,38 +14,7 @@
14 flake-utils.lib.eachDefaultSystem (
14 flake-utils.lib.eachDefaultSystem (
15 system: let
15 system: let
16 pkgs = nixpkgs.legacyPackages.${system};
16 pkgs = nixpkgs.legacyPackages.${system};
17− version = "devel";
18 in {
17 in {
19− packages.default = pkgs.buildGoModule {
20− pname = "crush";
21− inherit version;
22−
23− src = ./.;
24−
25− vendorHash = null;
26−
27− # Match the Go toolchain used in the dev shell.
28− go = pkgs.go_1_26;
29−
30− env.CGO_ENABLED = 0;
31−
32− ldflags = [
33− "-s"
34− "-w"
35− "-X github.com/charmbracelet/crush/internal/version.Version=${version}"
36− ];
37−
38− # The test suite requires network access and external tools.
39− doCheck = false;
40−
41− meta = {
42− description = "Terminal-based AI coding assistant";
43− homepage = "https://github.com/charmbracelet/crush";
44− license = pkgs.lib.licenses.mit;
45− mainProgram = "crush";
46− };
47− };
48−
49 devShells.default = pkgs.mkShell {
18 devShells.default = pkgs.mkShell {
50 buildInputs = with pkgs; [
19 buildInputs = with pkgs; [
51 # Go toolchain
20 # Go toolchain
go.mod+11−11
@@ -3,13 +3,13 @@ module github.com/charmbracelet/crush
3go 1.26.4
3go 1.26.4
4
4
5require (
5require (
6− charm.land/bubbles/v2 v2.1.1
6+ charm.land/bubbles/v2 v2.1.0
7− charm.land/bubbletea/v2 v2.0.8
7+ charm.land/bubbletea/v2 v2.0.7
8− charm.land/catwalk v0.49.0
8+ charm.land/catwalk v0.45.8
9 charm.land/fang/v2 v2.0.1
9 charm.land/fang/v2 v2.0.1
10− charm.land/fantasy v0.35.0
10+ charm.land/fantasy v0.34.0
11 charm.land/glamour/v2 v2.0.1
11 charm.land/glamour/v2 v2.0.1
12− charm.land/lipgloss/v2 v2.0.5
12+ charm.land/lipgloss/v2 v2.0.4
13 charm.land/log/v2 v2.0.0
13 charm.land/log/v2 v2.0.0
14 charm.land/x/vcr v0.1.1
14 charm.land/x/vcr v0.1.1
15 github.com/JohannesKaufmann/html-to-markdown v1.6.0
15 github.com/JohannesKaufmann/html-to-markdown v1.6.0
@@ -22,7 +22,7 @@ require (
22 github.com/charlievieth/fastwalk v1.0.14
22 github.com/charlievieth/fastwalk v1.0.14
23 github.com/charmbracelet/colorprofile v0.4.3
23 github.com/charmbracelet/colorprofile v0.4.3
24 github.com/charmbracelet/openai-go v0.0.0-20260617131321-5e4b9c18c4be
24 github.com/charmbracelet/openai-go v0.0.0-20260617131321-5e4b9c18c4be
25− github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7
25+ github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654
26 github.com/charmbracelet/x/ansi v0.11.7
26 github.com/charmbracelet/x/ansi v0.11.7
27 github.com/charmbracelet/x/editor v0.2.0
27 github.com/charmbracelet/x/editor v0.2.0
28 github.com/charmbracelet/x/etag v0.2.0
28 github.com/charmbracelet/x/etag v0.2.0
@@ -51,8 +51,8 @@ require (
51 github.com/ncruces/go-sqlite3 v0.35.1
51 github.com/ncruces/go-sqlite3 v0.35.1
52 github.com/nxadm/tail v1.4.11
52 github.com/nxadm/tail v1.4.11
53 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
53 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
54− github.com/posthog/posthog-go v1.17.5
54+ github.com/posthog/posthog-go v1.17.2
55− github.com/pressly/goose/v3 v3.27.2
55+ github.com/pressly/goose/v3 v3.27.1
56 github.com/qjebbs/go-jsons v1.0.0-alpha.6
56 github.com/qjebbs/go-jsons v1.0.0-alpha.6
57 github.com/rivo/uniseg v0.4.7
57 github.com/rivo/uniseg v0.4.7
58 github.com/sahilm/fuzzy v0.1.3
58 github.com/sahilm/fuzzy v0.1.3
@@ -103,7 +103,7 @@ require (
103 github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect
103 github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect
104 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect
104 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect
105 github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect
105 github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect
106− github.com/aws/smithy-go v1.27.3 // indirect
106+ github.com/aws/smithy-go v1.27.2 // indirect
107 github.com/aymerick/douceur v0.2.0 // indirect
107 github.com/aymerick/douceur v0.2.0 // indirect
108 github.com/bahlo/generic-list-go v0.2.0 // indirect
108 github.com/bahlo/generic-list-go v0.2.0 // indirect
109 github.com/buger/jsonparser v1.1.2 // indirect
109 github.com/buger/jsonparser v1.1.2 // indirect
@@ -153,7 +153,7 @@ require (
153 github.com/klauspost/cpuid/v2 v2.3.0 // indirect
153 github.com/klauspost/cpuid/v2 v2.3.0 // indirect
154 github.com/klauspost/pgzip v1.2.6 // indirect
154 github.com/klauspost/pgzip v1.2.6 // indirect
155 github.com/mailru/easyjson v0.7.7 // indirect
155 github.com/mailru/easyjson v0.7.7 // indirect
156− github.com/mattn/go-runewidth v0.0.24 // indirect
156+ github.com/mattn/go-runewidth v0.0.23 // indirect
157 github.com/mfridman/interpolate v0.0.2 // indirect
157 github.com/mfridman/interpolate v0.0.2 // indirect
158 github.com/microcosm-cc/bluemonday v1.0.27 // indirect
158 github.com/microcosm-cc/bluemonday v1.0.27 // indirect
159 github.com/mitchellh/mapstructure v1.5.0 // indirect
159 github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -207,7 +207,7 @@ require (
207 golang.org/x/time v0.15.0 // indirect
207 golang.org/x/time v0.15.0 // indirect
208 golang.org/x/tools v0.45.0 // indirect
208 golang.org/x/tools v0.45.0 // indirect
209 google.golang.org/api v0.286.0 // indirect
209 google.golang.org/api v0.286.0 // indirect
210− google.golang.org/genai v1.62.0 // indirect
210+ google.golang.org/genai v1.61.0 // indirect
211 google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d // indirect
211 google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d // indirect
212 google.golang.org/grpc v1.81.1 // indirect
212 google.golang.org/grpc v1.81.1 // indirect
213 google.golang.org/protobuf v1.36.11 // indirect
213 google.golang.org/protobuf v1.36.11 // indirect
go.sum+22−22
@@ -1,17 +1,17 @@
1−charm.land/bubbles/v2 v2.1.1 h1:7r55WzBxpo/R3z98hGmY7KKPd3ET6vsf0Fb9sDHOV60=
1+charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g=
2−charm.land/bubbles/v2 v2.1.1/go.mod h1:GE6M31gaWZVXzGw73OeuTTgy4lX+OtkH0E5ymnNsHxo=
2+charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY=
3−charm.land/bubbletea/v2 v2.0.8 h1:SxTJMhCAI3lbPmy4SgX5LWZ24AdINr4I6UEqzZvYJuY=
3+charm.land/bubbletea/v2 v2.0.7 h1:7qw2tTAVar7m7klOPBYfTB0mniv/RuexsYwMRNxSeL0=
4−charm.land/bubbletea/v2 v2.0.8/go.mod h1:2SkdgoTXluXJHOUwAoRlRXF/28vklb1rFl6GcgV1/ss=
4+charm.land/bubbletea/v2 v2.0.7/go.mod h1:DGW2q8gvzHnOpMpZTORs0aySVHCox5C+2Svk0fci1qs=
5−charm.land/catwalk v0.49.0 h1:pXcHh9H76VvwGqPYMZhJe5vRqMLpzGsv0anJArKw70g=
5+charm.land/catwalk v0.45.8 h1:fnd86JrpIDcuH8+QrTl5ZDChrwbHNMjM0GAMr2jFW5g=
6−charm.land/catwalk v0.49.0/go.mod h1:bw6/oiChsa9Fkr8LgYn84KrbbJY2z1+m7Jmlmmv3B2A=
6+charm.land/catwalk v0.45.8/go.mod h1:bw6/oiChsa9Fkr8LgYn84KrbbJY2z1+m7Jmlmmv3B2A=
7charm.land/fang/v2 v2.0.1 h1:zQCM8JQJ1JnQX/66B5jlCYBUxL2as5JXQZ2KJ6EL0mY=
7charm.land/fang/v2 v2.0.1 h1:zQCM8JQJ1JnQX/66B5jlCYBUxL2as5JXQZ2KJ6EL0mY=
8charm.land/fang/v2 v2.0.1/go.mod h1:S1GmkpcvK+OB5w9caywUnJcsMew45Ot8FXqoz8ALrII=
8charm.land/fang/v2 v2.0.1/go.mod h1:S1GmkpcvK+OB5w9caywUnJcsMew45Ot8FXqoz8ALrII=
9−charm.land/fantasy v0.35.0 h1:iFES7cnt3/o20T01+y2PDkHj5dHEWRFQH0vSXYzV8xo=
9+charm.land/fantasy v0.34.0 h1:InhMjeJ0u7tuJCFWSWV1hvojpDIIUMD5ur1ds333Qcw=
10−charm.land/fantasy v0.35.0/go.mod h1:rUqUhunz+c6k66L0hHd51CrXB3SSzPNKkwBrzIe6XnY=
10+charm.land/fantasy v0.34.0/go.mod h1:DAZjJ5w98fOJkkDWKpG2jKqDg58Kvri/5I0ZdD31Hjo=
11charm.land/glamour/v2 v2.0.1 h1:xl+r00A4aJWU0z8fgwKd9fQQ4rsphqGUzuEiXZP5n+c=
11charm.land/glamour/v2 v2.0.1 h1:xl+r00A4aJWU0z8fgwKd9fQQ4rsphqGUzuEiXZP5n+c=
12charm.land/glamour/v2 v2.0.1/go.mod h1:jo9z8XqVKPeEFMVdvCRLGk++RyJ3CdUwgNr7EvXLw3k=
12charm.land/glamour/v2 v2.0.1/go.mod h1:jo9z8XqVKPeEFMVdvCRLGk++RyJ3CdUwgNr7EvXLw3k=
13−charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY=
13+charm.land/lipgloss/v2 v2.0.4 h1:lcPeVtcp23SNra7lHy8iYE4UC2aIipVQ47sbGyyxR5Q=
14−charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc=
14+charm.land/lipgloss/v2 v2.0.4/go.mod h1:0653x8epbZSzdDfO/XPS1a/uYPOBeSsCssOpJOqDzik=
15charm.land/log/v2 v2.0.0 h1:SY3Cey7ipx86/MBXQHwsguOT6X1exT94mmJRdzTNs+s=
15charm.land/log/v2 v2.0.0 h1:SY3Cey7ipx86/MBXQHwsguOT6X1exT94mmJRdzTNs+s=
16charm.land/log/v2 v2.0.0/go.mod h1:c3cZSRqm20qUVVAR1WmS/7ab8bgha3C6G7DjPcaVZz0=
16charm.land/log/v2 v2.0.0/go.mod h1:c3cZSRqm20qUVVAR1WmS/7ab8bgha3C6G7DjPcaVZz0=
17charm.land/x/vcr v0.1.1 h1:PXCFMUG0rPtyk35rhfzYCJEduOzWXCIbrXTFq4OF/9Q=
17charm.land/x/vcr v0.1.1 h1:PXCFMUG0rPtyk35rhfzYCJEduOzWXCIbrXTFq4OF/9Q=
@@ -86,8 +86,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMb
86github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4=
86github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4=
87github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI=
87github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI=
88github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc=
88github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc=
89−github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY=
89+github.com/aws/smithy-go v1.27.2 h1:y9NPmSE6am6LjEFPfqHqG/jJk7AauQvhCJONKh7kpzk=
90−github.com/aws/smithy-go v1.27.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
90+github.com/aws/smithy-go v1.27.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
91github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
91github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
92github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
92github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
93github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
93github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
@@ -108,8 +108,8 @@ github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex
108github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
108github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
109github.com/charmbracelet/openai-go v0.0.0-20260617131321-5e4b9c18c4be h1:pg+OWlIkk9HOe/8P5J95aKe2wGDzFUiiyFOUpwR30B4=
109github.com/charmbracelet/openai-go v0.0.0-20260617131321-5e4b9c18c4be h1:pg+OWlIkk9HOe/8P5J95aKe2wGDzFUiiyFOUpwR30B4=
110github.com/charmbracelet/openai-go v0.0.0-20260617131321-5e4b9c18c4be/go.mod h1:1DahUaExbUZx/jD+FNT2PKP4L9rLE5+ZBRuI8mZjd/E=
110github.com/charmbracelet/openai-go v0.0.0-20260617131321-5e4b9c18c4be/go.mod h1:1DahUaExbUZx/jD+FNT2PKP4L9rLE5+ZBRuI8mZjd/E=
111−github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 h1:3FmWoGNWK4STvqg0O0Aeav2T7rodWJAPeF0QpH+8gFw=
111+github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 h1:FpSYhY28ucg9ZRr+2wj67FAQ0Ey5yiK0072PmRDJNek=
112−github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7/go.mod h1:f/jRa757WUmaOZrbPspXymbg/GnbF+rwe4OLsG7aXYo=
112+github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654/go.mod h1:hFpumms29Smx3LStRfku8vcCTBe1Kq8aCXtHUJa3mjY=
113github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
113github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
114github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
114github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
115github.com/charmbracelet/x/editor v0.2.0 h1:7XLUKtaRaB8jN7bWU2p2UChiySyaAuIfYiIRg8gGWwk=
115github.com/charmbracelet/x/editor v0.2.0 h1:7XLUKtaRaB8jN7bWU2p2UChiySyaAuIfYiIRg8gGWwk=
@@ -290,8 +290,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
290github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
290github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
291github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
291github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
292github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
292github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
293−github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
293+github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
294−github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
294+github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
295github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
295github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
296github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
296github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
297github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
297github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
@@ -341,10 +341,10 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1
341github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
341github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
342github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
342github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
343github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
343github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
344−github.com/posthog/posthog-go v1.17.5 h1:mrLiAdyiQpl8Yeyg23iShyAztJMaUrYzsBjKeH52Aak=
344+github.com/posthog/posthog-go v1.17.2 h1:w0TaCAd+Z3WoEgVyR/nlcXlqNN2tpoBfIyxuGssDgCE=
345−github.com/posthog/posthog-go v1.17.5/go.mod h1://M430hNH3e8CDv4i8SJesb26816Mpa6GIZaiP4pNQU=
345+github.com/posthog/posthog-go v1.17.2/go.mod h1://M430hNH3e8CDv4i8SJesb26816Mpa6GIZaiP4pNQU=
346−github.com/pressly/goose/v3 v3.27.2 h1:FjKNzcmMdGrQlSIu5alMSmakQtJFBgtw+A0bb1p/LC8=
346+github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4=
347−github.com/pressly/goose/v3 v3.27.2/go.mod h1:qWW+/8dkVtJYjJrbIpwD5xxnEJTUKvxkQ9JKQp9LaIM=
347+github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM=
348github.com/qjebbs/go-jsons v1.0.0-alpha.6 h1:9pMjdrCT7rj9CDIM0FXf9Ilie8PEy9g1b4qMkCnedjQ=
348github.com/qjebbs/go-jsons v1.0.0-alpha.6 h1:9pMjdrCT7rj9CDIM0FXf9Ilie8PEy9g1b4qMkCnedjQ=
349github.com/qjebbs/go-jsons v1.0.0-alpha.6/go.mod h1:wNJrtinHyC3YSf6giEh4FJN8+yZV7nXBjvmfjhBIcw4=
349github.com/qjebbs/go-jsons v1.0.0-alpha.6/go.mod h1:wNJrtinHyC3YSf6giEh4FJN8+yZV7nXBjvmfjhBIcw4=
350github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
350github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -559,8 +559,8 @@ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
559gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
559gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
560google.golang.org/api v0.286.0 h1:TdTXMvzYKnWV1/lPbCdbXRqBrkDqjPto22H2xeZZ8LI=
560google.golang.org/api v0.286.0 h1:TdTXMvzYKnWV1/lPbCdbXRqBrkDqjPto22H2xeZZ8LI=
561google.golang.org/api v0.286.0/go.mod h1:NlOlUIr8MPoIhT9Bb/oUnRuHbJOLwxb6JSYJM8Yz+jQ=
561google.golang.org/api v0.286.0/go.mod h1:NlOlUIr8MPoIhT9Bb/oUnRuHbJOLwxb6JSYJM8Yz+jQ=
562−google.golang.org/genai v1.62.0 h1:PaBju84orf4Vbcc6OfHe4vxhxhjwulKTgOpEc3iIc00=
562+google.golang.org/genai v1.61.0 h1:wCyNGiaC9q5A59B80zuEtNBhq3ypEvICFkZYOfK7IO0=
563−google.golang.org/genai v1.62.0/go.mod h1:mDdPDFXo1Ats7f1WXVyZgWb/CkMzFWTWJruIMy7hGIU=
563+google.golang.org/genai v1.61.0/go.mod h1:mDdPDFXo1Ats7f1WXVyZgWb/CkMzFWTWJruIMy7hGIU=
564google.golang.org/genproto v0.0.0-20260622175928-b703f567277d h1:CP5omUq8AJTiWMrPKM1WRLJ7zZeXd9OPcQD3TbBNAyY=
564google.golang.org/genproto v0.0.0-20260622175928-b703f567277d h1:CP5omUq8AJTiWMrPKM1WRLJ7zZeXd9OPcQD3TbBNAyY=
565google.golang.org/genproto v0.0.0-20260622175928-b703f567277d/go.mod h1:DrwuGJgFSEVNpv3S5Q5VxhRTvdnjauw9GtvwVOEARfA=
565google.golang.org/genproto v0.0.0-20260622175928-b703f567277d/go.mod h1:DrwuGJgFSEVNpv3S5Q5VxhRTvdnjauw9GtvwVOEARfA=
566google.golang.org/genproto/googleapis/api v0.0.0-20260622175928-b703f567277d h1:xr2lwHI91bn3UiXcnyzRMQjp2LRiM8wEHzwUaE0YhTs=
566google.golang.org/genproto/googleapis/api v0.0.0-20260622175928-b703f567277d h1:xr2lwHI91bn3UiXcnyzRMQjp2LRiM8wEHzwUaE0YhTs=
internal/agent/agent.go+31−52
@@ -157,7 +157,6 @@ type sessionAgent struct {
157 systemPromptPrefix *csync.Value[string]
157 systemPromptPrefix *csync.Value[string]
158 systemPrompt *csync.Value[string]
158 systemPrompt *csync.Value[string]
159 tools *csync.Slice[fantasy.AgentTool]
159 tools *csync.Slice[fantasy.AgentTool]
160− toolsGen atomic.Uint64 // bumped by SetTools; lets PrepareStep skip re-copying unchanged tools
161
160
162 isSubAgent bool
161 isSubAgent bool
163 sessions session.Service
162 sessions session.Service
@@ -349,7 +348,7 @@ func (a *sessionAgent) sessionMu(sessionID string) *sync.Mutex {
349func (a *sessionAgent) enqueueCall(call SessionAgentCall) {
348func (a *sessionAgent) enqueueCall(call SessionAgentCall) {
350 existing, ok := a.messageQueue.Get(call.SessionID)
349 existing, ok := a.messageQueue.Get(call.SessionID)
351 if !ok {
350 if !ok {
352− existing = make([]SessionAgentCall, 0, 4)
351+ existing = []SessionAgentCall{}
353 }
352 }
354 queued := call
353 queued := call
355 if call.Accepted != nil {
354 if call.Accepted != nil {
@@ -643,13 +642,6 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (result *
643
642
644 // Copy mutable fields under lock to avoid races with SetTools/SetModels.
643 // Copy mutable fields under lock to avoid races with SetTools/SetModels.
645 agentTools := a.tools.Copy()
644 agentTools := a.tools.Copy()
646− // Per-step tool snapshot reused across PrepareStep calls; only
647− // re-copied when SetTools bumps toolsGen (see PrepareStep below).
648− var (
649− stepTools []fantasy.AgentTool
650− stepToolsGen uint64
651− stepToolsInit bool
652− )
653 largeModel := a.largeModel.Get()
645 largeModel := a.largeModel.Get()
654 systemPrompt := a.systemPrompt.Get()
646 systemPrompt := a.systemPrompt.Get()
655 promptPrefix := a.systemPromptPrefix.Get()
647 promptPrefix := a.systemPromptPrefix.Get()
@@ -814,14 +806,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (result *
814 }
806 }
815
807
816 // Use latest tools (updated by SetTools when MCP tools change).
808 // Use latest tools (updated by SetTools when MCP tools change).
817− // Reuse the previous step's copy unless SetTools has bumped the
818− // generation, so an unchanged tool set isn't re-copied every step.
819− if gen := a.toolsGen.Load(); !stepToolsInit || gen != stepToolsGen {
820− stepTools = a.tools.Copy()
809+ prepared.Tools = a.tools.Copy()
821− stepToolsGen = gen
822− stepToolsInit = true
823− }
824− prepared.Tools = stepTools
825
810
826 // Drain queued follow-up prompts for this step. Calls covered
811 // Drain queued follow-up prompts for this step. Calls covered
827 // by a cancel recorded while they sat in the queue are dropped:
812 // by a cancel recorded while they sat in the queue are dropped:
@@ -866,12 +851,9 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (result *
866 prepared.Messages = append([]fantasy.Message{fantasy.NewSystemMessage(promptPrefix)}, prepared.Messages...)
851 prepared.Messages = append([]fantasy.Message{fantasy.NewSystemMessage(promptPrefix)}, prepared.Messages...)
867 }
852 }
868
853
869− // Save a reference to the prepared messages for token
854+ sessionLock.Lock()
870− // estimation in OnStepFinish. No clone needed here — the
871− // messages are not modified between PrepareStep and
872− // OnStepFinish (which run sequentially), and the clone is
873− // only needed when the provider returns zero usage.
874− stepMessages = prepared.Messages
855+ stepMessages = cloneFantasyMessages(prepared.Messages)
856+ sessionLock.Unlock()
875
857
876 var assistantMsg message.Message
858 var assistantMsg message.Message
877 assistantMsg, err = a.messages.Create(callContext, call.SessionID, message.CreateMessageParams{
859 assistantMsg, err = a.messages.Create(callContext, call.SessionID, message.CreateMessageParams{
@@ -1080,9 +1062,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (result *
1080 currentAssistant.FinishThinking()
1062 currentAssistant.FinishThinking()
1081 toolCalls := currentAssistant.ToolCalls()
1063 toolCalls := currentAssistant.ToolCalls()
1082 // INFO: we use the cleanup context here because the genCtx has been cancelled.
1064 // INFO: we use the cleanup context here because the genCtx has been cancelled.
1083− // Only load messages from the current assistant message onward — tool
1084− // results for this turn's tool calls can only exist after it.
1085− msgs, createErr := a.messages.ListAfter(cleanupCtx, currentAssistant.SessionID, currentAssistant.ID)
1065+ msgs, createErr := a.messages.List(cleanupCtx, currentAssistant.SessionID)
1086 if createErr != nil {
1066 if createErr != nil {
1087 return nil, createErr
1067 return nil, createErr
1088 }
1068 }
@@ -1454,10 +1434,7 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string, opts fan
1454 return qErr
1434 return qErr
1455}
1435}
1456
1436
1457−// cacheControlOptions is computed once for the process lifetime: the env check
1458−// is constant and callers only read (never mutate) the returned map, so the
1459−// same shared value can be reused across the ~3 calls per step.
1460−var cacheControlOptions = sync.OnceValue(func() fantasy.ProviderOptions {
1437+func (a *sessionAgent) getCacheControlOptions() fantasy.ProviderOptions {
1461 if t, _ := strconv.ParseBool(os.Getenv("CRUSH_DISABLE_ANTHROPIC_CACHE")); t {
1438 if t, _ := strconv.ParseBool(os.Getenv("CRUSH_DISABLE_ANTHROPIC_CACHE")); t {
1462 return fantasy.ProviderOptions{}
1439 return fantasy.ProviderOptions{}
1463 }
1440 }
@@ -1472,10 +1449,6 @@ var cacheControlOptions = sync.OnceValue(func() fantasy.ProviderOptions {
1472 CacheControl: anthropic.CacheControl{Type: "ephemeral"},
1449 CacheControl: anthropic.CacheControl{Type: "ephemeral"},
1473 },
1450 },
1474 }
1451 }
1475−})
1476−
1477−func (a *sessionAgent) getCacheControlOptions() fantasy.ProviderOptions {
1478− return cacheControlOptions()
1479}
1452}
1480
1453
1481func (a *sessionAgent) createUserMessage(ctx context.Context, call SessionAgentCall) (message.Message, error) {
1454func (a *sessionAgent) createUserMessage(ctx context.Context, call SessionAgentCall) (message.Message, error) {
@@ -1496,7 +1469,7 @@ func (a *sessionAgent) createUserMessage(ctx context.Context, call SessionAgentC
1496}
1469}
1497
1470
1498func (a *sessionAgent) preparePrompt(msgs []message.Message, supportsImages bool, attachments ...message.Attachment) ([]fantasy.Message, []fantasy.FilePart) {
1471func (a *sessionAgent) preparePrompt(msgs []message.Message, supportsImages bool, attachments ...message.Attachment) ([]fantasy.Message, []fantasy.FilePart) {
1499− history := make([]fantasy.Message, 0, len(msgs)+1)
1472+ var history []fantasy.Message
1500 if !a.isSubAgent {
1473 if !a.isSubAgent {
1501 history = append(history, fantasy.NewUserMessage(
1474 history = append(history, fantasy.NewUserMessage(
1502 fmt.Sprintf(
1475 fmt.Sprintf(
@@ -1596,7 +1569,7 @@ func filterOrphanedToolResults(m message.Message, knownToolCallIDs map[string]st
1596 if len(aiMsgs) == 0 {
1569 if len(aiMsgs) == 0 {
1597 return fantasy.Message{}, false
1570 return fantasy.Message{}, false
1598 }
1571 }
1599− validParts := make([]fantasy.MessagePart, 0, len(aiMsgs[0].Content))
1572+ var validParts []fantasy.MessagePart
1600 for _, part := range aiMsgs[0].Content {
1573 for _, part := range aiMsgs[0].Content {
1601 tr, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
1574 tr, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
1602 if !ok {
1575 if !ok {
@@ -1628,7 +1601,7 @@ func filterOrphanedToolResults(m message.Message, knownToolCallIDs map[string]st
1628// conversation. Returns the message and true if any synthetic results were
1601// conversation. Returns the message and true if any synthetic results were
1629// produced.
1602// produced.
1630func syntheticToolResultsForOrphanedCalls(m message.Message, knownToolResultIDs map[string]struct{}) (fantasy.Message, bool) {
1603func syntheticToolResultsForOrphanedCalls(m message.Message, knownToolResultIDs map[string]struct{}) (fantasy.Message, bool) {
1631− syntheticParts := make([]fantasy.MessagePart, 0, len(m.ToolCalls()))
1604+ var syntheticParts []fantasy.MessagePart
1632 for _, tc := range m.ToolCalls() {
1605 for _, tc := range m.ToolCalls() {
1633 if _, hasResult := knownToolResultIDs[tc.ID]; hasResult {
1606 if _, hasResult := knownToolResultIDs[tc.ID]; hasResult {
1634 continue
1607 continue
@@ -1655,20 +1628,23 @@ func syntheticToolResultsForOrphanedCalls(m message.Message, knownToolResultIDs
1655}
1628}
1656
1629
1657func (a *sessionAgent) getSessionMessages(ctx context.Context, session session.Session) ([]message.Message, error) {
1630func (a *sessionAgent) getSessionMessages(ctx context.Context, session session.Session) ([]message.Message, error) {
1631+ msgs, err := a.messages.List(ctx, session.ID)
1632+ if err != nil {
1633+ return nil, fmt.Errorf("failed to list messages: %w", err)
1634+ }
1635+
1658 if session.SummaryMessageID != "" {
1636 if session.SummaryMessageID != "" {
1637+ summaryMsgIndex := -1
1638+ for i, msg := range msgs {
1659− msgs, err := a.messages.ListAfter(ctx, session.ID, session.SummaryMessageID)
1639+ if msg.ID == session.SummaryMessageID {
1660− if err != nil {
1640+ summaryMsgIndex = i
1661− return nil, fmt.Errorf("failed to list messages after summary: %w", err)
1641+ break
1642+ }
1662 }
1643 }
1663− if len(msgs) > 0 {
1644+ if summaryMsgIndex != -1 {
1645+ msgs = msgs[summaryMsgIndex:]
1664 msgs[0].Role = message.User
1646 msgs[0].Role = message.User
1665 }
1647 }
1666− return msgs, nil
1667− }
1668−
1669− msgs, err := a.messages.List(ctx, session.ID)
1670− if err != nil {
1671− return nil, fmt.Errorf("failed to list messages: %w", err)
1672 }
1648 }
1673 return msgs, nil
1649 return msgs, nil
1674}
1650}
@@ -1999,10 +1975,14 @@ func (a *sessionAgent) CancelAll() {
1999}
1975}
2000
1976
2001func (a *sessionAgent) IsBusy() bool {
1977func (a *sessionAgent) IsBusy() bool {
2002− // activeRequests only ever holds non-nil cancel funcs (Set with a
1978+ var busy bool
2003− // real func, removed via Del), so a length check is exact — and it
1979+ for cancelFunc := range a.activeRequests.Seq() {
2004− // avoids cloning the whole map, which Seq() does on every call.
1980+ if cancelFunc != nil {
2005− return a.activeRequests.Len() > 0
1981+ busy = true
1982+ break
1983+ }
1984+ }
1985+ return busy
2006}
1986}
2007
1987
2008func (a *sessionAgent) IsSessionBusy(sessionID string) bool {
1988func (a *sessionAgent) IsSessionBusy(sessionID string) bool {
@@ -2037,7 +2017,6 @@ func (a *sessionAgent) SetModels(large Model, small Model) {
2037
2017
2038func (a *sessionAgent) SetTools(tools []fantasy.AgentTool) {
2018func (a *sessionAgent) SetTools(tools []fantasy.AgentTool) {
2039 a.tools.SetSlice(tools)
2019 a.tools.SetSlice(tools)
2040− a.toolsGen.Add(1)
2041}
2020}
2042
2021
2043func (a *sessionAgent) SetSystemPrompt(systemPrompt string) {
2022func (a *sessionAgent) SetSystemPrompt(systemPrompt string) {
internal/agent/coordinator.go+11−183
@@ -15,8 +15,6 @@ import (
15 "path/filepath"
15 "path/filepath"
16 "slices"
16 "slices"
17 "strings"
17 "strings"
18− "sync"
19− "sync/atomic"
20
18
21 "charm.land/catwalk/pkg/catwalk"
19 "charm.land/catwalk/pkg/catwalk"
22 "charm.land/fantasy"
20 "charm.land/fantasy"
@@ -24,9 +22,7 @@ import (
24 "github.com/charmbracelet/crush/internal/agent/notify"
22 "github.com/charmbracelet/crush/internal/agent/notify"
25 "github.com/charmbracelet/crush/internal/agent/prompt"
23 "github.com/charmbracelet/crush/internal/agent/prompt"
26 "github.com/charmbracelet/crush/internal/agent/tools"
24 "github.com/charmbracelet/crush/internal/agent/tools"
27− "github.com/charmbracelet/crush/internal/agent/tools/mcp"
28 "github.com/charmbracelet/crush/internal/config"
25 "github.com/charmbracelet/crush/internal/config"
29− "github.com/charmbracelet/crush/internal/csync"
30 "github.com/charmbracelet/crush/internal/discover"
26 "github.com/charmbracelet/crush/internal/discover"
31 "github.com/charmbracelet/crush/internal/event"
27 "github.com/charmbracelet/crush/internal/event"
32 "github.com/charmbracelet/crush/internal/filetracker"
28 "github.com/charmbracelet/crush/internal/filetracker"
@@ -127,32 +123,6 @@ type coordinator struct {
127 activeSkills []*skills.Skill // Post-filter: active skills only.
123 activeSkills []*skills.Skill // Post-filter: active skills only.
128 skillTracker *skills.Tracker
124 skillTracker *skills.Tracker
129
125
130− // providerClients caches built provider clients keyed by their fully
131− // resolved configuration. UpdateModels rebuilds models every turn;
132− // without this, each turn discards the provider's HTTP client and its
133− // keep-alive connection pool, forcing a fresh TLS handshake. The key
134− // captures every input buildProvider depends on, so any config/model/
135− // credential change misses the cache and rebuilds.
136− providerClients *csync.Map[string, fantasy.Provider]
137−
138− // toolsCache avoids rebuilding the tool set on every UpdateModels call.
139− // The cache is invalidated when the config pointer changes (reload or
140− // mutation) or when mcpToolsGen is bumped (MCP server connect/disconnect
141− // or tools/list_changed notification).
142− toolsCacheMu sync.Mutex
143− toolsCache []fantasy.AgentTool
144− toolsCacheCfg *config.Config
145− toolsCacheMCP uint64
146− mcpToolsGen atomic.Uint64
147−
148− // providerOptsCache avoids repeating the 3× JSON marshal + merge +
149− // unmarshal in getProviderOptions on every turn when the model and
150− // config have not changed. Keyed by config pointer + model CatwalkCfg
151− // ID + provider name.
152− providerOptsCacheMu sync.Mutex
153− providerOptsCache fantasy.ProviderOptions
154− providerOptsCacheKey string
155−
156 readyWg errgroup.Group
126 readyWg errgroup.Group
157}
127}
158
128
@@ -196,8 +166,6 @@ func NewCoordinator(
196 allSkills: allSkills,
166 allSkills: allSkills,
197 activeSkills: activeSkills,
167 activeSkills: activeSkills,
198 skillTracker: skillTracker,
168 skillTracker: skillTracker,
199−
200− providerClients: csync.NewMap[string, fantasy.Provider](),
201 }
169 }
202
170
203 agentCfg, ok := cfg.Config().Agents[config.AgentCoder]
171 agentCfg, ok := cfg.Config().Agents[config.AgentCoder]
@@ -217,17 +185,6 @@ func NewCoordinator(
217 }
185 }
218 c.currentAgent = agent
186 c.currentAgent = agent
219 c.agents[config.AgentCoder] = agent
187 c.agents[config.AgentCoder] = agent
220−
221− // Subscribe to MCP tool-list changes so the tools cache is invalidated
222− // when MCP servers connect/disconnect or announce new tools.
223− go func() {
224− for ev := range mcp.SubscribeEvents(ctx) {
225− if ev.Payload.Type == mcp.EventToolsListChanged || ev.Payload.Type == mcp.EventStateChanged {
226− c.mcpToolsGen.Add(1)
227− }
228− }
229− }()
230−
231 return c, nil
188 return c, nil
232}
189}
233
190
@@ -278,7 +235,7 @@ func (c *coordinator) run(ctx context.Context, accept *AcceptedRun, sessionID st
278 return nil, errModelProviderNotConfigured
235 return nil, errModelProviderNotConfigured
279 }
236 }
280
237
281− mergedOptions, temp, topP, topK, freqPenalty, presPenalty := c.cachedMergeCallOptions(model, providerCfg)
238+ mergedOptions, temp, topP, topK, freqPenalty, presPenalty := mergeCallOptions(model, providerCfg)
282
239
283 if err := c.refreshTokenIfExpired(ctx, providerCfg); err != nil {
240 if err := c.refreshTokenIfExpired(ctx, providerCfg); err != nil {
284 // NOTE(@andreynering): We don't return here because the event handling to ask the user to reauthenticate
241 // NOTE(@andreynering): We don't return here because the event handling to ask the user to reauthenticate
@@ -450,7 +407,6 @@ func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.
450 options[openai.Name] = parsed
407 options[openai.Name] = parsed
451 }
408 }
452 }
409 }
453−
454 case anthropic.Name, bedrock.Name:
410 case anthropic.Name, bedrock.Name:
455 var (
411 var (
456 _, hasEffort = mergedOptions["effort"]
412 _, hasEffort = mergedOptions["effort"]
@@ -459,7 +415,7 @@ func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.
459 )
415 )
460
416
461 switch providerCfg.ID {
417 switch providerCfg.ID {
462− case string(catwalk.InferenceProviderAlibabaSingapore), string(catwalk.InferenceProviderAlibabaUS):
418+ case string(catwalk.InferenceProviderAlibabaSingapore):
463 switch {
419 switch {
464 case !hasEffort && shouldSetEffort:
420 case !hasEffort && shouldSetEffort:
465 extraBody["reasoning_effort"] = reasoningEffort
421 extraBody["reasoning_effort"] = reasoningEffort
@@ -498,7 +454,6 @@ func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.
498 if err == nil {
454 if err == nil {
499 options[openrouter.Name] = parsed
455 options[openrouter.Name] = parsed
500 }
456 }
501−
502 case vercel.Name:
457 case vercel.Name:
503 _, hasReasoning := mergedOptions["reasoning"]
458 _, hasReasoning := mergedOptions["reasoning"]
504 if !hasReasoning && shouldSetEffort {
459 if !hasReasoning && shouldSetEffort {
@@ -511,7 +466,6 @@ func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.
511 if err == nil {
466 if err == nil {
512 options[vercel.Name] = parsed
467 options[vercel.Name] = parsed
513 }
468 }
514−
515 case google.Name:
469 case google.Name:
516 _, hasReasoning := mergedOptions["thinking_config"]
470 _, hasReasoning := mergedOptions["thinking_config"]
517 if !hasReasoning {
471 if !hasReasoning {
@@ -531,7 +485,6 @@ func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.
531 if err == nil {
485 if err == nil {
532 options[google.Name] = parsed
486 options[google.Name] = parsed
533 }
487 }
534−
535 case openaicompat.Name, hyper.Name:
488 case openaicompat.Name, hyper.Name:
536 extraBody := make(map[string]any)
489 extraBody := make(map[string]any)
537
490
@@ -560,14 +513,16 @@ func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.
560 extraBody["reasoning"] = map[string]string{"effort": "none"}
513 extraBody["reasoning"] = map[string]string{"effort": "none"}
561 }
514 }
562 }
515 }
563−
564 case string(catwalk.InferenceProviderZAI), string(catwalk.InferenceProviderDeepSeek):
516 case string(catwalk.InferenceProviderZAI), string(catwalk.InferenceProviderDeepSeek):
565 if model.ModelCfg.Think || reasoningEffort != "" {
517 if model.ModelCfg.Think || reasoningEffort != "" {
566− extraBody["thinking"] = map[string]any{"type": "enabled"}
518+ extraBody["thinking"] = map[string]any{
519+ "type": "enabled",
520+ }
567 } else {
521 } else {
568− extraBody["thinking"] = map[string]any{"type": "disabled"}
522+ extraBody["thinking"] = map[string]any{
523+ "type": "disabled",
524+ }
569 }
525 }
570−
571 case string(catwalk.InferenceProviderFireworks):
526 case string(catwalk.InferenceProviderFireworks):
572 // NOTE: Fireworks break if we set both `reasoning_effort` and `thinking`.
527 // NOTE: Fireworks break if we set both `reasoning_effort` and `thinking`.
573 if reasoningEffort == "" {
528 if reasoningEffort == "" {
@@ -577,15 +532,9 @@ func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.
577 extraBody["thinking"] = map[string]any{"type": "disabled"}
532 extraBody["thinking"] = map[string]any{"type": "disabled"}
578 }
533 }
579 }
534 }
580−
581− case string(catwalk.InferenceProviderBaseten):
582− extraBody["chat_template_args"] = map[string]any{
583− "enable_thinking": model.ModelCfg.Think || reasoningEffort != "",
584− }
585−
586− case string(catwalk.InferenceProviderAlibabaSingapore), string(catwalk.InferenceProviderAlibabaUS):
535+ case string(catwalk.InferenceProviderAlibabaSingapore):
587 if model.CatwalkCfg.CanReason {
536 if model.CatwalkCfg.CanReason {
588− extraBody["enable_thinking"] = model.ModelCfg.Think || reasoningEffort != ""
537+ extraBody["enable_thinking"] = model.ModelCfg.Think
589 }
538 }
590 }
539 }
591
540
@@ -595,7 +544,6 @@ func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.
595 if err == nil {
544 if err == nil {
596 options[openaicompat.Name] = parsed
545 options[openaicompat.Name] = parsed
597 }
546 }
598−
599 default:
547 default:
600 // Known custom providers (litellm, ollama, omlx) are
548 // Known custom providers (litellm, ollama, omlx) are
601 // openai-compat under the hood.
549 // openai-compat under the hood.
@@ -620,39 +568,6 @@ func mergeCallOptions(model Model, cfg config.ProviderConfig) (fantasy.ProviderO
620 return modelOptions, temp, topP, topK, freqPenalty, presPenalty
568 return modelOptions, temp, topP, topK, freqPenalty, presPenalty
621}
569}
622
570
623−// cachedMergeCallOptions wraps mergeCallOptions with a memoization cache.
624−// The provider options JSON merge is expensive (3× marshal + merge +
625−// unmarshal) and the inputs rarely change between turns. The cache key
626−// combines the config pointer identity with the model's catwalk ID and
627−// provider name so any config reload or model switch misses the cache.
628−func (c *coordinator) cachedMergeCallOptions(model Model, cfg config.ProviderConfig) (fantasy.ProviderOptions, *float64, *float64, *int64, *float64, *float64) {
629− key := fmt.Sprintf("%p|%s|%s", c.cfg.Config(), model.CatwalkCfg.ID, model.ModelCfg.Provider)
630−
631− c.providerOptsCacheMu.Lock()
632− if c.providerOptsCacheKey == key {
633− opts := c.providerOptsCache
634− c.providerOptsCacheMu.Unlock()
635− // Recompute the scalar overrides (cheap) since they don't need
636− // the JSON merge — they're just cmp.Or calls.
637− temp := cmp.Or(model.ModelCfg.Temperature, model.CatwalkCfg.Options.Temperature)
638− topP := cmp.Or(model.ModelCfg.TopP, model.CatwalkCfg.Options.TopP)
639− topK := cmp.Or(model.ModelCfg.TopK, model.CatwalkCfg.Options.TopK)
640− freqPenalty := cmp.Or(model.ModelCfg.FrequencyPenalty, model.CatwalkCfg.Options.FrequencyPenalty)
641− presPenalty := cmp.Or(model.ModelCfg.PresencePenalty, model.CatwalkCfg.Options.PresencePenalty)
642− return opts, temp, topP, topK, freqPenalty, presPenalty
643− }
644− c.providerOptsCacheMu.Unlock()
645−
646− opts, temp, topP, topK, freqPenalty, presPenalty := mergeCallOptions(model, cfg)
647−
648− c.providerOptsCacheMu.Lock()
649− c.providerOptsCache = opts
650− c.providerOptsCacheKey = key
651− c.providerOptsCacheMu.Unlock()
652−
653− return opts, temp, topP, topK, freqPenalty, presPenalty
654−}
655−
656func (c *coordinator) buildAgent(ctx context.Context, prompt *prompt.Prompt, agent config.Agent, isSubAgent bool) (SessionAgent, error) {
571func (c *coordinator) buildAgent(ctx context.Context, prompt *prompt.Prompt, agent config.Agent, isSubAgent bool) (SessionAgent, error) {
657 large, small, err := c.buildAgentModels(ctx, isSubAgent)
572 large, small, err := c.buildAgentModels(ctx, isSubAgent)
658 if err != nil {
573 if err != nil {
@@ -1119,74 +1034,6 @@ func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model con
1119 apiKey, _ := c.cfg.Resolve(providerCfg.APIKey)
1034 apiKey, _ := c.cfg.Resolve(providerCfg.APIKey)
1120 baseURL, _ := c.cfg.Resolve(providerCfg.BaseURL)
1035 baseURL, _ := c.cfg.Resolve(providerCfg.BaseURL)
1121
1036
1122− // Reuse a previously built client when nothing that affects it has
1123− // changed, so the HTTP keep-alive pool survives across turns. The key
1124− // is derived from the resolved inputs above (plus the debug flag); the
1125− // per-provider adjustments inside the switch below are deterministic
1126− // functions of these, so identical inputs always yield an identical
1127− // client.
1128− cacheKey := c.providerCacheKey(providerCfg, model, apiKey, baseURL, headers, isSubAgent)
1129− if p, ok := c.providerClients.Get(cacheKey); ok {
1130− return p, nil
1131− }
1132− provider, err := c.buildProviderUncached(providerCfg, model, apiKey, baseURL, headers, isSubAgent)
1133− if err != nil {
1134− return nil, err
1135− }
1136− c.providerClients.Set(cacheKey, provider)
1137− return provider, nil
1138−}
1139−
1140−// providerCacheKey builds a stable key from every input buildProvider's
1141−// switch consumes, so any credential, endpoint, header, or option change
1142−// produces a distinct key (and thus a rebuilt client).
1143−func (c *coordinator) providerCacheKey(providerCfg config.ProviderConfig, model config.SelectedModel, apiKey, baseURL string, headers map[string]string, isSubAgent bool) string {
1144− var b strings.Builder
1145− b.WriteString(providerCfg.ID)
1146− b.WriteByte(0)
1147− b.WriteString(string(providerCfg.Type))
1148− b.WriteByte(0)
1149− // The opencode providers pick their client type from the model, so the
1150− // model must participate in the key.
1151− b.WriteString(model.Model)
1152− b.WriteByte(0)
1153− b.WriteString(apiKey)
1154− b.WriteByte(0)
1155− // Bedrock falls back to AWS_BEARER_TOKEN_BEDROCK when no apiKey is
1156− // configured, and that token is baked into the built client. Fold it
1157− // into the key so a rotated token rebuilds the client instead of
1158− // reusing a stale one.
1159− if apiKey == "" && providerCfg.Type == bedrock.Name {
1160− b.WriteString(os.Getenv("AWS_BEARER_TOKEN_BEDROCK"))
1161− }
1162− b.WriteByte(0)
1163− b.WriteString(baseURL)
1164− b.WriteByte(0)
1165− for _, k := range slices.Sorted(maps.Keys(headers)) {
1166− b.WriteString(k)
1167− b.WriteByte('=')
1168− b.WriteString(headers[k])
1169− b.WriteByte(1)
1170− }
1171− b.WriteByte(0)
1172− if extra, err := json.Marshal(providerCfg.ExtraBody); err == nil {
1173− b.Write(extra)
1174− }
1175− b.WriteByte(0)
1176− if extra, err := json.Marshal(providerCfg.ExtraParams); err == nil {
1177− b.Write(extra)
1178− }
1179− b.WriteByte(0)
1180− if isSubAgent {
1181− b.WriteByte('s')
1182− }
1183− if c.cfg.Config().Options.Debug {
1184− b.WriteByte('d')
1185− }
1186− return b.String()
1187−}
1188−
1189−func (c *coordinator) buildProviderUncached(providerCfg config.ProviderConfig, model config.SelectedModel, apiKey, baseURL string, headers map[string]string, isSubAgent bool) (fantasy.Provider, error) {
1190 switch providerCfg.ID {
1037 switch providerCfg.ID {
1191 case string(catwalk.InferenceProviderOpenCodeGo), string(catwalk.InferenceProviderOpenCodeZen):
1038 case string(catwalk.InferenceProviderOpenCodeGo), string(catwalk.InferenceProviderOpenCodeZen):
1192 if opencodeMessagesModels[model.Model] {
1039 if opencodeMessagesModels[model.Model] {
@@ -1286,34 +1133,15 @@ func (c *coordinator) UpdateModels(ctx context.Context) error {
1286 }
1133 }
1287 c.currentAgent.SetModels(large, small)
1134 c.currentAgent.SetModels(large, small)
1288
1135
1289− cfg := c.cfg.Config()
1290− agentCfg, ok := cfg.Agents[config.AgentCoder]
1136+ agentCfg, ok := c.cfg.Config().Agents[config.AgentCoder]
1291 if !ok {
1137 if !ok {
1292 return errCoderAgentNotConfigured
1138 return errCoderAgentNotConfigured
1293 }
1139 }
1294
1140
1295− // Fast path: reuse cached tools when neither the config nor the MCP
1296− // tool set has changed since the last build.
1297− mcpGen := c.mcpToolsGen.Load()
1298− c.toolsCacheMu.Lock()
1299− if c.toolsCache != nil && c.toolsCacheCfg == cfg && c.toolsCacheMCP == mcpGen {
1300− c.currentAgent.SetTools(c.toolsCache)
1301− c.toolsCacheMu.Unlock()
1302− return nil
1303− }
1304− c.toolsCacheMu.Unlock()
1305−
1306 tools, err := c.buildTools(ctx, agentCfg, false)
1141 tools, err := c.buildTools(ctx, agentCfg, false)
1307 if err != nil {
1142 if err != nil {
1308 return err
1143 return err
1309 }
1144 }
1310−
1311− c.toolsCacheMu.Lock()
1312− c.toolsCache = tools
1313− c.toolsCacheCfg = cfg
1314− c.toolsCacheMCP = mcpGen
1315− c.toolsCacheMu.Unlock()
1316−
1317 c.currentAgent.SetTools(tools)
1145 c.currentAgent.SetTools(tools)
1318 return nil
1146 return nil
1319}
1147}
internal/agent/hyper/provider.go+1−1
@@ -88,7 +88,7 @@ func FetchCredits(ctx context.Context, apiKey string) (int, error) {
88 if err != nil {
88 if err != nil {
89 return 0, fmt.Errorf("failed to make request: %w", err)
89 return 0, fmt.Errorf("failed to make request: %w", err)
90 }
90 }
91− defer resp.Body.Close()
91+ defer resp.Body.Close() //nolint:errcheck
92
92
93 if resp.StatusCode != http.StatusOK {
93 if resp.StatusCode != http.StatusOK {
94 return 0, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
94 return 0, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
internal/agent/hyper/provider.json+25−25
@@ -43,9 +43,9 @@
43 {
43 {
44 "id": "gemma-4-26b-a4b-it",
44 "id": "gemma-4-26b-a4b-it",
45 "name": "Gemma 4 26B A4B",
45 "name": "Gemma 4 26B A4B",
46− "cost_per_1m_in": 0.126,
46+ "cost_per_1m_in": 0.122,
47− "cost_per_1m_out": 0.42,
47+ "cost_per_1m_out": 0.428,
48− "cost_per_1m_in_cached": 0.063,
48+ "cost_per_1m_in_cached": 0.061,
49 "cost_per_1m_out_cached": 0,
49 "cost_per_1m_out_cached": 0,
50 "context_window": 256000,
50 "context_window": 256000,
51 "default_max_tokens": 25600,
51 "default_max_tokens": 25600,
@@ -55,9 +55,9 @@
55 {
55 {
56 "id": "glm-5",
56 "id": "glm-5",
57 "name": "GLM-5",
57 "name": "GLM-5",
58− "cost_per_1m_in": 0.86,
58+ "cost_per_1m_in": 0.89,
59− "cost_per_1m_out": 2.784,
59+ "cost_per_1m_out": 2.848,
60− "cost_per_1m_in_cached": 0.43,
60+ "cost_per_1m_in_cached": 0.445,
61 "cost_per_1m_out_cached": 0,
61 "cost_per_1m_out_cached": 0,
62 "context_window": 202752,
62 "context_window": 202752,
63 "default_max_tokens": 20275,
63 "default_max_tokens": 20275,
@@ -67,12 +67,12 @@
67 {
67 {
68 "id": "glm-5.1",
68 "id": "glm-5.1",
69 "name": "GLM-5.1",
69 "name": "GLM-5.1",
70− "cost_per_1m_in": 1.3,
70+ "cost_per_1m_in": 1.4,
71− "cost_per_1m_out": 4.3,
71+ "cost_per_1m_out": 4.4,
72 "cost_per_1m_in_cached": 0.26,
72 "cost_per_1m_in_cached": 0.26,
73 "cost_per_1m_out_cached": 0,
73 "cost_per_1m_out_cached": 0,
74− "context_window": 202000,
74+ "context_window": 202800,
75− "default_max_tokens": 202000,
75+ "default_max_tokens": 64000,
76 "can_reason": true,
76 "can_reason": true,
77 "reasoning_levels": [
77 "reasoning_levels": [
78 "low",
78 "low",
@@ -102,9 +102,9 @@
102 {
102 {
103 "id": "gpt-oss-120b",
103 "id": "gpt-oss-120b",
104 "name": "gpt-oss-120b",
104 "name": "gpt-oss-120b",
105− "cost_per_1m_in": 0.168,
105+ "cost_per_1m_in": 0.178,
106− "cost_per_1m_out": 0.562,
106+ "cost_per_1m_out": 0.68,
107− "cost_per_1m_in_cached": 0.084,
107+ "cost_per_1m_in_cached": 0.089,
108 "cost_per_1m_out_cached": 0,
108 "cost_per_1m_out_cached": 0,
109 "context_window": 131072,
109 "context_window": 131072,
110 "default_max_tokens": 13107,
110 "default_max_tokens": 13107,
@@ -120,9 +120,9 @@
120 {
120 {
121 "id": "kimi-k2.5",
121 "id": "kimi-k2.5",
122 "name": "Kimi K2.5",
122 "name": "Kimi K2.5",
123− "cost_per_1m_in": 0.51,
123+ "cost_per_1m_in": 0.518,
124− "cost_per_1m_out": 2.67,
124+ "cost_per_1m_out": 2.72,
125− "cost_per_1m_in_cached": 0.255,
125+ "cost_per_1m_in_cached": 0.259,
126 "cost_per_1m_out_cached": 0,
126 "cost_per_1m_out_cached": 0,
127 "context_window": 262144,
127 "context_window": 262144,
128 "default_max_tokens": 26214,
128 "default_max_tokens": 26214,
@@ -168,9 +168,9 @@
168 {
168 {
169 "id": "llama-3.3-70b-instruct",
169 "id": "llama-3.3-70b-instruct",
170 "name": "Llama 3.3 70B Instruct",
170 "name": "Llama 3.3 70B Instruct",
171− "cost_per_1m_in": 0.6066,
171+ "cost_per_1m_in": 0.5786,
172− "cost_per_1m_out": 1.0386,
172+ "cost_per_1m_out": 1.0606,
173− "cost_per_1m_in_cached": 0.3033,
173+ "cost_per_1m_in_cached": 0.2893,
174 "cost_per_1m_out_cached": 0,
174 "cost_per_1m_out_cached": 0,
175 "context_window": 128000,
175 "context_window": 128000,
176 "default_max_tokens": 12800,
176 "default_max_tokens": 12800,
@@ -270,9 +270,9 @@
270 {
270 {
271 "id": "qwen3-coder-480b-a35b-instruct-int4-mixed-ar",
271 "id": "qwen3-coder-480b-a35b-instruct-int4-mixed-ar",
272 "name": "Qwen3 Coder 480B A35B Instruct INT4 Mixed AR",
272 "name": "Qwen3 Coder 480B A35B Instruct INT4 Mixed AR",
273− "cost_per_1m_in": 0.557,
273+ "cost_per_1m_in": 0.445,
274− "cost_per_1m_out": 2.545,
274+ "cost_per_1m_out": 2.145,
275− "cost_per_1m_in_cached": 0.2785,
275+ "cost_per_1m_in_cached": 0.2225,
276 "cost_per_1m_out_cached": 0,
276 "cost_per_1m_out_cached": 0,
277 "context_window": 106000,
277 "context_window": 106000,
278 "default_max_tokens": 10600,
278 "default_max_tokens": 10600,
@@ -282,9 +282,9 @@
282 {
282 {
283 "id": "qwen3-next-80b-a3b-instruct",
283 "id": "qwen3-next-80b-a3b-instruct",
284 "name": "Qwen3 Next 80B A3B Instruct",
284 "name": "Qwen3 Next 80B A3B Instruct",
285− "cost_per_1m_in": 0.128,
285+ "cost_per_1m_in": 0.1275,
286− "cost_per_1m_out": 1.28,
286+ "cost_per_1m_out": 1.216,
287− "cost_per_1m_in_cached": 0.064,
287+ "cost_per_1m_in_cached": 0.06375,
288 "cost_per_1m_out_cached": 0,
288 "cost_per_1m_out_cached": 0,
289 "context_window": 262144,
289 "context_window": 262144,
290 "default_max_tokens": 26214,
290 "default_max_tokens": 26214,
internal/agent/loop_detection.go+17−17
@@ -1,7 +1,8 @@
1package agent
1package agent
2
2
3import (
3import (
4− "hash/fnv"
4+ "crypto/sha256"
5+ "encoding/hex"
5 "io"
6 "io"
6
7
7 "charm.land/fantasy"
8 "charm.land/fantasy"
@@ -21,11 +22,11 @@ func hasRepeatedToolCalls(steps []fantasy.StepResult, windowSize, maxRepeats int
21 }
22 }
22
23
23 window := steps[len(steps)-windowSize:]
24 window := steps[len(steps)-windowSize:]
24− counts := make(map[uint64]int, windowSize)
25+ counts := make(map[string]int)
25
26
26 for _, step := range window {
27 for _, step := range window {
27 sig := getToolInteractionSignature(step.Content)
28 sig := getToolInteractionSignature(step.Content)
28− if sig == 0 {
29+ if sig == "" {
29 continue
30 continue
30 }
31 }
31 counts[sig]++
32 counts[sig]++
@@ -39,35 +40,34 @@ func hasRepeatedToolCalls(steps []fantasy.StepResult, windowSize, maxRepeats int
39
40
40// getToolInteractionSignature computes a hash signature for the tool
41// getToolInteractionSignature computes a hash signature for the tool
41// interactions in a single step's content. It pairs tool calls with their
42// interactions in a single step's content. It pairs tool calls with their
42−// results (matched by ToolCallID) and returns an FNV-1a hash. FNV is ~10x
43+// results (matched by ToolCallID) and returns a hex-encoded SHA-256 hash.
43−// faster than SHA-256 and collision risk is negligible for loop detection
44−// over a 10-step window. If the step contains no tool calls, it returns 0.
44+// If the step contains no tool calls, it returns "".
45−func getToolInteractionSignature(content fantasy.ResponseContent) uint64 {
45+func getToolInteractionSignature(content fantasy.ResponseContent) string {
46 toolCalls := content.ToolCalls()
46 toolCalls := content.ToolCalls()
47 if len(toolCalls) == 0 {
47 if len(toolCalls) == 0 {
48− return 0
48+ return ""
49 }
49 }
50
50
51 // Index tool results by their ToolCallID for fast lookup.
51 // Index tool results by their ToolCallID for fast lookup.
52− resultsByID := make(map[string]fantasy.ToolResultContent, len(toolCalls))
52+ resultsByID := make(map[string]fantasy.ToolResultContent)
53 for _, tr := range content.ToolResults() {
53 for _, tr := range content.ToolResults() {
54 resultsByID[tr.ToolCallID] = tr
54 resultsByID[tr.ToolCallID] = tr
55 }
55 }
56
56
57− h := fnv.New64a()
57+ h := sha256.New()
58 for _, tc := range toolCalls {
58 for _, tc := range toolCalls {
59 output := ""
59 output := ""
60 if tr, ok := resultsByID[tc.ToolCallID]; ok {
60 if tr, ok := resultsByID[tc.ToolCallID]; ok {
61 output = toolResultOutputString(tr.Result)
61 output = toolResultOutputString(tr.Result)
62 }
62 }
63− _, _ = io.WriteString(h, tc.ToolName)
63+ io.WriteString(h, tc.ToolName)
64− h.Write([]byte{0})
64+ io.WriteString(h, "\x00")
65− _, _ = io.WriteString(h, tc.Input)
65+ io.WriteString(h, tc.Input)
66− h.Write([]byte{0})
66+ io.WriteString(h, "\x00")
67− _, _ = io.WriteString(h, output)
67+ io.WriteString(h, output)
68− h.Write([]byte{0})
68+ io.WriteString(h, "\x00")
69 }
69 }
70− return h.Sum64()
70+ return hex.EncodeToString(h.Sum(nil))
71}
71}
72
72
73// toolResultOutputString converts a ToolResultOutputContent to a stable string
73// toolResultOutputString converts a ToolResultOutputContent to a stable string
internal/agent/loop_detection_test.go+8−8
@@ -145,18 +145,18 @@ func TestHasRepeatedToolCalls(t *testing.T) {
145func TestGetToolInteractionSignature(t *testing.T) {
145func TestGetToolInteractionSignature(t *testing.T) {
146 t.Run("empty content returns empty string", func(t *testing.T) {
146 t.Run("empty content returns empty string", func(t *testing.T) {
147 sig := getToolInteractionSignature(fantasy.ResponseContent{})
147 sig := getToolInteractionSignature(fantasy.ResponseContent{})
148− if sig != 0 {
148+ if sig != "" {
149− t.Errorf("expected zero signature, got %d", sig)
149+ t.Errorf("expected empty string, got %q", sig)
150 }
150 }
151 })
151 })
152
152
153− t.Run("text only content returns zero signature", func(t *testing.T) {
153+ t.Run("text only content returns empty string", func(t *testing.T) {
154 content := fantasy.ResponseContent{
154 content := fantasy.ResponseContent{
155 fantasy.TextContent{Text: "hello"},
155 fantasy.TextContent{Text: "hello"},
156 }
156 }
157 sig := getToolInteractionSignature(content)
157 sig := getToolInteractionSignature(content)
158− if sig != 0 {
158+ if sig != "" {
159− t.Errorf("expected zero signature, got %d", sig)
159+ t.Errorf("expected empty string, got %q", sig)
160 }
160 }
161 })
161 })
162
162
@@ -166,8 +166,8 @@ func TestGetToolInteractionSignature(t *testing.T) {
166 fantasy.ToolResultContent{ToolCallID: "1", ToolName: "read", Result: fantasy.ToolResultOutputContentText{Text: "content"}},
166 fantasy.ToolResultContent{ToolCallID: "1", ToolName: "read", Result: fantasy.ToolResultOutputContentText{Text: "content"}},
167 }
167 }
168 sig := getToolInteractionSignature(content)
168 sig := getToolInteractionSignature(content)
169− if sig == 0 {
169+ if sig == "" {
170− t.Error("expected non-zero signature")
170+ t.Error("expected non-empty signature")
171 }
171 }
172 })
172 })
173
173
@@ -183,7 +183,7 @@ func TestGetToolInteractionSignature(t *testing.T) {
183 sig1 := getToolInteractionSignature(content1)
183 sig1 := getToolInteractionSignature(content1)
184 sig2 := getToolInteractionSignature(content2)
184 sig2 := getToolInteractionSignature(content2)
185 if sig1 != sig2 {
185 if sig1 != sig2 {
186− t.Errorf("expected same signature for same interactions, got %d and %d", sig1, sig2)
186+ t.Errorf("expected same signature for same interactions, got %q and %q", sig1, sig2)
187 }
187 }
188 })
188 })
189
189
internal/agent/prompt/prompt.go+1−1
@@ -115,7 +115,7 @@ func processContextPath(p string, store *config.ConfigStore) []ContextFile {
115 return contexts
115 return contexts
116 }
116 }
117 if info.IsDir() {
117 if info.IsDir() {
118− _ = filepath.WalkDir(fullPath, func(path string, d os.DirEntry, err error) error {
118+ filepath.WalkDir(fullPath, func(path string, d os.DirEntry, err error) error {
119 if err != nil {
119 if err != nil {
120 return err
120 return err
121 }
121 }
internal/agent/tools/bash.go+16−4
@@ -206,7 +206,19 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
206 // Determine working directory
206 // Determine working directory
207 execWorkingDir := cmp.Or(params.WorkingDir, workingDir)
207 execWorkingDir := cmp.Or(params.WorkingDir, workingDir)
208
208
209− isSafeReadOnly := isSafeReadOnly(params.Command)
209+ isSafeReadOnly := false
210+ cmdLower := strings.ToLower(params.Command)
211+
212+ if !containsCommandChaining(params.Command) {
213+ for _, safe := range safeCommands {
214+ if strings.HasPrefix(cmdLower, safe) {
215+ if len(cmdLower) == len(safe) || cmdLower[len(safe)] == ' ' || cmdLower[len(safe)] == '-' {
216+ isSafeReadOnly = true
217+ break
218+ }
219+ }
220+ }
221+ }
210
222
211 sessionID := GetSessionFromContext(ctx)
223 sessionID := GetSessionFromContext(ctx)
212 if sessionID == "" {
224 if sessionID == "" {
@@ -250,7 +262,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
250
262
251 if done {
263 if done {
252 // Command failed or completed very quickly
264 // Command failed or completed very quickly
253− _ = bgManager.Remove(bgShell.ID)
265+ bgManager.Remove(bgShell.ID)
254
266
255 interrupted := shell.IsInterrupt(execErr)
267 interrupted := shell.IsInterrupt(execErr)
256 exitCode := shell.ExitCode(execErr)
268 exitCode := shell.ExitCode(execErr)
@@ -325,7 +337,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
325 case <-ctx.Done():
337 case <-ctx.Done():
326 // Incoming context was cancelled before we moved to background
338 // Incoming context was cancelled before we moved to background
327 // Kill the shell and return error
339 // Kill the shell and return error
328− _ = bgManager.Kill(bgShell.ID)
340+ bgManager.Kill(bgShell.ID)
329 return fantasy.ToolResponse{}, ctx.Err()
341 return fantasy.ToolResponse{}, ctx.Err()
330 }
342 }
331 }
343 }
@@ -334,7 +346,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
334 // Command completed within threshold - return synchronously
346 // Command completed within threshold - return synchronously
335 // Remove from background manager since we're returning directly
347 // Remove from background manager since we're returning directly
336 // Don't call Kill() as it cancels the context and corrupts the exit code
348 // Don't call Kill() as it cancels the context and corrupts the exit code
337− _ = bgManager.Remove(bgShell.ID)
349+ bgManager.Remove(bgShell.ID)
338
350
339 interrupted := shell.IsInterrupt(execErr)
351 interrupted := shell.IsInterrupt(execErr)
340 exitCode := shell.ExitCode(execErr)
352 exitCode := shell.ExitCode(execErr)
internal/agent/tools/download.go+3−3
@@ -67,7 +67,7 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
67 downloadDescription(),
67 downloadDescription(),
68 func(ctx context.Context, params DownloadParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
68 func(ctx context.Context, params DownloadParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
69 if params.URL == "" {
69 if params.URL == "" {
70− return fantasy.NewTextErrorResponse("url parameter is required"), nil
70+ return fantasy.NewTextErrorResponse("URL parameter is required"), nil
71 }
71 }
72
72
73 if params.FilePath == "" {
73 if params.FilePath == "" {
@@ -75,7 +75,7 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
75 }
75 }
76
76
77 if !strings.HasPrefix(params.URL, "http://") && !strings.HasPrefix(params.URL, "https://") {
77 if !strings.HasPrefix(params.URL, "http://") && !strings.HasPrefix(params.URL, "https://") {
78− return fantasy.NewTextErrorResponse("url must start with http:// or https://"), nil
78+ return fantasy.NewTextErrorResponse("URL must start with http:// or https://"), nil
79 }
79 }
80
80
81 filePath := filepathext.SmartJoin(workingDir, params.FilePath)
81 filePath := filepathext.SmartJoin(workingDir, params.FilePath)
@@ -131,7 +131,7 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
131 defer resp.Body.Close()
131 defer resp.Body.Close()
132
132
133 if resp.StatusCode != http.StatusOK {
133 if resp.StatusCode != http.StatusOK {
134− return fantasy.NewTextErrorResponse(fmt.Sprintf("request failed with status code: %d", resp.StatusCode)), nil
134+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
135 }
135 }
136
136
137 // Create parent directories if they don't exist
137 // Create parent directories if they don't exist
internal/agent/tools/edit.go+1−4
@@ -352,7 +352,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
352
352
353 sessionID := GetSessionFromContext(edit.ctx)
353 sessionID := GetSessionFromContext(edit.ctx)
354 if sessionID == "" {
354 if sessionID == "" {
355− return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for editing a file")
355+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for edit a file")
356 }
356 }
357
357
358 lastRead := edit.filetracker.LastReadTime(edit.ctx, sessionID, filePath)
358 lastRead := edit.filetracker.LastReadTime(edit.ctx, sessionID, filePath)
@@ -381,9 +381,6 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
381
381
382 if replaceAll {
382 if replaceAll {
383 newContent = strings.ReplaceAll(oldContent, oldString, newString)
383 newContent = strings.ReplaceAll(oldContent, oldString, newString)
384− if newContent == oldContent {
385− return oldStringNotFoundErr, nil
386− }
387 } else {
384 } else {
388 index := strings.Index(oldContent, oldString)
385 index := strings.Index(oldContent, oldString)
389 if index == -1 {
386 if index == -1 {
internal/agent/tools/fetch.go+12−12
@@ -60,21 +60,21 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
60 fetchDescription(),
60 fetchDescription(),
61 func(ctx context.Context, params FetchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
61 func(ctx context.Context, params FetchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
62 if params.URL == "" {
62 if params.URL == "" {
63− return fantasy.NewTextErrorResponse("url parameter is required"), nil
63+ return fantasy.NewTextErrorResponse("URL parameter is required"), nil
64 }
64 }
65
65
66 format := strings.ToLower(params.Format)
66 format := strings.ToLower(params.Format)
67 if format != "text" && format != "markdown" && format != "html" {
67 if format != "text" && format != "markdown" && format != "html" {
68− return fantasy.NewTextErrorResponse("format must be one of: text, markdown, html"), nil
68+ return fantasy.NewTextErrorResponse("Format must be one of: text, markdown, html"), nil
69 }
69 }
70
70
71 if !strings.HasPrefix(params.URL, "http://") && !strings.HasPrefix(params.URL, "https://") {
71 if !strings.HasPrefix(params.URL, "http://") && !strings.HasPrefix(params.URL, "https://") {
72− return fantasy.NewTextErrorResponse("url must start with http:// or https://"), nil
72+ return fantasy.NewTextErrorResponse("URL must start with http:// or https://"), nil
73 }
73 }
74
74
75 sessionID := GetSessionFromContext(ctx)
75 sessionID := GetSessionFromContext(ctx)
76 if sessionID == "" {
76 if sessionID == "" {
77− return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for fetching content")
77+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
78 }
78 }
79
79
80 p, err := permissions.Request(
80 p, err := permissions.Request(
@@ -124,19 +124,19 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
124 defer resp.Body.Close()
124 defer resp.Body.Close()
125
125
126 if resp.StatusCode != http.StatusOK {
126 if resp.StatusCode != http.StatusOK {
127− return fantasy.NewTextErrorResponse(fmt.Sprintf("request failed with status code: %d", resp.StatusCode)), nil
127+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
128 }
128 }
129
129
130 body, err := io.ReadAll(io.LimitReader(resp.Body, MaxFetchSize))
130 body, err := io.ReadAll(io.LimitReader(resp.Body, MaxFetchSize))
131 if err != nil {
131 if err != nil {
132− return fantasy.NewTextErrorResponse("failed to read response body: " + err.Error()), nil
132+ return fantasy.NewTextErrorResponse("Failed to read response body: " + err.Error()), nil
133 }
133 }
134
134
135 content := string(body)
135 content := string(body)
136
136
137 validUTF8 := utf8.ValidString(content)
137 validUTF8 := utf8.ValidString(content)
138 if !validUTF8 {
138 if !validUTF8 {
139− return fantasy.NewTextErrorResponse("response content is not valid UTF-8"), nil
139+ return fantasy.NewTextErrorResponse("Response content is not valid UTF-8"), nil
140 }
140 }
141 contentType := resp.Header.Get("Content-Type")
141 contentType := resp.Header.Get("Content-Type")
142
142
@@ -145,7 +145,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
145 if strings.Contains(contentType, "text/html") {
145 if strings.Contains(contentType, "text/html") {
146 text, err := extractTextFromHTML(content)
146 text, err := extractTextFromHTML(content)
147 if err != nil {
147 if err != nil {
148− return fantasy.NewTextErrorResponse("failed to extract text from HTML: " + err.Error()), nil
148+ return fantasy.NewTextErrorResponse("Failed to extract text from HTML: " + err.Error()), nil
149 }
149 }
150 content = text
150 content = text
151 }
151 }
@@ -154,7 +154,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
154 if strings.Contains(contentType, "text/html") {
154 if strings.Contains(contentType, "text/html") {
155 markdown, err := convertHTMLToMarkdown(content)
155 markdown, err := convertHTMLToMarkdown(content)
156 if err != nil {
156 if err != nil {
157− return fantasy.NewTextErrorResponse("failed to convert HTML to Markdown: " + err.Error()), nil
157+ return fantasy.NewTextErrorResponse("Failed to convert HTML to Markdown: " + err.Error()), nil
158 }
158 }
159 content = markdown
159 content = markdown
160 }
160 }
@@ -166,14 +166,14 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
166 if strings.Contains(contentType, "text/html") {
166 if strings.Contains(contentType, "text/html") {
167 doc, err := goquery.NewDocumentFromReader(strings.NewReader(content))
167 doc, err := goquery.NewDocumentFromReader(strings.NewReader(content))
168 if err != nil {
168 if err != nil {
169− return fantasy.NewTextErrorResponse("failed to parse HTML: " + err.Error()), nil
169+ return fantasy.NewTextErrorResponse("Failed to parse HTML: " + err.Error()), nil
170 }
170 }
171 body, err := doc.Find("body").Html()
171 body, err := doc.Find("body").Html()
172 if err != nil {
172 if err != nil {
173− return fantasy.NewTextErrorResponse("failed to extract body from HTML: " + err.Error()), nil
173+ return fantasy.NewTextErrorResponse("Failed to extract body from HTML: " + err.Error()), nil
174 }
174 }
175 if body == "" {
175 if body == "" {
176− return fantasy.NewTextErrorResponse("no body content found in HTML"), nil
176+ return fantasy.NewTextErrorResponse("No body content found in HTML"), nil
177 }
177 }
178 content = "<html>\n<body>\n" + body + "\n</body>\n</html>"
178 content = "<html>\n<body>\n" + body + "\n</body>\n</html>"
179 }
179 }
internal/agent/tools/grep.go+4−11
@@ -235,8 +235,6 @@ func searchWithRipgrep(ctx context.Context, pattern, path, include string) ([]gr
235 }
235 }
236
236
237 var matches []grepMatch
237 var matches []grepMatch
238− // modTime is only used for sorting; stat each unique path once.
239− modTimes := make(map[string]time.Time)
240 for line := range bytes.SplitSeq(bytes.TrimSpace(output), []byte{'\n'}) {
238 for line := range bytes.SplitSeq(bytes.TrimSpace(output), []byte{'\n'}) {
241 if len(line) == 0 {
239 if len(line) == 0 {
242 continue
240 continue
@@ -249,18 +247,13 @@ func searchWithRipgrep(ctx context.Context, pattern, path, include string) ([]gr
249 continue
247 continue
250 }
248 }
251 for _, m := range match.Data.Submatches {
249 for _, m := range match.Data.Submatches {
252− modTime, ok := modTimes[match.Data.Path.Text]
253− if !ok {
254− fi, err := os.Stat(match.Data.Path.Text)
250+ fi, err := os.Stat(match.Data.Path.Text)
255− if err != nil {
251+ if err != nil {
256− continue // Skip files we can't access
252+ continue // Skip files we can't access
257− }
258− modTime = fi.ModTime()
259− modTimes[match.Data.Path.Text] = modTime
260 }
253 }
261 matches = append(matches, grepMatch{
254 matches = append(matches, grepMatch{
262 path: match.Data.Path.Text,
255 path: match.Data.Path.Text,
263− modTime: modTime,
256+ modTime: fi.ModTime(),
264 lineNum: match.Data.LineNumber,
257 lineNum: match.Data.LineNumber,
265 charNum: m.Start + 1, // ensure 1-based
258 charNum: m.Start + 1, // ensure 1-based
266 lineText: strings.TrimSpace(match.Data.Lines.Text),
259 lineText: strings.TrimSpace(match.Data.Lines.Text),
internal/agent/tools/ls.go+1−1
@@ -163,7 +163,7 @@ func ListDirectoryTree(searchPath string, params LSParams, lsConfig config.ToolL
163 output = fmt.Sprintf("There are more than %d files in the directory. Use a more specific path or use the Glob tool to find specific files. The first %[1]d files and directories are included below.\n", maxFiles)
163 output = fmt.Sprintf("There are more than %d files in the directory. Use a more specific path or use the Glob tool to find specific files. The first %[1]d files and directories are included below.\n", maxFiles)
164 }
164 }
165 if depth > 0 {
165 if depth > 0 {
166− output += fmt.Sprintf("The directory tree is shown up to a depth of %d. Use a higher depth and a specific path to see more levels.\n", cmp.Or(params.Depth, depth))
166+ output = fmt.Sprintf("The directory tree is shown up to a depth of %d. Use a higher depth and a specific path to see more levels.\n", cmp.Or(params.Depth, depth))
167 }
167 }
168 return output + "\n" + printTree(tree, searchPath), metadata, nil
168 return output + "\n" + printTree(tree, searchPath), metadata, nil
169}
169}
internal/agent/tools/mcp-tools.go+2−2
@@ -99,7 +99,7 @@ func (m *Tool) Info() fantasy.ToolInfo {
99func (m *Tool) Run(ctx context.Context, params fantasy.ToolCall) (fantasy.ToolResponse, error) {
99func (m *Tool) Run(ctx context.Context, params fantasy.ToolCall) (fantasy.ToolResponse, error) {
100 sessionID := GetSessionFromContext(ctx)
100 sessionID := GetSessionFromContext(ctx)
101 if sessionID == "" {
101 if sessionID == "" {
102− return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for executing MCP tool")
102+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
103 }
103 }
104
104
105 // Skip permission for whitelisted Docker MCP tools.
105 // Skip permission for whitelisted Docker MCP tools.
@@ -134,7 +134,7 @@ func (m *Tool) Run(ctx context.Context, params fantasy.ToolCall) (fantasy.ToolRe
134 case "image", "media":
134 case "image", "media":
135 if !GetSupportsImagesFromContext(ctx) {
135 if !GetSupportsImagesFromContext(ctx) {
136 modelName := GetModelNameFromContext(ctx)
136 modelName := GetModelNameFromContext(ctx)
137− return fantasy.NewTextErrorResponse(fmt.Sprintf("this model (%s) does not support image data.", modelName)), nil
137+ return fantasy.NewTextErrorResponse(fmt.Sprintf("This model (%s) does not support image data.", modelName)), nil
138 }
138 }
139
139
140 var response fantasy.ToolResponse
140 var response fantasy.ToolResponse
internal/agent/tools/mcp/init.go+3−22
@@ -55,19 +55,11 @@ func (s *ClientSession) Close() error {
55var (
55var (
56 sessions = csync.NewMap[string, *ClientSession]()
56 sessions = csync.NewMap[string, *ClientSession]()
57 states = csync.NewMap[string, ClientInfo]()
57 states = csync.NewMap[string, ClientInfo]()
58− // sessionLastOK records the last time a session was confirmed alive
59− // (successful ping or freshly created). Used to skip redundant pings
60− // within livenessTTL before each MCP tool call.
61− sessionLastOK = csync.NewMap[string, time.Time]()
62− broker = pubsub.NewBroker[Event]()
58+ broker = pubsub.NewBroker[Event]()
63− initOnce sync.Once
59+ initOnce sync.Once
64− initDone = make(chan struct{})
60+ initDone = make(chan struct{})
65)
61)
66
62
67−// livenessTTL is how long a session is trusted to be alive after a successful
68−// ping/create before getOrRenewClient pings it again.
69−const livenessTTL = 30 * time.Second
70−
71// State represents the current state of an MCP client
63// State represents the current state of an MCP client
72type State int
64type State int
73
65
@@ -268,7 +260,6 @@ func initClient(ctx context.Context, cfg *config.ConfigStore, name string, m con
268 toolCount := updateTools(cfg, name, tools)
260 toolCount := updateTools(cfg, name, tools)
269 updatePrompts(name, prompts)
261 updatePrompts(name, prompts)
270 sessions.Set(name, session)
262 sessions.Set(name, session)
271− sessionLastOK.Set(name, time.Now())
272
263
273 updateState(name, StateConnected, nil, session, Counts{
264 updateState(name, StateConnected, nil, session, Counts{
274 Tools: toolCount,
265 Tools: toolCount,
@@ -289,7 +280,6 @@ func DisableSingle(cfg *config.ConfigStore, name string) error {
289 slog.Warn("Error closing MCP session", "name", name, "error", err)
280 slog.Warn("Error closing MCP session", "name", name, "error", err)
290 }
281 }
291 sessions.Del(name)
282 sessions.Del(name)
292− sessionLastOK.Del(name)
293 }
283 }
294
284
295 // Clear tools and prompts for this MCP.
285 // Clear tools and prompts for this MCP.
@@ -309,11 +299,6 @@ func getOrRenewClient(ctx context.Context, cfg *config.ConfigStore, name string)
309 return nil, fmt.Errorf("mcp '%s' not available", name)
299 return nil, fmt.Errorf("mcp '%s' not available", name)
310 }
300 }
311
301
312− // Skip the ping if this session was confirmed alive very recently.
313− if last, ok := sessionLastOK.Get(name); ok && time.Since(last) < livenessTTL {
314− return sess, nil
315− }
316−
317 m := cfg.Config().MCP[name]
302 m := cfg.Config().MCP[name]
318 state, _ := states.Get(name)
303 state, _ := states.Get(name)
319
304
@@ -322,10 +307,8 @@ func getOrRenewClient(ctx context.Context, cfg *config.ConfigStore, name string)
322 defer cancel()
307 defer cancel()
323 err := sess.Ping(pingCtx, nil)
308 err := sess.Ping(pingCtx, nil)
324 if err == nil {
309 if err == nil {
325− sessionLastOK.Set(name, time.Now())
326 return sess, nil
310 return sess, nil
327 }
311 }
328− sessionLastOK.Del(name)
329 updateState(name, StateError, maybeTimeoutErr(err, timeout), nil, state.Counts)
312 updateState(name, StateError, maybeTimeoutErr(err, timeout), nil, state.Counts)
330
313
331 sess, err = createSession(ctx, name, m, cfg.Resolver())
314 sess, err = createSession(ctx, name, m, cfg.Resolver())
@@ -335,7 +318,6 @@ func getOrRenewClient(ctx context.Context, cfg *config.ConfigStore, name string)
335
318
336 updateState(name, StateConnected, nil, sess, state.Counts)
319 updateState(name, StateConnected, nil, sess, state.Counts)
337 sessions.Set(name, sess)
320 sessions.Set(name, sess)
338− sessionLastOK.Set(name, time.Now())
339 return sess, nil
321 return sess, nil
340}
322}
341
323
@@ -353,7 +335,6 @@ func updateState(name string, state State, err error, client *ClientSession, cou
353 info.ConnectedAt = time.Now()
335 info.ConnectedAt = time.Now()
354 case StateError:
336 case StateError:
355 sessions.Del(name)
337 sessions.Del(name)
356− sessionLastOK.Del(name)
357 }
338 }
358 states.Set(name, info)
339 states.Set(name, info)
359
340
internal/agent/tools/mcp/tools.go−14
@@ -206,20 +206,6 @@ func ensureRawBytes(data []byte) []byte {
206}
206}
207
207
208func normalizeBase64Input(data []byte) []byte {
208func normalizeBase64Input(data []byte) []byte {
209− // Fast path: if the data is all-ASCII and contains no whitespace, there is
210− // nothing for strings.Fields to strip, so avoid copying the (potentially
211− // multi-MB) payload. Bytes >= 0x80 could be part of a non-ASCII Unicode
212− // space that strings.Fields would strip, so fall back to the slow path.
213− fast := true
214− for _, b := range data {
215− if b >= 0x80 || b == ' ' || b == '\t' || b == '\n' || b == '\v' || b == '\f' || b == '\r' {
216− fast = false
217− break
218− }
219− }
220− if fast {
221− return data
222− }
223 normalized := strings.Join(strings.Fields(string(data)), "")
209 normalized := strings.Join(strings.Fields(string(data)), "")
224 return []byte(normalized)
210 return []byte(normalized)
225}
211}
internal/agent/tools/multiedit.go+1
@@ -445,6 +445,7 @@ func applyEditToContent(content string, edit MultiEditOperation) (string, error)
445 }
445 }
446
446
447 newContent = content[:index] + edit.NewString + content[index+len(edit.OldString):]
447 newContent = content[:index] + edit.NewString + content[index+len(edit.OldString):]
448+ replacementCount = 1
448 }
449 }
449
450
450 return newContent, nil
451 return newContent, nil
internal/agent/tools/references.go+8
@@ -24,6 +24,10 @@ type ReferencesParams struct {
24 Path string `json:"path,omitempty" description:"The directory to search in. Use a directory/file to narrow down the symbol search. Defaults to the current working directory."`
24 Path string `json:"path,omitempty" description:"The directory to search in. Use a directory/file to narrow down the symbol search. Defaults to the current working directory."`
25}
25}
26
26
27+type referencesTool struct {
28+ lspManager *lsp.Manager
29+}
30+
27const ReferencesToolName = "lsp_references"
31const ReferencesToolName = "lsp_references"
28
32
29//go:embed references.md
33//go:embed references.md
@@ -87,6 +91,10 @@ func NewReferencesTool(lspManager *lsp.Manager) fantasy.AgentTool {
87 )
91 )
88}
92}
89
93
94+func (r *referencesTool) Name() string {
95+ return ReferencesToolName
96+}
97+
90func find(ctx context.Context, lspManager *lsp.Manager, symbol string, match grepMatch) ([]protocol.Location, error) {
98func find(ctx context.Context, lspManager *lsp.Manager, symbol string, match grepMatch) ([]protocol.Location, error) {
91 absPath, err := filepath.Abs(match.path)
99 absPath, err := filepath.Abs(match.path)
92 if err != nil {
100 if err != nil {
internal/agent/tools/safe.go+22−95
@@ -2,16 +2,10 @@ package tools
2
2
3import (
3import (
4 "runtime"
4 "runtime"
5+ "slices"
5 "strings"
6 "strings"
6−
7− "mvdan.cc/sh/v3/syntax"
8)
7)
9
8
10−// safeCommands are read-only commands that can run without a permission
11−// prompt. Entries may be multi-word (e.g. "git log") to scope a
12−// subcommand. Do NOT add wrapper commands that execute an arbitrary
13−// argument (env, time, timeout, nohup, nice, xargs, sh, bash, find -exec,
14−// ...): those would let an unapproved command ride in on a safe prefix.
15var safeCommands = []string{
9var safeCommands = []string{
16 // Bash builtins and core utils
10 // Bash builtins and core utils
17 "cal",
11 "cal",
@@ -19,6 +13,7 @@ var safeCommands = []string{
19 "df",
13 "df",
20 "du",
14 "du",
21 "echo",
15 "echo",
16+ "env",
22 "free",
17 "free",
23 "groups",
18 "groups",
24 "hostname",
19 "hostname",
@@ -26,10 +21,14 @@ var safeCommands = []string{
26 "kill",
21 "kill",
27 "killall",
22 "killall",
28 "ls",
23 "ls",
24+ "nice",
25+ "nohup",
29 "printenv",
26 "printenv",
30 "ps",
27 "ps",
31 "pwd",
28 "pwd",
32 "set",
29 "set",
30+ "time",
31+ "timeout",
33 "top",
32 "top",
34 "type",
33 "type",
35 "uname",
34 "uname",
@@ -59,6 +58,22 @@ var safeCommands = []string{
59 "git tag",
58 "git tag",
60}
59}
61
60
61+var chainingMetacharacters = []string{
62+ ";",
63+ "|",
64+ "&&",
65+ "$(",
66+ "`",
67+}
68+
69+// containsCommandChaining reports whether s contains shell metacharacters
70+// that enable command chaining or substitution.
71+func containsCommandChaining(s string) bool {
72+ return slices.ContainsFunc(chainingMetacharacters, func(c string) bool {
73+ return strings.Contains(s, c)
74+ })
75+}
76+
62func init() {
77func init() {
63 if runtime.GOOS == "windows" {
78 if runtime.GOOS == "windows" {
64 safeCommands = append(
79 safeCommands = append(
@@ -72,92 +87,4 @@ func init() {
72 "where",
87 "where",
73 )
88 )
74 }
89 }
75−
76− // Precompute the tokenized form of each safe command once, so
77− // isSafeReadOnly doesn't re-split on every call.
78− safeCommandTokens = make([][]string, len(safeCommands))
79− for i, safeCmd := range safeCommands {
80− safeCommandTokens[i] = strings.Fields(safeCmd)
81− }
82−}
83−
84−// safeCommandTokens holds the tokenized form of safeCommands, populated in
85−// init after any platform-specific entries are appended.
86−var safeCommandTokens [][]string
87−
88−// isSafeReadOnly reports whether command is a single, simple, read-only
89−// command that can run without a permission prompt.
90−//
91−// It parses the command and only returns true when the whole input is
92−// exactly one simple command (no pipes, chaining, subshells, blocks, or
93−// negation), with no redirections, no backgrounding, no inline variable
94−// assignments (which could inject e.g. LD_PRELOAD), and no command or
95−// process substitution anywhere. The command name (and any subcommand
96−// tokens) must be plain literals matching an entry in safeCommands.
97−//
98−// This replaces raw-string prefix matching, which let wrapper commands
99−// and redirects/chaining smuggle unapproved commands past the gate
100−// (e.g. "time rm -rf x", "echo x > file", "echo hi & rm -rf x").
101−func isSafeReadOnly(command string) bool {
102− parser := syntax.NewParser()
103− file, err := parser.Parse(strings.NewReader(command), "")
104− if err != nil {
105− return false
106− }
107−
108− // Exactly one statement, and it must be a plain foreground command.
109− if len(file.Stmts) != 1 {
110− return false
111− }
112− stmt := file.Stmts[0]
113− if stmt.Background || stmt.Negated || len(stmt.Redirs) > 0 {
114− return false
115− }
116− call, ok := stmt.Cmd.(*syntax.CallExpr)
117− if !ok || len(call.Args) == 0 || len(call.Assigns) > 0 {
118− return false
119− }
120−
121− // Reject any command/process substitution anywhere in the statement:
122− // those execute arbitrary commands the allowlist never sees.
123− safe := true
124− syntax.Walk(file, func(node syntax.Node) bool {
125− switch node.(type) {
126− case *syntax.CmdSubst, *syntax.ProcSubst:
127− safe = false
128− return false
129− }
130− return true
131− })
132− if !safe {
133− return false
134− }
135−
136− // Extract literal argv. Non-literal words (quoting, parameter
137− // expansion, globbing) yield "" via Word.Lit(); the command name and
138− // any matched subcommand tokens must be plain literals.
139− argv := make([]string, len(call.Args))
140− for i, w := range call.Args {
141− argv[i] = w.Lit()
142− }
143− if argv[0] == "" {
144− return false
145− }
146−
147− for _, tokens := range safeCommandTokens {
148− if len(argv) < len(tokens) {
149− continue
150− }
151− match := true
152− for i, tok := range tokens {
153− if argv[i] != tok {
154− match = false
155− break
156− }
157− }
158− if match {
159− return true
160− }
161− }
162− return false
163}
90}
internal/agent/tools/safe_test.go+24−51
@@ -6,7 +6,7 @@ import (
6 "github.com/stretchr/testify/assert"
6 "github.com/stretchr/testify/assert"
7)
7)
8
8
9−func TestIsSafeReadOnly(t *testing.T) {
9+func TestContainsCommandChaining(t *testing.T) {
10 t.Parallel()
10 t.Parallel()
11
11
12 tests := []struct {
12 tests := []struct {
@@ -14,61 +14,34 @@ func TestIsSafeReadOnly(t *testing.T) {
14 input string
14 input string
15 expected bool
15 expected bool
16 }{
16 }{
17− // Plain safe commands.
18− {"plain ls", "ls -la", true},
17+ {"plain ls", "ls -la", false},
19− {"plain echo", "echo hello world", true},
18+ {"plain echo", "echo hello world", false},
20− {"plain pwd", "pwd", true},
19+ {"plain pwd", "pwd", false},
21− {"git status", "git status", true},
20+ {"plain git status", "git status", false},
22− {"git log with flags", "git log --oneline", true},
21+ {"ls with redirect", "ls > /tmp/out", false},
23− {"git config --get", "git config --get user.name", true},
24− {"echo with param expansion", "echo $HOME", true},
25− {"simple kill", "kill 1234", true},
26−
27− // Not on the allowlist.
28− {"rm", "rm -rf /tmp/x", false},
29− {"unknown", "frobnicate", false},
30− {"git push not allowed", "git push", false},
31− {"prefix is not a match (lsof)", "lsof", false},
32−
33− // Chaining / multiple statements.
34− {"pipe", "ls | grep foo", false},
22+ {"ls with pipe", "ls | grep foo", true},
35− {"and", "ls && echo done", false},
23+ {"ls with double ampersand", "ls && echo done", true},
36− {"or", "ls || echo fail", false},
37− {"semicolon", "ls; echo done", false},
24+ {"ls with semicolon", "ls; echo done", true},
38− {"newline", "echo hi\nrm -rf x", false},
25+ {"ls with pipe pipe", "ls || echo fail", true},
26+ {"ls with backticks", "ls `echo foo`", true},
27+ {"ls with subshell", "ls $(echo foo)", true},
39− {"background then rm", "echo hi & rm -rf x", false},
28+ {"ls with background ampersand", "ls & echo done", false},
40− {"rm first with and", "rm -rf / && ls", false},
29+ {"rm -rf with && ls (rm first)", "rm -rf / && ls", true},
41−
30+ {"redirect with ampersand gt", "ls &> /dev/null", false},
42− // Substitution.
31+ {"redirect with gt ampersand", "ls >& /dev/null", false},
43− {"command subst", "ls $(rm -rf x)", false},
32+ {"simple kill", "kill 1234", false},
44− {"backticks", "ls `rm -rf x`", false},
33+ {"kill with pipe", "kill 1234 | echo foo", true},
45− {"param default with subst", "echo ${x:-$(rm -rf x)}", false},
34+ {"git log", "git log --oneline", false},
46−
35+ {"git log with pipe", "git log | head", true},
47− // Redirection.
36+ {"empty string", "", false},
48− {"redirect out", "echo x > /tmp/out", false},
37+ {"dollar sign in argument", "echo $HOME", false},
49− {"redirect append", "echo x >> ~/.bashrc", false},
50− {"redirect in", "cat < /etc/passwd", false},
51−
52− // Wrapper commands must not smuggle an unapproved command through.
53− {"time wrapper", "time rm -rf x", false},
54− {"env wrapper", "env FOO=bar rm -rf x", false},
55− {"timeout wrapper", "timeout 10 rm -rf x", false},
56− {"nohup wrapper", "nohup rm -rf x", false},
57− {"nice wrapper", "nice rm -rf x", false},
58−
59− // Inline assignment (e.g. LD_PRELOAD) is not safe.
60− {"inline assignment", "LD_PRELOAD=/evil.so ls", false},
61−
62− // Malformed / empty.
63− {"empty", "", false},
64− {"unbalanced quote", "echo \"unterminated", false},
65 }
38 }
66
39
67 for _, tt := range tests {
40 for _, tt := range tests {
68 t.Run(tt.name, func(t *testing.T) {
41 t.Run(tt.name, func(t *testing.T) {
69 t.Parallel()
42 t.Parallel()
70− got := isSafeReadOnly(tt.input)
43+ got := containsCommandChaining(tt.input)
71− assert.Equal(t, tt.expected, got, "isSafeReadOnly(%q)", tt.input)
44+ assert.Equal(t, tt.expected, got, "containsCommandChaining(%q)", tt.input)
72 })
45 })
73 }
46 }
74}
47}
internal/agent/tools/sourcegraph.go+4−4
@@ -64,7 +64,7 @@ func NewSourcegraphTool(client *http.Client) fantasy.AgentTool {
64 sourcegraphDescription(),
64 sourcegraphDescription(),
65 func(ctx context.Context, params SourcegraphParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
65 func(ctx context.Context, params SourcegraphParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
66 if params.Query == "" {
66 if params.Query == "" {
67− return fantasy.NewTextErrorResponse("query parameter is required"), nil
67+ return fantasy.NewTextErrorResponse("Query parameter is required"), nil
68 }
68 }
69
69
70 if params.Count <= 0 {
70 if params.Count <= 0 {
@@ -129,10 +129,10 @@ func NewSourcegraphTool(client *http.Client) fantasy.AgentTool {
129 if resp.StatusCode != http.StatusOK {
129 if resp.StatusCode != http.StatusOK {
130 body, _ := io.ReadAll(resp.Body)
130 body, _ := io.ReadAll(resp.Body)
131 if len(body) > 0 {
131 if len(body) > 0 {
132− return fantasy.NewTextErrorResponse(fmt.Sprintf("request failed with status code: %d, response: %s", resp.StatusCode, string(body))), nil
132+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d, response: %s", resp.StatusCode, string(body))), nil
133 }
133 }
134
134
135− return fantasy.NewTextErrorResponse(fmt.Sprintf("request failed with status code: %d", resp.StatusCode)), nil
135+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
136 }
136 }
137 body, err := io.ReadAll(resp.Body)
137 body, err := io.ReadAll(resp.Body)
138 if err != nil {
138 if err != nil {
@@ -146,7 +146,7 @@ func NewSourcegraphTool(client *http.Client) fantasy.AgentTool {
146
146
147 formattedResults, err := formatSourcegraphResults(result, params.ContextWindow)
147 formattedResults, err := formatSourcegraphResults(result, params.ContextWindow)
148 if err != nil {
148 if err != nil {
149− return fantasy.NewTextErrorResponse("failed to format results: " + err.Error()), nil
149+ return fantasy.NewTextErrorResponse("Failed to format results: " + err.Error()), nil
150 }
150 }
151
151
152 return fantasy.NewTextResponse(formattedResults), nil
152 return fantasy.NewTextResponse(formattedResults), nil
internal/agent/tools/view.go+24−25
@@ -12,7 +12,6 @@ import (
12 "net/http"
12 "net/http"
13 "os"
13 "os"
14 "path/filepath"
14 "path/filepath"
15− "strconv"
16 "strings"
15 "strings"
17 "time"
16 "time"
18 "unicode/utf8"
17 "unicode/utf8"
@@ -177,19 +176,19 @@ func NewViewTool(
177 }
176 }
178
177
179 if len(suggestions) > 0 {
178 if len(suggestions) > 0 {
180− return fantasy.NewTextErrorResponse(fmt.Sprintf("file not found: %s\n\nDid you mean one of these?\n%s",
179+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File not found: %s\n\nDid you mean one of these?\n%s",
181 filePath, strings.Join(suggestions, "\n"))), nil
180 filePath, strings.Join(suggestions, "\n"))), nil
182 }
181 }
183 }
182 }
184
183
185− return fantasy.NewTextErrorResponse(fmt.Sprintf("file not found: %s", filePath)), nil
184+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File not found: %s", filePath)), nil
186 }
185 }
187 return fantasy.ToolResponse{}, fmt.Errorf("error accessing file: %w", err)
186 return fantasy.ToolResponse{}, fmt.Errorf("error accessing file: %w", err)
188 }
187 }
189
188
190 // Check if it's a directory
189 // Check if it's a directory
191 if fileInfo.IsDir() {
190 if fileInfo.IsDir() {
192− return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
191+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", filePath)), nil
193 }
192 }
194
193
195 // Set default limit if not provided (no limit for SKILL.md files)
194 // Set default limit if not provided (no limit for SKILL.md files)
@@ -204,12 +203,12 @@ func NewViewTool(
204 isSupportedImage, mimeType := getImageMimeType(filePath)
203 isSupportedImage, mimeType := getImageMimeType(filePath)
205 if isSupportedImage {
204 if isSupportedImage {
206 if fileInfo.Size() > MaxViewSize {
205 if fileInfo.Size() > MaxViewSize {
207− return fantasy.NewTextErrorResponse(fmt.Sprintf("image file is too large (%d bytes). Maximum size is %d bytes",
206+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Image file is too large (%d bytes). Maximum size is %d bytes",
208 fileInfo.Size(), MaxViewSize)), nil
207 fileInfo.Size(), MaxViewSize)), nil
209 }
208 }
210 if !GetSupportsImagesFromContext(ctx) {
209 if !GetSupportsImagesFromContext(ctx) {
211 modelName := GetModelNameFromContext(ctx)
210 modelName := GetModelNameFromContext(ctx)
212− return fantasy.NewTextErrorResponse(fmt.Sprintf("this model (%s) does not support image data.", modelName)), nil
211+ return fantasy.NewTextErrorResponse(fmt.Sprintf("This model (%s) does not support image data.", modelName)), nil
213 }
212 }
214
213
215 imageData, readErr := os.ReadFile(filePath)
214 imageData, readErr := os.ReadFile(filePath)
@@ -237,13 +236,13 @@ func NewViewTool(
237 if err != nil {
236 if err != nil {
238 var tooLarge contentTooLargeError
237 var tooLarge contentTooLargeError
239 if errors.As(err, &tooLarge) {
238 if errors.As(err, &tooLarge) {
240− return fantasy.NewTextErrorResponse(fmt.Sprintf("content section is too large (%d bytes). Maximum size is %d bytes",
239+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Content section is too large (%d bytes). Maximum size is %d bytes",
241 tooLarge.Size, tooLarge.Max)), nil
240 tooLarge.Size, tooLarge.Max)), nil
242 }
241 }
243 return fantasy.ToolResponse{}, fmt.Errorf("error reading file: %w", err)
242 return fantasy.ToolResponse{}, fmt.Errorf("error reading file: %w", err)
244 }
243 }
245 if !utf8.ValidString(content) {
244 if !utf8.ValidString(content) {
246− return fantasy.NewTextErrorResponse("file content is not valid UTF-8"), nil
245+ return fantasy.NewTextErrorResponse("File content is not valid UTF-8"), nil
247 }
246 }
248
247
249 openInLSPs(ctx, lspManager, filePath)
248 openInLSPs(ctx, lspManager, filePath)
@@ -253,7 +252,7 @@ func NewViewTool(
253
252
254 if hasMore {
253 if hasMore {
255 output += fmt.Sprintf("\n\n(File has more lines. Use 'offset' parameter to read beyond line %d)",
254 output += fmt.Sprintf("\n\n(File has more lines. Use 'offset' parameter to read beyond line %d)",
256− params.Offset+strings.Count(content, "\n")+1)
255+ params.Offset+len(strings.Split(content, "\n")))
257 }
256 }
258 output += "\n</file>\n"
257 output += "\n</file>\n"
259 output += getDiagnostics(filePath, lspManager)
258 output += getDiagnostics(filePath, lspManager)
@@ -285,24 +284,24 @@ func addLineNumbers(content string, startLine int) string {
285 return ""
284 return ""
286 }
285 }
287
286
288− var b strings.Builder
287+ lines := strings.Split(content, "\n")
288+
289+ var result []string
289− for i, line := range strings.Split(content, "\n") {
290+ for i, line := range lines {
290− if i > 0 {
291− b.WriteByte('\n')
292− }
293 line = strings.TrimSuffix(line, "\r")
291 line = strings.TrimSuffix(line, "\r")
294
292
295− numStr := strconv.Itoa(i + startLine)
293+ lineNum := i + startLine
296− // Right-align to a width of 6 (numbers with >=6 digits are unpadded).
294+ numStr := fmt.Sprintf("%d", lineNum)
297− for pad := 6 - len(numStr); pad > 0; pad-- {
295+
298− b.WriteByte(' ')
296+ if len(numStr) >= 6 {
297+ result = append(result, fmt.Sprintf("%s|%s", numStr, line))
298+ } else {
299+ paddedNum := fmt.Sprintf("%6s", numStr)
300+ result = append(result, fmt.Sprintf("%s|%s", paddedNum, line))
299 }
301 }
300− b.WriteString(numStr)
301− b.WriteByte('|')
302− b.WriteString(line)
303 }
302 }
304
303
305− return b.String()
304+ return strings.Join(result, "\n")
306}
305}
307
306
308func readTextFile(filePath string, offset, limit, maxContentSize int) (string, bool, error) {
307func readTextFile(filePath string, offset, limit, maxContentSize int) (string, bool, error) {
@@ -448,12 +447,12 @@ func readBuiltinFile(params ViewParams, skillTracker *skills.Tracker) (fantasy.T
448
447
449 data, err := fs.ReadFile(builtinFS, embeddedPath)
448 data, err := fs.ReadFile(builtinFS, embeddedPath)
450 if err != nil {
449 if err != nil {
451− return fantasy.NewTextErrorResponse(fmt.Sprintf("builtin file not found: %s", params.FilePath)), nil
450+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Builtin file not found: %s", params.FilePath)), nil
452 }
451 }
453
452
454 content := string(data)
453 content := string(data)
455 if !utf8.ValidString(content) {
454 if !utf8.ValidString(content) {
456− return fantasy.NewTextErrorResponse("file content is not valid UTF-8"), nil
455+ return fantasy.NewTextErrorResponse("File content is not valid UTF-8"), nil
457 }
456 }
458
457
459 limit := params.Limit
458 limit := params.Limit
@@ -462,7 +461,7 @@ func readBuiltinFile(params ViewParams, skillTracker *skills.Tracker) (fantasy.T
462 }
461 }
463
462
464 lines := strings.Split(content, "\n")
463 lines := strings.Split(content, "\n")
465− offset := max(0, min(params.Offset, len(lines)))
464+ offset := min(params.Offset, len(lines))
466 lines = lines[offset:]
465 lines = lines[offset:]
467
466
468 hasMore := len(lines) > limit
467 hasMore := len(lines) > limit
internal/agent/tools/view_test.go+2−2
@@ -151,7 +151,7 @@ func TestViewToolBlocksOversizedReturnedSections(t *testing.T) {
151 })
151 })
152
152
153 require.True(t, resp.IsError)
153 require.True(t, resp.IsError)
154− require.Contains(t, resp.Content, "content section is too large")
154+ require.Contains(t, resp.Content, "Content section is too large")
155}
155}
156
156
157func TestViewToolBlocksOversizedImages(t *testing.T) {
157func TestViewToolBlocksOversizedImages(t *testing.T) {
@@ -169,7 +169,7 @@ func TestViewToolBlocksOversizedImages(t *testing.T) {
169 })
169 })
170
170
171 require.True(t, resp.IsError)
171 require.True(t, resp.IsError)
172− require.Contains(t, resp.Content, "image file is too large")
172+ require.Contains(t, resp.Content, "Image file is too large")
173}
173}
174
174
175func TestReadTextFileEnforcesMaxContentSize(t *testing.T) {
175func TestReadTextFileEnforcesMaxContentSize(t *testing.T) {
internal/agent/tools/web_fetch.go+4−4
@@ -45,7 +45,7 @@ func NewWebFetchTool(workingDir string, client *http.Client) fantasy.AgentTool {
45
45
46 content, err := FetchURLAndConvert(ctx, client, params.URL)
46 content, err := FetchURLAndConvert(ctx, client, params.URL)
47 if err != nil {
47 if err != nil {
48− return fantasy.NewTextErrorResponse(fmt.Sprintf("failed to fetch URL: %s", err)), nil
48+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to fetch URL: %s", err)), nil
49 }
49 }
50
50
51 hasLargeContent := len(content) > LargeContentThreshold
51 hasLargeContent := len(content) > LargeContentThreshold
@@ -54,16 +54,16 @@ func NewWebFetchTool(workingDir string, client *http.Client) fantasy.AgentTool {
54 if hasLargeContent {
54 if hasLargeContent {
55 tempFile, err := os.CreateTemp(workingDir, "page-*.md")
55 tempFile, err := os.CreateTemp(workingDir, "page-*.md")
56 if err != nil {
56 if err != nil {
57− return fantasy.NewTextErrorResponse(fmt.Sprintf("failed to create temporary file: %s", err)), nil
57+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to create temporary file: %s", err)), nil
58 }
58 }
59 tempFilePath := tempFile.Name()
59 tempFilePath := tempFile.Name()
60
60
61 if _, err := tempFile.WriteString(content); err != nil {
61 if _, err := tempFile.WriteString(content); err != nil {
62 _ = tempFile.Close() // Best effort close
62 _ = tempFile.Close() // Best effort close
63− return fantasy.NewTextErrorResponse(fmt.Sprintf("failed to write content to file: %s", err)), nil
63+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to write content to file: %s", err)), nil
64 }
64 }
65 if err := tempFile.Close(); err != nil {
65 if err := tempFile.Close(); err != nil {
66− return fantasy.NewTextErrorResponse(fmt.Sprintf("failed to close temporary file: %s", err)), nil
66+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to close temporary file: %s", err)), nil
67 }
67 }
68
68
69 fmt.Fprintf(&result, "Fetched content from %s (large page)\n\n", params.URL)
69 fmt.Fprintf(&result, "Fetched content from %s (large page)\n\n", params.URL)
internal/agent/tools/web_search.go+1−1
@@ -53,7 +53,7 @@ func NewWebSearchTool(client *http.Client) fantasy.AgentTool {
53 results, err := searchDuckDuckGo(ctx, client, params.Query, maxResults)
53 results, err := searchDuckDuckGo(ctx, client, params.Query, maxResults)
54 slog.Debug("Web search completed", "query", params.Query, "results", len(results), "err", err)
54 slog.Debug("Web search completed", "query", params.Query, "results", len(results), "err", err)
55 if err != nil {
55 if err != nil {
56− return fantasy.NewTextErrorResponse("failed to search: " + err.Error()), nil
56+ return fantasy.NewTextErrorResponse("Failed to search: " + err.Error()), nil
57 }
57 }
58
58
59 return fantasy.NewTextResponse(formatSearchResults(results)), nil
59 return fantasy.NewTextResponse(formatSearchResults(results)), nil
internal/agent/tools/write.go+14−15
@@ -66,25 +66,21 @@ func NewWriteTool(
66 filePath := filepathext.SmartJoin(workingDir, params.FilePath)
66 filePath := filepathext.SmartJoin(workingDir, params.FilePath)
67
67
68 fileInfo, err := os.Stat(filePath)
68 fileInfo, err := os.Stat(filePath)
69− oldContent := ""
70 if err == nil {
69 if err == nil {
71 if fileInfo.IsDir() {
70 if fileInfo.IsDir() {
72− return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
71+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", filePath)), nil
73 }
72 }
74
73
75 modTime := fileInfo.ModTime().Truncate(time.Second)
74 modTime := fileInfo.ModTime().Truncate(time.Second)
76 lastRead := filetracker.LastReadTime(ctx, sessionID, filePath)
75 lastRead := filetracker.LastReadTime(ctx, sessionID, filePath)
77 if modTime.After(lastRead) {
76 if modTime.After(lastRead) {
78− return fantasy.NewTextErrorResponse(fmt.Sprintf("file %s has been modified since it was last read.\nLast modification: %s\nLast read: %s\n\nPlease read the file again before modifying it.",
77+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File %s has been modified since it was last read.\nLast modification: %s\nLast read: %s\n\nPlease read the file again before modifying it.",
79 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339))), nil
78 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339))), nil
80 }
79 }
81
80
82− oldBytes, readErr := os.ReadFile(filePath)
81+ oldContent, readErr := os.ReadFile(filePath)
83− if readErr == nil {
84− oldContent = string(oldBytes)
85− if oldContent == params.Content {
82+ if readErr == nil && string(oldContent) == params.Content {
86− return fantasy.NewTextErrorResponse(fmt.Sprintf("file %s already contains the exact content. No changes made.", filePath)), nil
83+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File %s already contains the exact content. No changes made.", filePath)), nil
87− }
88 }
84 }
89 } else if !os.IsNotExist(err) {
85 } else if !os.IsNotExist(err) {
90 return fantasy.ToolResponse{}, fmt.Errorf("error checking file: %w", err)
86 return fantasy.ToolResponse{}, fmt.Errorf("error checking file: %w", err)
@@ -95,17 +91,20 @@ func NewWriteTool(
95 return fantasy.ToolResponse{}, fmt.Errorf("error creating directory: %w", err)
91 return fantasy.ToolResponse{}, fmt.Errorf("error creating directory: %w", err)
96 }
92 }
97
93
94+ oldContent := ""
95+ if fileInfo != nil && !fileInfo.IsDir() {
96+ oldBytes, readErr := os.ReadFile(filePath)
97+ if readErr == nil {
98+ oldContent = string(oldBytes)
99+ }
100+ }
101+
98 diff, additions, removals := diff.GenerateDiff(
102 diff, additions, removals := diff.GenerateDiff(
99 oldContent,
103 oldContent,
100 params.Content,
104 params.Content,
101 strings.TrimPrefix(filePath, workingDir),
105 strings.TrimPrefix(filePath, workingDir),
102 )
106 )
103
107
104− description := fmt.Sprintf("Create file %s", filePath)
105− if fileInfo != nil && !fileInfo.IsDir() {
106− description = fmt.Sprintf("Overwrite file %s", filePath)
107− }
108−
109 p, err := permissions.Request(
108 p, err := permissions.Request(
110 ctx,
109 ctx,
111 permission.CreatePermissionRequest{
110 permission.CreatePermissionRequest{
@@ -114,7 +113,7 @@ func NewWriteTool(
114 ToolCallID: call.ID,
113 ToolCallID: call.ID,
115 ToolName: WriteToolName,
114 ToolName: WriteToolName,
116 Action: "write",
115 Action: "write",
117− Description: description,
116+ Description: fmt.Sprintf("Create file %s", filePath),
118 Params: WritePermissionsParams{
117 Params: WritePermissionsParams{
119 FilePath: filePath,
118 FilePath: filePath,
120 OldContent: oldContent,
119 OldContent: oldContent,
internal/agent/usage_fallback.go+9
@@ -33,6 +33,15 @@ func fallbackStepUsage(messages []fantasy.Message, step fantasy.StepResult) (fan
33 }, true
33 }, true
34}
34}
35
35
36+func cloneFantasyMessages(messages []fantasy.Message) []fantasy.Message {
37+ cloned := make([]fantasy.Message, len(messages))
38+ for i, msg := range messages {
39+ cloned[i] = msg
40+ cloned[i].Content = append([]fantasy.MessagePart(nil), msg.Content...)
41+ }
42+ return cloned
43+}
44+
36func estimateMessageTokens(messages []fantasy.Message) int64 {
45func estimateMessageTokens(messages []fantasy.Message) int64 {
37 var tokens int64
46 var tokens int64
38 for _, msg := range messages {
47 for _, msg := range messages {
internal/app/app.go+6−77
@@ -27,7 +27,6 @@ import (
27 "github.com/charmbracelet/crush/internal/event"
27 "github.com/charmbracelet/crush/internal/event"
28 "github.com/charmbracelet/crush/internal/filetracker"
28 "github.com/charmbracelet/crush/internal/filetracker"
29 "github.com/charmbracelet/crush/internal/format"
29 "github.com/charmbracelet/crush/internal/format"
30− "github.com/charmbracelet/crush/internal/herdr"
31 "github.com/charmbracelet/crush/internal/history"
30 "github.com/charmbracelet/crush/internal/history"
32 "github.com/charmbracelet/crush/internal/log"
31 "github.com/charmbracelet/crush/internal/log"
33 "github.com/charmbracelet/crush/internal/lsp"
32 "github.com/charmbracelet/crush/internal/lsp"
@@ -74,13 +73,8 @@ type App struct {
74 tuiWG *sync.WaitGroup
73 tuiWG *sync.WaitGroup
75
74
76 // global context and cleanup functions
75 // global context and cleanup functions
77− globalCtx context.Context
76+ globalCtx context.Context
78− cleanupFuncs []func(context.Context) error
77+ cleanupFuncs []func(context.Context) error
79− // bgCancel cancels the context passed to the fire-and-forget
80− // goroutines launched from New (update check, MCP init, LSP tracking);
81− // bgWG tracks them so Shutdown can join them before closing the DB.
82− bgCancel context.CancelFunc
83− bgWG sync.WaitGroup
84 agentNotifications *pubsub.Broker[notify.Notification]
78 agentNotifications *pubsub.Broker[notify.Notification]
85 // runCompletions is the authoritative per-run completion signal,
79 // runCompletions is the authoritative per-run completion signal,
86 // emitted once per top-level agent turn after all message
80 // emitted once per top-level agent turn after all message
@@ -89,10 +83,6 @@ type App struct {
89 // drive their exit on a deterministic, payload-bearing event
83 // drive their exit on a deterministic, payload-bearing event
90 // instead of guessing from message finish parts.
84 // instead of guessing from message finish parts.
91 runCompletions *pubsub.Broker[notify.RunComplete]
85 runCompletions *pubsub.Broker[notify.RunComplete]
92−
93− // herdrClient reports agent state to herdr when running inside
94− // a herdr-managed pane. Nil when not in a herdr environment.
95− herdrClient *herdr.Client
96}
86}
97
87
98// New initializes a new application instance. skillsMgr carries the
88// New initializes a new application instance. skillsMgr carries the
@@ -139,25 +129,10 @@ func New(ctx context.Context, conn *sql.DB, store *config.ConfigStore, skillsMgr
139 slog.Warn("Clipboard initialization failed", "error", err)
129 slog.Warn("Clipboard initialization failed", "error", err)
140 }
130 }
141
131
142− // Background goroutines run on a cancelable context so Shutdown can
143− // stop and join them (see goBackground / Shutdown) rather than leaking
144− // them past teardown.
145− bgCtx, bgCancel := context.WithCancel(ctx)
146− app.bgCancel = bgCancel
147−
148 // Check for updates in the background.
132 // Check for updates in the background.
149− app.goBackground(func() { app.checkForUpdates(bgCtx) })
133+ go app.checkForUpdates(ctx)
150
134
151− app.goBackground(func() { mcp.Initialize(bgCtx, app.Permissions, store) })
135+ go mcp.Initialize(ctx, app.Permissions, store)
152−
153− // Start herdr integration when running inside a herdr pane.
154− app.herdrClient = herdr.Init()
155− herdr.BridgeLocal(ctx, app.herdrClient, herdr.BridgeSources{
156− PermRequests: app.Permissions,
157− PermNotifications: app.Permissions,
158− RunCompletions: app.runCompletions,
159− Messages: app.Messages,
160− })
161
136
162 // Release the shared database connection on shutdown. The pool
137 // Release the shared database connection on shutdown. The pool
163 // closes the underlying *sql.DB when the last reference is released.
138 // closes the underlying *sql.DB when the last reference is released.
@@ -186,22 +161,11 @@ func New(ctx context.Context, conn *sql.DB, store *config.ConfigStore, skillsMgr
186 client.SetDiagnosticsCallback(updateLSPDiagnostics)
161 client.SetDiagnosticsCallback(updateLSPDiagnostics)
187 updateLSPState(name, client.GetServerState(), nil, client, 0)
162 updateLSPState(name, client.GetServerState(), nil, client, 0)
188 })
163 })
189− app.goBackground(app.LSPManager.TrackConfigured)
164+ go app.LSPManager.TrackConfigured()
190
165
191 return app, nil
166 return app, nil
192}
167}
193
168
194−// goBackground runs fn in a goroutine tracked by bgWG so Shutdown can join
195−// it. Callers whose work is cancelable should close over the app's
196−// background context (see New) so bgCancel actually interrupts them.
197−func (app *App) goBackground(fn func()) {
198− app.bgWG.Add(1)
199− go func() {
200− defer app.bgWG.Done()
201− fn()
202− }()
203−}
204−
205// Config returns the pure-data configuration.
169// Config returns the pure-data configuration.
206func (app *App) Config() *config.Config {
170func (app *App) Config() *config.Config {
207 return app.config.Config()
171 return app.config.Config()
@@ -236,15 +200,6 @@ func (app *App) RunCompletions() *pubsub.Broker[notify.RunComplete] {
236 return app.runCompletions
200 return app.runCompletions
237}
201}
238
202
239−// ReportCurrentSession tells herdr which session the user is now
240−// viewing so it can persist a resumable reference for the pane. Safe
241−// to call when not running inside a herdr pane; the underlying client
242−// is nil-safe. Call this whenever the active session changes (load,
243−// new, or select).
244−func (app *App) ReportCurrentSession(sessionID string) {
245− app.herdrClient.SetSessionID(sessionID)
246−}
247−
248// resolveSession resolves which session to use for a non-interactive run
203// resolveSession resolves which session to use for a non-interactive run
249// If continueSessionID is set, it looks up that session by ID
204// If continueSessionID is set, it looks up that session by ID
250// If useLast is set, it returns the most recently updated top-level session
205// If useLast is set, it returns the most recently updated top-level session
@@ -342,9 +297,7 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt,
342 }
297 }
343
298
344 // force update of agent models before running so mcp tools are loaded
299 // force update of agent models before running so mcp tools are loaded
345− if err := app.AgentCoordinator.UpdateModels(ctx); err != nil {
300+ app.AgentCoordinator.UpdateModels(ctx)
346− slog.Warn("failed to update agent models", "error", err)
347− }
348
301
349 defer stopSpinner()
302 defer stopSpinner()
350
303
@@ -363,9 +316,6 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt,
363 // session.
316 // session.
364 app.Permissions.AutoApproveSession(sess.ID)
317 app.Permissions.AutoApproveSession(sess.ID)
365
318
366− // Report session identity to herdr.
367− app.ReportCurrentSession(sess.ID)
368−
369 type response struct {
319 type response struct {
370 result *fantasy.AgentResult
320 result *fantasy.AgentResult
371 err error
321 err error
@@ -714,24 +664,6 @@ func (app *App) Shutdown() {
714 }
664 }
715 }
665 }
716
666
717− // Cancel and join the fire-and-forget goroutines from New before the
718− // DB/MCP cleanup below, so none keep touching a closing store, DB, or
719− // MCP registry. Bounded by shutdownCtx so a goroutine that ignores
720− // cancellation can't hang shutdown.
721− if app.bgCancel != nil {
722− app.bgCancel()
723− }
724− bgJoined := make(chan struct{})
725− go func() {
726− app.bgWG.Wait()
727− close(bgJoined)
728− }()
729− select {
730− case <-bgJoined:
731− case <-shutdownCtx.Done():
732− slog.Warn("Timed out waiting for background goroutines on shutdown")
733− }
734−
735 // Now run remaining cleanup tasks in parallel.
667 // Now run remaining cleanup tasks in parallel.
736 var wg sync.WaitGroup
668 var wg sync.WaitGroup
737
669
@@ -745,9 +677,6 @@ func (app *App) Shutdown() {
745 shell.GetBackgroundShellManager().KillAll(shutdownCtx)
677 shell.GetBackgroundShellManager().KillAll(shutdownCtx)
746 })
678 })
747
679
748− // Close herdr client to stop its background writer.
749− app.herdrClient.Close()
750−
751 // Shutdown all LSP clients.
680 // Shutdown all LSP clients.
752 wg.Go(func() {
681 wg.Go(func() {
753 app.LSPManager.KillAll(shutdownCtx)
682 app.LSPManager.KillAll(shutdownCtx)
internal/backend/agent.go+10−1
@@ -8,6 +8,7 @@ import (
8 "github.com/charmbracelet/crush/internal/agent"
8 "github.com/charmbracelet/crush/internal/agent"
9 "github.com/charmbracelet/crush/internal/agent/notify"
9 "github.com/charmbracelet/crush/internal/agent/notify"
10 "github.com/charmbracelet/crush/internal/config"
10 "github.com/charmbracelet/crush/internal/config"
11+ "github.com/charmbracelet/crush/internal/message"
11 "github.com/charmbracelet/crush/internal/proto"
12 "github.com/charmbracelet/crush/internal/proto"
12 "github.com/charmbracelet/crush/internal/pubsub"
13 "github.com/charmbracelet/crush/internal/pubsub"
13 "github.com/charmbracelet/crush/internal/shell"
14 "github.com/charmbracelet/crush/internal/shell"
@@ -252,7 +253,15 @@ func (b *Backend) RunShellCommand(ctx context.Context, workspaceID string, req p
252 var persist shell.PersistFunc
253 var persist shell.PersistFunc
253 if req.SessionID != "" {
254 if req.SessionID != "" {
254 persist = func(cmd, output string, exitCode int) error {
255 persist = func(cmd, output string, exitCode int) error {
255− return shell.PersistOutput(ctx, ws.Messages, req.SessionID, cmd, output, exitCode)
256+ _, err := ws.Messages.Create(ctx, req.SessionID, message.CreateMessageParams{
257+ Role: message.User,
258+ Parts: []message.ContentPart{message.ShellCommand{
259+ Command: cmd,
260+ Output: output,
261+ ExitCode: exitCode,
262+ }},
263+ })
264+ return err
256 }
265 }
257 }
266 }
258
267
internal/backend/backend.go−18
@@ -649,24 +649,6 @@ func (w *Workspace) AttachedClientsForSession(sessionID string) int {
649 return n
649 return n
650}
650}
651
651
652−// AttachedClientCountsBySession returns, for every session with at least
653−// one attached client, the number of clients currently viewing it. Only
654−// clients with a live SSE stream (streams > 0) are counted; hold-only
655−// clients (streams == 0) do not contribute. It acquires [clientsMu] once
656−// and iterates the client set a single time, so callers that need counts
657−// for many sessions avoid an O(sessions x clients) scan.
658−func (w *Workspace) AttachedClientCountsBySession() map[string]int {
659− w.clientsMu.Lock()
660− defer w.clientsMu.Unlock()
661− counts := make(map[string]int)
662− for _, cs := range w.clients {
663− if cs.streams > 0 {
664− counts[cs.currentSessionID]++
665− }
666− }
667− return counts
668−}
669−
670// GetWorkspaceProto returns the proto representation of a workspace.
652// GetWorkspaceProto returns the proto representation of a workspace.
671func (b *Backend) GetWorkspaceProto(id string) (proto.Workspace, error) {
653func (b *Backend) GetWorkspaceProto(id string) (proto.Workspace, error) {
672 ws, err := b.GetWorkspace(id)
654 ws, err := b.GetWorkspace(id)
internal/backend/config_test.go+9−7
@@ -81,13 +81,15 @@ func TestRemoveConfigField_PublishesConfigChanged(t *testing.T) {
81
81
82func TestUpdatePreferredModel_PublishesConfigChanged(t *testing.T) {
82func TestUpdatePreferredModel_PublishesConfigChanged(t *testing.T) {
83 if raceEnabled {
83 if raceEnabled {
84− // The backend package has pre-existing, flaky data races under
84+ // UpdatePreferredModel writes config.Models concurrently
85− // -race in its agent-run/attach/hold lifecycle that surface only
85+ // with the agent coordinator's async sub-agent builder
86− // under full-package timing pressure. This test spins up the full
86+ // that reads it via buildAgentModels. That race is
87− // coordinator, so it is skipped under -race to keep CI stable
87+ // pre-existing in the codebase and unrelated to this
88− // until that concurrency debt is addressed. The mutator → publish
88+ // item; ConfigStore mutations are not currently
89+ // synchronized against background readers in [app.App].
90+ // The mutator → publish wiring is unit-tested via
89− // wiring is unit-tested via publishConfigChanged regardless.
91+ // publishConfigChanged regardless.
90− t.Skip("skipped under -race: pre-existing backend lifecycle races")
92+ t.Skip("skipped under -race: pre-existing race between ConfigStore writes and agent coordinator startup")
91 }
93 }
92 b, ws, evc := newPublishingWorkspace(t)
94 b, ws, evc := newPublishingWorkspace(t)
93
95
internal/backend/shell_command_test.godeleted−54
@@ -1,54 +0,0 @@
1−package backend
2−
3−import (
4− "context"
5− "testing"
6−
7− "github.com/charmbracelet/crush/internal/app"
8− "github.com/charmbracelet/crush/internal/db"
9− "github.com/charmbracelet/crush/internal/message"
10− "github.com/charmbracelet/crush/internal/proto"
11− "github.com/charmbracelet/crush/internal/session"
12− "github.com/google/uuid"
13− "github.com/stretchr/testify/require"
14−)
15−
16−func TestRunShellCommand_SkipsPersistenceForMissingSession(t *testing.T) {
17− t.Parallel()
18−
19− conn, err := db.Connect(t.Context(), t.TempDir())
20− require.NoError(t, err)
21− t.Cleanup(func() { conn.Close() })
22−
23− q := db.New(conn)
24− sessions := session.NewService(q, conn)
25− messages := message.NewService(q)
26−
27− b, _ := newTestBackend(t)
28− ws := &Workspace{
29− ID: uuid.New().String(),
30− Path: t.TempDir(),
31− resolvedPath: t.TempDir(),
32− clients: make(map[string]*clientState),
33− shutdownFn: func() {},
34− }
35− ws.App = &app.App{
36− Sessions: sessions,
37− Messages: messages,
38− }
39− ws.ctx, ws.cancel = context.WithCancel(b.ctx)
40− InsertWorkspaceForTest(b, ws)
41−
42− missingSessionID := uuid.New().String()
43− resp, err := b.RunShellCommand(t.Context(), ws.ID, proto.ShellCommandRequest{
44− SessionID: missingSessionID,
45− Command: "echo hello",
46− })
47− require.NoError(t, err)
48− require.Equal(t, "hello\n", resp.Output)
49− require.Zero(t, resp.ExitCode)
50−
51− stored, err := messages.List(t.Context(), missingSessionID)
52− require.NoError(t, err)
53− require.Empty(t, stored)
54−}
internal/client/proto.go+4−2
@@ -661,9 +661,11 @@ func (c *Client) GetConfig(ctx context.Context, id string) (*config.Config, erro
661 return &cfg, nil
661 return &cfg, nil
662}
662}
663
663
664−func jsonBody(v any) io.Reader {
664+func jsonBody(v any) *bytes.Buffer {
665+ b := new(bytes.Buffer)
665 m, _ := json.Marshal(v)
666 m, _ := json.Marshal(v)
666− return bytes.NewReader(m)
667+ b.Write(m)
668+ return b
667}
669}
668
670
669// SaveSession updates a session in a workspace, returning a proto type.
671// SaveSession updates a session in a workspace, returning a proto type.
internal/cmd/logs.go+3−3
@@ -95,7 +95,7 @@ func followLogs(ctx context.Context, logsFile string, tailLines int) error {
95 lines = lines[len(lines)-tailLines:]
95 lines = lines[len(lines)-tailLines:]
96 }
96 }
97 }
97 }
98− _ = t.Stop()
98+ t.Stop()
99
99
100 for _, line := range lines {
100 for _, line := range lines {
101 printLogLine(line)
101 printLogLine(line)
@@ -115,7 +115,7 @@ func followLogs(ctx context.Context, logsFile string, tailLines int) error {
115 if err != nil {
115 if err != nil {
116 return fmt.Errorf("failed to tail log file: %v", err)
116 return fmt.Errorf("failed to tail log file: %v", err)
117 }
117 }
118− defer func() { _ = t.Stop() }()
118+ defer t.Stop()
119
119
120 for {
120 for {
121 select {
121 select {
@@ -140,7 +140,7 @@ func showLogs(logsFile string, tailLines int) error {
140 if err != nil {
140 if err != nil {
141 return fmt.Errorf("failed to tail log file: %v", err)
141 return fmt.Errorf("failed to tail log file: %v", err)
142 }
142 }
143− defer func() { _ = t.Stop() }()
143+ defer t.Stop()
144
144
145 var lines []string
145 var lines []string
146 for line := range t.Lines {
146 for line := range t.Lines {
internal/cmd/root.go+26−1
@@ -3,6 +3,7 @@ package cmd
3import (
3import (
4 "bytes"
4 "bytes"
5 "context"
5 "context"
6+ _ "embed"
6 "errors"
7 "errors"
7 "fmt"
8 "fmt"
8 "io"
9 "io"
@@ -124,7 +125,7 @@ crush --continue
124 event.AppInitialized()
125 event.AppInitialized()
125
126
126 com := common.DefaultCommon(ws)
127 com := common.DefaultCommon(ws)
127− model := ui.New(cmd.Context(), com, sessionID, continueLast)
128+ model := ui.New(com, sessionID, continueLast)
128
129
129 inputFilter := ui.NewFilter()
130 inputFilter := ui.NewFilter()
130 var env uv.Environ = os.Environ()
131 var env uv.Environ = os.Environ()
@@ -839,3 +840,27 @@ func ResolveCwd(cmd *cobra.Command) (string, error) {
839 }
840 }
840 return cwd, nil
841 return cwd, nil
841}
842}
843+
844+func createDotCrushDir(dir string) error {
845+ if err := os.MkdirAll(dir, 0o700); err != nil {
846+ return fmt.Errorf("failed to create data directory: %q %w", dir, err)
847+ }
848+
849+ gitIgnorePath := filepath.Join(dir, ".gitignore")
850+ content, err := os.ReadFile(gitIgnorePath)
851+
852+ // create or update if old version
853+ if os.IsNotExist(err) || string(content) == oldGitIgnore {
854+ if err := os.WriteFile(gitIgnorePath, []byte(defaultGitIgnore), 0o644); err != nil {
855+ return fmt.Errorf("failed to create .gitignore file: %q %w", gitIgnorePath, err)
856+ }
857+ }
858+
859+ return nil
860+}
861+
862+//go:embed gitignore/old
863+var oldGitIgnore string
864+
865+//go:embed gitignore/default
866+var defaultGitIgnore string
internal/cmd/run.go−11
@@ -16,7 +16,6 @@ import (
16 "github.com/charmbracelet/crush/internal/config"
16 "github.com/charmbracelet/crush/internal/config"
17 "github.com/charmbracelet/crush/internal/event"
17 "github.com/charmbracelet/crush/internal/event"
18 "github.com/charmbracelet/crush/internal/format"
18 "github.com/charmbracelet/crush/internal/format"
19− "github.com/charmbracelet/crush/internal/herdr"
20 "github.com/charmbracelet/crush/internal/proto"
19 "github.com/charmbracelet/crush/internal/proto"
21 "github.com/charmbracelet/crush/internal/pubsub"
20 "github.com/charmbracelet/crush/internal/pubsub"
22 "github.com/charmbracelet/crush/internal/session"
21 "github.com/charmbracelet/crush/internal/session"
@@ -263,11 +262,6 @@ func runNonInteractive(
263 read: make(map[string]int),
262 read: make(map[string]int),
264 }
263 }
265
264
266− // Start herdr integration when running inside a herdr pane.
267− hc := herdr.Init()
268− hc.SetSessionID(sess.ID)
269− defer hc.Close()
270−
271 defer func() {
265 defer func() {
272 if progress && stderrTTY {
266 if progress && stderrTTY {
273 _, _ = fmt.Fprintf(os.Stderr, ansi.ResetProgressBar)
267 _, _ = fmt.Fprintf(os.Stderr, ansi.ResetProgressBar)
@@ -287,11 +281,6 @@ func runNonInteractive(
287 return nil
281 return nil
288 }
282 }
289
283
290− // Forward events to herdr if running inside a herdr pane.
291− if hev := herdr.Translate(ev); hev != nil {
292− hc.HandleEvent(hev)
293− }
294−
295 done, err := stream.handle(ev, stopSpinner)
284 done, err := stream.handle(ev, stopSpinner)
296 if err != nil {
285 if err != nil {
297 return err
286 return err
internal/cmd/session.go+1−1
@@ -485,7 +485,7 @@ func outputSessionHuman(ctx context.Context, cfg *config.ConfigStore, sess sessi
485
485
486 first := true
486 first := true
487 for _, msg := range msgs {
487 for _, msg := range msgs {
488− items := chat.ExtractMessageItems(&styles, msg, toolResults, "")
488+ items := chat.ExtractMessageItems(&styles, msg, toolResults)
489 for _, item := range items {
489 for _, item := range items {
490 if !first {
490 if !first {
491 fmt.Fprintln(&buf)
491 fmt.Fprintln(&buf)
internal/config/config.go+2−5
@@ -221,11 +221,8 @@ type LSPConfig struct {
221type TUIOptions struct {
221type TUIOptions struct {
222 CompactMode bool `json:"compact_mode,omitempty" jsonschema:"description=Enable compact mode for the TUI interface,default=false"`
222 CompactMode bool `json:"compact_mode,omitempty" jsonschema:"description=Enable compact mode for the TUI interface,default=false"`
223 DiffMode string `json:"diff_mode,omitempty" jsonschema:"description=Diff mode for the TUI interface,enum=unified,enum=split"`
223 DiffMode string `json:"diff_mode,omitempty" jsonschema:"description=Diff mode for the TUI interface,enum=unified,enum=split"`
224− DiffTool string `json:"diff_tool,omitempty" jsonschema:"description=External diff tool for edit diffs in the TUI,enum=builtin,enum=difftastic,default=builtin"`
224+ // Here we can add themes later or any TUI related options
225− // Theme selects the TUI color scheme. "auto" (the default) picks light
225+ //
226− // or dark based on the detected terminal background; "dark"/"light"
227− // force a specific scheme.
228− Theme string `json:"theme,omitempty" jsonschema:"description=TUI color scheme,enum=auto,enum=dark,enum=light,default=auto"`
229
226
230 Completions Completions `json:"completions,omitzero" jsonschema:"description=Completions UI options"`
227 Completions Completions `json:"completions,omitzero" jsonschema:"description=Completions UI options"`
231 Transparent *bool `json:"transparent,omitempty" jsonschema:"description=Enable transparent background for the TUI interface,default=false"`
228 Transparent *bool `json:"transparent,omitempty" jsonschema:"description=Enable transparent background for the TUI interface,default=false"`
internal/config/hyper.go+1−1
@@ -106,7 +106,7 @@ func (r realHyperClient) Get(ctx context.Context, etag string) (catwalk.Provider
106 if err != nil {
106 if err != nil {
107 return result, fmt.Errorf("failed to make request: %w", err)
107 return result, fmt.Errorf("failed to make request: %w", err)
108 }
108 }
109− defer resp.Body.Close()
109+ defer resp.Body.Close() //nolint:errcheck
110
110
111 if resp.StatusCode == http.StatusNotModified {
111 if resp.StatusCode == http.StatusNotModified {
112 return result, catwalk.ErrNotModified
112 return result, catwalk.ErrNotModified
internal/config/load.go+1−17
@@ -292,9 +292,7 @@ func (c *Config) configureProviders(ctx context.Context, store *ConfigStore, env
292 // state is kept consistent by the Providers.Del call below; any
292 // state is kept consistent by the Providers.Del call below; any
293 // concurrent reload that races with this write will also see the
293 // concurrent reload that races with this write will also see the
294 // removal because it re-reads from disk.
294 // removal because it re-reads from disk.
295− if err := store.RemoveConfigField(ScopeGlobal, "providers.anthropic"); err != nil {
295+ store.RemoveConfigField(ScopeGlobal, "providers.anthropic")
296− slog.Warn("failed to remove anthropic provider config field", "error", err)
297− }
298 c.Providers.Del(string(p.ID))
296 c.Providers.Del(string(p.ID))
299 continue
297 continue
300 case p.ID == catwalk.InferenceProviderCopilot && config.OAuthToken != nil:
298 case p.ID == catwalk.InferenceProviderCopilot && config.OAuthToken != nil:
@@ -971,26 +969,12 @@ func hasAWSCredentials(env env.Env) bool {
971 return false
969 return false
972}
970}
973
971
974−// migrateDisableNotificationsOnce guards migrateDisableNotifications so the
975−// one-shot migration for the removed disable_notifications field runs at most
976−// once per process. Without this, every config reload re-read both config
977−// files and re-ran gjson scans over them even though the migration is only
978−// meaningful on first run.
979−var migrateDisableNotificationsOnce sync.Once
980−
981// migrateDisableNotifications migrates the deprecated disable_notifications
972// migrateDisableNotifications migrates the deprecated disable_notifications
982// field to notification_style. It checks both the user config (~/.config) and
973// field to notification_style. It checks both the user config (~/.config) and
983// data config (~/.local) files. If disable_notifications is true, it sets
974// data config (~/.local) files. If disable_notifications is true, it sets
984// notification_style to "disabled" in the data file. Regardless of value, it
975// notification_style to "disabled" in the data file. Regardless of value, it
985// removes disable_notifications from any file that contains it.
976// removes disable_notifications from any file that contains it.
986−//
987−// It is safe to call from both Load and the reload path: the actual work runs
988−// at most once per process via migrateDisableNotificationsOnce.
989func migrateDisableNotifications() {
977func migrateDisableNotifications() {
990− migrateDisableNotificationsOnce.Do(migrateDisableNotificationsImpl)
991−}
992−
993−func migrateDisableNotificationsImpl() {
994 globalConfig := GlobalConfig()
978 globalConfig := GlobalConfig()
995 dataConfig := GlobalConfigData()
979 dataConfig := GlobalConfigData()
996
980
internal/config/resolve.go−11
@@ -3,7 +3,6 @@ package config
3import (
3import (
4 "context"
4 "context"
5 "fmt"
5 "fmt"
6− "strings"
7 "time"
6 "time"
8
7
9 "github.com/charmbracelet/crush/internal/env"
8 "github.com/charmbracelet/crush/internal/env"
@@ -95,16 +94,6 @@ func (r *shellVariableResolver) ResolveValue(value string) (string, error) {
95 return "", fmt.Errorf("invalid value format: %s", value)
94 return "", fmt.Errorf("invalid value format: %s", value)
96 }
95 }
97
96
98− // Fast path: a value with no shell-expansion metacharacters expands to
99− // itself, so we can skip materialising the process environment via
100− // r.env.Env() (which copies all of os.Environ()) and the whole shell
101− // round-trip. The metacharacter set mirrors shell.ExpandValue's own
102− // fast-path check ($, backtick, backslash, and both quote styles); any
103− // value lacking all of them is a literal and cannot expand.
104− if !strings.ContainsAny(value, "$`\\'\"") {
105− return value, nil
106− }
107−
108 ctx, cancel := context.WithTimeout(context.Background(), resolveTimeout)
97 ctx, cancel := context.WithTimeout(context.Background(), resolveTimeout)
109 defer cancel()
98 defer cancel()
110
99
internal/config/store.go+18−41
@@ -133,28 +133,19 @@ func (s *ConfigStore) WorkingDir() string {
133
133
134// Resolver returns the variable resolver.
134// Resolver returns the variable resolver.
135func (s *ConfigStore) Resolver() VariableResolver {
135func (s *ConfigStore) Resolver() VariableResolver {
136− s.configMu.RLock()
137− defer s.configMu.RUnlock()
138 return s.resolver
136 return s.resolver
139}
137}
140
138
141// Resolve resolves a variable reference using the configured resolver.
139// Resolve resolves a variable reference using the configured resolver.
142func (s *ConfigStore) Resolve(key string) (string, error) {
140func (s *ConfigStore) Resolve(key string) (string, error) {
143− // Snapshot the resolver under the lock, then resolve without it:
144− // ResolveValue may be slow and can call back into the store.
145− s.configMu.RLock()
146− resolver := s.resolver
147− s.configMu.RUnlock()
148− if resolver == nil {
141+ if s.resolver == nil {
149 return "", fmt.Errorf("no variable resolver configured")
142 return "", fmt.Errorf("no variable resolver configured")
150 }
143 }
151− return resolver.ResolveValue(key)
144+ return s.resolver.ResolveValue(key)
152}
145}
153
146
154// KnownProviders returns the list of known providers.
147// KnownProviders returns the list of known providers.
155func (s *ConfigStore) KnownProviders() []catwalk.Provider {
148func (s *ConfigStore) KnownProviders() []catwalk.Provider {
156− s.configMu.RLock()
157− defer s.configMu.RUnlock()
158 return s.knownProviders
149 return s.knownProviders
159}
150}
160
151
@@ -170,8 +161,6 @@ func (s *ConfigStore) Overrides() *RuntimeOverrides {
170
161
171// LoadedPaths returns the config file paths that were successfully loaded.
162// LoadedPaths returns the config file paths that were successfully loaded.
172func (s *ConfigStore) LoadedPaths() []string {
163func (s *ConfigStore) LoadedPaths() []string {
173− s.configMu.RLock()
174− defer s.configMu.RUnlock()
175 return slices.Clone(s.loadedPaths)
164 return slices.Clone(s.loadedPaths)
176}
165}
177
166
@@ -244,13 +233,10 @@ func (s *ConfigStore) atomicWrite(scope Scope, fn func(current []byte) ([]byte,
244func (s *ConfigStore) configPath(scope Scope) (string, error) {
233func (s *ConfigStore) configPath(scope Scope) (string, error) {
245 switch scope {
234 switch scope {
246 case ScopeWorkspace:
235 case ScopeWorkspace:
247− s.configMu.RLock()
248− workspacePath := s.workspacePath
249− s.configMu.RUnlock()
250− if workspacePath == "" {
236+ if s.workspacePath == "" {
251 return "", ErrNoWorkspaceConfig
237 return "", ErrNoWorkspaceConfig
252 }
238 }
253− return workspacePath, nil
239+ return s.workspacePath, nil
254 default:
240 default:
255 return s.globalDataPath, nil
241 return s.globalDataPath, nil
256 }
242 }
@@ -498,11 +484,8 @@ func (s *ConfigStore) SetProviderAPIKey(scope Scope, providerID string, apiKey a
498 return nil
484 return nil
499 }
485 }
500
486
501− s.configMu.RLock()
502− knownProviders := s.knownProviders
503− s.configMu.RUnlock()
504 var foundProvider *catwalk.Provider
487 var foundProvider *catwalk.Provider
505− for _, p := range knownProviders {
488+ for _, p := range s.knownProviders {
506 if string(p.ID) == providerID {
489 if string(p.ID) == providerID {
507 foundProvider = &p
490 foundProvider = &p
508 break
491 break
@@ -777,7 +760,7 @@ func (s *ConfigStore) ImportCopilot() (*oauth.Token, bool) {
777 }
760 }
778
761
779 slog.Info("Found existing GitHub Copilot token on disk. Authenticating...")
762 slog.Info("Found existing GitHub Copilot token on disk. Authenticating...")
780− token, err := copilot.RefreshToken(context.Background(), diskToken)
763+ token, err := copilot.RefreshToken(context.TODO(), diskToken)
781 if err != nil {
764 if err != nil {
782 slog.Error("Unable to import GitHub Copilot token", "error", err)
765 slog.Error("Unable to import GitHub Copilot token", "error", err)
783 return nil, false
766 return nil, false
@@ -902,11 +885,8 @@ func (s *ConfigStore) CaptureStalenessSnapshot(paths []string) {
902 }
885 }
903
886
904 // Also track workspace and global config paths if set
887 // Also track workspace and global config paths if set
905− s.configMu.RLock()
906− workspacePath := s.workspacePath
907− s.configMu.RUnlock()
908− if workspacePath != "" {
888+ if s.workspacePath != "" {
909− abs, err := filepath.Abs(workspacePath)
889+ abs, err := filepath.Abs(s.workspacePath)
910 if err == nil {
890 if err == nil {
911 seen[abs] = struct{}{}
891 seen[abs] = struct{}{}
912 }
892 }
@@ -926,9 +906,7 @@ func (s *ConfigStore) CaptureStalenessSnapshot(paths []string) {
926 slices.Sort(s.trackedConfigPaths)
906 slices.Sort(s.trackedConfigPaths)
927
907
928 // Capture initial snapshots
908 // Capture initial snapshots
929− if err := s.RefreshStalenessSnapshot(); err != nil {
909+ s.RefreshStalenessSnapshot()
930− slog.Warn("failed to refresh staleness snapshot", "error", err)
931− }
932}
910}
933
911
934// captureStalenessSnapshot is an alias for CaptureStalenessSnapshot for internal use.
912// captureStalenessSnapshot is an alias for CaptureStalenessSnapshot for internal use.
@@ -988,6 +966,9 @@ func (s *ConfigStore) reloadFromDiskLocked(ctx context.Context) error {
988 return fmt.Errorf("invalid hook configuration on reload: %w", err)
966 return fmt.Errorf("invalid hook configuration on reload: %w", err)
989 }
967 }
990
968
969+ // Preserve runtime overrides
970+ overrides := s.overrides
971+
991 // Reconfigure providers
972 // Reconfigure providers
992 env := env.New()
973 env := env.New()
993 resolver := NewShellVariableResolver(env)
974 resolver := NewShellVariableResolver(env)
@@ -1005,19 +986,16 @@ func (s *ConfigStore) reloadFromDiskLocked(ctx context.Context) error {
1005 oldLoadedPaths := s.loadedPaths
986 oldLoadedPaths := s.loadedPaths
1006 oldResolver := s.resolver
987 oldResolver := s.resolver
1007 oldKnownProviders := s.knownProviders
988 oldKnownProviders := s.knownProviders
989+ oldOverrides := s.overrides
1008 oldWorkspacePath := s.workspacePath
990 oldWorkspacePath := s.workspacePath
1009
991
1010− // Update store state BEFORE running model/agent setup (so they see new
992+ // Update store state BEFORE running model/agent setup (so they see new config)
1011− // config). All reload-mutated fields are published atomically under
993+ s.setConfig(cfg)
1012− // configMu so the lock-free public readers (Resolver, KnownProviders,
1013− // LoadedPaths, configPath) never observe a torn update.
1014− s.configMu.Lock()
1015− s.config = cfg
1016 s.loadedPaths = loadedPaths
994 s.loadedPaths = loadedPaths
1017 s.resolver = resolver
995 s.resolver = resolver
1018 s.knownProviders = providers
996 s.knownProviders = providers
997+ s.overrides = overrides
1019 s.workspacePath = workspacePath
998 s.workspacePath = workspacePath
1020− s.configMu.Unlock()
1021
999
1022 // Mirror startup flow: setup models and agents against NEW config.
1000 // Mirror startup flow: setup models and agents against NEW config.
1023 var setupErr error
1001 var setupErr error
@@ -1036,13 +1014,12 @@ func (s *ConfigStore) reloadFromDiskLocked(ctx context.Context) error {
1036
1014
1037 // Rollback on setup failure
1015 // Rollback on setup failure
1038 if setupErr != nil {
1016 if setupErr != nil {
1039− s.configMu.Lock()
1040− s.config = oldConfig
1017+ s.setConfig(oldConfig)
1041 s.loadedPaths = oldLoadedPaths
1018 s.loadedPaths = oldLoadedPaths
1042 s.resolver = oldResolver
1019 s.resolver = oldResolver
1043 s.knownProviders = oldKnownProviders
1020 s.knownProviders = oldKnownProviders
1021+ s.overrides = oldOverrides
1044 s.workspacePath = oldWorkspacePath
1022 s.workspacePath = oldWorkspacePath
1045− s.configMu.Unlock()
1046 return setupErr
1023 return setupErr
1047 }
1024 }
1048
1025
internal/csync/maps.go+5−20
@@ -75,30 +75,15 @@ func (m *Map[K, V]) Len() int {
75 return len(m.inner)
75 return len(m.inner)
76}
76}
77
77
78−// GetOrSet gets and returns the value for key if it exists, otherwise it
78+// GetOrSet gets and returns the key if it exists, otherwise, it executes the
79−// executes fn, stores its return value for key, and returns it.
79+// given function, set its return value for the given key, and returns it.
80−//
81−// The stored value and the returned value are always the same: if two
82−// goroutines race on a missing key, exactly one fn result is stored and
83−// every caller receives that stored value (the losers discard their own
84−// fn result). This keeps singleton values (a mutex, a client, a cache
85−// entry others mutate) consistent. fn may run more than once under
86−// contention, so it must be side-effect-free and safe to call redundantly.
87func (m *Map[K, V]) GetOrSet(key K, fn func() V) V {
80func (m *Map[K, V]) GetOrSet(key K, fn func() V) V {
88− if got, ok := m.Get(key); ok {
81+ got, ok := m.Get(key)
82+ if ok {
89 return got
83 return got
90 }
84 }
91− // Compute outside the lock so a slow fn (I/O, parsing) does not
92− // serialize other map users.
93 value := fn()
85 value := fn()
94− m.mu.Lock()
86+ m.Set(key, value)
95− defer m.mu.Unlock()
96− // Re-check under the write lock: another goroutine may have won the
97− // race between our Get above and acquiring the lock.
98− if got, ok := m.inner[key]; ok {
99− return got
100− }
101− m.inner[key] = value
102 return value
87 return value
103}
88}
104
89
internal/db/db.go+4−24
@@ -1,6 +1,6 @@
1// Code generated by sqlc. DO NOT EDIT.
1// Code generated by sqlc. DO NOT EDIT.
2// versions:
2// versions:
3−// sqlc v1.31.1
3+// sqlc v1.30.0
4
4
5package db
5package db
6
6
@@ -11,10 +11,10 @@ import (
11)
11)
12
12
13type DBTX interface {
13type DBTX interface {
14− ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
14+ ExecContext(context.Context, string, ...any) (sql.Result, error)
15 PrepareContext(context.Context, string) (*sql.Stmt, error)
15 PrepareContext(context.Context, string) (*sql.Stmt, error)
16− QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
16+ QueryContext(context.Context, string, ...any) (*sql.Rows, error)
17− QueryRowContext(context.Context, string, ...interface{}) *sql.Row
17+ QueryRowContext(context.Context, string, ...any) *sql.Row
18}
18}
19
19
20func New(db DBTX) *Queries {
20func New(db DBTX) *Queries {
@@ -66,9 +66,6 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
66 if q.getLastSessionStmt, err = db.PrepareContext(ctx, getLastSession); err != nil {
66 if q.getLastSessionStmt, err = db.PrepareContext(ctx, getLastSession); err != nil {
67 return nil, fmt.Errorf("error preparing query GetLastSession: %w", err)
67 return nil, fmt.Errorf("error preparing query GetLastSession: %w", err)
68 }
68 }
69− if q.getMaxFileVersionByPathStmt, err = db.PrepareContext(ctx, getMaxFileVersionByPath); err != nil {
70− return nil, fmt.Errorf("error preparing query GetMaxFileVersionByPath: %w", err)
71− }
72 if q.getMessageStmt, err = db.PrepareContext(ctx, getMessage); err != nil {
69 if q.getMessageStmt, err = db.PrepareContext(ctx, getMessage); err != nil {
73 return nil, fmt.Errorf("error preparing query GetMessage: %w", err)
70 return nil, fmt.Errorf("error preparing query GetMessage: %w", err)
74 }
71 }
@@ -111,9 +108,6 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
111 if q.listMessagesBySessionStmt, err = db.PrepareContext(ctx, listMessagesBySession); err != nil {
108 if q.listMessagesBySessionStmt, err = db.PrepareContext(ctx, listMessagesBySession); err != nil {
112 return nil, fmt.Errorf("error preparing query ListMessagesBySession: %w", err)
109 return nil, fmt.Errorf("error preparing query ListMessagesBySession: %w", err)
113 }
110 }
114− if q.listMessagesBySessionAfterStmt, err = db.PrepareContext(ctx, listMessagesBySessionAfter); err != nil {
115− return nil, fmt.Errorf("error preparing query ListMessagesBySessionAfter: %w", err)
116− }
117 if q.listNewFilesStmt, err = db.PrepareContext(ctx, listNewFiles); err != nil {
111 if q.listNewFilesStmt, err = db.PrepareContext(ctx, listNewFiles); err != nil {
118 return nil, fmt.Errorf("error preparing query ListNewFiles: %w", err)
112 return nil, fmt.Errorf("error preparing query ListNewFiles: %w", err)
119 }
113 }
@@ -216,11 +210,6 @@ func (q *Queries) Close() error {
216 err = fmt.Errorf("error closing getLastSessionStmt: %w", cerr)
210 err = fmt.Errorf("error closing getLastSessionStmt: %w", cerr)
217 }
211 }
218 }
212 }
219− if q.getMaxFileVersionByPathStmt != nil {
220− if cerr := q.getMaxFileVersionByPathStmt.Close(); cerr != nil {
221− err = fmt.Errorf("error closing getMaxFileVersionByPathStmt: %w", cerr)
222− }
223− }
224 if q.getMessageStmt != nil {
213 if q.getMessageStmt != nil {
225 if cerr := q.getMessageStmt.Close(); cerr != nil {
214 if cerr := q.getMessageStmt.Close(); cerr != nil {
226 err = fmt.Errorf("error closing getMessageStmt: %w", cerr)
215 err = fmt.Errorf("error closing getMessageStmt: %w", cerr)
@@ -291,11 +280,6 @@ func (q *Queries) Close() error {
291 err = fmt.Errorf("error closing listMessagesBySessionStmt: %w", cerr)
280 err = fmt.Errorf("error closing listMessagesBySessionStmt: %w", cerr)
292 }
281 }
293 }
282 }
294− if q.listMessagesBySessionAfterStmt != nil {
295− if cerr := q.listMessagesBySessionAfterStmt.Close(); cerr != nil {
296− err = fmt.Errorf("error closing listMessagesBySessionAfterStmt: %w", cerr)
297− }
298− }
299 if q.listNewFilesStmt != nil {
283 if q.listNewFilesStmt != nil {
300 if cerr := q.listNewFilesStmt.Close(); cerr != nil {
284 if cerr := q.listNewFilesStmt.Close(); cerr != nil {
301 err = fmt.Errorf("error closing listNewFilesStmt: %w", cerr)
285 err = fmt.Errorf("error closing listNewFilesStmt: %w", cerr)
@@ -394,7 +378,6 @@ type Queries struct {
394 getFileReadStmt *sql.Stmt
378 getFileReadStmt *sql.Stmt
395 getHourDayHeatmapStmt *sql.Stmt
379 getHourDayHeatmapStmt *sql.Stmt
396 getLastSessionStmt *sql.Stmt
380 getLastSessionStmt *sql.Stmt
397− getMaxFileVersionByPathStmt *sql.Stmt
398 getMessageStmt *sql.Stmt
381 getMessageStmt *sql.Stmt
399 getRecentActivityStmt *sql.Stmt
382 getRecentActivityStmt *sql.Stmt
400 getSessionByIDStmt *sql.Stmt
383 getSessionByIDStmt *sql.Stmt
@@ -409,7 +392,6 @@ type Queries struct {
409 listFilesBySessionStmt *sql.Stmt
392 listFilesBySessionStmt *sql.Stmt
410 listLatestSessionFilesStmt *sql.Stmt
393 listLatestSessionFilesStmt *sql.Stmt
411 listMessagesBySessionStmt *sql.Stmt
394 listMessagesBySessionStmt *sql.Stmt
412− listMessagesBySessionAfterStmt *sql.Stmt
413 listNewFilesStmt *sql.Stmt
395 listNewFilesStmt *sql.Stmt
414 listSessionReadFilesStmt *sql.Stmt
396 listSessionReadFilesStmt *sql.Stmt
415 listSessionsStmt *sql.Stmt
397 listSessionsStmt *sql.Stmt
@@ -439,7 +421,6 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
439 getFileReadStmt: q.getFileReadStmt,
421 getFileReadStmt: q.getFileReadStmt,
440 getHourDayHeatmapStmt: q.getHourDayHeatmapStmt,
422 getHourDayHeatmapStmt: q.getHourDayHeatmapStmt,
441 getLastSessionStmt: q.getLastSessionStmt,
423 getLastSessionStmt: q.getLastSessionStmt,
442− getMaxFileVersionByPathStmt: q.getMaxFileVersionByPathStmt,
443 getMessageStmt: q.getMessageStmt,
424 getMessageStmt: q.getMessageStmt,
444 getRecentActivityStmt: q.getRecentActivityStmt,
425 getRecentActivityStmt: q.getRecentActivityStmt,
445 getSessionByIDStmt: q.getSessionByIDStmt,
426 getSessionByIDStmt: q.getSessionByIDStmt,
@@ -454,7 +435,6 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
454 listFilesBySessionStmt: q.listFilesBySessionStmt,
435 listFilesBySessionStmt: q.listFilesBySessionStmt,
455 listLatestSessionFilesStmt: q.listLatestSessionFilesStmt,
436 listLatestSessionFilesStmt: q.listLatestSessionFilesStmt,
456 listMessagesBySessionStmt: q.listMessagesBySessionStmt,
437 listMessagesBySessionStmt: q.listMessagesBySessionStmt,
457− listMessagesBySessionAfterStmt: q.listMessagesBySessionAfterStmt,
458 listNewFilesStmt: q.listNewFilesStmt,
438 listNewFilesStmt: q.listNewFilesStmt,
459 listSessionReadFilesStmt: q.listSessionReadFilesStmt,
439 listSessionReadFilesStmt: q.listSessionReadFilesStmt,
460 listSessionsStmt: q.listSessionsStmt,
440 listSessionsStmt: q.listSessionsStmt,
internal/db/files.sql.go+5−21
@@ -1,6 +1,6 @@
1// Code generated by sqlc. DO NOT EDIT.
1// Code generated by sqlc. DO NOT EDIT.
2// versions:
2// versions:
3−// sqlc v1.31.1
3+// sqlc v1.30.0
4// source: files.sql
4// source: files.sql
5
5
6package db
6package db
@@ -122,21 +122,6 @@ func (q *Queries) GetFileByPathAndSession(ctx context.Context, arg GetFileByPath
122 return i, err
122 return i, err
123}
123}
124
124
125−const getMaxFileVersionByPath = `-- name: GetMaxFileVersionByPath :one
126−SELECT version
127−FROM files
128−WHERE path = ?
129−ORDER BY version DESC, created_at DESC
130−LIMIT 1
131−`
132−
133−func (q *Queries) GetMaxFileVersionByPath(ctx context.Context, path string) (int64, error) {
134− row := q.queryRow(ctx, q.getMaxFileVersionByPathStmt, getMaxFileVersionByPath, path)
135− var version int64
136− err := row.Scan(&version)
137− return version, err
138−}
139−
140const listFilesByPath = `-- name: ListFilesByPath :many
125const listFilesByPath = `-- name: ListFilesByPath :many
141SELECT id, session_id, path, content, version, created_at, updated_at
126SELECT id, session_id, path, content, version, created_at, updated_at
142FROM files
127FROM files
@@ -217,12 +202,11 @@ const listLatestSessionFiles = `-- name: ListLatestSessionFiles :many
217SELECT f.id, f.session_id, f.path, f.content, f.version, f.created_at, f.updated_at
202SELECT f.id, f.session_id, f.path, f.content, f.version, f.created_at, f.updated_at
218FROM files f
203FROM files f
219INNER JOIN (
204INNER JOIN (
220− SELECT f2.path, MAX(f2.version) as max_version, MAX(f2.created_at) as max_created_at
205+ SELECT path, MAX(version) as max_version, MAX(created_at) as max_created_at
221− FROM files f2
206+ FROM files
222− WHERE f2.session_id = ?1
223− GROUP BY f2.path
207+ GROUP BY path
224) latest ON f.path = latest.path AND f.version = latest.max_version AND f.created_at = latest.max_created_at
208) latest ON f.path = latest.path AND f.version = latest.max_version AND f.created_at = latest.max_created_at
225−WHERE f.session_id = ?1
209+WHERE f.session_id = ?
226ORDER BY f.path
210ORDER BY f.path
227`
211`
228
212
internal/db/messages.sql.go+2−50
@@ -1,6 +1,6 @@
1// Code generated by sqlc. DO NOT EDIT.
1// Code generated by sqlc. DO NOT EDIT.
2// versions:
2// versions:
3−// sqlc v1.31.1
3+// sqlc v1.30.0
4// source: messages.sql
4// source: messages.sql
5
5
6package db
6package db
@@ -38,8 +38,7 @@ type CreateMessageParams struct {
38}
38}
39
39
40func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error) {
40func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error) {
41− row := q.queryRow(
42− ctx, q.createMessageStmt, createMessage,
41+ row := q.queryRow(ctx, q.createMessageStmt, createMessage,
43 arg.ID,
42 arg.ID,
44 arg.SessionID,
43 arg.SessionID,
45 arg.Role,
44 arg.Role,
@@ -190,53 +189,6 @@ func (q *Queries) ListMessagesBySession(ctx context.Context, sessionID string) (
190 return items, nil
189 return items, nil
191}
190}
192
191
193−const listMessagesBySessionAfter = `-- name: ListMessagesBySessionAfter :many
194−SELECT m.id, m.session_id, m.role, m.parts, m.model, m.created_at, m.updated_at, m.finished_at, m.provider, m.is_summary_message
195−FROM messages m
196−WHERE m.session_id = ?
197− AND m.created_at >= (SELECT m2.created_at FROM messages m2 WHERE m2.id = ?)
198−ORDER BY m.created_at ASC
199−`
200−
201−type ListMessagesBySessionAfterParams struct {
202− SessionID string `json:"session_id"`
203− ID string `json:"id"`
204−}
205−
206−func (q *Queries) ListMessagesBySessionAfter(ctx context.Context, arg ListMessagesBySessionAfterParams) ([]Message, error) {
207− rows, err := q.query(ctx, q.listMessagesBySessionAfterStmt, listMessagesBySessionAfter, arg.SessionID, arg.ID)
208− if err != nil {
209− return nil, err
210− }
211− defer rows.Close()
212− items := []Message{}
213− for rows.Next() {
214− var i Message
215− if err := rows.Scan(
216− &i.ID,
217− &i.SessionID,
218− &i.Role,
219− &i.Parts,
220− &i.Model,
221− &i.CreatedAt,
222− &i.UpdatedAt,
223− &i.FinishedAt,
224− &i.Provider,
225− &i.IsSummaryMessage,
226− ); err != nil {
227− return nil, err
228− }
229− items = append(items, i)
230− }
231− if err := rows.Close(); err != nil {
232− return nil, err
233− }
234− if err := rows.Err(); err != nil {
235− return nil, err
236− }
237− return items, nil
238−}
239−
240const listUserMessagesBySession = `-- name: ListUserMessagesBySession :many
192const listUserMessagesBySession = `-- name: ListUserMessagesBySession :many
241SELECT id, session_id, role, parts, model, created_at, updated_at, finished_at, provider, is_summary_message
193SELECT id, session_id, role, parts, model, created_at, updated_at, finished_at, provider, is_summary_message
242FROM messages
194FROM messages
internal/db/migrations/20260701000000_add_composite_indexes.sqldeleted−12
@@ -1,12 +0,0 @@
1−-- +goose Up
2−-- +goose StatementBegin
3−-- Add composite indexes to speed up per-session ordered lookups.
4−CREATE INDEX IF NOT EXISTS idx_messages_session_created ON messages (session_id, created_at);
5−CREATE INDEX IF NOT EXISTS idx_files_session_version_created ON files (session_id, version, created_at);
6−-- +goose StatementEnd
7−
8−-- +goose Down
9−-- +goose StatementBegin
10−DROP INDEX IF EXISTS idx_messages_session_created;
11−DROP INDEX IF EXISTS idx_files_session_version_created;
12−-- +goose StatementEnd
internal/db/migrations/20260702000000_add_hot_path_indexes.sqldeleted−14
@@ -1,14 +0,0 @@
1−-- +goose Up
2−-- +goose StatementBegin
3−-- Add indexes for hot-path queries that previously required full table scans.
4−CREATE INDEX IF NOT EXISTS idx_sessions_updated_at ON sessions (updated_at DESC);
5−CREATE INDEX IF NOT EXISTS idx_messages_role_created ON messages (role, created_at DESC);
6−CREATE INDEX IF NOT EXISTS idx_files_path_version ON files (path, version DESC);
7−-- +goose StatementEnd
8−
9−-- +goose Down
10−-- +goose StatementBegin
11−DROP INDEX IF EXISTS idx_sessions_updated_at;
12−DROP INDEX IF EXISTS idx_messages_role_created;
13−DROP INDEX IF EXISTS idx_files_path_version;
14−-- +goose StatementEnd
internal/db/models.go+1−1
@@ -1,6 +1,6 @@
1// Code generated by sqlc. DO NOT EDIT.
1// Code generated by sqlc. DO NOT EDIT.
2// versions:
2// versions:
3−// sqlc v1.31.1
3+// sqlc v1.30.0
4
4
5package db
5package db
6
6
internal/db/querier.go+3−5
@@ -1,6 +1,6 @@
1// Code generated by sqlc. DO NOT EDIT.
1// Code generated by sqlc. DO NOT EDIT.
2// versions:
2// versions:
3−// sqlc v1.31.1
3+// sqlc v1.30.0
4
4
5package db
5package db
6
6
@@ -23,7 +23,6 @@ type Querier interface {
23 GetFileRead(ctx context.Context, arg GetFileReadParams) (ReadFile, error)
23 GetFileRead(ctx context.Context, arg GetFileReadParams) (ReadFile, error)
24 GetHourDayHeatmap(ctx context.Context) ([]GetHourDayHeatmapRow, error)
24 GetHourDayHeatmap(ctx context.Context) ([]GetHourDayHeatmapRow, error)
25 GetLastSession(ctx context.Context) (Session, error)
25 GetLastSession(ctx context.Context) (Session, error)
26− GetMaxFileVersionByPath(ctx context.Context, path string) (int64, error)
27 GetMessage(ctx context.Context, id string) (Message, error)
26 GetMessage(ctx context.Context, id string) (Message, error)
28 GetRecentActivity(ctx context.Context) ([]GetRecentActivityRow, error)
27 GetRecentActivity(ctx context.Context) ([]GetRecentActivityRow, error)
29 GetSessionByID(ctx context.Context, id string) (Session, error)
28 GetSessionByID(ctx context.Context, id string) (Session, error)
@@ -38,16 +37,15 @@ type Querier interface {
38 ListFilesBySession(ctx context.Context, sessionID string) ([]File, error)
37 ListFilesBySession(ctx context.Context, sessionID string) ([]File, error)
39 ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error)
38 ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error)
40 ListMessagesBySession(ctx context.Context, sessionID string) ([]Message, error)
39 ListMessagesBySession(ctx context.Context, sessionID string) ([]Message, error)
41− ListMessagesBySessionAfter(ctx context.Context, arg ListMessagesBySessionAfterParams) ([]Message, error)
42 ListNewFiles(ctx context.Context) ([]File, error)
40 ListNewFiles(ctx context.Context) ([]File, error)
43 ListSessionReadFiles(ctx context.Context, sessionID string) ([]ReadFile, error)
41 ListSessionReadFiles(ctx context.Context, sessionID string) ([]ReadFile, error)
44 ListSessions(ctx context.Context) ([]Session, error)
42 ListSessions(ctx context.Context) ([]Session, error)
45 ListUserMessagesBySession(ctx context.Context, sessionID string) ([]Message, error)
43 ListUserMessagesBySession(ctx context.Context, sessionID string) ([]Message, error)
46 RecordFileRead(ctx context.Context, arg RecordFileReadParams) error
44 RecordFileRead(ctx context.Context, arg RecordFileReadParams) error
47− RenameSession(ctx context.Context, arg RenameSessionParams) (Session, error)
45+ RenameSession(ctx context.Context, arg RenameSessionParams) error
48 UpdateMessage(ctx context.Context, arg UpdateMessageParams) error
46 UpdateMessage(ctx context.Context, arg UpdateMessageParams) error
49 UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error)
47 UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error)
50− UpdateSessionTitleAndUsage(ctx context.Context, arg UpdateSessionTitleAndUsageParams) (Session, error)
48+ UpdateSessionTitleAndUsage(ctx context.Context, arg UpdateSessionTitleAndUsageParams) error
51}
49}
52
50
53var _ Querier = (*Queries)(nil)
51var _ Querier = (*Queries)(nil)
internal/db/read_files.sql.go+1−1
@@ -1,6 +1,6 @@
1// Code generated by sqlc. DO NOT EDIT.
1// Code generated by sqlc. DO NOT EDIT.
2// versions:
2// versions:
3−// sqlc v1.31.1
3+// sqlc v1.30.0
4// source: read_files.sql
4// source: read_files.sql
5
5
6package db
6package db
internal/db/sessions.sql.go+9−39
@@ -1,6 +1,6 @@
1// Code generated by sqlc. DO NOT EDIT.
1// Code generated by sqlc. DO NOT EDIT.
2// versions:
2// versions:
3−// sqlc v1.31.1
3+// sqlc v1.30.0
4// source: sessions.sql
4// source: sessions.sql
5
5
6package db
6package db
@@ -176,12 +176,11 @@ func (q *Queries) ListSessions(ctx context.Context) ([]Session, error) {
176 return items, nil
176 return items, nil
177}
177}
178
178
179−const renameSession = `-- name: RenameSession :one
179+const renameSession = `-- name: RenameSession :exec
180UPDATE sessions
180UPDATE sessions
181SET
181SET
182 title = ?
182 title = ?
183WHERE id = ?
183WHERE id = ?
184−RETURNING id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id, todos
185`
184`
186
185
187type RenameSessionParams struct {
186type RenameSessionParams struct {
@@ -189,23 +188,9 @@ type RenameSessionParams struct {
189 ID string `json:"id"`
188 ID string `json:"id"`
190}
189}
191
190
192−func (q *Queries) RenameSession(ctx context.Context, arg RenameSessionParams) (Session, error) {
191+func (q *Queries) RenameSession(ctx context.Context, arg RenameSessionParams) error {
193− row := q.queryRow(ctx, q.renameSessionStmt, renameSession, arg.Title, arg.ID)
192+ _, err := q.exec(ctx, q.renameSessionStmt, renameSession, arg.Title, arg.ID)
194− var i Session
195− err := row.Scan(
196− &i.ID,
197− &i.ParentSessionID,
198− &i.Title,
199− &i.MessageCount,
200− &i.PromptTokens,
201− &i.CompletionTokens,
202− &i.Cost,
203− &i.UpdatedAt,
204− &i.CreatedAt,
205− &i.SummaryMessageID,
206− &i.Todos,
207− )
208− return i, err
193+ return err
209}
194}
210
195
211const updateSession = `-- name: UpdateSession :one
196const updateSession = `-- name: UpdateSession :one
@@ -258,7 +243,7 @@ func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (S
258 return i, err
243 return i, err
259}
244}
260
245
261−const updateSessionTitleAndUsage = `-- name: UpdateSessionTitleAndUsage :one
246+const updateSessionTitleAndUsage = `-- name: UpdateSessionTitleAndUsage :exec
262UPDATE sessions
247UPDATE sessions
263SET
248SET
264 title = ?,
249 title = ?,
@@ -267,7 +252,6 @@ SET
267 cost = cost + ?,
252 cost = cost + ?,
268 updated_at = strftime('%s', 'now')
253 updated_at = strftime('%s', 'now')
269WHERE id = ?
254WHERE id = ?
270−RETURNING id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id, todos
271`
255`
272
256
273type UpdateSessionTitleAndUsageParams struct {
257type UpdateSessionTitleAndUsageParams struct {
@@ -278,27 +262,13 @@ type UpdateSessionTitleAndUsageParams struct {
278 ID string `json:"id"`
262 ID string `json:"id"`
279}
263}
280
264
281−func (q *Queries) UpdateSessionTitleAndUsage(ctx context.Context, arg UpdateSessionTitleAndUsageParams) (Session, error) {
265+func (q *Queries) UpdateSessionTitleAndUsage(ctx context.Context, arg UpdateSessionTitleAndUsageParams) error {
282− row := q.queryRow(ctx, q.updateSessionTitleAndUsageStmt, updateSessionTitleAndUsage,
266+ _, err := q.exec(ctx, q.updateSessionTitleAndUsageStmt, updateSessionTitleAndUsage,
283 arg.Title,
267 arg.Title,
284 arg.PromptTokens,
268 arg.PromptTokens,
285 arg.CompletionTokens,
269 arg.CompletionTokens,
286 arg.Cost,
270 arg.Cost,
287 arg.ID,
271 arg.ID,
288 )
272 )
289− var i Session
290− err := row.Scan(
291− &i.ID,
292− &i.ParentSessionID,
293− &i.Title,
294− &i.MessageCount,
295− &i.PromptTokens,
296− &i.CompletionTokens,
297− &i.Cost,
298− &i.UpdatedAt,
299− &i.CreatedAt,
300− &i.SummaryMessageID,
301− &i.Todos,
302− )
303− return i, err
273+ return err
304}
274}
internal/db/sql/files.sql+4−12
@@ -22,13 +22,6 @@ FROM files
22WHERE path = ?
22WHERE path = ?
23ORDER BY version DESC, created_at DESC;
23ORDER BY version DESC, created_at DESC;
24
24
25−-- name: GetMaxFileVersionByPath :one
26−SELECT version
27−FROM files
28−WHERE path = ?
29−ORDER BY version DESC, created_at DESC
30−LIMIT 1;
31−
32-- name: CreateFile :one
25-- name: CreateFile :one
33INSERT INTO files (
26INSERT INTO files (
34 id,
27 id,
@@ -55,12 +48,11 @@ WHERE session_id = ?;
55SELECT f.*
48SELECT f.*
56FROM files f
49FROM files f
57INNER JOIN (
50INNER JOIN (
58− SELECT f2.path, MAX(f2.version) as max_version, MAX(f2.created_at) as max_created_at
51+ SELECT path, MAX(version) as max_version, MAX(created_at) as max_created_at
59− FROM files f2
52+ FROM files
60− WHERE f2.session_id = @session_id
61− GROUP BY f2.path
53+ GROUP BY path
62) latest ON f.path = latest.path AND f.version = latest.max_version AND f.created_at = latest.max_created_at
54) latest ON f.path = latest.path AND f.version = latest.max_version AND f.created_at = latest.max_created_at
63−WHERE f.session_id = @session_id
55+WHERE f.session_id = ?
64ORDER BY f.path;
56ORDER BY f.path;
65
57
66-- name: ListNewFiles :many
58-- name: ListNewFiles :many
internal/db/sql/messages.sql−7
@@ -9,13 +9,6 @@ FROM messages
9WHERE session_id = ?
9WHERE session_id = ?
10ORDER BY created_at ASC;
10ORDER BY created_at ASC;
11
11
12−-- name: ListMessagesBySessionAfter :many
13−SELECT m.*
14−FROM messages m
15−WHERE m.session_id = ?
16− AND m.created_at >= (SELECT m2.created_at FROM messages m2 WHERE m2.id = ?)
17−ORDER BY m.created_at ASC;
18−
19-- name: CreateMessage :one
12-- name: CreateMessage :one
20INSERT INTO messages (
13INSERT INTO messages (
21 id,
14 id,
internal/db/sql/sessions.sql+4−6
@@ -52,7 +52,7 @@ SET
52WHERE id = ?
52WHERE id = ?
53RETURNING *;
53RETURNING *;
54
54
55−-- name: UpdateSessionTitleAndUsage :one
55+-- name: UpdateSessionTitleAndUsage :exec
56UPDATE sessions
56UPDATE sessions
57SET
57SET
58 title = ?,
58 title = ?,
@@ -60,16 +60,14 @@ SET
60 completion_tokens = completion_tokens + ?,
60 completion_tokens = completion_tokens + ?,
61 cost = cost + ?,
61 cost = cost + ?,
62 updated_at = strftime('%s', 'now')
62 updated_at = strftime('%s', 'now')
63−WHERE id = ?
63+WHERE id = ?;
64−RETURNING *;
65
64
66
65
67−-- name: RenameSession :one
66+-- name: RenameSession :exec
68UPDATE sessions
67UPDATE sessions
69SET
68SET
70 title = ?
69 title = ?
71−WHERE id = ?
70+WHERE id = ?;
72−RETURNING *;
73
71
74-- name: DeleteSession :exec
72-- name: DeleteSession :exec
75DELETE FROM sessions
73DELETE FROM sessions
internal/db/stats.sql.go+1−1
@@ -1,6 +1,6 @@
1// Code generated by sqlc. DO NOT EDIT.
1// Code generated by sqlc. DO NOT EDIT.
2// versions:
2// versions:
3−// sqlc v1.31.1
3+// sqlc v1.30.0
4// source: stats.sql
4// source: stats.sql
5
5
6package db
6package db
internal/event/event.go+2−15
@@ -7,7 +7,6 @@ import (
7 "path/filepath"
7 "path/filepath"
8 "reflect"
8 "reflect"
9 "runtime"
9 "runtime"
10− "sync"
11 "time"
10 "time"
12
11
13 "github.com/charmbracelet/crush/internal/version"
12 "github.com/charmbracelet/crush/internal/version"
@@ -26,10 +25,7 @@ const (
26var (
25var (
27 client posthog.Client
26 client posthog.Client
28
27
29− // basePropsMu guards baseProps: the Set* mutators reassign it during
30− // startup while send() reads it concurrently from event goroutines.
31− basePropsMu sync.RWMutex
32− baseProps = posthog.NewProperties().
28+ baseProps = posthog.NewProperties().
33 Set("GOOS", runtime.GOOS).
29 Set("GOOS", runtime.GOOS).
34 Set("GOARCH", runtime.GOARCH).
30 Set("GOARCH", runtime.GOARCH).
35 Set("TERM", os.Getenv("TERM")).
31 Set("TERM", os.Getenv("TERM")).
@@ -40,20 +36,14 @@ var (
40)
36)
41
37
42func SetNonInteractive(nonInteractive bool) {
38func SetNonInteractive(nonInteractive bool) {
43− basePropsMu.Lock()
44− defer basePropsMu.Unlock()
45 baseProps = baseProps.Set(nonInteractiveAttrName, nonInteractive)
39 baseProps = baseProps.Set(nonInteractiveAttrName, nonInteractive)
46}
40}
47
41
48func SetContinueBySessionID(continueBySessionID bool) {
42func SetContinueBySessionID(continueBySessionID bool) {
49− basePropsMu.Lock()
50− defer basePropsMu.Unlock()
51 baseProps = baseProps.Set(continueSessionByIDAttrName, continueBySessionID)
43 baseProps = baseProps.Set(continueSessionByIDAttrName, continueBySessionID)
52}
44}
53
45
54func SetContinueLastSession(continueLastSession bool) {
46func SetContinueLastSession(continueLastSession bool) {
55− basePropsMu.Lock()
56− defer basePropsMu.Unlock()
57 baseProps = baseProps.Set(continueLastSessionAttrName, continueLastSession)
47 baseProps = baseProps.Set(continueLastSessionAttrName, continueLastSession)
58}
48}
59
49
@@ -91,13 +81,10 @@ func send(event string, props ...any) {
91 if client == nil {
81 if client == nil {
92 return
82 return
93 }
83 }
94− basePropsMu.RLock()
95− properties := pairsToProps(props...).Merge(baseProps)
96− basePropsMu.RUnlock()
97 err := client.Enqueue(posthog.Capture{
84 err := client.Enqueue(posthog.Capture{
98 DistinctId: distinctId,
85 DistinctId: distinctId,
99 Event: event,
86 Event: event,
100− Properties: properties,
87+ Properties: pairsToProps(props...).Merge(baseProps),
101 })
88 })
102 if err != nil {
89 if err != nil {
103 slog.Error("Failed to enqueue PostHog event", "event", event, "props", props, "error", err)
90 slog.Error("Failed to enqueue PostHog event", "event", event, "props", props, "error", err)
internal/filetracker/service.go+2−19
@@ -7,7 +7,6 @@ import (
7 "log/slog"
7 "log/slog"
8 "os"
8 "os"
9 "path/filepath"
9 "path/filepath"
10− "sync"
11 "time"
10 "time"
12
11
13 "github.com/charmbracelet/crush/internal/db"
12 "github.com/charmbracelet/crush/internal/db"
@@ -59,25 +58,9 @@ func (s *service) LastReadTime(ctx context.Context, sessionID, path string) time
59 return time.Unix(readFile.ReadAt, 0)
58 return time.Unix(readFile.ReadAt, 0)
60}
59}
61
60
62−// workingDir caches the process working directory. The working directory
63−// cannot legitimately change during normal operation, so resolving it once
64−// avoids an os.Getwd() syscall on every RecordRead/LastReadTime call.
65−var (
66− workingDirOnce sync.Once
67− workingDir string
68− workingDirErr error
69−)
70−
71−func getwd() (string, error) {
72− workingDirOnce.Do(func() {
73− workingDir, workingDirErr = os.Getwd()
74− })
75− return workingDir, workingDirErr
76−}
77−
78func relpath(path string) string {
61func relpath(path string) string {
79 path = filepath.Clean(path)
62 path = filepath.Clean(path)
80− basepath, err := getwd()
63+ basepath, err := os.Getwd()
81 if err != nil {
64 if err != nil {
82 slog.Warn("Error getting basepath", "error", err)
65 slog.Warn("Error getting basepath", "error", err)
83 return path
66 return path
@@ -97,7 +80,7 @@ func (s *service) ListReadFiles(ctx context.Context, sessionID string) ([]string
97 return nil, fmt.Errorf("listing read files: %w", err)
80 return nil, fmt.Errorf("listing read files: %w", err)
98 }
81 }
99
82
100− basepath, err := getwd()
83+ basepath, err := os.Getwd()
101 if err != nil {
84 if err != nil {
102 return nil, fmt.Errorf("getting working directory: %w", err)
85 return nil, fmt.Errorf("getting working directory: %w", err)
103 }
86 }
internal/fsext/fileutil.go+22−29
@@ -14,7 +14,6 @@ import (
14 "github.com/charlievieth/fastwalk"
14 "github.com/charlievieth/fastwalk"
15 "github.com/charmbracelet/crush/internal/csync"
15 "github.com/charmbracelet/crush/internal/csync"
16 "github.com/charmbracelet/crush/internal/home"
16 "github.com/charmbracelet/crush/internal/home"
17− "github.com/charmbracelet/x/ansi"
18)
17)
19
18
20type FileInfo struct {
19type FileInfo struct {
@@ -22,29 +21,6 @@ type FileInfo struct {
22 ModTime time.Time
21 ModTime time.Time
23}
22}
24
23
25−// commonIgnoredDirs is a set of directory names that SkipHidden always ignores.
26−// Hoisted to package scope so it is built once instead of on every call.
27−var commonIgnoredDirs = map[string]bool{
28− ".crush": true,
29− "node_modules": true,
30− "vendor": true,
31− "dist": true,
32− "build": true,
33− "target": true,
34− ".git": true,
35− ".idea": true,
36− ".vscode": true,
37− "__pycache__": true,
38− "bin": true,
39− "obj": true,
40− "out": true,
41− "coverage": true,
42− "logs": true,
43− "generated": true,
44− "bower_components": true,
45− "jspm_packages": true,
46−}
47−
48func SkipHidden(path string) bool {
24func SkipHidden(path string) bool {
49 // Check for hidden files (starting with a dot)
25 // Check for hidden files (starting with a dot)
50 base := filepath.Base(path)
26 base := filepath.Base(path)
@@ -52,6 +28,27 @@ func SkipHidden(path string) bool {
52 return true
28 return true
53 }
29 }
54
30
31+ commonIgnoredDirs := map[string]bool{
32+ ".crush": true,
33+ "node_modules": true,
34+ "vendor": true,
35+ "dist": true,
36+ "build": true,
37+ "target": true,
38+ ".git": true,
39+ ".idea": true,
40+ ".vscode": true,
41+ "__pycache__": true,
42+ "bin": true,
43+ "obj": true,
44+ "out": true,
45+ "coverage": true,
46+ "logs": true,
47+ "generated": true,
48+ "bower_components": true,
49+ "jspm_packages": true,
50+ }
51+
55 parts := strings.SplitSeq(path, string(os.PathSeparator))
52 parts := strings.SplitSeq(path, string(os.PathSeparator))
56 for part := range parts {
53 for part := range parts {
57 if commonIgnoredDirs[part] {
54 if commonIgnoredDirs[part] {
@@ -207,11 +204,7 @@ func DirTrim(pwd string, lim int) string {
207 if i == len(dirs)-1 {
204 if i == len(dirs)-1 {
208 out = dirs[i]
205 out = dirs[i]
209 } else if i >= len(dirs)-lim {
206 } else if i >= len(dirs)-lim {
210− // Keep the first grapheme cluster, not the first byte: CJK,
207+ out = string(dirs[i][0]) + out
211− // combining marks, and emoji can span multiple bytes and runes,
212− // so a byte or single rune would render the wrong character.
213− first, _ := ansi.FirstGraphemeCluster(dirs[i], ansi.GraphemeWidth)
214− out = first + out
215 } else {
208 } else {
216 out = "..." + out
209 out = "..." + out
217 break
210 break
internal/fsext/fileutil_dirtrim_test.godeleted−32
@@ -1,32 +0,0 @@
1−package fsext
2−
3−import (
4− "path/filepath"
5− "testing"
6−
7− "github.com/stretchr/testify/require"
8−)
9−
10−func TestDirTrimUnicode(t *testing.T) {
11− sep := string(filepath.Separator)
12−
13− // An abbreviated component keeps its first character.
14− require.Equal(t,
15− filepath.Join("~", "...", "p", "file"),
16− DirTrim(sep+"home"+sep+"user"+sep+"project"+sep+"file", 2),
17− )
18−
19− // A multi-byte (e.g. CJK) component must keep its first whole character,
20− // not its first byte, which would render a wrong character.
21− require.Equal(t,
22− filepath.Join("~", "...", "项", "file"),
23− DirTrim(sep+"home"+sep+"项目"+sep+"file", 2),
24− )
25−
26− // A grapheme cluster spanning multiple runes (a base letter plus a
27− // combining accent) must be kept whole, not split after the base rune.
28− require.Equal(t,
29− filepath.Join("~", "...", "e\u0301", "file"),
30− DirTrim(sep+"home"+sep+"e\u0301cole"+sep+"file", 2),
31− )
32−}
internal/fsext/lookup.go+7−2
@@ -258,7 +258,7 @@ func canonicalize(path string) string {
258
258
259// probeEnt checks if entity at given path exists and belongs to given owner
259// probeEnt checks if entity at given path exists and belongs to given owner
260func probeEnt(fspath string, owner int) error {
260func probeEnt(fspath string, owner int) error {
261− info, err := os.Stat(fspath)
261+ _, err := os.Stat(fspath)
262 if err != nil {
262 if err != nil {
263 return fmt.Errorf("cannot stat %s: %w", fspath, err)
263 return fmt.Errorf("cannot stat %s: %w", fspath, err)
264 }
264 }
@@ -268,7 +268,12 @@ func probeEnt(fspath string, owner int) error {
268 return nil
268 return nil
269 }
269 }
270
270
271− if ownerFromInfo(info) != owner {
271+ fowner, err := Owner(fspath)
272+ if err != nil {
273+ return fmt.Errorf("cannot get ownership for %s: %w", fspath, err)
274+ }
275+
276+ if fowner != owner {
272 return os.ErrPermission
277 return os.ErrPermission
273 }
278 }
274
279
internal/fsext/owner_others.go+5−8
@@ -14,14 +14,11 @@ func Owner(path string) (int, error) {
14 if err != nil {
14 if err != nil {
15 return 0, err
15 return 0, err
16 }
16 }
17− return ownerFromInfo(info), nil
17+ var uid int
18−}
19−
20−// ownerFromInfo derives the owner user ID from an already-obtained
21−// os.FileInfo, avoiding a redundant stat.
22−func ownerFromInfo(info os.FileInfo) int {
23 if stat, ok := info.Sys().(*syscall.Stat_t); ok {
18 if stat, ok := info.Sys().(*syscall.Stat_t); ok {
24− return int(stat.Uid)
19+ uid = int(stat.Uid)
20+ } else {
21+ uid = os.Getuid()
25 }
22 }
26− return os.Getuid()
23+ return uid, nil
27}
24}
internal/fsext/owner_windows.go−7
@@ -13,10 +13,3 @@ func Owner(path string) (int, error) {
13 }
13 }
14 return -1, nil
14 return -1, nil
15}
15}
16−
17−// ownerFromInfo derives the owner user ID from an already-obtained
18−// os.FileInfo, avoiding a redundant stat. Windows does not resolve
19−// ownership, so it always returns the bypass sentinel.
20−func ownerFromInfo(_ os.FileInfo) int {
21− return -1
22−}
internal/herdr/client.godeleted−405
@@ -1,405 +0,0 @@
1−// Package herdr provides native integration with the herdr terminal
2−// multiplexer. When Crush runs inside a herdr-managed pane it reports
3−// agent state (idle, working, blocked) and session identity over
4−// herdr's Unix socket API so herdr can display accurate status without
5−// screen scraping.
6−//
7−// The client consumes a small, herdr-specific event vocabulary rather
8−// than accepting raw proto or domain types. Callers translate their
9−// events into herdr.Event before forwarding. This keeps the client
10−// decoupled from both the proto and internal domain layers.
11−package herdr
12−
13−import (
14− "context"
15− "encoding/json"
16− "flag"
17− "fmt"
18− "io"
19− "log/slog"
20− "net"
21− "os"
22− "sync"
23− "time"
24−)
25−
26−// State values matching herdr's PaneAgentState enum.
27−const (
28− stateIdle = "idle"
29− stateWorking = "working"
30− stateBlocked = "blocked"
31−)
32−
33−// Event is the herdr-specific event vocabulary. Each type maps to a
34−// distinct state transition in the agent lifecycle. Callers translate
35−// from proto or domain types into these before calling HandleEvent.
36−type Event interface {
37− herdrEvent()
38−}
39−
40−// AssistantMessage indicates the agent produced output. Transitions
41−// to working if not already active.
42−type AssistantMessage struct {
43− SessionID string
44−}
45−
46−func (AssistantMessage) herdrEvent() {}
47−
48−// RunComplete indicates the agent finished a turn. Transitions to idle.
49−type RunComplete struct {
50− SessionID string
51−}
52−
53−func (RunComplete) herdrEvent() {}
54−
55−// PermissionRequested indicates the agent is waiting for user approval.
56−// Transitions to blocked.
57−type PermissionRequested struct{}
58−
59−func (PermissionRequested) herdrEvent() {}
60−
61−// PermissionResolved indicates a permission decision was made.
62−// Transitions back to working if a run is active, idle otherwise.
63−type PermissionResolved struct{}
64−
65−func (PermissionResolved) herdrEvent() {}
66−
67−// Summarizing indicates the agent is compacting context. Transitions
68−// to working if not already active.
69−type Summarizing struct{}
70−
71−func (Summarizing) herdrEvent() {}
72−
73−// sender abstracts the transport layer for reporting state to herdr.
74−// Production uses a Unix socket; tests use a recorder.
75−type sender interface {
76− send(req reportRequest) error
77− close()
78−}
79−
80−// Client reports Crush agent state to a running herdr instance.
81−type Client struct {
82− socketPath string
83− paneID string
84−
85− mu sync.Mutex
86− sessionID string
87− state string
88− runActive bool
89− seq uint64
90−
91− snd sender
92−}
93−
94−// defaultClient is the process-wide herdr client. Initialized once
95−// via Init(). All integration sites share this single instance so
96−// only one Unix socket connection exists per process.
97−var (
98− defaultClient *Client
99− initOnce sync.Once
100−)
101−
102−// Init returns the process-wide herdr Client, creating it on first
103−// call from environment variables. Returns nil when Crush is not
104−// running inside a herdr pane. Safe to call from any goroutine.
105−func Init() *Client {
106− initOnce.Do(func() {
107− defaultClient = newFromEnv()
108− })
109− return defaultClient
110−}
111−
112−func newFromEnv() *Client {
113− if os.Getenv("HERDR_ENV") != "1" {
114− return nil
115− }
116− // A test binary inherits the launching shell's HERDR_* env, so
117− // without this it would attach to the developer's live pane and
118− // release its agent on teardown. Skip herdr entirely under test.
119− if flag.Lookup("test.v") != nil {
120− slog.Debug("Herdr integration disabled: running under go test")
121− return nil
122− }
123− socketPath := os.Getenv("HERDR_SOCKET_PATH")
124− paneID := os.Getenv("HERDR_PANE_ID")
125− if socketPath == "" || paneID == "" {
126− slog.Debug("Herdr integration disabled: incomplete environment",
127− "has_socket", socketPath != "",
128− "has_pane_id", paneID != "",
129− )
130− return nil
131− }
132− c := &Client{
133− socketPath: socketPath,
134− paneID: paneID,
135− state: stateIdle,
136− seq: uint64(time.Now().UnixNano()),
137− snd: newUnixSender(socketPath),
138− }
139− c.registerInitial()
140− return c
141−}
142−
143−// registerInitial sends an initial idle-state report to herdr so the
144−// pane knows about the agent immediately, not just after the first
145−// event. Called once during client creation. Bypasses the dedup
146−// check since the initial state must always be reported regardless
147−// of redundancy.
148−//
149−// herdr remembers the highest seq it has seen per source for the
150−// lifetime of a pane and silently drops any report with a seq that
151−// is not strictly greater. Because crush seeds seq from the wall
152−// clock at startup (see newFromEnv), a restarted crush in the same
153−// pane always reports above the previous run's high-water mark, so
154−// the first report is accepted instead of being rejected as stale.
155−func (c *Client) registerInitial() {
156− if c == nil {
157− return
158− }
159− c.mu.Lock()
160− defer c.mu.Unlock()
161− if err := c.snd.send(c.newRequestLocked("pane.report_agent", "init", stateIdle)); err != nil {
162− slog.Warn("failed to send herdr agent report", "error", err)
163− }
164−}
165−
166−// Close releases the agent's authority on the pane and shuts down
167−// the background writer. Safe to call on a nil client.
168−func (c *Client) Close() {
169− if c == nil {
170− return
171− }
172− c.releaseAgent()
173− c.snd.close()
174−}
175−
176−// releaseAgent sends a pane.release_agent request to herdr so the
177−// pane is freed for a new agent to claim authority. This is the
178−// clean-shutdown protocol per herdr's socket API. Sends directly
179−// on the socket to ensure delivery even if the write loop is busy.
180−func (c *Client) releaseAgent() {
181− c.mu.Lock()
182− defer c.mu.Unlock()
183− req := c.newRequestLocked("pane.release_agent", "release", "")
184− if err := dialSend(c.socketPath, req); err != nil {
185− slog.Debug("Herdr release_agent failed", "error", err)
186− }
187−}
188−
189−// HandleEvent processes a single herdr event and reports state changes.
190−// Safe to call from any goroutine.
191−func (c *Client) HandleEvent(ev Event) {
192− if c == nil {
193− return
194− }
195− switch e := ev.(type) {
196− case AssistantMessage:
197− c.onAssistantMessage(e.SessionID)
198− case RunComplete:
199− c.onRunComplete(e.SessionID)
200− case PermissionRequested:
201− c.onPermissionRequest()
202− case PermissionResolved:
203− c.onPermissionResolved()
204− case Summarizing:
205− c.onSummarizing()
206− }
207−}
208−
209−// SetSessionID sets the session ID for reporting. Call this when the
210−// session is created or resolved, before events start flowing.
211−func (c *Client) SetSessionID(id string) {
212− if c == nil {
213− return
214− }
215− c.mu.Lock()
216− defer c.mu.Unlock()
217− c.sessionID = id
218−}
219−
220−func (c *Client) onAssistantMessage(sessionID string) {
221− c.mu.Lock()
222− defer c.mu.Unlock()
223− if sessionID != "" {
224− c.sessionID = sessionID
225− }
226− if !c.runActive {
227− c.runActive = true
228− c.reportLocked(stateWorking)
229− }
230−}
231−
232−func (c *Client) onRunComplete(sessionID string) {
233− c.mu.Lock()
234− defer c.mu.Unlock()
235− c.runActive = false
236− if sessionID != "" {
237− c.sessionID = sessionID
238− }
239− c.reportLocked(stateIdle)
240−}
241−
242−func (c *Client) onPermissionRequest() {
243− c.mu.Lock()
244− defer c.mu.Unlock()
245− // A permission request implies a run is active, even if no
246− // assistant message has arrived yet (e.g. tool calls that fire
247− // before any text output).
248− if !c.runActive {
249− c.runActive = true
250− }
251− c.reportLocked(stateBlocked)
252−}
253−
254−func (c *Client) onPermissionResolved() {
255− c.mu.Lock()
256− defer c.mu.Unlock()
257− if c.runActive {
258− c.reportLocked(stateWorking)
259− } else {
260− c.reportLocked(stateIdle)
261− }
262−}
263−
264−func (c *Client) onSummarizing() {
265− c.mu.Lock()
266− defer c.mu.Unlock()
267− if !c.runActive {
268− c.runActive = true
269− }
270− c.reportLocked(stateWorking)
271−}
272−
273−// newRequestLocked builds a seq-stamped JSON-RPC request to herdr.
274−// Must be called with c.mu held. Every request increments c.seq so
275−// herdr accepts it as strictly newer than the last (see
276−// registerInitial for why monotonic seq matters). State is empty for
277−// requests that carry no agent state, such as pane.release_agent.
278−func (c *Client) newRequestLocked(method, idPrefix, state string) reportRequest {
279− c.seq++
280− return reportRequest{
281− ID: fmt.Sprintf("crush:%s:%d", idPrefix, time.Now().UnixNano()),
282− Method: method,
283− Params: reportParams{
284− PaneID: c.paneID,
285− Source: "crush",
286− Agent: "crush",
287− State: state,
288− Seq: c.seq,
289− AgentSessionID: c.sessionID,
290− },
291− }
292−}
293−
294−// reportLocked sends a pane.report_agent request to herdr. Must be
295−// called with c.mu held. Skips redundant reports when the state has
296−// not changed.
297−func (c *Client) reportLocked(state string) {
298− if state == c.state {
299− return
300− }
301− c.state = state
302− if err := c.snd.send(c.newRequestLocked("pane.report_agent", "report", state)); err != nil {
303− slog.Warn("failed to send herdr agent report", "error", err)
304− }
305−}
306−
307−// reportRequest is the JSON-RPC envelope sent to herdr.
308−type reportRequest struct {
309− ID string `json:"id"`
310− Method string `json:"method"`
311− Params reportParams `json:"params"`
312−}
313−
314−// reportParams carries the agent state payload.
315−type reportParams struct {
316− PaneID string `json:"pane_id"`
317− Source string `json:"source"`
318− Agent string `json:"agent"`
319− State string `json:"state"`
320− Seq uint64 `json:"seq"`
321− AgentSessionID string `json:"agent_session_id"`
322−}
323−
324−// unixSender sends JSON-RPC requests over a Unix domain socket using
325−// a single background writer goroutine and a buffered channel. This
326−// serializes writes and avoids spawning unbounded goroutines under
327−// high event throughput. Each report opens a short-lived connection.
328−type unixSender struct {
329− socketPath string
330− ch chan reportRequest
331− cancel context.CancelFunc
332−}
333−
334−func newUnixSender(socketPath string) *unixSender {
335− ctx, cancel := context.WithCancel(context.Background())
336− s := &unixSender{
337− socketPath: socketPath,
338− ch: make(chan reportRequest, 16),
339− cancel: cancel,
340− }
341− go s.writeLoop(ctx)
342− return s
343−}
344−
345−func (s *unixSender) send(req reportRequest) error {
346− select {
347− case s.ch <- req:
348− default:
349− // Drop if the buffer is full. State reports are
350− // best-effort; blocking the agent is worse than
351− // missing a transition.
352− }
353− return nil
354−}
355−
356−func (s *unixSender) close() {
357− s.cancel()
358−}
359−
360−func (s *unixSender) writeLoop(ctx context.Context) {
361− for {
362− select {
363− case req, ok := <-s.ch:
364− if !ok {
365− return
366− }
367− if err := dialSend(s.socketPath, req); err != nil {
368− slog.Debug("Herdr report failed", "error", err)
369− }
370− case <-ctx.Done():
371− return
372− }
373− }
374−}
375−
376−// dialSend opens a short-lived Unix socket connection to herdr,
377−// sends a single JSON-RPC request, and drains the response.
378−func dialSend(socketPath string, req reportRequest) error {
379− ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
380− defer cancel()
381−
382− dialer := net.Dialer{}
383− conn, err := dialer.DialContext(ctx, "unix", socketPath)
384− if err != nil {
385− return err
386− }
387− defer conn.Close()
388−
389− _ = conn.SetDeadline(time.Now().Add(500 * time.Millisecond))
390−
391− data, err := json.Marshal(req)
392− if err != nil {
393− return err
394− }
395− data = append(data, '\n')
396−
397− _, err = conn.Write(data)
398− if err != nil {
399− return err
400− }
401−
402− // Drain the response to complete the request cycle.
403− _, _ = io.Copy(io.Discard, conn)
404− return nil
405−}
internal/herdr/client_test.godeleted−158
@@ -1,158 +0,0 @@
1−package herdr
2−
3−import (
4− "testing"
5−
6− "github.com/stretchr/testify/assert"
7−)
8−
9−// recordingSender captures state transitions without connecting to a
10−// real Unix socket.
11−type recordingSender struct {
12− states []string
13−}
14−
15−func (r *recordingSender) send(req reportRequest) error {
16− r.states = append(r.states, req.Params.State)
17− return nil
18−}
19−
20−func (r *recordingSender) close() {}
21−
22−// newTestClient creates a Client that records state transitions
23−// without connecting to a real Unix socket.
24−func newTestClient() *Client {
25− rec := &recordingSender{states: make([]string, 0, 16)}
26− return &Client{
27− state: stateIdle,
28− snd: rec,
29− }
30−}
31−
32−// reportedStates returns the states recorded by the test sender.
33−func reportedStates(c *Client) []string {
34− return c.snd.(*recordingSender).states
35−}
36−
37−func TestBasicLifecycle(t *testing.T) {
38− t.Parallel()
39− c := newTestClient()
40−
41− // Assistant message starts working.
42− c.HandleEvent(AssistantMessage{SessionID: "sess-1"})
43− assert.Equal(t, []string{stateWorking}, reportedStates(c))
44−
45− // Run complete returns to idle.
46− c.HandleEvent(RunComplete{SessionID: "sess-1"})
47− assert.Equal(t, []string{stateWorking, stateIdle}, reportedStates(c))
48−}
49−
50−func TestPermissionBlockAndUnblock(t *testing.T) {
51− t.Parallel()
52− c := newTestClient()
53−
54− // Start working.
55− c.HandleEvent(AssistantMessage{SessionID: "sess-1"})
56−
57− // Permission request blocks.
58− c.HandleEvent(PermissionRequested{})
59− assert.Equal(t, []string{stateWorking, stateBlocked}, reportedStates(c))
60−
61− // Permission granted returns to working (run still active).
62− c.HandleEvent(PermissionResolved{})
63− assert.Equal(t, []string{stateWorking, stateBlocked, stateWorking}, reportedStates(c))
64−
65− // Run complete returns to idle.
66− c.HandleEvent(RunComplete{SessionID: "sess-1"})
67− assert.Equal(t, []string{stateWorking, stateBlocked, stateWorking, stateIdle}, reportedStates(c))
68−}
69−
70−func TestPermissionBeforeAssistantMessage(t *testing.T) {
71− t.Parallel()
72− c := newTestClient()
73−
74− // Permission request arrives before any assistant message.
75− // This can happen when tool calls fire before text output.
76− c.HandleEvent(PermissionRequested{})
77− assert.Equal(t, []string{stateBlocked}, reportedStates(c))
78−
79− // Permission resolved should return to working, not idle,
80− // because the permission request implied a run was active.
81− c.HandleEvent(PermissionResolved{})
82− assert.Equal(t, []string{stateBlocked, stateWorking}, reportedStates(c))
83−}
84−
85−func TestSessionIDPropagation(t *testing.T) {
86− t.Parallel()
87− c := newTestClient()
88−
89− // SetSessionID before events.
90− c.SetSessionID("early-session")
91− assert.Equal(t, "early-session", c.sessionID)
92−
93− // RunComplete also updates session ID.
94− c.HandleEvent(RunComplete{SessionID: "final-session"})
95− assert.Equal(t, "final-session", c.sessionID)
96−}
97−
98−func TestDedupSkipsRedundantState(t *testing.T) {
99− t.Parallel()
100− c := newTestClient()
101−
102− // Two assistant messages in a row should only report working once.
103− c.HandleEvent(AssistantMessage{SessionID: "s1"})
104− c.HandleEvent(AssistantMessage{SessionID: "s1"})
105− assert.Equal(t, []string{stateWorking}, reportedStates(c))
106−}
107−
108−func TestSummarizingTriggersWorking(t *testing.T) {
109− t.Parallel()
110− c := newTestClient()
111−
112− // Summarizing event should trigger working.
113− c.HandleEvent(Summarizing{})
114− assert.Equal(t, []string{stateWorking}, reportedStates(c))
115−
116− // Second summarizing should not trigger another state change.
117− c.HandleEvent(Summarizing{})
118− assert.Equal(t, []string{stateWorking}, reportedStates(c))
119−}
120−
121−func TestNilClientSafe(t *testing.T) {
122− t.Parallel()
123− var c *Client
124− // These should not panic on a nil receiver.
125− c.SetSessionID("s1")
126− c.HandleEvent(AssistantMessage{SessionID: "s1"})
127− c.HandleEvent(RunComplete{SessionID: "s1"})
128− c.HandleEvent(PermissionRequested{})
129− c.HandleEvent(PermissionResolved{})
130− c.HandleEvent(Summarizing{})
131−}
132−
133−func TestRegisterInitial(t *testing.T) {
134− t.Parallel()
135− rec := &recordingSender{states: make([]string, 0, 16)}
136− c := &Client{
137− state: stateIdle,
138− seq: 100,
139− snd: rec,
140− }
141− c.registerInitial()
142− assert.Equal(t, []string{stateIdle}, rec.states)
143− // seq must strictly increase so herdr accepts the report.
144− assert.Equal(t, uint64(101), c.seq)
145−}
146−
147−// TestInitDisabledUnderTest guards the critical safety property that
148−// herdr never attaches to a real pane from a test binary. Test
149−// processes inherit the developer's HERDR_* environment, so a missing
150−// guard would release the live pane's agent on teardown. Because this
151−// test itself runs under `go test`, Init must return nil even with a
152−// complete, valid-looking environment.
153−func TestInitDisabledUnderTest(t *testing.T) {
154− t.Setenv("HERDR_ENV", "1")
155− t.Setenv("HERDR_SOCKET_PATH", "/tmp/does-not-matter.sock")
156− t.Setenv("HERDR_PANE_ID", "test:pane")
157− assert.Nil(t, newFromEnv())
158−}
internal/herdr/translate.godeleted−150
@@ -1,150 +0,0 @@
1−package herdr
2−
3−import (
4− "context"
5− "time"
6−
7− "github.com/charmbracelet/crush/internal/agent/notify"
8− "github.com/charmbracelet/crush/internal/message"
9− "github.com/charmbracelet/crush/internal/permission"
10− "github.com/charmbracelet/crush/internal/proto"
11− "github.com/charmbracelet/crush/internal/pubsub"
12−)
13−
14−// Translate converts a pub/sub event (domain or proto) into a herdr
15−// Event. Returns nil for event types herdr doesn't care about. This
16−// is the single translation point for all integration modes.
17−func Translate(ev any) Event {
18− switch e := ev.(type) {
19− // Domain types (TUI / local headless).
20− case pubsub.Event[message.Message]:
21− return translateMessage(
22− e.Payload.Role == message.Assistant,
23− e.Payload.SessionID,
24− e.Payload.IsSummaryMessage,
25− )
26− case pubsub.Event[notify.RunComplete]:
27− return RunComplete{SessionID: e.Payload.SessionID}
28− case pubsub.Event[permission.PermissionRequest]:
29− return PermissionRequested{}
30− case pubsub.Event[permission.PermissionNotification]:
31− return PermissionResolved{}
32−
33− // Proto types (client/server mode).
34− case pubsub.Event[proto.Message]:
35− return translateMessage(
36− e.Payload.Role == proto.Assistant,
37− e.Payload.SessionID,
38− false,
39− )
40− case pubsub.Event[proto.RunComplete]:
41− return RunComplete{SessionID: e.Payload.SessionID}
42− case pubsub.Event[proto.PermissionRequest]:
43− return PermissionRequested{}
44− case pubsub.Event[proto.PermissionNotification]:
45− return PermissionResolved{}
46− case pubsub.Event[proto.AgentEvent]:
47− if e.Payload.Type == proto.AgentEventTypeSummarize && !e.Payload.Done {
48− return Summarizing{}
49− }
50− return nil
51−
52− default:
53− return nil
54− }
55−}
56−
57−// translateMessage is the shared message-mapping logic for both domain
58−// and proto message types.
59−func translateMessage(isAssistant bool, sessionID string, isSummary bool) Event {
60− if !isAssistant {
61− return nil
62− }
63− if isSummary {
64− return Summarizing{}
65− }
66− return AssistantMessage{SessionID: sessionID}
67−}
68−
69−// permNotificationSubscriber is the subset of the permission service
70−// needed by BridgeLocal to subscribe to permission notifications.
71−type permNotificationSubscriber interface {
72− SubscribeNotifications(context.Context) <-chan pubsub.Event[permission.PermissionNotification]
73−}
74−
75−// BridgeSources groups the pub/sub sources that BridgeLocal subscribes
76−// to. Adding a new event type means adding a field here rather than
77−// growing the function signature.
78−type BridgeSources struct {
79− PermRequests pubsub.Subscriber[permission.PermissionRequest]
80− PermNotifications permNotificationSubscriber
81− RunCompletions pubsub.Subscriber[notify.RunComplete]
82− Messages pubsub.Subscriber[message.Message]
83−}
84−
85−// BridgeLocal subscribes to local pub/sub brokers and forwards
86−// translated events to the client. Used in TUI and local headless
87−// modes where the agent runs in-process. Cancelling ctx stops the
88−// bridge goroutines.
89−//
90−// The spawned goroutines are best-effort and may briefly outlive
91−// Client.Close(). This is safe: HandleEvent is nil-safe, and the
92−// unixSender drops messages on a full buffer rather than blocking.
93−//
94−// Each goroutine uses a resilient subscription loop that re-subscribes
95−// if the channel closes unexpectedly, ensuring the bridge survives
96−// transient pub/sub broker resets.
97−func BridgeLocal(ctx context.Context, c *Client, src BridgeSources) {
98− if c == nil {
99− return
100− }
101− go forward(ctx, c, func(subCtx context.Context) <-chan pubsub.Event[permission.PermissionRequest] {
102− return src.PermRequests.Subscribe(subCtx)
103− })
104− go forward(ctx, c, func(subCtx context.Context) <-chan pubsub.Event[permission.PermissionNotification] {
105− return src.PermNotifications.SubscribeNotifications(subCtx)
106− })
107− go forward(ctx, c, func(subCtx context.Context) <-chan pubsub.Event[notify.RunComplete] {
108− return src.RunCompletions.Subscribe(subCtx)
109− })
110− go forward(ctx, c, func(subCtx context.Context) <-chan pubsub.Event[message.Message] {
111− return src.Messages.Subscribe(subCtx)
112− })
113−}
114−
115−// forward reads from a pub/sub channel and forwards translated
116−// events to the herdr client. If the channel closes (e.g., due to
117−// broker reset), it re-subscribes after a brief delay. Runs until ctx
118−// is cancelled.
119−func forward[T any](ctx context.Context, c *Client, subscribe func(context.Context) <-chan pubsub.Event[T]) {
120− for {
121− select {
122− case <-ctx.Done():
123− return
124− default:
125− }
126−
127− subCtx, cancel := context.WithCancel(ctx)
128− ch := subscribe(subCtx)
129−
130− inner:
131− for {
132− select {
133− case <-ctx.Done():
134− cancel()
135− return
136− case ev, ok := <-ch:
137− if !ok {
138− // Channel closed — broker may have reset.
139− // Cancel the sub-context and re-subscribe.
140− cancel()
141− time.Sleep(100 * time.Millisecond)
142− break inner
143− }
144− if hev := Translate(ev); hev != nil {
145− c.HandleEvent(hev)
146− }
147− }
148− }
149− }
150−}
internal/herdr/translate_test.godeleted−145
@@ -1,145 +0,0 @@
1−package herdr
2−
3−import (
4− "testing"
5−
6− "github.com/charmbracelet/crush/internal/agent/notify"
7− "github.com/charmbracelet/crush/internal/message"
8− "github.com/charmbracelet/crush/internal/permission"
9− "github.com/charmbracelet/crush/internal/proto"
10− "github.com/charmbracelet/crush/internal/pubsub"
11− "github.com/stretchr/testify/assert"
12−)
13−
14−// Domain type translation.
15−
16−func TestTranslateDomainAssistantMessage(t *testing.T) {
17− t.Parallel()
18− ev := pubsub.Event[message.Message]{
19− Payload: message.Message{Role: message.Assistant, SessionID: "s1"},
20− }
21− assert.Equal(t, AssistantMessage{SessionID: "s1"}, Translate(ev))
22−}
23−
24−func TestTranslateDomainSummaryMessage(t *testing.T) {
25− t.Parallel()
26− ev := pubsub.Event[message.Message]{
27− Payload: message.Message{
28− Role: message.Assistant,
29− SessionID: "s1",
30− IsSummaryMessage: true,
31− },
32− }
33− assert.Equal(t, Summarizing{}, Translate(ev))
34−}
35−
36−func TestTranslateDomainNonAssistantIgnored(t *testing.T) {
37− t.Parallel()
38− ev := pubsub.Event[message.Message]{
39− Payload: message.Message{Role: message.System},
40− }
41− assert.Nil(t, Translate(ev))
42−}
43−
44−func TestTranslateDomainRunComplete(t *testing.T) {
45− t.Parallel()
46− ev := pubsub.Event[notify.RunComplete]{
47− Payload: notify.RunComplete{SessionID: "s1"},
48− }
49− assert.Equal(t, RunComplete{SessionID: "s1"}, Translate(ev))
50−}
51−
52−func TestTranslateDomainPermissionRequest(t *testing.T) {
53− t.Parallel()
54− ev := pubsub.Event[permission.PermissionRequest]{
55− Payload: permission.PermissionRequest{ToolName: "bash"},
56− }
57− assert.Equal(t, PermissionRequested{}, Translate(ev))
58−}
59−
60−func TestTranslateDomainPermissionNotification(t *testing.T) {
61− t.Parallel()
62− ev := pubsub.Event[permission.PermissionNotification]{
63− Payload: permission.PermissionNotification{Granted: true},
64− }
65− assert.Equal(t, PermissionResolved{}, Translate(ev))
66−}
67−
68−// Proto type translation.
69−
70−func TestTranslateProtoAssistantMessage(t *testing.T) {
71− t.Parallel()
72− ev := pubsub.Event[proto.Message]{
73− Payload: proto.Message{Role: proto.Assistant, SessionID: "s1"},
74− }
75− assert.Equal(t, AssistantMessage{SessionID: "s1"}, Translate(ev))
76−}
77−
78−func TestTranslateProtoNonAssistantIgnored(t *testing.T) {
79− t.Parallel()
80− ev := pubsub.Event[proto.Message]{
81− Payload: proto.Message{Role: proto.User, SessionID: "s1"},
82− }
83− assert.Nil(t, Translate(ev))
84−}
85−
86−func TestTranslateProtoRunComplete(t *testing.T) {
87− t.Parallel()
88− ev := pubsub.Event[proto.RunComplete]{
89− Payload: proto.RunComplete{SessionID: "s1"},
90− }
91− assert.Equal(t, RunComplete{SessionID: "s1"}, Translate(ev))
92−}
93−
94−func TestTranslateProtoPermissionRequest(t *testing.T) {
95− t.Parallel()
96− ev := pubsub.Event[proto.PermissionRequest]{
97− Payload: proto.PermissionRequest{ToolName: "bash"},
98− }
99− assert.Equal(t, PermissionRequested{}, Translate(ev))
100−}
101−
102−func TestTranslateProtoPermissionNotification(t *testing.T) {
103− t.Parallel()
104− ev := pubsub.Event[proto.PermissionNotification]{
105− Payload: proto.PermissionNotification{Granted: true},
106− }
107− assert.Equal(t, PermissionResolved{}, Translate(ev))
108−}
109−
110−func TestTranslateProtoSummarizing(t *testing.T) {
111− t.Parallel()
112− ev := pubsub.Event[proto.AgentEvent]{
113− Payload: proto.AgentEvent{
114− Type: proto.AgentEventTypeSummarize,
115− Done: false,
116− },
117− }
118− assert.Equal(t, Summarizing{}, Translate(ev))
119−}
120−
121−func TestTranslateProtoSummarizeDoneIgnored(t *testing.T) {
122− t.Parallel()
123− ev := pubsub.Event[proto.AgentEvent]{
124− Payload: proto.AgentEvent{
125− Type: proto.AgentEventTypeSummarize,
126− Done: true,
127− },
128− }
129− assert.Nil(t, Translate(ev))
130−}
131−
132−func TestTranslateProtoSessionIgnored(t *testing.T) {
133− t.Parallel()
134− ev := pubsub.Event[proto.Session]{
135− Payload: proto.Session{ID: "s1"},
136− }
137− assert.Nil(t, Translate(ev))
138−}
139−
140−// Unknown types.
141−
142−func TestTranslateUnknownReturnsNil(t *testing.T) {
143− t.Parallel()
144− assert.Nil(t, Translate("not an event"))
145−}
internal/history/file.go+15−20
@@ -3,7 +3,6 @@ package history
3import (
3import (
4 "context"
4 "context"
5 "database/sql"
5 "database/sql"
6− "errors"
7 "fmt"
6 "fmt"
8 "strings"
7 "strings"
9
8
@@ -64,22 +63,20 @@ func (s *service) Create(ctx context.Context, sessionID, path, content string) (
64// number. If no previous versions exist for the path, it creates the initial
63// number. If no previous versions exist for the path, it creates the initial
65// version. The provided content is stored as the new version.
64// version. The provided content is stored as the new version.
66func (s *service) CreateVersion(ctx context.Context, sessionID, path, content string) (File, error) {
65func (s *service) CreateVersion(ctx context.Context, sessionID, path, content string) (File, error) {
67− // Get the latest version for this path. Only the version column is read
66+ // Get the latest version for this path
68− // (via ORDER BY version DESC LIMIT 1) to avoid loading every version's
67+ files, err := s.q.ListFilesByPath(ctx, path)
69− // full content just to compute the max. Versions start at InitialVersion
70− // (0), so MAX cannot distinguish "no rows" from "only the initial
71− // version"; sql.ErrNoRows is the reliable signal for "no previous
72− // version", in which case we create the initial version.
73− maxVersion, err := s.q.GetMaxFileVersionByPath(ctx, path)
74 if err != nil {
68 if err != nil {
75− if errors.Is(err, sql.ErrNoRows) {
76− // No previous versions, create initial
77− return s.Create(ctx, sessionID, path, content)
78− }
79 return File{}, err
69 return File{}, err
80 }
70 }
81
71
72+ if len(files) == 0 {
73+ // No previous versions, create initial
74+ return s.Create(ctx, sessionID, path, content)
75+ }
76+
77+ // Get the latest version
78+ latestFile := files[0] // Files are ordered by version DESC, created_at DESC
82− nextVersion := maxVersion + 1
79+ nextVersion := latestFile.Version + 1
83
80
84 return s.createWithVersion(ctx, sessionID, path, content, nextVersion)
81 return s.createWithVersion(ctx, sessionID, path, content, nextVersion)
85}
82}
@@ -111,7 +108,7 @@ func (s *service) createWithVersion(ctx context.Context, sessionID, path, conten
111 })
108 })
112 if txErr != nil {
109 if txErr != nil {
113 // Rollback the transaction
110 // Rollback the transaction
114− _ = tx.Rollback()
111+ tx.Rollback()
115
112
116 // Check if this is a uniqueness constraint violation
113 // Check if this is a uniqueness constraint violation
117 if strings.Contains(txErr.Error(), "UNIQUE constraint failed") {
114 if strings.Contains(txErr.Error(), "UNIQUE constraint failed") {
@@ -194,17 +191,15 @@ func (s *service) Delete(ctx context.Context, id string) error {
194}
191}
195
192
196func (s *service) DeleteSessionFiles(ctx context.Context, sessionID string) error {
193func (s *service) DeleteSessionFiles(ctx context.Context, sessionID string) error {
197− // Fetch all files first so we can publish per-file DeletedEvent.
198 files, err := s.ListBySession(ctx, sessionID)
194 files, err := s.ListBySession(ctx, sessionID)
199 if err != nil {
195 if err != nil {
200 return err
196 return err
201 }
197 }
202− // Single batch DELETE instead of N individual deletes.
203− if err := s.q.DeleteSessionFiles(ctx, sessionID); err != nil {
204− return err
205− }
206 for _, file := range files {
198 for _, file := range files {
207− s.Publish(pubsub.DeletedEvent, file)
199+ err = s.Delete(ctx, file.ID)
200+ if err != nil {
201+ return err
202+ }
208 }
203 }
209 return nil
204 return nil
210}
205}
internal/lsp/client.go−5
@@ -53,11 +53,6 @@ type Client struct {
53 // Diagnostic cache
53 // Diagnostic cache
54 diagnostics *csync.VersionedMap[protocol.DocumentURI, []protocol.Diagnostic]
54 diagnostics *csync.VersionedMap[protocol.DocumentURI, []protocol.Diagnostic]
55
55
56− // diagnosticsTotal is a running total of diagnostics across all files,
57− // maintained incrementally as diagnostics are set to avoid recounting
58− // every file on each publishDiagnostics notification.
59− diagnosticsTotal atomic.Int64
60−
61 // Cached diagnostic counts to avoid map copy on every UI render.
56 // Cached diagnostic counts to avoid map copy on every UI render.
62 diagCountsCache DiagnosticCounts
57 diagCountsCache DiagnosticCounts
63 diagCountsVersion uint64
58 diagCountsVersion uint64
internal/lsp/handlers.go+6−6
@@ -108,13 +108,13 @@ func HandleDiagnostics(client *Client, params json.RawMessage) {
108 return
108 return
109 }
109 }
110
110
111− // Maintain the running total incrementally: subtract the previous
112− // diagnostics for this URI (if any) and add the new ones. This avoids
113− // recounting every file's diagnostics on each notification.
114− previous, _ := client.diagnostics.Get(diagParams.URI)
115 client.diagnostics.Set(diagParams.URI, diagParams.Diagnostics)
111 client.diagnostics.Set(diagParams.URI, diagParams.Diagnostics)
116− delta := len(diagParams.Diagnostics) - len(previous)
112+
117− totalCount := int(client.diagnosticsTotal.Add(int64(delta)))
113+ // Calculate total diagnostic count
114+ totalCount := 0
115+ for _, diagnostics := range client.diagnostics.Seq2() {
116+ totalCount += len(diagnostics)
117+ }
118
118
119 // Trigger callback if set
119 // Trigger callback if set
120 if client.onDiagnosticsChanged != nil {
120 if client.onDiagnosticsChanged != nil {
internal/lsp/manager.go+8−28
@@ -27,7 +27,6 @@ const unavailableRetryDelay = 30 * time.Second
27type Manager struct {
27type Manager struct {
28 clients *csync.Map[string, *Client]
28 clients *csync.Map[string, *Client]
29 unavailable *csync.Map[string, time.Time]
29 unavailable *csync.Map[string, time.Time]
30− rootMarkers *csync.Map[string, bool]
31 cfg *config.ConfigStore
30 cfg *config.ConfigStore
32 manager *powernapconfig.Manager
31 manager *powernapconfig.Manager
33 callback func(name string, client *Client)
32 callback func(name string, client *Client)
@@ -37,9 +36,7 @@ type Manager struct {
37// NewManager creates a new LSP manager service.
36// NewManager creates a new LSP manager service.
38func NewManager(cfg *config.ConfigStore) *Manager {
37func NewManager(cfg *config.ConfigStore) *Manager {
39 manager := powernapconfig.NewManager()
38 manager := powernapconfig.NewManager()
40− if err := manager.LoadDefaults(); err != nil {
39+ manager.LoadDefaults()
41− slog.Warn("failed to load LSP defaults", "error", err)
42− }
43
40
44 // Merge user-configured LSPs into the manager.
41 // Merge user-configured LSPs into the manager.
45 for name, clientConfig := range cfg.Config().LSP {
42 for name, clientConfig := range cfg.Config().LSP {
@@ -66,7 +63,6 @@ func NewManager(cfg *config.ConfigStore) *Manager {
66 return &Manager{
63 return &Manager{
67 clients: csync.NewMap[string, *Client](),
64 clients: csync.NewMap[string, *Client](),
68 unavailable: csync.NewMap[string, time.Time](),
65 unavailable: csync.NewMap[string, time.Time](),
69− rootMarkers: csync.NewMap[string, bool](),
70 cfg: cfg,
66 cfg: cfg,
71 manager: manager,
67 manager: manager,
72 callback: func(string, *Client) {}, // default no-op callback
68 callback: func(string, *Client) {}, // default no-op callback
@@ -172,14 +168,6 @@ func (s *Manager) startServer(ctx context.Context, name, filepath string, server
172 }
168 }
173 }
169 }
174
170
175− // Cheap, pure string check first: if this server does not handle the
176− // file type at all, skip the expensive PATH lookup and workspace
177− // root-marker glob below. This gates the costly I/O on the zero-I/O
178− // filetype match, without changing whether the server ultimately starts.
179− if !handlesFiletype(server.Command, server.FileTypes, filepath) {
180− return
181− }
182−
183 if !isUserConfigured {
171 if !isUserConfigured {
184 if s.recentlyUnavailable(name) {
172 if s.recentlyUnavailable(name) {
185 return
173 return
@@ -196,8 +184,8 @@ func (s *Manager) startServer(ctx context.Context, name, filepath string, server
196 }
184 }
197 }
185 }
198
186
199− // hasRootMarkers is the slowest bit (a workspace glob), so we do it last.
187+ // this is the slowest bit, so we do it last.
200− if !s.hasRootMarkers(s.cfg.WorkingDir(), server.RootMarkers) {
188+ if !handles(server, filepath, s.cfg.WorkingDir()) {
201 // nothing to do
189 // nothing to do
202 return
190 return
203 }
191 }
@@ -341,19 +329,6 @@ func handlesFiletype(sname string, fileTypes []string, filePath string) bool {
341 return false
329 return false
342}
330}
343
331
344−// hasRootMarkers reports whether dir contains any of the given root
345−// markers. The result is invariant for a fixed (dir, markers) pair, so it
346−// is memoized to avoid re-running the workspace glob on every file view.
347−func (s *Manager) hasRootMarkers(dir string, markers []string) bool {
348− if len(markers) == 0 {
349− return true
350− }
351− key := dir + "\x00" + strings.Join(markers, "\x00")
352− return s.rootMarkers.GetOrSet(key, func() bool {
353− return hasRootMarkers(dir, markers)
354− })
355−}
356−
357func hasRootMarkers(dir string, markers []string) bool {
332func hasRootMarkers(dir string, markers []string) bool {
358 if len(markers) == 0 {
333 if len(markers) == 0 {
359 return true
334 return true
@@ -370,6 +345,11 @@ func hasRootMarkers(dir string, markers []string) bool {
370 return false
345 return false
371}
346}
372
347
348+func handles(server *powernapconfig.ServerConfig, filePath, workDir string) bool {
349+ return handlesFiletype(server.Command, server.FileTypes, filePath) &&
350+ hasRootMarkers(workDir, server.RootMarkers)
351+}
352+
373// KillAll force-kills all the LSP clients.
353// KillAll force-kills all the LSP clients.
374//
354//
375// This is generally faster than [Manager.StopAll] because it doesn't wait for
355// This is generally faster than [Manager.StopAll] because it doesn't wait for
internal/lsp/util/edit.go+34−41
@@ -4,7 +4,6 @@ import (
4 "bytes"
4 "bytes"
5 "fmt"
5 "fmt"
6 "os"
6 "os"
7− "slices"
8 "sort"
7 "sort"
9 "strings"
8 "strings"
10
9
@@ -38,40 +37,28 @@ func applyTextEdits(uri protocol.DocumentURI, edits []protocol.TextEdit, encodin
38 // Split into lines without the endings
37 // Split into lines without the endings
39 lines := strings.Split(string(content), lineEnding)
38 lines := strings.Split(string(content), lineEnding)
40
39
41− // Sort edits once by ascending start position (end position as a stable
40+ // Check for overlapping edits
42− // tiebreaker). This lets us both detect overlaps in a single linear pass
41+ for i, edit1 := range edits {
43− // and apply edits from the bottom of the file upward.
42+ for j := i + 1; j < len(edits); j++ {
43+ if rangesOverlap(edit1.Range, edits[j].Range) {
44+ return fmt.Errorf("overlapping edits detected between edit %d and %d", i, j)
45+ }
46+ }
47+ }
48+
49+ // Sort edits in reverse order
44 sortedEdits := make([]protocol.TextEdit, len(edits))
50 sortedEdits := make([]protocol.TextEdit, len(edits))
45 copy(sortedEdits, edits)
51 copy(sortedEdits, edits)
46 sort.Slice(sortedEdits, func(i, j int) bool {
52 sort.Slice(sortedEdits, func(i, j int) bool {
47− a, b := sortedEdits[i].Range, sortedEdits[j].Range
48− if a.Start.Line != b.Start.Line {
53+ if sortedEdits[i].Range.Start.Line != sortedEdits[j].Range.Start.Line {
49− return a.Start.Line < b.Start.Line
54+ return sortedEdits[i].Range.Start.Line > sortedEdits[j].Range.Start.Line
50 }
55 }
51− if a.Start.Character != b.Start.Character {
56+ return sortedEdits[i].Range.Start.Character > sortedEdits[j].Range.Start.Character
52− return a.Start.Character < b.Start.Character
53− }
54− if a.End.Line != b.End.Line {
55− return a.End.Line < b.End.Line
56− }
57− return a.End.Character < b.End.Character
58 })
57 })
59
58
60− // Detect overlapping edits in a single linear pass. Because the edits are
59+ // Apply each edit
61− // sorted by ascending start, any overlap manifests between two adjacent
62− // edits, so checking neighbors is sufficient.
63− for i := 1; i < len(sortedEdits); i++ {
64− if rangesOverlap(sortedEdits[i-1].Range, sortedEdits[i].Range) {
65− return fmt.Errorf("overlapping edits detected between edit %d and %d", i-1, i)
66− }
67− }
68−
69− // Apply edits from the bottom of the file upward so that earlier edits'
70− // line/character offsets are not shifted by later ones. applyTextEdit
71− // splices the affected range in place instead of rebuilding the whole
72− // slice per edit.
73− for i := len(sortedEdits) - 1; i >= 0; i-- {
60+ for _, edit := range sortedEdits {
74− newLines, err := applyTextEdit(lines, sortedEdits[i], encoding)
61+ newLines, err := applyTextEdit(lines, edit, encoding)
75 if err != nil {
62 if err != nil {
76 return fmt.Errorf("failed to apply edit: %w", err)
63 return fmt.Errorf("failed to apply edit: %w", err)
77 }
64 }
@@ -131,6 +118,12 @@ func applyTextEdit(lines []string, edit protocol.TextEdit, encoding powernap.Off
131 endChar = utf32ToByteOffset(endLineContent, edit.Range.End.Character)
118 endChar = utf32ToByteOffset(endLineContent, edit.Range.End.Character)
132 }
119 }
133
120
121+ // Create result slice with initial capacity
122+ result := make([]string, 0, len(lines))
123+
124+ // Copy lines before edit
125+ result = append(result, lines[:startLine]...)
126+
134 // Get the prefix of the start line
127 // Get the prefix of the start line
135 startLineContent := lines[startLine]
128 startLineContent := lines[startLine]
136 if startChar < 0 || startChar > len(startLineContent) {
129 if startChar < 0 || startChar > len(startLineContent) {
@@ -145,11 +138,10 @@ func applyTextEdit(lines []string, edit protocol.TextEdit, encoding powernap.Off
145 }
138 }
146 suffix := endLineContent[endChar:]
139 suffix := endLineContent[endChar:]
147
140
148− // Build the replacement lines for the edited range [startLine, endLine].
141+ // Handle the edit
149− var region []string
150 if edit.NewText == "" {
142 if edit.NewText == "" {
151 if prefix+suffix != "" {
143 if prefix+suffix != "" {
152− region = []string{prefix + suffix}
144+ result = append(result, prefix+suffix)
153 }
145 }
154 } else {
146 } else {
155 // Split new text into lines, being careful not to add extra newlines
147 // Split new text into lines, being careful not to add extra newlines
@@ -158,20 +150,21 @@ func applyTextEdit(lines []string, edit protocol.TextEdit, encoding powernap.Off
158
150
159 if len(newLines) == 1 {
151 if len(newLines) == 1 {
160 // Single line change
152 // Single line change
161− region = []string{prefix + newLines[0] + suffix}
153+ result = append(result, prefix+newLines[0]+suffix)
162 } else {
154 } else {
163 // Multi-line change
155 // Multi-line change
164− region = make([]string, 0, len(newLines))
165− region = append(region, prefix+newLines[0])
156+ result = append(result, prefix+newLines[0])
166− region = append(region, newLines[1:len(newLines)-1]...)
157+ result = append(result, newLines[1:len(newLines)-1]...)
167− region = append(region, newLines[len(newLines)-1]+suffix)
158+ result = append(result, newLines[len(newLines)-1]+suffix)
168 }
159 }
169 }
160 }
170
161
171− // Splice the replacement in place instead of rebuilding the whole slice.
162+ // Add remaining lines
172− // Callers apply edits from the bottom of the file upward, so mutating the
163+ if endLine+1 < len(lines) {
173− // shared slice does not disturb not-yet-applied (earlier) edits.
164+ result = append(result, lines[endLine+1:]...)
174− return slices.Replace(lines, startLine, endLine+1, region...), nil
165+ }
166+
167+ return result, nil
175}
168}
176
169
177// applyDocumentChange applies a DocumentChange (create/rename/delete operations)
170// applyDocumentChange applies a DocumentChange (create/rename/delete operations)
internal/message/content.go+23−69
@@ -171,18 +171,6 @@ type Message struct {
171 CreatedAt int64
171 CreatedAt int64
172 UpdatedAt int64
172 UpdatedAt int64
173 IsSummaryMessage bool
173 IsSummaryMessage bool
174−
175− // textBuf and reasoningBuf accumulate streaming deltas in amortized
176− // O(1) so building a long response is O(n) rather than O(n²) (the
177− // naive `existing + delta` reallocated and copied the whole
178− // accumulated string on every token). They are nil until the first
179− // delta, unexported (never persisted), and deliberately dropped by
180− // Clone: only the streaming message that owns them ever appends, so
181− // no other goroutine touches the builder. Snapshots observe an
182− // immutable prefix of the builder's append-only buffer, which is
183− // race-free to read concurrently.
184− textBuf *strings.Builder
185− reasoningBuf *strings.Builder
186}
174}
187
175
188func (m *Message) Content() TextContent {
176func (m *Message) Content() TextContent {
@@ -233,21 +221,6 @@ func (m *Message) ToolCalls() []ToolCall {
233 return toolCalls
221 return toolCalls
234}
222}
235
223
236−// toolCallStats returns the number of tool-call parts and how many of
237−// them are finished, in a single pass without allocating (unlike
238−// ToolCalls, which builds a slice). Used on the streaming hot path.
239−func (m *Message) toolCallStats() (count, finished int) {
240− for _, part := range m.Parts {
241− if c, ok := part.(ToolCall); ok {
242− count++
243− if c.Finished {
244− finished++
245− }
246− }
247− }
248− return count, finished
249−}
250−
251func (m *Message) ToolResults() []ToolResult {
224func (m *Message) ToolResults() []ToolResult {
252 toolResults := make([]ToolResult, 0)
225 toolResults := make([]ToolResult, 0)
253 for _, part := range m.Parts {
226 for _, part := range m.Parts {
@@ -293,51 +266,37 @@ func (m *Message) IsThinking() bool {
293}
266}
294
267
295func (m *Message) AppendContent(delta string) {
268func (m *Message) AppendContent(delta string) {
296− if m.textBuf == nil {
269+ found := false
297− m.textBuf = &strings.Builder{}
298− // Seed from any pre-existing text part (e.g. a restored message);
299− // streaming messages start empty so this is normally a no-op.
300− for _, part := range m.Parts {
301− if c, ok := part.(TextContent); ok {
302− m.textBuf.WriteString(c.Text)
303− break
304− }
305− }
306− }
307− m.textBuf.WriteString(delta)
308− text := m.textBuf.String()
309 for i, part := range m.Parts {
270 for i, part := range m.Parts {
310− if _, ok := part.(TextContent); ok {
271+ if c, ok := part.(TextContent); ok {
311− m.Parts[i] = TextContent{Text: text}
272+ m.Parts[i] = TextContent{Text: c.Text + delta}
312− return
273+ found = true
313 }
274 }
314 }
275 }
276+ if !found {
315− m.Parts = append(m.Parts, TextContent{Text: text})
277+ m.Parts = append(m.Parts, TextContent{Text: delta})
278+ }
316}
279}
317
280
318func (m *Message) AppendReasoningContent(delta string) {
281func (m *Message) AppendReasoningContent(delta string) {
319− if m.reasoningBuf == nil {
282+ found := false
320− m.reasoningBuf = &strings.Builder{}
321− for _, part := range m.Parts {
322− if c, ok := part.(ReasoningContent); ok {
323− m.reasoningBuf.WriteString(c.Thinking)
324− break
325− }
326− }
327− }
328− m.reasoningBuf.WriteString(delta)
329− thinking := m.reasoningBuf.String()
330 for i, part := range m.Parts {
283 for i, part := range m.Parts {
331 if c, ok := part.(ReasoningContent); ok {
284 if c, ok := part.(ReasoningContent); ok {
332− c.Thinking = thinking
333− m.Parts[i] = c
285+ m.Parts[i] = ReasoningContent{
334− return
286+ Thinking: c.Thinking + delta,
287+ Signature: c.Signature,
288+ StartedAt: c.StartedAt,
289+ FinishedAt: c.FinishedAt,
290+ }
291+ found = true
335 }
292 }
336 }
293 }
294+ if !found {
337− m.Parts = append(m.Parts, ReasoningContent{
295+ m.Parts = append(m.Parts, ReasoningContent{
338− Thinking: thinking,
296+ Thinking: delta,
339− StartedAt: time.Now().Unix(),
297+ StartedAt: time.Now().Unix(),
340− })
298+ })
299+ }
341}
300}
342
301
343func (m *Message) AppendThoughtSignature(signature string, toolCallID string) {
302func (m *Message) AppendThoughtSignature(signature string, toolCallID string) {
@@ -491,10 +450,6 @@ func (m *Message) Clone() Message {
491 clone := *m
450 clone := *m
492 clone.Parts = make([]ContentPart, len(m.Parts))
451 clone.Parts = make([]ContentPart, len(m.Parts))
493 copy(clone.Parts, m.Parts)
452 copy(clone.Parts, m.Parts)
494− // Snapshots never stream; drop the accumulation builders so only the
495− // owning message ever appends to them (keeps them single-goroutine).
496− clone.textBuf = nil
497− clone.reasoningBuf = nil
498 return clone
453 return clone
499}
454}
500
455
@@ -547,9 +502,8 @@ func (m *Message) ToAIMessage() []fantasy.Message {
547 case User:
502 case User:
548 var parts []fantasy.MessagePart
503 var parts []fantasy.MessagePart
549 text := strings.TrimSpace(m.Content().Text)
504 text := strings.TrimSpace(m.Content().Text)
550− binaryContent := m.BinaryContent()
551 var textAttachments []Attachment
505 var textAttachments []Attachment
552− for _, content := range binaryContent {
506+ for _, content := range m.BinaryContent() {
553 if !strings.HasPrefix(content.MIMEType, "text/") {
507 if !strings.HasPrefix(content.MIMEType, "text/") {
554 continue
508 continue
555 }
509 }
@@ -572,7 +526,7 @@ func (m *Message) ToAIMessage() []fantasy.Message {
572 if text != "" {
526 if text != "" {
573 parts = append(parts, fantasy.TextPart{Text: text})
527 parts = append(parts, fantasy.TextPart{Text: text})
574 }
528 }
575− for _, content := range binaryContent {
529+ for _, content := range m.BinaryContent() {
576 // skip text attachements
530 // skip text attachements
577 if strings.HasPrefix(content.MIMEType, "text/") {
531 if strings.HasPrefix(content.MIMEType, "text/") {
578 continue
532 continue
internal/message/message.go+56−102
@@ -49,7 +49,6 @@ type Service interface {
49 Update(ctx context.Context, message Message) error
49 Update(ctx context.Context, message Message) error
50 Get(ctx context.Context, id string) (Message, error)
50 Get(ctx context.Context, id string) (Message, error)
51 List(ctx context.Context, sessionID string) ([]Message, error)
51 List(ctx context.Context, sessionID string) ([]Message, error)
52− ListAfter(ctx context.Context, sessionID, afterMessageID string) ([]Message, error)
53 ListUserMessages(ctx context.Context, sessionID string) ([]Message, error)
52 ListUserMessages(ctx context.Context, sessionID string) ([]Message, error)
54 ListAllUserMessages(ctx context.Context) ([]Message, error)
53 ListAllUserMessages(ctx context.Context) ([]Message, error)
55 Delete(ctx context.Context, id string) error
54 Delete(ctx context.Context, id string) error
@@ -86,11 +85,6 @@ type pendingState struct {
86 // but other flushers must back off.
85 // but other flushers must back off.
87 flushing bool
86 flushing bool
88
87
89− // flushDone is non-nil exactly while flushing is true. It is closed
90− // (under service.mu) when the in-flight flush completes, so a sync
91− // caller can wait on it instead of polling.
92− flushDone chan struct{}
93−
94 // timer is the active debounce timer, or nil if no flush is
88 // timer is the active debounce timer, or nil if no flush is
95 // scheduled. Stopped and reset when a terminal update preempts
89 // scheduled. Stopped and reset when a terminal update preempts
96 // the debounce window.
90 // the debounce window.
@@ -202,34 +196,18 @@ func (s *service) Create(ctx context.Context, sessionID string, params CreateMes
202}
196}
203
197
204func (s *service) DeleteSessionMessages(ctx context.Context, sessionID string) error {
198func (s *service) DeleteSessionMessages(ctx context.Context, sessionID string) error {
205− // Fetch all messages first so we can publish per-message DeletedEvent
206− // for each (the UI uses them to remove individual chat entries).
207 messages, err := s.List(ctx, sessionID)
199 messages, err := s.List(ctx, sessionID)
208 if err != nil {
200 if err != nil {
209 return err
201 return err
210 }
202 }
211−
212− // Single batch DELETE instead of N individual deletes.
213− if err := s.q.DeleteSessionMessages(ctx, sessionID); err != nil {
214− return err
215− }
216−
217− // Drop any pending coalesced state and publish events for the
218− // already-fetched messages — no redundant Get queries needed.
219− s.mu.Lock()
220− for _, msg := range messages {
203+ for _, message := range messages {
221− if p, ok := s.pending[msg.ID]; ok {
204+ if message.SessionID == sessionID {
205+ err = s.Delete(ctx, message.ID)
222− if p.timer != nil {
206+ if err != nil {
223− p.timer.Stop()
207+ return err
224 }
208 }
225− delete(s.pending, msg.ID)
226 }
209 }
227 }
210 }
228− s.mu.Unlock()
229−
230− for _, msg := range messages {
231− s.Publish(pubsub.DeletedEvent, msg.Clone())
232− }
233 return nil
211 return nil
234}
212}
235
213
@@ -346,17 +324,9 @@ func (s *service) flushOne(ctx context.Context, id string, syncCaller bool) erro
346 s.mu.Unlock()
324 s.mu.Unlock()
347 return nil
325 return nil
348 }
326 }
349− // Wait for the in-flight flush to finish rather than
350− // polling. flushDone is closed under s.mu when flushing
351− // clears, and we capture it under the same lock, so the
352− // wakeup can never be missed.
353− done := p.flushDone
354 s.mu.Unlock()
327 s.mu.Unlock()
355− select {
328+ // Brief yield; in-flight write should land in <1ms typical.
356− case <-done:
329+ time.Sleep(time.Millisecond)
357− case <-ctx.Done():
358− return ctx.Err()
359− }
360 continue
330 continue
361 }
331 }
362 if !p.dirty {
332 if !p.dirty {
@@ -379,7 +349,6 @@ func (s *service) flushOne(ctx context.Context, id string, syncCaller bool) erro
379 }
349 }
380 isTerminal := shouldFlushNow(prev, &snap)
350 isTerminal := shouldFlushNow(prev, &snap)
381 p.flushing = true
351 p.flushing = true
382− p.flushDone = make(chan struct{})
383 p.dirty = false
352 p.dirty = false
384 s.mu.Unlock()
353 s.mu.Unlock()
385
354
@@ -387,10 +356,6 @@ func (s *service) flushOne(ctx context.Context, id string, syncCaller bool) erro
387
356
388 s.mu.Lock()
357 s.mu.Lock()
389 p.flushing = false
358 p.flushing = false
390− if p.flushDone != nil {
391− close(p.flushDone)
392− p.flushDone = nil
393− }
394 if err == nil {
359 if err == nil {
395 p.lastFlushed = snap
360 p.lastFlushed = snap
396 p.hasFlushed = true
361 p.hasFlushed = true
@@ -455,20 +420,25 @@ func shouldFlushNow(prev, next *Message) bool {
455 return true
420 return true
456 }
421 }
457
422
458− var prevCount, prevFinished int
423+ var prevCalls []ToolCall
459 var prevReasoningFinishedAt int64
424 var prevReasoningFinishedAt int64
460 if prev != nil {
425 if prev != nil {
461− prevCount, prevFinished = prev.toolCallStats()
426+ prevCalls = prev.ToolCalls()
462 prevReasoningFinishedAt = prev.ReasoningContent().FinishedAt
427 prevReasoningFinishedAt = prev.ReasoningContent().FinishedAt
463 }
428 }
464− nextCount, nextFinished := next.toolCallStats()
429+ nextCalls := next.ToolCalls()
465− // The tool-call set grew, or a call transitioned to finished. Tool
430+ if len(nextCalls) != len(prevCalls) {
466− // calls never un-finish and the count only grows, so comparing the
467− // finished counts is equivalent to the old per-index Finished check
468− // but allocation-free (a call's Input only matters once Finished).
469− if nextCount != prevCount || nextFinished != prevFinished {
470 return true
431 return true
471 }
432 }
433+ for i := range nextCalls {
434+ // Bounds-safe: lengths are equal here.
435+ if nextCalls[i].Finished != prevCalls[i].Finished {
436+ return true
437+ }
438+ // A tool call's input only matters once it has landed (Finished
439+ // flips true). Earlier deltas to Input are debounced with the
440+ // rest of the streaming state.
441+ }
472 if next.ReasoningContent().FinishedAt > 0 && prevReasoningFinishedAt == 0 {
442 if next.ReasoningContent().FinishedAt > 0 && prevReasoningFinishedAt == 0 {
473 return true
443 return true
474 }
444 }
@@ -498,24 +468,6 @@ func (s *service) List(ctx context.Context, sessionID string) ([]Message, error)
498 return messages, nil
468 return messages, nil
499}
469}
500
470
501−func (s *service) ListAfter(ctx context.Context, sessionID, afterMessageID string) ([]Message, error) {
502− dbMessages, err := s.q.ListMessagesBySessionAfter(ctx, db.ListMessagesBySessionAfterParams{
503− SessionID: sessionID,
504− ID: afterMessageID,
505− })
506− if err != nil {
507− return nil, err
508− }
509− messages := make([]Message, len(dbMessages))
510− for i, dbMessage := range dbMessages {
511− messages[i], err = s.fromDBItem(dbMessage)
512− if err != nil {
513− return nil, err
514− }
515− }
516− return messages, nil
517−}
518−
519func (s *service) ListUserMessages(ctx context.Context, sessionID string) ([]Message, error) {
471func (s *service) ListUserMessages(ctx context.Context, sessionID string) ([]Message, error) {
520 dbMessages, err := s.q.ListUserMessagesBySession(ctx, sessionID)
472 dbMessages, err := s.q.ListUserMessagesBySession(ctx, sessionID)
521 if err != nil {
473 if err != nil {
@@ -618,74 +570,76 @@ func marshalParts(parts []ContentPart) ([]byte, error) {
618}
570}
619
571
620func unmarshalParts(data []byte) ([]ContentPart, error) {
572func unmarshalParts(data []byte) ([]ContentPart, error) {
621− // Single-pass unmarshal into a wrapper with Data as json.RawMessage,
573+ temp := []json.RawMessage{}
622− // avoiding the intermediate []json.RawMessage + per-element re-decode.
623− var temp []struct {
624− Type partType `json:"type"`
625− Data json.RawMessage `json:"data"`
626− }
627
574
628 if err := json.Unmarshal(data, &temp); err != nil {
575 if err := json.Unmarshal(data, &temp); err != nil {
629 return nil, err
576 return nil, err
630 }
577 }
631
578
632− parts := make([]ContentPart, 0, len(temp))
579+ parts := make([]ContentPart, 0)
580+
581+ for _, rawPart := range temp {
582+ var wrapper struct {
583+ Type partType `json:"type"`
584+ Data json.RawMessage `json:"data"`
585+ }
586+
587+ if err := json.Unmarshal(rawPart, &wrapper); err != nil {
588+ return nil, err
589+ }
633
590
634− for _, wrapper := range temp {
635− var part ContentPart
636 switch wrapper.Type {
591 switch wrapper.Type {
637 case reasoningType:
592 case reasoningType:
638− p := ReasoningContent{}
593+ part := ReasoningContent{}
639− if err := json.Unmarshal(wrapper.Data, &p); err != nil {
594+ if err := json.Unmarshal(wrapper.Data, &part); err != nil {
640 return nil, err
595 return nil, err
641 }
596 }
642− part = p
597+ parts = append(parts, part)
643 case textType:
598 case textType:
644− p := TextContent{}
599+ part := TextContent{}
645− if err := json.Unmarshal(wrapper.Data, &p); err != nil {
600+ if err := json.Unmarshal(wrapper.Data, &part); err != nil {
646 return nil, err
601 return nil, err
647 }
602 }
648− part = p
603+ parts = append(parts, part)
649 case imageURLType:
604 case imageURLType:
650− p := ImageURLContent{}
605+ part := ImageURLContent{}
651− if err := json.Unmarshal(wrapper.Data, &p); err != nil {
606+ if err := json.Unmarshal(wrapper.Data, &part); err != nil {
652 return nil, err
607 return nil, err
653 }
608 }
654− part = p
609+ parts = append(parts, part)
655 case binaryType:
610 case binaryType:
656− p := BinaryContent{}
611+ part := BinaryContent{}
657− if err := json.Unmarshal(wrapper.Data, &p); err != nil {
612+ if err := json.Unmarshal(wrapper.Data, &part); err != nil {
658 return nil, err
613 return nil, err
659 }
614 }
660− part = p
615+ parts = append(parts, part)
661 case toolCallType:
616 case toolCallType:
662− p := ToolCall{}
617+ part := ToolCall{}
663− if err := json.Unmarshal(wrapper.Data, &p); err != nil {
618+ if err := json.Unmarshal(wrapper.Data, &part); err != nil {
664 return nil, err
619 return nil, err
665 }
620 }
666− part = p
621+ parts = append(parts, part)
667 case toolResultType:
622 case toolResultType:
668− p := ToolResult{}
623+ part := ToolResult{}
669− if err := json.Unmarshal(wrapper.Data, &p); err != nil {
624+ if err := json.Unmarshal(wrapper.Data, &part); err != nil {
670 return nil, err
625 return nil, err
671 }
626 }
672− part = p
627+ parts = append(parts, part)
673 case finishType:
628 case finishType:
674− p := Finish{}
629+ part := Finish{}
675− if err := json.Unmarshal(wrapper.Data, &p); err != nil {
630+ if err := json.Unmarshal(wrapper.Data, &part); err != nil {
676 return nil, err
631 return nil, err
677 }
632 }
678− part = p
633+ parts = append(parts, part)
679 case shellCommandType:
634 case shellCommandType:
680− p := ShellCommand{}
635+ part := ShellCommand{}
681− if err := json.Unmarshal(wrapper.Data, &p); err != nil {
636+ if err := json.Unmarshal(wrapper.Data, &part); err != nil {
682 return nil, err
637 return nil, err
683 }
638 }
684− part = p
639+ parts = append(parts, part)
685 default:
640 default:
686 return nil, fmt.Errorf("unknown part type: %s", wrapper.Type)
641 return nil, fmt.Errorf("unknown part type: %s", wrapper.Type)
687 }
642 }
688− parts = append(parts, part)
689 }
643 }
690
644
691 return parts, nil
645 return parts, nil
internal/oauth/copilot/client.go+7−22
@@ -17,20 +17,14 @@ var assistantRolePattern = regexp.MustCompile(`"role"\s*:\s*"assistant"`)
17// NewClient creates a new HTTP client with a custom transport that adds the
17// NewClient creates a new HTTP client with a custom transport that adds the
18// X-Initiator header based on message history in the request body.
18// X-Initiator header based on message history in the request body.
19func NewClient(isSubAgent, debug bool) *http.Client {
19func NewClient(isSubAgent, debug bool) *http.Client {
20− t := &initiatorTransport{debug: debug, isSubAgent: isSubAgent}
21− if debug {
22− // Build the debug HTTP client once and reuse it on every RoundTrip.
23− t.debugClient = log.NewHTTPClient()
24− }
25 return &http.Client{
20 return &http.Client{
26− Transport: t,
21+ Transport: &initiatorTransport{debug: debug, isSubAgent: isSubAgent},
27 }
22 }
28}
23}
29
24
30type initiatorTransport struct {
25type initiatorTransport struct {
31− debug bool
26+ debug bool
32− isSubAgent bool
27+ isSubAgent bool
33− debugClient *http.Client
34}
28}
35
29
36func (t *initiatorTransport) RoundTrip(req *http.Request) (*http.Response, error) {
30func (t *initiatorTransport) RoundTrip(req *http.Request) (*http.Response, error) {
@@ -43,17 +37,8 @@ func (t *initiatorTransport) RoundTrip(req *http.Request) (*http.Response, error
43 if req == nil {
37 if req == nil {
44 return nil, fmt.Errorf("HTTP request is nil")
38 return nil, fmt.Errorf("HTTP request is nil")
45 }
39 }
46− // Sub-agents are always attributed to the agent initiator regardless of
47− // body content, so short-circuit before reading/cloning the body.
48− if t.isSubAgent {
49− req.Header.Set(xInitiatorHeader, agentInitiator)
50− slog.Debug("Setting X-Initiator header to agent (sub-agent)")
51− return t.roundTrip(req)
52− }
53− if req.Body == nil || req.Body == http.NoBody {
40+ if req.Body == http.NoBody {
54− // No body to inspect; default to user. A nil Body is valid for
41+ // No body to inspect; default to user.
55− // bodyless requests (e.g. GET), and is distinct from http.NoBody,
56− // so both must be handled before reading below.
57 req.Header.Set(xInitiatorHeader, userInitiator)
42 req.Header.Set(xInitiatorHeader, userInitiator)
58 slog.Debug("Setting X-Initiator header to user (no request body)")
43 slog.Debug("Setting X-Initiator header to user (no request body)")
59 return t.roundTrip(req)
44 return t.roundTrip(req)
@@ -75,7 +60,7 @@ func (t *initiatorTransport) RoundTrip(req *http.Request) (*http.Response, error
75 // Check for assistant messages using regex to handle whitespace
60 // Check for assistant messages using regex to handle whitespace
76 // variations in the JSON while avoiding full unmarshalling overhead.
61 // variations in the JSON while avoiding full unmarshalling overhead.
77 initiator := userInitiator
62 initiator := userInitiator
78− if assistantRolePattern.Match(bodyBytes) {
63+ if assistantRolePattern.Match(bodyBytes) || t.isSubAgent {
79 slog.Debug("Setting X-Initiator header to agent (found assistant messages in history)")
64 slog.Debug("Setting X-Initiator header to agent (found assistant messages in history)")
80 initiator = agentInitiator
65 initiator = agentInitiator
81 } else {
66 } else {
@@ -88,7 +73,7 @@ func (t *initiatorTransport) RoundTrip(req *http.Request) (*http.Response, error
88
73
89func (t *initiatorTransport) roundTrip(req *http.Request) (*http.Response, error) {
74func (t *initiatorTransport) roundTrip(req *http.Request) (*http.Response, error) {
90 if t.debug {
75 if t.debug {
91− return t.debugClient.Transport.RoundTrip(req)
76+ return log.NewHTTPClient().Transport.RoundTrip(req)
92 }
77 }
93 return http.DefaultTransport.RoundTrip(req)
78 return http.DefaultTransport.RoundTrip(req)
94}
79}
internal/oauth/copilot/client_test.godeleted−70
@@ -1,70 +0,0 @@
1−package copilot
2−
3−import (
4− "net/http"
5− "net/http/httptest"
6− "strings"
7− "testing"
8−
9− "github.com/stretchr/testify/require"
10−)
11−
12−func TestInitiatorTransportSetsHeader(t *testing.T) {
13− tests := []struct {
14− name string
15− // body builds the request body; nil means a bodyless request.
16− body func() (int, string)
17− want string
18− }{
19− {
20− name: "nil body defaults to user",
21− body: func() (int, string) { return 0, "" }, // req.Body == nil
22− want: "user",
23− },
24− {
25− name: "NoBody defaults to user",
26− body: func() (int, string) { return -1, "" }, // req.Body == http.NoBody
27− want: "user",
28− },
29− {
30− name: "user-only history stays user",
31− body: func() (int, string) { return 1, `{"messages":[{"role":"user"}]}` },
32− want: "user",
33− },
34− {
35− name: "assistant history becomes agent",
36− body: func() (int, string) { return 1, `{"messages":[{"role":"assistant"}]}` },
37− want: "agent",
38− },
39− }
40−
41− for _, tt := range tests {
42− t.Run(tt.name, func(t *testing.T) {
43− var got string
44− srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
45− got = r.Header.Get("X-Initiator")
46− }))
47− defer srv.Close()
48−
49− kind, payload := tt.body()
50− var req *http.Request
51− var err error
52− switch kind {
53− case 0:
54− req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL, nil)
55− case -1:
56− req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL, http.NoBody)
57− default:
58− req, err = http.NewRequestWithContext(t.Context(), http.MethodPost, srv.URL, strings.NewReader(payload))
59− }
60− require.NoError(t, err)
61−
62− client := &http.Client{Transport: &initiatorTransport{}}
63− resp, err := client.Do(req)
64− require.NoError(t, err)
65− resp.Body.Close()
66−
67− require.Equal(t, tt.want, got)
68− })
69− }
70−}
internal/permission/permission.go+47−55
@@ -104,10 +104,10 @@ type permissionService struct {
104 skip atomic.Bool
104 skip atomic.Bool
105 allowedTools []string
105 allowedTools []string
106
106
107− // requestMu serializes the setup of a permission request (up to the
107+ // used to make sure we only process one request at a time
108− // point the pending entry is registered); it is released before the
108+ requestMu sync.Mutex
109− // human wait so parallel requests don't queue behind one another.
109+ activeRequest *PermissionRequest
110− requestMu sync.Mutex
110+ activeRequestMu sync.Mutex
111}
111}
112
112
113// resolve atomically removes the pending request entry for the given
113// resolve atomically removes the pending request entry for the given
@@ -146,6 +146,12 @@ func (s *permissionService) resolve(permission PermissionRequest, granted, denie
146 // per request because Take removes the entry under the map lock,
146 // per request because Take removes the entry under the map lock,
147 // so this send never blocks.
147 // so this send never blocks.
148 respCh <- granted
148 respCh <- granted
149+
150+ s.activeRequestMu.Lock()
151+ if s.activeRequest != nil && s.activeRequest.ID == permission.ID {
152+ s.activeRequest = nil
153+ }
154+ s.activeRequestMu.Unlock()
149 return true
155 return true
150}
156}
151
157
@@ -195,17 +201,26 @@ func (s *permissionService) Request(ctx context.Context, opts CreatePermissionRe
195 return true, nil
201 return true, nil
196 }
202 }
197
203
198− // Set up the request under requestMu, then release it *before*
204+ s.requestMu.Lock()
199− // blocking on the human response. Holding requestMu across the wait
205+ defer s.requestMu.Unlock()
200− // would serialize every permission prompt process-wide, so parallel
206+
201− // sub-agents each needing approval would queue behind one another.
207+ // tell the UI that a permission was requested
202− // Per-request resolution is scoped by the pendingRequests map (keyed
208+ s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
203− // by request ID), so the wait needs no global lock.
209+ ToolCallID: opts.ToolCallID,
204− // Resolve the target path and build the request before taking
210+ })
205− // requestMu. os.Stat is a blocking syscall and none of this setup
211+
206− // touches shared state (workingDir is immutable, uuid.New is
212+ s.autoApproveSessionsMu.RLock()
207− // self-contained), so it does not need to serialize with other
213+ autoApprove := s.autoApproveSessions[opts.SessionID]
208− // in-flight requests.
214+ s.autoApproveSessionsMu.RUnlock()
215+
216+ if autoApprove {
217+ s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
218+ ToolCallID: opts.ToolCallID,
219+ Granted: true,
220+ })
221+ return true, nil
222+ }
223+
209 fileInfo, err := os.Stat(opts.Path)
224 fileInfo, err := os.Stat(opts.Path)
210 dir := opts.Path
225 dir := opts.Path
211 if err == nil {
226 if err == nil {
@@ -230,52 +245,29 @@ func (s *permissionService) Request(ctx context.Context, opts CreatePermissionRe
230 Params: opts.Params,
245 Params: opts.Params,
231 }
246 }
232
247
233− respCh, requestID, resolved, granted := func() (chan bool, string, bool, bool) {
248+ if _, ok := s.sessionPermissions.Get(PermissionKey{
234− s.requestMu.Lock()
249+ SessionID: permission.SessionID,
235− defer s.requestMu.Unlock()
250+ ToolName: permission.ToolName,
236−
251+ Action: permission.Action,
237− // tell the UI that a permission was requested
252+ Path: permission.Path,
253+ }); ok {
238 s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
254 s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
239 ToolCallID: opts.ToolCallID,
255 ToolCallID: opts.ToolCallID,
256+ Granted: true,
240 })
257 })
258+ return true, nil
259+ }
241
260
242− s.autoApproveSessionsMu.RLock()
261+ s.activeRequestMu.Lock()
243− autoApprove := s.autoApproveSessions[opts.SessionID]
244− s.autoApproveSessionsMu.RUnlock()
245−
246− if autoApprove {
247− s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
248− ToolCallID: opts.ToolCallID,
249− Granted: true,
250− })
251− return nil, "", true, true
252− }
253−
254− if _, ok := s.sessionPermissions.Get(PermissionKey{
255− SessionID: permission.SessionID,
256− ToolName: permission.ToolName,
257− Action: permission.Action,
262+ s.activeRequest = &permission
258− Path: permission.Path,
263+ s.activeRequestMu.Unlock()
259− }); ok {
260− s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
261− ToolCallID: opts.ToolCallID,
262− Granted: true,
263− })
264− return nil, "", true, true
265− }
266−
267− respCh := make(chan bool, 1)
268− s.pendingRequests.Set(permission.ID, respCh)
269
264
270− // Publish the request
265+ respCh := make(chan bool, 1)
271− s.Publish(pubsub.CreatedEvent, permission)
266+ s.pendingRequests.Set(permission.ID, respCh)
272− return respCh, permission.ID, false, false
267+ defer s.pendingRequests.Del(permission.ID)
273− }()
274
268
275− if resolved {
269+ // Publish the request
276− return granted, nil
270+ s.Publish(pubsub.CreatedEvent, permission)
277− }
278− defer s.pendingRequests.Del(requestID)
279
271
280 select {
272 select {
281 case <-ctx.Done():
273 case <-ctx.Done():
internal/pubsub/broker.go+27−69
@@ -83,12 +83,6 @@ func (b *Broker[T]) SetMustDeliverTimeout(d time.Duration) {
83}
83}
84
84
85func (b *Broker[T]) Shutdown() {
85func (b *Broker[T]) Shutdown() {
86− b.mu.Lock()
87− defer b.mu.Unlock()
88−
89− // Close b.done under the lock so concurrent Shutdown calls (and the
90− // per-subscriber goroutines, which also close under the lock) cannot
91− // double-close it and panic.
92 select {
86 select {
93 case <-b.done: // Already closed
87 case <-b.done: // Already closed
94 return
88 return
@@ -96,6 +90,9 @@ func (b *Broker[T]) Shutdown() {
96 close(b.done)
90 close(b.done)
97 }
91 }
98
92
93+ b.mu.Lock()
94+ defer b.mu.Unlock()
95+
99 for ch := range b.subs {
96 for ch := range b.subs {
100 delete(b.subs, ch)
97 delete(b.subs, ch)
101 close(ch)
98 close(ch)
@@ -121,20 +118,11 @@ func (b *Broker[T]) Subscribe(ctx context.Context) <-chan Event[T] {
121 b.subCount++
118 b.subCount++
122
119
123 go func() {
120 go func() {
124− // Wake on either the subscriber's context ending or the broker
125− // shutting down. Without the b.done case, a subscriber with a
126− // long-lived context would park here forever after Shutdown,
127− // leaking the goroutine.
128− select {
129− case <-ctx.Done():
121+ <-ctx.Done()
130− case <-b.done:
131− }
132
122
133 b.mu.Lock()
123 b.mu.Lock()
134 defer b.mu.Unlock()
124 defer b.mu.Unlock()
135
125
136− // If the broker shut down, it already closed this channel under
137− // the lock; don't close it again.
138 select {
126 select {
139 case <-b.done:
127 case <-b.done:
140 return
128 return
@@ -191,16 +179,10 @@ func (b *Broker[T]) Publish(t EventType, payload T) {
191 case sub <- event:
179 case sub <- event:
192 default:
180 default:
193 // Channel is full, subscriber is slow — skip this event.
181 // Channel is full, subscriber is slow — skip this event.
194− // Lossy by design; counted so saturation is observable via
182+ // Lossy by design; counted and logged so saturation is
195− // DropCount. The warning is throttled to power-of-two drop
183+ // observable.
196− // counts (1, 2, 4, 8, …): saturation is exactly when this
197− // path runs hottest, and an unthrottled per-event log — while
198− // holding the RLock — would turn a slow subscriber into a
199− // log-storm that slows every publisher further.
200− if n := b.dropCount.Add(1); n&(n-1) == 0 {
184+ b.dropCount.Add(1)
201− slog.Warn("Pubsub buffer full; dropping events",
185+ slog.Warn("Pubsub buffer full; dropping event", "type", t)
202− "type", t, "dropped_total", n)
203− }
204 }
186 }
205 }
187 }
206}
188}
@@ -217,19 +199,6 @@ func (b *Broker[T]) Publish(t EventType, payload T) {
217// after timeout — recovery is the subscriber's responsibility (e.g. a
199// after timeout — recovery is the subscriber's responsibility (e.g. a
218// re-fetch on the next session-visible event).
200// re-fetch on the next session-visible event).
219func (b *Broker[T]) PublishMustDeliver(ctx context.Context, t EventType, payload T) {
201func (b *Broker[T]) PublishMustDeliver(ctx context.Context, t EventType, payload T) {
220− // Hold the read lock across the bounded-blocking sends below. Both
221− // Shutdown and per-subscriber teardown close subscriber channels under
222− // the write lock, so holding RLock here is what guarantees a send can
223− // never race a close. The earlier lock-free snapshot released b.mu
224− // before sending, which let Shutdown/unsubscribe close a channel
225− // mid-send — a data race the race detector flags (the recover() that
226− // used to sit in mustDeliver only masked the resulting panic).
227− //
228− // The cost is bounded and paid only under saturation: a stalled
229− // subscriber can delay Subscribe/Shutdown by at most (number of
230− // subscribers × mustDeliverTimeout). With this app's handful of
231− // subscribers that ceiling is a few hundred milliseconds, and only
232− // when a subscriber's buffer is already full.
233 b.mu.RLock()
202 b.mu.RLock()
234 defer b.mu.RUnlock()
203 defer b.mu.RUnlock()
235
204
@@ -241,38 +210,27 @@ func (b *Broker[T]) PublishMustDeliver(ctx context.Context, t EventType, payload
241
210
242 event := Event[T]{Type: t, Payload: payload}
211 event := Event[T]{Type: t, Payload: payload}
243 timeout := b.mustDeliverTimeout
212 timeout := b.mustDeliverTimeout
213+
244 for sub := range b.subs {
214 for sub := range b.subs {
245− if b.mustDeliver(ctx, sub, event, timeout) {
215+ // Fast path: non-blocking send.
246− // Context cancelled; stop delivering to the remaining subs.
216+ select {
247− return
217+ case sub <- event:
218+ continue
219+ default:
248 }
220 }
249− }
250−}
251−
252−// mustDeliver performs one bounded-blocking send. It reports whether the
253−// context was cancelled (so the caller stops delivering to remaining
254−// subscribers). The caller holds b.mu.RLock, and every subscriber-channel
255−// close happens under b.mu's write lock, so the channel cannot be closed
256−// concurrently with this send.
257−func (b *Broker[T]) mustDeliver(ctx context.Context, sub chan Event[T], event Event[T], timeout time.Duration) (ctxDone bool) {
258− // Fast path: non-blocking send.
259− select {
260− case sub <- event:
261− return false
262− default:
263− }
264
221
265− // Slow path: bounded blocking send.
222+ // Slow path: bounded blocking send.
266− timer := time.NewTimer(timeout)
223+ timer := time.NewTimer(timeout)
267− defer timer.Stop()
268− select {
224+ select {
269− case sub <- event:
225+ case sub <- event:
226+ timer.Stop()
270− case <-timer.C:
227+ case <-timer.C:
271− b.mustDeliverDropCount.Add(1)
228+ b.mustDeliverDropCount.Add(1)
272− slog.Error("PublishMustDeliver timed out delivering event",
229+ slog.Error("PublishMustDeliver timed out delivering event",
273− "type", event.Type, "timeout", timeout)
230+ "type", t, "timeout", timeout)
274− case <-ctx.Done():
231+ case <-ctx.Done():
232+ timer.Stop()
275− return true
233+ return
234+ }
276 }
235 }
277− return false
278}
236}
internal/pubsub/broker_test.godeleted−117
@@ -1,117 +0,0 @@
1−package pubsub
2−
3−import (
4− "context"
5− "sync"
6− "testing"
7− "time"
8−)
9−
10−// TestConcurrentShutdown ensures concurrent Shutdown calls don't panic on a
11−// double-close of the done channel.
12−func TestConcurrentShutdown(t *testing.T) {
13− t.Parallel()
14− b := NewBroker[int]()
15− ctx, cancel := context.WithCancel(context.Background())
16− defer cancel()
17− for range 8 {
18− b.Subscribe(ctx)
19− }
20−
21− var wg sync.WaitGroup
22− for range 16 {
23− wg.Add(1)
24− go func() {
25− defer wg.Done()
26− b.Shutdown()
27− }()
28− }
29− wg.Wait()
30−}
31−
32−// TestPublishMustDeliverDuringShutdown ensures sending outside the lock is
33−// safe when a subscriber channel is closed concurrently.
34−func TestPublishMustDeliverDuringShutdown(t *testing.T) {
35− t.Parallel()
36− b := NewBrokerWithOptions[int](1)
37− b.SetMustDeliverTimeout(time.Millisecond)
38−
39− ctx, cancel := context.WithCancel(context.Background())
40− defer cancel()
41− for range 8 {
42− b.Subscribe(ctx) // never drained, so buffers fill and sends block
43− }
44−
45− var wg sync.WaitGroup
46− wg.Add(2)
47− go func() {
48− defer wg.Done()
49− for range 50 {
50− b.PublishMustDeliver(context.Background(), CreatedEvent, 1)
51− }
52− }()
53− go func() {
54− defer wg.Done()
55− b.Shutdown()
56− }()
57− wg.Wait()
58−}
59−
60−// TestPublishMustDeliverDuringUnsubscribe ensures a send can't race a
61−// per-subscriber teardown: each subscriber is cancelled individually while
62−// PublishMustDeliver is delivering to it. Run under -race, this fails if the
63−// broker ever sends on a channel a teardown goroutine is concurrently closing.
64−func TestPublishMustDeliverDuringUnsubscribe(t *testing.T) {
65− t.Parallel()
66− b := NewBrokerWithOptions[int](1)
67− b.SetMustDeliverTimeout(time.Millisecond)
68− defer b.Shutdown()
69−
70− var wg sync.WaitGroup
71− wg.Add(1)
72− go func() {
73− defer wg.Done()
74− for range 200 {
75− b.PublishMustDeliver(context.Background(), CreatedEvent, 1)
76− }
77− }()
78−
79− for range 8 {
80− wg.Add(1)
81− go func() {
82− defer wg.Done()
83− // Per-subscriber context: cancelling it tears the subscriber
84− // down (removing + closing its channel) concurrently with the
85− // publisher above. Never drained, so its buffer saturates and
86− // sends block, widening the race window.
87− ctx, cancel := context.WithCancel(context.Background())
88− b.Subscribe(ctx)
89− cancel()
90− }()
91− }
92− wg.Wait()
93−}
94−
95−// TestSubscribeGoroutineExitsOnShutdown ensures the per-subscriber goroutine
96−// unblocks on Shutdown even when its context outlives the broker.
97−func TestSubscribeGoroutineExitsOnShutdown(t *testing.T) {
98− t.Parallel()
99− b := NewBroker[int]()
100− // Background context: without the b.done wakeup this goroutine would
101− // leak forever after Shutdown.
102− ch := b.Subscribe(context.Background())
103− b.Shutdown()
104−
105− select {
106− case _, ok := <-ch:
107− if ok {
108− t.Fatal("expected channel to be closed after Shutdown")
109− }
110− case <-time.After(time.Second):
111− t.Fatal("subscriber channel not closed after Shutdown")
112− }
113−
114− if got := b.GetSubscriberCount(); got != 0 {
115− t.Fatalf("subscriber count = %d, want 0", got)
116− }
117−}
internal/server/proto.go+3−13
@@ -3,6 +3,7 @@ package server
3import (
3import (
4 "encoding/json"
4 "encoding/json"
5 "errors"
5 "errors"
6+ "fmt"
6 "net/http"
7 "net/http"
7
8
8 "github.com/charmbracelet/crush/internal/backend"
9 "github.com/charmbracelet/crush/internal/backend"
@@ -305,12 +306,7 @@ func (c *controllerV1) handleGetWorkspaceEvents(w http.ResponseWriter, r *http.R
305 continue
306 continue
306 }
307 }
307
308
308− // Write the SSE frame in three chunks to avoid the
309+ fmt.Fprintf(w, "data: %s\n\n", data)
309− // []byte->string copy that a "%s"-formatted write would
310− // incur on the (potentially large) marshaled payload.
311− _, _ = w.Write([]byte("data: "))
312− _, _ = w.Write(data)
313− _, _ = w.Write([]byte("\n\n"))
314 flusher.Flush()
310 flusher.Flush()
315 }
311 }
316 }
312 }
@@ -386,17 +382,11 @@ func (c *controllerV1) handleGetWorkspaceSessions(w http.ResponseWriter, r *http
386 return
382 return
387 }
383 }
388 ws, _ := c.backend.GetWorkspace(id)
384 ws, _ := c.backend.GetWorkspace(id)
389− // Snapshot the client->session attachment counts once instead of
390− // re-scanning every client for each session (O(sessions x clients)).
391− var attachedCounts map[string]int
392− if ws != nil {
393− attachedCounts = ws.AttachedClientCountsBySession()
394− }
395 result := make([]proto.Session, len(sessions))
385 result := make([]proto.Session, len(sessions))
396 for i, s := range sessions {
386 for i, s := range sessions {
397 result[i] = sessionToProto(s)
387 result[i] = sessionToProto(s)
398 result[i].IsBusy = isSessionBusy(ws, s.ID)
388 result[i].IsBusy = isSessionBusy(ws, s.ID)
399− result[i].AttachedClients = attachedCounts[s.ID]
389+ result[i].AttachedClients = attachedClients(ws, s.ID)
400 }
390 }
401 jsonEncode(w, result)
391 jsonEncode(w, result)
402}
392}
internal/session/session.go+18−13
@@ -27,7 +27,7 @@ const (
27// HashID returns the XXH3 hash of a session ID (UUID) as a hex string.
27// HashID returns the XXH3 hash of a session ID (UUID) as a hex string.
28func HashID(id string) string {
28func HashID(id string) string {
29 h := xxh3.New()
29 h := xxh3.New()
30− _, _ = h.WriteString(id)
30+ h.WriteString(id)
31 return fmt.Sprintf("%x", h.Sum(nil))
31 return fmt.Sprintf("%x", h.Sum(nil))
32}
32}
33
33
@@ -223,35 +223,29 @@ func (s *service) Save(ctx context.Context, session Session) (Session, error) {
223// UpdateTitleAndUsage updates only the title and usage fields atomically.
223// UpdateTitleAndUsage updates only the title and usage fields atomically.
224// This is safer than fetching, modifying, and saving the entire session.
224// This is safer than fetching, modifying, and saving the entire session.
225func (s *service) UpdateTitleAndUsage(ctx context.Context, sessionID, title string, promptTokens, completionTokens int64, cost float64) error {
225func (s *service) UpdateTitleAndUsage(ctx context.Context, sessionID, title string, promptTokens, completionTokens int64, cost float64) error {
226− dbSession, err := s.q.UpdateSessionTitleAndUsage(ctx, db.UpdateSessionTitleAndUsageParams{
226+ if err := s.q.UpdateSessionTitleAndUsage(ctx, db.UpdateSessionTitleAndUsageParams{
227 ID: sessionID,
227 ID: sessionID,
228 Title: title,
228 Title: title,
229 PromptTokens: promptTokens,
229 PromptTokens: promptTokens,
230 CompletionTokens: completionTokens,
230 CompletionTokens: completionTokens,
231 Cost: cost,
231 Cost: cost,
232− })
233− if err != nil {
232+ }); err != nil {
234 return err
233 return err
235 }
234 }
236− session := s.fromDBItem(dbSession)
235+ s.publishSessionUpdate(ctx, sessionID)
237− s.applyEstimatedUsageState(&session)
238− s.Publish(pubsub.UpdatedEvent, session)
239 return nil
236 return nil
240}
237}
241
238
242// Rename updates only the title of a session without touching updated_at or
239// Rename updates only the title of a session without touching updated_at or
243// usage fields.
240// usage fields.
244func (s *service) Rename(ctx context.Context, id string, title string) error {
241func (s *service) Rename(ctx context.Context, id string, title string) error {
245− dbSession, err := s.q.RenameSession(ctx, db.RenameSessionParams{
242+ if err := s.q.RenameSession(ctx, db.RenameSessionParams{
246 ID: id,
243 ID: id,
247 Title: title,
244 Title: title,
248− })
249− if err != nil {
245+ }); err != nil {
250 return err
246 return err
251 }
247 }
252− session := s.fromDBItem(dbSession)
248+ s.publishSessionUpdate(ctx, id)
253− s.applyEstimatedUsageState(&session)
254− s.Publish(pubsub.UpdatedEvent, session)
255 return nil
249 return nil
256}
250}
257
251
@@ -268,6 +262,17 @@ func (s *service) List(ctx context.Context) ([]Session, error) {
268 return sessions, nil
262 return sessions, nil
269}
263}
270
264
265+// publishSessionUpdate re-fetches a session and publishes an UpdatedEvent so
266+// that UI subscribers reflect title or usage changes.
267+func (s *service) publishSessionUpdate(ctx context.Context, sessionID string) {
268+ session, err := s.Get(ctx, sessionID)
269+ if err != nil {
270+ slog.Error("Failed to re-fetch session for event publish", "error", err, "sessionID", sessionID)
271+ return
272+ }
273+ s.Publish(pubsub.UpdatedEvent, session)
274+}
275+
271func (s *service) applyEstimatedUsageState(session *Session) {
276func (s *service) applyEstimatedUsageState(session *Session) {
272 s.estimatedUsageMu.RLock()
277 s.estimatedUsageMu.RLock()
273 session.EstimatedUsage = s.estimatedUsage[session.ID]
278 session.EstimatedUsage = s.estimatedUsage[session.ID]
internal/shell/background.go+1−1
@@ -185,7 +185,7 @@ func (m *BackgroundShellManager) Cleanup() int {
185 }
185 }
186
186
187 for _, id := range toRemove {
187 for _, id := range toRemove {
188− _ = m.Remove(id)
188+ m.Remove(id)
189 }
189 }
190
190
191 return len(toRemove)
191 return len(toRemove)
internal/shell/persist_message.godeleted−44
@@ -1,44 +0,0 @@
1−package shell
2−
3−import (
4− "context"
5− "log/slog"
6− "strings"
7−
8− "github.com/charmbracelet/crush/internal/message"
9−)
10−
11−// PersistOutput stores a bang-mode shell command result as a user message.
12−// If the target session no longer exists (deleted before or during the
13−// command), persistence is skipped without surfacing an error.
14−func PersistOutput(
15− ctx context.Context,
16− messages message.Service,
17− sessionID, command, output string,
18− exitCode int,
19−) error {
20− if sessionID == "" {
21− return nil
22− }
23−
24− _, err := messages.Create(ctx, sessionID, message.CreateMessageParams{
25− Role: message.User,
26− Parts: []message.ContentPart{message.ShellCommand{
27− Command: command,
28− Output: output,
29− ExitCode: exitCode,
30− }},
31− })
32− // The messages table has a single foreign key (session_id), so an FK
33− // failure here can only mean the session is gone. We match on the error
34− // text because the codebase builds against two swappable SQLite drivers
35− // (modernc and ncruces) and this is the one signal stable across both.
36− if err != nil && strings.Contains(err.Error(), "FOREIGN KEY constraint failed") {
37− slog.Debug(
38− "Skipping shell command persistence: session no longer exists",
39− "session_id", sessionID,
40− )
41− return nil
42− }
43− return err
44−}
internal/shell/persist_message_test.godeleted−69
@@ -1,69 +0,0 @@
1−package shell
2−
3−import (
4− "testing"
5−
6− "github.com/charmbracelet/crush/internal/db"
7− "github.com/charmbracelet/crush/internal/message"
8− "github.com/charmbracelet/crush/internal/session"
9− "github.com/google/uuid"
10− "github.com/stretchr/testify/require"
11−)
12−
13−func TestPersistOutput_SkipsMissingSession(t *testing.T) {
14− t.Parallel()
15−
16− conn, err := db.Connect(t.Context(), t.TempDir())
17− require.NoError(t, err)
18− t.Cleanup(func() { conn.Close() })
19−
20− messages := message.NewService(db.New(conn))
21−
22− missingID := uuid.New().String()
23− err = PersistOutput(t.Context(), messages, missingID, "cat file.txt", "hello", 0)
24− require.NoError(t, err)
25−
26− stored, err := messages.List(t.Context(), missingID)
27− require.NoError(t, err)
28− require.Empty(t, stored)
29−}
30−
31−func TestPersistOutput_NoOpForEmptySessionID(t *testing.T) {
32− t.Parallel()
33−
34− conn, err := db.Connect(t.Context(), t.TempDir())
35− require.NoError(t, err)
36− t.Cleanup(func() { conn.Close() })
37−
38− messages := message.NewService(db.New(conn))
39−
40− require.NoError(t, PersistOutput(t.Context(), messages, "", "echo hi", "hi", 0))
41−}
42−
43−func TestPersistOutput_PersistsForExistingSession(t *testing.T) {
44− t.Parallel()
45−
46− conn, err := db.Connect(t.Context(), t.TempDir())
47− require.NoError(t, err)
48− t.Cleanup(func() { conn.Close() })
49−
50− q := db.New(conn)
51− sessions := session.NewService(q, conn)
52− messages := message.NewService(q)
53−
54− sess, err := sessions.Create(t.Context(), "shell test")
55− require.NoError(t, err)
56−
57− err = PersistOutput(t.Context(), messages, sess.ID, "cat file.txt", "hello", 0)
58− require.NoError(t, err)
59−
60− stored, err := messages.List(t.Context(), sess.ID)
61− require.NoError(t, err)
62− require.Len(t, stored, 1)
63− require.Equal(t, message.User, stored[0].Role)
64− shellParts := stored[0].ShellCommands()
65− require.Len(t, shellParts, 1)
66− require.Equal(t, "cat file.txt", shellParts[0].Command)
67− require.Equal(t, "hello", shellParts[0].Output)
68− require.Zero(t, shellParts[0].ExitCode)
69−}
internal/shell/run.go−30
@@ -255,36 +255,6 @@ func withNonInteractiveEnv(env []string) []string {
255 return append(result, nonInteractiveEnvVars...)
255 return append(result, nonInteractiveEnvVars...)
256}
256}
257
257
258−// herdrEnvVars are the environment variables herdr injects into panes
259−// so agents can report state over its Unix socket API. Subprocesses
260−// must not inherit these: a child process that calls herdr.Init()
261−// would attach to the parent's pane and, on exit, release its agent
262−// authority — making the status vanish. Stripping them here closes
263−// that gap for every command the bash tool runs.
264−var herdrEnvVars = []string{
265− "HERDR_ENV",
266− "HERDR_SOCKET_PATH",
267− "HERDR_PANE_ID",
268−}
269−
270−// withoutHerdrEnv returns env with all HERDR_* variables removed.
271−// The returned slice is a new allocation safe to use concurrently
272−// with the input.
273−func withoutHerdrEnv(env []string) []string {
274− strip := make(map[string]bool, len(herdrEnvVars))
275− for _, k := range herdrEnvVars {
276− strip[k] = true
277− }
278− result := make([]string, 0, len(env))
279− for _, e := range env {
280− if key, _, ok := strings.Cut(e, "="); ok && strip[key] {
281− continue
282− }
283− result = append(result, e)
284− }
285− return result
286−}
287−
288// standardHandlers returns the exec-handler middleware chain used by both
258// standardHandlers returns the exec-handler middleware chain used by both
289// [Run] and [Shell]. Order matters:
259// [Run] and [Shell]. Order matters:
290// 1. builtins first (so Crush's in-process jq wins over any PATH binary);
260// 1. builtins first (so Crush's in-process jq wins over any PATH binary);
internal/shell/run_test.go−43
@@ -309,49 +309,6 @@ func TestWithNonInteractiveEnv_SliceIndependence(t *testing.T) {
309 }
309 }
310}
310}
311
311
312−func TestWithoutHerdrEnv_StripsAllVars(t *testing.T) {
313− t.Parallel()
314− env := []string{
315− "HERDR_ENV=1",
316− "HERDR_SOCKET_PATH=/tmp/herdr.sock",
317− "HERDR_PANE_ID=wA:p1",
318− "PATH=/usr/bin",
319− "HOME=/home/user",
320− }
321− result := withoutHerdrEnv(env)
322− for _, e := range result {
323− if strings.HasPrefix(e, "HERDR_") {
324− t.Errorf("herdr var not stripped: %s", e)
325− }
326− }
327− if !slices.Contains(result, "PATH=/usr/bin") {
328− t.Error("non-herdr var PATH was incorrectly removed")
329− }
330− if !slices.Contains(result, "HOME=/home/user") {
331− t.Error("non-herdr var HOME was incorrectly removed")
332− }
333−}
334−
335−func TestWithoutHerdrEnv_EmptyInput(t *testing.T) {
336− t.Parallel()
337− result := withoutHerdrEnv(nil)
338− if len(result) != 0 {
339− t.Errorf("expected empty result for nil input, got %v", result)
340− }
341−}
342−
343−func TestWithoutHerdrEnv_SliceIndependence(t *testing.T) {
344− t.Parallel()
345− env := []string{"HERDR_ENV=1", "FOO=bar"}
346− result := withoutHerdrEnv(env)
347− env[1] = "FOO=baz"
348− for _, e := range result {
349− if e == "FOO=baz" {
350− t.Error("result shares backing array with input")
351− }
352− }
353−}
354−
355func TestRun_DiscardsNilWriters(t *testing.T) {
312func TestRun_DiscardsNilWriters(t *testing.T) {
356 // No panic when Stdout/Stderr are nil.
313 // No panic when Stdout/Stderr are nil.
357 err := Run(t.Context(), RunOptions{
314 err := Run(t.Context(), RunOptions{
internal/shell/shell.go−5
@@ -94,11 +94,6 @@ func NewShell(opts *Options) *Shell {
94 env = os.Environ()
94 env = os.Environ()
95 }
95 }
96
96
97− // Strip herdr pane-ownership vars so subprocesses (including test
98− // binaries and nested crush instances) can't attach to or release
99− // the parent pane's agent authority.
100− env = withoutHerdrEnv(env)
101−
102 // Allow tools to detect execution by Crush.
97 // Allow tools to detect execution by Crush.
103 env = append(env, CrushEnvMarkers()...)
98 env = append(env, CrushEnvMarkers()...)
104
99
internal/skills/embed.go+1−1
@@ -32,7 +32,7 @@ func DiscoverBuiltinWithStates() ([]*Skill, []*SkillState) {
32 var discovered []*Skill
32 var discovered []*Skill
33 var states []*SkillState
33 var states []*SkillState
34
34
35− _ = fs.WalkDir(builtinFS, "builtin", func(path string, d fs.DirEntry, err error) error {
35+ fs.WalkDir(builtinFS, "builtin", func(path string, d fs.DirEntry, err error) error {
36 if err != nil {
36 if err != nil {
37 return nil
37 return nil
38 }
38 }
internal/skills/skills.go+1−30
@@ -34,26 +34,6 @@ var (
34 latestStatesMu sync.RWMutex
34 latestStatesMu sync.RWMutex
35)
35)
36
36
37−// ignoredWalkDirs is a set of directory names that skill discovery never
38−// descends into. Pruning these avoids walking large dependency/VCS trees that
39−// cannot contain user skills.
40−var ignoredWalkDirs = map[string]bool{
41− "node_modules": true,
42− ".git": true,
43− ".svn": true,
44− ".hg": true,
45− "vendor": true,
46− "dist": true,
47− "build": true,
48− "target": true,
49− ".idea": true,
50− ".vscode": true,
51− "__pycache__": true,
52− ".cache": true,
53− "bin": true,
54− "obj": true,
55−}
56−
57// Skill represents a parsed SKILL.md file.
37// Skill represents a parsed SKILL.md file.
58type Skill struct {
38type Skill struct {
59 Name string `yaml:"name" json:"name"`
39 Name string `yaml:"name" json:"name"`
@@ -269,16 +249,7 @@ func DiscoverWithStates(paths []string) ([]*Skill, []*SkillState) {
269 addState("", path, StateError, err)
249 addState("", path, StateError, err)
270 return nil
250 return nil
271 }
251 }
272− if d.IsDir() {
273− // Prune large/irrelevant directory trees so discovery does
274− // not descend into dependency and VCS directories. Never
275− // prune the walk root itself.
276− if path != base && ignoredWalkDirs[d.Name()] {
277− return filepath.SkipDir
278− }
279− return nil
280− }
281− if d.Name() != SkillFileName {
252+ if d.IsDir() || d.Name() != SkillFileName {
282 return nil
253 return nil
283 }
254 }
284 mu.Lock()
255 mu.Lock()
internal/ui/attachments/attachments.go+2−2
@@ -132,8 +132,8 @@ func (r *Renderer) Render(attachments []message.Attachment, deleting bool, width
132 )
132 )
133 }
133 }
134
134
135− if i == fits && len(attachments) > i+1 {
135+ if i == fits && len(attachments) > i {
136− chips = append(chips, lipgloss.NewStyle().Width(maxItemWidth).Render(fmt.Sprintf("%d more…", len(attachments)-fits-1)))
136+ chips = append(chips, lipgloss.NewStyle().Width(maxItemWidth).Render(fmt.Sprintf("%d more…", len(attachments)-fits)))
137 break
137 break
138 }
138 }
139 }
139 }
internal/ui/chat/agent.go+26−61
@@ -2,7 +2,6 @@ package chat
2
2
3import (
3import (
4 "encoding/json"
4 "encoding/json"
5− "fmt"
6 "strings"
5 "strings"
7
6
8 tea "charm.land/bubbletea/v2"
7 tea "charm.land/bubbletea/v2"
@@ -25,18 +24,6 @@ type NestedToolContainer interface {
25 AddNestedTool(tool ToolMessageItem)
24 AddNestedTool(tool ToolMessageItem)
26}
25}
27
26
28−// collapsedNestedHint is the affordance shown under a collapsed sub-agent
29−// card, indicating its nested tool calls can be revealed with space.
30−const collapsedNestedHint = " ▸ space to expand"
31−
32−// toolCountLabel renders a "N tools" summary for a sub-agent header line.
33−func toolCountLabel(n int) string {
34− if n == 1 {
35− return "1 tool"
36− }
37− return fmt.Sprintf("%d tools", n)
38−}
39−
40// AgentToolMessageItem is a message item that represents an agent tool call.
27// AgentToolMessageItem is a message item that represents an agent tool call.
41type AgentToolMessageItem struct {
28type AgentToolMessageItem struct {
42 *baseToolMessageItem
29 *baseToolMessageItem
@@ -57,7 +44,7 @@ func NewAgentToolMessageItem(
57 canceled bool,
44 canceled bool,
58) *AgentToolMessageItem {
45) *AgentToolMessageItem {
59 t := &AgentToolMessageItem{}
46 t := &AgentToolMessageItem{}
60− t.baseToolMessageItem = newBaseToolMessageItem(sty, toolCall, result, &AgentToolRenderContext{agent: t}, canceled, "")
47+ t.baseToolMessageItem = newBaseToolMessageItem(sty, toolCall, result, &AgentToolRenderContext{agent: t}, canceled)
61 // For the agent tool we keep spinning until the tool call is finished.
48 // For the agent tool we keep spinning until the tool call is finished.
62 t.spinningFunc = func(state SpinningState) bool {
49 t.spinningFunc = func(state SpinningState) bool {
63 return !state.HasResult() && !state.IsCanceled()
50 return !state.HasResult() && !state.IsCanceled()
@@ -147,15 +134,9 @@ func (r *AgentToolRenderContext) RenderTool(sty *styles.Styles, width int, opts
147 _ = json.Unmarshal([]byte(opts.ToolCall.Input), &params)
134 _ = json.Unmarshal([]byte(opts.ToolCall.Input), &params)
148
135
149 prompt := params.Prompt
136 prompt := params.Prompt
150− if !opts.ExpandedContent {
151− prompt = strings.ReplaceAll(prompt, "\n", " ")
137+ prompt = strings.ReplaceAll(prompt, "\n", " ")
152− }
153
138
154− header := toolHeader(sty, opts.Status, "Agent", cappedWidth, opts)
139+ header := toolHeader(sty, opts.Status, "Agent", cappedWidth, opts.Compact)
155− nestedCount := len(r.agent.nestedTools)
156− if nestedCount > 0 {
157− header += " " + sty.Tool.ParamMain.Render(toolCountLabel(nestedCount))
158− }
159 if opts.Compact {
140 if opts.Compact {
160 return header
141 return header
161 }
142 }
@@ -181,23 +162,18 @@ func (r *AgentToolRenderContext) RenderTool(sty *styles.Styles, width int, opts
181 ),
162 ),
182 )
163 )
183
164
184− // Nested tool calls are collapsed by default to keep the conversation
165+ // Build tree with nested tool calls.
185− // readable; press space to expand the full call tree.
186− var parts []string
187− if opts.ExpandedContent {
188− childTools := tree.Root(header)
166+ childTools := tree.Root(header)
167+
189− for _, nestedTool := range r.agent.nestedTools {
168+ for _, nestedTool := range r.agent.nestedTools {
190− childView := nestedTool.Render(remainingWidth)
169+ childView := nestedTool.Render(remainingWidth)
191− childTools.Child(childView)
170+ childTools.Child(childView)
192− }
193− parts = append(parts, childTools.Enumerator(roundedEnumerator(2, taskTagWidth-5)).String())
194− } else {
195− parts = append(parts, header)
196− if nestedCount > 0 {
197− parts = append(parts, sty.Tool.AgentPrompt.Render(collapsedNestedHint))
198− }
199 }
171 }
200
172
173+ // Build parts.
174+ var parts []string
175+ parts = append(parts, childTools.Enumerator(roundedEnumerator(2, taskTagWidth-5)).String())
176+
201 // Show animation if still running.
177 // Show animation if still running.
202 if !opts.HasResult() && !opts.IsCanceled() {
178 if !opts.HasResult() && !opts.IsCanceled() {
203 parts = append(parts, "", opts.Anim.Render())
179 parts = append(parts, "", opts.Anim.Render())
@@ -238,7 +214,7 @@ func NewAgenticFetchToolMessageItem(
238 canceled bool,
214 canceled bool,
239) *AgenticFetchToolMessageItem {
215) *AgenticFetchToolMessageItem {
240 t := &AgenticFetchToolMessageItem{}
216 t := &AgenticFetchToolMessageItem{}
241− t.baseToolMessageItem = newBaseToolMessageItem(sty, toolCall, result, &AgenticFetchToolRenderContext{fetch: t}, canceled, "")
217+ t.baseToolMessageItem = newBaseToolMessageItem(sty, toolCall, result, &AgenticFetchToolRenderContext{fetch: t}, canceled)
242 // For the agentic fetch tool we keep spinning until the tool call is finished.
218 // For the agentic fetch tool we keep spinning until the tool call is finished.
243 t.spinningFunc = func(state SpinningState) bool {
219 t.spinningFunc = func(state SpinningState) bool {
244 return !state.HasResult() && !state.IsCanceled()
220 return !state.HasResult() && !state.IsCanceled()
@@ -319,9 +295,7 @@ func (r *AgenticFetchToolRenderContext) RenderTool(sty *styles.Styles, width int
319 _ = json.Unmarshal([]byte(opts.ToolCall.Input), &params)
295 _ = json.Unmarshal([]byte(opts.ToolCall.Input), &params)
320
296
321 prompt := params.Prompt
297 prompt := params.Prompt
322− if !opts.ExpandedContent {
323− prompt = strings.ReplaceAll(prompt, "\n", " ")
298+ prompt = strings.ReplaceAll(prompt, "\n", " ")
324− }
325
299
326 // Build header with optional URL param.
300 // Build header with optional URL param.
327 var toolParams []string
301 var toolParams []string
@@ -329,11 +303,7 @@ func (r *AgenticFetchToolRenderContext) RenderTool(sty *styles.Styles, width int
329 toolParams = append(toolParams, params.URL)
303 toolParams = append(toolParams, params.URL)
330 }
304 }
331
305
332− header := toolHeader(sty, opts.Status, "Agentic Fetch", cappedWidth, opts, toolParams...)
306+ header := toolHeader(sty, opts.Status, "Agentic Fetch", cappedWidth, opts.Compact, toolParams...)
333− nestedCount := len(r.fetch.nestedTools)
334− if nestedCount > 0 {
335− header += " " + sty.Tool.ParamMain.Render(toolCountLabel(nestedCount))
336− }
337 if opts.Compact {
307 if opts.Compact {
338 return header
308 return header
339 }
309 }
@@ -359,23 +329,18 @@ func (r *AgenticFetchToolRenderContext) RenderTool(sty *styles.Styles, width int
359 ),
329 ),
360 )
330 )
361
331
362− // Nested tool calls are collapsed by default to keep the conversation
332+ // Build tree with nested tool calls.
363− // readable; press space to expand the full call tree.
364− var parts []string
365− if opts.ExpandedContent {
366− childTools := tree.Root(header)
333+ childTools := tree.Root(header)
334+
367− for _, nestedTool := range r.fetch.nestedTools {
335+ for _, nestedTool := range r.fetch.nestedTools {
368− childView := nestedTool.Render(remainingWidth)
336+ childView := nestedTool.Render(remainingWidth)
369− childTools.Child(childView)
337+ childTools.Child(childView)
370− }
371− parts = append(parts, childTools.Enumerator(roundedEnumerator(2, promptTagWidth-5)).String())
372− } else {
373− parts = append(parts, header)
374− if nestedCount > 0 {
375− parts = append(parts, sty.Tool.AgentPrompt.Render(collapsedNestedHint))
376− }
377 }
338 }
378
339
340+ // Build parts.
341+ var parts []string
342+ parts = append(parts, childTools.Enumerator(roundedEnumerator(2, promptTagWidth-5)).String())
343+
379 // Show animation if still running.
344 // Show animation if still running.
380 if !opts.HasResult() && !opts.IsCanceled() {
345 if !opts.HasResult() && !opts.IsCanceled() {
381 parts = append(parts, "", opts.Anim.Render())
346 parts = append(parts, "", opts.Anim.Render())
internal/ui/chat/assistant.go+85−313
@@ -1,7 +1,9 @@
1package chat
1package chat
2
2
3import (
3import (
4+ "encoding/binary"
4 "fmt"
5 "fmt"
6+ "hash/fnv"
5 "strings"
7 "strings"
6
8
7 tea "charm.land/bubbletea/v2"
9 tea "charm.land/bubbletea/v2"
@@ -23,7 +25,7 @@ const assistantMessageTruncateFormat = "… (%d lines hidden) [click or space to
23// promote the view to a full expansion. The promotion is wired through
25// promote the view to a full expansion. The promotion is wired through
24// the existing ToggleExpanded path (click / space) — F5 deliberately
26// the existing ToggleExpanded path (click / space) — F5 deliberately
25// does not add a new keybinding.
27// does not add a new keybinding.
26−const assistantMessageTailWindowFormat = "… (%d earlier lines hidden) [click or space to expand]"
28+const assistantMessageTailWindowFormat = "… %d earlier lines hidden [click or space for full view]"
27
29
28// maxCollapsedThinkingHeight defines the maximum height of the thinking
30// maxCollapsedThinkingHeight defines the maximum height of the thinking
29const maxCollapsedThinkingHeight = 10
31const maxCollapsedThinkingHeight = 10
@@ -91,47 +93,11 @@ func (s *assistantSection) reset() {
91 *s = assistantSection{}
93 *s = assistantSection{}
92}
94}
93
95
94−// FNV-64a constants. We fold bytes into a plain uint64 accumulator
95−// rather than allocating a fnv.New64a per call: the returned
96−// hash.Hash64 escapes to the heap, which is wasteful on the per-render
97−// hashing hot path. The algorithm (offset basis, prime, xor-then-multiply)
98−// is identical to hash/fnv, so hashes are byte-for-byte the same.
99−const (
100− fnvOffset64 = 14695981039346656037
101− fnvPrime64 = 1099511628211
102−)
103−
104−// fnvAddString folds s into an in-progress FNV-64a hash.
105−func fnvAddString(h uint64, s string) uint64 {
106− for i := 0; i < len(s); i++ {
107− h ^= uint64(s[i])
108− h *= fnvPrime64
109− }
110− return h
111−}
112−
113−// fnvAddBytes folds b into an in-progress FNV-64a hash.
114−func fnvAddBytes(h uint64, b []byte) uint64 {
115− for _, c := range b {
116− h ^= uint64(c)
117− h *= fnvPrime64
118− }
119− return h
120−}
121−
122−// fnvAddU64 folds v's 8 little-endian bytes into an in-progress hash,
123−// matching binary.LittleEndian.PutUint64 followed by a Write.
124−func fnvAddU64(h, v uint64) uint64 {
125− for i := 0; i < 8; i++ {
126− h ^= uint64(byte(v >> (8 * i)))
127− h *= fnvPrime64
128− }
129− return h
130−}
131−
132−// fnv64 hashes a single string with FNV-64a.
96+// fnv64 hashes a single string with FNV-64.
133func fnv64(s string) uint64 {
97func fnv64(s string) uint64 {
134− return fnvAddString(fnvOffset64, s)
98+ h := fnv.New64a()
99+ _, _ = h.Write([]byte(s))
100+ return h.Sum64()
135}
101}
136
102
137// fnvFields hashes a list of byte fields with length-prefix framing
103// fnvFields hashes a list of byte fields with length-prefix framing
@@ -140,41 +106,14 @@ func fnv64(s string) uint64 {
140// boundary between two fields). Each field is preceded by its
106// boundary between two fields). Each field is preceded by its
141// length encoded as 8 bytes little-endian.
107// length encoded as 8 bytes little-endian.
142func fnvFields(fields ...[]byte) uint64 {
108func fnvFields(fields ...[]byte) uint64 {
143− h := uint64(fnvOffset64)
109+ h := fnv.New64a()
110+ var lenBuf [8]byte
144 for _, f := range fields {
111 for _, f := range fields {
145− h = fnvAddU64(h, uint64(len(f)))
112+ binary.LittleEndian.PutUint64(lenBuf[:], uint64(len(f)))
146− h = fnvAddBytes(h, f)
113+ _, _ = h.Write(lenBuf[:])
147− }
114+ _, _ = h.Write(f)
148− return h
149−}
150−
151−// assistantRenderKeys bundles the per-render cache keys and tool-call
152−// count computed once at the top of Render/RawRender and threaded
153−// through the render path, so each section's source text is hashed only
154−// once and ToolCalls() is materialized only once per render.
155−type assistantRenderKeys struct {
156− thinkSrc, thinkExtra uint64
157− contentSrc, contentExtra uint64
158− errSrc, errExtra uint64
159− toolCallCount int
160−}
161−
162−// renderKeys computes the per-render keys once. It calls ToolCalls()
163−// exactly once and reuses its length everywhere the render path needs it.
164−func (a *AssistantMessageItem) renderKeys() assistantRenderKeys {
165− toolCallCount := len(a.message.ToolCalls())
166− thinkSrc, thinkExtra := a.thinkingKey(toolCallCount)
167− contentSrc, contentExtra := a.contentKey()
168− errSrc, errExtra := a.errorKey()
169− return assistantRenderKeys{
170− thinkSrc: thinkSrc,
171− thinkExtra: thinkExtra,
172− contentSrc: contentSrc,
173− contentExtra: contentExtra,
174− errSrc: errSrc,
175− errExtra: errExtra,
176− toolCallCount: toolCallCount,
177 }
115 }
116+ return h.Sum64()
178}
117}
179
118
180// AssistantMessageItem represents an assistant message in the chat UI.
119// AssistantMessageItem represents an assistant message in the chat UI.
@@ -269,36 +208,23 @@ func (a *AssistantMessageItem) ID() string {
269
208
270// RawRender implements [MessageItem].
209// RawRender implements [MessageItem].
271func (a *AssistantMessageItem) RawRender(width int) string {
210func (a *AssistantMessageItem) RawRender(width int) string {
272− return a.rawRender(width, a.renderKeys())
273−}
274−
275−// rawRenderBody renders the assistant message body (thinking / content /
276−// error sections) WITHOUT the spinner suffix. The body is stable across
277−// animation ticks — only the spinner changes — so Render caches its
278−// prefixed form and reuses it while re-rendering just the spinner.
279−func (a *AssistantMessageItem) rawRenderBody(width int, keys assistantRenderKeys) string {
280 cappedWidth := cappedMessageWidth(width)
211 cappedWidth := cappedMessageWidth(width)
281− content, height := a.renderMessageContent(cappedWidth, keys)
212+
282− return a.renderHighlighted(content, cappedWidth, height)
213+ var spinner string
283−}
214+ if a.isSpinning() {
284−
285−// rawRender renders the item body plus, when spinning, the spinner suffix.
286−// It is the reference full render; Render reproduces its output byte-for-byte
287−// via a cached body + freshly-prefixed spinner (validated in
288−// assistant_render_test.go).
289−func (a *AssistantMessageItem) rawRender(width int, keys assistantRenderKeys) string {
290− body := a.rawRenderBody(width, keys)
291− if !a.isSpinningWithToolCalls(keys.toolCallCount) {
292− return body
293− }
294− spinner := a.renderSpinning()
215+ spinner = a.renderSpinning()
295− if spinner == "" {
296− return body
297 }
216 }
298− if body != "" {
217+
218+ content, height := a.renderMessageContent(cappedWidth)
219+ highlightedContent := a.renderHighlighted(content, cappedWidth, height)
220+ if spinner != "" {
221+ if highlightedContent != "" {
222+ highlightedContent += "\n\n"
223+ }
299− return body + "\n\n" + spinner
224+ return highlightedContent + spinner
300 }
225 }
301− return spinner
226+
227+ return highlightedContent
302}
228}
303
229
304// Render implements MessageItem.
230// Render implements MessageItem.
@@ -317,82 +243,30 @@ func (a *AssistantMessageItem) Render(width int) string {
317 // drop. Bypass the cache while spinning (RawRender's spinner
243 // drop. Bypass the cache while spinning (RawRender's spinner
318 // suffix changes every animation frame) or while a highlight
244 // suffix changes every animation frame) or while a highlight
319 // range is active (selection drag).
245 // range is active (selection drag).
320− keys := a.renderKeys()
246+ useCache := !a.isSpinning() && !a.isHighlighted()
321− spinning := a.isSpinningWithToolCalls(keys.toolCallCount)
322 cappedWidth := cappedMessageWidth(width)
247 cappedWidth := cappedMessageWidth(width)
323− key := a.prefixCacheKey(cappedWidth, keys)
248+ key := a.prefixCacheKey(cappedWidth)
324−
325− // Only render the style actually used; rendering both wastes work
326− // (lipgloss.Render is not free) when one is discarded.
327− var prefix string
328− if a.focused {
329− prefix = a.sty.Messages.AssistantFocused.Render()
330− } else {
331− prefix = a.sty.Messages.AssistantBlurred.Render()
332− }
333−
334− // Resolve the prefixed body. Its cache key excludes the spinner frame,
335− // so it stays valid across animation ticks — the whole point of the
336− // decoupling: prefix the (potentially long) thinking body once per
337− // content change instead of on every 20fps spinner tick. Highlighting
338− // mutates the body itself, so it bypasses the cache.
339− useCache := !a.isHighlighted()
340− prefixedBody, ok := "", false
341 if useCache {
249 if useCache {
342− prefixedBody, ok = a.getCachedPrefixedRender(width, key)
250+ if cached, ok := a.getCachedPrefixedRender(width, key); ok {
343− }
251+ return cached
344− bodyEmpty := false
345− if !ok {
346− body := a.rawRenderBody(width, keys)
347− bodyEmpty = body == ""
348− prefixedBody = prefixLines(prefix, body)
349− // Cache only a non-empty body: an empty body prefixes to the bare
350− // prefix and is cheap to recompute, and the cache treats "" as a
351− // miss, so caching it would be indistinguishable from no entry.
352− if useCache && !bodyEmpty {
353− a.setCachedPrefixedRender(prefixedBody, width, key)
354 }
252 }
355 }
253 }
356− // A cache hit implies a non-empty body (empty bodies are never cached),
254+ focused := a.sty.Messages.AssistantFocused.Render()
357− // so bodyEmpty stays false on the hit path.
255+ blurred := a.sty.Messages.AssistantBlurred.Render()
358−
256+ rendered := a.RawRender(width)
359− if !spinning {
360− return prefixedBody
361− }
362− spinner := a.renderSpinning()
363− if spinner == "" {
364− return prefixedBody
365− }
366− return assembleSpinningRender(prefix, prefixedBody, bodyEmpty, prefixLines(prefix, spinner))
367−}
368−
369−// prefixLines prepends prefix to every line of s, matching the per-line
370−// focus/selection prefix that Render applies (done manually rather than via
371−// lipgloss.Render, whose wrapping logic is costly for long messages).
372−func prefixLines(prefix, s string) string {
373− lines := strings.Split(s, "\n")
257+ lines := strings.Split(rendered, "\n")
374− var sb strings.Builder
375 for i, line := range lines {
258 for i, line := range lines {
376− if i > 0 {
259+ if a.focused {
377− sb.WriteString("\n")
260+ lines[i] = focused + line
261+ } else {
262+ lines[i] = blurred + line
378 }
263 }
379− sb.WriteString(prefix)
380− sb.WriteString(line)
381 }
264 }
382− return sb.String()
265+ out := strings.Join(lines, "\n")
383−}
266+ if useCache {
384−
267+ a.setCachedPrefixedRender(out, width, key)
385−// assembleSpinningRender reconstructs the prefixed body+spinner output
386−// byte-for-byte identically to prefixLines(prefix, body+"\n\n"+spinner),
387−// but from the separately-prefixed (and body-cached) parts. When the body is
388−// empty the reference render is the spinner alone; otherwise the "\n\n"
389−// separator becomes one prefixed empty line between body and spinner. This
390−// exact identity is validated in assistant_render_test.go.
391−func assembleSpinningRender(prefix, prefixedBody string, bodyEmpty bool, prefixedSpinner string) string {
392− if bodyEmpty {
393− return prefixedSpinner
394 }
268 }
395− return prefixedBody + "\n" + prefix + "\n" + prefixedSpinner
269+ return out
396}
270}
397
271
398// prefixCacheKey builds the F3 prefixed-render cache key. We pack the
272// prefixCacheKey builds the F3 prefixed-render cache key. We pack the
@@ -404,17 +278,27 @@ func assembleSpinningRender(prefix, prefixedBody string, bodyEmpty bool, prefixe
404// folded in too because it controls the composition of
278// folded in too because it controls the composition of
405// renderMessageContent (e.g. appending the constant "Canceled"
279// renderMessageContent (e.g. appending the constant "Canceled"
406// string) — that decision lives outside any section's own hash.
280// string) — that decision lives outside any section's own hash.
407−func (a *AssistantMessageItem) prefixCacheKey(cappedWidth int, keys assistantRenderKeys) uint64 {
281+func (a *AssistantMessageItem) prefixCacheKey(cappedWidth int) uint64 {
408− h := uint64(fnvOffset64)
282+ thinkSrc, thinkExtra := a.thinkingKey()
409− h = fnvAddU64(h, uint64(cappedWidth))
283+ contentSrc, contentExtra := a.contentKey()
410− h = fnvAddU64(h, keys.thinkSrc)
284+ errSrc, errExtra := a.errorKey()
411− h = fnvAddU64(h, keys.thinkExtra)
285+ h := fnv.New64a()
412− h = fnvAddU64(h, keys.contentSrc)
286+ var buf [8]byte
287+ writeU64 := func(v uint64) {
288+ for i := range 8 {
289+ buf[i] = byte(v >> (8 * i))
290+ }
291+ _, _ = h.Write(buf[:])
292+ }
293+ writeU64(uint64(cappedWidth))
294+ writeU64(thinkSrc)
295+ writeU64(thinkExtra)
296+ writeU64(contentSrc)
413− h = fnvAddU64(h, keys.contentExtra)
297+ writeU64(contentExtra)
414− h = fnvAddU64(h, keys.errSrc)
298+ writeU64(errSrc)
415− h = fnvAddU64(h, keys.errExtra)
299+ writeU64(errExtra)
416− h = fnvAddU64(h, a.compositionKey())
300+ writeU64(a.compositionKey())
417− fingerprint := h
301+ fingerprint := h.Sum64()
418 var focusBit uint64
302 var focusBit uint64
419 if a.focused {
303 if a.focused {
420 focusBit = 1
304 focusBit = 1
@@ -443,20 +327,20 @@ func (a *AssistantMessageItem) compositionKey() uint64 {
443// content, and finish reason. Each section is served from its own cache;
327// content, and finish reason. Each section is served from its own cache;
444// only the section whose source text or extras changed since the last
328// only the section whose source text or extras changed since the last
445// render is recomputed.
329// render is recomputed.
446−func (a *AssistantMessageItem) renderMessageContent(width int, keys assistantRenderKeys) (string, int) {
330+func (a *AssistantMessageItem) renderMessageContent(width int) (string, int) {
447 var messageParts []string
331 var messageParts []string
448 thinking := strings.TrimSpace(a.message.ReasoningContent().Thinking)
332 thinking := strings.TrimSpace(a.message.ReasoningContent().Thinking)
449 content := strings.TrimSpace(a.message.Content().Text)
333 content := strings.TrimSpace(a.message.Content().Text)
450
334
451 if thinking != "" {
335 if thinking != "" {
452− messageParts = append(messageParts, a.cachedThinking(width, keys))
336+ messageParts = append(messageParts, a.cachedThinking(width))
453 }
337 }
454
338
455 if content != "" {
339 if content != "" {
456 if thinking != "" {
340 if thinking != "" {
457 messageParts = append(messageParts, "")
341 messageParts = append(messageParts, "")
458 }
342 }
459− messageParts = append(messageParts, a.cachedContent(width, keys))
343+ messageParts = append(messageParts, a.cachedContent(width))
460 }
344 }
461
345
462 if a.message.IsFinished() {
346 if a.message.IsFinished() {
@@ -464,7 +348,7 @@ func (a *AssistantMessageItem) renderMessageContent(width int, keys assistantRen
464 case message.FinishReasonCanceled:
348 case message.FinishReasonCanceled:
465 messageParts = append(messageParts, a.sty.Messages.AssistantCanceled.Render("Canceled"))
349 messageParts = append(messageParts, a.sty.Messages.AssistantCanceled.Render("Canceled"))
466 case message.FinishReasonError:
350 case message.FinishReasonError:
467− messageParts = append(messageParts, a.cachedError(width, keys))
351+ messageParts = append(messageParts, a.cachedError(width))
468 }
352 }
469 }
353 }
470
354
@@ -477,11 +361,11 @@ func (a *AssistantMessageItem) renderMessageContent(width int, keys assistantRen
477// thinking text that affects the rendered output: the view mode
361// thinking text that affects the rendered output: the view mode
478// (collapsed / tail-window / full) and the footer state (which
362// (collapsed / tail-window / full) and the footer state (which
479// depends on IsThinking, ToolCalls, and ThinkingDuration).
363// depends on IsThinking, ToolCalls, and ThinkingDuration).
480−func (a *AssistantMessageItem) thinkingKey(toolCallCount int) (uint64, uint64) {
364+func (a *AssistantMessageItem) thinkingKey() (uint64, uint64) {
481 thinking := a.message.ReasoningContent().Thinking
365 thinking := a.message.ReasoningContent().Thinking
482 srcHash := fnv64(thinking)
366 srcHash := fnv64(thinking)
483
367
484− showFooter := !a.message.IsThinking() || toolCallCount > 0
368+ showFooter := !a.message.IsThinking() || len(a.message.ToolCalls()) > 0
485 var durationStr string
369 var durationStr string
486 if showFooter {
370 if showFooter {
487 duration := a.message.ThinkingDuration()
371 duration := a.message.ThinkingDuration()
@@ -528,20 +412,20 @@ func (a *AssistantMessageItem) errorKey() (uint64, uint64) {
528// caching it on miss. The thinking-box height (used for click target
412// caching it on miss. The thinking-box height (used for click target
529// detection) is preserved across hits via assistantSection.aux so the
413// detection) is preserved across hits via assistantSection.aux so the
530// cached path never desyncs click detection.
414// cached path never desyncs click detection.
531−func (a *AssistantMessageItem) cachedThinking(width int, keys assistantRenderKeys) string {
415+func (a *AssistantMessageItem) cachedThinking(width int) string {
532− srcHash, extra := keys.thinkSrc, keys.thinkExtra
416+ srcHash, extra := a.thinkingKey()
533 if a.thinkingSec.hit(width, srcHash, extra) {
417 if a.thinkingSec.hit(width, srcHash, extra) {
534 a.thinkingBoxHeight = a.thinkingSec.aux
418 a.thinkingBoxHeight = a.thinkingSec.aux
535 return a.thinkingSec.out
419 return a.thinkingSec.out
536 }
420 }
537− out := a.renderThinking(a.message.ReasoningContent().Thinking, width, keys.toolCallCount)
421+ out := a.renderThinking(a.message.ReasoningContent().Thinking, width)
538 a.thinkingSec.store(width, srcHash, extra, out, a.thinkingBoxHeight)
422 a.thinkingSec.store(width, srcHash, extra, out, a.thinkingBoxHeight)
539 return out
423 return out
540}
424}
541
425
542// cachedContent returns the rendered content section.
426// cachedContent returns the rendered content section.
543−func (a *AssistantMessageItem) cachedContent(width int, keys assistantRenderKeys) string {
427+func (a *AssistantMessageItem) cachedContent(width int) string {
544− srcHash, extra := keys.contentSrc, keys.contentExtra
428+ srcHash, extra := a.contentKey()
545 if a.contentSec.hit(width, srcHash, extra) {
429 if a.contentSec.hit(width, srcHash, extra) {
546 return a.contentSec.out
430 return a.contentSec.out
547 }
431 }
@@ -551,8 +435,8 @@ func (a *AssistantMessageItem) cachedContent(width int, keys assistantRenderKeys
551}
435}
552
436
553// cachedError returns the rendered error section.
437// cachedError returns the rendered error section.
554−func (a *AssistantMessageItem) cachedError(width int, keys assistantRenderKeys) string {
438+func (a *AssistantMessageItem) cachedError(width int) string {
555− srcHash, extra := keys.errSrc, keys.errExtra
439+ srcHash, extra := a.errorKey()
556 if a.errorSec.hit(width, srcHash, extra) {
440 if a.errorSec.hit(width, srcHash, extra) {
557 return a.errorSec.out
441 return a.errorSec.out
558 }
442 }
@@ -568,35 +452,18 @@ func (a *AssistantMessageItem) cachedError(width int, keys assistantRenderKeys)
568// boundary problem §4.4 of the design note flags. The bordered
452// boundary problem §4.4 of the design note flags. The bordered
569// ThinkingBox style is applied on top of the (already-windowed)
453// ThinkingBox style is applied on top of the (already-windowed)
570// lines so the visual box matches what the user sees today.
454// lines so the visual box matches what the user sees today.
571−func (a *AssistantMessageItem) renderThinking(thinking string, width int, toolCallCount int) string {
455+func (a *AssistantMessageItem) renderThinking(thinking string, width int) string {
572 renderer := common.QuietMarkdownRenderer(a.sty, width)
456 renderer := common.QuietMarkdownRenderer(a.sty, width)
573−
574− // Cap the raw text handed to glamour while the block is windowed.
575− // The collapsed and tail-window views only ever DISPLAY the last
576− // N rendered lines, so rendering the entire (unbounded) reasoning
577− // trace on every ~33ms streaming flush is wasted work that grows
578− // with the trace — O(n) per flush, O(n²) over the stream — and is
579− // the source of the "long thinking makes the UI lag" problem.
580− // Full expansion is an explicit request to see everything and is
581− // never bounded. droppedLines keeps the "N lines hidden" count
582− // roughly truthful and never affects what is displayed.
583− windowed, droppedLines := boundThinkingInput(thinking, a.thinkingViewMode, width)
584− rendered := a.streamingThinking.Render(windowed, width, renderer)
457+ rendered := a.streamingThinking.Render(thinking, width, renderer)
585 rendered = strings.TrimSpace(rendered)
458 rendered = strings.TrimSpace(rendered)
586
459
587 lines := strings.Split(rendered, "\n")
460 lines := strings.Split(rendered, "\n")
588− renderedLines := len(lines)
461+ totalLines := len(lines)
589− // totalLines counts everything the trace would render to, so the
590− // affordance's hidden-line count stays honest even though only
591− // the windowed tail was actually rendered.
592− totalLines := renderedLines + droppedLines
593
462
594 switch a.thinkingViewMode {
463 switch a.thinkingViewMode {
595 case thinkingCollapsed:
464 case thinkingCollapsed:
596 if totalLines > maxCollapsedThinkingHeight {
465 if totalLines > maxCollapsedThinkingHeight {
597− if renderedLines > maxCollapsedThinkingHeight {
598− lines = lines[renderedLines-maxCollapsedThinkingHeight:]
466+ lines = lines[totalLines-maxCollapsedThinkingHeight:]
599− }
600 hint := a.sty.Messages.ThinkingTruncationHint.Render(
467 hint := a.sty.Messages.ThinkingTruncationHint.Render(
601 fmt.Sprintf(assistantMessageTruncateFormat, totalLines-maxCollapsedThinkingHeight),
468 fmt.Sprintf(assistantMessageTruncateFormat, totalLines-maxCollapsedThinkingHeight),
602 )
469 )
@@ -604,9 +471,7 @@ func (a *AssistantMessageItem) renderThinking(thinking string, width int, toolCa
604 }
471 }
605 case thinkingTailWindow:
472 case thinkingTailWindow:
606 if totalLines > maxExpandedThinkingTailLines {
473 if totalLines > maxExpandedThinkingTailLines {
607− if renderedLines > maxExpandedThinkingTailLines {
608− lines = lines[renderedLines-maxExpandedThinkingTailLines:]
474+ lines = lines[totalLines-maxExpandedThinkingTailLines:]
609− }
610 hint := a.sty.Messages.ThinkingTruncationHint.Render(
475 hint := a.sty.Messages.ThinkingTruncationHint.Render(
611 fmt.Sprintf(assistantMessageTailWindowFormat, totalLines-maxExpandedThinkingTailLines),
476 fmt.Sprintf(assistantMessageTailWindowFormat, totalLines-maxExpandedThinkingTailLines),
612 )
477 )
@@ -620,7 +485,7 @@ func (a *AssistantMessageItem) renderThinking(thinking string, width int, toolCa
620
485
621 var footer string
486 var footer string
622 // if thinking is done add the thought for footer
487 // if thinking is done add the thought for footer
623− if !a.message.IsThinking() || toolCallCount > 0 {
488+ if !a.message.IsThinking() || len(a.message.ToolCalls()) > 0 {
624 duration := a.message.ThinkingDuration()
489 duration := a.message.ThinkingDuration()
625 if duration.String() != "0s" {
490 if duration.String() != "0s" {
626 footer = a.sty.Messages.ThinkingFooterTitle.Render("Thought for ") +
491 footer = a.sty.Messages.ThinkingFooterTitle.Render("Thought for ") +
@@ -635,94 +500,6 @@ func (a *AssistantMessageItem) renderThinking(thinking string, width int, toolCa
635 return result
500 return result
636}
501}
637
502
638−// boundThinkingInput caps the raw reasoning text fed to glamour so a
639−// streaming flush costs O(window) instead of O(entire trace). It
640−// returns the (possibly shortened) text to render plus the number of
641−// source lines dropped ahead of the window.
642−//
643−// A full render is preserved for [thinkingFullExpanded] (the user
644−// asked to see everything) and whenever the trace already fits the
645−// budget, so short blocks and the non-streaming steady state behave
646−// exactly as before.
647−//
648−// The window is snapped forward to a line boundary so a source line
649−// is never cut mid-way, and a code fence left open by the cut is
650−// re-opened at the window head so the tail keeps rendering as code
651−// (matching reality) instead of flipping to prose. Any other
652−// construct a raw cut might tear — a list, table, or blockquote at
653−// the very top of the window — only corrupts lines that sit above
654−// the displayed tail slice and are discarded before display.
655−func boundThinkingInput(thinking string, mode thinkingViewMode, width int) (windowed string, droppedLines int) {
656− budget := thinkingWindowBytes(mode, width)
657− if budget <= 0 || len(thinking) <= budget {
658− return thinking, 0
659− }
660− // The streamingMarkdown incremental cache already keeps a flush
661− // cheap when a safe boundary sits within `budget` of the end: it
662− // re-renders only the small trailing segment. Windowing would
663− // break that fast path (a sliding window is never a prefix
664− // extension, forcing a cache reset every flush), so only window
665− // when the tail since the last boundary is itself larger than the
666− // budget — the "wall of text with no paragraph break" case the
667− // cache cannot handle.
668− if b := findSafeMarkdownBoundary(thinking); b >= 0 && len(thinking)-b <= budget {
669− return thinking, 0
670− }
671− cut := len(thinking) - budget
672− // Snap forward to the start of the next line so the window never
673− // begins in the middle of a source line.
674− if nl := strings.IndexByte(thinking[cut:], '\n'); nl >= 0 {
675− cut += nl + 1
676− }
677− dropped := thinking[:cut]
678− windowed = thinking[cut:]
679− // Re-open a code fence left dangling by the cut so the window's
680− // code renders as code rather than prose.
681− if fenceLineCount(dropped)%2 != 0 {
682− windowed = "```\n" + windowed
683− }
684− return windowed, strings.Count(dropped, "\n")
685−}
686−
687−// thinkingWindowBytes returns the maximum bytes of raw reasoning text
688−// to render for a view mode, or -1 for "no bound". The budget scales
689−// with the wrap width so the window still comfortably overfills the
690−// visible line cap after wrapping, with a floor for narrow widths.
691−func thinkingWindowBytes(mode thinkingViewMode, width int) int {
692− if width < 1 {
693− width = 1
694− }
695− switch mode {
696− case thinkingCollapsed:
697− return max(4096, maxCollapsedThinkingHeight*width*8)
698− case thinkingTailWindow:
699− return max(32768, maxExpandedThinkingTailLines*width*4)
700− default:
701− return -1
702− }
703−}
704−
705−// fenceLineCount counts fenced-code-block delimiter lines in s. An
706−// odd count means a fence is still open at the end of s. Reuses the
707−// same [isFenceLine] rule as the streaming-markdown boundary scanner.
708−func fenceLineCount(s string) int {
709− n := 0
710− start := 0
711− for i := 0; i < len(s); i++ {
712− if s[i] != '\n' {
713− continue
714− }
715− if isFenceLine(s[start:i]) {
716− n++
717− }
718− start = i + 1
719− }
720− if start < len(s) && isFenceLine(s[start:]) {
721− n++
722− }
723− return n
724−}
725−
726// renderMarkdown renders content as markdown. F8 routes the call
503// renderMarkdown renders content as markdown. F8 routes the call
727// through streamingContent, which caches the glamour render of a
504// through streamingContent, which caches the glamour render of a
728// "stable prefix" so each streaming flush only re-renders the
505// "stable prefix" so each streaming flush only re-renders the
@@ -758,16 +535,11 @@ func (a *AssistantMessageItem) renderError(width int) string {
758
535
759// isSpinning returns true if the assistant message is still generating.
536// isSpinning returns true if the assistant message is still generating.
760func (a *AssistantMessageItem) isSpinning() bool {
537func (a *AssistantMessageItem) isSpinning() bool {
761− return a.isSpinningWithToolCalls(len(a.message.ToolCalls()))
762−}
763−
764−// isSpinningWithToolCalls is isSpinning given an already-computed
765−// tool-call count, so the render path avoids re-materializing ToolCalls().
766−func (a *AssistantMessageItem) isSpinningWithToolCalls(toolCallCount int) bool {
767 isThinking := a.message.IsThinking()
538 isThinking := a.message.IsThinking()
768 isFinished := a.message.IsFinished()
539 isFinished := a.message.IsFinished()
769 hasContent := strings.TrimSpace(a.message.Content().Text) != ""
540 hasContent := strings.TrimSpace(a.message.Content().Text) != ""
541+ hasToolCalls := len(a.message.ToolCalls()) > 0
770− return (isThinking || !isFinished) && !hasContent && toolCallCount == 0
542+ return (isThinking || !isFinished) && !hasContent && !hasToolCalls
771}
543}
772
544
773// SetMessage is used to update the underlying message. Only the
545// SetMessage is used to update the underlying message. Only the
internal/ui/chat/assistant_render_test.godeleted−78
@@ -1,78 +0,0 @@
1−package chat
2−
3−import (
4− "strings"
5− "testing"
6−)
7−
8−// combineBodySpinner replicates rawRender's spinning combine: an empty body
9−// yields the spinner alone, otherwise the two are joined by a blank line.
10−func combineBodySpinner(body, spinner string) string {
11− if body != "" {
12− return body + "\n\n" + spinner
13− }
14− return spinner
15−}
16−
17−// TestAssembleSpinningRender_ByteIdentical proves the decoupled Render path
18−// (cache the prefixed body, prefix the spinner separately, reassemble) is
19−// byte-for-byte identical to the old path of prefixing the full
20−// body+spinner string in one pass. If this ever diverges, the spinner
21−// decoupling would visibly corrupt assistant rendering.
22−func TestAssembleSpinningRender_ByteIdentical(t *testing.T) {
23− t.Parallel()
24− prefixes := []string{"", "P", "▏ ", "\x1b[38;5;1m│\x1b[0m "}
25− bodies := []string{
26− "",
27− "single line",
28− "line1\nline2\nline3",
29− "has\n\ninternal blank",
30− "trailing\n",
31− }
32− spinners := []string{
33− "spin",
34− "spin1\nspin2",
35− "⠋ Thinking",
36− }
37− for _, prefix := range prefixes {
38− for _, body := range bodies {
39− for _, spinner := range spinners {
40− want := prefixLines(prefix, combineBodySpinner(body, spinner))
41− got := assembleSpinningRender(
42− prefix,
43− prefixLines(prefix, body),
44− body == "",
45− prefixLines(prefix, spinner),
46− )
47− if got != want {
48− t.Errorf("prefix=%q body=%q spinner=%q\n got=%q\nwant=%q",
49− prefix, body, spinner, got, want)
50− }
51− }
52− }
53− }
54−}
55−
56−// TestPrefixLines matches the original inline per-line prefix loop.
57−func TestPrefixLines(t *testing.T) {
58− t.Parallel()
59− cases := []struct{ prefix, in, want string }{
60− {"P", "", "P"},
61− {"P", "a", "Pa"},
62− {"P", "a\nb", "Pa\nPb"},
63− {"P", "a\n\nb", "Pa\nP\nPb"},
64− {"", "a\nb", "a\nb"},
65− {"P", "trailing\n", "Ptrailing\nP"},
66− }
67− for _, c := range cases {
68− if got := prefixLines(c.prefix, c.in); got != c.want {
69− t.Errorf("prefixLines(%q,%q)=%q want %q", c.prefix, c.in, got, c.want)
70− }
71− }
72− // Cross-check against strings.Split semantics for a random-ish sample.
73− s := "x1\nx2\n\nx3"
74− want := strings.ReplaceAll("Q"+strings.ReplaceAll(s, "\n", "\nQ"), "\r", "")
75− if got := prefixLines("Q", s); got != want {
76− t.Errorf("prefixLines cross-check: got %q want %q", got, want)
77− }
78−}
internal/ui/chat/assistant_section_cache_test.go+3−3
@@ -149,8 +149,8 @@ func TestAssistantSectionCache_HashKeyDiscrimination(t *testing.T) {
149 itemA := NewAssistantMessageItem(&sty, msgA).(*AssistantMessageItem)
149 itemA := NewAssistantMessageItem(&sty, msgA).(*AssistantMessageItem)
150 itemB := NewAssistantMessageItem(&sty, msgB).(*AssistantMessageItem)
150 itemB := NewAssistantMessageItem(&sty, msgB).(*AssistantMessageItem)
151
151
152− thinkSrcA, _ := itemA.thinkingKey(0)
152+ thinkSrcA, _ := itemA.thinkingKey()
153− thinkSrcB, _ := itemB.thinkingKey(0)
153+ thinkSrcB, _ := itemB.thinkingKey()
154 require.NotEqual(t, thinkSrcA, thinkSrcB,
154 require.NotEqual(t, thinkSrcA, thinkSrcB,
155 "distinct thinking text must produce distinct FNV-64 source hashes")
155 "distinct thinking text must produce distinct FNV-64 source hashes")
156
156
@@ -162,7 +162,7 @@ func TestAssistantSectionCache_HashKeyDiscrimination(t *testing.T) {
162 // Identical source text on a fresh item must produce the same
162 // Identical source text on a fresh item must produce the same
163 // hashes — keying invariant for cache hits.
163 // hashes — keying invariant for cache hits.
164 itemAClone := NewAssistantMessageItem(&sty, thinkingMessage("a3", "thinking A", "content A")).(*AssistantMessageItem)
164 itemAClone := NewAssistantMessageItem(&sty, thinkingMessage("a3", "thinking A", "content A")).(*AssistantMessageItem)
165− thinkSrcAClone, _ := itemAClone.thinkingKey(0)
165+ thinkSrcAClone, _ := itemAClone.thinkingKey()
166 contentSrcAClone, _ := itemAClone.contentKey()
166 contentSrcAClone, _ := itemAClone.contentKey()
167 require.Equal(t, thinkSrcA, thinkSrcAClone)
167 require.Equal(t, thinkSrcA, thinkSrcAClone)
168 require.Equal(t, contentSrcA, contentSrcAClone)
168 require.Equal(t, contentSrcA, contentSrcAClone)
internal/ui/chat/assistant_thinking_bench_test.godeleted−69
@@ -1,69 +0,0 @@
1−package chat
2−
3−import (
4− "fmt"
5− "strings"
6− "testing"
7−
8− "github.com/charmbracelet/crush/internal/message"
9− "github.com/charmbracelet/crush/internal/ui/styles"
10−)
11−
12−// benchThinkingStream simulates a reasoning stream: it grows `body`
13−// to full length in `steps` increments and drives the real
14−// AssistantMessageItem render path (SetMessage + RawRender) on every
15−// increment, exactly as the live TUI does on each ~33ms flush while
16−// the block sits collapsed. It guards against a regression of the
17−// O(n²) full-trace re-render that made long thinking lag the UI:
18−// with boundThinkingInput the per-flush cost is bounded, so the wall
19−// (no-blank-line) case stays close to the blank-line case instead of
20−// blowing up quadratically.
21−func benchThinkingStream(b *testing.B, body string, steps int) {
22− sty := styles.CharmtonePantera()
23− const width = 80
24− b.ReportAllocs()
25− b.ResetTimer()
26− for n := 0; n < b.N; n++ {
27− item := NewAssistantMessageItem(&sty, thinkingMsg("")).(*AssistantMessageItem)
28− for i := 1; i <= steps; i++ {
29− cut := len(body) * i / steps
30− item.SetMessage(thinkingMsg(body[:cut]))
31− _ = item.RawRender(width)
32− }
33− }
34−}
35−
36−func thinkingMsg(thinking string) *message.Message {
37− return &message.Message{
38− ID: "bench",
39− Role: message.Assistant,
40− Parts: []message.ContentPart{
41− message.ReasoningContent{Thinking: thinking, StartedAt: testStartedAt},
42− },
43− }
44−}
45−
46−func benchParagraphs(n int) string {
47− var sb strings.Builder
48− for i := 0; i < n; i++ {
49− fmt.Fprintf(&sb, "Reasoning paragraph %d with a few sentences of prose that a model might emit while working through a problem step by step.\n\n", i)
50− }
51− return sb.String()
52−}
53−
54−// benchWall is one giant paragraph with NO blank line — the case
55−// where no safe markdown boundary ever exists and, before
56−// boundThinkingInput, every flush re-rendered the whole growing
57−// trace.
58−func benchWall(n int) string {
59− var sb strings.Builder
60− for i := 0; i < n; i++ {
61− fmt.Fprintf(&sb, "sentence %d that keeps going with no paragraph break at all. ", i)
62− }
63− return sb.String()
64−}
65−
66−func BenchmarkThinkingStream_BlankLines(b *testing.B) {
67− benchThinkingStream(b, benchParagraphs(200), 100)
68−}
69−func BenchmarkThinkingStream_Wall(b *testing.B) { benchThinkingStream(b, benchWall(1500), 100) }
internal/ui/chat/assistant_thinking_bound_test.godeleted−105
@@ -1,105 +0,0 @@
1−package chat
2−
3−import (
4− "strings"
5− "testing"
6−
7− "github.com/stretchr/testify/require"
8−)
9−
10−// TestBoundThinkingInput_ShortNotWindowed: content within the budget
11−// is returned verbatim so short blocks and the non-streaming steady
12−// state are untouched.
13−func TestBoundThinkingInput_ShortNotWindowed(t *testing.T) {
14− t.Parallel()
15− in := "a short thought\n\nwith two paragraphs"
16− out, dropped := boundThinkingInput(in, thinkingCollapsed, 80)
17− require.Equal(t, in, out)
18− require.Zero(t, dropped)
19−}
20−
21−// TestBoundThinkingInput_FullExpandedNeverWindowed: full expansion is
22−// an explicit request to see everything, so it is never bounded even
23−// for a huge trace.
24−func TestBoundThinkingInput_FullExpandedNeverWindowed(t *testing.T) {
25− t.Parallel()
26− in := strings.Repeat("wall of text with no breaks ", 100000)
27− out, dropped := boundThinkingInput(in, thinkingFullExpanded, 80)
28− require.Equal(t, in, out)
29− require.Zero(t, dropped)
30−}
31−
32−// TestBoundThinkingInput_BoundariedNotWindowed: a large trace whose
33−// tail has a recent safe boundary is NOT windowed — the incremental
34−// cache renders only the small trailing segment, so windowing (which
35−// would reset that cache every flush) must be skipped.
36−func TestBoundThinkingInput_BoundariedNotWindowed(t *testing.T) {
37− t.Parallel()
38− var b strings.Builder
39− for i := 0; i < 5000; i++ {
40− b.WriteString("paragraph with a few words\n\n")
41− }
42− in := b.String()
43− require.Greater(t, len(in), thinkingWindowBytes(thinkingCollapsed, 80),
44− "fixture must exceed the budget so only the boundary gate can spare it")
45− out, dropped := boundThinkingInput(in, thinkingCollapsed, 80)
46− require.Equal(t, in, out, "boundaried content must not be windowed")
47− require.Zero(t, dropped)
48−}
49−
50−// TestBoundThinkingInput_WallWindowed: a long unbroken wall (no
51−// paragraph break the incremental cache can use) IS windowed to a
52−// bounded tail.
53−func TestBoundThinkingInput_WallWindowed(t *testing.T) {
54− t.Parallel()
55− budget := thinkingWindowBytes(thinkingCollapsed, 80)
56− in := strings.Repeat("word ", budget) // one giant line, no '\n'
57− out, _ := boundThinkingInput(in, thinkingCollapsed, 80)
58− require.Less(t, len(out), len(in), "a wall past budget must be shortened")
59− require.LessOrEqual(t, len(out), budget+8, "window must respect the budget (+fence slack)")
60− require.True(t, strings.HasSuffix(in, out), "window must be a tail of the source")
61−}
62−
63−// TestBoundThinkingInput_SnapsToLineBoundary: when a newline exists
64−// inside the trailing budget the window begins at the start of a
65−// line, never mid-line.
66−func TestBoundThinkingInput_SnapsToLineBoundary(t *testing.T) {
67− t.Parallel()
68− budget := thinkingWindowBytes(thinkingCollapsed, 80)
69− // A long single line (forces windowing) followed by clean lines
70− // within the budget window.
71− head := strings.Repeat("x", budget)
72− tail := "\nclean line one\nclean line two\n"
73− out, dropped := boundThinkingInput(head+tail, thinkingCollapsed, 80)
74− require.Greater(t, dropped, 0)
75− require.False(t, strings.HasPrefix(out, "x"),
76− "window must snap past the truncated head line")
77− require.True(t, strings.HasPrefix(out, "clean line"))
78−}
79−
80−// TestBoundThinkingInput_ReopensDanglingFence: when the dropped
81−// prefix leaves a code fence open, the window is prefixed with a
82−// fence opener so the tail renders as code (matching reality) instead
83−// of flipping to prose.
84−func TestBoundThinkingInput_ReopensDanglingFence(t *testing.T) {
85− t.Parallel()
86− budget := thinkingWindowBytes(thinkingCollapsed, 80)
87− // Open a fence, then emit a long body (no closing fence) that
88− // pushes the opener out of the window.
89− in := "```go\n" + strings.Repeat("code line here\n", budget/15+500)
90− out, dropped := boundThinkingInput(in, thinkingCollapsed, 80)
91− require.Greater(t, dropped, 0, "fixture must actually be windowed")
92− require.True(t, strings.HasPrefix(out, "```\n"),
93− "a dangling open fence must be re-opened at the window head")
94−}
95−
96−// TestFenceLineCount checks parity accounting used to detect a
97−// dangling open fence.
98−func TestFenceLineCount(t *testing.T) {
99− t.Parallel()
100− require.Equal(t, 0, fenceLineCount("no fences here\njust prose\n"))
101− require.Equal(t, 2, fenceLineCount("```go\ncode\n```\n"))
102− require.Equal(t, 1, fenceLineCount("```\nopen but never closed\n"))
103− // A final line with no trailing newline still counts.
104− require.Equal(t, 1, fenceLineCount("prose\n```"))
105−}
internal/ui/chat/bash.go+7−10
@@ -31,7 +31,7 @@ func NewBashToolMessageItem(
31 result *message.ToolResult,
31 result *message.ToolResult,
32 canceled bool,
32 canceled bool,
33) ToolMessageItem {
33) ToolMessageItem {
34− return newBaseToolMessageItem(sty, toolCall, result, &BashToolRenderContext{}, canceled, "")
34+ return newBaseToolMessageItem(sty, toolCall, result, &BashToolRenderContext{}, canceled)
35}
35}
36
36
37// BashToolRenderContext renders bash tool messages.
37// BashToolRenderContext renders bash tool messages.
@@ -62,17 +62,14 @@ func (b *BashToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *
62 }
62 }
63
63
64 // Regular bash command.
64 // Regular bash command.
65− cmd := params.Command
66− if !opts.ExpandedContent {
67− cmd = strings.ReplaceAll(cmd, "\n", " ")
65+ cmd := strings.ReplaceAll(params.Command, "\n", " ")
68− }
69 cmd = strings.ReplaceAll(cmd, "\t", " ")
66 cmd = strings.ReplaceAll(cmd, "\t", " ")
70 toolParams := []string{cmd}
67 toolParams := []string{cmd}
71 if params.RunInBackground {
68 if params.RunInBackground {
72 toolParams = append(toolParams, "background", "true")
69 toolParams = append(toolParams, "background", "true")
73 }
70 }
74
71
75− header := toolHeader(sty, opts.Status, "Bash", cappedWidth, opts, toolParams...)
72+ header := toolHeader(sty, opts.Status, "Bash", cappedWidth, opts.Compact, toolParams...)
76 if opts.Compact {
73 if opts.Compact {
77 return header
74 return header
78 }
75 }
@@ -116,7 +113,7 @@ func NewJobOutputToolMessageItem(
116 result *message.ToolResult,
113 result *message.ToolResult,
117 canceled bool,
114 canceled bool,
118) ToolMessageItem {
115) ToolMessageItem {
119− return newBaseToolMessageItem(sty, toolCall, result, &JobOutputToolRenderContext{}, canceled, "")
116+ return newBaseToolMessageItem(sty, toolCall, result, &JobOutputToolRenderContext{}, canceled)
120}
117}
121
118
122// JobOutputToolRenderContext renders job_output tool messages.
119// JobOutputToolRenderContext renders job_output tool messages.
@@ -131,7 +128,7 @@ func (j *JobOutputToolRenderContext) RenderTool(sty *styles.Styles, width int, o
131
128
132 var params tools.JobOutputParams
129 var params tools.JobOutputParams
133 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
130 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
134− return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth, false)
131+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
135 }
132 }
136
133
137 var description string
134 var description string
@@ -167,7 +164,7 @@ func NewJobKillToolMessageItem(
167 result *message.ToolResult,
164 result *message.ToolResult,
168 canceled bool,
165 canceled bool,
169) ToolMessageItem {
166) ToolMessageItem {
170− return newBaseToolMessageItem(sty, toolCall, result, &JobKillToolRenderContext{}, canceled, "")
167+ return newBaseToolMessageItem(sty, toolCall, result, &JobKillToolRenderContext{}, canceled)
171}
168}
172
169
173// JobKillToolRenderContext renders job_kill tool messages.
170// JobKillToolRenderContext renders job_kill tool messages.
@@ -182,7 +179,7 @@ func (j *JobKillToolRenderContext) RenderTool(sty *styles.Styles, width int, opt
182
179
183 var params tools.JobKillParams
180 var params tools.JobKillParams
184 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
181 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
185− return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth, false)
182+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
186 }
183 }
187
184
188 var description string
185 var description string
internal/ui/chat/diagnostics.go+2−2
@@ -27,7 +27,7 @@ func NewDiagnosticsToolMessageItem(
27 result *message.ToolResult,
27 result *message.ToolResult,
28 canceled bool,
28 canceled bool,
29) ToolMessageItem {
29) ToolMessageItem {
30− return newBaseToolMessageItem(sty, toolCall, result, &DiagnosticsToolRenderContext{}, canceled, "")
30+ return newBaseToolMessageItem(sty, toolCall, result, &DiagnosticsToolRenderContext{}, canceled)
31}
31}
32
32
33// DiagnosticsToolRenderContext renders diagnostics tool messages.
33// DiagnosticsToolRenderContext renders diagnostics tool messages.
@@ -49,7 +49,7 @@ func (d *DiagnosticsToolRenderContext) RenderTool(sty *styles.Styles, width int,
49 mainParam = fsext.PrettyPath(params.FilePath)
49 mainParam = fsext.PrettyPath(params.FilePath)
50 }
50 }
51
51
52− header := toolHeader(sty, opts.Status, "Diagnostics", cappedWidth, opts, mainParam)
52+ header := toolHeader(sty, opts.Status, "Diagnostics", cappedWidth, opts.Compact, mainParam)
53 if opts.Compact {
53 if opts.Compact {
54 return header
54 return header
55 }
55 }
internal/ui/chat/docker_mcp.go+6−6
@@ -28,7 +28,7 @@ func NewDockerMCPToolMessageItem(
28 result *message.ToolResult,
28 result *message.ToolResult,
29 canceled bool,
29 canceled bool,
30) ToolMessageItem {
30) ToolMessageItem {
31− return newBaseToolMessageItem(sty, toolCall, result, &DockerMCPToolRenderContext{}, canceled, "")
31+ return newBaseToolMessageItem(sty, toolCall, result, &DockerMCPToolRenderContext{}, canceled)
32}
32}
33
33
34// DockerMCPToolRenderContext renders Docker MCP tool messages.
34// DockerMCPToolRenderContext renders Docker MCP tool messages.
@@ -138,7 +138,7 @@ func (d *DockerMCPToolRenderContext) RenderTool(sty *styles.Styles, width int, o
138
138
139 // Handle text content.
139 // Handle text content.
140 if opts.Result.Content != "" {
140 if opts.Result.Content != "" {
141− body := renderToolResultTextContent(sty, opts.Result.Content, toolResultContentWidths{Body: bodyWidth, Diff: cappedWidth}, opts.ExpandedContent, opts.DiffTool)
141+ body := renderToolResultTextContent(sty, opts.Result.Content, toolResultContentWidths{Body: bodyWidth, Diff: cappedWidth}, opts.ExpandedContent)
142 parts = append(parts, body)
142 parts = append(parts, body)
143 }
143 }
144
144
@@ -220,12 +220,12 @@ func (d *DockerMCPToolRenderContext) makeHeader(sty *styles.Styles, tool string,
220 icon = sty.Tool.IconPending.Render()
220 icon = sty.Tool.IconPending.Render()
221 }
221 }
222 prefix := fmt.Sprintf("%s %s ", icon, d.formatToolName(sty, tool))
222 prefix := fmt.Sprintf("%s %s ", icon, d.formatToolName(sty, tool))
223− return prefix + toolParamList(sty, params, width-lipgloss.Width(prefix), opts)
223+ return prefix + toolParamList(sty, params, width-lipgloss.Width(prefix))
224}
224}
225
225
226func (d *DockerMCPToolRenderContext) formatToolName(sty *styles.Styles, tool string) string {
226func (d *DockerMCPToolRenderContext) formatToolName(sty *styles.Styles, tool string) string {
227 mainTool := "Docker MCP"
227 mainTool := "Docker MCP"
228− var action string
228+ action := tool
229 actionStyle := sty.Tool.MCPToolName
229 actionStyle := sty.Tool.MCPToolName
230 switch tool {
230 switch tool {
231 case "mcp-exec":
231 case "mcp-exec":
@@ -254,7 +254,7 @@ func (d *DockerMCPToolRenderContext) formatToolName(sty *styles.Styles, tool str
254}
254}
255
255
256func (d *DockerMCPToolRenderContext) makeCompactHeader(sty *styles.Styles, tool string, width int, params ...string) string {
256func (d *DockerMCPToolRenderContext) makeCompactHeader(sty *styles.Styles, tool string, width int, params ...string) string {
257− var action string
257+ action := tool
258 switch tool {
258 switch tool {
259 case "mcp-exec":
259 case "mcp-exec":
260 action = "exec"
260 action = "exec"
@@ -274,7 +274,7 @@ func (d *DockerMCPToolRenderContext) makeCompactHeader(sty *styles.Styles, tool
274 }
274 }
275
275
276 name := fmt.Sprintf("Docker MCP: %s", action)
276 name := fmt.Sprintf("Docker MCP: %s", action)
277− return toolHeader(sty, ToolStatusSuccess, name, width, &ToolRenderOpts{Compact: true}, params...)
277+ return toolHeader(sty, ToolStatusSuccess, name, width, true, params...)
278}
278}
279
279
280// IsDockerMCPTool returns true if the tool name is a Docker MCP tool.
280// IsDockerMCPTool returns true if the tool name is a Docker MCP tool.
internal/ui/chat/fetch.go+10−10
@@ -26,7 +26,7 @@ func NewFetchToolMessageItem(
26 result *message.ToolResult,
26 result *message.ToolResult,
27 canceled bool,
27 canceled bool,
28) ToolMessageItem {
28) ToolMessageItem {
29− return newBaseToolMessageItem(sty, toolCall, result, &FetchToolRenderContext{}, canceled, "")
29+ return newBaseToolMessageItem(sty, toolCall, result, &FetchToolRenderContext{}, canceled)
30}
30}
31
31
32// FetchToolRenderContext renders fetch tool messages.
32// FetchToolRenderContext renders fetch tool messages.
@@ -41,7 +41,7 @@ func (f *FetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts
41
41
42 var params tools.FetchParams
42 var params tools.FetchParams
43 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
43 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
44− return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth, false)
44+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
45 }
45 }
46
46
47 toolParams := []string{params.URL}
47 toolParams := []string{params.URL}
@@ -52,7 +52,7 @@ func (f *FetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts
52 toolParams = append(toolParams, "timeout", formatTimeout(params.Timeout))
52 toolParams = append(toolParams, "timeout", formatTimeout(params.Timeout))
53 }
53 }
54
54
55− header := toolHeader(sty, opts.Status, "Fetch", cappedWidth, opts, toolParams...)
55+ header := toolHeader(sty, opts.Status, "Fetch", cappedWidth, opts.Compact, toolParams...)
56 if opts.Compact {
56 if opts.Compact {
57 return header
57 return header
58 }
58 }
@@ -101,7 +101,7 @@ func NewWebFetchToolMessageItem(
101 result *message.ToolResult,
101 result *message.ToolResult,
102 canceled bool,
102 canceled bool,
103) ToolMessageItem {
103) ToolMessageItem {
104− return newBaseToolMessageItem(sty, toolCall, result, &WebFetchToolRenderContext{}, canceled, "")
104+ return newBaseToolMessageItem(sty, toolCall, result, &WebFetchToolRenderContext{}, canceled)
105}
105}
106
106
107// WebFetchToolRenderContext renders web_fetch tool messages.
107// WebFetchToolRenderContext renders web_fetch tool messages.
@@ -111,16 +111,16 @@ type WebFetchToolRenderContext struct{}
111func (w *WebFetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
111func (w *WebFetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
112 cappedWidth := cappedMessageWidth(width)
112 cappedWidth := cappedMessageWidth(width)
113 if opts.IsPending() {
113 if opts.IsPending() {
114− return pendingTool(sty, "Web Fetch", opts.Anim, opts.Compact)
114+ return pendingTool(sty, "Fetch", opts.Anim, opts.Compact)
115 }
115 }
116
116
117 var params tools.WebFetchParams
117 var params tools.WebFetchParams
118 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
118 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
119− return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth, false)
119+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
120 }
120 }
121
121
122 toolParams := []string{params.URL}
122 toolParams := []string{params.URL}
123− header := toolHeader(sty, opts.Status, "Web Fetch", cappedWidth, opts, toolParams...)
123+ header := toolHeader(sty, opts.Status, "Fetch", cappedWidth, opts.Compact, toolParams...)
124 if opts.Compact {
124 if opts.Compact {
125 return header
125 return header
126 }
126 }
@@ -155,7 +155,7 @@ func NewWebSearchToolMessageItem(
155 result *message.ToolResult,
155 result *message.ToolResult,
156 canceled bool,
156 canceled bool,
157) ToolMessageItem {
157) ToolMessageItem {
158− return newBaseToolMessageItem(sty, toolCall, result, &WebSearchToolRenderContext{}, canceled, "")
158+ return newBaseToolMessageItem(sty, toolCall, result, &WebSearchToolRenderContext{}, canceled)
159}
159}
160
160
161// WebSearchToolRenderContext renders web_search tool messages.
161// WebSearchToolRenderContext renders web_search tool messages.
@@ -170,11 +170,11 @@ func (w *WebSearchToolRenderContext) RenderTool(sty *styles.Styles, width int, o
170
170
171 var params tools.WebSearchParams
171 var params tools.WebSearchParams
172 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
172 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
173− return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth, false)
173+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
174 }
174 }
175
175
176 toolParams := []string{params.Query}
176 toolParams := []string{params.Query}
177− header := toolHeader(sty, opts.Status, "Search", cappedWidth, opts, toolParams...)
177+ header := toolHeader(sty, opts.Status, "Search", cappedWidth, opts.Compact, toolParams...)
178 if opts.Compact {
178 if opts.Compact {
179 return header
179 return header
180 }
180 }
internal/ui/chat/file.go+22−22
@@ -29,7 +29,7 @@ func NewViewToolMessageItem(
29 result *message.ToolResult,
29 result *message.ToolResult,
30 canceled bool,
30 canceled bool,
31) ToolMessageItem {
31) ToolMessageItem {
32− return newBaseToolMessageItem(sty, toolCall, result, &ViewToolRenderContext{}, canceled, "")
32+ return newBaseToolMessageItem(sty, toolCall, result, &ViewToolRenderContext{}, canceled)
33}
33}
34
34
35// ViewToolRenderContext renders view tool messages.
35// ViewToolRenderContext renders view tool messages.
@@ -44,7 +44,7 @@ func (v *ViewToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *
44
44
45 var params tools.ViewParams
45 var params tools.ViewParams
46 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
46 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
47− return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth, false)
47+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
48 }
48 }
49
49
50 file := fsext.PrettyPath(params.FilePath)
50 file := fsext.PrettyPath(params.FilePath)
@@ -56,7 +56,7 @@ func (v *ViewToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *
56 toolParams = append(toolParams, "offset", fmt.Sprintf("%d", params.Offset))
56 toolParams = append(toolParams, "offset", fmt.Sprintf("%d", params.Offset))
57 }
57 }
58
58
59− header := toolHeader(sty, opts.Status, "View", cappedWidth, opts, toolParams...)
59+ header := toolHeader(sty, opts.Status, "View", cappedWidth, opts.Compact, toolParams...)
60 if opts.Compact {
60 if opts.Compact {
61 return header
61 return header
62 }
62 }
@@ -115,7 +115,7 @@ func NewWriteToolMessageItem(
115 result *message.ToolResult,
115 result *message.ToolResult,
116 canceled bool,
116 canceled bool,
117) ToolMessageItem {
117) ToolMessageItem {
118− return newBaseToolMessageItem(sty, toolCall, result, &WriteToolRenderContext{}, canceled, "")
118+ return newBaseToolMessageItem(sty, toolCall, result, &WriteToolRenderContext{}, canceled)
119}
119}
120
120
121// WriteToolRenderContext renders write tool messages.
121// WriteToolRenderContext renders write tool messages.
@@ -130,11 +130,11 @@ func (w *WriteToolRenderContext) RenderTool(sty *styles.Styles, width int, opts
130
130
131 var params tools.WriteParams
131 var params tools.WriteParams
132 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
132 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
133− return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth, false)
133+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
134 }
134 }
135
135
136 file := fsext.PrettyPath(params.FilePath)
136 file := fsext.PrettyPath(params.FilePath)
137− header := toolHeader(sty, opts.Status, "Write", cappedWidth, opts, file)
137+ header := toolHeader(sty, opts.Status, "Write", cappedWidth, opts.Compact, file)
138 if opts.Compact {
138 if opts.Compact {
139 return header
139 return header
140 }
140 }
@@ -150,11 +150,11 @@ func (w *WriteToolRenderContext) RenderTool(sty *styles.Styles, width int, opts
150 if opts.Result.IsError {
150 if opts.Result.IsError {
151 var meta tools.WriteResponseMetadata
151 var meta tools.WriteResponseMetadata
152 if err := json.Unmarshal([]byte(opts.Result.Metadata), &meta); err == nil && meta.Diff != "" {
152 if err := json.Unmarshal([]byte(opts.Result.Metadata), &meta); err == nil && meta.Diff != "" {
153− errLine := toolErrorContent(sty, opts.Result, cappedWidth, opts.ExpandedContent)
153+ errLine := toolErrorContent(sty, opts.Result, cappedWidth)
154− diff := toolOutputDiffContentFromUnified(sty, meta.Diff, cappedWidth, opts.ExpandedContent, opts.DiffTool)
154+ diff := toolOutputDiffContentFromUnified(sty, meta.Diff, cappedWidth, opts.ExpandedContent)
155 return strings.Join([]string{header, "", errLine, "", diff}, "\n")
155 return strings.Join([]string{header, "", errLine, "", diff}, "\n")
156 }
156 }
157− return joinToolParts(header, toolErrorContent(sty, opts.Result, cappedWidth, opts.ExpandedContent))
157+ return joinToolParts(header, toolErrorContent(sty, opts.Result, cappedWidth))
158 }
158 }
159
159
160 // Render code content with syntax highlighting.
160 // Render code content with syntax highlighting.
@@ -184,7 +184,7 @@ func NewEditToolMessageItem(
184 result *message.ToolResult,
184 result *message.ToolResult,
185 canceled bool,
185 canceled bool,
186) ToolMessageItem {
186) ToolMessageItem {
187− return newBaseToolMessageItem(sty, toolCall, result, &EditToolRenderContext{}, canceled, "")
187+ return newBaseToolMessageItem(sty, toolCall, result, &EditToolRenderContext{}, canceled)
188}
188}
189
189
190// EditToolRenderContext renders edit tool messages.
190// EditToolRenderContext renders edit tool messages.
@@ -199,11 +199,11 @@ func (e *EditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *
199
199
200 var params tools.EditParams
200 var params tools.EditParams
201 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
201 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
202− return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width, false)
202+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width)
203 }
203 }
204
204
205 file := fsext.PrettyPath(params.FilePath)
205 file := fsext.PrettyPath(params.FilePath)
206− header := toolHeader(sty, opts.Status, "Edit", width, opts, file)
206+ header := toolHeader(sty, opts.Status, "Edit", width, opts.Compact, file)
207 if opts.Compact {
207 if opts.Compact {
208 return header
208 return header
209 }
209 }
@@ -223,11 +223,11 @@ func (e *EditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *
223 return joinToolParts(header, body)
223 return joinToolParts(header, body)
224 }
224 }
225
225
226− diff := toolOutputDiffContent(sty, file, meta.OldContent, meta.NewContent, width, opts.ExpandedContent, opts.DiffTool)
226+ diff := toolOutputDiffContent(sty, file, meta.OldContent, meta.NewContent, width, opts.ExpandedContent)
227
227
228 // On error (e.g. denied permission), show error above the diff.
228 // On error (e.g. denied permission), show error above the diff.
229 if opts.Result.IsError {
229 if opts.Result.IsError {
230− errLine := toolErrorContent(sty, opts.Result, width, opts.ExpandedContent)
230+ errLine := toolErrorContent(sty, opts.Result, width)
231 return strings.Join([]string{header, "", errLine, "", diff}, "\n")
231 return strings.Join([]string{header, "", errLine, "", diff}, "\n")
232 }
232 }
233
233
@@ -252,7 +252,7 @@ func NewMultiEditToolMessageItem(
252 result *message.ToolResult,
252 result *message.ToolResult,
253 canceled bool,
253 canceled bool,
254) ToolMessageItem {
254) ToolMessageItem {
255− return newBaseToolMessageItem(sty, toolCall, result, &MultiEditToolRenderContext{}, canceled, "")
255+ return newBaseToolMessageItem(sty, toolCall, result, &MultiEditToolRenderContext{}, canceled)
256}
256}
257
257
258// MultiEditToolRenderContext renders multi-edit tool messages.
258// MultiEditToolRenderContext renders multi-edit tool messages.
@@ -267,7 +267,7 @@ func (m *MultiEditToolRenderContext) RenderTool(sty *styles.Styles, width int, o
267
267
268 var params tools.MultiEditParams
268 var params tools.MultiEditParams
269 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
269 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
270− return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width, false)
270+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width)
271 }
271 }
272
272
273 file := fsext.PrettyPath(params.FilePath)
273 file := fsext.PrettyPath(params.FilePath)
@@ -276,7 +276,7 @@ func (m *MultiEditToolRenderContext) RenderTool(sty *styles.Styles, width int, o
276 toolParams = append(toolParams, "edits", fmt.Sprintf("%d", len(params.Edits)))
276 toolParams = append(toolParams, "edits", fmt.Sprintf("%d", len(params.Edits)))
277 }
277 }
278
278
279− header := toolHeader(sty, opts.Status, "Multi-Edit", width, opts, toolParams...)
279+ header := toolHeader(sty, opts.Status, "Multi-Edit", width, opts.Compact, toolParams...)
280 if opts.Compact {
280 if opts.Compact {
281 return header
281 return header
282 }
282 }
@@ -297,11 +297,11 @@ func (m *MultiEditToolRenderContext) RenderTool(sty *styles.Styles, width int, o
297 }
297 }
298
298
299 // Render diff with optional failed edits note.
299 // Render diff with optional failed edits note.
300− diff := toolOutputMultiEditDiffContent(sty, file, meta, len(params.Edits), width, opts.ExpandedContent, opts.DiffTool)
300+ diff := toolOutputMultiEditDiffContent(sty, file, meta, len(params.Edits), width, opts.ExpandedContent)
301
301
302 // On error (e.g. denied permission), show error above the diff.
302 // On error (e.g. denied permission), show error above the diff.
303 if opts.Result.IsError {
303 if opts.Result.IsError {
304− errLine := toolErrorContent(sty, opts.Result, width, opts.ExpandedContent)
304+ errLine := toolErrorContent(sty, opts.Result, width)
305 return strings.Join([]string{header, "", errLine, "", diff}, "\n")
305 return strings.Join([]string{header, "", errLine, "", diff}, "\n")
306 }
306 }
307
307
@@ -326,7 +326,7 @@ func NewDownloadToolMessageItem(
326 result *message.ToolResult,
326 result *message.ToolResult,
327 canceled bool,
327 canceled bool,
328) ToolMessageItem {
328) ToolMessageItem {
329− return newBaseToolMessageItem(sty, toolCall, result, &DownloadToolRenderContext{}, canceled, "")
329+ return newBaseToolMessageItem(sty, toolCall, result, &DownloadToolRenderContext{}, canceled)
330}
330}
331
331
332// DownloadToolRenderContext renders download tool messages.
332// DownloadToolRenderContext renders download tool messages.
@@ -341,7 +341,7 @@ func (d *DownloadToolRenderContext) RenderTool(sty *styles.Styles, width int, op
341
341
342 var params tools.DownloadParams
342 var params tools.DownloadParams
343 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
343 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
344− return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth, false)
344+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
345 }
345 }
346
346
347 toolParams := []string{params.URL}
347 toolParams := []string{params.URL}
@@ -352,7 +352,7 @@ func (d *DownloadToolRenderContext) RenderTool(sty *styles.Styles, width int, op
352 toolParams = append(toolParams, "timeout", formatTimeout(params.Timeout))
352 toolParams = append(toolParams, "timeout", formatTimeout(params.Timeout))
353 }
353 }
354
354
355− header := toolHeader(sty, opts.Status, "Download", cappedWidth, opts, toolParams...)
355+ header := toolHeader(sty, opts.Status, "Download", cappedWidth, opts.Compact, toolParams...)
356 if opts.Compact {
356 if opts.Compact {
357 return header
357 return header
358 }
358 }
internal/ui/chat/generic.go+4−4
@@ -22,7 +22,7 @@ func NewGenericToolMessageItem(
22 result *message.ToolResult,
22 result *message.ToolResult,
23 canceled bool,
23 canceled bool,
24) ToolMessageItem {
24) ToolMessageItem {
25− return newBaseToolMessageItem(sty, toolCall, result, &GenericToolRenderContext{}, canceled, "")
25+ return newBaseToolMessageItem(sty, toolCall, result, &GenericToolRenderContext{}, canceled)
26}
26}
27
27
28// GenericToolRenderContext renders unknown/generic tool messages.
28// GenericToolRenderContext renders unknown/generic tool messages.
@@ -39,7 +39,7 @@ func (g *GenericToolRenderContext) RenderTool(sty *styles.Styles, width int, opt
39
39
40 var params map[string]any
40 var params map[string]any
41 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
41 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
42− return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth, false)
42+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
43 }
43 }
44
44
45 var toolParams []string
45 var toolParams []string
@@ -48,7 +48,7 @@ func (g *GenericToolRenderContext) RenderTool(sty *styles.Styles, width int, opt
48 toolParams = append(toolParams, string(parsed))
48 toolParams = append(toolParams, string(parsed))
49 }
49 }
50
50
51− header := toolHeader(sty, opts.Status, name, cappedWidth, opts, toolParams...)
51+ header := toolHeader(sty, opts.Status, name, cappedWidth, opts.Compact, toolParams...)
52 if opts.Compact {
52 if opts.Compact {
53 return header
53 return header
54 }
54 }
@@ -68,6 +68,6 @@ func (g *GenericToolRenderContext) RenderTool(sty *styles.Styles, width int, opt
68 return joinToolParts(header, body)
68 return joinToolParts(header, body)
69 }
69 }
70
70
71− body := renderToolResultTextContent(sty, opts.Result.Content, toolResultContentWidths{Body: bodyWidth, Diff: cappedWidth}, opts.ExpandedContent, opts.DiffTool)
71+ body := renderToolResultTextContent(sty, opts.Result.Content, toolResultContentWidths{Body: bodyWidth, Diff: cappedWidth}, opts.ExpandedContent)
72 return joinToolParts(header, body)
72 return joinToolParts(header, body)
73}
73}
internal/ui/chat/incremental_glamour_test.go+16−144
@@ -1,7 +1,6 @@
1package chat
1package chat
2
2
3import (
3import (
4− "fmt"
5 "strings"
4 "strings"
6 "testing"
5 "testing"
7
6
@@ -224,46 +223,12 @@ func TestFindSafeMarkdownBoundary_TableDriven(t *testing.T) {
224 {
223 {
225 name: "closed list then paragraph",
224 name: "closed list then paragraph",
226 content: "- one\n- two\n\nPara.",
225 content: "- one\n- two\n\nPara.",
227− // blank line after the list, then "Para." at
226+ // blank line after the list. Last non-blank line
228− // indent 0 (< listBaseIndent+2). The closure-
227+ // of prefix is "- two" — a list item — so the
229− // aware tracker sees the list close at "Para."
228+ // candidate is REJECTED. (Conservative: we don't
230− // and accepts the boundary at its start — the
229+ // know the list is "closed" without looking at
231− // prefix contains a complete list.
230+ // what follows.)
232− want: len("- one\n- two\n\n"),
231+ want: -1,
233− },
234− {
235− name: "closed ordered list then paragraph",
236− content: "1. first\n2. second\n\nAfter list.",
237− // Same closure rule for ordered lists. Boundary at
238− // the start of "After list." — the list closed on
239− // the dedented non-marker line.
240− want: len("1. first\n2. second\n\n"),
241− },
242− {
243− name: "list then multiple paragraphs",
244− content: "- item\n\nPara one.\n\nPara two.",
245− // The list closes at "Para one." and a boundary is
246− // accepted there. A later boundary at "Para two."
247− // should also be found (no list open anymore).
248− // Latest boundary wins.
249− want: len("- item\n\nPara one.\n\n"),
250− },
251− {
252− name: "loose list continuation keeps list open",
253− content: "- item one\n\n continuation\n\nPara after.",
254− // " continuation" is indented 2 (>=
255− // listBaseIndent+2) so the list stays open
256− // through it. The list only closes at "Para
257− // after." Boundary at the start of "Para after.".
258− want: len("- item one\n\n continuation\n\n"),
259− },
260− {
261− name: "nested list then closure",
262− content: "- outer\n - inner\n\nBack to top level.",
263− // Inner list opens at indent 2. "Back to top
264− // level." at indent 0 closes everything.
265− // Boundary at the start of "Back to top level.".
266− want: len("- outer\n - inner\n\n"),
267 },
232 },
268 {
233 {
269 name: "table at end",
234 name: "table at end",
@@ -673,69 +638,9 @@ func TestStreamingMarkdown_NoSafeBoundaryDoesNotCrash(t *testing.T) {
673 }
638 }
674}
639}
675
640
676−// TestStreamingMarkdown_LongThinkingWithLists is the regression
641+// -----------------------------------------------------------------------
677−// test for the lag bug: a long thinking trace that contains a list
642+// Integration assertions on the wired-in path.
678−// early on, followed by many paragraphs. Before the closure-aware
643+// -----------------------------------------------------------------------
679−// list tracker, the list marker forced findSafeMarkdownBoundary to
680−// return -1 for the ENTIRE trace, so every streaming delta did a
681−// full glamour re-render of the growing text — O(n²) overall and
682−// visible lag on long reasoning.
683−//
684−// With the closure-aware tracker the list closes at the first
685−// dedented paragraph, the stable-prefix cache advances past it,
686−// and subsequent deltas only re-render the trailing partial. This
687−// test asserts the cache actually advances (stablePrefix grows
688−// beyond the list) and the final output is visually equivalent to
689−// a fresh full render.
690−func TestStreamingMarkdown_LongThinkingWithLists(t *testing.T) {
691− t.Parallel()
692−
693− // Build a thinking trace: intro, a short list, then many
694− // paragraphs that simulate a long reasoning section.
695− var doc strings.Builder
696− doc.WriteString("Let me analyze this step by step.\n\n")
697− doc.WriteString("1. First consideration.\n\n")
698− doc.WriteString("2. Second consideration.\n\n")
699− doc.WriteString("3. Third consideration.\n\n")
700− for i := range 200 {
701− fmt.Fprintf(&doc, "Paragraph %d of the reasoning trace. It has enough text to be a real paragraph.\n\n", i)
702− }
703−
704− full := doc.String()
705− listEnd := strings.Index(full, "3. Third consideration.\n\n") + len("3. Third consideration.\n\n")
706− require.Greater(t, listEnd, 0, "test setup: list end offset")
707−
708− const width = 80
709− r := newTestRenderer(t, width)
710− var sm streamingMarkdown
711−
712− // Stream the document in ~50 progressive prefixes. After the
713− // list closes, the cache should advance past listEnd.
714− steps := 50
715− prefixes := progressivePrefixes(full, steps)
716− var lastOut string
717− advanced := false
718− for i, p := range prefixes {
719− if p == "" {
720− continue
721− }
722− lastOut = sm.Render(p, width, r)
723− // Once the prefix extends well past the list, the
724− // stable prefix should have advanced past listEnd.
725− if len(p) > listEnd+200 && len(sm.stablePrefix) > listEnd {
726− advanced = true
727− }
728− _ = i
729− }
730− require.True(t, advanced,
731− "stable prefix must advance past the closed list — if it didn't, "+
732− "every delta is doing a full re-render (the lag bug)")
733−
734− // Final output must be visually equivalent to a fresh render.
735− fresh := freshRender(t, full, width)
736− require.Equal(t, normalizeRender(fresh), normalizeRender(lastOut),
737− "final streaming output must match a fresh full render visually")
738−}
739
644
740// -----------------------------------------------------------------------
645// -----------------------------------------------------------------------
741// T5 / T6 / T7: anywhere-in-prefix hazards (B1 / B2 / B3 from the
646// T5 / T6 / T7: anywhere-in-prefix hazards (B1 / B2 / B3 from the
@@ -821,11 +726,8 @@ func runProgressiveBoundaryRespectTest(t *testing.T, doc string, hazardLineOffse
821// trailing paragraph creates a candidate boundary between the list
726// trailing paragraph creates a candidate boundary between the list
822// item and its continuation; the trailing non-blank line of that
727// item and its continuation; the trailing non-blank line of that
823// candidate prefix is the continuation paragraph (not a list
728// candidate prefix is the continuation paragraph (not a list
824−// marker), so the line-only check would accept it. The closure-
729+// marker), so the line-only check would accept it. The
825−// aware list tracker keeps the list open through the continuation
730+// anywhere-in-prefix list-marker check rejects it.
826−// (indent >= listBaseIndent+2) and only closes it at the dedented
827−// "Trailing paragraph" line, so boundaries INSIDE the list are
828−// rejected while the boundary at the closing line is accepted.
829func TestStreamingMarkdown_LooseListContinuation(t *testing.T) {
731func TestStreamingMarkdown_LooseListContinuation(t *testing.T) {
830 t.Parallel()
732 t.Parallel()
831
733
@@ -842,42 +744,12 @@ func TestStreamingMarkdown_LooseListContinuation(t *testing.T) {
842 }, "\n")
744 }, "\n")
843
745
844 // The first list marker line begins after "Intro paragraph.\n\n".
746 // The first list marker line begins after "Intro paragraph.\n\n".
845− // The closure boundary is the start of "Trailing paragraph after
747+ // The cached stable prefix may include that boundary (BEFORE
846− // the list." — the first safe boundary after the list closes.
748+ // the list opens) but must never advance into the list.
847− listStart := strings.Index(doc, "- item one")
749+ hazardOffset := strings.Index(doc, "- item one")
848− require.Greater(t, listStart, 0, "test setup")
750+ require.Greater(t, hazardOffset, 0, "test setup")
849− closureStart := strings.Index(doc, "Trailing paragraph")
850− require.Greater(t, closureStart, listStart, "test setup")
851−
852− const width = 80
853− const steps = 25
854− renderer := newTestRenderer(t, width)
855− var sm streamingMarkdown
856−
857− prefixes := progressivePrefixes(doc, steps)
858− var lastOut string
859− for i, p := range prefixes {
860− if p == "" {
861− continue
862− }
863− lastOut = sm.Render(p, width, renderer)
864− require.NotEmptyf(t, lastOut, "step %d: empty render", i)
865− // The stable prefix must NEVER be inside the list —
866− // either it's at/before the intro boundary (before the
867− // list opens) or at/after the closure boundary (after
868− // the list closes). A value strictly between listStart
869− // and closureStart would mean a boundary inside the
870− // list was accepted, which would split the loose list.
871− if len(sm.stablePrefix) > listStart && len(sm.stablePrefix) < closureStart {
872− t.Fatalf("step %d: cached stable prefix landed inside the loose list "+
873− "(len=%d, list starts at %d, closure at %d)\nsm.stablePrefix=%q",
874− i, len(sm.stablePrefix), listStart, closureStart, sm.stablePrefix)
875− }
876− }
877
751
878− fresh := freshRender(t, doc, width)
752+ runProgressiveBoundaryRespectTest(t, doc, hazardOffset)
879− require.Equal(t, nonBlankLines(fresh), nonBlankLines(lastOut),
880− "final streaming output must contain the same non-blank lines as a fresh full render")
881}
753}
882
754
883// TestStreamingMarkdown_HTMLBlock locks in the B2 fix. A raw HTML
755// TestStreamingMarkdown_HTMLBlock locks in the B2 fix. A raw HTML
internal/ui/chat/lsp_restart.go+2−2
@@ -22,7 +22,7 @@ func NewLSPRestartToolMessageItem(
22 result *message.ToolResult,
22 result *message.ToolResult,
23 canceled bool,
23 canceled bool,
24) ToolMessageItem {
24) ToolMessageItem {
25− return newBaseToolMessageItem(sty, toolCall, result, &LSPRestartToolRenderContext{}, canceled, "")
25+ return newBaseToolMessageItem(sty, toolCall, result, &LSPRestartToolRenderContext{}, canceled)
26}
26}
27
27
28// LSPRestartToolRenderContext renders lsprestart tool messages.
28// LSPRestartToolRenderContext renders lsprestart tool messages.
@@ -43,7 +43,7 @@ func (r *LSPRestartToolRenderContext) RenderTool(sty *styles.Styles, width int,
43 toolParams = append(toolParams, params.Name)
43 toolParams = append(toolParams, params.Name)
44 }
44 }
45
45
46− header := toolHeader(sty, opts.Status, "Restart LSP", cappedWidth, opts, toolParams...)
46+ header := toolHeader(sty, opts.Status, "Restart LSP", cappedWidth, opts.Compact, toolParams...)
47 if opts.Compact {
47 if opts.Compact {
48 return header
48 return header
49 }
49 }
internal/ui/chat/mcp.go+5−5
@@ -23,7 +23,7 @@ func NewMCPToolMessageItem(
23 result *message.ToolResult,
23 result *message.ToolResult,
24 canceled bool,
24 canceled bool,
25) ToolMessageItem {
25) ToolMessageItem {
26− return newBaseToolMessageItem(sty, toolCall, result, &MCPToolRenderContext{}, canceled, "")
26+ return newBaseToolMessageItem(sty, toolCall, result, &MCPToolRenderContext{}, canceled)
27}
27}
28
28
29// MCPToolRenderContext renders bash tool messages.
29// MCPToolRenderContext renders bash tool messages.
@@ -34,7 +34,7 @@ func (b *MCPToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *T
34 cappedWidth := cappedMessageWidth(width)
34 cappedWidth := cappedMessageWidth(width)
35 toolNameParts := strings.SplitN(opts.ToolCall.Name, "_", 3)
35 toolNameParts := strings.SplitN(opts.ToolCall.Name, "_", 3)
36 if len(toolNameParts) != 3 {
36 if len(toolNameParts) != 3 {
37− return toolErrorContent(sty, &message.ToolResult{Content: "Invalid tool name"}, cappedWidth, false)
37+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid tool name"}, cappedWidth)
38 }
38 }
39 mcpName := humanizedToolName(toolNameParts[1])
39 mcpName := humanizedToolName(toolNameParts[1])
40 toolName := humanizedToolName(toolNameParts[2])
40 toolName := humanizedToolName(toolNameParts[2])
@@ -50,7 +50,7 @@ func (b *MCPToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *T
50
50
51 var params map[string]any
51 var params map[string]any
52 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
52 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
53− return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth, false)
53+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
54 }
54 }
55
55
56 var toolParams []string
56 var toolParams []string
@@ -59,7 +59,7 @@ func (b *MCPToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *T
59 toolParams = append(toolParams, string(parsed))
59 toolParams = append(toolParams, string(parsed))
60 }
60 }
61
61
62− header := toolHeader(sty, opts.Status, name, cappedWidth, opts, toolParams...)
62+ header := toolHeader(sty, opts.Status, name, cappedWidth, opts.Compact, toolParams...)
63 if opts.Compact {
63 if opts.Compact {
64 return header
64 return header
65 }
65 }
@@ -73,6 +73,6 @@ func (b *MCPToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *T
73 }
73 }
74
74
75 bodyWidth := cappedWidth - toolBodyLeftPaddingTotal
75 bodyWidth := cappedWidth - toolBodyLeftPaddingTotal
76− body := renderToolResultTextContent(sty, opts.Result.Content, toolResultContentWidths{Body: bodyWidth, Diff: cappedWidth}, opts.ExpandedContent, opts.DiffTool)
76+ body := renderToolResultTextContent(sty, opts.Result.Content, toolResultContentWidths{Body: bodyWidth, Diff: cappedWidth}, opts.ExpandedContent)
77 return joinToolParts(header, body)
77 return joinToolParts(header, body)
78}
78}
internal/ui/chat/messages.go+1−2
@@ -364,7 +364,7 @@ func cappedMessageWidth(availableWidth int) int {
364//
364//
365// For assistant messages with tool calls, pass a toolResults map to link results.
365// For assistant messages with tool calls, pass a toolResults map to link results.
366// Use BuildToolResultMap to create this map from all messages in a session.
366// Use BuildToolResultMap to create this map from all messages in a session.
367−func ExtractMessageItems(sty *styles.Styles, msg *message.Message, toolResults map[string]message.ToolResult, diffTool string) []MessageItem {
367+func ExtractMessageItems(sty *styles.Styles, msg *message.Message, toolResults map[string]message.ToolResult) []MessageItem {
368 switch msg.Role {
368 switch msg.Role {
369 case message.User:
369 case message.User:
370 // Reconstruct shell command items from ShellCommand parts.
370 // Reconstruct shell command items from ShellCommand parts.
@@ -401,7 +401,6 @@ func ExtractMessageItems(sty *styles.Styles, msg *message.Message, toolResults m
401 tc,
401 tc,
402 result,
402 result,
403 msg.FinishReason() == message.FinishReasonCanceled,
403 msg.FinishReason() == message.FinishReasonCanceled,
404− diffTool,
405 ))
404 ))
406 }
405 }
407 return items
406 return items
internal/ui/chat/references.go+2−2
@@ -23,7 +23,7 @@ func NewReferencesToolMessageItem(
23 result *message.ToolResult,
23 result *message.ToolResult,
24 canceled bool,
24 canceled bool,
25) ToolMessageItem {
25) ToolMessageItem {
26− return newBaseToolMessageItem(sty, toolCall, result, &ReferencesToolRenderContext{}, canceled, "")
26+ return newBaseToolMessageItem(sty, toolCall, result, &ReferencesToolRenderContext{}, canceled)
27}
27}
28
28
29// ReferencesToolRenderContext renders references tool messages.
29// ReferencesToolRenderContext renders references tool messages.
@@ -44,7 +44,7 @@ func (r *ReferencesToolRenderContext) RenderTool(sty *styles.Styles, width int,
44 toolParams = append(toolParams, "path", fsext.PrettyPath(params.Path))
44 toolParams = append(toolParams, "path", fsext.PrettyPath(params.Path))
45 }
45 }
46
46
47− header := toolHeader(sty, opts.Status, "Find References", cappedWidth, opts, toolParams...)
47+ header := toolHeader(sty, opts.Status, "Find References", cappedWidth, opts.Compact, toolParams...)
48 if opts.Compact {
48 if opts.Compact {
49 return header
49 return header
50 }
50 }
internal/ui/chat/search.go+12−12
@@ -27,7 +27,7 @@ func NewGlobToolMessageItem(
27 result *message.ToolResult,
27 result *message.ToolResult,
28 canceled bool,
28 canceled bool,
29) ToolMessageItem {
29) ToolMessageItem {
30− return newBaseToolMessageItem(sty, toolCall, result, &GlobToolRenderContext{}, canceled, "")
30+ return newBaseToolMessageItem(sty, toolCall, result, &GlobToolRenderContext{}, canceled)
31}
31}
32
32
33// GlobToolRenderContext renders glob tool messages.
33// GlobToolRenderContext renders glob tool messages.
@@ -42,7 +42,7 @@ func (g *GlobToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *
42
42
43 var params tools.GlobParams
43 var params tools.GlobParams
44 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
44 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
45− return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth, false)
45+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
46 }
46 }
47
47
48 toolParams := []string{params.Pattern}
48 toolParams := []string{params.Pattern}
@@ -50,7 +50,7 @@ func (g *GlobToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *
50 toolParams = append(toolParams, "path", params.Path)
50 toolParams = append(toolParams, "path", params.Path)
51 }
51 }
52
52
53− header := toolHeader(sty, opts.Status, "Glob", cappedWidth, opts, toolParams...)
53+ header := toolHeader(sty, opts.Status, "Glob", cappedWidth, opts.Compact, toolParams...)
54 if opts.Compact {
54 if opts.Compact {
55 return header
55 return header
56 }
56 }
@@ -86,7 +86,7 @@ func NewGrepToolMessageItem(
86 result *message.ToolResult,
86 result *message.ToolResult,
87 canceled bool,
87 canceled bool,
88) ToolMessageItem {
88) ToolMessageItem {
89− return newBaseToolMessageItem(sty, toolCall, result, &GrepToolRenderContext{}, canceled, "")
89+ return newBaseToolMessageItem(sty, toolCall, result, &GrepToolRenderContext{}, canceled)
90}
90}
91
91
92// GrepToolRenderContext renders grep tool messages.
92// GrepToolRenderContext renders grep tool messages.
@@ -101,7 +101,7 @@ func (g *GrepToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *
101
101
102 var params tools.GrepParams
102 var params tools.GrepParams
103 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
103 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
104− return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth, false)
104+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
105 }
105 }
106
106
107 toolParams := []string{params.Pattern}
107 toolParams := []string{params.Pattern}
@@ -115,7 +115,7 @@ func (g *GrepToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *
115 toolParams = append(toolParams, "literal", "true")
115 toolParams = append(toolParams, "literal", "true")
116 }
116 }
117
117
118− header := toolHeader(sty, opts.Status, "Grep", cappedWidth, opts, toolParams...)
118+ header := toolHeader(sty, opts.Status, "Grep", cappedWidth, opts.Compact, toolParams...)
119 if opts.Compact {
119 if opts.Compact {
120 return header
120 return header
121 }
121 }
@@ -151,7 +151,7 @@ func NewLSToolMessageItem(
151 result *message.ToolResult,
151 result *message.ToolResult,
152 canceled bool,
152 canceled bool,
153) ToolMessageItem {
153) ToolMessageItem {
154− return newBaseToolMessageItem(sty, toolCall, result, &LSToolRenderContext{}, canceled, "")
154+ return newBaseToolMessageItem(sty, toolCall, result, &LSToolRenderContext{}, canceled)
155}
155}
156
156
157// LSToolRenderContext renders ls tool messages.
157// LSToolRenderContext renders ls tool messages.
@@ -166,7 +166,7 @@ func (l *LSToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *To
166
166
167 var params tools.LSParams
167 var params tools.LSParams
168 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
168 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
169− return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth, false)
169+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
170 }
170 }
171
171
172 path := params.Path
172 path := params.Path
@@ -175,7 +175,7 @@ func (l *LSToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *To
175 }
175 }
176 path = fsext.PrettyPath(path)
176 path = fsext.PrettyPath(path)
177
177
178− header := toolHeader(sty, opts.Status, "List", cappedWidth, opts, path)
178+ header := toolHeader(sty, opts.Status, "List", cappedWidth, opts.Compact, path)
179 if opts.Compact {
179 if opts.Compact {
180 return header
180 return header
181 }
181 }
@@ -211,7 +211,7 @@ func NewSourcegraphToolMessageItem(
211 result *message.ToolResult,
211 result *message.ToolResult,
212 canceled bool,
212 canceled bool,
213) ToolMessageItem {
213) ToolMessageItem {
214− return newBaseToolMessageItem(sty, toolCall, result, &SourcegraphToolRenderContext{}, canceled, "")
214+ return newBaseToolMessageItem(sty, toolCall, result, &SourcegraphToolRenderContext{}, canceled)
215}
215}
216
216
217// SourcegraphToolRenderContext renders sourcegraph tool messages.
217// SourcegraphToolRenderContext renders sourcegraph tool messages.
@@ -226,7 +226,7 @@ func (s *SourcegraphToolRenderContext) RenderTool(sty *styles.Styles, width int,
226
226
227 var params tools.SourcegraphParams
227 var params tools.SourcegraphParams
228 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
228 if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
229− return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth, false)
229+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
230 }
230 }
231
231
232 toolParams := []string{params.Query}
232 toolParams := []string{params.Query}
@@ -237,7 +237,7 @@ func (s *SourcegraphToolRenderContext) RenderTool(sty *styles.Styles, width int,
237 toolParams = append(toolParams, "context", formatNonZero(params.ContextWindow))
237 toolParams = append(toolParams, "context", formatNonZero(params.ContextWindow))
238 }
238 }
239
239
240− header := toolHeader(sty, opts.Status, "Sourcegraph", cappedWidth, opts, toolParams...)
240+ header := toolHeader(sty, opts.Status, "Sourcegraph", cappedWidth, opts.Compact, toolParams...)
241 if opts.Compact {
241 if opts.Compact {
242 return header
242 return header
243 }
243 }
internal/ui/chat/shell.go+32−85
@@ -42,21 +42,6 @@ type ShellItem struct {
42 sty *styles.Styles
42 sty *styles.Styles
43 pending bool
43 pending bool
44 anim *anim.Anim
44 anim *anim.Anim
45−
46− // xOffset-independent render cache. The expensive part of RawRender
47− // (ANSI-reset trim, RemapANSI16, split, per-line width) does not
48− // depend on the horizontal scroll offset, yet hscroll/highlight
49− // Bump() forces a full re-render on every keystroke. Cache the
50− // prepared display lines keyed by the inputs that actually affect
51− // them, and reapply only the xOffset windowing per render.
52− shellCacheValid bool
53− shellCacheOutput string
54− shellCacheExpanded bool
55− shellCacheWidth int
56− shellCachePending bool
57− shellCacheLines []string
58− shellCacheTruncated int
59− shellCacheMaxWidth int
60}
45}
61
46
62var (
47var (
@@ -196,14 +181,6 @@ func (s *ShellItem) ScrollHorizontal(delta int) {
196 s.Bump()
181 s.Bump()
197}
182}
198
183
199−// clearCache drops the base render cache and the xOffset-independent
200−// output cache. The latter embeds the current theme's remapped ANSI
201−// colors (via RemapANSI16), so a style change must re-run the remap.
202−func (s *ShellItem) clearCache() {
203− s.cachedMessageItem.clearCache()
204− s.shellCacheValid = false
205−}
206−
207// ToggleExpanded toggles the expanded state and invalidates the cache.
184// ToggleExpanded toggles the expanded state and invalidates the cache.
208func (s *ShellItem) ToggleExpanded() bool {
185func (s *ShellItem) ToggleExpanded() bool {
209 s.expandedContent = !s.expandedContent
186 s.expandedContent = !s.expandedContent
@@ -211,19 +188,33 @@ func (s *ShellItem) ToggleExpanded() bool {
211 return s.expandedContent
188 return s.expandedContent
212}
189}
213
190
214−// prepareOutput computes the xOffset-independent render intermediate:
191+func (s *ShellItem) RawRender(width int) string {
215−// the trimmed, ANSI-remapped, split-and-truncated display lines plus the
192+ cappedWidth := cappedMessageWidth(width)
216−// max-line-width clamp value. The result is cached and only recomputed
193+
217−// when the output, expansion, capped width, or pending state change, so a
194+ cmd := strings.ReplaceAll(s.command, "\n", " ")
218−// horizontal-scroll keystroke (which Bumps the list cache) no longer
195+ cmd = strings.ReplaceAll(cmd, "\t", " ")
219−// re-runs the ANSI trim, RemapANSI16, split, and per-line width scan.
196+
220−func (s *ShellItem) prepareOutput(cappedWidth int) (displayLines []string, truncatedCount, maxLineWidth int) {
197+ var prompt string
221− if s.shellCacheValid &&
198+ if s.focused {
222− s.shellCacheOutput == s.output &&
199+ prompt = s.sty.Messages.ShellPrompt.Render("$")
223− s.shellCacheExpanded == s.expandedContent &&
200+ } else {
224− s.shellCacheWidth == cappedWidth &&
201+ prompt = s.sty.Messages.ShellPromptBlurred.Render("$")
225− s.shellCachePending == s.pending {
202+ }
226− return s.shellCacheLines, s.shellCacheTruncated, s.shellCacheMaxWidth
203+
204+ highlighted := s.sty.Messages.ShellCommand.Render(cmd)
205+ header := prompt + " " + highlighted
206+
207+ if s.pending {
208+ if s.output == "" {
209+ // Nothing streamed yet: show the spinner under the header.
210+ return header + "\n" + s.anim.Render()
211+ }
212+ } else if s.exitCode != 0 {
213+ header += " " + s.sty.Messages.ShellExitCode.Render(fmt.Sprintf("(exit %d)", s.exitCode))
214+ }
215+
216+ if s.output == "" {
217+ return header
227 }
218 }
228
219
229 // Remap raw ANSI 16-color codes onto legible Charmtone colors so
220 // Remap raw ANSI 16-color codes onto legible Charmtone colors so
@@ -252,8 +243,8 @@ func (s *ShellItem) prepareOutput(cappedWidth int) (displayLines []string, trunc
252 maxLines = len(lines)
243 maxLines = len(lines)
253 }
244 }
254
245
255− displayLines = lines
246+ displayLines := lines
256− truncatedCount = 0
247+ truncatedCount := 0
257 if len(lines) > maxLines {
248 if len(lines) > maxLines {
258 if s.pending {
249 if s.pending {
259 // Show the most recent lines while still running.
250 // Show the most recent lines while still running.
@@ -272,51 +263,7 @@ func (s *ShellItem) prepareOutput(cappedWidth int) (displayLines []string, trunc
272 maxW = w
263 maxW = w
273 }
264 }
274 }
265 }
275− maxLineWidth = max(0, maxW-cappedWidth)
266+ s.maxLineWidth = max(0, maxW-cappedWidth)
276−
277− s.shellCacheValid = true
278− s.shellCacheOutput = s.output
279− s.shellCacheExpanded = s.expandedContent
280− s.shellCacheWidth = cappedWidth
281− s.shellCachePending = s.pending
282− s.shellCacheLines = displayLines
283− s.shellCacheTruncated = truncatedCount
284− s.shellCacheMaxWidth = maxLineWidth
285−
286− return displayLines, truncatedCount, maxLineWidth
287−}
288−
289−func (s *ShellItem) RawRender(width int) string {
290− cappedWidth := cappedMessageWidth(width)
291−
292− cmd := strings.ReplaceAll(s.command, "\n", " ")
293− cmd = strings.ReplaceAll(cmd, "\t", " ")
294−
295− var prompt string
296− if s.focused {
297− prompt = s.sty.Messages.ShellPrompt.Render("$")
298− } else {
299− prompt = s.sty.Messages.ShellPromptBlurred.Render("$")
300− }
301−
302− highlighted := s.sty.Messages.ShellCommand.Render(cmd)
303− header := prompt + " " + highlighted
304−
305− if s.pending {
306− if s.output == "" {
307− // Nothing streamed yet: show the spinner under the header.
308− return header + "\n" + s.anim.Render()
309− }
310− } else if s.exitCode != 0 {
311− header += " " + s.sty.Messages.ShellExitCode.Render(fmt.Sprintf("(exit %d)", s.exitCode))
312− }
313−
314− if s.output == "" {
315− return header
316− }
317−
318− displayLines, truncatedCount, maxLineWidth := s.prepareOutput(cappedWidth)
319− s.maxLineWidth = maxLineWidth
320
267
321 var body strings.Builder
268 var body strings.Builder
322
269
@@ -324,7 +271,7 @@ func (s *ShellItem) RawRender(width int) string {
324 // the "more lines" notice before the output.
271 // the "more lines" notice before the output.
325 if truncatedCount > 0 && s.pending {
272 if truncatedCount > 0 && s.pending {
326 body.WriteString(s.sty.Messages.ShellTruncation.Render(
273 body.WriteString(s.sty.Messages.ShellTruncation.Render(
327− fmt.Sprintf(assistantMessageTailWindowFormat, truncatedCount),
274+ fmt.Sprintf("… %d earlier lines", truncatedCount),
328 ))
275 ))
329 body.WriteString("\n")
276 body.WriteString("\n")
330 }
277 }
@@ -342,7 +289,7 @@ func (s *ShellItem) RawRender(width int) string {
342 // When finished, hidden lines are below, so show the notice after.
289 // When finished, hidden lines are below, so show the notice after.
343 if truncatedCount > 0 && !s.pending && !s.expandedContent {
290 if truncatedCount > 0 && !s.pending && !s.expandedContent {
344 body.WriteString(s.sty.Messages.ShellTruncation.Render(
291 body.WriteString(s.sty.Messages.ShellTruncation.Render(
345− fmt.Sprintf(assistantMessageTruncateFormat, truncatedCount),
292+ fmt.Sprintf("… %d more lines", truncatedCount),
346 ))
293 ))
347 return header + "\n" + body.String()
294 return header + "\n" + body.String()
348 }
295 }
internal/ui/chat/streaming_markdown.go+251−232
@@ -201,268 +201,245 @@ func trimGlamourMargins(s string) string {
201// we have the slightest doubt we return -1 and let the caller fall
201// we have the slightest doubt we return -1 and let the caller fall
202// back to a full render.
202// back to a full render.
203//
203//
204−// The implementation is a single O(n) forward scan
204+// Decision tree, in order of preference (latest boundary wins):
205−// ([scanSafeBoundaries]) that tracks block state across the whole
206−// document: fenced-code toggle, list open/close depth, HTML-block
207−// opener, and link-reference-definition presence. State is updated
208−// per line, so every blank-line candidate is evaluated against
209−// already-computed state in O(1) — no re-scanning of the prefix.
210//
205//
211−// Closure-aware list tracking (B1) replaces the prior "any list
206+// 1. Walk backward through every "blank line" position p such that
212−// marker anywhere → reject" rule. A list is considered open from
207+// content[:p] ends with "\n\n" (or "\n[ \t]*\n").
213−// the first marker line until a blank line is followed by a
208+// 2. For each candidate, check that content[:p] has an even
214−// non-continuation, non-marker line at the same or shallower
209+// number of triple-backtick fence lines (no open fenced
215−// indent. Once closed, prefixes ending after the close are
210+// block). Any odd count means we'd be cutting inside a fence
216−// accepted, so the incremental cache resumes working for the
211+// and mis-syntax-highlighting the trailing partial.
217−// paragraphs that follow a list — the common case in LLM reasoning
212+// 2b. Reject if any line in content[:p] (outside fenced blocks)
218−// traces. On any ambiguity the list is kept open (conservative —
213+// is a list-marker line, an HTML-block opener, or a link
219−// falls back to -1).
220−//
221−// B2 (HTML block) and B3 (link reference definition) remain
214+// reference definition. See [prefixHasOpenHazard] for the
222−// anywhere-in-prefix rejects: the typical assistant output
215+// reasoning behind these "anywhere in prefix" rejects.
223−// contains neither, so the perf cost is zero in the common case.
216+// 3. Reject if the last non-blank line of content[:p] is:
217+// - a list item marker line ("^\s*([-*+]|\d+\.)\s")
218+// - a table line (contains "|")
219+// - a block quote ("^\s*>")
220+// - a setext header underline ("^=+\s*$" or "^-+\s*$")
221+// - an indented code line (4+ leading spaces or a tab)
222+// 4. Reject if the line immediately AFTER the boundary (skipping
223+// leading blank lines) looks like a setext underline (a line
224+// of '=' or '-' only). Rendering the prefix as a paragraph
225+// would change once the underline arrived; that's exactly the
226+// "splitting changes the prefix render" hazard §4.4 calls out.
224//
227//
225// Returns the byte offset of the first character AFTER the blank
228// Returns the byte offset of the first character AFTER the blank
226// line, i.e. the start of the trailing segment.
229// line, i.e. the start of the trailing segment.
227func findSafeMarkdownBoundary(content string) int {
230func findSafeMarkdownBoundary(content string) int {
228− boundaries := scanSafeBoundaries(content)
229− if len(boundaries) == 0 {
231+ if len(content) == 0 {
230 return -1
232 return -1
231 }
233 }
232− return boundaries[len(boundaries)-1]
234+
235+ // Iterate every blank-line position from latest to earliest.
236+ for p := blankLineBefore(content, len(content)); p > 0; p = blankLineBefore(content, p-1) {
237+ if !isSafeBoundaryAt(content, p) {
238+ continue
239+ }
240+ return p
241+ }
242+ return -1
233}
243}
234
244
235−// scanSafeBoundaries walks content once, O(n), tracking block
245+// blankLineBefore returns the byte offset of the first character
236−// state, and returns the byte offsets of every safe stable-prefix
246+// AFTER the latest blank-line separator that ends strictly before
237−// boundary in increasing order. A boundary is the byte offset
247+// `until`. A blank-line separator is a sequence "\n([ \t]*\n)+"
238−// immediately after a blank-line separator at which content[:p]
248+// — one newline, then one or more lines containing only spaces or
239−// can be rendered independently of content[p:].
249+// tabs and terminated by another newline. The returned offset is
250+// the start of the first non-blank line that follows the
251+// separator (or the position immediately after the final newline,
252+// if no further content remains).
240//
253//
241−// State carried across the scan:
254+// Returns -1 when no blank-line separator exists before `until`.
242−//
243−// - inFence: toggled by each fence line. While true, no boundary
244−// is safe (we're inside a fenced code block) and list/html/ref
245−// detection is skipped so code-block contents don't false-
246−// trigger the hazards.
247−// - listDepth: current open-list nesting. Incremented on a
248−// marker line, decremented on closure (see below). A boundary
249−// is only safe when listDepth == 0 (no list open).
250−// - sawHTMLBlock / sawLinkRef: anywhere-in-prefix flags for B2
251−// and B3. Once set they never clear — the prefix is
252−// permanently unsafe for splitting.
253−// - prevNonBlank: the last non-blank line seen before the
254−// current candidate, for the last-line check
255−// ([lineOpensConstruct]).
256−//
257−// Each blank-line candidate is evaluated in O(1) against the
258−// already-computed state, so the full scan is O(lines) regardless
259−// of how many candidates exist. This replaces the prior O(n²)
260−// backward search that re-scanned the prefix for every candidate.
261−//
262−// List closure detection (B1 fix): a list opens on any marker line
263−// when no list is open. A list closes when, after a blank line, a
264−// non-marker line appears at indent < listBaseIndent+2 (not a
265−// continuation paragraph). On any ambiguity the list is kept open
266−// (conservative — falls back to -1). This replaces the prior "any
267−// list marker anywhere → reject" rule, so boundaries after a
268−// closed list followed by paragraphs are accepted — the common
269−// case in LLM reasoning traces.
270−//
271−// Crucially, boundary evaluation happens BEFORE the state update
272−// for the current line: the boundary at the start of line L has
273−// prefix = content[:L], so the state must reflect everything
274−// BEFORE L, not including L's effects. For example, the boundary
275−// at the start of a fence-opening line is safe (the fence hasn't
276−// opened in the prefix yet); the boundary at the start of a list-
277−// marker line is safe (the list hasn't opened in the prefix yet).
278−func scanSafeBoundaries(content string) []int {
255+func blankLineBefore(content string, until int) int {
279− if len(content) == 0 {
256+ if until <= 0 {
280− return nil
257+ return -1
281 }
258 }
282−
259+ // Walk backward looking for a newline followed (after optional
283− var boundaries []int
260+ // blank-line content) by another newline. We track the latest
284− var prevNonBlank string
261+ // newline we've seen; if the next earlier newline has only
285− inFence := false
262+ // blank chars between them, we have a blank-line separator
286− listDepth := 0
263+ // and the boundary sits immediately after the latest newline.
287− sawHTMLBlock := false
264+ end := until
288− sawLinkRef := false
265+ for end > 0 {
289−
266+ nl := strings.LastIndexByte(content[:end], '\n')
290− // listBaseIndent is the indent column of the innermost open
267+ if nl < 0 {
291− // list's markers. Used for closure detection: a non-marker
292− // line at indent < listBaseIndent+2 after a blank line closes
293− // the list.
294− listBaseIndent := 0
295−
296− // blankPending tracks whether the line immediately before the
297− // current line was blank. A boundary candidate exists at the
298− // start of the first non-blank line after a blank line.
299− blankPending := false
300−
301− // processLine handles one line (without its newline
302− // terminator) starting at byte offset off. It evaluates any
303− // pending boundary candidate, then updates block state for
304− // the line.
305− //
306− // Boundary evaluation happens BEFORE the state update for the
307− // current line: the boundary at the start of line L has
308− // prefix = content[:off], so the state must reflect everything
309− // BEFORE L. However, when an open list CLOSES at line L (L is
310− // a non-marker, dedented line after a blank line), the prefix
311− // contains a COMPLETE list — the boundary is safe even though
312− // listDepth is still > 0 in the pre-L state. We compute
313− // listClosesHere before evaluation and pass it as an override.
314− processLine := func(line string, off int) {
315− blank := strings.TrimRight(line, " \t") == ""
316−
317− if blank {
318− blankPending = true
319− return
268+ return -1
320 }
269 }
321−
270+ // Look for an earlier newline whose gap to nl is empty
322− trimmed := strings.TrimLeft(line, " \t")
271+ // or whitespace only.
323− indent := len(line) - len(trimmed)
272+ prev := strings.LastIndexByte(content[:nl], '\n')
324− isMarker := isListItemMarker(trimmed)
273+ for prev >= 0 {
325−
274+ gap := content[prev+1 : nl]
326− // Compute whether this line closes the open list.
275+ if isBlankOrSpaces(gap) {
327− // A list closes when a blank line is followed by a
276+ return nl + 1
328− // non-marker line at indent < listBaseIndent+2 (not
329− // a continuation paragraph).
330− listClosesHere := listDepth > 0 && blankPending && !isMarker && indent < listBaseIndent+2
331−
332− // Non-blank line. If a blank line preceded, this is a
333− // boundary candidate at offset `off`. Evaluate BEFORE
334− // updating state — the prefix is content[:off].
335− if blankPending {
336− if isSafeBoundaryState(inFence, listDepth, listClosesHere, sawHTMLBlock, sawLinkRef, prevNonBlank, trimmed) {
337− boundaries = append(boundaries, off)
338 }
277 }
339− blankPending = false
278+ // Gap had non-whitespace; nl is not a blank-line
340− }
279+ // separator. Move up: try with the earlier newline as
341−
280+ // the new "nl" candidate.
342− // Now update state for this line.
281+ break
343− if isFenceLine(line) {
344− inFence = !inFence
345− prevNonBlank = line
346− return
347− }
348−
349− if inFence {
350− prevNonBlank = line
351− return
352 }
282 }
353−
283+ end = nl
354− // Update list depth (closure-aware B1).
355− switch {
356− case listDepth == 0 && isMarker:
357− listDepth = 1
358− listBaseIndent = indent
359− case listDepth > 0 && isMarker:
360− switch {
361− case indent > listBaseIndent:
362− // Nested list opens.
363− listDepth++
364− listBaseIndent = indent
365− case indent < listBaseIndent:
366− // Marker at shallower indent: close
367− // current list. If an outer list
368− // existed we'd need its base indent;
369− // we don't track the stack, so be
370− // conservative and close everything,
371− // then open a new list at this indent.
372− listDepth = 1
373− listBaseIndent = indent
374− }
375− // Same indent: another item, depth unchanged.
376− case listDepth > 0 && !isMarker && blankPending:
377− // Blank line then non-marker line: check for
378− // closure. A continuation paragraph is indented
379− // 2+ relative to the marker.
380− if indent < listBaseIndent+2 {
381− // Dedent: list closed. Be conservative
382− // and close all nesting levels.
383− listDepth = 0
384− }
385− // Otherwise: continuation, list stays open.
386− }
387− // listDepth > 0 && !isMarker && !blankPending:
388− // A non-marker line directly after another line
389− // (no blank line between). This is either a lazy
390− // continuation or part of a tight list's item.
391− // Keep the list open (conservative).
392−
393− // B2: HTML block opener (anywhere in prefix).
394− if !sawHTMLBlock && isHTMLBlockOpener(line) {
395− sawHTMLBlock = true
396− }
397− // B3: link reference definition (anywhere in prefix).
398− if !sawLinkRef && isLinkRefDefinition(line) {
399− sawLinkRef = true
400− }
401−
402− prevNonBlank = line
403 }
284 }
285+ return -1
286+}
404
287
405− start := 0
288+// isBlankOrSpaces reports whether s consists entirely of spaces
289+// and tabs (or is empty).
290+func isBlankOrSpaces(s string) bool {
406− for i := 0; i < len(content); i++ {
291+ for i := range len(s) {
407− if content[i] != '\n' {
292+ if s[i] != ' ' && s[i] != '\t' {
408− continue
293+ return false
409 }
294 }
410− processLine(content[start:i], start)
411− start = i + 1
412− }
413− // Process the trailing segment (content after the last '\n').
414− // A non-blank trailing line after a blank line is a valid
415− // boundary candidate. A blank trailing line is irrelevant.
416− if start < len(content) {
417− processLine(content[start:], start)
418 }
295 }
419−
420− return boundaries
296+ return true
421}
297}
422
298
423−// isSafeBoundaryState evaluates the safety of a boundary candidate
299+// isSafeBoundaryAt reports whether content[:p] is a safe stable
424−// given the block state computed by the forward scan (reflecting
300+// prefix. p must be a blank-line boundary (start of a line, with a
425−// everything BEFORE the boundary line). All state is passed in
301+// blank line immediately preceding).
426−// explicitly so this helper stays pure and testable.
427//
302//
428−// nextLineTrimmed is the first non-blank line of the suffix
303+// Beyond the last-line checks, three "anywhere in the prefix"
429−// (already left-trimmed), used for the setext-underline check
304+// hazards force a reject because they cannot be reliably reasoned
430−// (rule 4). listClosesHere reports whether the next line closes
305+// about by inspecting the trailing line alone. For each of these
431−// the currently-open list; when true, the prefix contains a
306+// the simplest, safest rule was chosen — see prefixHasOpenHazard.
432−// complete list and the boundary is safe even though listDepth
307+func isSafeBoundaryAt(content string, p int) bool {
433−// is non-zero in the pre-line state.
308+ prefix := content[:p]
434−func isSafeBoundaryState(inFence bool, listDepth int, listClosesHere, sawHTMLBlock, sawLinkRef bool, prevNonBlank, nextLineTrimmed string) bool {
309+
435− // (2) Not inside a fence.
310+ // (2) Even number of triple-backtick fence lines.
436− if inFence {
311+ if countFenceLines(prefix)%2 != 0 {
437 return false
312 return false
438 }
313 }
439− // (B1) No open list, unless the next line closes it.
314+
440− if listDepth != 0 && !listClosesHere {
315+ // (2b) Anywhere-in-prefix hazards: open list (B1), HTML block
316+ // opener (B2), reference link definition (B3). Any of these
317+ // anywhere in the prefix forces a fallback.
318+ if prefixHasOpenHazard(prefix) {
441 return false
319 return false
442 }
320 }
443− // (B2) No HTML block opener anywhere in the prefix.
321+
444− if sawHTMLBlock {
322+ // (3) Inspect the last non-blank line of the prefix.
323+ lastLine := lastNonBlankLine(prefix)
324+ if lastLine != "" && lineOpensConstruct(lastLine) {
445 return false
325 return false
446 }
326 }
447− // (B3) No link reference definition anywhere in the prefix.
327+
448− if sawLinkRef {
328+ // (4) If anything follows, make sure it doesn't look like a
329+ // setext underline that would retroactively turn the last
330+ // paragraph of the prefix into a header.
331+ if rest := content[p:]; rest != "" {
332+ first := firstNonBlankLine(rest)
333+ if isSetextUnderlineCandidate(first) {
449− return false
334+ return false
335+ }
450 }
336 }
451− // (3) The last non-blank line of the prefix must not keep a
337+
452− // construct open. When the list closes at the next line, the
338+ return true
453− // prefix's last non-blank line is a list marker — that's
339+}
454− // expected and safe because the list is complete, so skip
340+
455− // the check in that case.
341+// prefixHasOpenHazard reports whether prefix contains any of three
456− if !listClosesHere && prevNonBlank != "" && lineOpensConstruct(prevNonBlank) {
342+// constructs that cannot be safely cut at a blank-line boundary
343+// even when the immediately preceding line looks fine. Each check
344+// uses the SIMPLEST viable conservative rule per the F8 round-2
345+// review:
346+//
347+// B1 (loose lists). A loose list has a blank line between an item
348+// and a continuation paragraph that begins with indentation
349+// but no list marker. If a candidate boundary lands on that
350+// blank line, the prefix's trailing non-blank line is the
351+// continuation paragraph, NOT a list marker, so the last-line
352+// check would accept it even though the list is still open.
353+//
354+// Rule chosen: any list-marker line ANYWHERE in the prefix
355+// forces -1. This is overly conservative — it forfeits
356+// boundary advancement past a closed list — but it eliminates
357+// the entire bug class with zero parsing of CommonMark's
358+// loose-list closure semantics. We retain the most useful
359+// boundary in practice: the one BEFORE the list opens (no
360+// marker has appeared in the prefix yet).
361+//
362+// B2 (HTML blocks). CommonMark defines seven HTML-block opener
363+// patterns (script/pre/style/textarea, comments, processing
364+// instructions, CDATA, declarations, recognised tag names).
365+// If the prefix opens an HTML block that the suffix closes,
366+// splitting renders the prefix as raw HTML and the suffix as
367+// prose.
368+//
369+// Rule chosen: any HTML-block opener anywhere in the prefix
370+// forces -1. Same trade-off as B1 — the typical assistant
371+// output contains no raw HTML, so the perf cost is zero in
372+// the common case.
373+//
374+// B3 (reference link definitions). A line of the form
375+// "[label]: <url>" defines a link reference that the suffix
376+// may later use as "[text][label]". Splitting the document
377+// loses the definition because each half is rendered as an
378+// independent glamour document.
379+//
380+// Rule chosen: any reference link definition line anywhere in
381+// the prefix forces -1. Suffix-side reference detection is
382+// fragile (three syntaxes: [text][label], [label][], [label]),
383+// so the prefix-side check is the simpler safe choice.
384+//
385+// All three rules accept the perf hit of "no boundary after a
386+// list / HTML block / link def" in exchange for guaranteed
387+// soundness. If profiling shows this kills the F8 win on real
388+// streaming traces, the next iteration can promote each rule to
389+// its less-conservative variant (closure-aware list tracking,
390+// per-tag HTML close detection, suffix-aware ref tracking).
391+func prefixHasOpenHazard(prefix string) bool {
457− return false
392+ inFence := false
393+ for line := range splitLines(prefix) {
394+ // Track fenced state so list/html/ref patterns inside a
395+ // fenced code block do not falsely trigger the hazards.
396+ if isFenceLine(line) {
397+ inFence = !inFence
398+ continue
399+ }
400+ if inFence {
401+ continue
402+ }
403+ trimmed := strings.TrimLeft(line, " \t")
404+ if trimmed == "" {
405+ continue
406+ }
407+ // B1: any list-item marker.
408+ if isListItemMarker(trimmed) {
409+ return true
410+ }
411+ // B2: HTML block opener.
412+ if isHTMLBlockOpener(line) {
413+ return true
414+ }
415+ // B3: link reference definition.
416+ if isLinkRefDefinition(line) {
417+ return true
418+ }
458 }
419 }
459− // (4) The first non-blank line of the suffix must not look
420+ return false
460− // like a setext underline that would retroactively turn the
421+}
461− // last paragraph of the prefix into a header.
422+
462− if isSetextUnderlineCandidate(nextLineTrimmed) {
423+// countFenceLines counts lines that begin a fenced code block in
463− return false
424+// the CommonMark sense: a line whose first non-whitespace run is
425+// at least three consecutive backticks (or tildes). Each such
426+// line toggles the fenced state, so an even count means every
427+// opened fence has been closed.
428+//
429+// We accept up to three leading spaces of indentation (CommonMark
430+// rule) and require the fence characters to be the FIRST
431+// non-whitespace content of the line. We deliberately do NOT
432+// attempt to parse info-strings or differentiate opener from
433+// closer beyond toggling — a closing fence is just any line
434+// whose first non-whitespace run is >=3 of the same fence char.
435+func countFenceLines(s string) int {
436+ n := 0
437+ for line := range splitLines(s) {
438+ if isFenceLine(line) {
439+ n++
440+ }
464 }
441 }
465− return true
442+ return n
466}
443}
467
444
468// isFenceLine reports whether line opens or closes a fenced code
445// isFenceLine reports whether line opens or closes a fenced code
@@ -488,6 +465,48 @@ func isFenceLine(line string) bool {
488 return run >= 3
465 return run >= 3
489}
466}
490
467
468+// lastNonBlankLine returns the last non-blank line of s, or ""
469+// when every line is blank.
470+func lastNonBlankLine(s string) string {
471+ last := ""
472+ for line := range splitLines(s) {
473+ if strings.TrimSpace(line) != "" {
474+ last = line
475+ }
476+ }
477+ return last
478+}
479+
480+// firstNonBlankLine returns the first non-blank line of s, or ""
481+// when every line is blank.
482+func firstNonBlankLine(s string) string {
483+ for line := range splitLines(s) {
484+ if strings.TrimSpace(line) != "" {
485+ return line
486+ }
487+ }
488+ return ""
489+}
490+
491+// splitLines yields the lines of s without their terminators. The
492+// final segment is yielded even if not newline-terminated.
493+func splitLines(s string) func(yield func(string) bool) {
494+ return func(yield func(string) bool) {
495+ start := 0
496+ for i := 0; i < len(s); i++ {
497+ if s[i] == '\n' {
498+ if !yield(s[start:i]) {
499+ return
500+ }
501+ start = i + 1
502+ }
503+ }
504+ if start <= len(s)-1 {
505+ yield(s[start:])
506+ }
507+ }
508+}
509+
491// lineOpensConstruct reports whether line keeps a markdown
510// lineOpensConstruct reports whether line keeps a markdown
492// construct open across the boundary. We err conservatively —
511// construct open across the boundary. We err conservatively —
493// any case that smells like list/table/quote/setext/indented-code
512// any case that smells like list/table/quote/setext/indented-code
internal/ui/chat/todos.go+2−2
@@ -31,7 +31,7 @@ func NewTodosToolMessageItem(
31 result *message.ToolResult,
31 result *message.ToolResult,
32 canceled bool,
32 canceled bool,
33) ToolMessageItem {
33) ToolMessageItem {
34− return newBaseToolMessageItem(sty, toolCall, result, &TodosToolRenderContext{}, canceled, "")
34+ return newBaseToolMessageItem(sty, toolCall, result, &TodosToolRenderContext{}, canceled)
35}
35}
36
36
37// TodosToolRenderContext renders todos tool messages.
37// TodosToolRenderContext renders todos tool messages.
@@ -119,7 +119,7 @@ func (t *TodosToolRenderContext) RenderTool(sty *styles.Styles, width int, opts
119 }
119 }
120
120
121 toolParams := []string{headerText}
121 toolParams := []string{headerText}
122− header := toolHeader(sty, opts.Status, "To-Do", cappedWidth, opts, toolParams...)
122+ header := toolHeader(sty, opts.Status, "To-Do", cappedWidth, opts.Compact, toolParams...)
123 if opts.Compact {
123 if opts.Compact {
124 return header
124 return header
125 }
125 }
internal/ui/chat/tool_result_content.go+14−15
@@ -20,20 +20,19 @@ func humanizedToolName(name string) string {
20 return stringext.Capitalize(name)
20 return stringext.Capitalize(name)
21}
21}
22
22
23−var markdownPatterns = []string{
24− "# ",
25− "## ",
26− "**",
27− "```",
28− "- ",
29− "1. ",
30− "> ",
31− "---",
32− "***",
33−}
34−
35func looksLikeMarkdown(content string) bool {
23func looksLikeMarkdown(content string) bool {
24+ patterns := []string{
25+ "# ",
26+ "## ",
27+ "**",
28+ "```",
29+ "- ",
30+ "1. ",
31+ "> ",
32+ "---",
33+ "***",
34+ }
36− for _, p := range markdownPatterns {
35+ for _, p := range patterns {
37 if strings.Contains(content, p) {
36 if strings.Contains(content, p) {
38 return true
37 return true
39 }
38 }
@@ -41,7 +40,7 @@ func looksLikeMarkdown(content string) bool {
41 return false
40 return false
42}
41}
43
42
44−func renderToolResultTextContent(sty *styles.Styles, content string, widths toolResultContentWidths, expanded bool, diffTool string) string {
43+func renderToolResultTextContent(sty *styles.Styles, content string, widths toolResultContentWidths, expanded bool) string {
45 var result json.RawMessage
44 var result json.RawMessage
46 if err := json.Unmarshal([]byte(content), &result); err == nil {
45 if err := json.Unmarshal([]byte(content), &result); err == nil {
47 prettyResult, err := json.MarshalIndent(result, "", " ")
46 prettyResult, err := json.MarshalIndent(result, "", " ")
@@ -51,7 +50,7 @@ func renderToolResultTextContent(sty *styles.Styles, content string, widths tool
51 return sty.Tool.Body.Render(toolOutputPlainContent(sty, content, widths.Body, expanded))
50 return sty.Tool.Body.Render(toolOutputPlainContent(sty, content, widths.Body, expanded))
52 }
51 }
53 if diffdetect.IsUnifiedDiff(content) {
52 if diffdetect.IsUnifiedDiff(content) {
54− return toolOutputDiffContentFromUnified(sty, content, widths.Diff, expanded, diffTool)
53+ return toolOutputDiffContentFromUnified(sty, content, widths.Diff, expanded)
55 }
54 }
56 if looksLikeMarkdown(content) {
55 if looksLikeMarkdown(content) {
57 return sty.Tool.Body.Render(toolOutputCodeContent(sty, "result.md", content, 0, widths.Body, expanded))
56 return sty.Tool.Body.Render(toolOutputCodeContent(sty, "result.md", content, 0, widths.Body, expanded))
internal/ui/chat/tool_result_content_test.go+5−5
@@ -72,7 +72,7 @@ func TestRenderToolResultTextContent(t *testing.T) {
72 t.Fatalf("json.MarshalIndent() error = %v", err)
72 t.Fatalf("json.MarshalIndent() error = %v", err)
73 }
73 }
74 expected := styPtr.Tool.Body.Render(toolOutputCodeContent(styPtr, "result.json", string(prettyResult), 0, widths.Body, false))
74 expected := styPtr.Tool.Body.Render(toolOutputCodeContent(styPtr, "result.json", string(prettyResult), 0, widths.Body, false))
75− got := renderToolResultTextContent(styPtr, content, widths, false, "")
75+ got := renderToolResultTextContent(styPtr, content, widths, false)
76 if got != expected {
76 if got != expected {
77 t.Fatal("renderToolResultTextContent() did not choose JSON rendering")
77 t.Fatal("renderToolResultTextContent() did not choose JSON rendering")
78 }
78 }
@@ -87,8 +87,8 @@ func TestRenderToolResultTextContent(t *testing.T) {
87-# Old
87-# Old
88+# New
88+# New
89`
89`
90− expected := toolOutputDiffContentFromUnified(styPtr, content, widths.Diff, false, "")
90+ expected := toolOutputDiffContentFromUnified(styPtr, content, widths.Diff, false)
91− got := renderToolResultTextContent(styPtr, content, widths, false, "")
91+ got := renderToolResultTextContent(styPtr, content, widths, false)
92 if got != expected {
92 if got != expected {
93 t.Fatal("renderToolResultTextContent() did not choose diff rendering")
93 t.Fatal("renderToolResultTextContent() did not choose diff rendering")
94 }
94 }
@@ -98,7 +98,7 @@ func TestRenderToolResultTextContent(t *testing.T) {
98 t.Parallel()
98 t.Parallel()
99 content := "# Title\n\nBody"
99 content := "# Title\n\nBody"
100 expected := styPtr.Tool.Body.Render(toolOutputCodeContent(styPtr, "result.md", content, 0, widths.Body, false))
100 expected := styPtr.Tool.Body.Render(toolOutputCodeContent(styPtr, "result.md", content, 0, widths.Body, false))
101− got := renderToolResultTextContent(styPtr, content, widths, false, "")
101+ got := renderToolResultTextContent(styPtr, content, widths, false)
102 if got != expected {
102 if got != expected {
103 t.Fatal("renderToolResultTextContent() did not choose markdown rendering")
103 t.Fatal("renderToolResultTextContent() did not choose markdown rendering")
104 }
104 }
@@ -108,7 +108,7 @@ func TestRenderToolResultTextContent(t *testing.T) {
108 t.Parallel()
108 t.Parallel()
109 content := "plain text"
109 content := "plain text"
110 expected := styPtr.Tool.Body.Render(toolOutputPlainContent(styPtr, content, widths.Body, false))
110 expected := styPtr.Tool.Body.Render(toolOutputPlainContent(styPtr, content, widths.Body, false))
111− got := renderToolResultTextContent(styPtr, content, widths, false, "")
111+ got := renderToolResultTextContent(styPtr, content, widths, false)
112 if got != expected {
112 if got != expected {
113 t.Fatal("renderToolResultTextContent() did not choose plain rendering")
113 t.Fatal("renderToolResultTextContent() did not choose plain rendering")
114 }
114 }
internal/ui/chat/tools.go+64−152
@@ -19,7 +19,6 @@ import (
19 "github.com/charmbracelet/crush/internal/stringext"
19 "github.com/charmbracelet/crush/internal/stringext"
20 "github.com/charmbracelet/crush/internal/ui/anim"
20 "github.com/charmbracelet/crush/internal/ui/anim"
21 "github.com/charmbracelet/crush/internal/ui/common"
21 "github.com/charmbracelet/crush/internal/ui/common"
22− "github.com/charmbracelet/crush/internal/ui/difftastic"
23 "github.com/charmbracelet/crush/internal/ui/list"
22 "github.com/charmbracelet/crush/internal/ui/list"
24 "github.com/charmbracelet/crush/internal/ui/styles"
23 "github.com/charmbracelet/crush/internal/ui/styles"
25 "github.com/charmbracelet/x/ansi"
24 "github.com/charmbracelet/x/ansi"
@@ -28,11 +27,6 @@ import (
28// responseContextHeight limits the number of lines displayed in tool output.
27// responseContextHeight limits the number of lines displayed in tool output.
29const responseContextHeight = 10
28const responseContextHeight = 10
30
29
31−// diffContextHeight limits the number of lines displayed in code-edit
32−// diffs before truncation. Diffs get a larger default cap than plain
33−// tool output so most of a change is visible without expanding.
34−const diffContextHeight = 30
35−
36// toolBodyLeftPaddingTotal represents the padding that should be applied to each tool body
30// toolBodyLeftPaddingTotal represents the padding that should be applied to each tool body
37const toolBodyLeftPaddingTotal = 2
31const toolBodyLeftPaddingTotal = 2
38
32
@@ -58,7 +52,6 @@ type ToolMessageItem interface {
58 SetMessageID(id string)
52 SetMessageID(id string)
59 SetStatus(status ToolStatus)
53 SetStatus(status ToolStatus)
60 Status() ToolStatus
54 Status() ToolStatus
61− SetDiffTool(tool string)
62}
55}
63
56
64// Compactable is an interface for tool items that can render in a compacted mode.
57// Compactable is an interface for tool items that can render in a compacted mode.
@@ -93,7 +86,7 @@ type DefaultToolRenderContext struct{}
93
86
94// RenderTool implements the [ToolRenderer] interface.
87// RenderTool implements the [ToolRenderer] interface.
95func (d *DefaultToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
88func (d *DefaultToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
96− return sty.Messages.AssistantBlurred.Render(fmt.Sprintf("Unknown tool: %s", opts.ToolCall.Name))
89+ return "TODO: Implement Tool Renderer For: " + opts.ToolCall.Name
97}
90}
98
91
99// ToolRenderOpts contains the data needed to render a tool call.
92// ToolRenderOpts contains the data needed to render a tool call.
@@ -105,10 +98,6 @@ type ToolRenderOpts struct {
105 Compact bool
98 Compact bool
106 IsSpinning bool
99 IsSpinning bool
107 Status ToolStatus
100 Status ToolStatus
108− // DiffTool selects the external diff renderer for edit/multi-edit diffs.
109− // "builtin" (the default) uses the in-process renderer; "difftastic"
110− // shells out to the difft binary.
111− DiffTool string
112}
101}
113
102
114// IsPending returns true if the tool call is still pending (not finished and
103// IsPending returns true if the tool call is still pending (not finished and
@@ -169,8 +158,6 @@ type baseToolMessageItem struct {
169 sty *styles.Styles
158 sty *styles.Styles
170 anim *anim.Anim
159 anim *anim.Anim
171 expandedContent bool
160 expandedContent bool
172− // diffTool selects the external diff renderer for edit/multi-edit diffs.
173− diffTool string
174}
161}
175
162
176var _ Expandable = (*baseToolMessageItem)(nil)
163var _ Expandable = (*baseToolMessageItem)(nil)
@@ -182,7 +169,6 @@ func newBaseToolMessageItem(
182 result *message.ToolResult,
169 result *message.ToolResult,
183 toolRenderer ToolRenderer,
170 toolRenderer ToolRenderer,
184 canceled bool,
171 canceled bool,
185− diffTool string,
186) *baseToolMessageItem {
172) *baseToolMessageItem {
187 // we only do full width for diffs (as far as I know)
173 // we only do full width for diffs (as far as I know)
188 hasCappedWidth := toolCall.Name != tools.EditToolName && toolCall.Name != tools.MultiEditToolName
174 hasCappedWidth := toolCall.Name != tools.EditToolName && toolCall.Name != tools.MultiEditToolName
@@ -204,7 +190,6 @@ func newBaseToolMessageItem(
204 result: result,
190 result: result,
205 status: status,
191 status: status,
206 hasCappedWidth: hasCappedWidth,
192 hasCappedWidth: hasCappedWidth,
207− diffTool: diffTool,
208 }
193 }
209 t.anim = anim.New(anim.Settings{
194 t.anim = anim.New(anim.Settings{
210 ID: toolCall.ID,
195 ID: toolCall.ID,
@@ -229,7 +214,6 @@ func NewToolMessageItem(
229 toolCall message.ToolCall,
214 toolCall message.ToolCall,
230 result *message.ToolResult,
215 result *message.ToolResult,
231 canceled bool,
216 canceled bool,
232− diffTool string,
233) ToolMessageItem {
217) ToolMessageItem {
234 var item ToolMessageItem
218 var item ToolMessageItem
235 switch toolCall.Name {
219 switch toolCall.Name {
@@ -284,7 +268,6 @@ func NewToolMessageItem(
284 item = NewGenericToolMessageItem(sty, toolCall, result, canceled)
268 item = NewGenericToolMessageItem(sty, toolCall, result, canceled)
285 }
269 }
286 }
270 }
287− item.SetDiffTool(diffTool)
288 item.SetMessageID(messageID)
271 item.SetMessageID(messageID)
289 return item
272 return item
290}
273}
@@ -299,16 +282,6 @@ func (t *baseToolMessageItem) SetCompact(compact bool) {
299 t.Bump()
282 t.Bump()
300}
283}
301
284
302−// SetDiffTool sets the external diff tool preference and invalidates the cache.
303−func (t *baseToolMessageItem) SetDiffTool(tool string) {
304− if t.diffTool == tool {
305− return
306− }
307− t.diffTool = tool
308− t.clearCache()
309− t.Bump()
310−}
311−
312// ID returns the unique identifier for this tool message item.
285// ID returns the unique identifier for this tool message item.
313func (t *baseToolMessageItem) ID() string {
286func (t *baseToolMessageItem) ID() string {
314 return t.toolCall.ID
287 return t.toolCall.ID
@@ -360,7 +333,6 @@ func (t *baseToolMessageItem) RawRender(width int) string {
360 Compact: t.isCompact,
333 Compact: t.isCompact,
361 IsSpinning: t.isSpinning(),
334 IsSpinning: t.isSpinning(),
362 Status: t.computeStatus(),
335 Status: t.computeStatus(),
363− DiffTool: t.diffTool,
364 })
336 })
365
337
366 // Prepend hook indicator if hooks ran for this tool call.
338 // Prepend hook indicator if hooks ran for this tool call.
@@ -407,15 +379,10 @@ func (t *baseToolMessageItem) Render(width int) string {
407 prefix = t.sty.Messages.ToolCallBlurred.Render()
379 prefix = t.sty.Messages.ToolCallBlurred.Render()
408 }
380 }
409 lines := strings.Split(t.RawRender(width), "\n")
381 lines := strings.Split(t.RawRender(width), "\n")
410− var sb strings.Builder
411 for i, ln := range lines {
382 for i, ln := range lines {
412− if i > 0 {
383+ lines[i] = prefix + ln
413− sb.WriteString("\n")
414− }
415− sb.WriteString(prefix)
416− sb.WriteString(ln)
417 }
384 }
418− out := sb.String()
385+ out := strings.Join(lines, "\n")
419 if useCache {
386 if useCache {
420 t.setCachedPrefixedRender(out, width, key)
387 t.setCachedPrefixedRender(out, width, key)
421 }
388 }
@@ -556,7 +523,7 @@ func toolEarlyStateContent(sty *styles.Styles, opts *ToolRenderOpts, width int)
556 var msg string
523 var msg string
557 switch opts.Status {
524 switch opts.Status {
558 case ToolStatusError:
525 case ToolStatusError:
559− msg = toolErrorContent(sty, opts.Result, width, opts.ExpandedContent)
526+ msg = toolErrorContent(sty, opts.Result, width)
560 case ToolStatusCanceled:
527 case ToolStatusCanceled:
561 msg = sty.Tool.StateCancelled.Render("Canceled.")
528 msg = sty.Tool.StateCancelled.Render("Canceled.")
562 case ToolStatusAwaitingPermission:
529 case ToolStatusAwaitingPermission:
@@ -570,42 +537,21 @@ func toolEarlyStateContent(sty *styles.Styles, opts *ToolRenderOpts, width int)
570}
537}
571
538
572// toolErrorContent formats an error message with an ERROR or WARN tag.
539// toolErrorContent formats an error message with an ERROR or WARN tag.
573−//
574−// When collapsed it shows a single truncated line plus a hidden-line
575−// notice; when expanded it renders the full message wrapped to width,
576−// preserving newlines, so multi-line failures (stack traces, compiler
577−// output, edit-failure messages) stay actionable. This mirrors the
578−// assistant-level error renderer's wrapped Details.
579−func toolErrorContent(sty *styles.Styles, result *message.ToolResult, width int, expanded bool) string {
540+func toolErrorContent(sty *styles.Styles, result *message.ToolResult, width int) string {
580 if result == nil {
541 if result == nil {
581 return ""
542 return ""
582 }
543 }
583− content := strings.TrimRight(result.Content, "\n")
544+ errContent := strings.ReplaceAll(result.Content, "\n", " ")
584−
585− tag := sty.Tool.ErrorTag.Render("ERROR")
586− msgStyle := sty.Tool.ErrorMessage
587− if strings.Contains(content, "User denied permission") {
545+ if strings.Contains(errContent, "User denied permission") {
588− tag = sty.Tool.WarnTag.Render("WARN")
546+ deniedTag := sty.Tool.WarnTag.Render("WARN")
589− msgStyle = sty.Tool.WarnMessage
590− }
591− tagWidth := lipgloss.Width(tag)
547+ deniedTagWidth := lipgloss.Width(deniedTag)
592− msgWidth := max(1, width-tagWidth-1)
548+ errContent = ansi.Truncate(errContent, width-deniedTagWidth-3, "…")
593−
549+ return fmt.Sprintf("%s %s", deniedTag, sty.Tool.WarnMessage.Render(errContent))
594− if !expanded {
595− oneLine := ansi.Truncate(strings.ReplaceAll(content, "\n", " "), msgWidth, "…")
596− out := fmt.Sprintf("%s %s", tag, msgStyle.Render(oneLine))
597− if hidden := strings.Count(content, "\n"); hidden > 0 {
598− out += "\n" + sty.Tool.ContentTruncation.
599− Width(width).
600− Render(fmt.Sprintf(assistantMessageTruncateFormat, hidden))
601− }
602− return out
603 }
550 }
604−
551+ errTag := sty.Tool.ErrorTag.Render("ERROR")
605− // Expanded: wrap the full message to width, preserving newlines,
552+ tagWidth := lipgloss.Width(errTag)
606− // and align continuation lines under the message column.
553+ errContent = ansi.Truncate(errContent, width-tagWidth-3, "…")
607− wrapped := msgStyle.Width(msgWidth).Render(content)
554+ return fmt.Sprintf("%s %s", errTag, sty.Tool.ErrorMessage.Render(errContent))
608− return lipgloss.JoinHorizontal(lipgloss.Top, tag+" ", wrapped)
609}
555}
610
556
611// toolIcon returns the status icon for a tool call.
557// toolIcon returns the status icon for a tool call.
@@ -623,9 +569,9 @@ func toolIcon(sty *styles.Styles, status ToolStatus) string {
623 }
569 }
624}
570}
625
571
572+// toolParamList formats parameters as "main (key=value, ...)" with truncation.
626// toolParamList formats tool parameters as "main (key=value, ...)" with truncation.
573// toolParamList formats tool parameters as "main (key=value, ...)" with truncation.
627−// When opts.ExpandedContent is true, the output wraps instead of truncating.
628−func toolParamList(sty *styles.Styles, params []string, width int, opts *ToolRenderOpts) string {
574+func toolParamList(sty *styles.Styles, params []string, width int) string {
629 // minSpaceForMainParam is the min space required for the main param
575 // minSpaceForMainParam is the min space required for the main param
630 // if this is less that the value set we will only show the main param nothing else
576 // if this is less that the value set we will only show the main param nothing else
631 const minSpaceForMainParam = 30
577 const minSpaceForMainParam = 30
@@ -652,18 +598,14 @@ func toolParamList(sty *styles.Styles, params []string, width int, opts *ToolRen
652 }
598 }
653 }
599 }
654
600
655− if width >= 0 && (opts == nil || !opts.ExpandedContent) {
601+ if width >= 0 {
656 output = ansi.Truncate(output, width, "…")
602 output = ansi.Truncate(output, width, "…")
657− } else if opts != nil && opts.ExpandedContent && width > 0 && lipgloss.Width(output) > width {
658− output = ansi.Hardwrap(output, width, false)
659 }
603 }
660 return sty.Tool.ParamMain.Render(output)
604 return sty.Tool.ParamMain.Render(output)
661}
605}
662
606
663// toolHeader builds the tool header line: "● ToolName params..."
607// toolHeader builds the tool header line: "● ToolName params..."
664−// When opts.ExpandedContent is true, long parameters wrap instead of truncating.
665−func toolHeader(sty *styles.Styles, status ToolStatus, name string, width int, opts *ToolRenderOpts, params ...string) string {
608+func toolHeader(sty *styles.Styles, status ToolStatus, name string, width int, nested bool, params ...string) string {
666− nested := opts != nil && opts.Compact
667 icon := toolIcon(sty, status)
609 icon := toolIcon(sty, status)
668 nameStyle := sty.Tool.NameNormal
610 nameStyle := sty.Tool.NameNormal
669 if nested {
611 if nested {
@@ -673,18 +615,7 @@ func toolHeader(sty *styles.Styles, status ToolStatus, name string, width int, o
673 prefix := fmt.Sprintf("%s %s ", icon, toolName)
615 prefix := fmt.Sprintf("%s %s ", icon, toolName)
674 prefixWidth := lipgloss.Width(prefix)
616 prefixWidth := lipgloss.Width(prefix)
675 remainingWidth := width - prefixWidth
617 remainingWidth := width - prefixWidth
676− paramsStr := toolParamList(sty, params, remainingWidth, opts)
618+ paramsStr := toolParamList(sty, params, remainingWidth)
677−
678− // When expanded, toolParamList may return multiple lines. Indent
679− // continuation lines to align with the first line's param text.
680− if strings.Contains(paramsStr, "\n") {
681− lines := strings.Split(paramsStr, "\n")
682− indent := strings.Repeat(" ", prefixWidth)
683− for i := 1; i < len(lines); i++ {
684− lines[i] = indent + lines[i]
685− }
686− return prefix + strings.Join(lines, "\n")
687− }
688 return prefix + paramsStr
619 return prefix + paramsStr
689}
620}
690
621
@@ -698,7 +629,6 @@ func toolOutputPlainContent(sty *styles.Styles, content string, width int, expan
698 maxLines = len(lines) // Show all
629 maxLines = len(lines) // Show all
699 }
630 }
700
631
701− contentLineStyle := sty.Tool.ContentLine.Width(width)
702 var out []string
632 var out []string
703 for i, ln := range lines {
633 for i, ln := range lines {
704 if i >= maxLines {
634 if i >= maxLines {
@@ -708,7 +638,7 @@ func toolOutputPlainContent(sty *styles.Styles, content string, width int, expan
708 if lipgloss.Width(ln) > width {
638 if lipgloss.Width(ln) > width {
709 ln = ansi.Truncate(ln, width, "…")
639 ln = ansi.Truncate(ln, width, "…")
710 }
640 }
711− out = append(out, contentLineStyle.Render(ln))
641+ out = append(out, sty.Tool.ContentLine.Width(width).Render(ln))
712 }
642 }
713
643
714 wasTruncated := len(lines) > responseContextHeight
644 wasTruncated := len(lines) > responseContextHeight
@@ -750,16 +680,16 @@ func toolOutputCodeContent(sty *styles.Styles, path, content string, offset, wid
750 bodyWidth := width - toolBodyLeftPaddingTotal
680 bodyWidth := width - toolBodyLeftPaddingTotal
751 codeWidth := bodyWidth - maxDigits
681 codeWidth := bodyWidth - maxDigits
752
682
753− codeLineStyle := sty.Tool.ContentCodeLine.Width(codeWidth)
754− codeLinePadding := sty.Tool.ContentCodeLine.GetHorizontalPadding()
755 var out []string
683 var out []string
756 for i, ln := range highlightedLines {
684 for i, ln := range highlightedLines {
757 lineNum := sty.Tool.ContentLineNumber.Render(fmt.Sprintf(numFmt, i+1+offset))
685 lineNum := sty.Tool.ContentLineNumber.Render(fmt.Sprintf(numFmt, i+1+offset))
758
686
759 // Truncate accounting for padding that will be added.
687 // Truncate accounting for padding that will be added.
760− ln = ansi.Truncate(ln, codeWidth-codeLinePadding, "…")
688+ ln = ansi.Truncate(ln, codeWidth-sty.Tool.ContentCodeLine.GetHorizontalPadding(), "…")
761
689
690+ codeLine := sty.Tool.ContentCodeLine.
691+ Width(codeWidth).
762− codeLine := codeLineStyle.Render(ln)
692+ Render(ln)
763
693
764 out = append(out, lipgloss.JoinHorizontal(lipgloss.Left, lineNum, codeLine))
694 out = append(out, lipgloss.JoinHorizontal(lipgloss.Left, lineNum, codeLine))
765 }
695 }
@@ -991,33 +921,18 @@ func formatSize(bytes int) string {
991 )
921 )
992 switch {
922 switch {
993 case bytes >= mb:
923 case bytes >= mb:
994− result := fmt.Sprintf("%.1f MB", float64(bytes)/float64(mb))
924+ return fmt.Sprintf("%.1f MB", float64(bytes)/float64(mb))
995− if strings.HasSuffix(result, ".0 MB") {
996− result = strings.Replace(result, ".0 MB", " MB", 1)
997− }
998− return result
999 case bytes >= kb:
925 case bytes >= kb:
1000− result := fmt.Sprintf("%.1f KB", float64(bytes)/float64(kb))
926+ return fmt.Sprintf("%.1f KB", float64(bytes)/float64(kb))
1001− if strings.HasSuffix(result, ".0 KB") {
1002− result = strings.Replace(result, ".0 KB", " KB", 1)
1003− }
1004− return result
1005 default:
927 default:
1006 return fmt.Sprintf("%d B", bytes)
928 return fmt.Sprintf("%d B", bytes)
1007 }
929 }
1008}
930}
1009
931
1010// toolOutputDiffContent renders a diff between old and new content.
932// toolOutputDiffContent renders a diff between old and new content.
1011−func toolOutputDiffContent(sty *styles.Styles, file, oldContent, newContent string, width int, expanded bool, diffTool string) string {
933+func toolOutputDiffContent(sty *styles.Styles, file, oldContent, newContent string, width int, expanded bool) string {
1012 bodyWidth := width - toolBodyLeftPaddingTotal
934 bodyWidth := width - toolBodyLeftPaddingTotal
1013
935
1014− // Try difftastic if enabled. Fall back to builtin on any failure.
1015− if diffTool == "difftastic" {
1016− if out, err := difftastic.RenderDiff(file, oldContent, newContent, bodyWidth); err == nil && out != "" {
1017− return sty.Tool.Body.Render(truncateDiff(sty, out, bodyWidth, expanded))
1018− }
1019− }
1020−
1021 formatter := common.DiffFormatter(sty).
936 formatter := common.DiffFormatter(sty).
1022 Before(file, oldContent).
937 Before(file, oldContent).
1023 After(file, newContent).
938 After(file, newContent).
@@ -1029,7 +944,22 @@ func toolOutputDiffContent(sty *styles.Styles, file, oldContent, newContent stri
1029 }
944 }
1030
945
1031 formatted := formatter.String()
946 formatted := formatter.String()
947+ lines := strings.Split(formatted, "\n")
948+
949+ // Truncate if needed.
950+ maxLines := responseContextHeight
951+ if expanded {
952+ maxLines = len(lines)
953+ }
954+
955+ if len(lines) > maxLines && !expanded {
956+ truncMsg := sty.Tool.DiffTruncation.
957+ Width(bodyWidth).
958+ Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines))
959+ formatted = strings.Join(lines[:maxLines], "\n") + "\n" + truncMsg
960+ }
961+
1032− return sty.Tool.Body.Render(truncateDiff(sty, formatted, bodyWidth, expanded))
962+ return sty.Tool.Body.Render(formatted)
1033}
963}
1034
964
1035// formatTimeout converts timeout seconds to a duration string (e.g., "30s").
965// formatTimeout converts timeout seconds to a duration string (e.g., "30s").
@@ -1050,24 +980,9 @@ func formatNonZero(value int) string {
1050}
980}
1051
981
1052// toolOutputMultiEditDiffContent renders a diff with optional failed edits note.
982// toolOutputMultiEditDiffContent renders a diff with optional failed edits note.
1053−func toolOutputMultiEditDiffContent(sty *styles.Styles, file string, meta tools.MultiEditResponseMetadata, totalEdits, width int, expanded bool, diffTool string) string {
983+func toolOutputMultiEditDiffContent(sty *styles.Styles, file string, meta tools.MultiEditResponseMetadata, totalEdits, width int, expanded bool) string {
1054 bodyWidth := width - toolBodyLeftPaddingTotal
984 bodyWidth := width - toolBodyLeftPaddingTotal
1055
985
1056− // Try difftastic if enabled. Fall back to builtin on any failure.
1057− if diffTool == "difftastic" {
1058− if out, err := difftastic.RenderDiff(file, meta.OldContent, meta.NewContent, bodyWidth); err == nil && out != "" {
1059− formatted := truncateDiff(sty, out, bodyWidth, expanded)
1060− // Add failed edits note if any exist.
1061− if len(meta.EditsFailed) > 0 {
1062− noteTag := sty.Tool.NoteTag.Render("Note")
1063− noteMsg := fmt.Sprintf("%d of %d edits succeeded", meta.EditsApplied, totalEdits)
1064− note := fmt.Sprintf("%s %s", noteTag, sty.Tool.NoteMessage.Render(noteMsg))
1065− formatted = formatted + "\n\n" + note
1066− }
1067− return sty.Tool.Body.Render(formatted)
1068− }
1069− }
1070−
1071 formatter := common.DiffFormatter(sty).
986 formatter := common.DiffFormatter(sty).
1072 Before(file, meta.OldContent).
987 Before(file, meta.OldContent).
1073 After(file, meta.NewContent).
988 After(file, meta.NewContent).
@@ -1079,7 +994,20 @@ func toolOutputMultiEditDiffContent(sty *styles.Styles, file string, meta tools.
1079 }
994 }
1080
995
1081 formatted := formatter.String()
996 formatted := formatter.String()
1082− formatted = truncateDiff(sty, formatted, bodyWidth, expanded)
997+ lines := strings.Split(formatted, "\n")
998+
999+ // Truncate if needed.
1000+ maxLines := responseContextHeight
1001+ if expanded {
1002+ maxLines = len(lines)
1003+ }
1004+
1005+ if len(lines) > maxLines && !expanded {
1006+ truncMsg := sty.Tool.DiffTruncation.
1007+ Width(bodyWidth).
1008+ Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines))
1009+ formatted = truncMsg + "\n" + strings.Join(lines[:maxLines], "\n")
1010+ }
1083
1011
1084 // Add failed edits note if any exist.
1012 // Add failed edits note if any exist.
1085 if len(meta.EditsFailed) > 0 {
1013 if len(meta.EditsFailed) > 0 {
@@ -1092,22 +1020,6 @@ func toolOutputMultiEditDiffContent(sty *styles.Styles, file string, meta tools.
1092 return sty.Tool.Body.Render(formatted)
1020 return sty.Tool.Body.Render(formatted)
1093}
1021}
1094
1022
1095−// truncateDiff truncates diff output to diffContextHeight lines unless expanded.
1096−func truncateDiff(sty *styles.Styles, content string, bodyWidth int, expanded bool) string {
1097− lines := strings.Split(content, "\n")
1098− maxLines := diffContextHeight
1099− if expanded {
1100− maxLines = len(lines)
1101− }
1102− if len(lines) > maxLines && !expanded {
1103− truncMsg := sty.Tool.DiffTruncation.
1104− Width(bodyWidth).
1105− Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines))
1106− return strings.Join(lines[:maxLines], "\n") + "\n" + truncMsg
1107− }
1108− return content
1109−}
1110−
1111// roundedEnumerator creates a tree enumerator with rounded corners.
1023// roundedEnumerator creates a tree enumerator with rounded corners.
1112func roundedEnumerator(lPadding, width int) tree.Enumerator {
1024func roundedEnumerator(lPadding, width int) tree.Enumerator {
1113 if width == 0 {
1025 if width == 0 {
@@ -1408,7 +1320,7 @@ func (t *baseToolMessageItem) formatBashResultForCopy() string {
1408
1320
1409 var meta tools.BashResponseMetadata
1321 var meta tools.BashResponseMetadata
1410 if t.result.Metadata != "" {
1322 if t.result.Metadata != "" {
1411− _ = json.Unmarshal([]byte(t.result.Metadata), &meta)
1323+ json.Unmarshal([]byte(t.result.Metadata), &meta)
1412 }
1324 }
1413
1325
1414 output := meta.Output
1326 output := meta.Output
@@ -1431,7 +1343,7 @@ func (t *baseToolMessageItem) formatViewResultForCopy() string {
1431
1343
1432 var meta tools.ViewResponseMetadata
1344 var meta tools.ViewResponseMetadata
1433 if t.result.Metadata != "" {
1345 if t.result.Metadata != "" {
1434− _ = json.Unmarshal([]byte(t.result.Metadata), &meta)
1346+ json.Unmarshal([]byte(t.result.Metadata), &meta)
1435 }
1347 }
1436
1348
1437 if meta.Content == "" {
1349 if meta.Content == "" {
@@ -1502,7 +1414,7 @@ func (t *baseToolMessageItem) formatEditResultForCopy() string {
1502 }
1414 }
1503
1415
1504 var params tools.EditParams
1416 var params tools.EditParams
1505− _ = json.Unmarshal([]byte(t.toolCall.Input), &params)
1417+ json.Unmarshal([]byte(t.toolCall.Input), &params)
1506
1418
1507 var result strings.Builder
1419 var result strings.Builder
1508
1420
@@ -1537,7 +1449,7 @@ func (t *baseToolMessageItem) formatMultiEditResultForCopy() string {
1537 }
1449 }
1538
1450
1539 var params tools.MultiEditParams
1451 var params tools.MultiEditParams
1540− _ = json.Unmarshal([]byte(t.toolCall.Input), &params)
1452+ json.Unmarshal([]byte(t.toolCall.Input), &params)
1541
1453
1542 var result strings.Builder
1454 var result strings.Builder
1543 if meta.OldContent != "" || meta.NewContent != "" {
1455 if meta.OldContent != "" || meta.NewContent != "" {
@@ -1728,7 +1640,7 @@ func prettifyToolName(name string) string {
1728 case tools.AgenticFetchToolName:
1640 case tools.AgenticFetchToolName:
1729 return "Agentic Fetch"
1641 return "Agentic Fetch"
1730 case tools.WebFetchToolName:
1642 case tools.WebFetchToolName:
1731− return "Web Fetch"
1643+ return "Fetch"
1732 case tools.WebSearchToolName:
1644 case tools.WebSearchToolName:
1733 return "Search"
1645 return "Search"
1734 case tools.GlobToolName:
1646 case tools.GlobToolName:
internal/ui/chat/unified_diff.go+14−13
@@ -1,11 +1,11 @@
1package chat
1package chat
2
2
3import (
3import (
4+ "fmt"
4 "strings"
5 "strings"
5
6
6 "github.com/charmbracelet/crush/internal/diffdetect"
7 "github.com/charmbracelet/crush/internal/diffdetect"
7 "github.com/charmbracelet/crush/internal/ui/common"
8 "github.com/charmbracelet/crush/internal/ui/common"
8− "github.com/charmbracelet/crush/internal/ui/difftastic"
9 "github.com/charmbracelet/crush/internal/ui/styles"
9 "github.com/charmbracelet/crush/internal/ui/styles"
10)
10)
11
11
@@ -130,23 +130,13 @@ func parseUnifiedDiff(content string) []parsedDiffFile {
130 return result
130 return result
131}
131}
132
132
133−func toolOutputDiffContentFromUnified(sty *styles.Styles, content string, width int, expanded bool, diffTool string) string {
133+func toolOutputDiffContentFromUnified(sty *styles.Styles, content string, width int, expanded bool) string {
134 files := parseUnifiedDiff(content)
134 files := parseUnifiedDiff(content)
135 if len(files) == 0 {
135 if len(files) == 0 {
136 bodyWidth := width - toolBodyLeftPaddingTotal
136 bodyWidth := width - toolBodyLeftPaddingTotal
137 return sty.Tool.Body.Render(toolOutputCodeContent(sty, "result.diff", content, 0, bodyWidth, expanded))
137 return sty.Tool.Body.Render(toolOutputCodeContent(sty, "result.diff", content, 0, bodyWidth, expanded))
138 }
138 }
139 bodyWidth := width - toolBodyLeftPaddingTotal
139 bodyWidth := width - toolBodyLeftPaddingTotal
140−
141− // Try difftastic if enabled and there's a single file diff. Fall back to
142− // builtin on any failure or when multiple files are involved.
143− if diffTool == "difftastic" && len(files) == 1 {
144− f := files[0]
145− if out, err := difftastic.RenderDiff(f.path, f.before, f.after, bodyWidth); err == nil && out != "" {
146− return sty.Tool.Body.Render(truncateDiff(sty, out, bodyWidth, expanded))
147− }
148− }
149−
150 var blocks []string
140 var blocks []string
151 for i, f := range files {
141 for i, f := range files {
152 formatter := common.DiffFormatter(sty).
142 formatter := common.DiffFormatter(sty).
@@ -166,5 +156,16 @@ func toolOutputDiffContentFromUnified(sty *styles.Styles, content string, width
166 blocks = append(blocks, formatted)
156 blocks = append(blocks, formatted)
167 }
157 }
168 combined := strings.Join(blocks, "\n")
158 combined := strings.Join(blocks, "\n")
159+ lines := strings.Split(combined, "\n")
160+ maxLines := responseContextHeight
161+ if expanded {
162+ maxLines = len(lines)
163+ }
164+ if len(lines) > maxLines && !expanded {
165+ truncMsg := sty.Tool.DiffTruncation.
166+ Width(bodyWidth).
167+ Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines))
168+ combined = strings.Join(lines[:maxLines], "\n") + "\n" + truncMsg
169+ }
169− return sty.Tool.Body.Render(truncateDiff(sty, combined, bodyWidth, expanded))
170+ return sty.Tool.Body.Render(combined)
170}
171}
internal/ui/chat/version_bump_test.go+10−10
@@ -131,7 +131,7 @@ func TestBaseToolMessageItem_MutatorsBumpVersion(t *testing.T) {
131
131
132 sty := styles.CharmtonePantera()
132 sty := styles.CharmtonePantera()
133 tc := message.ToolCall{ID: "tc1", Name: "bash", Input: "{}", Finished: false}
133 tc := message.ToolCall{ID: "tc1", Name: "bash", Input: "{}", Finished: false}
134− item := NewToolMessageItem(&sty, "msg", tc, nil, false, "")
134+ item := NewToolMessageItem(&sty, "msg", tc, nil, false)
135
135
136 v := item.(versionedItem)
136 v := item.(versionedItem)
137
137
@@ -283,7 +283,7 @@ func TestAgentToolMessageItem_NestedToolMutatorsBumpVersion(t *testing.T) {
283
283
284 mkChild := func(id string) ToolMessageItem {
284 mkChild := func(id string) ToolMessageItem {
285 tc := message.ToolCall{ID: id, Name: "bash", Input: `{}`, Finished: false}
285 tc := message.ToolCall{ID: id, Name: "bash", Input: `{}`, Finished: false}
286− return NewToolMessageItem(&sty, "msg", tc, nil, false, "")
286+ return NewToolMessageItem(&sty, "msg", tc, nil, false)
287 }
287 }
288
288
289 // AddNestedTool always bumps.
289 // AddNestedTool always bumps.
@@ -320,7 +320,7 @@ func TestAgenticFetchToolMessageItem_NestedToolMutatorsBumpVersion(t *testing.T)
320
320
321 mkChild := func(id string) ToolMessageItem {
321 mkChild := func(id string) ToolMessageItem {
322 tc := message.ToolCall{ID: id, Name: "fetch", Input: `{}`, Finished: false}
322 tc := message.ToolCall{ID: id, Name: "fetch", Input: `{}`, Finished: false}
323− return NewToolMessageItem(&sty, "msg", tc, nil, false, "")
323+ return NewToolMessageItem(&sty, "msg", tc, nil, false)
324 }
324 }
325
325
326 requireBump(t, "AddNestedTool", item, func() {
326 requireBump(t, "AddNestedTool", item, func() {
@@ -357,7 +357,7 @@ func TestAgentToolMessageItem_NestedChildInPlaceMutationBumpsParent(t *testing.T
357 item := NewAgentToolMessageItem(&sty, parent, nil, false)
357 item := NewAgentToolMessageItem(&sty, parent, nil, false)
358
358
359 childTC := message.ToolCall{ID: "c1", Name: "bash", Input: `{}`, Finished: false}
359 childTC := message.ToolCall{ID: "c1", Name: "bash", Input: `{}`, Finished: false}
360− child := NewToolMessageItem(&sty, "msg", childTC, nil, false, "")
360+ child := NewToolMessageItem(&sty, "msg", childTC, nil, false)
361 item.AddNestedTool(child)
361 item.AddNestedTool(child)
362
362
363 v0 := item.Version()
363 v0 := item.Version()
@@ -386,7 +386,7 @@ func TestAgenticFetchToolMessageItem_NestedChildInPlaceMutationBumpsParent(t *te
386 item := NewAgenticFetchToolMessageItem(&sty, parent, nil, false)
386 item := NewAgenticFetchToolMessageItem(&sty, parent, nil, false)
387
387
388 childTC := message.ToolCall{ID: "c1", Name: "fetch", Input: `{}`, Finished: false}
388 childTC := message.ToolCall{ID: "c1", Name: "fetch", Input: `{}`, Finished: false}
389− child := NewToolMessageItem(&sty, "msg", childTC, nil, false, "")
389+ child := NewToolMessageItem(&sty, "msg", childTC, nil, false)
390 item.AddNestedTool(child)
390 item.AddNestedTool(child)
391
391
392 v0 := item.Version()
392 v0 := item.Version()
@@ -427,7 +427,7 @@ func TestBaseToolMessageItem_AnimateBumpsVersion(t *testing.T) {
427
427
428 sty := styles.CharmtonePantera()
428 sty := styles.CharmtonePantera()
429 tc := message.ToolCall{ID: "tc-spin", Name: "bash", Input: "{}", Finished: false}
429 tc := message.ToolCall{ID: "tc-spin", Name: "bash", Input: "{}", Finished: false}
430− item := NewToolMessageItem(&sty, "msg", tc, nil, false, "")
430+ item := NewToolMessageItem(&sty, "msg", tc, nil, false)
431 v := item.(versionedItem)
431 v := item.(versionedItem)
432 a, ok := item.(Animatable)
432 a, ok := item.(Animatable)
433 require.True(t, ok, "base tool message item must implement Animatable")
433 require.True(t, ok, "base tool message item must implement Animatable")
@@ -475,7 +475,7 @@ func TestAgentToolMessageItem_AnimateBumpsVersion(t *testing.T) {
475 parent := NewAgentToolMessageItem(&sty, parentTC, nil, false)
475 parent := NewAgentToolMessageItem(&sty, parentTC, nil, false)
476
476
477 childTC := message.ToolCall{ID: "agent-child", Name: "bash", Input: `{}`, Finished: false}
477 childTC := message.ToolCall{ID: "agent-child", Name: "bash", Input: `{}`, Finished: false}
478− child := NewToolMessageItem(&sty, "msg", childTC, nil, false, "")
478+ child := NewToolMessageItem(&sty, "msg", childTC, nil, false)
479 parent.AddNestedTool(child)
479 parent.AddNestedTool(child)
480
480
481 // Spinning + parent's own ID → parent bumps.
481 // Spinning + parent's own ID → parent bumps.
@@ -520,7 +520,7 @@ func TestAgenticFetchToolMessageItem_AnimateBumpsVersion(t *testing.T) {
520 parent := NewAgenticFetchToolMessageItem(&sty, parentTC, nil, false)
520 parent := NewAgenticFetchToolMessageItem(&sty, parentTC, nil, false)
521
521
522 childTC := message.ToolCall{ID: "fetch-child", Name: "fetch", Input: `{}`, Finished: false}
522 childTC := message.ToolCall{ID: "fetch-child", Name: "fetch", Input: `{}`, Finished: false}
523− child := NewToolMessageItem(&sty, "msg", childTC, nil, false, "")
523+ child := NewToolMessageItem(&sty, "msg", childTC, nil, false)
524 parent.AddNestedTool(child)
524 parent.AddNestedTool(child)
525
525
526 requireBump(t, "Animate[spinning,parent ID]", parent, func() {
526 requireBump(t, "Animate[spinning,parent ID]", parent, func() {
@@ -551,7 +551,7 @@ func TestBaseToolMessageItem_FinishedTransition(t *testing.T) {
551
551
552 sty := styles.CharmtonePantera()
552 sty := styles.CharmtonePantera()
553 tc := message.ToolCall{ID: "tc-fin", Name: "bash", Input: "{}", Finished: false}
553 tc := message.ToolCall{ID: "tc-fin", Name: "bash", Input: "{}", Finished: false}
554− item := NewToolMessageItem(&sty, "msg", tc, nil, false, "")
554+ item := NewToolMessageItem(&sty, "msg", tc, nil, false)
555 require.False(t, item.Finished(), "running tool must not be Finished()")
555 require.False(t, item.Finished(), "running tool must not be Finished()")
556
556
557 tcFinished := tc
557 tcFinished := tc
@@ -562,6 +562,6 @@ func TestBaseToolMessageItem_FinishedTransition(t *testing.T) {
562
562
563 // Canceled tool with no result is also Finished.
563 // Canceled tool with no result is also Finished.
564 tcCanceled := message.ToolCall{ID: "tc-cancel", Name: "bash", Input: "{}", Finished: false}
564 tcCanceled := message.ToolCall{ID: "tc-cancel", Name: "bash", Input: "{}", Finished: false}
565− canceled := NewToolMessageItem(&sty, "msg", tcCanceled, nil, true, "")
565+ canceled := NewToolMessageItem(&sty, "msg", tcCanceled, nil, true)
566 require.True(t, canceled.Finished(), "canceled tool must be Finished()")
566 require.True(t, canceled.Finished(), "canceled tool must be Finished()")
567}
567}
internal/ui/common/capabilities.go+12
@@ -1,6 +1,7 @@
1package common
1package common
2
2
3import (
3import (
4+ "slices"
4 "strings"
5 "strings"
5
6
6 tea "charm.land/bubbletea/v2"
7 tea "charm.land/bubbletea/v2"
@@ -28,6 +29,8 @@ type Capabilities struct {
28 // KittyGraphics indicates whether the terminal supports the Kitty graphics
29 // KittyGraphics indicates whether the terminal supports the Kitty graphics
29 // protocol.
30 // protocol.
30 KittyGraphics bool
31 KittyGraphics bool
32+ // SixelGraphics indicates whether the terminal supports Sixel graphics.
33+ SixelGraphics bool
31 // Env is the terminal environment variables.
34 // Env is the terminal environment variables.
32 Env uv.Environ
35 Env uv.Environ
33 // TerminalVersion is the terminal version string.
36 // TerminalVersion is the terminal version string.
@@ -53,6 +56,10 @@ func (c *Capabilities) Update(msg any) {
53 c.PixelY = m.Height
56 c.PixelY = m.Height
54 case uv.KittyGraphicsEvent:
57 case uv.KittyGraphicsEvent:
55 c.KittyGraphics = true
58 c.KittyGraphics = true
59+ case uv.PrimaryDeviceAttributesEvent:
60+ if slices.Contains(m, 4) {
61+ c.SixelGraphics = true
62+ }
56 case tea.TerminalVersionMsg:
63 case tea.TerminalVersionMsg:
57 c.TerminalVersion = m.Name
64 c.TerminalVersion = m.Name
58 case tea.ModeReportMsg:
65 case tea.ModeReportMsg:
@@ -101,6 +108,11 @@ func (c Capabilities) SupportsKittyGraphics() bool {
101 return c.KittyGraphics
108 return c.KittyGraphics
102}
109}
103
110
111+// SupportsSixelGraphics returns true if the terminal supports Sixel graphics.
112+func (c Capabilities) SupportsSixelGraphics() bool {
113+ return c.SixelGraphics
114+}
115+
104// CellSize returns the size of a single terminal cell in pixels.
116// CellSize returns the size of a single terminal cell in pixels.
105func (c Capabilities) CellSize() (width, height int) {
117func (c Capabilities) CellSize() (width, height int) {
106 if c.Columns == 0 || c.Rows == 0 {
118 if c.Columns == 0 || c.Rows == 0 {
internal/ui/common/diff.go+1−20
@@ -1,35 +1,16 @@
1package common
1package common
2
2
3import (
3import (
4− "sync"
5−
6 "github.com/alecthomas/chroma/v2"
4 "github.com/alecthomas/chroma/v2"
7 "github.com/charmbracelet/crush/internal/ui/diffview"
5 "github.com/charmbracelet/crush/internal/ui/diffview"
8 "github.com/charmbracelet/crush/internal/ui/styles"
6 "github.com/charmbracelet/crush/internal/ui/styles"
9)
7)
10
8
11−// diffStyleCache memoizes the *chroma.Style built for a given theme. Building
12−// the style (MustNewStyle) parses colors for every entry and was previously
13−// done on every DiffFormatter call. The Styles struct is mutated in place on
14−// theme change, so we key on the theme's content fingerprint rather than its
15−// pointer identity.
16−var diffStyleCache sync.Map // map[uint64]*chroma.Style
17−
18// DiffFormatter returns a diff formatter with the given styles that can be
9// DiffFormatter returns a diff formatter with the given styles that can be
19// used to format diff outputs.
10// used to format diff outputs.
20func DiffFormatter(s *styles.Styles) *diffview.DiffView {
11func DiffFormatter(s *styles.Styles) *diffview.DiffView {
21 formatDiff := diffview.New()
12 formatDiff := diffview.New()
22−
23− theme := s.ChromaTheme()
24− key := chromaStyleKey(theme, nil)
25− var style *chroma.Style
26− if cached, ok := diffStyleCache.Load(key); ok {
27− style = cached.(*chroma.Style)
28− } else {
29− style = chroma.MustNewStyle("crush", theme)
13+ style := chroma.MustNewStyle("crush", s.ChromaTheme())
30− diffStyleCache.Store(key, style)
31− }
32−
33 diff := formatDiff.ChromaStyle(style).Style(s.Diff).TabWidth(4)
14 diff := formatDiff.ChromaStyle(style).Style(s.Diff).TabWidth(4)
34 return diff
15 return diff
35}
16}
internal/ui/common/elements.go−3
@@ -115,9 +115,6 @@ func formatTokensAndCost(t *styles.Styles, tokens, contextWindow int64, cost flo
115 if contextWindow > 0 {
115 if contextWindow > 0 {
116 percentage = (float64(tokens) / float64(contextWindow)) * 100
116 percentage = (float64(tokens) / float64(contextWindow)) * 100
117 }
117 }
118− if percentage > 100 {
119− percentage = 100
120− }
121
118
122 formattedCost := t.ModelInfo.Cost.Render(fmt.Sprintf("$%.2f", cost))
119 formattedCost := t.ModelInfo.Cost.Render(fmt.Sprintf("$%.2f", cost))
123
120
internal/ui/common/highlight.go+11−60
@@ -2,11 +2,7 @@ package common
2
2
3import (
3import (
4 "bytes"
4 "bytes"
5− "hash/fnv"
6 "image/color"
5 "image/color"
7− "sort"
8− "strconv"
9− "sync"
10
6
11 "github.com/alecthomas/chroma/v2"
7 "github.com/alecthomas/chroma/v2"
12 "github.com/alecthomas/chroma/v2/formatters"
8 "github.com/alecthomas/chroma/v2/formatters"
@@ -15,41 +11,6 @@ import (
15 "github.com/charmbracelet/crush/internal/ui/styles"
11 "github.com/charmbracelet/crush/internal/ui/styles"
16)
12)
17
13
18−// highlightStyleCache memoizes the *chroma.Style built for a given theme +
19−// background combination. Building the style (MustNewStyle + Builder.Transform.
20−// Build) parses colors for every entry and was previously done on every
21−// SyntaxHighlight call. The Styles struct is mutated in place on theme change,
22−// so we key on the theme's content fingerprint (plus bg) rather than its
23−// pointer identity.
24−var highlightStyleCache sync.Map // map[uint64]*chroma.Style
25−
26−// chromaStyleKey derives a stable cache key from the theme entries and bg.
27−func chromaStyleKey(theme chroma.StyleEntries, bg color.Color) uint64 {
28− h := fnv.New64a()
29− // Sort token types for a deterministic fingerprint (map order is random).
30− types := make([]int, 0, len(theme))
31− for tt := range theme {
32− types = append(types, int(tt))
33− }
34− sort.Ints(types)
35− for _, tt := range types {
36− h.Write([]byte(strconv.Itoa(tt)))
37− h.Write([]byte{0})
38− h.Write([]byte(theme[chroma.TokenType(tt)]))
39− h.Write([]byte{0})
40− }
41− var buf [8]byte
42− if bg != nil {
43− r, g, b, a := bg.RGBA()
44− buf[0] = byte(r >> 8)
45− buf[1] = byte(g >> 8)
46− buf[2] = byte(b >> 8)
47− buf[3] = byte(a >> 8)
48− }
49− h.Write(buf[:4])
50− return h.Sum64()
51−}
52−
53// SyntaxHighlight applies syntax highlighting to the given source code based
14// SyntaxHighlight applies syntax highlighting to the given source code based
54// on the file name and background color. It returns the highlighted code as a
15// on the file name and background color. It returns the highlighted code as a
55// string.
16// string.
@@ -70,28 +31,18 @@ func SyntaxHighlight(st *styles.Styles, source, fileName string, bg color.Color)
70 f = formatters.Fallback
31 f = formatters.Fallback
71 }
32 }
72
33
73− // Build (or reuse a cached) chroma style for this theme + background.
74− theme := st.ChromaTheme()
34+ style := chroma.MustNewStyle("crush", st.ChromaTheme())
75− key := chromaStyleKey(theme, bg)
76− var s *chroma.Style
77− if cached, ok := highlightStyleCache.Load(key); ok {
78− s = cached.(*chroma.Style)
79− } else {
80− style := chroma.MustNewStyle("crush", theme)
81
35
82− // Modify the style to use the provided background
36+ // Modify the style to use the provided background
83− built, err := style.Builder().Transform(
37+ s, err := style.Builder().Transform(
84− func(t chroma.StyleEntry) chroma.StyleEntry {
38+ func(t chroma.StyleEntry) chroma.StyleEntry {
85− r, g, b, _ := bg.RGBA()
39+ r, g, b, _ := bg.RGBA()
86− t.Background = chroma.NewColour(uint8(r>>8), uint8(g>>8), uint8(b>>8))
40+ t.Background = chroma.NewColour(uint8(r>>8), uint8(g>>8), uint8(b>>8))
87− return t
41+ return t
88− },
42+ },
89− ).Build()
43+ ).Build()
90− if err != nil {
44+ if err != nil {
91− built = chromastyles.Fallback
45+ s = chromastyles.Fallback
92− }
93− s = built
94− highlightStyleCache.Store(key, s)
95 }
46 }
96
47
97 // Tokenize and format
48 // Tokenize and format
internal/ui/common/scrollbar.go+2−4
@@ -30,17 +30,15 @@ func Scrollbar(s *styles.Styles, height, contentSize, viewportSize, offset int)
30 }
30 }
31
31
32 // Build the scrollbar.
32 // Build the scrollbar.
33− thumb := s.Dialog.ScrollbarThumb.Render(styles.ScrollbarThumb)
34− track := s.Dialog.ScrollbarTrack.Render(styles.ScrollbarTrack)
35 var sb strings.Builder
33 var sb strings.Builder
36 for i := range height {
34 for i := range height {
37 if i > 0 {
35 if i > 0 {
38 sb.WriteString("\n")
36 sb.WriteString("\n")
39 }
37 }
40 if i >= thumbPos && i < thumbPos+thumbSize {
38 if i >= thumbPos && i < thumbPos+thumbSize {
41− sb.WriteString(thumb)
39+ sb.WriteString(s.Dialog.ScrollbarThumb.Render(styles.ScrollbarThumb))
42 } else {
40 } else {
43− sb.WriteString(track)
41+ sb.WriteString(s.Dialog.ScrollbarTrack.Render(styles.ScrollbarTrack))
44 }
42 }
45 }
43 }
46
44
internal/ui/completions/completions.go+4−6
@@ -230,13 +230,11 @@ func (c *Completions) applyNamePriorityFilter(query string) {
230 }
230 }
231
231
232 c.list.SetItems(c.allItems...)
232 c.list.SetItems(c.allItems...)
233− // SetFilter computes the filtered set (a single fuzzy pass) and stores it
234− // in the list; read it back instead of recomputing via FilteredItems.
235 c.list.SetFilter(query)
233 c.list.SetFilter(query)
236− n := c.list.Len()
234+ raw := c.list.FilteredItems()
237− filtered := make([]list.FilterableItem, 0, n)
235+ filtered := make([]list.FilterableItem, 0, len(raw))
238− for i := range n {
236+ for _, item := range raw {
239− filterable, ok := c.list.ItemAt(i).(list.FilterableItem)
237+ filterable, ok := item.(list.FilterableItem)
240 if !ok {
238 if !ok {
241 continue
239 continue
242 }
240 }
internal/ui/dialog/actions.go−3
@@ -102,9 +102,6 @@ type (
102type (
102type (
103 ActionChangeAPIKeyState struct {
103 ActionChangeAPIKeyState struct {
104 State APIKeyInputState
104 State APIKeyInputState
105− // Reason is an optional human-readable explanation, set when
106− // transitioning to APIKeyInputStateError.
107− Reason string
108 }
105 }
109)
106)
110
107
internal/ui/dialog/api_key_input.go+7−54
@@ -40,9 +40,8 @@ type APIKeyInput struct {
40 model config.SelectedModel
40 model config.SelectedModel
41 modelType config.SelectedModelType
41 modelType config.SelectedModelType
42
42
43− width int
43+ width int
44− state APIKeyInputState
44+ state APIKeyInputState
45− errMsg string // reason for the last verification failure
46
45
47 keyMap struct {
46 keyMap struct {
48 Submit key.Binding
47 Submit key.Binding
@@ -92,7 +91,7 @@ func NewAPIKeyInput(
92
91
93 m.keyMap.Submit = key.NewBinding(
92 m.keyMap.Submit = key.NewBinding(
94 key.WithKeys("enter", "ctrl+y"),
93 key.WithKeys("enter", "ctrl+y"),
95− key.WithHelp("enter", "confirm"),
94+ key.WithHelp("enter", "submit"),
96 )
95 )
97 m.keyMap.Close = CloseKey
96 m.keyMap.Close = CloseKey
98
97
@@ -111,11 +110,8 @@ func (m *APIKeyInput) HandleMsg(msg tea.Msg) Action {
111 m.state = msg.State
110 m.state = msg.State
112 switch m.state {
111 switch m.state {
113 case APIKeyInputStateVerifying:
112 case APIKeyInputStateVerifying:
114− m.errMsg = ""
115 cmd := tea.Batch(m.spinner.Tick, m.verifyAPIKey)
113 cmd := tea.Batch(m.spinner.Tick, m.verifyAPIKey)
116 return ActionCmd{cmd}
114 return ActionCmd{cmd}
117− case APIKeyInputStateError:
118− m.errMsg = msg.Reason
119 }
115 }
120 case spinner.TickMsg:
116 case spinner.TickMsg:
121 switch m.state {
117 switch m.state {
@@ -173,23 +169,14 @@ func (m *APIKeyInput) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
173
169
174 m.input.Prompt = m.spinner.View()
170 m.input.Prompt = m.spinner.View()
175
171
176− lines := []string{
172+ content := strings.Join([]string{
177 m.headerView(),
173 m.headerView(),
178 inputStyle.Render(m.inputView()),
174 inputStyle.Render(m.inputView()),
179− }
180− if m.state == APIKeyInputStateError && m.errMsg != "" {
181− errStyle := textStyle.
182− Foreground(charmtone.Cherry).
183− Width(m.width - dialogStyle.GetHorizontalFrameSize())
184− lines = append(lines, errStyle.Render(m.errMsg))
185− }
186− lines = append(lines,
187 textStyle.Render("This will be written in your global configuration:"),
175 textStyle.Render("This will be written in your global configuration:"),
188 textStyle.Render(config.GlobalConfigData()),
176 textStyle.Render(config.GlobalConfigData()),
189 "",
177 "",
190 helpStyle.Render(m.help.View(m)),
178 helpStyle.Render(m.help.View(m)),
191− )
179+ }, "\n")
192− content := strings.Join(lines, "\n")
193
180
194 cur := m.Cursor()
181 cur := m.Cursor()
195
182
@@ -314,43 +301,9 @@ func (m *APIKeyInput) verifyAPIKey() tea.Msg {
314 }
301 }
315
302
316 if err == nil {
303 if err == nil {
317− return ActionChangeAPIKeyState{State: APIKeyInputStateVerified}
304+ return ActionChangeAPIKeyState{APIKeyInputStateVerified}
318− }
319− return ActionChangeAPIKeyState{
320− State: APIKeyInputStateError,
321− Reason: apiKeyErrorReason(err),
322− }
323−}
324−
325−// apiKeyErrorReason maps a TestConnection error to a concise, user-facing
326−// explanation. Common cases (auth, permission, network) get a friendly hint;
327−// anything else falls back to the raw error message.
328−func apiKeyErrorReason(err error) string {
329− if err == nil {
330− return ""
331− }
332− msg := strings.TrimSpace(err.Error())
333− low := strings.ToLower(msg)
334− switch {
335− case strings.Contains(low, "401"),
336− strings.Contains(low, "unauthorized"),
337− strings.Contains(low, "invalid api key"),
338− strings.Contains(low, "invalid_api_key"),
339− strings.Contains(low, "authentication"):
340− return "Authentication failed — the key was rejected (401)."
341− case strings.Contains(low, "403"),
342− strings.Contains(low, "forbidden"):
343− return "Access forbidden (403) — the key may lack permissions."
344− case strings.Contains(low, "no such host"),
345− strings.Contains(low, "timeout"),
346− strings.Contains(low, "timed out"),
347− strings.Contains(low, "connection refused"),
348− strings.Contains(low, "network"),
349− strings.Contains(low, "dial tcp"):
350− return "Network error — could not reach the provider."
351− default:
352− return msg
353 }
305 }
306+ return ActionChangeAPIKeyState{APIKeyInputStateError}
354}
307}
355
308
356func (m *APIKeyInput) saveKeyAndContinue() Action {
309func (m *APIKeyInput) saveKeyAndContinue() Action {
internal/ui/dialog/arguments.go+11−20
@@ -23,21 +23,6 @@ import (
23// ArgumentsID is the identifier for the arguments dialog.
23// ArgumentsID is the identifier for the arguments dialog.
24const ArgumentsID = "arguments"
24const ArgumentsID = "arguments"
25
25
26−// argumentTitleCaser title-cases argument labels. It is stateless and safe to
27−// reuse, so it is kept package-level to avoid rebuilding it on every Draw.
28−var argumentTitleCaser = cases.Title(language.English)
29−
30−// prettyArgumentLabel converts an argument title into a human-friendly label.
31−func prettyArgumentLabel(title string) string {
32− title = strings.ReplaceAll(title, "_", " ")
33− title = strings.ReplaceAll(title, "-", " ")
34− titleParts := strings.Fields(title)
35− for i, part := range titleParts {
36− titleParts[i] = argumentTitleCaser.String(strings.ToLower(part))
37− }
38− return strings.Join(titleParts, " ")
39−}
40−
41// Dialog sizing for arguments.
26// Dialog sizing for arguments.
42const (
27const (
43 maxInputWidth = 120
28 maxInputWidth = 120
@@ -51,7 +36,6 @@ type Arguments struct {
51 com *common.Common
36 com *common.Common
52 title string
37 title string
53 arguments []commands.Argument
38 arguments []commands.Argument
54− labels []string // pretty labels, precomputed from argument titles
55 inputs []textinput.Model
39 inputs []textinput.Model
56 focused int
40 focused int
57 spinner spinner.Model
41 spinner spinner.Model
@@ -104,9 +88,7 @@ func NewArguments(com *common.Common, title, description string, arguments []com
104
88
105 // Create input fields for each argument.
89 // Create input fields for each argument.
106 a.inputs = make([]textinput.Model, len(arguments))
90 a.inputs = make([]textinput.Model, len(arguments))
107− a.labels = make([]string, len(arguments))
108 for i, arg := range arguments {
91 for i, arg := range arguments {
109− a.labels[i] = prettyArgumentLabel(arg.Title)
110 input := textinput.New()
92 input := textinput.New()
111 input.SetVirtualCursor(false)
93 input.SetVirtualCursor(false)
112 input.SetStyles(com.Styles.TextInput)
94 input.SetStyles(com.Styles.TextInput)
@@ -280,12 +262,20 @@ func (a *Arguments) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
280 dialogContentStyle := s.Dialog.Arguments.Content
262 dialogContentStyle := s.Dialog.Arguments.Content
281 possibleWidth := area.Dx() - s.Dialog.View.GetHorizontalFrameSize() - dialogContentStyle.GetHorizontalFrameSize()
263 possibleWidth := area.Dx() - s.Dialog.View.GetHorizontalFrameSize() - dialogContentStyle.GetHorizontalFrameSize()
282 // Build fields with label and input.
264 // Build fields with label and input.
265+ caser := cases.Title(language.English)
266+
283 var fields []string
267 var fields []string
284 for i, arg := range a.arguments {
268 for i, arg := range a.arguments {
285 isFocused := i == a.focused
269 isFocused := i == a.focused
286
270
287− // Label was pretty-printed once at construction.
271+ // Try to pretty up the title for the label.
288− labelText := a.labels[i]
272+ title := strings.ReplaceAll(arg.Title, "_", " ")
273+ title = strings.ReplaceAll(title, "-", " ")
274+ titleParts := strings.Fields(title)
275+ for i, part := range titleParts {
276+ titleParts[i] = caser.String(strings.ToLower(part))
277+ }
278+ labelText := strings.Join(titleParts, " ")
289
279
290 markRequiredStyle := s.Dialog.Arguments.InputRequiredMarkBlurred
280 markRequiredStyle := s.Dialog.Arguments.InputRequiredMarkBlurred
291
281
@@ -315,6 +305,7 @@ func (a *Arguments) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
315 renderedFields := lipgloss.JoinVertical(lipgloss.Left, fields...)
305 renderedFields := lipgloss.JoinVertical(lipgloss.Left, fields...)
316
306
317 // Anchor width to the longest field, capped at maxInputWidth.
307 // Anchor width to the longest field, capped at maxInputWidth.
308+ const scrollbarWidth = 1
318 width := lipgloss.Width(renderedFields)
309 width := lipgloss.Width(renderedFields)
319 height := lipgloss.Height(renderedFields)
310 height := lipgloss.Height(renderedFields)
320
311
internal/ui/dialog/commands.go+163−48
@@ -2,6 +2,7 @@ package dialog
2
2
3import (
3import (
4 "os"
4 "os"
5+ "strings"
5
6
6 "charm.land/bubbles/v2/help"
7 "charm.land/bubbles/v2/help"
7 "charm.land/bubbles/v2/key"
8 "charm.land/bubbles/v2/key"
@@ -12,16 +13,29 @@ import (
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/ui/common"
14 "github.com/charmbracelet/crush/internal/ui/common"
14 "github.com/charmbracelet/crush/internal/ui/list"
15 "github.com/charmbracelet/crush/internal/ui/list"
16+ "github.com/charmbracelet/crush/internal/ui/styles"
15 uv "github.com/charmbracelet/ultraviolet"
17 uv "github.com/charmbracelet/ultraviolet"
16)
18)
17
19
18// CommandsID is the identifier for the commands dialog.
20// CommandsID is the identifier for the commands dialog.
19const CommandsID = "commands"
21const CommandsID = "commands"
20
22
23+// CommandType represents the type of commands being displayed.
24+type CommandType uint
25+
26+// String returns the string representation of the CommandType.
27+func (c CommandType) String() string { return []string{"System", "User", "MCP"}[c] }
28+
21const (
29const (
22 sidebarCompactModeBreakpoint = 120
30 sidebarCompactModeBreakpoint = 120
23)
31)
24
32
33+const (
34+ SystemCommands CommandType = iota
35+ UserCommands
36+ MCPPrompts
37+)
38+
25// Commands represents a dialog that shows available commands.
39// Commands represents a dialog that shows available commands.
26type dockerMCPAvailabilityCheckedMsg struct {
40type dockerMCPAvailabilityCheckedMsg struct {
27 available bool
41 available bool
@@ -34,6 +48,8 @@ type Commands struct {
34 UpDown,
48 UpDown,
35 Next,
49 Next,
36 Previous,
50 Previous,
51+ Tab,
52+ ShiftTab,
37 Close key.Binding
53 Close key.Binding
38 }
54 }
39
55
@@ -41,6 +57,7 @@ type Commands struct {
41 hasSession bool
57 hasSession bool
42 hasTodos bool
58 hasTodos bool
43 hasQueue bool
59 hasQueue bool
60+ selected CommandType
44
61
45 spinner spinner.Model
62 spinner spinner.Model
46 loading bool
63 loading bool
@@ -64,6 +81,7 @@ var _ Dialog = (*Commands)(nil)
64func NewCommands(com *common.Common, sessionID string, hasSession, hasTodos, hasQueue bool, customCommands []commands.CustomCommand, mcpPrompts []commands.MCPPrompt) (*Commands, error) {
81func NewCommands(com *common.Common, sessionID string, hasSession, hasTodos, hasQueue bool, customCommands []commands.CustomCommand, mcpPrompts []commands.MCPPrompt) (*Commands, error) {
65 c := &Commands{
82 c := &Commands{
66 com: com,
83 com: com,
84+ selected: SystemCommands,
67 sessionID: sessionID,
85 sessionID: sessionID,
68 hasSession: hasSession,
86 hasSession: hasSession,
69 hasTodos: hasTodos,
87 hasTodos: hasTodos,
@@ -80,8 +98,6 @@ func NewCommands(com *common.Common, sessionID string, hasSession, hasTodos, has
80 c.list = list.NewFilterableList()
98 c.list = list.NewFilterableList()
81 c.list.Focus()
99 c.list.Focus()
82 c.list.SetSelected(0)
100 c.list.SetSelected(0)
83− c.list.SetEmptyMessage("No commands")
84− c.list.SetFilterEmptyMessage("No matches")
85
101
86 c.input = textinput.New()
102 c.input = textinput.New()
87 c.input.SetVirtualCursor(false)
103 c.input.SetVirtualCursor(false)
@@ -105,6 +121,14 @@ func NewCommands(com *common.Common, sessionID string, hasSession, hasTodos, has
105 key.WithKeys("up", "ctrl+p"),
121 key.WithKeys("up", "ctrl+p"),
106 key.WithHelp("↑", "previous item"),
122 key.WithHelp("↑", "previous item"),
107 )
123 )
124+ c.keyMap.Tab = key.NewBinding(
125+ key.WithKeys("tab"),
126+ key.WithHelp("tab", "switch selection"),
127+ )
128+ c.keyMap.ShiftTab = key.NewBinding(
129+ key.WithKeys("shift+tab"),
130+ key.WithHelp("shift+tab", "switch selection prev"),
131+ )
108 closeKey := CloseKey
132 closeKey := CloseKey
109 closeKey.SetHelp("esc", "cancel")
133 closeKey.SetHelp("esc", "cancel")
110 c.keyMap.Close = closeKey
134 c.keyMap.Close = closeKey
@@ -114,7 +138,7 @@ func NewCommands(com *common.Common, sessionID string, hasSession, hasTodos, has
114 }
138 }
115
139
116 // Set initial commands
140 // Set initial commands
117− c.setCommandItems()
141+ c.setCommandItems(c.selected)
118
142
119 s := spinner.New()
143 s := spinner.New()
120 s.Spinner = spinner.Dot
144 s.Spinner = spinner.Dot
@@ -135,7 +159,9 @@ func (c *Commands) HandleMsg(msg tea.Msg) Action {
135 case dockerMCPAvailabilityCheckedMsg:
159 case dockerMCPAvailabilityCheckedMsg:
136 c.dockerMCPAvailable = &msg.available
160 c.dockerMCPAvailable = &msg.available
137 c.dockerMCPCheckInFlight = false
161 c.dockerMCPCheckInFlight = false
162+ if c.selected == SystemCommands {
138− c.setCommandItems()
163+ c.setCommandItems(c.selected)
164+ }
139 return nil
165 return nil
140 case spinner.TickMsg:
166 case spinner.TickMsg:
141 if c.loading {
167 if c.loading {
@@ -169,6 +195,16 @@ func (c *Commands) HandleMsg(msg tea.Msg) Action {
169 return item.Action()
195 return item.Action()
170 }
196 }
171 }
197 }
198+ case key.Matches(msg, c.keyMap.Tab):
199+ if len(c.customCommands) > 0 || len(c.mcpPrompts) > 0 {
200+ c.selected = c.nextCommandType()
201+ c.setCommandItems(c.selected)
202+ }
203+ case key.Matches(msg, c.keyMap.ShiftTab):
204+ if len(c.customCommands) > 0 || len(c.mcpPrompts) > 0 {
205+ c.selected = c.previousCommandType()
206+ c.setCommandItems(c.selected)
207+ }
172 default:
208 default:
173 var cmd tea.Cmd
209 var cmd tea.Cmd
174 for _, item := range c.list.FilteredItems() {
210 for _, item := range c.list.FilteredItems() {
@@ -208,16 +244,43 @@ func (c *Commands) Cursor() *tea.Cursor {
208 return InputCursor(c.com.Styles, c.input.Cursor())
244 return InputCursor(c.com.Styles, c.input.Cursor())
209}
245}
210
246
247+// commandsRadioView generates the command type selector radio buttons.
248+func commandsRadioView(sty *styles.Styles, selected CommandType, hasUserCmds bool, hasMCPPrompts bool) string {
249+ if !hasUserCmds && !hasMCPPrompts {
250+ return ""
251+ }
252+
253+ selectedFn := func(t CommandType) string {
254+ if t == selected {
255+ return sty.Radio.On.Padding(0, 1).Render() + sty.Radio.Label.Render(t.String())
256+ }
257+ return sty.Radio.Off.Padding(0, 1).Render() + sty.Radio.Label.Render(t.String())
258+ }
259+
260+ parts := []string{
261+ selectedFn(SystemCommands),
262+ }
263+
264+ if hasUserCmds {
265+ parts = append(parts, selectedFn(UserCommands))
266+ }
267+ if hasMCPPrompts {
268+ parts = append(parts, selectedFn(MCPPrompts))
269+ }
270+
271+ return strings.Join(parts, " ")
272+}
273+
211// Draw implements [Dialog].
274// Draw implements [Dialog].
212func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
275func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
213 t := c.com.Styles
276 t := c.com.Styles
214 width := max(0, min(defaultDialogMaxWidth, area.Dx()-t.Dialog.View.GetHorizontalBorderSize()))
277 width := max(0, min(defaultDialogMaxWidth, area.Dx()-t.Dialog.View.GetHorizontalBorderSize()))
215 height := max(0, min(defaultDialogHeight, area.Dy()-t.Dialog.View.GetVerticalBorderSize()))
278 height := max(0, min(defaultDialogHeight, area.Dy()-t.Dialog.View.GetVerticalBorderSize()))
216− if area.Dx() != c.windowWidth {
279+ if area.Dx() != c.windowWidth && c.selected == SystemCommands {
217 c.windowWidth = area.Dx()
280 c.windowWidth = area.Dx()
218− // Since some items in the list depend on width (e.g. toggle sidebar
281+ // since some items in the list depend on width (e.g. toggle sidebar command),
219− // command), we need to reset the command items when width changes.
282+ // we need to reset the command items when width changes
220− c.setCommandItems()
283+ c.setCommandItems(c.selected)
221 }
284 }
222
285
223 innerWidth := width - c.com.Styles.Dialog.View.GetHorizontalFrameSize()
286 innerWidth := width - c.com.Styles.Dialog.View.GetHorizontalFrameSize()
@@ -233,6 +296,7 @@ func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
233
296
234 rc := NewRenderContext(t, width)
297 rc := NewRenderContext(t, width)
235 rc.Title = "Commands"
298 rc.Title = "Commands"
299+ rc.TitleInfo = commandsRadioView(t, c.selected, len(c.customCommands) > 0, len(c.mcpPrompts) > 0)
236 inputView := t.Dialog.InputPrompt.Render(c.input.View())
300 inputView := t.Dialog.InputPrompt.Render(c.input.View())
237 rc.AddPart(inputView)
301 rc.AddPart(inputView)
238 listView := t.Dialog.List.Height(c.list.Height()).Render(c.list.Render())
302 listView := t.Dialog.List.Height(c.list.Height()).Render(c.list.Render())
@@ -253,6 +317,7 @@ func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
253// ShortHelp implements [help.KeyMap].
317// ShortHelp implements [help.KeyMap].
254func (c *Commands) ShortHelp() []key.Binding {
318func (c *Commands) ShortHelp() []key.Binding {
255 return []key.Binding{
319 return []key.Binding{
320+ c.keyMap.Tab,
256 c.keyMap.UpDown,
321 c.keyMap.UpDown,
257 c.keyMap.Select,
322 c.keyMap.Select,
258 c.keyMap.Close,
323 c.keyMap.Close,
@@ -262,50 +327,96 @@ func (c *Commands) ShortHelp() []key.Binding {
262// FullHelp implements [help.KeyMap].
327// FullHelp implements [help.KeyMap].
263func (c *Commands) FullHelp() [][]key.Binding {
328func (c *Commands) FullHelp() [][]key.Binding {
264 return [][]key.Binding{
329 return [][]key.Binding{
265− {c.keyMap.Select, c.keyMap.Next, c.keyMap.Previous},
330+ {c.keyMap.Select, c.keyMap.Next, c.keyMap.Previous, c.keyMap.Tab},
266 {c.keyMap.Close},
331 {c.keyMap.Close},
267 }
332 }
268}
333}
269
334
270−// setCommandItems sets the command items, combining system, user, and MCP
335+// nextCommandType returns the next command type in the cycle.
271−// prompt commands into a single unified list.
336+func (c *Commands) nextCommandType() CommandType {
272−func (c *Commands) setCommandItems() {
337+ switch c.selected {
273− commandItems := []list.FilterableItem{}
338+ case SystemCommands:
274−
339+ if len(c.customCommands) > 0 {
275− // System commands.
340+ return UserCommands
276− for _, cmd := range c.defaultCommands() {
341+ }
277− commandItems = append(commandItems, cmd)
342+ if len(c.mcpPrompts) > 0 {
343+ return MCPPrompts
344+ }
345+ fallthrough
346+ case UserCommands:
347+ if len(c.mcpPrompts) > 0 {
348+ return MCPPrompts
349+ }
350+ fallthrough
351+ case MCPPrompts:
352+ return SystemCommands
353+ default:
354+ return SystemCommands
278 }
355 }
356+}
279
357
280− // User (custom) commands.
358+// previousCommandType returns the previous command type in the cycle.
359+func (c *Commands) previousCommandType() CommandType {
360+ switch c.selected {
361+ case SystemCommands:
362+ if len(c.mcpPrompts) > 0 {
363+ return MCPPrompts
364+ }
281− for _, cmd := range c.customCommands {
365+ if len(c.customCommands) > 0 {
282− var action Action
366+ return UserCommands
283− if cmd.Skill != nil {
284− action = ActionAttachSkill{ID: cmd.Skill.SkillFilePath, Name: cmd.Skill.Name}
285− } else {
286− action = ActionRunCustomCommand{
287− Content: cmd.Content,
288− Arguments: cmd.Arguments,
289− Skill: cmd.Skill,
290− }
291 }
367 }
292− item := NewCommandItem(c.com.Styles, "custom_"+cmd.ID, cmd.Name, "", action)
368+ return SystemCommands
293− if cmd.Skill != nil {
369+ case UserCommands:
294− item = item.WithDescription(cmd.Skill.Description)
370+ return SystemCommands
371+ case MCPPrompts:
372+ if len(c.customCommands) > 0 {
373+ return UserCommands
295 }
374 }
296− commandItems = append(commandItems, item)
375+ return SystemCommands
376+ default:
377+ return SystemCommands
297 }
378 }
379+}
380+
381+// setCommandItems sets the command items based on the specified command type.
382+func (c *Commands) setCommandItems(commandType CommandType) {
383+ c.selected = commandType
298
384
299− // MCP prompts.
385+ commandItems := []list.FilterableItem{}
386+ switch c.selected {
387+ case SystemCommands:
388+ for _, cmd := range c.defaultCommands() {
389+ commandItems = append(commandItems, cmd)
390+ }
391+ case UserCommands:
392+ for _, cmd := range c.customCommands {
393+ var action Action
394+ if cmd.Skill != nil {
395+ action = ActionAttachSkill{ID: cmd.Skill.SkillFilePath, Name: cmd.Skill.Name}
396+ } else {
397+ action = ActionRunCustomCommand{
398+ Content: cmd.Content,
399+ Arguments: cmd.Arguments,
400+ Skill: cmd.Skill,
401+ }
402+ }
403+ item := NewCommandItem(c.com.Styles, "custom_"+cmd.ID, cmd.Name, "", action)
404+ if cmd.Skill != nil {
405+ item = item.WithDescription(cmd.Skill.Description)
406+ }
407+ commandItems = append(commandItems, item)
408+ }
409+ case MCPPrompts:
300− for _, cmd := range c.mcpPrompts {
410+ for _, cmd := range c.mcpPrompts {
301− action := ActionRunMCPPrompt{
411+ action := ActionRunMCPPrompt{
302− Title: cmd.Title,
412+ Title: cmd.Title,
303− Description: cmd.Description,
413+ Description: cmd.Description,
304− PromptID: cmd.PromptID,
414+ PromptID: cmd.PromptID,
305− ClientID: cmd.ClientID,
415+ ClientID: cmd.ClientID,
306− Arguments: cmd.Arguments,
416+ Arguments: cmd.Arguments,
417+ }
418+ commandItems = append(commandItems, NewCommandItem(c.com.Styles, "mcp_"+cmd.ID, cmd.PromptID, "", action))
307 }
419 }
308− commandItems = append(commandItems, NewCommandItem(c.com.Styles, "mcp_"+cmd.ID, cmd.PromptID, "", action))
309 }
420 }
310
421
311 c.list.SetItems(commandItems...)
422 c.list.SetItems(commandItems...)
@@ -370,9 +481,9 @@ func (c *Commands) defaultCommands() []*CommandItem {
370
481
371 // Add external editor command if $EDITOR is available.
482 // Add external editor command if $EDITOR is available.
372 //
483 //
373− // TODO: Use [tea.EnvMsg] to get environment variables instead of os.Getenv;
484+ // TODO: Use [tea.EnvMsg] to get environment variable instead of os.Getenv;
374− // reading the environment directly bypasses the TEA message system and is
485+ // because os.Getenv does IO is breaks the TEA paradigm and is generally an
375− // generally an antipattern.
486+ // antipattern.
376 if os.Getenv("EDITOR") != "" {
487 if os.Getenv("EDITOR") != "" {
377 commands = append(commands, NewCommandItem(c.com.Styles, "open_external_editor", "Open External Editor", "ctrl+o", ActionExternalEditor{}))
488 commands = append(commands, NewCommandItem(c.com.Styles, "open_external_editor", "Open External Editor", "ctrl+o", ActionExternalEditor{}))
378 }
489 }
@@ -426,16 +537,20 @@ func (c *Commands) defaultCommands() []*CommandItem {
426 return commands
537 return commands
427}
538}
428
539
429−// SetCustomCommands sets the custom commands and refreshes the view.
540+// SetCustomCommands sets the custom commands and refreshes the view if user commands are currently displayed.
430func (c *Commands) SetCustomCommands(customCommands []commands.CustomCommand) {
541func (c *Commands) SetCustomCommands(customCommands []commands.CustomCommand) {
431 c.customCommands = customCommands
542 c.customCommands = customCommands
543+ if c.selected == UserCommands {
432− c.setCommandItems()
544+ c.setCommandItems(c.selected)
545+ }
433}
546}
434
547
435−// SetMCPPrompts sets the MCP prompts and refreshes the view.
548+// SetMCPPrompts sets the MCP prompts and refreshes the view if MCP prompts are currently displayed.
436func (c *Commands) SetMCPPrompts(mcpPrompts []commands.MCPPrompt) {
549func (c *Commands) SetMCPPrompts(mcpPrompts []commands.MCPPrompt) {
437 c.mcpPrompts = mcpPrompts
550 c.mcpPrompts = mcpPrompts
551+ if c.selected == MCPPrompts {
438− c.setCommandItems()
552+ c.setCommandItems(c.selected)
553+ }
439}
554}
440
555
441// StartLoading implements [LoadingDialog].
556// StartLoading implements [LoadingDialog].
internal/ui/dialog/commands_item.go+6−6
@@ -124,13 +124,13 @@ func (c *CommandItem) Render(width int) string {
124 descStyle = c.t.Dialog.SelectedItem
124 descStyle = c.t.Dialog.SelectedItem
125 }
125 }
126 contentWidth := max(0, width-descStyle.GetHorizontalFrameSize()+1)
126 contentWidth := max(0, width-descStyle.GetHorizontalFrameSize()+1)
127− // Indent the description to align with the marker/title text.
128− indent := strings.Repeat(" ", listItemMarkerWidth)
129− textWidth := max(0, contentWidth-listItemMarkerWidth)
130− description := ansi.Truncate(strings.TrimSpace(c.description), textWidth, "…")
127+ description := ansi.Truncate(strings.TrimSpace(c.description), contentWidth, "...")
131 descVisWidth := lipgloss.Width(description)
128 descVisWidth := lipgloss.Width(description)
132− gap := strings.Repeat(" ", max(0, textWidth-descVisWidth))
129+ gap := strings.Repeat(" ", max(0, contentWidth-descVisWidth))
130+ if description == "" {
131+ description = " "
132+ }
133− rendered = lipgloss.JoinVertical(lipgloss.Left, rendered, descStyle.Render(indent+description+gap))
133+ rendered = lipgloss.JoinVertical(lipgloss.Left, rendered, descStyle.Render(description+gap))
134 }
134 }
135 return rendered
135 return rendered
136}
136}
internal/ui/dialog/dialog.go+1−1
@@ -25,7 +25,7 @@ const (
25// CloseKey is the default key binding to close dialogs.
25// CloseKey is the default key binding to close dialogs.
26var CloseKey = key.NewBinding(
26var CloseKey = key.NewBinding(
27 key.WithKeys("esc", "alt+esc"),
27 key.WithKeys("esc", "alt+esc"),
28− key.WithHelp("esc", "cancel"),
28+ key.WithHelp("esc", "exit"),
29)
29)
30
30
31// Action represents an action taken in a dialog after handling a message.
31// Action represents an action taken in a dialog after handling a message.
internal/ui/dialog/filepicker.go+2−11
@@ -14,12 +14,10 @@ import (
14 "charm.land/bubbles/v2/key"
14 "charm.land/bubbles/v2/key"
15 tea "charm.land/bubbletea/v2"
15 tea "charm.land/bubbletea/v2"
16 "charm.land/lipgloss/v2"
16 "charm.land/lipgloss/v2"
17− "github.com/charmbracelet/crush/internal/fsext"
18 "github.com/charmbracelet/crush/internal/home"
17 "github.com/charmbracelet/crush/internal/home"
19 "github.com/charmbracelet/crush/internal/ui/common"
18 "github.com/charmbracelet/crush/internal/ui/common"
20 fimage "github.com/charmbracelet/crush/internal/ui/image"
19 fimage "github.com/charmbracelet/crush/internal/ui/image"
21 uv "github.com/charmbracelet/ultraviolet"
20 uv "github.com/charmbracelet/ultraviolet"
22− "github.com/charmbracelet/x/ansi"
23)
21)
24
22
25// FilePickerID is the identifier for the FilePicker dialog.
23// FilePickerID is the identifier for the FilePicker dialog.
@@ -71,7 +69,7 @@ func NewFilePicker(com *common.Common) (*FilePicker, tea.Cmd) {
71
69
72 f.km.Select = key.NewBinding(
70 f.km.Select = key.NewBinding(
73 key.WithKeys("enter"),
71 key.WithKeys("enter"),
74− key.WithHelp("enter", "confirm"),
72+ key.WithHelp("enter", "accept"),
75 )
73 )
76 f.km.Down = key.NewBinding(
74 f.km.Down = key.NewBinding(
77 key.WithKeys("down", "j"),
75 key.WithKeys("down", "j"),
@@ -95,7 +93,7 @@ func NewFilePicker(com *common.Common) (*FilePicker, tea.Cmd) {
95 )
93 )
96 f.km.Close = key.NewBinding(
94 f.km.Close = key.NewBinding(
97 key.WithKeys("esc", "alt+esc"),
95 key.WithKeys("esc", "alt+esc"),
98− key.WithHelp("esc", "cancel"),
96+ key.WithHelp("esc", "close/exit"),
99 )
97 )
100
98
101 fp := filepicker.New()
99 fp := filepicker.New()
@@ -243,15 +241,8 @@ func (f *FilePicker) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
243 rc := NewRenderContext(t, width)
241 rc := NewRenderContext(t, width)
244 rc.Gap = 1
242 rc.Gap = 1
245 rc.Title = "Add Image"
243 rc.Title = "Add Image"
246− // Make the image-only restriction visible instead of silently no-oping.
247− rc.TitleInfo = t.Dialog.SecondaryText.Render(" Images only: jpg, png")
248 rc.Help = f.help.View(f)
244 rc.Help = f.help.View(f)
249
245
250− // Breadcrumb: show the (truncated) current directory so users know where
251− // they are while navigating.
252− currentDir := ansi.Truncate(fsext.PrettyPath(f.fp.CurrentDirectory), innerWidth, "…")
253− rc.AddPart(t.Dialog.SecondaryText.Render(currentDir))
254−
255 imgPreview := t.Dialog.ImagePreview.Align(lipgloss.Center).Width(innerWidth).Render(f.imagePreview(imgPrevWidth, imgPrevHeight))
246 imgPreview := t.Dialog.ImagePreview.Align(lipgloss.Center).Width(innerWidth).Render(f.imagePreview(imgPrevWidth, imgPrevHeight))
256 rc.AddPart(imgPreview)
247 rc.AddPart(imgPreview)
257
248
internal/ui/dialog/models.go−2
@@ -110,8 +110,6 @@ func NewModels(com *common.Common, isOnboarding bool) (*Models, error) {
110 m.list = NewModelsList(t)
110 m.list = NewModelsList(t)
111 m.list.Focus()
111 m.list.Focus()
112 m.list.SetSelected(0)
112 m.list.SetSelected(0)
113− m.list.SetEmptyMessage("No models")
114− m.list.SetFilterEmptyMessage("No matches")
115
113
116 m.input = textinput.New()
114 m.input = textinput.New()
117 m.input.SetVirtualCursor(false)
115 m.input.SetVirtualCursor(false)
internal/ui/dialog/models_list.go+56−52
@@ -1,6 +1,9 @@
1package dialog
1package dialog
2
2
3import (
3import (
4+ "fmt"
5+ "slices"
6+ "sort"
4 "strings"
7 "strings"
5
8
6 "github.com/charmbracelet/crush/internal/ui/list"
9 "github.com/charmbracelet/crush/internal/ui/list"
@@ -14,11 +17,6 @@ type ModelsList struct {
14 groups []ModelGroup
17 groups []ModelGroup
15 query string
18 query string
16 t *styles.Styles
19 t *styles.Styles
17−
18− // emptyMessage is shown when there are no items and no active filter.
19− emptyMessage string
20− // filterEmptyMessage is shown when a filter is active but matches nothing.
21− filterEmptyMessage string
22}
20}
23
21
24// NewModelsList creates a new list suitable for model items and groups.
22// NewModelsList creates a new list suitable for model items and groups.
@@ -192,56 +190,58 @@ func (f *ModelsList) VisibleItems() []list.Item {
192 return items
190 return items
193 }
191 }
194
192
195− // Build the match text once across all items (O(N) rather than O(N*G)).
193+ filterableItems := make([]list.FilterableItem, 0, f.Len())
196− // Each entry is prefixed with its group's lowercased title, exactly as the
197− // previous per-group code matched against.
198− names := make([]string, 0, f.Len())
199 for _, g := range f.groups {
194 for _, g := range f.groups {
200− prefix := strings.ToLower(g.Title) + " "
201 for _, item := range g.Items {
195 for _, item := range g.Items {
202− names = append(names, prefix+item.Filter())
196+ filterableItems = append(filterableItems, item)
203 }
197 }
204 }
198 }
205
199
206− // Single fuzzy pass, then bucket matches back to their group by index.
207− matches := fuzzy.Find(query, names)
208− matched := make([]*fuzzy.Match, len(names))
209− for i := range matches {
210− matched[matches[i].Index] = &matches[i]
211− }
212−
213 items := []list.Item{}
200 items := []list.Item{}
201+ visitedGroups := map[int]bool{}
214
202
215− // Reconstruct groups with matched items, preserving original group and
203+ // Reconstruct groups with matched items
216− // within-group item order.
204+ // Find which group this item belongs to
217− idx := 0
218− for _, g := range f.groups {
205+ for gi, g := range f.groups {
219− prefixLen := len(strings.ToLower(g.Title) + " ")
220 addedCount := 0
206 addedCount := 0
221− for _, item := range g.Items {
207+ name := strings.ToLower(g.Title) + " "
222− if m := matched[idx]; m != nil {
208+
209+ names := make([]string, len(filterableItems))
210+ for i, item := range filterableItems {
211+ ms := item.(*ModelItem)
212+ names[i] = fmt.Sprintf("%s%s", name, ms.Filter())
213+ }
214+
215+ matches := fuzzy.Find(query, names)
216+
217+ // Sort by original index to preserve order within the group
218+ sort.SliceStable(matches, func(i, j int) bool {
219+ return matches[i].Index < matches[j].Index
220+ })
221+
222+ for _, match := range matches {
223+ item := filterableItems[match.Index].(*ModelItem)
223− idxs := []int{}
224+ idxs := []int{}
224− for _, mIdx := range m.MatchedIndexes {
225+ for _, idx := range match.MatchedIndexes {
225− // Adjusts removing provider name highlights
226+ // Adjusts removing provider name highlights
226− if mIdx < prefixLen {
227+ if idx < len(name) {
227− continue
228+ continue
228− }
229− idxs = append(idxs, mIdx-prefixLen)
230 }
229 }
230+ idxs = append(idxs, idx-len(name))
231+ }
231
232
232− match := *m
233− match.MatchedIndexes = idxs
233+ match.MatchedIndexes = idxs
234− if addedCount == 0 {
234+ if slices.Contains(g.Items, item) {
235+ if !visitedGroups[gi] {
235 // Add section header
236 // Add section header
236− g := g
237 items = append(items, &g)
237 items = append(items, &g)
238+ visitedGroups[gi] = true
238 }
239 }
239 // Add the matched item
240 // Add the matched item
240 item.SetMatch(match)
241 item.SetMatch(match)
241 items = append(items, item)
242 items = append(items, item)
242 addedCount++
243 addedCount++
243 }
244 }
244− idx++
245 }
245 }
246 if addedCount > 0 {
246 if addedCount > 0 {
247 // Add a space separator after each provider section
247 // Add a space separator after each provider section
@@ -252,24 +252,28 @@ func (f *ModelsList) VisibleItems() []list.Item {
252 return items
252 return items
253}
253}
254
254
255−// SetEmptyMessage sets the placeholder shown when the list has no items and
255+// Render renders the filterable list.
256−// no filter is active.
257−func (f *ModelsList) SetEmptyMessage(msg string) {
256+func (f *ModelsList) Render() string {
258− f.emptyMessage = msg
257+ return f.List.Render()
259}
258}
260
259
261−// SetFilterEmptyMessage sets the placeholder shown when a filter is active but
260+type modelGroups []ModelGroup
262−// yields no matches.
261+
263−func (f *ModelsList) SetFilterEmptyMessage(msg string) {
262+func (m modelGroups) Len() int {
264− f.filterEmptyMessage = msg
263+ n := 0
264+ for _, g := range m {
265+ n += len(g.Items)
266+ }
267+ return n
265}
268}
266
269
267−// Render renders the filterable list.
270+func (m modelGroups) String(i int) string {
268−func (f *ModelsList) Render() string {
271+ count := 0
269− if f.query != "" && f.filterEmptyMessage != "" {
272+ for _, g := range m {
270− f.List.SetEmptyMessage(f.filterEmptyMessage)
273+ if i < count+len(g.Items) {
271− } else {
274+ return g.Items[i-count].Filter()
272− f.List.SetEmptyMessage(f.emptyMessage)
275+ }
276+ count += len(g.Items)
273 }
277 }
274− return f.List.Render()
278+ return ""
275}
279}
internal/ui/dialog/oauth.go+2
@@ -1,6 +1,7 @@
1package dialog
1package dialog
2
2
3import (
3import (
4+ "context"
4 "fmt"
5 "fmt"
5 "strings"
6 "strings"
6
7
@@ -65,6 +66,7 @@ type OAuth struct {
65 expiresIn int
66 expiresIn int
66 interval int
67 interval int
67 token *oauth.Token
68 token *oauth.Token
69+ cancelFunc context.CancelFunc
68}
70}
69
71
70var _ Dialog = (*OAuth)(nil)
72var _ Dialog = (*OAuth)(nil)
internal/ui/dialog/permissions.go+21−84
@@ -15,7 +15,6 @@ import (
15 "github.com/charmbracelet/crush/internal/permission"
15 "github.com/charmbracelet/crush/internal/permission"
16 "github.com/charmbracelet/crush/internal/stringext"
16 "github.com/charmbracelet/crush/internal/stringext"
17 "github.com/charmbracelet/crush/internal/ui/common"
17 "github.com/charmbracelet/crush/internal/ui/common"
18− "github.com/charmbracelet/crush/internal/ui/diffview"
19 "github.com/charmbracelet/crush/internal/ui/styles"
18 "github.com/charmbracelet/crush/internal/ui/styles"
20 uv "github.com/charmbracelet/ultraviolet"
19 uv "github.com/charmbracelet/ultraviolet"
21)
20)
@@ -56,8 +55,10 @@ const (
56
55
57// Permissions represents a dialog for permission requests.
56// Permissions represents a dialog for permission requests.
58type Permissions struct {
57type Permissions struct {
59− com *common.Common
58+ com *common.Common
59+ windowWidth int // Terminal window dimensions.
60+ windowHeight int
60− fullscreen bool // true when dialog is fullscreen
61+ fullscreen bool // true when dialog is fullscreen
61
62
62 permission permission.PermissionRequest
63 permission permission.PermissionRequest
63 selectedOption int // 0: Allow, 1: Allow for session, 2: Deny
64 selectedOption int // 0: Allow, 1: Allow for session, 2: Deny
@@ -72,13 +73,6 @@ type Permissions struct {
72 diffXOffset int // horizontal scroll offset for diff view
73 diffXOffset int // horizontal scroll offset for diff view
73 unifiedDiffContent string
74 unifiedDiffContent string
74 splitDiffContent string
75 splitDiffContent string
75− // diffView is a persistent diff renderer, reused across scrolls/width
76− // changes so its syntax and diff caches are preserved.
77− diffView *diffview.DiffView
78−
79− // Cached non-diff content, invalidated by viewportDirty or a width change.
80− cachedContent string
81− cachedContentWidth int
82
76
83 help help.Model
77 help help.Model
84 keyMap permissionsKeyMap
78 keyMap permissionsKeyMap
@@ -133,7 +127,7 @@ func defaultPermissionsKeyMap() permissionsKeyMap {
133 key.WithKeys("d", "D"),
127 key.WithKeys("d", "D"),
134 key.WithHelp("d", "deny"),
128 key.WithHelp("d", "deny"),
135 ),
129 ),
136− Close: denyCloseKey(),
130+ Close: CloseKey,
137 ToggleDiffMode: key.NewBinding(
131 ToggleDiffMode: key.NewBinding(
138 key.WithKeys("t"),
132 key.WithKeys("t"),
139 key.WithHelp("t", "toggle diff view"),
133 key.WithHelp("t", "toggle diff view"),
@@ -169,15 +163,6 @@ func defaultPermissionsKeyMap() permissionsKeyMap {
169 }
163 }
170}
164}
171
165
172−// denyCloseKey returns the close binding for the permissions dialog. It
173−// mirrors [CloseKey] but its help label reads "deny" because pressing esc
174−// rejects the permission request rather than merely dismissing the dialog.
175−func denyCloseKey() key.Binding {
176− k := CloseKey
177− k.SetHelp("esc", "deny")
178− return k
179−}
180−
181var _ Dialog = (*Permissions)(nil)
166var _ Dialog = (*Permissions)(nil)
182
167
183// PermissionsOption configures the permissions dialog.
168// PermissionsOption configures the permissions dialog.
@@ -212,11 +197,9 @@ func NewPermissions(com *common.Common, perm permission.PermissionRequest, opts
212 }
197 }
213
198
214 p := &Permissions{
199 p := &Permissions{
215− com: com,
200+ com: com,
216− permission: perm,
201+ permission: perm,
217− // Default to Deny (index 2) so that confirming with enter does not
218− // grant the most permissive choice. See renderButtons for ordering.
219− selectedOption: 2,
202+ selectedOption: 0,
220 viewport: vp,
203 viewport: vp,
221 help: h,
204 help: h,
222 keyMap: km,
205 keyMap: km,
@@ -455,11 +438,7 @@ func (p *Permissions) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
455 if content != "" {
438 if content != "" {
456 parts = append(parts, "", content)
439 parts = append(parts, "", content)
457 }
440 }
458− parts = append(parts, "", buttons)
441+ parts = append(parts, "", buttons, "", helpView)
459− if hint := p.renderButtonHint(contentWidth); hint != "" {
460− parts = append(parts, hint)
461− }
462− parts = append(parts, "", helpView)
463
442
464 innerContent := lipgloss.JoinVertical(lipgloss.Left, parts...)
443 innerContent := lipgloss.JoinVertical(lipgloss.Left, parts...)
465 DrawCenterCursor(scr, area, dialogStyle.Render(innerContent), nil)
444 DrawCenterCursor(scr, area, dialogStyle.Render(innerContent), nil)
@@ -548,44 +527,28 @@ func prettyName(name string) string {
548}
527}
549
528
550func (p *Permissions) renderContent(width int) string {
529func (p *Permissions) renderContent(width int) string {
551− // Diff views manage their own caching (see renderDiff).
552 switch p.permission.ToolName {
530 switch p.permission.ToolName {
531+ case tools.BashToolName:
532+ return p.renderBashContent(width)
553 case tools.EditToolName:
533 case tools.EditToolName:
554 return p.renderEditContent(width)
534 return p.renderEditContent(width)
555 case tools.WriteToolName:
535 case tools.WriteToolName:
556 return p.renderWriteContent(width)
536 return p.renderWriteContent(width)
557 case tools.MultiEditToolName:
537 case tools.MultiEditToolName:
558 return p.renderMultiEditContent(width)
538 return p.renderMultiEditContent(width)
559− }
560−
561− // Non-diff content is cached the same way the diff is: recompute only when
562− // the viewport is dirty or the width changed. This avoids re-running JSON
563− // (un)marshalling, syntax highlighting and wrapping on every frame.
564− if !p.viewportDirty && width == p.cachedContentWidth {
565− return p.cachedContent
566− }
567−
568− var content string
569− switch p.permission.ToolName {
570− case tools.BashToolName:
571− content = p.renderBashContent(width)
572 case tools.DownloadToolName:
539 case tools.DownloadToolName:
573− content = p.renderDownloadContent(width)
540+ return p.renderDownloadContent(width)
574 case tools.FetchToolName:
541 case tools.FetchToolName:
575− content = p.renderFetchContent(width)
542+ return p.renderFetchContent(width)
576 case tools.AgenticFetchToolName:
543 case tools.AgenticFetchToolName:
577− content = p.renderAgenticFetchContent(width)
544+ return p.renderAgenticFetchContent(width)
578 case tools.ViewToolName:
545 case tools.ViewToolName:
579− content = p.renderViewContent(width)
546+ return p.renderViewContent(width)
580 case tools.LSToolName:
547 case tools.LSToolName:
581− content = p.renderLSContent(width)
548+ return p.renderLSContent(width)
582 default:
549 default:
583− content = p.renderDefaultContent(width)
550+ return p.renderDefaultContent(width)
584 }
551 }
585−
586− p.cachedContent = content
587− p.cachedContentWidth = width
588− return content
589}
552}
590
553
591func (p *Permissions) renderBashContent(width int) string {
554func (p *Permissions) renderBashContent(width int) string {
@@ -630,17 +593,9 @@ func (p *Permissions) renderDiff(filePath, oldContent, newContent string, conten
630 }
593 }
631
594
632 isSplitMode := p.isSplitMode()
595 isSplitMode := p.isSplitMode()
633−
634− // Build the diff view once. Its content never changes for the lifetime of
635− // the dialog, so reusing it across scrolls and width changes preserves the
636− // syntax/diff caches; only XOffset/Width/layout are updated per render.
637− if p.diffView == nil {
638− p.diffView = common.DiffFormatter(p.com.Styles).
596+ formatter := common.DiffFormatter(p.com.Styles).
639− Before(fsext.PrettyPath(filePath), oldContent).
597+ Before(fsext.PrettyPath(filePath), oldContent).
640− After(fsext.PrettyPath(filePath), newContent)
598+ After(fsext.PrettyPath(filePath), newContent).
641− }
642−
643− formatter := p.diffView.
644 XOffset(p.diffXOffset).
599 XOffset(p.diffXOffset).
645 Width(contentWidth)
600 Width(contentWidth)
646
601
@@ -707,7 +662,7 @@ func (p *Permissions) renderViewContent(width int) string {
707 if params.Offset > 0 {
662 if params.Offset > 0 {
708 content += fmt.Sprintf("\nStarting from line: %d", params.Offset+1)
663 content += fmt.Sprintf("\nStarting from line: %d", params.Offset+1)
709 }
664 }
710− if params.Limit > 0 && params.Limit != tools.DefaultReadLimit {
665+ if params.Limit > 0 && params.Limit != 2000 {
711 content += fmt.Sprintf("\nLines to read: %d", params.Limit)
666 content += fmt.Sprintf("\nLines to read: %d", params.Limit)
712 }
667 }
713
668
@@ -803,24 +758,6 @@ func (p *Permissions) renderButtons(contentWidth int) string {
803 Render(content)
758 Render(content)
804}
759}
805
760
806−// renderButtonHint renders a dim one-line description of the currently
807−// highlighted button so the consequence of the selection is explicit.
808−func (p *Permissions) renderButtonHint(contentWidth int) string {
809− var msg string
810− switch p.selectedOption {
811− case 0:
812− msg = "Allow this action once"
813− case 1:
814− msg = "Auto-allow this tool for the rest of the session"
815− default:
816− msg = "Reject and tell the agent"
817− }
818− return p.com.Styles.Dialog.SecondaryText.
819− Width(contentWidth).
820− Align(lipgloss.Right).
821− Render(msg)
822−}
823−
824func (p *Permissions) canScroll() bool {
761func (p *Permissions) canScroll() bool {
825 if p.hasDiffView() {
762 if p.hasDiffView() {
826 // Diff views can always scroll.
763 // Diff views can always scroll.
internal/ui/dialog/permissions_test.go+6−7
@@ -54,23 +54,22 @@ func TestPermissions_NavigationCyclesOptions(t *testing.T) {
54 t.Parallel()
54 t.Parallel()
55
55
56 p := newTestPermissions(t)
56 p := newTestPermissions(t)
57− // The dialog defaults to Deny (index 2) so enter does not grant the most
58− // permissive choice.
59− require.Equal(t, 2, p.selectedOption)
60−
61− // Tab cycles forward (wraps to 0).
62− p.HandleMsg(tea.KeyPressMsg{Code: tea.KeyTab})
63 require.Equal(t, 0, p.selectedOption)
57 require.Equal(t, 0, p.selectedOption)
64
58
59+ // Tab cycles forward.
65 p.HandleMsg(tea.KeyPressMsg{Code: tea.KeyTab})
60 p.HandleMsg(tea.KeyPressMsg{Code: tea.KeyTab})
66 require.Equal(t, 1, p.selectedOption)
61 require.Equal(t, 1, p.selectedOption)
67
62
68 p.HandleMsg(tea.KeyPressMsg{Code: tea.KeyTab})
63 p.HandleMsg(tea.KeyPressMsg{Code: tea.KeyTab})
69 require.Equal(t, 2, p.selectedOption)
64 require.Equal(t, 2, p.selectedOption)
70
65
66+ // Wrap around.
67+ p.HandleMsg(tea.KeyPressMsg{Code: tea.KeyTab})
68+ require.Equal(t, 0, p.selectedOption)
69+
71 // Left cycles backward.
70 // Left cycles backward.
72 p.HandleMsg(keyMsg('h'))
71 p.HandleMsg(keyMsg('h'))
73− require.Equal(t, 1, p.selectedOption)
72+ require.Equal(t, 2, p.selectedOption)
74}
73}
75
74
76// TestPermissions_EnterConfirmsSelection verifies that enter confirms the
75// TestPermissions_EnterConfirmsSelection verifies that enter confirms the
internal/ui/dialog/sessions.go+11−16
@@ -61,7 +61,7 @@ func NewSessions(com *common.Common, selectedSessionID string) (*Session, error)
61 s := new(Session)
61 s := new(Session)
62 s.sessionsMode = sessionsModeNormal
62 s.sessionsMode = sessionsModeNormal
63 s.com = com
63 s.com = com
64− sessions, err := com.Workspace.ListSessions(context.Background())
64+ sessions, err := com.Workspace.ListSessions(context.TODO())
65 if err != nil {
65 if err != nil {
66 return nil, err
66 return nil, err
67 }
67 }
@@ -81,8 +81,6 @@ func NewSessions(com *common.Common, selectedSessionID string) (*Session, error)
81 s.list = list.NewFilterableList(sessionItems(com.Styles, sessionsModeNormal, sessions...)...)
81 s.list = list.NewFilterableList(sessionItems(com.Styles, sessionsModeNormal, sessions...)...)
82 s.list.Focus()
82 s.list.Focus()
83 s.list.SetSelected(s.selectedSessionInx)
83 s.list.SetSelected(s.selectedSessionInx)
84− s.list.SetEmptyMessage("No sessions yet")
85− s.list.SetFilterEmptyMessage("No matches")
86
84
87 s.input = textinput.New()
85 s.input = textinput.New()
88 s.input.SetVirtualCursor(false)
86 s.input.SetVirtualCursor(false)
@@ -91,8 +89,8 @@ func NewSessions(com *common.Common, selectedSessionID string) (*Session, error)
91 s.input.Focus()
89 s.input.Focus()
92
90
93 s.keyMap.Select = key.NewBinding(
91 s.keyMap.Select = key.NewBinding(
94− key.WithKeys("enter", "ctrl+y"),
92+ key.WithKeys("enter", "tab", "ctrl+y"),
95− key.WithHelp("enter", "confirm"),
93+ key.WithHelp("enter", "choose"),
96 )
94 )
97 s.keyMap.Next = key.NewBinding(
95 s.keyMap.Next = key.NewBinding(
98 key.WithKeys("down", "ctrl+n"),
96 key.WithKeys("down", "ctrl+n"),
@@ -123,7 +121,7 @@ func NewSessions(com *common.Common, selectedSessionID string) (*Session, error)
123 key.WithHelp("esc", "cancel"),
121 key.WithHelp("esc", "cancel"),
124 )
122 )
125 s.keyMap.ConfirmDelete = key.NewBinding(
123 s.keyMap.ConfirmDelete = key.NewBinding(
126− key.WithKeys("y", "enter"),
124+ key.WithKeys("y"),
127 key.WithHelp("y", "delete"),
125 key.WithHelp("y", "delete"),
128 )
126 )
129 s.keyMap.CancelDelete = key.NewBinding(
127 s.keyMap.CancelDelete = key.NewBinding(
@@ -206,9 +204,8 @@ func (s *Session) HandleMsg(msg tea.Msg) Action {
206 s.list.ScrollToSelected()
204 s.list.ScrollToSelected()
207 case key.Matches(msg, s.keyMap.Select):
205 case key.Matches(msg, s.keyMap.Select):
208 if item := s.list.SelectedItem(); item != nil {
206 if item := s.list.SelectedItem(); item != nil {
209− if sessionItem, ok := item.(*SessionItem); ok {
207+ sessionItem := item.(*SessionItem)
210− return ActionSelectSession{sessionItem.Session}
208+ return ActionSelectSession{sessionItem.Session}
211− }
212 }
209 }
213 default:
210 default:
214 var cmd tea.Cmd
211 var cmd tea.Cmd
@@ -330,9 +327,7 @@ func (s *Session) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
330
327
331func (s *Session) selectedSessionItem() *SessionItem {
328func (s *Session) selectedSessionItem() *SessionItem {
332 if item := s.list.SelectedItem(); item != nil {
329 if item := s.list.SelectedItem(); item != nil {
333− if sessionItem, ok := item.(*SessionItem); ok {
330+ return item.(*SessionItem)
334− return sessionItem
335− }
336 }
331 }
337 return nil
332 return nil
338}
333}
@@ -361,7 +356,7 @@ func (s *Session) removeSession(id string) {
361
356
362func (s *Session) deleteSessionCmd(id string) tea.Cmd {
357func (s *Session) deleteSessionCmd(id string) tea.Cmd {
363 return func() tea.Msg {
358 return func() tea.Msg {
364− err := s.com.Workspace.DeleteSession(context.Background(), id)
359+ err := s.com.Workspace.DeleteSession(context.TODO(), id)
365 if err != nil {
360 if err != nil {
366 return util.NewErrorMsg(err)
361 return util.NewErrorMsg(err)
367 }
362 }
@@ -387,9 +382,9 @@ func (s *Session) confirmRenameSession() Action {
387}
382}
388
383
389func (s *Session) updateSession(session session.Session) {
384func (s *Session) updateSession(session session.Session) {
390− for i, sess := range s.sessions {
385+ for existingID, sess := range s.sessions {
391 if sess.ID == session.ID {
386 if sess.ID == session.ID {
392− s.sessions[i] = session
387+ s.sessions[existingID] = session
393 break
388 break
394 }
389 }
395 }
390 }
@@ -397,7 +392,7 @@ func (s *Session) updateSession(session session.Session) {
397
392
398func (s *Session) updateSessionCmd(session session.Session) tea.Cmd {
393func (s *Session) updateSessionCmd(session session.Session) tea.Cmd {
399 return func() tea.Msg {
394 return func() tea.Msg {
400− _, err := s.com.Workspace.SaveSession(context.Background(), session)
395+ _, err := s.com.Workspace.SaveSession(context.TODO(), session)
401 if err != nil {
396 if err != nil {
402 return util.NewErrorMsg(err)
397 return util.NewErrorMsg(err)
403 }
398 }
internal/ui/dialog/sessions_item.go+2−12
@@ -140,14 +140,6 @@ type ListItemStyles struct {
140 InfoTextFocused lipgloss.Style
140 InfoTextFocused lipgloss.Style
141}
141}
142
142
143−// listItemMarker is a leading glyph prepended to the focused list row so
144−// selection is conveyed by shape (not background color alone). Non-focused
145−// rows are padded with equivalent leading spaces to keep text aligned.
146−const (
147− listItemMarker = "▍ "
148− listItemMarkerWidth = 2
149−)
150−
151func renderItem(t ListItemStyles, title string, info string, focused bool, width int, cache map[int]string, m *fuzzy.Match) string {
143func renderItem(t ListItemStyles, title string, info string, focused bool, width int, cache map[int]string, m *fuzzy.Match) string {
152 if cache == nil {
144 if cache == nil {
153 cache = make(map[int]string)
145 cache = make(map[int]string)
@@ -159,15 +151,13 @@ func renderItem(t ListItemStyles, title string, info string, focused bool, width
159 }
151 }
160
152
161 style := t.ItemBlurred
153 style := t.ItemBlurred
162− marker := strings.Repeat(" ", listItemMarkerWidth)
163 if focused {
154 if focused {
164 style = t.ItemFocused
155 style = t.ItemFocused
165− marker = listItemMarker
166 }
156 }
167
157
168 var infoText string
158 var infoText string
169 var infoWidth int
159 var infoWidth int
170− lineWidth := max(0, width-listItemMarkerWidth)
160+ lineWidth := width
171 if len(info) > 0 {
161 if len(info) > 0 {
172 infoText = fmt.Sprintf(" %s ", info)
162 infoText = fmt.Sprintf(" %s ", info)
173 if focused {
163 if focused {
@@ -212,7 +202,7 @@ func renderItem(t ListItemStyles, title string, info string, focused bool, width
212 content = strings.Join(parts, "")
202 content = strings.Join(parts, "")
213 }
203 }
214
204
215− content = style.Render(marker + content + gap + infoText)
205+ content = style.Render(content + gap + infoText)
216 cache[width] = content
206 cache[width] = content
217 return content
207 return content
218}
208}
internal/ui/difftastic/difftastic.godeleted−60
@@ -1,60 +0,0 @@
1−// Package difftastic provides a syntax-aware structural diff renderer by
2−// shelling out to the difftastic binary (https://github.com/Wilfred/difftastic).
3−package difftastic
4−
5−import (
6− "context"
7− "fmt"
8− "os"
9− "os/exec"
10− "path/filepath"
11− "strconv"
12− "strings"
13−)
14−
15−// RenderDiff runs difftastic on before/after content and returns ANSI-colored
16−// output. Returns empty string and a nil error if difft is not available so
17−// callers can fall back to the builtin renderer.
18−func RenderDiff(filePath, before, after string, width int) (string, error) {
19− if _, err := exec.LookPath("difft"); err != nil {
20− return "", nil
21− }
22−
23− dir, err := os.MkdirTemp("", "crush-difftastic-*")
24− if err != nil {
25− return "", fmt.Errorf("failed to create temp directory: %w", err)
26− }
27− defer os.RemoveAll(dir)
28−
29− ext := filepath.Ext(filePath)
30− oldPath := filepath.Join(dir, "old"+ext)
31− newPath := filepath.Join(dir, "new"+ext)
32−
33− if err := os.WriteFile(oldPath, []byte(before), 0o644); err != nil {
34− return "", fmt.Errorf("failed to write before content: %w", err)
35− }
36− if err := os.WriteFile(newPath, []byte(after), 0o644); err != nil {
37− return "", fmt.Errorf("failed to write after content: %w", err)
38− }
39−
40− args := []string{
41− "--color=always",
42− "--display=inline",
43− "--width=" + strconv.Itoa(width),
44− oldPath,
45− newPath,
46− }
47−
48− cmd := exec.CommandContext(context.Background(), "difft", args...)
49− output, err := cmd.CombinedOutput()
50− if err != nil {
51− // difftastic returns non-zero exit code when differences are found.
52− // The output is still valid, so return it rather than the error.
53− if len(output) > 0 {
54− return string(output), nil
55− }
56− return "", fmt.Errorf("failed to run difftastic: %w", err)
57− }
58−
59− return strings.TrimRight(string(output), "\n"), nil
60−}
internal/ui/diffview/diffview.go+2−2
@@ -791,8 +791,8 @@ func (dv *DiffView) createSyntaxCacheKey(source string, bgColor color.Color) str
791
791
792 // Create a hash of the content + color to use as cache key
792 // Create a hash of the content + color to use as cache key
793 h := xxh3.New()
793 h := xxh3.New()
794− _, _ = h.Write([]byte(source))
794+ h.Write([]byte(source))
795− _, _ = h.Write([]byte(colorStr))
795+ h.Write([]byte(colorStr))
796 return fmt.Sprintf("%x", h.Sum(nil))
796 return fmt.Sprintf("%x", h.Sum(nil))
797}
797}
798
798
internal/ui/diffview/diffview_test.go+8
@@ -329,3 +329,11 @@ func assertLineWidth(t *testing.T, expected int, output string) {
329 t.Errorf("expected output width to be == %d, got %d", expected, lineWidth)
329 t.Errorf("expected output width to be == %d, got %d", expected, lineWidth)
330 }
330 }
331}
331}
332+
333+func assertHeight(t *testing.T, expected int, output string) {
334+ output = strings.TrimSuffix(output, "\n")
335+ lines := strings.Count(output, "\n") + 1
336+ if lines != expected {
337+ t.Errorf("expected output height to be == %d, got %d", expected, lines)
338+ }
339+}
internal/ui/diffview/style.go+44−46
@@ -1,52 +1,10 @@
1package diffview
1package diffview
2
2
3import (
3import (
4− "image/color"
5−
6 "charm.land/lipgloss/v2"
4 "charm.land/lipgloss/v2"
7 "github.com/charmbracelet/x/exp/charmtone"
5 "github.com/charmbracelet/x/exp/charmtone"
8)
6)
9
7
10−// Diff insert/delete backgrounds are derived by washing a semantic accent
11−// (green for inserts, red for deletes) over the base background. Keeping the
12−// derivation here gives both DefaultDarkStyle/DefaultLightStyle and the styles
13−// package's palette-driven theme a single source of truth, rather than several
14−// copies of the same hardcoded hex.
15−const (
16− diffCodeTint = 0.10 // symbol/code background wash strength.
17− diffLineNumTint = 0.07 // line-number background wash (slightly darker).
18−)
19−
20−// BlendBg washes accent over base by ratio in [0,1], returning a subtle tint.
21−// It is the shared basis for diff insert/delete backgrounds.
22−func BlendBg(base, accent color.Color, ratio float64) color.Color {
23− br, bg, bb, _ := base.RGBA()
24− ar, ag, ab, _ := accent.RGBA()
25− lerp := func(a, b uint32) uint8 {
26− return uint8(float64(a>>8)*(1-ratio) + float64(b>>8)*ratio + 0.5)
27− }
28− return color.RGBA{R: lerp(br, ar), G: lerp(bg, ag), B: lerp(bb, ab), A: 0xff}
29−}
30−
31−// ChangeLineStyle builds an insert- or delete-line style from semantic colors:
32−// accent drives the symbol/line-number foreground and the background wash over
33−// base, while codeFg colors the code text (pass nil to leave it unset).
34−func ChangeLineStyle(accent, base, codeFg color.Color) LineStyle {
35− code := lipgloss.NewStyle().Background(BlendBg(base, accent, diffCodeTint))
36− if codeFg != nil {
37− code = code.Foreground(codeFg)
38− }
39− return LineStyle{
40− LineNumber: lipgloss.NewStyle().
41− Foreground(accent).
42− Background(BlendBg(base, accent, diffLineNumTint)),
43− Symbol: lipgloss.NewStyle().
44− Foreground(accent).
45− Background(BlendBg(base, accent, diffCodeTint)),
46− Code: code,
47− }
48−}
49−
50// LineStyle defines the styles for a given line type in the diff view.
8// LineStyle defines the styles for a given line type in the diff view.
51type LineStyle struct {
9type LineStyle struct {
52 LineNumber lipgloss.Style
10 LineNumber lipgloss.Style
@@ -91,8 +49,28 @@ func DefaultLightStyle() Style {
91 Foreground(charmtone.Pepper).
49 Foreground(charmtone.Pepper).
92 Background(charmtone.Salt),
50 Background(charmtone.Salt),
93 },
51 },
94− InsertLine: ChangeLineStyle(charmtone.Turtle, charmtone.Salt, charmtone.Pepper),
52+ InsertLine: LineStyle{
95− DeleteLine: ChangeLineStyle(charmtone.Cherry, charmtone.Salt, charmtone.Pepper),
53+ LineNumber: lipgloss.NewStyle().
54+ Foreground(charmtone.Turtle).
55+ Background(lipgloss.Color("#c8e6c9")),
56+ Symbol: lipgloss.NewStyle().
57+ Foreground(charmtone.Turtle).
58+ Background(lipgloss.Color("#e8f5e9")),
59+ Code: lipgloss.NewStyle().
60+ Foreground(charmtone.Pepper).
61+ Background(lipgloss.Color("#e8f5e9")),
62+ },
63+ DeleteLine: LineStyle{
64+ LineNumber: lipgloss.NewStyle().
65+ Foreground(charmtone.Cherry).
66+ Background(lipgloss.Color("#ffcdd2")),
67+ Symbol: lipgloss.NewStyle().
68+ Foreground(charmtone.Cherry).
69+ Background(lipgloss.Color("#ffebee")),
70+ Code: lipgloss.NewStyle().
71+ Foreground(charmtone.Pepper).
72+ Background(lipgloss.Color("#ffebee")),
73+ },
96 Filename: LineStyle{
74 Filename: LineStyle{
97 LineNumber: lipgloss.NewStyle().
75 LineNumber: lipgloss.NewStyle().
98 Foreground(charmtone.Iron).
76 Foreground(charmtone.Iron).
@@ -129,8 +107,28 @@ func DefaultDarkStyle() Style {
129 Foreground(charmtone.Salt).
107 Foreground(charmtone.Salt).
130 Background(charmtone.Pepper),
108 Background(charmtone.Pepper),
131 },
109 },
132− InsertLine: ChangeLineStyle(charmtone.Turtle, charmtone.Pepper, charmtone.Salt),
110+ InsertLine: LineStyle{
133− DeleteLine: ChangeLineStyle(charmtone.Cherry, charmtone.Pepper, charmtone.Salt),
111+ LineNumber: lipgloss.NewStyle().
112+ Foreground(charmtone.Turtle).
113+ Background(lipgloss.Color("#293229")),
114+ Symbol: lipgloss.NewStyle().
115+ Foreground(charmtone.Turtle).
116+ Background(lipgloss.Color("#303a30")),
117+ Code: lipgloss.NewStyle().
118+ Foreground(charmtone.Salt).
119+ Background(lipgloss.Color("#303a30")),
120+ },
121+ DeleteLine: LineStyle{
122+ LineNumber: lipgloss.NewStyle().
123+ Foreground(charmtone.Cherry).
124+ Background(lipgloss.Color("#332929")),
125+ Symbol: lipgloss.NewStyle().
126+ Foreground(charmtone.Cherry).
127+ Background(lipgloss.Color("#3a3030")),
128+ Code: lipgloss.NewStyle().
129+ Foreground(charmtone.Salt).
130+ Background(lipgloss.Color("#3a3030")),
131+ },
134 Filename: LineStyle{
132 Filename: LineStyle{
135 LineNumber: lipgloss.NewStyle().
133 LineNumber: lipgloss.NewStyle().
136 Foreground(charmtone.Smoke).
134 Foreground(charmtone.Smoke).
internal/ui/diffview/testdata/TestDiffView/Split/CustomContextLines/DarkMode.golden+3−3
@@ -3,7 +3,7 @@
3  2     2   
3  2     2   
4  3  import (   3  import ( 
4  3  import (   3  import ( 
5  4   "fmt"   4   "fmt" 
5  4   "fmt"   4   "fmt" 
6−       5 +  "strings" 
6+       5 +  "strings" 
7  5  )   6  ) 
7  5  )   6  ) 
8  6     7   
8  6     7   
9  7  func main() {   8  func main() { 
9  7  func main() {   8  func main() { 
@@ -11,6 +11,6 @@
11  9  }  10  } 
11  9  }  10  } 
12 10    11   
12 10    11   
13 11  func getContent() string {  12  func getContent() string { 
13 11  func getContent() string {  12  func getContent() string { 
14− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
15−      14 +  return content 
15+      14 +  return content 
16 13  }  15  } 
16 13  }  15  } 
internal/ui/diffview/testdata/TestDiffView/Split/CustomContextLines/LightMode.golden+3−3
@@ -3,7 +3,7 @@
3  2     2   
3  2     2   
4  3  import (   3  import ( 
4  3  import (   3  import ( 
5  4   "fmt"   4   "fmt" 
5  4   "fmt"   4   "fmt" 
6−       5 +  "strings" 
6+       5 +  "strings" 
7  5  )   6  ) 
7  5  )   6  ) 
8  6     7   
8  6     7   
9  7  func main() {   8  func main() { 
9  7  func main() {   8  func main() { 
@@ -11,6 +11,6 @@
11  9  }  10  } 
11  9  }  10  } 
12 10    11   
12 10    11   
13 11  func getContent() string {  12  func getContent() string { 
13 11  func getContent() string {  12  func getContent() string { 
14− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
15−      14 +  return content 
15+      14 +  return content 
16 13  }  15  } 
16 13  }  15  } 
internal/ui/diffview/testdata/TestDiffView/Split/Default/DarkMode.golden+2−2
@@ -2,6 +2,6 @@
2  5  )   5  ) 
2  5  )   5  ) 
3  6     6   
3  6     6   
4  7  func main() {   7  func main() { 
4  7  func main() {   7  func main() { 
5−  8 -  fmt.Println("Hello, world!")   8 +  content := "Hello, world!" 
5+  8 -  fmt.Println("Hello, world!")   8 +  content := "Hello, world!" 
6−       9 +  fmt.Println(content) 
6+       9 +  fmt.Println(content) 
7  9  }  10  } 
7  9  }  10  } 
internal/ui/diffview/testdata/TestDiffView/Split/Default/LightMode.golden+2−2
@@ -2,6 +2,6 @@
2  5  )   5  ) 
2  5  )   5  ) 
3  6     6   
3  6     6   
4  7  func main() {   7  func main() { 
4  7  func main() {   7  func main() { 
5−  8 -  fmt.Println("Hello, world!")   8 +  content := "Hello, world!" 
5+  8 -  fmt.Println("Hello, world!")   8 +  content := "Hello, world!" 
6−       9 +  fmt.Println(content) 
6+       9 +  fmt.Println(content) 
7  9  }  10  } 
7  9  }  10  } 
internal/ui/diffview/testdata/TestDiffView/Split/LargeWidth/DarkMode.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffView/Split/LargeWidth/LightMode.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffView/Split/MultipleHunks/DarkMode.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffView/Split/MultipleHunks/LightMode.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffView/Split/Narrow/DarkMode.golden+3−3
@@ -1,4 +1,4 @@
1 …  @@ -1,3 +1,3 @@   …   
1 …  @@ -1,3 +1,3 @@   …   
2− 1 - a  1 + d 
2+ 1 - a  1 + d 
3− 2 - b  2 + e 
3+ 2 - b  2 + e 
4− 3 - c  3 + f 
4+ 3 - c  3 + f 
internal/ui/diffview/testdata/TestDiffView/Split/Narrow/LightMode.golden+3−3
@@ -1,4 +1,4 @@
1 …  @@ -1,3 +1,3 @@   …   
1 …  @@ -1,3 +1,3 @@   …   
2− 1 - a  1 + d 
2+ 1 - a  1 + d 
3− 2 - b  2 + e 
3+ 2 - b  2 + e 
4− 3 - c  3 + f 
4+ 3 - c  3 + f 
internal/ui/diffview/testdata/TestDiffView/Split/NoLineNumbers/DarkMode.golden+2−2
@@ -2,6 +2,6 @@
2 )  ) 
2 )  ) 
3    
3    
4 func main() {  func main() { 
4 func main() {  func main() { 
5−-  fmt.Println("Hello, world!") +  content := "Hello, world!" 
5+-  fmt.Println("Hello, world!") +  content := "Hello, world!" 
6−  +  fmt.Println(content) 
6+  +  fmt.Println(content) 
7 }  } 
7 }  } 
internal/ui/diffview/testdata/TestDiffView/Split/NoLineNumbers/LightMode.golden+2−2
@@ -2,6 +2,6 @@
2 )  ) 
2 )  ) 
3    
3    
4 func main() {  func main() { 
4 func main() {  func main() { 
5−-  fmt.Println("Hello, world!") +  content := "Hello, world!" 
5+-  fmt.Println("Hello, world!") +  content := "Hello, world!" 
6−  +  fmt.Println(content) 
6+  +  fmt.Println(content) 
7 }  } 
7 }  } 
internal/ui/diffview/testdata/TestDiffView/Split/NoSyntaxHighlight/DarkMode.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4  "fmt"   4  "fmt" 
4  4  "fmt"   4  "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffView/Split/NoSyntaxHighlight/LightMode.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4  "fmt"   4  "fmt" 
4  4  "fmt"   4  "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffView/Split/SmallWidth/DarkMode.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getConte… 12  func getConte…
12 11  func getConte… 12  func getConte…
13− 12 -  return "H… 13 +  content :…
13+ 12 -  return "H… 13 +  content :…
14−      14 +  return co…
14+      14 +  return co…
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffView/Split/SmallWidth/LightMode.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getConte… 12  func getConte…
12 11  func getConte… 12  func getConte…
13− 12 -  return "H… 13 +  content :…
13+ 12 -  return "H… 13 +  content :…
14−      14 +  return co…
14+      14 +  return co…
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffView/Unified/CustomContextLines/DarkMode.golden+4−4
@@ -3,7 +3,7 @@
3  2   2   
3  2   2   
4  3   3  import ( 
4  3   3  import ( 
5  4   4   "fmt" 
5  4   4   "fmt" 
6−     5 +  "strings" 
6+     5 +  "strings" 
7  5   6  ) 
7  5   6  ) 
8  6   7   
8  6   7   
9  7   8  func main() { 
9  7   8  func main() { 
@@ -11,7 +11,7 @@
11  9  10  } 
11  9  10  } 
12 10  11   
12 10  11   
13 11  12  func getContent() string { 
13 11  12  func getContent() string { 
14− 12    -  return "Hello, world!" 
14+ 12    -  return "Hello, world!" 
15−    13 +  content := strings.ToUpper("Hello, World!") 
15+    13 +  content := strings.ToUpper("Hello, World!") 
16−    14 +  return content 
16+    14 +  return content 
17 13  15  } 
17 13  15  } 
internal/ui/diffview/testdata/TestDiffView/Unified/CustomContextLines/LightMode.golden+4−4
@@ -3,7 +3,7 @@
3  2   2   
3  2   2   
4  3   3  import ( 
4  3   3  import ( 
5  4   4   "fmt" 
5  4   4   "fmt" 
6−     5 +  "strings" 
6+     5 +  "strings" 
7  5   6  ) 
7  5   6  ) 
8  6   7   
8  6   7   
9  7   8  func main() { 
9  7   8  func main() { 
@@ -11,7 +11,7 @@
11  9  10  } 
11  9  10  } 
12 10  11   
12 10  11   
13 11  12  func getContent() string { 
13 11  12  func getContent() string { 
14− 12    -  return "Hello, world!" 
14+ 12    -  return "Hello, world!" 
15−    13 +  content := strings.ToUpper("Hello, World!") 
15+    13 +  content := strings.ToUpper("Hello, World!") 
16−    14 +  return content 
16+    14 +  return content 
17 13  15  } 
17 13  15  } 
internal/ui/diffview/testdata/TestDiffView/Unified/Default/DarkMode.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  ) 
2  5   5  ) 
3  6   6   
3  6   6   
4  7   7  func main() { 
4  7   7  func main() { 
5−  8    -  fmt.Println("Hello, world!") 
5+  8    -  fmt.Println("Hello, world!") 
6−     8 +  content := "Hello, world!" 
6+     8 +  content := "Hello, world!" 
7−     9 +  fmt.Println(content) 
7+     9 +  fmt.Println(content) 
8  9  10  } 
8  9  10  } 
internal/ui/diffview/testdata/TestDiffView/Unified/Default/LightMode.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  ) 
2  5   5  ) 
3  6   6   
3  6   6   
4  7   7  func main() { 
4  7   7  func main() { 
5−  8    -  fmt.Println("Hello, world!") 
5+  8    -  fmt.Println("Hello, world!") 
6−     8 +  content := "Hello, world!" 
6+     8 +  content := "Hello, world!" 
7−     9 +  fmt.Println(content) 
7+     9 +  fmt.Println(content) 
8  9  10  } 
8  9  10  } 
internal/ui/diffview/testdata/TestDiffView/Unified/LargeWidth/DarkMode.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World!") 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffView/Unified/LargeWidth/LightMode.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World!") 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffView/Unified/MultipleHunks/DarkMode.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World!") 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffView/Unified/MultipleHunks/LightMode.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World!") 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffView/Unified/Narrow/DarkMode.golden+6−6
@@ -1,7 +1,7 @@
1 …  …  @@ -1,3 +1,3 @@  
1 …  …  @@ -1,3 +1,3 @@  
2− 1    - a 
2+ 1    - a 
3− 2    - b 
3+ 2    - b 
4− 3    - c 
4+ 3    - c 
5−    1 + d 
5+    1 + d 
6−    2 + e 
6+    2 + e 
7−    3 + f 
7+    3 + f 
internal/ui/diffview/testdata/TestDiffView/Unified/Narrow/LightMode.golden+6−6
@@ -1,7 +1,7 @@
1 …  …  @@ -1,3 +1,3 @@  
1 …  …  @@ -1,3 +1,3 @@  
2− 1    - a 
2+ 1    - a 
3− 2    - b 
3+ 2    - b 
4− 3    - c 
4+ 3    - c 
5−    1 + d 
5+    1 + d 
6−    2 + e 
6+    2 + e 
7−    3 + f 
7+    3 + f 
internal/ui/diffview/testdata/TestDiffView/Unified/NoLineNumbers/DarkMode.golden+3−3
@@ -2,7 +2,7 @@
2 ) 
2 ) 
3  
3  
4 func main() { 
4 func main() { 
5−-  fmt.Println("Hello, world!") 
5+-  fmt.Println("Hello, world!") 
6−+  content := "Hello, world!" 
6++  content := "Hello, world!" 
7−+  fmt.Println(content) 
7++  fmt.Println(content) 
8 } 
8 } 
internal/ui/diffview/testdata/TestDiffView/Unified/NoLineNumbers/LightMode.golden+3−3
@@ -2,7 +2,7 @@
2 ) 
2 ) 
3  
3  
4 func main() { 
4 func main() { 
5−-  fmt.Println("Hello, world!") 
5+-  fmt.Println("Hello, world!") 
6−+  content := "Hello, world!" 
6++  content := "Hello, world!" 
7−+  fmt.Println(content) 
7++  fmt.Println(content) 
8 } 
8 } 
internal/ui/diffview/testdata/TestDiffView/Unified/NoSyntaxHighlight/DarkMode.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4  "fmt" 
4  4   4  "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World!") 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffView/Unified/NoSyntaxHighlight/LightMode.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4  "fmt" 
4  4   4  "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World!") 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffView/Unified/SmallWidth/DarkMode.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUppe…
14+    13 +  content := strings.ToUppe…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffView/Unified/SmallWidth/LightMode.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUppe…
14+    13 +  content := strings.ToUppe…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewHeight/Split/HeightOf005.golden+1−1
@@ -2,4 +2,4 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−  …  …   …  … 
5+  …  …   …  … 
internal/ui/diffview/testdata/TestDiffViewHeight/Split/HeightOf006.golden+1−1
@@ -2,5 +2,5 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  …  …   …  … 
6  …  …   …  … 
internal/ui/diffview/testdata/TestDiffViewHeight/Split/HeightOf007.golden+1−1
@@ -2,6 +2,6 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  …  …   …  … 
7  …  …   …  … 
internal/ui/diffview/testdata/TestDiffViewHeight/Split/HeightOf008.golden+1−1
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  …  …   …  … 
8  …  …   …  … 
internal/ui/diffview/testdata/TestDiffViewHeight/Split/HeightOf009.golden+1−1
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
internal/ui/diffview/testdata/TestDiffViewHeight/Split/HeightOf010.golden+1−1
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
internal/ui/diffview/testdata/TestDiffViewHeight/Split/HeightOf011.golden+1−1
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
internal/ui/diffview/testdata/TestDiffViewHeight/Split/HeightOf012.golden+1−1
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
internal/ui/diffview/testdata/TestDiffViewHeight/Split/HeightOf013.golden+2−2
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,4 +10,4 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13−  …  …   …  … 
13+  …  …   …  … 
internal/ui/diffview/testdata/TestDiffViewHeight/Split/HeightOf014.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,5 +10,5 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14−  …  …   …  … 
14+  …  …   …  … 
internal/ui/diffview/testdata/TestDiffViewHeight/Split/HeightOf015.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewHeight/Split/HeightOf016.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewHeight/Split/HeightOf017.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewHeight/Split/HeightOf018.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewHeight/Split/HeightOf019.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewHeight/Split/HeightOf020.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewHeight/Unified/HeightOf005.golden+1−1
@@ -2,4 +2,4 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−  …   …  … 
5+  …   …  … 
internal/ui/diffview/testdata/TestDiffViewHeight/Unified/HeightOf006.golden+1−1
@@ -2,5 +2,5 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  …   …  … 
6  …   …  … 
internal/ui/diffview/testdata/TestDiffViewHeight/Unified/HeightOf007.golden+1−1
@@ -2,6 +2,6 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  …   …  … 
7  …   …  … 
internal/ui/diffview/testdata/TestDiffViewHeight/Unified/HeightOf008.golden+1−1
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  …   …  … 
8  …   …  … 
internal/ui/diffview/testdata/TestDiffViewHeight/Unified/HeightOf009.golden+1−1
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
internal/ui/diffview/testdata/TestDiffViewHeight/Unified/HeightOf010.golden+1−1
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
internal/ui/diffview/testdata/TestDiffViewHeight/Unified/HeightOf011.golden+1−1
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
internal/ui/diffview/testdata/TestDiffViewHeight/Unified/HeightOf012.golden+1−1
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
internal/ui/diffview/testdata/TestDiffViewHeight/Unified/HeightOf013.golden+2−2
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,4 +10,4 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13−  …   …  … 
13+  …   …  … 
internal/ui/diffview/testdata/TestDiffViewHeight/Unified/HeightOf014.golden+3−3
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,5 +10,5 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−  …   …  … 
14+  …   …  … 
internal/ui/diffview/testdata/TestDiffViewHeight/Unified/HeightOf015.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,6 +10,6 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World!") 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15−  …   …  … 
15+  …   …  … 
internal/ui/diffview/testdata/TestDiffViewHeight/Unified/HeightOf016.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World!") 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewHeight/Unified/HeightOf017.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World!") 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewHeight/Unified/HeightOf018.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World!") 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewHeight/Unified/HeightOf019.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World!") 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewHeight/Unified/HeightOf020.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World!") 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewLineBreakIssue/Split.golden+5−5
@@ -1,9 +1,9 @@
1  …  @@ -1,6 +1,8 @@    …   
1  …  @@ -1,6 +1,8 @@    …   
2−  1 - // this is   1 + /** 
2+  1 - // this is   1 + /** 
3−  2 - // a regular   2 +  * this is 
3+  2 - // a regular   2 +  * this is 
4−  3 - // comment   3 +  * a block 
4+  3 - // comment   3 +  * a block 
5−       4 +  * comment 
5+       4 +  * comment 
6−       5 +  */ 
6+       5 +  */ 
7  4  $(function() {   6  $(function() { 
7  4  $(function() {   6  $(function() { 
8  5   console.log("Hello, world!");   7   console.log("Hello, world!"); 
8  5   console.log("Hello, world!");   7   console.log("Hello, world!"); 
9  6  });   8  }); 
9  6  });   8  }); 
internal/ui/diffview/testdata/TestDiffViewLineBreakIssue/Unified.golden+8−8
@@ -1,12 +1,12 @@
1  …   …  @@ -1,6 +1,8 @@  
1  …   …  @@ -1,6 +1,8 @@  
2−  1    - // this is 
2+  1    - // this is 
3−  2    - // a regular 
3+  2    - // a regular 
4−  3    - // comment 
4+  3    - // comment 
5−     1 + /** 
5+     1 + /** 
6−     2 +  * this is 
6+     2 +  * this is 
7−     3 +  * a block 
7+     3 +  * a block 
8−     4 +  * comment 
8+     4 +  * comment 
9−     5 +  */ 
9+     5 +  */ 
10  4   6  $(function() { 
10  4   6  $(function() { 
11  5   7   console.log("Hello, world!"); 
11  5   7   console.log("Hello, world!"); 
12  6   8  }); 
12  6   8  }); 
internal/ui/diffview/testdata/TestDiffViewTabs/Split.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewTabs/Unified.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World!") 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf001.golden+3−3
@@ -2,7 +2,7 @@
2 
2 
3 
3 
4 
4 
5− 
5+ 
6 
6 
7 
7 
8 
8 
@@ -10,6 +10,6 @@
10 
10 
11 
11 
12 
12 
13− 
13+ 
14− 
14+ 
15 
15 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf002.golden+3−3
@@ -2,7 +2,7 @@
2  
2  
3  
3  
4  
4  
5−  
5+  
6  
6  
7  
7  
8  
8  
@@ -10,6 +10,6 @@
10  
10  
11 1
11 1
12 1
12 1
13− 1
13+ 1
14−  
14+  
15 1
15 1
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf003.golden+3−3
@@ -2,7 +2,7 @@
2  2
2  2
3  3
3  3
4  4
4  4
5−  
5+  
6  5
6  5
7  6
7  6
8  7
8  7
@@ -10,6 +10,6 @@
10  9
10  9
11 10
11 10
12 11
12 11
13− 12
13+ 12
14−  
14+  
15 13
15 13
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf004.golden+3−3
@@ -2,7 +2,7 @@
2  2 
2  2 
3  3 
3  3 
4  4 
4  4 
5−   
5+   
6  5 
6  5 
7  6 
7  6 
8  7 
8  7 
@@ -10,6 +10,6 @@
10  9 
10  9 
11 10 
11 10 
12 11 
12 11 
13− 12 
13+ 12 
14−   
14+   
15 13 
15 13 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf005.golden+3−3
@@ -2,7 +2,7 @@
2  2  
2  2  
3  3  
3  3  
4  4  
4  4  
5−    
5+    
6  5  
6  5  
7  6  
7  6  
8  7  
8  7  
@@ -10,6 +10,6 @@
10  9  
10  9  
11 10  
11 10  
12 11  
12 11  
13− 12 -
13+ 12 -
14−    
14+    
15 13  
15 13  
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf006.golden+3−3
@@ -2,7 +2,7 @@
2  2  
2  2  
3  3  
3  3  
4  4  
4  4  
5−    
5+    
6  5  
6  5  
7  6  
7  6  
8  7  
8  7  
@@ -10,6 +10,6 @@
10  9  
10  9  
11 10  
11 10  
12 11  
12 11  
13− 12 - 
13+ 12 - 
14−    
14+    
15 13  
15 13  
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf007.golden+3−3
@@ -2,7 +2,7 @@
2  2   
2  2   
3  3   
3  3   
4  4   
4  4   
5−     
5+     
6  5   
6  5   
7  6   
7  6   
8  7   
8  7   
@@ -10,6 +10,6 @@
10  9   
10  9   
11 10   
11 10   
12 11   
12 11   
13− 12 -  
13+ 12 -  
14−     
14+     
15 13   
15 13   
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf008.golden+3−3
@@ -2,7 +2,7 @@
2  2    
2  2    
3  3    
3  3    
4  4    
4  4    
5−      
5+      
6  5    
6  5    
7  6    
7  6    
8  7    
8  7    
@@ -10,6 +10,6 @@
10  9   1
10  9   1
11 10   1
11 10   1
12 11   1
12 11   1
13− 12 -  1
13+ 12 -  1
14−     1
14+     1
15 13   1
15 13   1
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf009.golden+3−3
@@ -2,7 +2,7 @@
2  2    2 
2  2    2 
3  3    3 
3  3    3 
4  4    4 
4  4    4 
5−      5 
5+      5 
6  5    6 
6  5    6 
7  6    7 
7  6    7 
8  7    8 
8  7    8 
@@ -10,6 +10,6 @@
10  9   10 
10  9   10 
11 10   11 
11 10   11 
12 11   12 
12 11   12 
13− 12 - 13 
13+ 12 - 13 
14−     14 
14+     14 
15 13   15 
15 13   15 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf010.golden+3−3
@@ -2,7 +2,7 @@
2  2    2  
2  2    2  
3  3    3  
3  3    3  
4  4    4  
4  4    4  
5−      5 +
5+      5 +
6  5    6  
6  5    6  
7  6    7  
7  6    7  
8  7    8  
8  7    8  
@@ -10,6 +10,6 @@
10  9   10  
10  9   10  
11 10   11  
11 10   11  
12 11   12  
12 11   12  
13− 12 - 13 +
13+ 12 - 13 +
14−     14 +
14+     14 +
15 13   15  
15 13   15  
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf011.golden+3−3
@@ -2,7 +2,7 @@
2  2    2  
2  2    2  
3  3    3  
3  3    3  
4  4    4  
4  4    4  
5−      5 +
5+      5 +
6  5    6  
6  5    6  
7  6    7  
7  6    7  
8  7    8  
8  7    8  
@@ -10,6 +10,6 @@
10  9   10  
10  9   10  
11 10   11  
11 10   11  
12 11   12  
12 11   12  
13− 12 -  13 +
13+ 12 -  13 +
14−     14 +
14+     14 +
15 13   15  
15 13   15  
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf012.golden+3−3
@@ -2,7 +2,7 @@
2  2    2  
2  2    2  
3  3    3  
3  3    3  
4  4    4  
4  4    4  
5−      5 + 
5+      5 + 
6  5    6  
6  5    6  
7  6    7  
7  6    7  
8  7    8  
8  7    8  
@@ -10,6 +10,6 @@
10  9   10  
10  9   10  
11 10   11  
11 10   11  
12 11   12  
12 11   12  
13− 12 -  13 + 
13+ 12 -  13 + 
14−     14 + 
14+     14 + 
15 13   15  
15 13   15  
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf013.golden+3−3
@@ -2,7 +2,7 @@
2  2    2   
2  2    2   
3  3    3   
3  3    3   
4  4    4   
4  4    4   
5−      5 +  
5+      5 +  
6  5    6   
6  5    6   
7  6    7   
7  6    7   
8  7    8   
8  7    8   
@@ -10,6 +10,6 @@
10  9   10   
10  9   10   
11 10   11   
11 10   11   
12 11   12   
12 11   12   
13− 12 -  13 +  
13+ 12 -  13 +  
14−     14 +  
14+     14 +  
15 13   15   
15 13   15   
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf014.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  …  3  …
3  3  …  3  …
4  4  …  4  …
4  4  …  4  …
5−       5 + …
5+       5 + …
6  5  )  6  )
6  5  )  6  )
7  6     7   
7  6     7   
8  7  …  8  …
8  7  …  8  …
@@ -10,6 +10,6 @@
10  9  } 10  }
10  9  } 10  }
11 10    11   
11 10    11   
12 11  … 12  …
12 11  … 12  …
13− 12 - … 13 + …
13+ 12 - … 13 + …
14−      14 + …
14+      14 + …
15 13  } 15  }
15 13  } 15  }
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf015.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  …  3  … 
3  3  …  3  … 
4  4  …  4  … 
4  4  …  4  … 
5−       5 + … 
5+       5 + … 
6  5  )  6  ) 
6  5  )  6  ) 
7  6     7   
7  6     7   
8  7  …  8  … 
8  7  …  8  … 
@@ -10,6 +10,6 @@
10  9  } 10  } 
10  9  } 10  } 
11 10    11   
11 10    11   
12 11  … 12  … 
12 11  … 12  … 
13− 12 - … 13 + … 
13+ 12 - … 13 + … 
14−      14 + … 
14+      14 + … 
15 13  } 15  } 
15 13  } 15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf016.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  i…  3  i…
3  3  i…  3  i…
4  4   …  4   …
4  4   …  4   …
5−       5 +  …
5+       5 +  …
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  f…  8  f…
8  7  f…  8  f…
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  f… 12  f…
12 11  f… 12  f…
13− 12 -  … 13 +  …
13+ 12 -  … 13 +  …
14−      14 +  …
14+      14 +  …
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf017.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  i…  3  i… 
3  3  i…  3  i… 
4  4   …  4   … 
4  4   …  4   … 
5−       5 +  … 
5+       5 +  … 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  f…  8  f… 
8  7  f…  8  f… 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  f… 12  f… 
12 11  f… 12  f… 
13− 12 -  … 13 +  … 
13+ 12 -  … 13 +  … 
14−      14 +  … 
14+      14 +  … 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf018.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  im…  3  im…
3  3  im…  3  im…
4  4   …  4   …
4  4   …  4   …
5−       5 +  …
5+       5 +  …
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  fu…  8  fu…
8  7  fu…  8  fu…
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  fu… 12  fu…
12 11  fu… 12  fu…
13− 12 -  … 13 +  …
13+ 12 -  … 13 +  …
14−      14 +  …
14+      14 +  …
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf019.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  im…  3  im… 
3  3  im…  3  im… 
4  4   …  4   … 
4  4   …  4   … 
5−       5 +  … 
5+       5 +  … 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  fu…  8  fu… 
8  7  fu…  8  fu… 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  fu… 12  fu… 
12 11  fu… 12  fu… 
13− 12 -  … 13 +  … 
13+ 12 -  … 13 +  … 
14−      14 +  … 
14+      14 +  … 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf020.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  imp…  3  imp…
3  3  imp…  3  imp…
4  4   …  4   …
4  4   …  4   …
5−       5 +  …
5+       5 +  …
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  fun…  8  fun…
8  7  fun…  8  fun…
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  fun… 12  fun…
12 11  fun… 12  fun…
13− 12 -  … 13 +  …
13+ 12 -  … 13 +  …
14−      14 +  …
14+      14 +  …
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf021.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  imp…  3  imp… 
3  3  imp…  3  imp… 
4  4   …  4   … 
4  4   …  4   … 
5−       5 +  … 
5+       5 +  … 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  fun…  8  fun… 
8  7  fun…  8  fun… 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  fun… 12  fun… 
12 11  fun… 12  fun… 
13− 12 -  … 13 +  … 
13+ 12 -  … 13 +  … 
14−      14 +  … 
14+      14 +  … 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf022.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  impo…  3  impo…
3  3  impo…  3  impo…
4  4   …  4   …
4  4   …  4   …
5−       5 +  …
5+       5 +  …
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func…  8  func…
8  7  func…  8  func…
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func… 12  func…
12 11  func… 12  func…
13− 12 -  … 13 +  …
13+ 12 -  … 13 +  …
14−      14 +  …
14+      14 +  …
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf023.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  impo…  3  impo… 
3  3  impo…  3  impo… 
4  4   …  4   … 
4  4   …  4   … 
5−       5 +  … 
5+       5 +  … 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func…  8  func… 
8  7  func…  8  func… 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func… 12  func… 
12 11  func… 12  func… 
13− 12 -  … 13 +  … 
13+ 12 -  … 13 +  … 
14−      14 +  … 
14+      14 +  … 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf024.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  impor…  3  impor…
3  3  impor…  3  impor…
4  4   "…  4   "…
4  4   "…  4   "…
5−       5 +  "…
5+       5 +  "…
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func …  8  func …
8  7  func …  8  func …
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func … 12  func …
12 11  func … 12  func …
13− 12 -  r… 13 +  c…
13+ 12 -  r… 13 +  c…
14−      14 +  r…
14+      14 +  r…
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf025.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  impor…  3  impor… 
3  3  impor…  3  impor… 
4  4   "…  4   "… 
4  4   "…  4   "… 
5−       5 +  "… 
5+       5 +  "… 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func …  8  func … 
8  7  func …  8  func … 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func … 12  func … 
12 11  func … 12  func … 
13− 12 -  r… 13 +  c… 
13+ 12 -  r… 13 +  c… 
14−      14 +  r… 
14+      14 +  r… 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf026.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import…  3  import…
3  3  import…  3  import…
4  4   "f…  4   "f…
4  4   "f…  4   "f…
5−       5 +  "s…
5+       5 +  "s…
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func m…  8  func m…
8  7  func m…  8  func m…
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func g… 12  func g…
12 11  func g… 12  func g…
13− 12 -  re… 13 +  co…
13+ 12 -  re… 13 +  co…
14−      14 +  re…
14+      14 +  re…
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf027.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import…  3  import… 
3  3  import…  3  import… 
4  4   "f…  4   "f… 
4  4   "f…  4   "f… 
5−       5 +  "s… 
5+       5 +  "s… 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func m…  8  func m… 
8  7  func m…  8  func m… 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func g… 12  func g… 
12 11  func g… 12  func g… 
13− 12 -  re… 13 +  co… 
13+ 12 -  re… 13 +  co… 
14−      14 +  re… 
14+      14 +  re… 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf028.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (  3  import (
3  3  import (  3  import (
4  4   "fm…  4   "fm…
4  4   "fm…  4   "fm…
5−       5 +  "st…
5+       5 +  "st…
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func ma…  8  func ma…
8  7  func ma…  8  func ma…
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func ge… 12  func ge…
12 11  func ge… 12  func ge…
13− 12 -  ret… 13 +  con…
13+ 12 -  ret… 13 +  con…
14−      14 +  ret…
14+      14 +  ret…
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf029.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (  3  import ( 
3  3  import (  3  import ( 
4  4   "fm…  4   "fm… 
4  4   "fm…  4   "fm… 
5−       5 +  "st… 
5+       5 +  "st… 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func ma…  8  func ma… 
8  7  func ma…  8  func ma… 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func ge… 12  func ge… 
12 11  func ge… 12  func ge… 
13− 12 -  ret… 13 +  con… 
13+ 12 -  ret… 13 +  con… 
14−      14 +  ret… 
14+      14 +  ret… 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf030.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"  4   "fmt"
4  4   "fmt"  4   "fmt"
5−       5 +  "str…
5+       5 +  "str…
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func mai…  8  func mai…
8  7  func mai…  8  func mai…
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func get… 12  func get…
12 11  func get… 12  func get…
13− 12 -  retu… 13 +  cont…
13+ 12 -  retu… 13 +  cont…
14−      14 +  retu…
14+      14 +  retu…
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf031.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"  4   "fmt" 
4  4   "fmt"  4   "fmt" 
5−       5 +  "str… 
5+       5 +  "str… 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func mai…  8  func mai… 
8  7  func mai…  8  func mai… 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func get… 12  func get… 
12 11  func get… 12  func get… 
13− 12 -  retu… 13 +  cont… 
13+ 12 -  retu… 13 +  cont… 
14−      14 +  retu… 
14+      14 +  retu… 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf032.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "stri…
5+       5 +  "stri…
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main…  8  func main…
8  7  func main…  8  func main…
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getC… 12  func getC…
12 11  func getC… 12  func getC…
13− 12 -  retur… 13 +  conte…
13+ 12 -  retur… 13 +  conte…
14−      14 +  retur…
14+      14 +  retur…
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf033.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "stri… 
5+       5 +  "stri… 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main…  8  func main… 
8  7  func main…  8  func main… 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getC… 12  func getC… 
12 11  func getC… 12  func getC… 
13− 12 -  retur… 13 +  conte… 
13+ 12 -  retur… 13 +  conte… 
14−      14 +  retur… 
14+      14 +  retur… 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf034.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strin…
5+       5 +  "strin…
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main(…  8  func main(…
8  7  func main(…  8  func main(…
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getCo… 12  func getCo…
12 11  func getCo… 12  func getCo…
13− 12 -  return… 13 +  conten…
13+ 12 -  return… 13 +  conten…
14−      14 +  return…
14+      14 +  return…
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf035.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strin… 
5+       5 +  "strin… 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main(…  8  func main(… 
8  7  func main(…  8  func main(… 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getCo… 12  func getCo… 
12 11  func getCo… 12  func getCo… 
13− 12 -  return… 13 +  conten… 
13+ 12 -  return… 13 +  conten… 
14−      14 +  return… 
14+      14 +  return… 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf036.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "string…
5+       5 +  "string…
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main()…  8  func main()…
8  7  func main()…  8  func main()…
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getCon… 12  func getCon…
12 11  func getCon… 12  func getCon…
13− 12 -  return … 13 +  content…
13+ 12 -  return … 13 +  content…
14−      14 +  return …
14+      14 +  return …
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf037.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "string… 
5+       5 +  "string… 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main()…  8  func main()… 
8  7  func main()…  8  func main()… 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getCon… 12  func getCon… 
12 11  func getCon… 12  func getCon… 
13− 12 -  return … 13 +  content… 
13+ 12 -  return … 13 +  content… 
14−      14 +  return … 
14+      14 +  return … 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf038.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings"
5+       5 +  "strings"
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {  8  func main() {
8  7  func main() {  8  func main() {
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getCont… 12  func getCont…
12 11  func getCont… 12  func getCont…
13− 12 -  return "… 13 +  content …
13+ 12 -  return "… 13 +  content …
14−      14 +  return c…
14+      14 +  return c…
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf039.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {  8  func main() { 
8  7  func main() {  8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getCont… 12  func getCont… 
12 11  func getCont… 12  func getCont… 
13− 12 -  return "… 13 +  content … 
13+ 12 -  return "… 13 +  content … 
14−      14 +  return c… 
14+      14 +  return c… 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf040.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getConte… 12  func getConte…
12 11  func getConte… 12  func getConte…
13− 12 -  return "H… 13 +  content :…
13+ 12 -  return "H… 13 +  content :…
14−      14 +  return co…
14+      14 +  return co…
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf041.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getConte… 12  func getConte… 
12 11  func getConte… 12  func getConte… 
13− 12 -  return "H… 13 +  content :… 
13+ 12 -  return "H… 13 +  content :… 
14−      14 +  return co… 
14+      14 +  return co… 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf042.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getConten… 12  func getConten…
12 11  func getConten… 12  func getConten…
13− 12 -  return "He… 13 +  content :=…
13+ 12 -  return "He… 13 +  content :=…
14−      14 +  return con…
14+      14 +  return con…
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf043.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getConten… 12  func getConten… 
12 11  func getConten… 12  func getConten… 
13− 12 -  return "He… 13 +  content :=… 
13+ 12 -  return "He… 13 +  content :=… 
14−      14 +  return con… 
14+      14 +  return con… 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf044.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent… 12  func getContent…
12 11  func getContent… 12  func getContent…
13− 12 -  return "Hel… 13 +  content := …
13+ 12 -  return "Hel… 13 +  content := …
14−      14 +  return cont…
14+      14 +  return cont…
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf045.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent… 12  func getContent… 
12 11  func getContent… 12  func getContent… 
13− 12 -  return "Hel… 13 +  content := … 
13+ 12 -  return "Hel… 13 +  content := … 
14−      14 +  return cont… 
14+      14 +  return cont… 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf046.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent(… 12  func getContent(…
12 11  func getContent(… 12  func getContent(…
13− 12 -  return "Hell… 13 +  content := s…
13+ 12 -  return "Hell… 13 +  content := s…
14−      14 +  return conte…
14+      14 +  return conte…
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf047.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent(… 12  func getContent(… 
12 11  func getContent(… 12  func getContent(… 
13− 12 -  return "Hell… 13 +  content := s… 
13+ 12 -  return "Hell… 13 +  content := s… 
14−      14 +  return conte… 
14+      14 +  return conte… 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf048.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent()… 12  func getContent()…
12 11  func getContent()… 12  func getContent()…
13− 12 -  return "Hello… 13 +  content := st…
13+ 12 -  return "Hello… 13 +  content := st…
14−      14 +  return content
14+      14 +  return content
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf049.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent()… 12  func getContent()… 
12 11  func getContent()… 12  func getContent()… 
13− 12 -  return "Hello… 13 +  content := st… 
13+ 12 -  return "Hello… 13 +  content := st… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf050.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() … 12  func getContent() …
12 11  func getContent() … 12  func getContent() …
13− 12 -  return "Hello,… 13 +  content := str…
13+ 12 -  return "Hello,… 13 +  content := str…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf051.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() … 12  func getContent() … 
12 11  func getContent() … 12  func getContent() … 
13− 12 -  return "Hello,… 13 +  content := str… 
13+ 12 -  return "Hello,… 13 +  content := str… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf052.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() s… 12  func getContent() s…
12 11  func getContent() s… 12  func getContent() s…
13− 12 -  return "Hello, … 13 +  content := stri…
13+ 12 -  return "Hello, … 13 +  content := stri…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf053.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() s… 12  func getContent() s… 
12 11  func getContent() s… 12  func getContent() s… 
13− 12 -  return "Hello, … 13 +  content := stri… 
13+ 12 -  return "Hello, … 13 +  content := stri… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf054.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() st… 12  func getContent() st…
12 11  func getContent() st… 12  func getContent() st…
13− 12 -  return "Hello, w… 13 +  content := strin…
13+ 12 -  return "Hello, w… 13 +  content := strin…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf055.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() st… 12  func getContent() st… 
12 11  func getContent() st… 12  func getContent() st… 
13− 12 -  return "Hello, w… 13 +  content := strin… 
13+ 12 -  return "Hello, w… 13 +  content := strin… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf056.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() str… 12  func getContent() str…
12 11  func getContent() str… 12  func getContent() str…
13− 12 -  return "Hello, wo… 13 +  content := string…
13+ 12 -  return "Hello, wo… 13 +  content := string…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf057.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() str… 12  func getContent() str… 
12 11  func getContent() str… 12  func getContent() str… 
13− 12 -  return "Hello, wo… 13 +  content := string… 
13+ 12 -  return "Hello, wo… 13 +  content := string… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf058.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() stri… 12  func getContent() stri…
12 11  func getContent() stri… 12  func getContent() stri…
13− 12 -  return "Hello, wor… 13 +  content := strings…
13+ 12 -  return "Hello, wor… 13 +  content := strings…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf059.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() stri… 12  func getContent() stri… 
12 11  func getContent() stri… 12  func getContent() stri… 
13− 12 -  return "Hello, wor… 13 +  content := strings… 
13+ 12 -  return "Hello, wor… 13 +  content := strings… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf060.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() strin… 12  func getContent() strin…
12 11  func getContent() strin… 12  func getContent() strin…
13− 12 -  return "Hello, worl… 13 +  content := strings.…
13+ 12 -  return "Hello, worl… 13 +  content := strings.…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf061.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() strin… 12  func getContent() strin… 
12 11  func getContent() strin… 12  func getContent() strin… 
13− 12 -  return "Hello, worl… 13 +  content := strings.… 
13+ 12 -  return "Hello, worl… 13 +  content := strings.… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf062.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string… 12  func getContent() string…
12 11  func getContent() string… 12  func getContent() string…
13− 12 -  return "Hello, world… 13 +  content := strings.T…
13+ 12 -  return "Hello, world… 13 +  content := strings.T…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf063.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string… 12  func getContent() string… 
12 11  func getContent() string… 12  func getContent() string… 
13− 12 -  return "Hello, world… 13 +  content := strings.T… 
13+ 12 -  return "Hello, world… 13 +  content := strings.T… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf064.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string { 12  func getContent() string {
12 11  func getContent() string { 12  func getContent() string {
13− 12 -  return "Hello, world!" 13 +  content := strings.To…
13+ 12 -  return "Hello, world!" 13 +  content := strings.To…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf065.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string { 12  func getContent() string { 
12 11  func getContent() string { 12  func getContent() string { 
13− 12 -  return "Hello, world!" 13 +  content := strings.To… 
13+ 12 -  return "Hello, world!" 13 +  content := strings.To… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf066.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToU…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToU…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf067.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToU… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToU… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf068.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUp…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUp…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf069.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUp… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUp… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf070.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpp…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpp…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf071.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpp… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpp… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf072.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUppe…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUppe…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf073.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUppe… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUppe… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf074.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf075.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf076.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper(…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper(…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf077.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper(… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper(… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf078.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf079.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf080.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("H…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("H…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf081.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("H… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("H… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf082.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("He…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("He…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf083.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("He… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("He… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf084.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hel…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hel…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf085.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hel… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hel… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf086.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hell…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hell…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf087.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hell… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hell… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf088.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf089.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf090.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello,…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello,…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf091.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello,… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello,… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf092.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, …
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, …
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf093.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, … 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, … 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf094.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, W…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, W…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf095.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, W… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, W… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf096.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, Wo…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, Wo…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf097.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, Wo… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, Wo… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf098.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, Wor…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, Wor…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf099.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, Wor… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, Wor… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf100.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, Worl…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, Worl…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf101.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, Worl… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, Worl… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf102.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf103.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf104.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!…
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!…
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf105.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!… 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!… 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf106.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!")
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!")
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf107.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf108.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf109.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Split/WidthOf110.golden+3−3
@@ -2,7 +2,7 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−       5 +  "strings" 
5+       5 +  "strings" 
6  5  )   6  ) 
6  5  )   6  ) 
7  6     7   
7  6     7   
8  7  func main() {   8  func main() { 
8  7  func main() {   8  func main() { 
@@ -10,6 +10,6 @@
10  9  }  10  } 
10  9  }  10  } 
11 10    11   
11 10    11   
12 11  func getContent() string {  12  func getContent() string { 
12 11  func getContent() string {  12  func getContent() string { 
13− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14−      14 +  return content 
14+      14 +  return content 
15 13  }  15  } 
15 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf001.golden+4−4
@@ -2,7 +2,7 @@
2 
2 
3 
3 
4 
4 
5− 
5+ 
6 
6 
7 
7 
8 
8 
@@ -10,7 +10,7 @@
10 
10 
11 
11 
12 
12 
13− 
13+ 
14− 
14+ 
15− 
15+ 
16 
16 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf002.golden+4−4
@@ -2,7 +2,7 @@
2  
2  
3  
3  
4  
4  
5−  
5+  
6  
6  
7  
7  
8  
8  
@@ -10,7 +10,7 @@
10  
10  
11 1
11 1
12 1
12 1
13− 1
13+ 1
14−  
14+  
15−  
15+  
16 1
16 1
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf003.golden+4−4
@@ -2,7 +2,7 @@
2  2
2  2
3  3
3  3
4  4
4  4
5−  
5+  
6  5
6  5
7  6
7  6
8  7
8  7
@@ -10,7 +10,7 @@
10  9
10  9
11 10
11 10
12 11
12 11
13− 12
13+ 12
14−  
14+  
15−  
15+  
16 13
16 13
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf004.golden+4−4
@@ -2,7 +2,7 @@
2  2 
2  2 
3  3 
3  3 
4  4 
4  4 
5−   
5+   
6  5 
6  5 
7  6 
7  6 
8  7 
8  7 
@@ -10,7 +10,7 @@
10  9 
10  9 
11 10 
11 10 
12 11 
12 11 
13− 12 
13+ 12 
14−   
14+   
15−   
15+   
16 13 
16 13 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf005.golden+4−4
@@ -2,7 +2,7 @@
2  2  
2  2  
3  3  
3  3  
4  4  
4  4  
5−    
5+    
6  5  
6  5  
7  6  
7  6  
8  7  
8  7  
@@ -10,7 +10,7 @@
10  9  
10  9  
11 10  
11 10  
12 11  
12 11  
13− 12  
13+ 12  
14−    
14+    
15−    
15+    
16 13  
16 13  
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf006.golden+4−4
@@ -2,7 +2,7 @@
2  2   
2  2   
3  3   
3  3   
4  4   
4  4   
5−     
5+     
6  5   
6  5   
7  6   
7  6   
8  7   
8  7   
@@ -10,7 +10,7 @@
10  9  1
10  9  1
11 10  1
11 10  1
12 11  1
12 11  1
13− 12   
13+ 12   
14−    1
14+    1
15−    1
15+    1
16 13  1
16 13  1
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf007.golden+4−4
@@ -2,7 +2,7 @@
2  2   2
2  2   2
3  3   3
3  3   3
4  4   4
4  4   4
5−     5
5+     5
6  5   6
6  5   6
7  6   7
7  6   7
8  7   8
8  7   8
@@ -10,7 +10,7 @@
10  9  10
10  9  10
11 10  11
11 10  11
12 11  12
12 11  12
13− 12   
13+ 12   
14−    13
14+    13
15−    14
15+    14
16 13  15
16 13  15
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf008.golden+4−4
@@ -2,7 +2,7 @@
2  2   2 
2  2   2 
3  3   3 
3  3   3 
4  4   4 
4  4   4 
5−     5 
5+     5 
6  5   6 
6  5   6 
7  6   7 
7  6   7 
8  7   8 
8  7   8 
@@ -10,7 +10,7 @@
10  9  10 
10  9  10 
11 10  11 
11 10  11 
12 11  12 
12 11  12 
13− 12    
13+ 12    
14−    13 
14+    13 
15−    14 
15+    14 
16 13  15 
16 13  15 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf009.golden+4−4
@@ -2,7 +2,7 @@
2  2   2  
2  2   2  
3  3   3  
3  3   3  
4  4   4  
4  4   4  
5−     5 +
5+     5 +
6  5   6  
6  5   6  
7  6   7  
7  6   7  
8  7   8  
8  7   8  
@@ -10,7 +10,7 @@
10  9  10  
10  9  10  
11 10  11  
11 10  11  
12 11  12  
12 11  12  
13− 12    -
13+ 12    -
14−    13 +
14+    13 +
15−    14 +
15+    14 +
16 13  15  
16 13  15  
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf010.golden+4−4
@@ -2,7 +2,7 @@
2  2   2  
2  2   2  
3  3   3  
3  3   3  
4  4   4  
4  4   4  
5−     5 + 
5+     5 + 
6  5   6  
6  5   6  
7  6   7  
7  6   7  
8  7   8  
8  7   8  
@@ -10,7 +10,7 @@
10  9  10  
10  9  10  
11 10  11  
11 10  11  
12 11  12  
12 11  12  
13− 12    - 
13+ 12    - 
14−    13 + 
14+    13 + 
15−    14 + 
15+    14 + 
16 13  15  
16 13  15  
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf011.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  …
3  3   3  …
4  4   4  …
4  4   4  …
5−     5 + …
5+     5 + …
6  5   6  )
6  5   6  )
7  6   7   
7  6   7   
8  7   8  …
8  7   8  …
@@ -10,7 +10,7 @@
10  9  10  }
10  9  10  }
11 10  11   
11 10  11   
12 11  12  …
12 11  12  …
13− 12    - …
13+ 12    - …
14−    13 + …
14+    13 + …
15−    14 + …
15+    14 + …
16 13  15  }
16 13  15  }
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf012.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  i…
3  3   3  i…
4  4   4   …
4  4   4   …
5−     5 +  …
5+     5 +  …
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  f…
8  7   8  f…
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  f…
12 11  12  f…
13− 12    -  …
13+ 12    -  …
14−    13 +  …
14+    13 +  …
15−    14 +  …
15+    14 +  …
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf013.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  im…
3  3   3  im…
4  4   4   …
4  4   4   …
5−     5 +  …
5+     5 +  …
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  fu…
8  7   8  fu…
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  fu…
12 11  12  fu…
13− 12    -  …
13+ 12    -  …
14−    13 +  …
14+    13 +  …
15−    14 +  …
15+    14 +  …
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf014.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  imp…
3  3   3  imp…
4  4   4   …
4  4   4   …
5−     5 +  …
5+     5 +  …
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  fun…
8  7   8  fun…
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  fun…
12 11  12  fun…
13− 12    -  …
13+ 12    -  …
14−    13 +  …
14+    13 +  …
15−    14 +  …
15+    14 +  …
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf015.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  impo…
3  3   3  impo…
4  4   4   …
4  4   4   …
5−     5 +  …
5+     5 +  …
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func…
8  7   8  func…
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func…
12 11  12  func…
13− 12    -  …
13+ 12    -  …
14−    13 +  …
14+    13 +  …
15−    14 +  …
15+    14 +  …
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf016.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  impor…
3  3   3  impor…
4  4   4   "…
4  4   4   "…
5−     5 +  "…
5+     5 +  "…
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func …
8  7   8  func …
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func …
12 11  12  func …
13− 12    -  r…
13+ 12    -  r…
14−    13 +  c…
14+    13 +  c…
15−    14 +  r…
15+    14 +  r…
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf017.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import…
3  3   3  import…
4  4   4   "f…
4  4   4   "f…
5−     5 +  "s…
5+     5 +  "s…
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func m…
8  7   8  func m…
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func g…
12 11  12  func g…
13− 12    -  re…
13+ 12    -  re…
14−    13 +  co…
14+    13 +  co…
15−    14 +  re…
15+    14 +  re…
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf018.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import (
3  3   3  import (
4  4   4   "fm…
4  4   4   "fm…
5−     5 +  "st…
5+     5 +  "st…
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func ma…
8  7   8  func ma…
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func ge…
12 11  12  func ge…
13− 12    -  ret…
13+ 12    -  ret…
14−    13 +  con…
14+    13 +  con…
15−    14 +  ret…
15+    14 +  ret…
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf019.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt"
4  4   4   "fmt"
5−     5 +  "str…
5+     5 +  "str…
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func mai…
8  7   8  func mai…
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func get…
12 11  12  func get…
13− 12    -  retu…
13+ 12    -  retu…
14−    13 +  cont…
14+    13 +  cont…
15−    14 +  retu…
15+    14 +  retu…
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf020.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "stri…
5+     5 +  "stri…
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main…
8  7   8  func main…
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getC…
12 11  12  func getC…
13− 12    -  retur…
13+ 12    -  retur…
14−    13 +  conte…
14+    13 +  conte…
15−    14 +  retur…
15+    14 +  retur…
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf021.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strin…
5+     5 +  "strin…
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main(…
8  7   8  func main(…
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getCo…
12 11  12  func getCo…
13− 12    -  return…
13+ 12    -  return…
14−    13 +  conten…
14+    13 +  conten…
15−    14 +  return…
15+    14 +  return…
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf022.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "string…
5+     5 +  "string…
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main()…
8  7   8  func main()…
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getCon…
12 11  12  func getCon…
13− 12    -  return …
13+ 12    -  return …
14−    13 +  content…
14+    13 +  content…
15−    14 +  return …
15+    14 +  return …
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf023.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings"
5+     5 +  "strings"
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() {
8  7   8  func main() {
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getCont…
12 11  12  func getCont…
13− 12    -  return "…
13+ 12    -  return "…
14−    13 +  content …
14+    13 +  content …
15−    14 +  return c…
15+    14 +  return c…
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf024.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getConte…
12 11  12  func getConte…
13− 12    -  return "H…
13+ 12    -  return "H…
14−    13 +  content :…
14+    13 +  content :…
15−    14 +  return co…
15+    14 +  return co…
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf025.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getConten…
12 11  12  func getConten…
13− 12    -  return "He…
13+ 12    -  return "He…
14−    13 +  content :=…
14+    13 +  content :=…
15−    14 +  return con…
15+    14 +  return con…
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf026.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent…
12 11  12  func getContent…
13− 12    -  return "Hel…
13+ 12    -  return "Hel…
14−    13 +  content := …
14+    13 +  content := …
15−    14 +  return cont…
15+    14 +  return cont…
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf027.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent(…
12 11  12  func getContent(…
13− 12    -  return "Hell…
13+ 12    -  return "Hell…
14−    13 +  content := s…
14+    13 +  content := s…
15−    14 +  return conte…
15+    14 +  return conte…
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf028.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent()…
12 11  12  func getContent()…
13− 12    -  return "Hello…
13+ 12    -  return "Hello…
14−    13 +  content := st…
14+    13 +  content := st…
15−    14 +  return content
15+    14 +  return content
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf029.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() …
12 11  12  func getContent() …
13− 12    -  return "Hello,…
13+ 12    -  return "Hello,…
14−    13 +  content := str…
14+    13 +  content := str…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf030.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() s…
12 11  12  func getContent() s…
13− 12    -  return "Hello, …
13+ 12    -  return "Hello, …
14−    13 +  content := stri…
14+    13 +  content := stri…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf031.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() st…
12 11  12  func getContent() st…
13− 12    -  return "Hello, w…
13+ 12    -  return "Hello, w…
14−    13 +  content := strin…
14+    13 +  content := strin…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf032.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() str…
12 11  12  func getContent() str…
13− 12    -  return "Hello, wo…
13+ 12    -  return "Hello, wo…
14−    13 +  content := string…
14+    13 +  content := string…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf033.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() stri…
12 11  12  func getContent() stri…
13− 12    -  return "Hello, wor…
13+ 12    -  return "Hello, wor…
14−    13 +  content := strings…
14+    13 +  content := strings…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf034.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() strin…
12 11  12  func getContent() strin…
13− 12    -  return "Hello, worl…
13+ 12    -  return "Hello, worl…
14−    13 +  content := strings.…
14+    13 +  content := strings.…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf035.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string…
12 11  12  func getContent() string…
13− 12    -  return "Hello, world…
13+ 12    -  return "Hello, world…
14−    13 +  content := strings.T…
14+    13 +  content := strings.T…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf036.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string {
12 11  12  func getContent() string {
13− 12    -  return "Hello, world!"
13+ 12    -  return "Hello, world!"
14−    13 +  content := strings.To…
14+    13 +  content := strings.To…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf037.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToU…
14+    13 +  content := strings.ToU…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf038.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUp…
14+    13 +  content := strings.ToUp…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf039.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpp…
14+    13 +  content := strings.ToUpp…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf040.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUppe…
14+    13 +  content := strings.ToUppe…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf041.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper…
14+    13 +  content := strings.ToUpper…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf042.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper(…
14+    13 +  content := strings.ToUpper(…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf043.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("…
14+    13 +  content := strings.ToUpper("…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf044.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("H…
14+    13 +  content := strings.ToUpper("H…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf045.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("He…
14+    13 +  content := strings.ToUpper("He…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf046.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hel…
14+    13 +  content := strings.ToUpper("Hel…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf047.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hell…
14+    13 +  content := strings.ToUpper("Hell…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf048.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello…
14+    13 +  content := strings.ToUpper("Hello…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf049.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello,…
14+    13 +  content := strings.ToUpper("Hello,…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf050.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, …
14+    13 +  content := strings.ToUpper("Hello, …
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf051.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, W…
14+    13 +  content := strings.ToUpper("Hello, W…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf052.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, Wo…
14+    13 +  content := strings.ToUpper("Hello, Wo…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf053.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, Wor…
14+    13 +  content := strings.ToUpper("Hello, Wor…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf054.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, Worl…
14+    13 +  content := strings.ToUpper("Hello, Worl…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf055.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World…
14+    13 +  content := strings.ToUpper("Hello, World…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf056.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World!…
14+    13 +  content := strings.ToUpper("Hello, World!…
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf057.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World!")
14+    13 +  content := strings.ToUpper("Hello, World!")
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf058.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World!") 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf059.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World!") 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewWidth/Unified/WidthOf060.golden+4−4
@@ -2,7 +2,7 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−     5 +  "strings" 
5+     5 +  "strings" 
6  5   6  ) 
6  5   6  ) 
7  6   7   
7  6   7   
8  7   8  func main() { 
8  7   8  func main() { 
@@ -10,7 +10,7 @@
10  9  10  } 
10  9  10  } 
11 10  11   
11 10  11   
12 11  12  func getContent() string { 
12 11  12  func getContent() string { 
13− 12    -  return "Hello, world!" 
13+ 12    -  return "Hello, world!" 
14−    13 +  content := strings.ToUpper("Hello, World!") 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15−    14 +  return content 
15+    14 +  return content 
16 13  15  } 
16 13  15  } 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf00.golden+2−2
@@ -2,6 +2,6 @@
2  5  )   5  ) 
2  5  )   5  ) 
3  6     6   
3  6     6   
4  7  func main() {   7  func main() { 
4  7  func main() {   7  func main() { 
5−  8 -  fmt.Println("Hello,…  8 +  content := "Hello, …
5+  8 -  fmt.Println("Hello,…  8 +  content := "Hello, …
6−       9 +  fmt.Println(content)
6+       9 +  fmt.Println(content)
7  9  }  10  } 
7  9  }  10  } 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf01.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  …unc main() {   7  …unc main() { 
4  7  …unc main() {   7  …unc main() { 
5−  8 -… fmt.Println("Hello, …  8 +… content := "Hello, w…
5+  8 -… fmt.Println("Hello, …  8 +… content := "Hello, w…
6−       9 +… fmt.Println(content) 
6+       9 +… fmt.Println(content) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf02.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  …nc main() {   7  …nc main() { 
4  7  …nc main() {   7  …nc main() { 
5−  8 -… fmt.Println("Hello, w…  8 +… content := "Hello, wo…
5+  8 -… fmt.Println("Hello, w…  8 +… content := "Hello, wo…
6−       9 +… fmt.Println(content) 
6+       9 +… fmt.Println(content) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf03.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  …c main() {   7  …c main() { 
4  7  …c main() {   7  …c main() { 
5−  8 -… fmt.Println("Hello, wo…  8 +… content := "Hello, wor…
5+  8 -… fmt.Println("Hello, wo…  8 +… content := "Hello, wor…
6−       9 +… fmt.Println(content) 
6+       9 +… fmt.Println(content) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf04.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  … main() {   7  … main() { 
4  7  … main() {   7  … main() { 
5−  8 -…fmt.Println("Hello, wor…  8 +…content := "Hello, worl…
5+  8 -…fmt.Println("Hello, wor…  8 +…content := "Hello, worl…
6−       9 +…fmt.Println(content) 
6+       9 +…fmt.Println(content) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf05.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  …main() {   7  …main() { 
4  7  …main() {   7  …main() { 
5−  8 -…mt.Println("Hello, worl…  8 +…ontent := "Hello, world…
5+  8 -…mt.Println("Hello, worl…  8 +…ontent := "Hello, world…
6−       9 +…mt.Println(content) 
6+       9 +…mt.Println(content) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf06.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  …ain() {   7  …ain() { 
4  7  …ain() {   7  …ain() { 
5−  8 -…t.Println("Hello, world…  8 +…ntent := "Hello, world!"
5+  8 -…t.Println("Hello, world…  8 +…ntent := "Hello, world!"
6−       9 +…t.Println(content) 
6+       9 +…t.Println(content) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf07.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  …in() {   7  …in() { 
4  7  …in() {   7  …in() { 
5−  8 -….Println("Hello, world!…  8 +…tent := "Hello, world!" 
5+  8 -….Println("Hello, world!…  8 +…tent := "Hello, world!" 
6−       9 +….Println(content) 
6+       9 +….Println(content) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf08.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  …n() {   7  …n() { 
4  7  …n() {   7  …n() { 
5−  8 -…Println("Hello, world!")  8 +…ent := "Hello, world!" 
5+  8 -…Println("Hello, world!")  8 +…ent := "Hello, world!" 
6−       9 +…Println(content) 
6+       9 +…Println(content) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf09.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  …() {   7  …() { 
4  7  …() {   7  …() { 
5−  8 -…rintln("Hello, world!")   8 +…nt := "Hello, world!" 
5+  8 -…rintln("Hello, world!")   8 +…nt := "Hello, world!" 
6−       9 +…rintln(content) 
6+       9 +…rintln(content) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf10.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  …) {   7  …) { 
4  7  …) {   7  …) { 
5−  8 -…intln("Hello, world!")   8 +…t := "Hello, world!" 
5+  8 -…intln("Hello, world!")   8 +…t := "Hello, world!" 
6−       9 +…intln(content) 
6+       9 +…intln(content) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf11.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  … {   7  … { 
4  7  … {   7  … { 
5−  8 -…ntln("Hello, world!")   8 +… := "Hello, world!" 
5+  8 -…ntln("Hello, world!")   8 +… := "Hello, world!" 
6−       9 +…ntln(content) 
6+       9 +…ntln(content) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf12.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  …{   7  …{ 
4  7  …{   7  …{ 
5−  8 -…tln("Hello, world!")   8 +…:= "Hello, world!" 
5+  8 -…tln("Hello, world!")   8 +…:= "Hello, world!" 
6−       9 +…tln(content) 
6+       9 +…tln(content) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf13.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  …   7  … 
4  7  …   7  … 
5−  8 -…ln("Hello, world!")   8 +…= "Hello, world!" 
5+  8 -…ln("Hello, world!")   8 +…= "Hello, world!" 
6−       9 +…ln(content) 
6+       9 +…ln(content) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf14.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  …   7  … 
4  7  …   7  … 
5−  8 -…n("Hello, world!")   8 +… "Hello, world!" 
5+  8 -…n("Hello, world!")   8 +… "Hello, world!" 
6−       9 +…n(content) 
6+       9 +…n(content) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf15.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  …   7  … 
4  7  …   7  … 
5−  8 -…("Hello, world!")   8 +…"Hello, world!" 
5+  8 -…("Hello, world!")   8 +…"Hello, world!" 
6−       9 +…(content) 
6+       9 +…(content) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf16.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  …   7  … 
4  7  …   7  … 
5−  8 -…"Hello, world!")   8 +…Hello, world!" 
5+  8 -…"Hello, world!")   8 +…Hello, world!" 
6−       9 +…content) 
6+       9 +…content) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf17.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  …   7  … 
4  7  …   7  … 
5−  8 -…Hello, world!")   8 +…ello, world!" 
5+  8 -…Hello, world!")   8 +…ello, world!" 
6−       9 +…ontent) 
6+       9 +…ontent) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf18.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  …   7  … 
4  7  …   7  … 
5−  8 -…ello, world!")   8 +…llo, world!" 
5+  8 -…ello, world!")   8 +…llo, world!" 
6−       9 +…ntent) 
6+       9 +…ntent) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf19.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  …   7  … 
4  7  …   7  … 
5−  8 -…llo, world!")   8 +…lo, world!" 
5+  8 -…llo, world!")   8 +…lo, world!" 
6−       9 +…tent) 
6+       9 +…tent) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf20.golden+2−2
@@ -2,6 +2,6 @@
2  5  …   5  … 
2  5  …   5  … 
3  6     6   
3  6     6   
4  7  …   7  … 
4  7  …   7  … 
5−  8 -…lo, world!")   8 +…o, world!" 
5+  8 -…lo, world!")   8 +…o, world!" 
6−       9 +…ent) 
6+       9 +…ent) 
7  9  …  10  … 
7  9  …  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf00.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  ) 
2  5   5  ) 
3  6   6   
3  6   6   
4  7   7  func main() { 
4  7   7  func main() { 
5−  8    -  fmt.Println("Hello, world!") 
5+  8    -  fmt.Println("Hello, world!") 
6−     8 +  content := "Hello, world!" 
6+     8 +  content := "Hello, world!" 
7−     9 +  fmt.Println(content) 
7+     9 +  fmt.Println(content) 
8  9  10  } 
8  9  10  } 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf01.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  …unc main() { 
4  7   7  …unc main() { 
5−  8    -… fmt.Println("Hello, world!") 
5+  8    -… fmt.Println("Hello, world!") 
6−     8 +… content := "Hello, world!" 
6+     8 +… content := "Hello, world!" 
7−     9 +… fmt.Println(content) 
7+     9 +… fmt.Println(content) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf02.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  …nc main() { 
4  7   7  …nc main() { 
5−  8    -… fmt.Println("Hello, world!") 
5+  8    -… fmt.Println("Hello, world!") 
6−     8 +… content := "Hello, world!" 
6+     8 +… content := "Hello, world!" 
7−     9 +… fmt.Println(content) 
7+     9 +… fmt.Println(content) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf03.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  …c main() { 
4  7   7  …c main() { 
5−  8    -… fmt.Println("Hello, world!") 
5+  8    -… fmt.Println("Hello, world!") 
6−     8 +… content := "Hello, world!" 
6+     8 +… content := "Hello, world!" 
7−     9 +… fmt.Println(content) 
7+     9 +… fmt.Println(content) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf04.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  … main() { 
4  7   7  … main() { 
5−  8    -…fmt.Println("Hello, world!") 
5+  8    -…fmt.Println("Hello, world!") 
6−     8 +…content := "Hello, world!" 
6+     8 +…content := "Hello, world!" 
7−     9 +…fmt.Println(content) 
7+     9 +…fmt.Println(content) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf05.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  …main() { 
4  7   7  …main() { 
5−  8    -…mt.Println("Hello, world!") 
5+  8    -…mt.Println("Hello, world!") 
6−     8 +…ontent := "Hello, world!" 
6+     8 +…ontent := "Hello, world!" 
7−     9 +…mt.Println(content) 
7+     9 +…mt.Println(content) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf06.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  …ain() { 
4  7   7  …ain() { 
5−  8    -…t.Println("Hello, world!") 
5+  8    -…t.Println("Hello, world!") 
6−     8 +…ntent := "Hello, world!" 
6+     8 +…ntent := "Hello, world!" 
7−     9 +…t.Println(content) 
7+     9 +…t.Println(content) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf07.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  …in() { 
4  7   7  …in() { 
5−  8    -….Println("Hello, world!") 
5+  8    -….Println("Hello, world!") 
6−     8 +…tent := "Hello, world!" 
6+     8 +…tent := "Hello, world!" 
7−     9 +….Println(content) 
7+     9 +….Println(content) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf08.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  …n() { 
4  7   7  …n() { 
5−  8    -…Println("Hello, world!") 
5+  8    -…Println("Hello, world!") 
6−     8 +…ent := "Hello, world!" 
6+     8 +…ent := "Hello, world!" 
7−     9 +…Println(content) 
7+     9 +…Println(content) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf09.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  …() { 
4  7   7  …() { 
5−  8    -…rintln("Hello, world!") 
5+  8    -…rintln("Hello, world!") 
6−     8 +…nt := "Hello, world!" 
6+     8 +…nt := "Hello, world!" 
7−     9 +…rintln(content) 
7+     9 +…rintln(content) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf10.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  …) { 
4  7   7  …) { 
5−  8    -…intln("Hello, world!") 
5+  8    -…intln("Hello, world!") 
6−     8 +…t := "Hello, world!" 
6+     8 +…t := "Hello, world!" 
7−     9 +…intln(content) 
7+     9 +…intln(content) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf11.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  … { 
4  7   7  … { 
5−  8    -…ntln("Hello, world!") 
5+  8    -…ntln("Hello, world!") 
6−     8 +… := "Hello, world!" 
6+     8 +… := "Hello, world!" 
7−     9 +…ntln(content) 
7+     9 +…ntln(content) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf12.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  …{ 
4  7   7  …{ 
5−  8    -…tln("Hello, world!") 
5+  8    -…tln("Hello, world!") 
6−     8 +…:= "Hello, world!" 
6+     8 +…:= "Hello, world!" 
7−     9 +…tln(content) 
7+     9 +…tln(content) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf13.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  … 
4  7   7  … 
5−  8    -…ln("Hello, world!") 
5+  8    -…ln("Hello, world!") 
6−     8 +…= "Hello, world!" 
6+     8 +…= "Hello, world!" 
7−     9 +…ln(content) 
7+     9 +…ln(content) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf14.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  … 
4  7   7  … 
5−  8    -…n("Hello, world!") 
5+  8    -…n("Hello, world!") 
6−     8 +… "Hello, world!" 
6+     8 +… "Hello, world!" 
7−     9 +…n(content) 
7+     9 +…n(content) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf15.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  … 
4  7   7  … 
5−  8    -…("Hello, world!") 
5+  8    -…("Hello, world!") 
6−     8 +…"Hello, world!" 
6+     8 +…"Hello, world!" 
7−     9 +…(content) 
7+     9 +…(content) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf16.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  … 
4  7   7  … 
5−  8    -…"Hello, world!") 
5+  8    -…"Hello, world!") 
6−     8 +…Hello, world!" 
6+     8 +…Hello, world!" 
7−     9 +…content) 
7+     9 +…content) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf17.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  … 
4  7   7  … 
5−  8    -…Hello, world!") 
5+  8    -…Hello, world!") 
6−     8 +…ello, world!" 
6+     8 +…ello, world!" 
7−     9 +…ontent) 
7+     9 +…ontent) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf18.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  … 
4  7   7  … 
5−  8    -…ello, world!") 
5+  8    -…ello, world!") 
6−     8 +…llo, world!" 
6+     8 +…llo, world!" 
7−     9 +…ntent) 
7+     9 +…ntent) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf19.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  … 
4  7   7  … 
5−  8    -…llo, world!") 
5+  8    -…llo, world!") 
6−     8 +…lo, world!" 
6+     8 +…lo, world!" 
7−     9 +…tent) 
7+     9 +…tent) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf20.golden+3−3
@@ -2,7 +2,7 @@
2  5   5  … 
2  5   5  … 
3  6   6   
3  6   6   
4  7   7  … 
4  7   7  … 
5−  8    -…lo, world!") 
5+  8    -…lo, world!") 
6−     8 +…o, world!" 
6+     8 +…o, world!" 
7−     9 +…ent) 
7+     9 +…ent) 
8  9  10  … 
8  9  10  … 
internal/ui/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf00.golden+1−1
@@ -2,4 +2,4 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−  …  …   …  … 
5+  …  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf01.golden+1−1
@@ -1,5 +1,5 @@
1  2     2   
1  2     2   
2  3  import (   3  import ( 
2  3  import (   3  import ( 
3  4   "fmt"   4   "fmt" 
3  4   "fmt"   4   "fmt" 
4−       5 +  "strings" 
4+       5 +  "strings" 
5  …  …   …  … 
5  …  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf02.golden+1−1
@@ -1,5 +1,5 @@
1  3  import (   3  import ( 
1  3  import (   3  import ( 
2  4   "fmt"   4   "fmt" 
2  4   "fmt"   4   "fmt" 
3−       5 +  "strings" 
3+       5 +  "strings" 
4  5  )   6  ) 
4  5  )   6  ) 
5  …  …   …  … 
5  …  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf03.golden+1−1
@@ -1,5 +1,5 @@
1  4   "fmt"   4   "fmt" 
1  4   "fmt"   4   "fmt" 
2−       5 +  "strings" 
2+       5 +  "strings" 
3  5  )   6  ) 
3  5  )   6  ) 
4  6     7   
4  6     7   
5  …  …   …  … 
5  …  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf04.golden+1−1
@@ -1,4 +1,4 @@
1−       5 +  "strings" 
1+       5 +  "strings" 
2  5  )   6  ) 
2  5  )   6  ) 
3  6     7   
3  6     7   
4  7  func main() {   8  func main() { 
4  7  func main() {   8  func main() { 
internal/ui/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf08.golden+1−1
@@ -2,4 +2,4 @@
2  9  }  10  } 
2  9  }  10  } 
3 10    11   
3 10    11   
4 11  func getContent() string {  12  func getContent() string { 
4 11  func getContent() string {  12  func getContent() string { 
5−  …  …   …  … 
5+  …  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf09.golden+2−2
@@ -1,5 +1,5 @@
1  9  }  10  } 
1  9  }  10  } 
2 10    11   
2 10    11   
3 11  func getContent() string {  12  func getContent() string { 
3 11  func getContent() string {  12  func getContent() string { 
4− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
4+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
5−  …  …   …  … 
5+  …  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf10.golden+2−2
@@ -1,5 +1,5 @@
1 10    11   
1 10    11   
2 11  func getContent() string {  12  func getContent() string { 
2 11  func getContent() string {  12  func getContent() string { 
3− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
3+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
4−      14 +  return content 
4+      14 +  return content 
5 13  }  15  } 
5 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf11.golden+2−2
@@ -1,5 +1,5 @@
1 10    11   
1 10    11   
2 11  func getContent() string {  12  func getContent() string { 
2 11  func getContent() string {  12  func getContent() string { 
3− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
3+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
4−      14 +  return content 
4+      14 +  return content 
5 13  }  15  } 
5 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf12.golden+2−2
@@ -1,5 +1,5 @@
1 10    11   
1 10    11   
2 11  func getContent() string {  12  func getContent() string { 
2 11  func getContent() string {  12  func getContent() string { 
3− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
3+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
4−      14 +  return content 
4+      14 +  return content 
5 13  }  15  } 
5 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf13.golden+2−2
@@ -1,5 +1,5 @@
1 10    11   
1 10    11   
2 11  func getContent() string {  12  func getContent() string { 
2 11  func getContent() string {  12  func getContent() string { 
3− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
3+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
4−      14 +  return content 
4+      14 +  return content 
5 13  }  15  } 
5 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf14.golden+2−2
@@ -1,5 +1,5 @@
1 10    11   
1 10    11   
2 11  func getContent() string {  12  func getContent() string { 
2 11  func getContent() string {  12  func getContent() string { 
3− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
3+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
4−      14 +  return content 
4+      14 +  return content 
5 13  }  15  } 
5 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf15.golden+2−2
@@ -1,5 +1,5 @@
1 10    11   
1 10    11   
2 11  func getContent() string {  12  func getContent() string { 
2 11  func getContent() string {  12  func getContent() string { 
3− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
3+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
4−      14 +  return content 
4+      14 +  return content 
5 13  }  15  } 
5 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf16.golden+2−2
@@ -1,5 +1,5 @@
1 10    11   
1 10    11   
2 11  func getContent() string {  12  func getContent() string { 
2 11  func getContent() string {  12  func getContent() string { 
3− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
3+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
4−      14 +  return content 
4+      14 +  return content 
5 13  }  15  } 
5 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf00.golden+1−1
@@ -2,4 +2,4 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−  …   …  … 
5+  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf01.golden+1−1
@@ -1,5 +1,5 @@
1  2   2   
1  2   2   
2  3   3  import ( 
2  3   3  import ( 
3  4   4   "fmt" 
3  4   4   "fmt" 
4−     5 +  "strings" 
4+     5 +  "strings" 
5  …   …  … 
5  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf02.golden+1−1
@@ -1,5 +1,5 @@
1  3   3  import ( 
1  3   3  import ( 
2  4   4   "fmt" 
2  4   4   "fmt" 
3−     5 +  "strings" 
3+     5 +  "strings" 
4  5   6  ) 
4  5   6  ) 
5  …   …  … 
5  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf03.golden+1−1
@@ -1,5 +1,5 @@
1  4   4   "fmt" 
1  4   4   "fmt" 
2−     5 +  "strings" 
2+     5 +  "strings" 
3  5   6  ) 
3  5   6  ) 
4  6   7   
4  6   7   
5  …   …  … 
5  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf04.golden+1−1
@@ -1,4 +1,4 @@
1−     5 +  "strings" 
1+     5 +  "strings" 
2  5   6  ) 
2  5   6  ) 
3  6   7   
3  6   7   
4  7   8  func main() { 
4  7   8  func main() { 
internal/ui/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf08.golden+1−1
@@ -2,4 +2,4 @@
2  9  10  } 
2  9  10  } 
3 10  11   
3 10  11   
4 11  12  func getContent() string { 
4 11  12  func getContent() string { 
5−  …   …  … 
5+  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf09.golden+2−2
@@ -1,5 +1,5 @@
1  9  10  } 
1  9  10  } 
2 10  11   
2 10  11   
3 11  12  func getContent() string { 
3 11  12  func getContent() string { 
4− 12    -  return "Hello, world!" 
4+ 12    -  return "Hello, world!" 
5−  …   …  … 
5+  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf10.golden+3−3
@@ -1,5 +1,5 @@
1 10  11   
1 10  11   
2 11  12  func getContent() string { 
2 11  12  func getContent() string { 
3− 12    -  return "Hello, world!" 
3+ 12    -  return "Hello, world!" 
4−    13 +  content := strings.ToUpper("Hello, World!") 
4+    13 +  content := strings.ToUpper("Hello, World!") 
5−  …   …  … 
5+  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf11.golden+3−3
@@ -1,5 +1,5 @@
1 11  12  func getContent() string { 
1 11  12  func getContent() string { 
2− 12    -  return "Hello, world!" 
2+ 12    -  return "Hello, world!" 
3−    13 +  content := strings.ToUpper("Hello, World!") 
3+    13 +  content := strings.ToUpper("Hello, World!") 
4−    14 +  return content 
4+    14 +  return content 
5 13  15  } 
5 13  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf12.golden+3−3
@@ -1,5 +1,5 @@
1 11  12  func getContent() string { 
1 11  12  func getContent() string { 
2− 12    -  return "Hello, world!" 
2+ 12    -  return "Hello, world!" 
3−    13 +  content := strings.ToUpper("Hello, World!") 
3+    13 +  content := strings.ToUpper("Hello, World!") 
4−    14 +  return content 
4+    14 +  return content 
5 13  15  } 
5 13  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf13.golden+3−3
@@ -1,5 +1,5 @@
1 11  12  func getContent() string { 
1 11  12  func getContent() string { 
2− 12    -  return "Hello, world!" 
2+ 12    -  return "Hello, world!" 
3−    13 +  content := strings.ToUpper("Hello, World!") 
3+    13 +  content := strings.ToUpper("Hello, World!") 
4−    14 +  return content 
4+    14 +  return content 
5 13  15  } 
5 13  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf14.golden+3−3
@@ -1,5 +1,5 @@
1 11  12  func getContent() string { 
1 11  12  func getContent() string { 
2− 12    -  return "Hello, world!" 
2+ 12    -  return "Hello, world!" 
3−    13 +  content := strings.ToUpper("Hello, World!") 
3+    13 +  content := strings.ToUpper("Hello, World!") 
4−    14 +  return content 
4+    14 +  return content 
5 13  15  } 
5 13  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf15.golden+3−3
@@ -1,5 +1,5 @@
1 11  12  func getContent() string { 
1 11  12  func getContent() string { 
2− 12    -  return "Hello, world!" 
2+ 12    -  return "Hello, world!" 
3−    13 +  content := strings.ToUpper("Hello, World!") 
3+    13 +  content := strings.ToUpper("Hello, World!") 
4−    14 +  return content 
4+    14 +  return content 
5 13  15  } 
5 13  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf16.golden+3−3
@@ -1,5 +1,5 @@
1 11  12  func getContent() string { 
1 11  12  func getContent() string { 
2− 12    -  return "Hello, world!" 
2+ 12    -  return "Hello, world!" 
3−    13 +  content := strings.ToUpper("Hello, World!") 
3+    13 +  content := strings.ToUpper("Hello, World!") 
4−    14 +  return content 
4+    14 +  return content 
5 13  15  } 
5 13  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf00.golden+1−1
@@ -2,4 +2,4 @@
2  2     2   
2  2     2   
3  3  import (   3  import ( 
3  3  import (   3  import ( 
4  4   "fmt"   4   "fmt" 
4  4   "fmt"   4   "fmt" 
5−  …  …   …  … 
5+  …  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf01.golden+1−1
@@ -1,5 +1,5 @@
1  2     2   
1  2     2   
2  3  import (   3  import ( 
2  3  import (   3  import ( 
3  4   "fmt"   4   "fmt" 
3  4   "fmt"   4   "fmt" 
4−       5 +  "strings" 
4+       5 +  "strings" 
5  …  …   …  … 
5  …  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf02.golden+1−1
@@ -1,5 +1,5 @@
1  3  import (   3  import ( 
1  3  import (   3  import ( 
2  4   "fmt"   4   "fmt" 
2  4   "fmt"   4   "fmt" 
3−       5 +  "strings" 
3+       5 +  "strings" 
4  5  )   6  ) 
4  5  )   6  ) 
5  …  …   …  … 
5  …  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf03.golden+1−1
@@ -1,5 +1,5 @@
1  4   "fmt"   4   "fmt" 
1  4   "fmt"   4   "fmt" 
2−       5 +  "strings" 
2+       5 +  "strings" 
3  5  )   6  ) 
3  5  )   6  ) 
4  6     7   
4  6     7   
5  …  …   …  … 
5  …  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf04.golden+1−1
@@ -1,4 +1,4 @@
1−       5 +  "strings" 
1+       5 +  "strings" 
2  5  )   6  ) 
2  5  )   6  ) 
3  6     7   
3  6     7   
4  7  func main() {   8  func main() { 
4  7  func main() {   8  func main() { 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf08.golden+1−1
@@ -2,4 +2,4 @@
2  9  }  10  } 
2  9  }  10  } 
3 10    11   
3 10    11   
4 11  func getContent() string {  12  func getContent() string { 
4 11  func getContent() string {  12  func getContent() string { 
5−  …  …   …  … 
5+  …  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf09.golden+2−2
@@ -1,5 +1,5 @@
1  9  }  10  } 
1  9  }  10  } 
2 10    11   
2 10    11   
3 11  func getContent() string {  12  func getContent() string { 
3 11  func getContent() string {  12  func getContent() string { 
4− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
4+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
5−  …  …   …  … 
5+  …  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf10.golden+2−2
@@ -1,5 +1,5 @@
1 10    11   
1 10    11   
2 11  func getContent() string {  12  func getContent() string { 
2 11  func getContent() string {  12  func getContent() string { 
3− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
3+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
4−      14 +  return content 
4+      14 +  return content 
5 13  }  15  } 
5 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf11.golden+2−2
@@ -1,4 +1,4 @@
1 11  func getContent() string {  12  func getContent() string { 
1 11  func getContent() string {  12  func getContent() string { 
2− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
2+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
3−      14 +  return content 
3+      14 +  return content 
4 13  }  15  } 
4 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf12.golden+2−2
@@ -1,3 +1,3 @@
1− 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
1+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
2−      14 +  return content 
2+      14 +  return content 
3 13  }  15  } 
3 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf13.golden+1−1
@@ -1,2 +1,2 @@
1−      14 +  return content 
1+      14 +  return content 
2 13  }  15  } 
2 13  }  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf00.golden+1−1
@@ -2,4 +2,4 @@
2  2   2   
2  2   2   
3  3   3  import ( 
3  3   3  import ( 
4  4   4   "fmt" 
4  4   4   "fmt" 
5−  …   …  … 
5+  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf01.golden+1−1
@@ -1,5 +1,5 @@
1  2   2   
1  2   2   
2  3   3  import ( 
2  3   3  import ( 
3  4   4   "fmt" 
3  4   4   "fmt" 
4−     5 +  "strings" 
4+     5 +  "strings" 
5  …   …  … 
5  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf02.golden+1−1
@@ -1,5 +1,5 @@
1  3   3  import ( 
1  3   3  import ( 
2  4   4   "fmt" 
2  4   4   "fmt" 
3−     5 +  "strings" 
3+     5 +  "strings" 
4  5   6  ) 
4  5   6  ) 
5  …   …  … 
5  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf03.golden+1−1
@@ -1,5 +1,5 @@
1  4   4   "fmt" 
1  4   4   "fmt" 
2−     5 +  "strings" 
2+     5 +  "strings" 
3  5   6  ) 
3  5   6  ) 
4  6   7   
4  6   7   
5  …   …  … 
5  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf04.golden+1−1
@@ -1,4 +1,4 @@
1−     5 +  "strings" 
1+     5 +  "strings" 
2  5   6  ) 
2  5   6  ) 
3  6   7   
3  6   7   
4  7   8  func main() { 
4  7   8  func main() { 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf08.golden+1−1
@@ -2,4 +2,4 @@
2  9  10  } 
2  9  10  } 
3 10  11   
3 10  11   
4 11  12  func getContent() string { 
4 11  12  func getContent() string { 
5−  …   …  … 
5+  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf09.golden+2−2
@@ -1,5 +1,5 @@
1  9  10  } 
1  9  10  } 
2 10  11   
2 10  11   
3 11  12  func getContent() string { 
3 11  12  func getContent() string { 
4− 12    -  return "Hello, world!" 
4+ 12    -  return "Hello, world!" 
5−  …   …  … 
5+  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf10.golden+3−3
@@ -1,5 +1,5 @@
1 10  11   
1 10  11   
2 11  12  func getContent() string { 
2 11  12  func getContent() string { 
3− 12    -  return "Hello, world!" 
3+ 12    -  return "Hello, world!" 
4−    13 +  content := strings.ToUpper("Hello, World!") 
4+    13 +  content := strings.ToUpper("Hello, World!") 
5−  …   …  … 
5+  …   …  … 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf11.golden+3−3
@@ -1,5 +1,5 @@
1 11  12  func getContent() string { 
1 11  12  func getContent() string { 
2− 12    -  return "Hello, world!" 
2+ 12    -  return "Hello, world!" 
3−    13 +  content := strings.ToUpper("Hello, World!") 
3+    13 +  content := strings.ToUpper("Hello, World!") 
4−    14 +  return content 
4+    14 +  return content 
5 13  15  } 
5 13  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf12.golden+3−3
@@ -1,4 +1,4 @@
1− 12    -  return "Hello, world!" 
1+ 12    -  return "Hello, world!" 
2−    13 +  content := strings.ToUpper("Hello, World!") 
2+    13 +  content := strings.ToUpper("Hello, World!") 
3−    14 +  return content 
3+    14 +  return content 
4 13  15  } 
4 13  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf13.golden+2−2
@@ -1,3 +1,3 @@
1−    13 +  content := strings.ToUpper("Hello, World!") 
1+    13 +  content := strings.ToUpper("Hello, World!") 
2−    14 +  return content 
2+    14 +  return content 
3 13  15  } 
3 13  15  } 
internal/ui/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf14.golden+1−1
@@ -1,2 +1,2 @@
1−    14 +  return content 
1+    14 +  return content 
2 13  15  } 
2 13  15  } 
internal/ui/diffview/util.go+1−8
@@ -2,20 +2,13 @@ package diffview
2
2
3import (
3import (
4 "fmt"
4 "fmt"
5− "strconv"
6 "strings"
5 "strings"
7
6
8 "github.com/charmbracelet/x/ansi"
7 "github.com/charmbracelet/x/ansi"
9)
8)
10
9
11func pad(v any, width int) string {
10func pad(v any, width int) string {
12− var s string
11+ s := fmt.Sprintf("%v", v)
13− switch n := v.(type) {
14− case int:
15− s = strconv.Itoa(n)
16− default:
17− s = fmt.Sprintf("%v", v)
18− }
19 w := ansi.StringWidth(s)
12 w := ansi.StringWidth(s)
20 if w >= width {
13 if w >= width {
21 return s
14 return s
internal/ui/list/filterable.go+1−33
@@ -23,14 +23,6 @@ type FilterableList struct {
23 *List
23 *List
24 items []FilterableItem
24 items []FilterableItem
25 query string
25 query string
26− // dirty tracks whether the filtered set needs to be recomputed. It is set
27− // by any mutation of items or query and cleared after Render recomputes.
28− dirty bool
29−
30− // emptyMessage is shown when there are no items and no active filter.
31− emptyMessage string
32− // filterEmptyMessage is shown when a filter is active but matches nothing.
33− filterEmptyMessage string
34}
26}
35
27
36// NewFilterableList creates a new filterable list.
28// NewFilterableList creates a new filterable list.
@@ -47,7 +39,6 @@ func NewFilterableList(items ...FilterableItem) *FilterableList {
47// SetItems sets the list items and updates the filtered items.
39// SetItems sets the list items and updates the filtered items.
48func (f *FilterableList) SetItems(items ...FilterableItem) {
40func (f *FilterableList) SetItems(items ...FilterableItem) {
49 f.items = items
41 f.items = items
50− f.dirty = true
51 fitems := make([]Item, len(items))
42 fitems := make([]Item, len(items))
52 for i, item := range items {
43 for i, item := range items {
53 fitems[i] = item
44 fitems[i] = item
@@ -58,7 +49,6 @@ func (f *FilterableList) SetItems(items ...FilterableItem) {
58// AppendItems appends items to the list and updates the filtered items.
49// AppendItems appends items to the list and updates the filtered items.
59func (f *FilterableList) AppendItems(items ...FilterableItem) {
50func (f *FilterableList) AppendItems(items ...FilterableItem) {
60 f.items = append(f.items, items...)
51 f.items = append(f.items, items...)
61− f.dirty = true
62 itms := make([]Item, len(f.items))
52 itms := make([]Item, len(f.items))
63 for i, item := range f.items {
53 for i, item := range f.items {
64 itms[i] = item
54 itms[i] = item
@@ -69,7 +59,6 @@ func (f *FilterableList) AppendItems(items ...FilterableItem) {
69// PrependItems prepends items to the list and updates the filtered items.
59// PrependItems prepends items to the list and updates the filtered items.
70func (f *FilterableList) PrependItems(items ...FilterableItem) {
60func (f *FilterableList) PrependItems(items ...FilterableItem) {
71 f.items = append(items, f.items...)
61 f.items = append(items, f.items...)
72− f.dirty = true
73 itms := make([]Item, len(f.items))
62 itms := make([]Item, len(f.items))
74 for i, item := range f.items {
63 for i, item := range f.items {
75 itms[i] = item
64 itms[i] = item
@@ -80,7 +69,6 @@ func (f *FilterableList) PrependItems(items ...FilterableItem) {
80// SetFilter sets the filter query and updates the list items.
69// SetFilter sets the filter query and updates the list items.
81func (f *FilterableList) SetFilter(q string) {
70func (f *FilterableList) SetFilter(q string) {
82 f.query = q
71 f.query = q
83− f.dirty = true
84 f.List.SetItems(f.FilteredItems()...)
72 f.List.SetItems(f.FilteredItems()...)
85 f.ScrollToTop()
73 f.ScrollToTop()
86}
74}
@@ -130,28 +118,8 @@ func (f *FilterableList) FilteredItems() []Item {
130 return matchedItems
118 return matchedItems
131}
119}
132
120
133−// SetEmptyMessage sets the placeholder shown when the list has no items and
134−// no filter is active.
135−func (f *FilterableList) SetEmptyMessage(msg string) {
136− f.emptyMessage = msg
137−}
138−
139−// SetFilterEmptyMessage sets the placeholder shown when a filter is active but
140−// yields no matches.
141−func (f *FilterableList) SetFilterEmptyMessage(msg string) {
142− f.filterEmptyMessage = msg
143−}
144−
145// Render renders the filterable list.
121// Render renders the filterable list.
146func (f *FilterableList) Render() string {
122func (f *FilterableList) Render() string {
147− if f.dirty {
123+ f.List.SetItems(f.FilteredItems()...)
148− f.List.SetItems(f.FilteredItems()...)
149− f.dirty = false
150− }
151− if f.query != "" && f.filterEmptyMessage != "" {
152− f.List.SetEmptyMessage(f.filterEmptyMessage)
153− } else {
154− f.List.SetEmptyMessage(f.emptyMessage)
155− }
156 return f.List.Render()
124 return f.List.Render()
157}
125}
internal/ui/list/highlight.go+1−2
@@ -136,10 +136,9 @@ func HighlightBuffer(content string, area image.Rectangle, startLine, startCol,
136
136
137// ToHighlighter converts a [lipgloss.Style] to a [Highlighter].
137// ToHighlighter converts a [lipgloss.Style] to a [Highlighter].
138func ToHighlighter(lgStyle lipgloss.Style) Highlighter {
138func ToHighlighter(lgStyle lipgloss.Style) Highlighter {
139− uvStyle := ToStyle(lgStyle)
140 return func(_ int, _ int, c *uv.Cell) *uv.Cell {
139 return func(_ int, _ int, c *uv.Cell) *uv.Cell {
141 if c != nil {
140 if c != nil {
142− c.Style = uvStyle
141+ c.Style = ToStyle(lgStyle)
143 }
142 }
144 return c
143 return c
145 }
144 }
internal/ui/list/list.go+4−23
@@ -2,8 +2,6 @@ package list
2
2
3import (
3import (
4 "strings"
4 "strings"
5−
6− "charm.land/lipgloss/v2"
7)
5)
8
6
9// List represents a list of items that can be lazily rendered. A list is
7// List represents a list of items that can be lazily rendered. A list is
@@ -52,11 +50,6 @@ type List struct {
52 // without calling back into Render.
50 // without calling back into Render.
53 cache map[Item]*listCacheEntry
51 cache map[Item]*listCacheEntry
54
52
55− // emptyMessage is a placeholder rendered centered (and dimmed) when
56− // the list has zero items, so an empty or fully-filtered list is not
57− // shown as a blank void. Empty by default (renders nothing).
58− emptyMessage string
59−
60 // freezeSuppressed marks items the list must not freeze on the
53 // freezeSuppressed marks items the list must not freeze on the
61 // next render even when their Finished() reports true. This is
54 // next render even when their Finished() reports true. This is
62 // the §4.5.1 selection-drag escape hatch (option (a)): items
55 // the §4.5.1 selection-drag escape hatch (option (a)): items
@@ -116,12 +109,6 @@ func (l *List) SetSize(width, height int) {
116 l.height = height
109 l.height = height
117}
110}
118
111
119−// SetEmptyMessage sets a placeholder message rendered centered and dimmed
120−// when the list has no items. An empty string disables the placeholder.
121−func (l *List) SetEmptyMessage(msg string) {
122− l.emptyMessage = msg
123−}
124−
125// SetGap sets the gap between items.
112// SetGap sets the gap between items.
126func (l *List) SetGap(gap int) {
113func (l *List) SetGap(gap int) {
127 l.gap = gap
114 l.gap = gap
@@ -334,7 +321,9 @@ func (l *List) renderItemEntry(idx int) *listCacheEntry {
334
321
335// invalidateAll drops every cache entry. Called on width changes.
322// invalidateAll drops every cache entry. Called on width changes.
336func (l *List) invalidateAll() {
323func (l *List) invalidateAll() {
337− l.cache = make(map[Item]*listCacheEntry)
324+ for k := range l.cache {
325+ delete(l.cache, k)
326+ }
338 l.totalHeightValid = false
327 l.totalHeightValid = false
339}
328}
340
329
@@ -527,15 +516,7 @@ func (l *List) VisibleItemIndices() (startIdx, endIdx int) {
527// lines we now drop implicitly per item).
516// lines we now drop implicitly per item).
528func (l *List) Render() string {
517func (l *List) Render() string {
529 if len(l.items) == 0 {
518 if len(l.items) == 0 {
530− if l.emptyMessage == "" {
519+ return ""
531− return ""
532− }
533− msg := lipgloss.NewStyle().Faint(true).Render(l.emptyMessage)
534− return lipgloss.Place(
535− max(l.width, 0), max(l.height, 0),
536− lipgloss.Center, lipgloss.Center,
537− msg,
538− )
539 }
520 }
540
521
541 budget := max(l.height, 0)
522 budget := max(l.height, 0)
internal/ui/model/chat.go+12−38
@@ -302,7 +302,6 @@ func (m *Chat) InvalidateRenderCaches() {
302func (m *Chat) SetMessages(msgs ...chat.MessageItem) tea.Cmd {
302func (m *Chat) SetMessages(msgs ...chat.MessageItem) tea.Cmd {
303 m.idInxMap = make(map[string]int)
303 m.idInxMap = make(map[string]int)
304 m.pausedAnimations = make(map[string]struct{})
304 m.pausedAnimations = make(map[string]struct{})
305− m.scrollbarVisible = false // Reset scrollbar visibility on new session load
306
305
307 items := make([]list.Item, len(msgs))
306 items := make([]list.Item, len(msgs))
308 for i, msg := range msgs {
307 for i, msg := range msgs {
@@ -316,8 +315,7 @@ func (m *Chat) SetMessages(msgs ...chat.MessageItem) tea.Cmd {
316 items[i] = msg
315 items[i] = msg
317 }
316 }
318 m.list.SetItems(items...)
317 m.list.SetItems(items...)
319− m.ScrollToBottom()
318+ return m.ScrollToBottom()
320− return nil
321}
319}
322
320
323// AppendMessages appends a new message item to the chat list.
321// AppendMessages appends a new message item to the chat list.
@@ -448,11 +446,10 @@ func (m *Chat) Follow() bool {
448}
446}
449
447
450// ScrollToBottom scrolls the chat view to the bottom.
448// ScrollToBottom scrolls the chat view to the bottom.
451−// Does not trigger scrollbar visibility (auto-scroll to show new content).
452func (m *Chat) ScrollToBottom() tea.Cmd {
449func (m *Chat) ScrollToBottom() tea.Cmd {
453 m.list.ScrollToBottom()
450 m.list.ScrollToBottom()
454− m.follow = true
451+ m.follow = true // Enable follow mode when user scrolls to bottom
455− return nil
452+ return m.showScrollbar()
456}
453}
457
454
458// ScrollToTop scrolls the chat view to the top.
455// ScrollToTop scrolls the chat view to the top.
@@ -465,13 +462,7 @@ func (m *Chat) ScrollToTop() tea.Cmd {
465// ScrollBy scrolls the chat view by the given number of line deltas.
462// ScrollBy scrolls the chat view by the given number of line deltas.
466func (m *Chat) ScrollBy(lines int) tea.Cmd {
463func (m *Chat) ScrollBy(lines int) tea.Cmd {
467 m.list.ScrollBy(lines)
464 m.list.ScrollBy(lines)
468− if lines < 0 {
465+ m.follow = lines > 0 && m.AtBottom() // Disable follow mode if user scrolls up
469− // Scrolling up always disables follow mode.
470− m.follow = false
471− } else if m.AtBottom() {
472− // Scrolling down re-enables follow when we reach the bottom.
473− m.follow = true
474− }
475 return m.showScrollbar()
466 return m.showScrollbar()
476}
467}
477
468
@@ -661,7 +652,6 @@ func (m *Chat) SelectLastInView() {
661func (m *Chat) ClearMessages() {
652func (m *Chat) ClearMessages() {
662 m.idInxMap = make(map[string]int)
653 m.idInxMap = make(map[string]int)
663 m.pausedAnimations = make(map[string]struct{})
654 m.pausedAnimations = make(map[string]struct{})
664− m.scrollbarVisible = false
665 m.list.SetItems()
655 m.list.SetItems()
666 m.ClearMouse()
656 m.ClearMouse()
667}
657}
@@ -673,18 +663,6 @@ func (m *Chat) RemoveMessage(id string) {
673 return
663 return
674 }
664 }
675
665
676− // Clean up nested tool IDs that point to this item's index before
677− // removing it, so they don't end up pointing to a wrong item after
678− // the rebuild.
679− if item, ok := m.list.ItemAt(idx).(chat.MessageItem); ok {
680− if container, ok := item.(chat.NestedToolContainer); ok {
681− for _, nested := range container.NestedTools() {
682− delete(m.idInxMap, nested.ID())
683− delete(m.pausedAnimations, nested.ID())
684− }
685− }
686− }
687−
688 // Remove from list
666 // Remove from list
689 m.list.RemoveItem(idx)
667 m.list.RemoveItem(idx)
690
668
@@ -695,12 +673,6 @@ func (m *Chat) RemoveMessage(id string) {
695 for i := idx; i < m.list.Len(); i++ {
673 for i := idx; i < m.list.Len(); i++ {
696 if item, ok := m.list.ItemAt(i).(chat.MessageItem); ok {
674 if item, ok := m.list.ItemAt(i).(chat.MessageItem); ok {
697 m.idInxMap[item.ID()] = i
675 m.idInxMap[item.ID()] = i
698− // Also update nested tool IDs for containers.
699− if container, ok := item.(chat.NestedToolContainer); ok {
700− for _, nested := range container.NestedTools() {
701− m.idInxMap[nested.ID()] = i
702− }
703− }
704 }
676 }
705 }
677 }
706
678
@@ -724,11 +696,10 @@ func (m *Chat) MessageItem(id string) chat.MessageItem {
724// ToggleExpandedSelectedItem expands the selected message item if it is expandable.
696// ToggleExpandedSelectedItem expands the selected message item if it is expandable.
725func (m *Chat) ToggleExpandedSelectedItem() {
697func (m *Chat) ToggleExpandedSelectedItem() {
726 if expandable, ok := m.list.SelectedItem().(chat.Expandable); ok {
698 if expandable, ok := m.list.SelectedItem().(chat.Expandable); ok {
727− wasFollowing := m.follow
728 if !expandable.ToggleExpanded() {
699 if !expandable.ToggleExpanded() {
729 m.ScrollToIndex(m.list.Selected())
700 m.ScrollToIndex(m.list.Selected())
730 }
701 }
731− if wasFollowing {
702+ if m.AtBottom() {
732 m.ScrollToBottom()
703 m.ScrollToBottom()
733 }
704 }
734 }
705 }
@@ -855,15 +826,14 @@ func (m *Chat) HandleDelayedClick(msg DelayedClickMsg) bool {
855 // toggling expansion for clicks outside the clickable area.
826 // toggling expansion for clicks outside the clickable area.
856 if handled {
827 if handled {
857 if expandable, ok := selectedItem.(chat.Expandable); ok {
828 if expandable, ok := selectedItem.(chat.Expandable); ok {
858− wasFollowing := m.follow
859 if !expandable.ToggleExpanded() {
829 if !expandable.ToggleExpanded() {
860 m.ScrollToIndex(m.list.Selected())
830 m.ScrollToIndex(m.list.Selected())
861 }
831 }
862− if wasFollowing {
863− m.ScrollToBottom()
864− }
865 }
832 }
866 }
833 }
834+ if m.AtBottom() {
835+ m.ScrollToBottom()
836+ }
867 return handled
837 return handled
868 }
838 }
869
839
@@ -1124,6 +1094,10 @@ func findWordBoundaries(line string, col int) (startCol, endCol int) {
1124 return 0, 0
1094 return 0, 0
1125 }
1095 }
1126
1096
1097+ i := displaywidth.StringGraphemes(line)
1098+ for i.Next() {
1099+ }
1100+
1127 // Segment the line into words using UAX#29.
1101 // Segment the line into words using UAX#29.
1128 lineCol := 0 // tracks the visited column widths
1102 lineCol := 0 // tracks the visited column widths
1129 lastCol := 0 // tracks the start of the current token
1103 lastCol := 0 // tracks the start of the current token
internal/ui/model/header.go+10−93
@@ -2,8 +2,6 @@ package model
2
2
3import (
3import (
4 "fmt"
4 "fmt"
5− "hash/fnv"
6− "strconv"
7 "strings"
5 "strings"
8
6
9 "charm.land/lipgloss/v2"
7 "charm.land/lipgloss/v2"
@@ -32,18 +30,6 @@ type header struct {
32 com *common.Common
30 com *common.Common
33 width int
31 width int
34 compact bool
32 compact bool
35−
36− // compact-header render cache. drawHeader otherwise rebuilds the whole
37− // compact header (LSP loop, config/model lookups, several
38− // lipgloss.Render calls, PrettyPath+DirTrim, ansi.Truncate) every
39− // frame; memoize the rendered string keyed by a cheap fingerprint of
40− // everything it depends on.
41− compactCacheKey uint64
42− compactCacheView string
43− hasCompactCache bool
44− // compactFPBuf is a scratch buffer reused across frames to build the
45− // compact-header fingerprint without allocating (see compactFingerprint).
46− compactFPBuf []byte
47}
33}
48
34
49// newHeader creates a new header model.
35// newHeader creates a new header model.
@@ -73,13 +59,9 @@ func (h *header) refresh() {
73 // Force drawHeader to re-render the wide logo on the next frame.
59 // Force drawHeader to re-render the wide logo on the next frame.
74 h.width = 0
60 h.width = 0
75 h.logo = ""
61 h.logo = ""
76− // The compact-header cache embeds the old theme's styles; drop it.
77− h.hasCompactCache = false
78}
62}
79
63
80−// drawHeader draws the header for the given session. lspErrorCount is the
64+// drawHeader draws the header for the given session.
81−// total diagnostic count summed by the caller from local LSP state, so
82−// this render path never issues an LSPGetStates RPC.
83func (h *header) drawHeader(
65func (h *header) drawHeader(
84 scr uv.Screen,
66 scr uv.Screen,
85 area uv.Rectangle,
67 area uv.Rectangle,
@@ -88,8 +70,8 @@ func (h *header) drawHeader(
88 detailsOpen bool,
70 detailsOpen bool,
89 width int,
71 width int,
90 hyperCredits *int,
72 hyperCredits *int,
91− lspErrorCount int,
92) {
73) {
74+ t := h.com.Styles
93 if width != h.width || compact != h.compact {
75 if width != h.width || compact != h.compact {
94 h.logo = renderLogo(h.com.Styles, compact, h.com.IsHyper(), width)
76 h.logo = renderLogo(h.com.Styles, compact, h.com.IsHyper(), width)
95 }
77 }
@@ -106,79 +88,14 @@ func (h *header) drawHeader(
106 return
88 return
107 }
89 }
108
90
109− key := h.compactFingerprint(session, detailsOpen, width, hyperCredits, lspErrorCount)
110− if !h.hasCompactCache || key != h.compactCacheKey {
111− h.compactCacheView = h.buildCompactHeader(session, detailsOpen, width, hyperCredits, lspErrorCount)
112− h.compactCacheKey = key
113− h.hasCompactCache = true
114− }
115−
116− uv.NewStyledString(h.compactCacheView).Draw(scr, area)
117−}
118−
119−// compactFingerprint hashes every input that affects the compact header
120−// render, so drawHeader can reuse the cached string when nothing changed.
121−func (h *header) compactFingerprint(
122− session *session.Session,
123− detailsOpen bool,
124− width int,
125− hyperCredits *int,
126− lspErrorCount int,
127−) uint64 {
128− var contextWindow int64
129− agentCfg := h.com.Config().Agents[config.AgentCoder]
130− if model := h.com.Config().GetModelByType(agentCfg.Model); model != nil {
131− contextWindow = model.ContextWindow
132− }
133− credits := 0
134− hasCredits := hyperCredits != nil
135− if hasCredits {
136− credits = *hyperCredits
137− }
138− // Build into a reused buffer with strconv appends and hash once, rather
139− // than fmt.Sprintf: this runs every frame in compact mode, so the arg
140− // boxing and result-string allocation Sprintf performed were pure churn.
141− b := h.compactFPBuf[:0]
142− putInt := func(k string, v int64) { b = append(b, k...); b = strconv.AppendInt(b, v, 10) }
143− putInt("w=", int64(width))
144− b = append(b, ";sid="...)
145− b = append(b, session.ID...)
146− putInt(";ct=", session.CompletionTokens)
147− putInt(";pt=", session.PromptTokens)
148− b = append(b, ";eu="...)
149− b = strconv.AppendBool(b, session.EstimatedUsage)
150− putInt(";lsp=", int64(lspErrorCount))
151− b = append(b, ";det="...)
152− b = strconv.AppendBool(b, detailsOpen)
153− b = append(b, ";hyper="...)
154− b = strconv.AppendBool(b, h.com.IsHyper())
155− b = append(b, ";hc="...)
156− b = strconv.AppendBool(b, hasCredits)
157− putInt("/", int64(credits))
158− putInt(";cw=", contextWindow)
159− b = append(b, ";cwd="...)
160− b = append(b, h.com.Workspace.WorkingDir()...)
161− h.compactFPBuf = b // retain the grown buffer for reuse next frame
162−
163− hash := fnv.New64a()
164− hash.Write(b)
165− return hash.Sum64()
166−}
167−
168−// buildCompactHeader renders the compact header string.
169−func (h *header) buildCompactHeader(
170− session *session.Session,
171− detailsOpen bool,
172− width int,
173− hyperCredits *int,
174− lspErrorCount int,
175−) string {
176− t := h.com.Styles
177−
178 var b strings.Builder
91 var b strings.Builder
179 b.WriteString(h.compactLogo)
92 b.WriteString(h.compactLogo)
180
93
181 availDetailWidth := width - leftPadding - rightPadding - lipgloss.Width(b.String()) - minHeaderDiags - diagToDetailsSpacing
94 availDetailWidth := width - leftPadding - rightPadding - lipgloss.Width(b.String()) - minHeaderDiags - diagToDetailsSpacing
95+ lspErrorCount := 0
96+ for _, info := range h.com.Workspace.LSPGetStates() {
97+ lspErrorCount += info.DiagnosticCount
98+ }
182 details := renderHeaderDetails(
99 details := renderHeaderDetails(
183 h.com,
100 h.com,
184 session,
101 session,
@@ -204,7 +121,10 @@ func (h *header) buildCompactHeader(
204
121
205 b.WriteString(details)
122 b.WriteString(details)
206
123
207− return t.Header.Wrapper.Padding(0, rightPadding, 0, leftPadding).Render(b.String())
124+ view := uv.NewStyledString(
125+ t.Header.Wrapper.Padding(0, rightPadding, 0, leftPadding).Render(b.String()),
126+ )
127+ view.Draw(scr, area)
208}
128}
209
129
210// renderHeaderDetails renders the details section of the header.
130// renderHeaderDetails renders the details section of the header.
@@ -228,9 +148,6 @@ func renderHeaderDetails(
228 model := com.Config().GetModelByType(agentCfg.Model)
148 model := com.Config().GetModelByType(agentCfg.Model)
229 if model != nil && model.ContextWindow > 0 {
149 if model != nil && model.ContextWindow > 0 {
230 percentage := (float64(session.CompletionTokens+session.PromptTokens) / float64(model.ContextWindow)) * 100
150 percentage := (float64(session.CompletionTokens+session.PromptTokens) / float64(model.ContextWindow)) * 100
231− if percentage > 100 {
232− percentage = 100
233− }
234 percentageText := fmt.Sprintf("%d%%", int(percentage))
151 percentageText := fmt.Sprintf("%d%%", int(percentage))
235 if session.EstimatedUsage {
152 if session.EstimatedUsage {
236 percentageText = "~" + percentageText
153 percentageText = "~" + percentageText
internal/ui/model/history.go+2−1
@@ -1,6 +1,7 @@
1package model
1package model
2
2
3import (
3import (
4+ "context"
4 "log/slog"
5 "log/slog"
5 "strings"
6 "strings"
6
7
@@ -17,7 +18,7 @@ type promptHistoryLoadedMsg struct {
17// loadPromptHistory loads user messages for history navigation.
18// loadPromptHistory loads user messages for history navigation.
18func (m *UI) loadPromptHistory() tea.Cmd {
19func (m *UI) loadPromptHistory() tea.Cmd {
19 return func() tea.Msg {
20 return func() tea.Msg {
20− ctx := m.ctx
21+ ctx := context.Background()
21 var messages []message.Message
22 var messages []message.Message
22 var err error
23 var err error
23
24
internal/ui/model/keys.go+21−25
@@ -4,14 +4,14 @@ import "charm.land/bubbles/v2/key"
4
4
5type KeyMap struct {
5type KeyMap struct {
6 Editor struct {
6 Editor struct {
7− SendMessage key.Binding
7+ AddFile key.Binding
8− OpenEditor key.Binding
8+ SendMessage key.Binding
9− Newline key.Binding
9+ OpenEditor key.Binding
10− AddImage key.Binding
10+ Newline key.Binding
11− PasteImage key.Binding
11+ AddImage key.Binding
12− MentionFile key.Binding
12+ PasteImage key.Binding
13− Commands key.Binding
13+ MentionFile key.Binding
14− ShellCommand key.Binding
14+ Commands key.Binding
15
15
16 // Attachments key maps
16 // Attachments key maps
17 AttachmentDeleteMode key.Binding
17 AttachmentDeleteMode key.Binding
@@ -25,6 +25,7 @@ type KeyMap struct {
25
25
26 Chat struct {
26 Chat struct {
27 NewSession key.Binding
27 NewSession key.Binding
28+ AddAttachment key.Binding
28 Cancel key.Binding
29 Cancel key.Binding
29 Tab key.Binding
30 Tab key.Binding
30 Details key.Binding
31 Details key.Binding
@@ -66,7 +67,6 @@ type KeyMap struct {
66 Sessions key.Binding
67 Sessions key.Binding
67 Tab key.Binding
68 Tab key.Binding
68 ToggleYolo key.Binding
69 ToggleYolo key.Binding
69− CopyError key.Binding
70}
70}
71
71
72func DefaultKeyMap() KeyMap {
72func DefaultKeyMap() KeyMap {
@@ -103,12 +103,12 @@ func DefaultKeyMap() KeyMap {
103 key.WithKeys("ctrl+y"),
103 key.WithKeys("ctrl+y"),
104 key.WithHelp("ctrl+y", "toggle yolo"),
104 key.WithHelp("ctrl+y", "toggle yolo"),
105 ),
105 ),
106− CopyError: key.NewBinding(
107− key.WithKeys("ctrl+e"),
108− key.WithHelp("ctrl+e", "copy error"),
109− ),
110 }
106 }
111
107
108+ km.Editor.AddFile = key.NewBinding(
109+ key.WithKeys("/"),
110+ key.WithHelp("/", "add file"),
111+ )
112 km.Editor.SendMessage = key.NewBinding(
112 km.Editor.SendMessage = key.NewBinding(
113 key.WithKeys("enter"),
113 key.WithKeys("enter"),
114 key.WithHelp("enter", "send"),
114 key.WithHelp("enter", "send"),
@@ -140,14 +140,6 @@ func DefaultKeyMap() KeyMap {
140 key.WithKeys("/"),
140 key.WithKeys("/"),
141 key.WithHelp("/", "commands"),
141 key.WithHelp("/", "commands"),
142 )
142 )
143− // ShellCommand is a help-only hint: bang (!) shell mode is entered by
144− // typing "!" at the start of an empty editor (see the bang-mode handling
145− // in ui.go), not via a key.Matches lookup, so this binding exists purely
146− // to surface the feature in the help view.
147− km.Editor.ShellCommand = key.NewBinding(
148− key.WithKeys("!"),
149− key.WithHelp("!", "shell command"),
150− )
151 km.Editor.AttachmentDeleteMode = key.NewBinding(
143 km.Editor.AttachmentDeleteMode = key.NewBinding(
152 key.WithKeys("ctrl+r"),
144 key.WithKeys("ctrl+r"),
153 key.WithHelp("ctrl+r+{i}", "delete attachment at index i"),
145 key.WithHelp("ctrl+r+{i}", "delete attachment at index i"),
@@ -171,6 +163,10 @@ func DefaultKeyMap() KeyMap {
171 key.WithKeys("ctrl+n"),
163 key.WithKeys("ctrl+n"),
172 key.WithHelp("ctrl+n", "new session"),
164 key.WithHelp("ctrl+n", "new session"),
173 )
165 )
166+ km.Chat.AddAttachment = key.NewBinding(
167+ key.WithKeys("ctrl+f"),
168+ key.WithHelp("ctrl+f", "add attachment"),
169+ )
174 km.Chat.Cancel = key.NewBinding(
170 km.Chat.Cancel = key.NewBinding(
175 key.WithKeys("esc", "alt+esc"),
171 key.WithKeys("esc", "alt+esc"),
176 key.WithHelp("esc", "cancel"),
172 key.WithHelp("esc", "cancel"),
@@ -198,11 +194,11 @@ func DefaultKeyMap() KeyMap {
198
194
199 km.Chat.Down = key.NewBinding(
195 km.Chat.Down = key.NewBinding(
200 key.WithKeys("down", "ctrl+j", "j"),
196 key.WithKeys("down", "ctrl+j", "j"),
201− key.WithHelp("↓/j", "down"),
197+ key.WithHelp("↓", "down"),
202 )
198 )
203 km.Chat.Up = key.NewBinding(
199 km.Chat.Up = key.NewBinding(
204 key.WithKeys("up", "ctrl+k", "k"),
200 key.WithKeys("up", "ctrl+k", "k"),
205− key.WithHelp("↑/k", "up"),
201+ key.WithHelp("↑", "up"),
206 )
202 )
207 km.Chat.UpDown = key.NewBinding(
203 km.Chat.UpDown = key.NewBinding(
208 key.WithKeys("up", "down"),
204 key.WithKeys("up", "down"),
@@ -210,11 +206,11 @@ func DefaultKeyMap() KeyMap {
210 )
206 )
211 km.Chat.UpOneItem = key.NewBinding(
207 km.Chat.UpOneItem = key.NewBinding(
212 key.WithKeys("shift+up", "K"),
208 key.WithKeys("shift+up", "K"),
213− key.WithHelp("shift+↑/K", "up one item"),
209+ key.WithHelp("shift+↑", "up one item"),
214 )
210 )
215 km.Chat.DownOneItem = key.NewBinding(
211 km.Chat.DownOneItem = key.NewBinding(
216 key.WithKeys("shift+down", "J"),
212 key.WithKeys("shift+down", "J"),
217− key.WithHelp("shift+↓/J", "down one item"),
213+ key.WithHelp("shift+↓", "down one item"),
218 )
214 )
219 km.Chat.UpDownOneItem = key.NewBinding(
215 km.Chat.UpDownOneItem = key.NewBinding(
220 key.WithKeys("shift+up", "shift+down"),
216 key.WithKeys("shift+up", "shift+down"),
internal/ui/model/landing.go+9−78
@@ -1,11 +1,9 @@
1package model
1package model
2
2
3import (
3import (
4− "fmt"
5 "image"
4 "image"
6
5
7 "charm.land/lipgloss/v2"
6 "charm.land/lipgloss/v2"
8− "github.com/charmbracelet/crush/internal/config"
9 "github.com/charmbracelet/crush/internal/ui/common"
7 "github.com/charmbracelet/crush/internal/ui/common"
10 "github.com/charmbracelet/crush/internal/workspace"
8 "github.com/charmbracelet/crush/internal/workspace"
11 "github.com/charmbracelet/ultraviolet/layout"
9 "github.com/charmbracelet/ultraviolet/layout"
@@ -13,68 +11,14 @@ import (
13
11
14// selectedLargeModel returns the currently selected large language model from
12// selectedLargeModel returns the currently selected large language model from
15// the agent coordinator, if one exists.
13// the agent coordinator, if one exists.
16−//
17−// Resolving the model hits the agent coordinator, which is an RPC
18−// (AgentIsReady + AgentModel) in client mode. Because this runs on the
19−// per-frame sidebar fingerprint path, the result is cached against a
20−// cheap, locally-cached config fingerprint (largeModelConfigKey) and
21−// only re-fetched when the selection changes. A nil result is never
22−// cached, so the not-ready → ready transition is picked up on the next
23−// frame.
24func (m *UI) selectedLargeModel() *workspace.AgentModel {
14func (m *UI) selectedLargeModel() *workspace.AgentModel {
25− key := m.largeModelConfigKey()
26− if m.renderCache.largeModel != nil && key == m.renderCache.largeModelCacheKey {
27− return m.renderCache.largeModel
28− }
29− m.renderCache.largeModelCacheKey = key
30 if m.com.Workspace.AgentIsReady() {
15 if m.com.Workspace.AgentIsReady() {
31 model := m.com.Workspace.AgentModel()
16 model := m.com.Workspace.AgentModel()
32− m.renderCache.largeModel = &model
17+ return &model
33− return m.renderCache.largeModel
34 }
18 }
35− m.renderCache.largeModel = nil
36 return nil
19 return nil
37}
20}
38
21
39−// largeModelConfigKey builds a cheap fingerprint of the locally-cached
40−// config that determines the selected large (coder) model. The resolved
41−// model is cached against this key so selectedLargeModel only re-fetches
42−// when the selection actually changes. The cached workspace config is
43−// refreshed on every server-side config change (ConfigChanged), so
44−// remote model switches flip this key too.
45−func (m *UI) largeModelConfigKey() string {
46− cfg := m.com.Config()
47− if cfg == nil {
48− return ""
49− }
50− agentCfg, ok := cfg.Agents[config.AgentCoder]
51− if !ok {
52− return "no-agent"
53− }
54− sm, ok := cfg.Models[agentCfg.Model]
55− if !ok {
56− return "no-model:" + string(agentCfg.Model)
57− }
58− // Fold in the catwalk-derived fields that the sidebar renders (looked
59− // up from the same cached config, no RPC), so a provider-config
60− // change that alters e.g. the context window or reasoning support for
61− // the same provider+model still flips the key and forces a refresh.
62− var cwName, provName string
63− var contextWindow int64
64− var canReason bool
65− if cm := cfg.GetModelByType(agentCfg.Model); cm != nil {
66− cwName = cm.Name
67− contextWindow = cm.ContextWindow
68− canReason = cm.CanReason
69− }
70− if pc, ok := cfg.Providers.Get(sm.Provider); ok {
71− provName = pc.Name
72− }
73− return fmt.Sprintf("%s|%s|%s|%t|%s|%s|%s|%d|%t",
74− agentCfg.Model, sm.Provider, sm.Model, sm.Think, sm.ReasoningEffort,
75− provName, cwName, contextWindow, canReason)
76−}
77−
78// landingView renders the landing page view showing the current working
22// landingView renders the landing page view showing the current working
79// directory, model information, and LSP/MCP status in a two-column layout.
23// directory, model information, and LSP/MCP status in a two-column layout.
80func (m *UI) landingView() string {
24func (m *UI) landingView() string {
@@ -86,8 +30,7 @@ func (m *UI) landingView() string {
86 cwd,
30 cwd,
87 }
31 }
88
32
89− cta := t.Resource.AdditionalText.Render("Type a message to begin · / for commands · ctrl+s for sessions")
33+ parts = append(parts, "", m.modelInfo(width))
90− parts = append(parts, "", m.modelInfo(width), "", cta)
91 infoSection := lipgloss.JoinVertical(lipgloss.Left, parts...)
34 infoSection := lipgloss.JoinVertical(lipgloss.Left, parts...)
92
35
93 var remainingHeightArea image.Rectangle
36 var remainingHeightArea image.Rectangle
@@ -96,25 +39,13 @@ func (m *UI) landingView() string {
96 layout.Fill(1),
39 layout.Fill(1),
97 ).Split(m.layout.main).Assign(new(image.Rectangle), &remainingHeightArea)
40 ).Split(m.layout.main).Assign(new(image.Rectangle), &remainingHeightArea)
98
41
99− // On narrow terminals the three side-by-side sections get squeezed to a
42+ mcpLspSectionWidth := min(30, (width-2)/3)
100− // few columns each, so stack them vertically instead. narrowLandingWidth
43+
101− // is the point below which a 3-column split would leave each column under
44+ lspSection := m.lspInfo(mcpLspSectionWidth, max(1, remainingHeightArea.Dy()), false)
102− // ~20 cells wide ((width-2)/3 < 20).
45+ mcpSection := m.mcpInfo(mcpLspSectionWidth, max(1, remainingHeightArea.Dy()), false)
103− const narrowLandingWidth = 60
46+ skillsSection := m.skillsInfo(mcpLspSectionWidth, max(1, remainingHeightArea.Dy()), false)
104− var content string
47+
105− if width < narrowLandingWidth {
48+ content := lipgloss.JoinHorizontal(lipgloss.Left, lspSection, " ", mcpSection, " ", skillsSection)
106− perSection := max(1, remainingHeightArea.Dy()/3)
107− lspSection := m.lspInfo(width, perSection, false)
108− mcpSection := m.mcpInfo(width, perSection, false)
109− skillsSection := m.skillsInfo(width, perSection, false)
110− content = lipgloss.JoinVertical(lipgloss.Left, lspSection, "", mcpSection, "", skillsSection)
111− } else {
112− mcpLspSectionWidth := min(30, (width-2)/3)
113− lspSection := m.lspInfo(mcpLspSectionWidth, max(1, remainingHeightArea.Dy()), false)
114− mcpSection := m.mcpInfo(mcpLspSectionWidth, max(1, remainingHeightArea.Dy()), false)
115− skillsSection := m.skillsInfo(mcpLspSectionWidth, max(1, remainingHeightArea.Dy()), false)
116− content = lipgloss.JoinHorizontal(lipgloss.Left, lspSection, " ", mcpSection, " ", skillsSection)
117− }
118
49
119 return lipgloss.NewStyle().
50 return lipgloss.NewStyle().
120 Width(width).
51 Width(width).
internal/ui/model/lsp.go+2−23
@@ -20,23 +20,6 @@ type LSPInfo struct {
20 Diagnostics map[protocol.DiagnosticSeverity]int
20 Diagnostics map[protocol.DiagnosticSeverity]int
21}
21}
22
22
23−// refreshLSPState re-fetches the LSP client states and their per-severity
24−// diagnostic counts into the local mirrors (m.lspStates / m.lspDiagnostics).
25−// It is called from the LSP event handlers (state change / diagnostics
26−// change) so the sidebar fingerprint — computed every frame — can read
27−// fresh values from local state instead of issuing per-LSP diagnostics
28−// RPCs on the render hot path. Every diagnostics change publishes an LSP
29−// event (see internal/app/lsp_events.go), so the mirror always changes
30−// whenever the rendered sidebar would.
31−func (m *UI) refreshLSPState() {
32− m.lspStates = m.com.Workspace.LSPGetStates()
33− counts := make(map[string]lsp.DiagnosticCounts, len(m.lspStates))
34− for name := range m.lspStates {
35− counts[name] = m.com.Workspace.LSPGetDiagnosticCounts(name)
36− }
37− m.lspDiagnostics = counts
38−}
39−
40// lspInfo renders the LSP status section showing active LSP clients and their
23// lspInfo renders the LSP status section showing active LSP clients and their
41// diagnostic counts.
24// diagnostic counts.
42func (m *UI) lspInfo(width, maxItems int, isSection bool) string {
25func (m *UI) lspInfo(width, maxItems int, isSection bool) string {
@@ -49,11 +32,7 @@ func (m *UI) lspInfo(width, maxItems int, isSection bool) string {
49 var lsps []LSPInfo
32 var lsps []LSPInfo
50 for _, state := range states {
33 for _, state := range states {
51 lspErrs := map[protocol.DiagnosticSeverity]int{}
34 lspErrs := map[protocol.DiagnosticSeverity]int{}
52− // Read from the local mirror (refreshed on every LSP event) rather
35+ counts := m.com.Workspace.LSPGetDiagnosticCounts(state.Name)
53− // than issuing a per-LSP diagnostics RPC on the render path. This
54− // also keeps the rendered counts consistent with the sidebar
55− // fingerprint, which reads the same mirror.
56− counts := m.lspDiagnostics[state.Name]
57 lspErrs[protocol.SeverityError] = counts.Error
36 lspErrs[protocol.SeverityError] = counts.Error
58 lspErrs[protocol.SeverityWarning] = counts.Warning
37 lspErrs[protocol.SeverityWarning] = counts.Warning
59 lspErrs[protocol.SeverityHint] = counts.Hint
38 lspErrs[protocol.SeverityHint] = counts.Hint
@@ -139,7 +118,7 @@ func lspList(t *styles.Styles, lsps []LSPInfo, width, maxItems int) string {
139
118
140 if len(renderedLsps) > maxItems {
119 if len(renderedLsps) > maxItems {
141 visibleItems := renderedLsps[:maxItems-1]
120 visibleItems := renderedLsps[:maxItems-1]
142− remaining := len(renderedLsps) - (maxItems - 1)
121+ remaining := len(renderedLsps) - maxItems
143 visibleItems = append(visibleItems, t.Resource.AdditionalText.Render(fmt.Sprintf("…and %d more", remaining)))
122 visibleItems = append(visibleItems, t.Resource.AdditionalText.Render(fmt.Sprintf("…and %d more", remaining)))
144 return lipgloss.JoinVertical(lipgloss.Left, visibleItems...)
123 return lipgloss.JoinVertical(lipgloss.Left, visibleItems...)
145 }
124 }
internal/ui/model/mcp.go+2−9
@@ -14,17 +14,10 @@ import (
14// mcpInfo renders the MCP status section showing active MCP clients and their
14// mcpInfo renders the MCP status section showing active MCP clients and their
15// tool/prompt counts.
15// tool/prompt counts.
16func (m *UI) mcpInfo(width, maxItems int, isSection bool) string {
16func (m *UI) mcpInfo(width, maxItems int, isSection bool) string {
17− return m.mcpInfoSorted(m.com.Config().MCP.Sorted(), width, maxItems, isSection)
18−}
19−
20−// mcpInfoSorted renders the MCP status section from an already-sorted MCP
21−// config list, letting callers on the sidebar render path reuse a single
22−// MCP.Sorted() result instead of recomputing it.
23−func (m *UI) mcpInfoSorted(mcpSorted []config.MCP, width, maxItems int, isSection bool) string {
24 var mcps []mcp.ClientInfo
17 var mcps []mcp.ClientInfo
25 t := m.com.Styles
18 t := m.com.Styles
26
19
27− for _, mcp := range mcpSorted {
20+ for _, mcp := range m.com.Config().MCP.Sorted() {
28 if state, ok := m.mcpStates[mcp.Name]; ok {
21 if state, ok := m.mcpStates[mcp.Name]; ok {
29 mcps = append(mcps, state)
22 mcps = append(mcps, state)
30 }
23 }
@@ -106,7 +99,7 @@ func mcpList(t *styles.Styles, mcps []mcp.ClientInfo, width, maxItems int) strin
106
99
107 if len(renderedMcps) > maxItems {
100 if len(renderedMcps) > maxItems {
108 visibleItems := renderedMcps[:maxItems-1]
101 visibleItems := renderedMcps[:maxItems-1]
109− remaining := len(renderedMcps) - (maxItems - 1)
102+ remaining := len(renderedMcps) - maxItems
110 visibleItems = append(visibleItems, t.Resource.AdditionalText.Render(fmt.Sprintf("…and %d more", remaining)))
103 visibleItems = append(visibleItems, t.Resource.AdditionalText.Render(fmt.Sprintf("…and %d more", remaining)))
111 return lipgloss.JoinVertical(lipgloss.Left, visibleItems...)
104 return lipgloss.JoinVertical(lipgloss.Left, visibleItems...)
112 }
105 }
internal/ui/model/onboarding.go+2−1
@@ -72,6 +72,7 @@ func (m *UI) initializeProject() tea.Cmd {
72
72
73// skipInitializeProject skips project initialization and transitions to the landing view.
73// skipInitializeProject skips project initialization and transitions to the landing view.
74func (m *UI) skipInitializeProject() tea.Cmd {
74func (m *UI) skipInitializeProject() tea.Cmd {
75+ // TODO: initialize the project
75 m.setState(uiLanding, uiFocusEditor)
76 m.setState(uiLanding, uiFocusEditor)
76 // mark the project as initialized
77 // mark the project as initialized
77 return m.markProjectInitializedCmd()
78 return m.markProjectInitializedCmd()
@@ -85,7 +86,7 @@ func (m *UI) initializeView() string {
85
86
86 header := s.Header.Render("Would you like to initialize this project?")
87 header := s.Header.Render("Would you like to initialize this project?")
87 path := s.Accent.PaddingLeft(2).Render(cwd)
88 path := s.Accent.PaddingLeft(2).Render(cwd)
88− desc := s.Content.Render(fmt.Sprintf("When I initialize your codebase I examine the project and put the result into a %s file which serves as general context.", initFile))
89+ desc := s.Content.Render(fmt.Sprintf("When I initialize your codebase I examine the project and put the result into an %s file which serves as general context.", initFile))
89 hint := s.Content.Render("You can also initialize anytime via ") + s.Accent.Render("ctrl+p") + s.Content.Render(".")
90 hint := s.Content.Render("You can also initialize anytime via ") + s.Accent.Render("ctrl+p") + s.Content.Render(".")
90 prompt := s.Content.Render("Would you like to initialize now?")
91 prompt := s.Content.Render("Would you like to initialize now?")
91
92
internal/ui/model/pills.go+4−49
@@ -88,7 +88,7 @@ func todoPill(todos []session.Todo, spinnerView string, focused, panelFocused bo
88
88
89 total := len(todos)
89 total := len(todos)
90
90
91− label := t.Pills.TodoLabel.Render("Tasks")
91+ label := t.Pills.TodoLabel.Render("To-Do")
92 progress := t.Pills.TodoProgress.Render(fmt.Sprintf("%d/%d", completed, total))
92 progress := t.Pills.TodoProgress.Render(fmt.Sprintf("%d/%d", completed, total))
93
93
94 var content string
94 var content string
@@ -192,8 +192,6 @@ func (m *UI) togglePillsExpanded() tea.Cmd {
192 m.updateLayoutAndSize()
192 m.updateLayoutAndSize()
193
193
194 // Make sure to follow scroll if follow is enabled when toggling pills.
194 // Make sure to follow scroll if follow is enabled when toggling pills.
195− // Note: uses ScrollToBottom (no scrollbar) since this is layout adjustment,
196− // not user-initiated scrolling.
197 if m.chat.Follow() {
195 if m.chat.Follow() {
198 m.chat.ScrollToBottom()
196 m.chat.ScrollToBottom()
199 }
197 }
@@ -245,52 +243,9 @@ func (m *UI) pillsAreaHeight() int {
245 return pillsAreaHeight
243 return pillsAreaHeight
246}
244}
247
245
248−// pillsCacheState builds a fingerprint of every input that affects the
246+// renderPills renders the pills panel and stores it in m.pillsView.
249−// rendered pills. Returned alongside the width for reuse by renderPills.
250−func (m *UI) pillsCacheState() string {
251− if !m.hasSession() {
252− return "no-session"
253− }
254− width := m.layout.pills.Dx()
255− hasQueue := m.promptQueue > 0
256−
257− var kb strings.Builder
258− fmt.Fprintf(&kb, "w=%d;theme=%s;q=%d;exp=%t;sec=%d;",
259− width, m.themeKey, m.promptQueue, m.pillsExpanded, m.focusedPillSection)
260− // The in-progress spinner icon changes every animation frame, so fold
261− // its current view in only while spinning.
262− if m.todoIsSpinning {
263− kb.WriteString("spin=")
264− kb.WriteString(m.todoSpinner.View())
265− kb.WriteByte(';')
266− }
267− for i := range m.session.Todos {
268− td := &m.session.Todos[i]
269− fmt.Fprintf(&kb, "t%d=%v|%s|%s;", i, td.Status, td.Content, td.ActiveForm)
270− }
271− // The expanded queue list content only affects output when the queue
272− // section is expanded and focused.
273− if m.pillsExpanded && m.focusedPillSection == pillSectionQueue && hasQueue {
274− if m.com != nil && m.com.Workspace != nil && m.com.Workspace.AgentIsReady() {
275− for _, it := range m.com.Workspace.AgentQueuedPromptsList(m.session.ID) {
276− kb.WriteString(it)
277− kb.WriteByte(';')
278− }
279− }
280− }
281− return kb.String()
282−}
283−
284−// renderPills renders the pills panel and stores it in m.renderCache.pillsView.
285func (m *UI) renderPills() {
247func (m *UI) renderPills() {
286− // Skip re-rendering when nothing that affects the output changed.
248+ m.pillsView = ""
287− key := m.pillsCacheState()
288− if key == m.renderCache.pillsCacheKey {
289− return
290− }
291− m.renderCache.pillsCacheKey = key
292−
293− m.renderCache.pillsView = ""
294 if !m.hasSession() {
249 if !m.hasSession() {
295 return
250 return
296 }
251 }
@@ -359,5 +314,5 @@ func (m *UI) renderPills() {
359 pillsArea = lipgloss.JoinVertical(lipgloss.Left, pillsRow, expandedList)
314 pillsArea = lipgloss.JoinVertical(lipgloss.Left, pillsRow, expandedList)
360 }
315 }
361
316
362− m.renderCache.pillsView = t.Pills.Area.MaxWidth(width).PaddingLeft(paddingLeft).Render(pillsArea)
317+ m.pillsView = t.Pills.Area.MaxWidth(width).PaddingLeft(paddingLeft).Render(pillsArea)
363}
318}
internal/ui/model/session.go+6−5
@@ -1,6 +1,7 @@
1package model
1package model
2
2
3import (
3import (
4+ "context"
4 "fmt"
5 "fmt"
5 "log/slog"
6 "log/slog"
6 "path/filepath"
7 "path/filepath"
@@ -70,7 +71,7 @@ type SessionFile struct {
70// UI never blocks on the call.
71// UI never blocks on the call.
71func (m *UI) loadSession(sessionID string) tea.Cmd {
72func (m *UI) loadSession(sessionID string) tea.Cmd {
72 load := func() tea.Msg {
73 load := func() tea.Msg {
73− session, err := m.com.Workspace.GetSession(m.ctx, sessionID)
74+ session, err := m.com.Workspace.GetSession(context.Background(), sessionID)
74 if err != nil {
75 if err != nil {
75 return util.ReportError(err)
76 return util.ReportError(err)
76 }
77 }
@@ -80,7 +81,7 @@ func (m *UI) loadSession(sessionID string) tea.Cmd {
80 return util.ReportError(err)
81 return util.ReportError(err)
81 }
82 }
82
83
83− readFiles, err := m.com.Workspace.FileTrackerListReadFiles(m.ctx, sessionID)
84+ readFiles, err := m.com.Workspace.FileTrackerListReadFiles(context.Background(), sessionID)
84 if err != nil {
85 if err != nil {
85 slog.Error("Failed to load read files for session", "error", err)
86 slog.Error("Failed to load read files for session", "error", err)
86 }
87 }
@@ -101,7 +102,7 @@ func (m *UI) loadSession(sessionID string) tea.Cmd {
101// state.
102// state.
102func (m *UI) reportCurrentSession(sessionID string) tea.Cmd {
103func (m *UI) reportCurrentSession(sessionID string) tea.Cmd {
103 return func() tea.Msg {
104 return func() tea.Msg {
104− if err := m.com.Workspace.SetCurrentSession(m.ctx, sessionID); err != nil {
105+ if err := m.com.Workspace.SetCurrentSession(context.Background(), sessionID); err != nil {
105 slog.Debug("Failed to report current session", "session_id", sessionID, "error", err)
106 slog.Debug("Failed to report current session", "session_id", sessionID, "error", err)
106 }
107 }
107 return nil
108 return nil
@@ -109,7 +110,7 @@ func (m *UI) reportCurrentSession(sessionID string) tea.Cmd {
109}
110}
110
111
111func (m *UI) loadSessionFiles(sessionID string) ([]SessionFile, error) {
112func (m *UI) loadSessionFiles(sessionID string) ([]SessionFile, error) {
112− files, err := m.com.Workspace.ListSessionHistory(m.ctx, sessionID)
113+ files, err := m.com.Workspace.ListSessionHistory(context.Background(), sessionID)
113 if err != nil {
114 if err != nil {
114 return nil, err
115 return nil, err
115 }
116 }
@@ -263,7 +264,7 @@ func (m *UI) startLSPs(paths []string) tea.Cmd {
263 }
264 }
264
265
265 return func() tea.Msg {
266 return func() tea.Msg {
266− ctx := m.ctx
267+ ctx := context.Background()
267 for _, path := range paths {
268 for _, path := range paths {
268 m.com.Workspace.LSPStart(ctx, path)
269 m.com.Workspace.LSPStart(ctx, path)
269 }
270 }
internal/ui/model/sidebar.go+21−175
@@ -3,13 +3,9 @@ package model
3import (
3import (
4 "cmp"
4 "cmp"
5 "fmt"
5 "fmt"
6− "hash/fnv"
7 "image"
6 "image"
8− "sort"
9− "strconv"
10
7
11 "charm.land/lipgloss/v2"
8 "charm.land/lipgloss/v2"
12− "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/ui/common"
9 "github.com/charmbracelet/crush/internal/ui/common"
14 "github.com/charmbracelet/crush/internal/ui/logo"
10 "github.com/charmbracelet/crush/internal/ui/logo"
15 uv "github.com/charmbracelet/ultraviolet"
11 uv "github.com/charmbracelet/ultraviolet"
@@ -130,139 +126,6 @@ func getDynamicHeightLimits(availableHeight, fileCount, lspCount, mcpCount, skil
130 return maxFiles, maxLSPs, maxMCPs, maxSkills
126 return maxFiles, maxLSPs, maxMCPs, maxSkills
131}
127}
132
128
133−// sidebarFingerprint computes a hash of every input that affects the
134−// rendered sidebar, so drawSidebar can reuse a cached render when nothing
135−// changed. skillItems and mcpSorted are computed once per render by
136−// drawSidebar and reused by buildSidebar to avoid recomputing them.
137−func (m *UI) sidebarFingerprint(area uv.Rectangle, skillItems []skillStatusItem, mcpSorted []config.MCP) uint64 {
138− // Build the fingerprint into a reused scratch buffer using strconv
139− // appends instead of fmt.Fprintf. This runs on every frame (before the
140− // sidebar render cache is consulted), so the reflection-based arg boxing
141− // fmt.Fprintf performs was pure per-frame allocation churn. Appending to
142− // a retained buffer and hashing it once is allocation-free after warmup.
143− b := m.renderCache.sidebarFPBuf[:0]
144− putInt := func(k string, v int64) { b = append(b, k...); b = strconv.AppendInt(b, v, 10) }
145− putStr := func(k, v string) { b = append(b, k...); b = append(b, v...) }
146− putBool := func(k string, v bool) { b = append(b, k...); b = strconv.AppendBool(b, v) }
147−
148− putInt("w=", int64(area.Dx()))
149− putInt(";h=", int64(area.Dy()))
150− putStr(";theme=", m.themeKey)
151− b = append(b, ';')
152−
153− if m.session != nil {
154− putStr("sid=", m.session.ID)
155− putStr(";title=", m.session.Title)
156− putInt(";ct=", m.session.CompletionTokens)
157− putInt(";pt=", m.session.PromptTokens)
158− b = append(b, ";cost="...)
159− b = strconv.AppendFloat(b, m.session.Cost, 'f', -1, 64)
160− putBool(";eu=", m.session.EstimatedUsage)
161− b = append(b, ';')
162− }
163−
164− putStr("cwd=", m.com.Workspace.WorkingDir())
165− putBool(";hyper=", m.com.IsHyper())
166− b = append(b, ';')
167− if m.hyperCredits != nil {
168− putInt("hc=", int64(*m.hyperCredits))
169− b = append(b, ';')
170− }
171− // The logo string is already cached; fold it in so any change to it (or
172− // the small-render breakpoint) invalidates the sidebar.
173− putStr("logo=", m.sidebarLogo)
174− b = append(b, ';')
175−
176− if model := m.selectedLargeModel(); model != nil {
177− putStr("model=", model.ModelCfg.Provider)
178− putStr("|", model.CatwalkCfg.Name)
179− putBool("|reason=", model.CatwalkCfg.CanReason)
180− putInt("|lvls=", int64(len(model.CatwalkCfg.ReasoningLevels)))
181− putStr("|def=", model.CatwalkCfg.DefaultReasoningEffort)
182− putBool("|think=", model.ModelCfg.Think)
183− putStr("|eff=", model.ModelCfg.ReasoningEffort)
184− putInt("|ctx=", int64(model.CatwalkCfg.ContextWindow))
185− b = append(b, ';')
186− } else {
187− b = append(b, "model=nil;"...)
188− }
189−
190− // Files.
191− for i := range m.sessionFiles {
192− f := &m.sessionFiles[i]
193− putInt("f", int64(i))
194− putStr("=", f.LatestVersion.Path)
195− putInt("|", int64(f.Additions))
196− putInt("|", int64(f.Deletions))
197− putInt("|", f.LatestVersion.UpdatedAt)
198− b = append(b, ';')
199− }
200−
201− // LSPs (sorted by name for a stable fingerprint), with diagnostic counts.
202− lspNames := make([]string, 0, len(m.lspStates))
203− for name := range m.lspStates {
204− lspNames = append(lspNames, name)
205− }
206− sort.Strings(lspNames)
207− for _, name := range lspNames {
208− st := m.lspStates[name]
209− // Read the split diagnostic counts from the local mirror instead
210− // of issuing a per-LSP diagnostics RPC every frame. The mirror is
211− // refreshed on every LSP event (state or diagnostics change), the
212− // same events that keep m.lspStates fresh, so this changes exactly
213− // when the rendered LSP section would.
214− counts := m.lspDiagnostics[name]
215− // Hash the error text, not just its presence: the render prints
216− // the message, so a changed message within the same state must
217− // invalidate the cache.
218− putStr("l=", st.Name)
219− putInt("|", int64(st.State))
220− putStr("|err=", errText(st.Error))
221− putInt("|", int64(counts.Error))
222− putInt("/", int64(counts.Warning))
223− putInt("/", int64(counts.Hint))
224− putInt("/", int64(counts.Information))
225− b = append(b, ';')
226− }
227−
228− // MCPs (config order, matching the render).
229− for _, mcpCfg := range mcpSorted {
230− state, ok := m.mcpStates[mcpCfg.Name]
231− if !ok {
232− continue
233− }
234− putStr("m=", state.Name)
235− putInt("|", int64(state.State))
236− putStr("|err=", errText(state.Error))
237− putInt("|", int64(state.Counts.Tools))
238− putInt("/", int64(state.Counts.Prompts))
239− putInt("/", int64(state.Counts.Resources))
240− b = append(b, ';')
241− }
242−
243− // Skills.
244− for _, it := range skillItems {
245− putStr("s=", it.icon)
246− putStr("|", it.name)
247− putStr("|", it.title)
248− b = append(b, ';')
249− }
250−
251− m.renderCache.sidebarFPBuf = b // retain the grown buffer for reuse next frame
252− h := fnv.New64a()
253− h.Write(b)
254− return h.Sum64()
255−}
256−
257−// errText returns err's message, or "" when nil. Used to fold error text
258−// into cache fingerprints.
259−func errText(err error) string {
260− if err == nil {
261− return ""
262− }
263− return err.Error()
264−}
265−
266// sidebar renders the chat sidebar containing session title, working
129// sidebar renders the chat sidebar containing session title, working
267// directory, model info, file list, LSP status, and MCP status.
130// directory, model info, file list, LSP status, and MCP status.
268func (m *UI) drawSidebar(scr uv.Screen, area uv.Rectangle) {
131func (m *UI) drawSidebar(scr uv.Screen, area uv.Rectangle) {
@@ -270,25 +133,6 @@ func (m *UI) drawSidebar(scr uv.Screen, area uv.Rectangle) {
270 return
133 return
271 }
134 }
272
135
273− // Compute the skill items and sorted MCP list once per render and
274− // thread them through both the fingerprint and buildSidebar, instead
275− // of recomputing (clone+sort / map build) them several times.
276− skillItems := m.skillStatusItems()
277− mcpSorted := m.com.Config().MCP.Sorted()
278−
279− key := m.sidebarFingerprint(area, skillItems, mcpSorted)
280− if !m.renderCache.hasSidebarCache || key != m.renderCache.sidebarCacheKey {
281− m.renderCache.sidebarView = m.buildSidebar(area, skillItems, mcpSorted)
282− m.renderCache.sidebarCacheKey = key
283− m.renderCache.hasSidebarCache = true
284− }
285−
286− uv.NewStyledString(m.renderCache.sidebarView).Draw(scr, area)
287−}
288−
289−// buildSidebar renders the sidebar content block as a string. skillItems
290−// and mcpSorted are precomputed by drawSidebar and reused here.
291−func (m *UI) buildSidebar(area uv.Rectangle, skillItems []skillStatusItem, mcpSorted []config.MCP) string {
292 const logoHeightBreakpoint = 30
136 const logoHeightBreakpoint = 30
293
137
294 t := m.com.Styles
138 t := m.com.Styles
@@ -335,35 +179,37 @@ func (m *UI) buildSidebar(area uv.Rectangle, skillItems []skillStatusItem, mcpSo
335 lspsCount := len(m.lspStates)
179 lspsCount := len(m.lspStates)
336
180
337 mcpsCount := 0
181 mcpsCount := 0
338− for _, mcpCfg := range mcpSorted {
182+ for _, mcpCfg := range m.com.Config().MCP.Sorted() {
339 if _, ok := m.mcpStates[mcpCfg.Name]; ok {
183 if _, ok := m.mcpStates[mcpCfg.Name]; ok {
340 mcpsCount++
184 mcpsCount++
341 }
185 }
342 }
186 }
343
187
344− skillsCount := len(skillItems)
188+ skillsCount := len(m.skillStatusItems())
345
189
346 maxFiles, maxLSPs, maxMCPs, maxSkills := getDynamicHeightLimits(remainingHeight, filesCount, lspsCount, mcpsCount, skillsCount)
190 maxFiles, maxLSPs, maxMCPs, maxSkills := getDynamicHeightLimits(remainingHeight, filesCount, lspsCount, mcpsCount, skillsCount)
347
191
348 lspSection := m.lspInfo(width, maxLSPs, true)
192 lspSection := m.lspInfo(width, maxLSPs, true)
349− mcpSection := m.mcpInfoSorted(mcpSorted, width, maxMCPs, true)
193+ mcpSection := m.mcpInfo(width, maxMCPs, true)
350− skillsSection := m.skillsInfoItems(skillItems, width, maxSkills, true)
194+ skillsSection := m.skillsInfo(width, maxSkills, true)
351 filesSection := m.filesInfo(m.com.Workspace.WorkingDir(), width, maxFiles, true)
195 filesSection := m.filesInfo(m.com.Workspace.WorkingDir(), width, maxFiles, true)
352
196
353− return lipgloss.NewStyle().
197+ uv.NewStyledString(
354− MaxWidth(width).
198+ lipgloss.NewStyle().
355− MaxHeight(height).
199+ MaxWidth(width).
356− Render(
200+ MaxHeight(height).
357− lipgloss.JoinVertical(
201+ Render(
358− lipgloss.Left,
202+ lipgloss.JoinVertical(
359− sidebarHeader,
203+ lipgloss.Left,
360− filesSection,
204+ sidebarHeader,
361− "",
205+ filesSection,
362− lspSection,
206+ "",
363− "",
207+ lspSection,
364− mcpSection,
208+ "",
365− "",
209+ mcpSection,
366− skillsSection,
210+ "",
211+ skillsSection,
212+ ),
367 ),
213 ),
368− )
214+ ).Draw(scr, area)
369}
215}
internal/ui/model/skills.go+5−15
@@ -28,14 +28,7 @@ var builtinSkillsCache struct {
28
28
29func cachedBuiltinSkills() []*skills.Skill {
29func cachedBuiltinSkills() []*skills.Skill {
30 builtinSkillsCache.once.Do(func() {
30 builtinSkillsCache.once.Do(func() {
31− builtin := skills.DiscoverBuiltin()
31+ builtinSkillsCache.skills = skills.DiscoverBuiltin()
32− // Sort once here rather than on every skillStatusItems call: the
33− // returned slice is shared, so sorting it in place repeatedly is
34− // wasted work (and mutates shared state on the render hot path).
35− slices.SortStableFunc(builtin, func(a, b *skills.Skill) int {
36− return strings.Compare(a.Name, b.Name)
37− })
38− builtinSkillsCache.skills = builtin
39 })
32 })
40 return builtinSkillsCache.skills
33 return builtinSkillsCache.skills
41}
34}
@@ -43,13 +36,6 @@ func cachedBuiltinSkills() []*skills.Skill {
43// skillsInfo renders the skill discovery status section showing loaded and
36// skillsInfo renders the skill discovery status section showing loaded and
44// invalid skills.
37// invalid skills.
45func (m *UI) skillsInfo(width, maxItems int, isSection bool) string {
38func (m *UI) skillsInfo(width, maxItems int, isSection bool) string {
46− return m.skillsInfoItems(m.skillStatusItems(), width, maxItems, isSection)
47−}
48−
49−// skillsInfoItems renders the skill status section from an already-computed
50−// item list, letting callers on the sidebar render path reuse a single
51−// skillStatusItems() result instead of recomputing it.
52−func (m *UI) skillsInfoItems(items []skillStatusItem, width, maxItems int, isSection bool) string {
53 t := m.com.Styles
39 t := m.com.Styles
54
40
55 title := t.Resource.Heading.Render("Skills")
41 title := t.Resource.Heading.Render("Skills")
@@ -57,6 +43,7 @@ func (m *UI) skillsInfoItems(items []skillStatusItem, width, maxItems int, isSec
57 title = common.Section(t, title, width)
43 title = common.Section(t, title, width)
58 }
44 }
59
45
46+ items := m.skillStatusItems()
60 if len(items) == 0 {
47 if len(items) == 0 {
61 list := t.Resource.AdditionalText.Render("None")
48 list := t.Resource.AdditionalText.Render("None")
62 return lipgloss.NewStyle().Width(width).Render(fmt.Sprintf("%s\n\n%s", title, list))
49 return lipgloss.NewStyle().Width(width).Render(fmt.Sprintf("%s\n\n%s", title, list))
@@ -108,6 +95,9 @@ func (m *UI) skillStatusItems() []skillStatusItem {
108 }
95 }
109
96
110 builtin := cachedBuiltinSkills()
97 builtin := cachedBuiltinSkills()
98+ slices.SortStableFunc(builtin, func(a, b *skills.Skill) int {
99+ return strings.Compare(a.Name, b.Name)
100+ })
111 for _, skill := range builtin {
101 for _, skill := range builtin {
112 if _, ok := stateNames[skill.Name]; ok {
102 if _, ok := stateNames[skill.Name]; ok {
113 continue
103 continue
internal/ui/model/ui.go+86−303
@@ -38,7 +38,6 @@ import (
38 "github.com/charmbracelet/crush/internal/fsext"
38 "github.com/charmbracelet/crush/internal/fsext"
39 "github.com/charmbracelet/crush/internal/history"
39 "github.com/charmbracelet/crush/internal/history"
40 "github.com/charmbracelet/crush/internal/home"
40 "github.com/charmbracelet/crush/internal/home"
41− "github.com/charmbracelet/crush/internal/lsp"
42 "github.com/charmbracelet/crush/internal/message"
41 "github.com/charmbracelet/crush/internal/message"
43 "github.com/charmbracelet/crush/internal/permission"
42 "github.com/charmbracelet/crush/internal/permission"
44 "github.com/charmbracelet/crush/internal/pubsub"
43 "github.com/charmbracelet/crush/internal/pubsub"
@@ -175,38 +174,7 @@ type (
175)
174)
176
175
177// UI represents the main user interface model.
176// UI represents the main user interface model.
178−// renderCache holds the per-frame render memoization the UI consults on
179−// every draw: the sidebar block (drawSidebar re-renders fully each frame),
180−// the resolved large model (an RPC in client mode), and the pills block.
181−// Grouped out of the UI struct so the memoization layer is separable from
182−// model state.
183−type renderCache struct {
184− // sidebarView is the memoized sidebar block, keyed by sidebarCacheKey
185− // (a fingerprint of every input affecting its output).
186− sidebarView string
187− sidebarCacheKey uint64
188− hasSidebarCache bool
189− // sidebarFPBuf is a scratch buffer reused across frames to build the
190− // sidebar fingerprint without allocating (see sidebarFingerprint).
191− sidebarFPBuf []byte
192−
193− // largeModel caches the resolved selected large model — resolving it
194− // hits the agent coordinator (an RPC in client mode) — keyed by
195− // largeModelCacheKey. A nil result is never cached so the
196− // not-ready → ready transition is picked up on the next frame.
197− largeModel *workspace.AgentModel
198− largeModelCacheKey string
199−
200− // pillsView is the memoized pills block, keyed by pillsCacheKey.
201− pillsView string
202− pillsCacheKey string
203−}
204−
205type UI struct {
177type UI struct {
206− // ctx is the program's lifecycle context (cancelled on SIGINT / program
207− // exit). Async workspace operations launched from tea.Cmd closures use
208− // it so they can be cancelled at teardown instead of orphaning.
209− ctx context.Context
210 com *common.Common
178 com *common.Common
211 session *session.Session
179 session *session.Session
212 sessionFiles []SessionFile
180 sessionFiles []SessionFile
@@ -230,15 +198,9 @@ type UI struct {
230
198
231 // themeKey identifies the currently applied theme so applyTheme can
199 // themeKey identifies the currently applied theme so applyTheme can
232 // skip the expensive style rebuild when switching to a provider that
200 // skip the expensive style rebuild when switching to a provider that
233− // resolves to the same theme. It encodes both the provider theme family
201+ // resolves to the same theme.
234− // and the light/dark background mode.
235 themeKey string
202 themeKey string
236
203
237− // dark tracks the most recently detected terminal background mode (from
238− // tea.BackgroundColorMsg). It defaults to true and is only consulted
239− // when the configured theme is "auto"; see effectiveDark.
240− dark bool
241−
242 focus uiFocusState
204 focus uiFocusState
243 state uiState
205 state uiState
244
206
@@ -251,12 +213,6 @@ type UI struct {
251 // isCanceling tracks whether the user has pressed escape once to cancel.
213 // isCanceling tracks whether the user has pressed escape once to cancel.
252 isCanceling bool
214 isCanceling bool
253
215
254− // lastError holds the full text of the most recent error/warning status
255− // message. Error/warning notifications are truncated to a single line in
256− // the status bar and are not auto-expired; this retains the untruncated
257− // text so it can be copied to the clipboard (keyMap.CopyError).
258− lastError string
259−
260 // bangMode tracks whether the editor is in bang (!) shell mode.
216 // bangMode tracks whether the editor is in bang (!) shell mode.
261 bangMode bool
217 bangMode bool
262 bangWasEmpty bool // true when bang prompt became empty on last keystroke
218 bangWasEmpty bool // true when bang prompt became empty on last keystroke
@@ -309,13 +265,6 @@ type UI struct {
309
265
310 // lsp
266 // lsp
311 lspStates map[string]workspace.LSPClientInfo
267 lspStates map[string]workspace.LSPClientInfo
312− // lspDiagnostics mirrors the per-LSP diagnostic severity counts
313− // locally. It is refreshed only when an LSP event arrives (the same
314− // events that already refresh lspStates), so the sidebar fingerprint
315− // — computed every frame — can read the split counts without issuing
316− // a per-LSP diagnostics RPC (which, in client mode, fetches and
317− // decodes every diagnostic).
318− lspDiagnostics map[string]lsp.DiagnosticCounts
319
268
320 // mcp
269 // mcp
321 mcpStates map[string]mcp.ClientInfo
270 mcpStates map[string]mcp.ClientInfo
@@ -326,10 +275,6 @@ type UI struct {
326 // sidebarLogo keeps a cached version of the sidebar sidebarLogo.
275 // sidebarLogo keeps a cached version of the sidebar sidebarLogo.
327 sidebarLogo string
276 sidebarLogo string
328
277
329− // renderCache holds the per-frame render memoization (sidebar, pills,
330− // and resolved large-model caches); see the renderCache type.
331− renderCache renderCache
332−
333 // Notification state
278 // Notification state
334 notifyBackend notification.Backend
279 notifyBackend notification.Backend
335 notifyWindowFocused bool
280 notifyWindowFocused bool
@@ -352,6 +297,7 @@ type UI struct {
352 pillsAutoExpanded bool
297 pillsAutoExpanded bool
353 focusedPillSection pillSection
298 focusedPillSection pillSection
354 promptQueue int
299 promptQueue int
300+ pillsView string
355
301
356 // Todo spinner
302 // Todo spinner
357 todoSpinner spinner.Model
303 todoSpinner spinner.Model
@@ -372,7 +318,7 @@ type UI struct {
372}
318}
373
319
374// New creates a new instance of the [UI] model.
320// New creates a new instance of the [UI] model.
375−func New(ctx context.Context, com *common.Common, initialSessionID string, continueLast bool) *UI {
321+func New(com *common.Common, initialSessionID string, continueLast bool) *UI {
376 // Editor components
322 // Editor components
377 ta := textarea.New()
323 ta := textarea.New()
378 ta.SetStyles(com.Styles.Editor.Textarea)
324 ta.SetStyles(com.Styles.Editor.Textarea)
@@ -423,7 +369,6 @@ func New(ctx context.Context, com *common.Common, initialSessionID string, conti
423 header := newHeader(com)
369 header := newHeader(com)
424
370
425 ui := &UI{
371 ui := &UI{
426− ctx: ctx,
427 com: com,
372 com: com,
428 dialog: dialog.NewOverlay(),
373 dialog: dialog.NewOverlay(),
429 keyMap: keyMap,
374 keyMap: keyMap,
@@ -434,7 +379,6 @@ func New(ctx context.Context, com *common.Common, initialSessionID string, conti
434 attachments: attachments,
379 attachments: attachments,
435 todoSpinner: todoSpinner,
380 todoSpinner: todoSpinner,
436 lspStates: make(map[string]workspace.LSPClientInfo),
381 lspStates: make(map[string]workspace.LSPClientInfo),
437− lspDiagnostics: make(map[string]lsp.DiagnosticCounts),
438 mcpStates: make(map[string]mcp.ClientInfo),
382 mcpStates: make(map[string]mcp.ClientInfo),
439 notifyBackend: notification.NoopBackend{},
383 notifyBackend: notification.NoopBackend{},
440 notifyWindowFocused: true,
384 notifyWindowFocused: true,
@@ -447,17 +391,8 @@ func New(ctx context.Context, com *common.Common, initialSessionID string, conti
447
391
448 // Seed the active theme key from the large model provider so the
392 // Seed the active theme key from the large model provider so the
449 // first model selection can correctly skip a redundant theme swap.
393 // first model selection can correctly skip a redundant theme swap.
450− // DefaultCommon builds the dark theme at startup, so the mode is seeded
451− // dark; a tea.BackgroundColorMsg (or a forced "light" config) reconciles
452− // it to the light palette on the first frame.
453− ui.dark = true
454 if cfg := com.Config(); cfg != nil {
394 if cfg := com.Config(); cfg != nil {
455− // Seed with the dark key because DefaultCommon builds the dark theme;
395+ ui.themeKey = styles.ThemeKeyForProvider(cfg.Models[config.SelectedModelTypeLarge].Provider)
456− // reconcileTheme applies the light palette on the first frame when
457− // the terminal reports a light background or "light" is forced.
458− ui.themeKey = styles.ThemeKeyForProviderMode(
459− cfg.Models[config.SelectedModelTypeLarge].Provider, true,
460− )
461 }
396 }
462
397
463 ui.setEditorPrompt(com.Workspace.PermissionSkipRequests())
398 ui.setEditorPrompt(com.Workspace.PermissionSkipRequests())
@@ -495,9 +430,6 @@ func New(ctx context.Context, com *common.Common, initialSessionID string, conti
495// Init initializes the UI model.
430// Init initializes the UI model.
496func (m *UI) Init() tea.Cmd {
431func (m *UI) Init() tea.Cmd {
497 var cmds []tea.Cmd
432 var cmds []tea.Cmd
498− // Ask the terminal for its background color so we can pick the light or
499− // dark palette (see the tea.BackgroundColorMsg handler in Update).
500− cmds = append(cmds, tea.RequestBackgroundColor)
501 if m.state == uiOnboarding {
433 if m.state == uiOnboarding {
502 if cmd := m.openModelsDialog(); cmd != nil {
434 if cmd := m.openModelsDialog(); cmd != nil {
503 cmds = append(cmds, cmd)
435 cmds = append(cmds, cmd)
@@ -527,7 +459,7 @@ func (m *UI) loadInitialSession() tea.Cmd {
527 return m.loadSession(m.initialSessionID)
459 return m.loadSession(m.initialSessionID)
528 case m.continueLastSession:
460 case m.continueLastSession:
529 return func() tea.Msg {
461 return func() tea.Msg {
530− sessions, err := m.com.Workspace.ListSessions(m.ctx)
462+ sessions, err := m.com.Workspace.ListSessions(context.Background())
531 if err != nil || len(sessions) == 0 {
463 if err != nil || len(sessions) == 0 {
532 return nil
464 return nil
533 }
465 }
@@ -609,32 +541,14 @@ func (m *UI) updateNotificationBackend() {
609}
541}
610
542
611// shouldSendNotification returns true if notifications should be sent based on
543// shouldSendNotification returns true if notifications should be sent based on
612−// current state. The window must not be focused and notifications must not be
544+// current state. Focus reporting must be supported, window must not be
613−// disabled in config. Focus reporting is only required in auto-detection mode:
545+// focused, and notifications must not be disabled in config.
614−// when the user explicitly chooses a notification style, we honor it even on
615−// terminals that don't support focus events (e.g. SSH sessions).
616func (m *UI) shouldSendNotification() bool {
546func (m *UI) shouldSendNotification() bool {
617 cfg := m.com.Config()
547 cfg := m.com.Config()
618− var style string
548+ if cfg != nil && cfg.Options != nil && cfg.Options.NotificationStyle == "disabled" {
619− if cfg != nil && cfg.Options != nil {
620− style = cfg.Options.NotificationStyle
621− }
622− if style == "disabled" {
623 return false
549 return false
624 }
550 }
625− // Never notify while the window is focused. On terminals without focus
551+ return m.caps.ReportFocusEvents && !m.notifyWindowFocused
626− // reporting, notifyWindowFocused stays false, so this doesn't suppress.
627− if m.notifyWindowFocused {
628− return false
629− }
630− // An explicit style (anything other than auto/empty) is honored regardless
631− // of focus event support.
632− if style != "" && style != "auto" {
633− return true
634− }
635− // Auto-detection mode: without focus events we can't suppress notifications
636− // while focused, so we disable them entirely to avoid spamming the user.
637− return m.caps.ReportFocusEvents
638}
552}
639
553
640// setState changes the UI state and focus.
554// setState changes the UI state and focus.
@@ -657,7 +571,7 @@ func (m *UI) loadCustomCommands() tea.Cmd {
657 slog.Error("Failed to load custom commands", "error", err)
571 slog.Error("Failed to load custom commands", "error", err)
658 }
572 }
659 // Append user-invocable skills as commands.
573 // Append user-invocable skills as commands.
660− skillEntries, err := m.com.Workspace.ListSkills(m.ctx)
574+ skillEntries, err := m.com.Workspace.ListSkills(context.Background())
661 if err != nil {
575 if err != nil {
662 slog.Error("Failed to load skill commands", "error", err)
576 slog.Error("Failed to load skill commands", "error", err)
663 }
577 }
@@ -718,7 +632,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
718 m.session = msg.session
632 m.session = msg.session
719 m.sessionFiles = msg.files
633 m.sessionFiles = msg.files
720 cmds = append(cmds, m.startLSPs(msg.lspFilePaths()))
634 cmds = append(cmds, m.startLSPs(msg.lspFilePaths()))
721− msgs, err := m.com.Workspace.ListMessages(m.ctx, m.session.ID)
635+ msgs, err := m.com.Workspace.ListMessages(context.Background(), m.session.ID)
722 if err != nil {
636 if err != nil {
723 cmds = append(cmds, util.ReportError(err))
637 cmds = append(cmds, util.ReportError(err))
724 break
638 break
@@ -858,9 +772,9 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
858 case pubsub.Event[history.File]:
772 case pubsub.Event[history.File]:
859 cmds = append(cmds, m.handleFileEvent(msg.Payload))
773 cmds = append(cmds, m.handleFileEvent(msg.Payload))
860 case pubsub.Event[app.LSPEvent]:
774 case pubsub.Event[app.LSPEvent]:
861− m.refreshLSPState()
775+ m.lspStates = m.com.Workspace.LSPGetStates()
862 case pubsub.Event[workspace.LSPEvent]:
776 case pubsub.Event[workspace.LSPEvent]:
863− m.refreshLSPState()
777+ m.lspStates = m.com.Workspace.LSPGetStates()
864 case pubsub.Event[skills.Event]:
778 case pubsub.Event[skills.Event]:
865 m.skillStates = msg.Payload.States
779 m.skillStates = msg.Payload.States
866 case pubsub.Event[mcp.Event]:
780 case pubsub.Event[mcp.Event]:
@@ -882,7 +796,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
882 cmds = append(cmds, cmd)
796 cmds = append(cmds, cmd)
883 }
797 }
884 if cmd := m.sendNotification(notification.Notification{
798 if cmd := m.sendNotification(notification.Notification{
885− Title: "Crush needs permission",
799+ Title: "Crush is waiting...",
886 Message: fmt.Sprintf("Permission required to execute \"%s\"", msg.Payload.ToolName),
800 Message: fmt.Sprintf("Permission required to execute \"%s\"", msg.Payload.ToolName),
887 }); cmd != nil {
801 }); cmd != nil {
888 cmds = append(cmds, cmd)
802 cmds = append(cmds, cmd)
@@ -898,17 +812,9 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
898 m.sendProgressBar = xstrings.ContainsAnyOf(termVersion, "ghostty", "iterm2", "rio")
812 m.sendProgressBar = xstrings.ContainsAnyOf(termVersion, "ghostty", "iterm2", "rio")
899 }
813 }
900 return m, nil
814 return m, nil
901− case tea.BackgroundColorMsg:
902− // The terminal reported its background color; switch to the light
903− // or dark palette to match (unless the theme is forced via config).
904− m.dark = msg.IsDark()
905− m.reconcileTheme()
906 case tea.WindowSizeMsg:
815 case tea.WindowSizeMsg:
907 m.width, m.height = msg.Width, msg.Height
816 m.width, m.height = msg.Width, msg.Height
908 m.updateLayoutAndSize()
817 m.updateLayoutAndSize()
909− // Fallback for terminals that never answer the background-color
910− // query: a forced "light"/"dark" config still takes effect here.
911− m.reconcileTheme()
912 if m.state == uiChat && m.chat.Follow() {
818 if m.state == uiChat && m.chat.Follow() {
913 if cmd := m.chat.ScrollToBottomAndAnimate(); cmd != nil {
819 if cmd := m.chat.ScrollToBottomAndAnimate(); cmd != nil {
914 cmds = append(cmds, cmd)
820 cmds = append(cmds, cmd)
@@ -1152,18 +1058,11 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
1152 slog.Error("Error reported", "error", msg.Msg)
1058 slog.Error("Error reported", "error", msg.Msg)
1153 }
1059 }
1154 m.status.SetInfoMsg(msg)
1060 m.status.SetInfoMsg(msg)
1155− // Errors and warnings persist until replaced or dismissed (rather than
1061+ ttl := msg.TTL
1156− // auto-expiring like info/success/update) so the user can read them and
1062+ if ttl <= 0 {
1157− // copy the full text (ctrl+e). Retain the untruncated message for that.
1063+ ttl = DefaultStatusTTL
1158− if msg.Type == util.InfoTypeError || msg.Type == util.InfoTypeWarn {
1159− m.lastError = msg.Msg
1160− } else {
1161− ttl := msg.TTL
1162− if ttl <= 0 {
1163− ttl = DefaultStatusTTL
1164− }
1165− cmds = append(cmds, clearInfoMsgCmd(ttl))
1166 }
1064 }
1065+ cmds = append(cmds, clearInfoMsgCmd(ttl))
1167 case app.UpdateAvailableMsg:
1066 case app.UpdateAvailableMsg:
1168 text := fmt.Sprintf("Crush update available: v%s → v%s.", msg.CurrentVersion, msg.LatestVersion)
1067 text := fmt.Sprintf("Crush update available: v%s → v%s.", msg.CurrentVersion, msg.LatestVersion)
1169 if msg.IsDevelopment {
1068 if msg.IsDevelopment {
@@ -1228,23 +1127,25 @@ func (m *UI) setSessionMessages(msgs []message.Message) tea.Cmd {
1228 msgPtrs[i] = &msgs[i]
1127 msgPtrs[i] = &msgs[i]
1229 }
1128 }
1230 toolResultMap := chat.BuildToolResultMap(msgPtrs)
1129 toolResultMap := chat.BuildToolResultMap(msgPtrs)
1130+ if len(msgPtrs) > 0 {
1131+ m.lastUserMessageTime = msgPtrs[0].CreatedAt
1132+ }
1231
1133
1232 // Add messages to chat with linked tool results
1134 // Add messages to chat with linked tool results
1233− diffTool := m.com.Config().Options.TUI.DiffTool
1234 items := make([]chat.MessageItem, 0, len(msgs)*2)
1135 items := make([]chat.MessageItem, 0, len(msgs)*2)
1235 for _, msg := range msgPtrs {
1136 for _, msg := range msgPtrs {
1236 switch msg.Role {
1137 switch msg.Role {
1237 case message.User:
1138 case message.User:
1238 m.lastUserMessageTime = msg.CreatedAt
1139 m.lastUserMessageTime = msg.CreatedAt
1239− items = append(items, chat.ExtractMessageItems(m.com.Styles, msg, toolResultMap, diffTool)...)
1140+ items = append(items, chat.ExtractMessageItems(m.com.Styles, msg, toolResultMap)...)
1240 case message.Assistant:
1141 case message.Assistant:
1241− items = append(items, chat.ExtractMessageItems(m.com.Styles, msg, toolResultMap, diffTool)...)
1142+ items = append(items, chat.ExtractMessageItems(m.com.Styles, msg, toolResultMap)...)
1242 if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
1143 if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
1243 infoItem := chat.NewAssistantInfoItem(m.com.Styles, msg, m.com.Config(), time.Unix(m.lastUserMessageTime, 0))
1144 infoItem := chat.NewAssistantInfoItem(m.com.Styles, msg, m.com.Config(), time.Unix(m.lastUserMessageTime, 0))
1244 items = append(items, infoItem)
1145 items = append(items, infoItem)
1245 }
1146 }
1246 default:
1147 default:
1247− items = append(items, chat.ExtractMessageItems(m.com.Styles, msg, toolResultMap, diffTool)...)
1148+ items = append(items, chat.ExtractMessageItems(m.com.Styles, msg, toolResultMap)...)
1248 }
1149 }
1249 }
1150 }
1250
1151
@@ -1290,7 +1191,7 @@ func (m *UI) loadNestedToolCalls(items []chat.MessageItem) {
1290 agentSessionID := m.com.Workspace.CreateAgentToolSessionID(messageID, tc.ID)
1191 agentSessionID := m.com.Workspace.CreateAgentToolSessionID(messageID, tc.ID)
1291
1192
1292 // Fetch nested messages.
1193 // Fetch nested messages.
1293− nestedMsgs, err := m.com.Workspace.ListMessages(m.ctx, agentSessionID)
1194+ nestedMsgs, err := m.com.Workspace.ListMessages(context.Background(), agentSessionID)
1294 if err != nil || len(nestedMsgs) == 0 {
1195 if err != nil || len(nestedMsgs) == 0 {
1295 continue
1196 continue
1296 }
1197 }
@@ -1305,7 +1206,7 @@ func (m *UI) loadNestedToolCalls(items []chat.MessageItem) {
1305 // Extract nested tool items.
1206 // Extract nested tool items.
1306 var nestedTools []chat.ToolMessageItem
1207 var nestedTools []chat.ToolMessageItem
1307 for _, nestedMsg := range nestedMsgPtrs {
1208 for _, nestedMsg := range nestedMsgPtrs {
1308− nestedItems := chat.ExtractMessageItems(m.com.Styles, nestedMsg, nestedToolResultMap, m.com.Config().Options.TUI.DiffTool)
1209+ nestedItems := chat.ExtractMessageItems(m.com.Styles, nestedMsg, nestedToolResultMap)
1309 for _, nestedItem := range nestedItems {
1210 for _, nestedItem := range nestedItems {
1310 if nestedToolItem, ok := nestedItem.(chat.ToolMessageItem); ok {
1211 if nestedToolItem, ok := nestedItem.(chat.ToolMessageItem); ok {
1311 // Mark nested tools as simple (compact) rendering.
1212 // Mark nested tools as simple (compact) rendering.
@@ -1355,7 +1256,7 @@ func (m *UI) appendSessionMessage(msg message.Message) tea.Cmd {
1355 return nil
1256 return nil
1356 }
1257 }
1357 m.lastUserMessageTime = msg.CreatedAt
1258 m.lastUserMessageTime = msg.CreatedAt
1358− items := chat.ExtractMessageItems(m.com.Styles, &msg, nil, m.com.Config().Options.TUI.DiffTool)
1259+ items := chat.ExtractMessageItems(m.com.Styles, &msg, nil)
1359 for _, item := range items {
1260 for _, item := range items {
1360 if animatable, ok := item.(chat.Animatable); ok {
1261 if animatable, ok := item.(chat.Animatable); ok {
1361 if cmd := animatable.StartAnimation(); cmd != nil {
1262 if cmd := animatable.StartAnimation(); cmd != nil {
@@ -1368,7 +1269,7 @@ func (m *UI) appendSessionMessage(msg message.Message) tea.Cmd {
1368 cmds = append(cmds, cmd)
1269 cmds = append(cmds, cmd)
1369 }
1270 }
1370 case message.Assistant:
1271 case message.Assistant:
1371− items := chat.ExtractMessageItems(m.com.Styles, &msg, nil, m.com.Config().Options.TUI.DiffTool)
1272+ items := chat.ExtractMessageItems(m.com.Styles, &msg, nil)
1372 for _, item := range items {
1273 for _, item := range items {
1373 if animatable, ok := item.(chat.Animatable); ok {
1274 if animatable, ok := item.(chat.Animatable); ok {
1374 if cmd := animatable.StartAnimation(); cmd != nil {
1275 if cmd := animatable.StartAnimation(); cmd != nil {
@@ -1477,7 +1378,7 @@ func (m *UI) updateSessionMessage(msg message.Message) tea.Cmd {
1477 }
1378 }
1478 }
1379 }
1479 if existingToolItem == nil {
1380 if existingToolItem == nil {
1480− items = append(items, chat.NewToolMessageItem(m.com.Styles, msg.ID, tc, nil, false, m.com.Config().Options.TUI.DiffTool))
1381+ items = append(items, chat.NewToolMessageItem(m.com.Styles, msg.ID, tc, nil, false))
1481 }
1382 }
1482 }
1383 }
1483
1384
@@ -1518,13 +1419,18 @@ func (m *UI) handleChildSessionMessage(event pubsub.Event[message.Message]) tea.
1518
1419
1519 // Find the parent agent tool item.
1420 // Find the parent agent tool item.
1520 var agentItem chat.NestedToolContainer
1421 var agentItem chat.NestedToolContainer
1521− if item := m.chat.MessageItem(toolCallID); item != nil {
1422+ for i := 0; i < m.chat.Len(); i++ {
1423+ item := m.chat.MessageItem(toolCallID)
1424+ if item == nil {
1425+ continue
1426+ }
1522 if agent, ok := item.(chat.NestedToolContainer); ok {
1427 if agent, ok := item.(chat.NestedToolContainer); ok {
1523 if toolMessageItem, ok := item.(chat.ToolMessageItem); ok {
1428 if toolMessageItem, ok := item.(chat.ToolMessageItem); ok {
1524 if toolMessageItem.ToolCall().ID == toolCallID {
1429 if toolMessageItem.ToolCall().ID == toolCallID {
1525 // Verify this agent belongs to the correct parent message.
1430 // Verify this agent belongs to the correct parent message.
1526 // We can't directly check parentMessageID on the item, so we trust the session parsing.
1431 // We can't directly check parentMessageID on the item, so we trust the session parsing.
1527 agentItem = agent
1432 agentItem = agent
1433+ break
1528 }
1434 }
1529 }
1435 }
1530 }
1436 }
@@ -1549,7 +1455,7 @@ func (m *UI) handleChildSessionMessage(event pubsub.Event[message.Message]) tea.
1549 }
1455 }
1550 if !found {
1456 if !found {
1551 // Create a new nested tool item.
1457 // Create a new nested tool item.
1552− nestedItem := chat.NewToolMessageItem(m.com.Styles, event.Payload.ID, tc, nil, false, m.com.Config().Options.TUI.DiffTool)
1458+ nestedItem := chat.NewToolMessageItem(m.com.Styles, event.Payload.ID, tc, nil, false)
1553 if simplifiable, ok := nestedItem.(chat.Compactable); ok {
1459 if simplifiable, ok := nestedItem.(chat.Compactable); ok {
1554 simplifiable.SetCompact(true)
1460 simplifiable.SetCompact(true)
1555 }
1461 }
@@ -1670,7 +1576,7 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
1670 break
1576 break
1671 }
1577 }
1672 cmds = append(cmds, func() tea.Msg {
1578 cmds = append(cmds, func() tea.Msg {
1673− err := m.com.Workspace.AgentSummarize(m.ctx, msg.SessionID)
1579+ err := m.com.Workspace.AgentSummarize(context.Background(), msg.SessionID)
1674 if err != nil {
1580 if err != nil {
1675 return util.ReportError(err)()
1581 return util.ReportError(err)()
1676 }
1582 }
@@ -1716,9 +1622,7 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
1716 if err := m.com.Workspace.UpdatePreferredModel(config.ScopeGlobal, agentCfg.Model, currentModel); err != nil {
1622 if err := m.com.Workspace.UpdatePreferredModel(config.ScopeGlobal, agentCfg.Model, currentModel); err != nil {
1717 return util.ReportError(err)()
1623 return util.ReportError(err)()
1718 }
1624 }
1719− if err := m.com.Workspace.UpdateAgentModel(m.ctx); err != nil {
1625+ m.com.Workspace.UpdateAgentModel(context.TODO())
1720− slog.Warn("failed to update agent model", "error", err)
1721− }
1722 status := "disabled"
1626 status := "disabled"
1723 if currentModel.Think {
1627 if currentModel.Think {
1724 status = "enabled"
1628 status = "enabled"
@@ -1757,7 +1661,7 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
1757 cmds = append(cmds, m.disableDockerMCP)
1661 cmds = append(cmds, m.disableDockerMCP)
1758 case dialog.ActionInitializeProject:
1662 case dialog.ActionInitializeProject:
1759 if m.isAgentBusy() {
1663 if m.isAgentBusy() {
1760− cmds = append(cmds, util.ReportWarn("Agent is busy, please wait before initializing project..."))
1664+ cmds = append(cmds, util.ReportWarn("Agent is busy, please wait before summarizing session..."))
1761 break
1665 break
1762 }
1666 }
1763 cmds = append(cmds, m.initializeProject())
1667 cmds = append(cmds, m.initializeProject())
@@ -1793,9 +1697,7 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
1793 }
1697 }
1794
1698
1795 cmds = append(cmds, func() tea.Msg {
1699 cmds = append(cmds, func() tea.Msg {
1796− if err := m.com.Workspace.UpdateAgentModel(m.ctx); err != nil {
1700+ m.com.Workspace.UpdateAgentModel(context.TODO())
1797− slog.Warn("failed to update agent model", "error", err)
1798− }
1799 return util.NewInfoMsg("Reasoning effort set to " + msg.Effort)
1701 return util.NewInfoMsg("Reasoning effort set to " + msg.Effort)
1800 })
1702 })
1801 m.dialog.CloseDialog(dialog.ReasoningID)
1703 m.dialog.CloseDialog(dialog.ReasoningID)
@@ -1886,7 +1788,7 @@ func substituteArgs(content string, args map[string]string) string {
1886// OAuth dialog opens.
1788// OAuth dialog opens.
1887func (m *UI) refreshHyperAndRetrySelect(msg dialog.ActionSelectModel) tea.Cmd {
1789func (m *UI) refreshHyperAndRetrySelect(msg dialog.ActionSelectModel) tea.Cmd {
1888 return func() tea.Msg {
1790 return func() tea.Msg {
1889− ctx, cancel := context.WithTimeout(m.ctx, 15*time.Second)
1791+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
1890 defer cancel()
1792 defer cancel()
1891 if err := m.com.Workspace.RefreshOAuthToken(ctx, config.ScopeGlobal, "hyper"); err != nil {
1793 if err := m.com.Workspace.RefreshOAuthToken(ctx, config.ScopeGlobal, "hyper"); err != nil {
1892 slog.Warn("Hyper OAuth refresh failed, requesting re-auth", "error", err)
1794 slog.Warn("Hyper OAuth refresh failed, requesting re-auth", "error", err)
@@ -1921,7 +1823,7 @@ func (m *UI) fetchHyperCredits() tea.Cmd {
1921 }
1823 }
1922
1824
1923 if providerCfg.OAuthToken != nil && providerCfg.OAuthToken.IsExpired() {
1825 if providerCfg.OAuthToken != nil && providerCfg.OAuthToken.IsExpired() {
1924− ctxRefresh, cancelRefresh := context.WithTimeout(m.ctx, 15*time.Second)
1826+ ctxRefresh, cancelRefresh := context.WithTimeout(context.Background(), 15*time.Second)
1925 defer cancelRefresh()
1827 defer cancelRefresh()
1926 if err := m.com.Workspace.RefreshOAuthToken(ctxRefresh, config.ScopeGlobal, hyper.Name); err != nil {
1828 if err := m.com.Workspace.RefreshOAuthToken(ctxRefresh, config.ScopeGlobal, hyper.Name); err != nil {
1927 slog.Warn("Hyper OAuth refresh failed before fetching credits, trying with existing token", "error", err)
1829 slog.Warn("Hyper OAuth refresh failed before fetching credits, trying with existing token", "error", err)
@@ -1930,7 +1832,7 @@ func (m *UI) fetchHyperCredits() tea.Cmd {
1930 }
1832 }
1931 }
1833 }
1932
1834
1933− ctx, cancel := context.WithTimeout(m.ctx, 10*time.Second)
1835+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
1934 defer cancel()
1836 defer cancel()
1935 credits, err := hyper.FetchCredits(ctx, apiKey)
1837 credits, err := hyper.FetchCredits(ctx, apiKey)
1936 if err != nil {
1838 if err != nil {
@@ -2006,7 +1908,7 @@ func (m *UI) handleSelectModel(msg dialog.ActionSelectModel) tea.Cmd {
2006 }
1908 }
2007
1909
2008 cmds = append(cmds, func() tea.Msg {
1910 cmds = append(cmds, func() tea.Msg {
2009− if err := m.com.Workspace.UpdateAgentModel(m.ctx); err != nil {
1911+ if err := m.com.Workspace.UpdateAgentModel(context.TODO()); err != nil {
2010 return util.ReportError(err)
1912 return util.ReportError(err)
2011 }
1913 }
2012
1914
@@ -2029,7 +1931,7 @@ func (m *UI) handleSelectModel(msg dialog.ActionSelectModel) tea.Cmd {
2029 if isOnboarding {
1931 if isOnboarding {
2030 m.setState(uiLanding, uiFocusEditor)
1932 m.setState(uiLanding, uiFocusEditor)
2031 m.com.Config().SetupAgents()
1933 m.com.Config().SetupAgents()
2032− if err := m.com.Workspace.InitCoderAgent(m.ctx); err != nil {
1934+ if err := m.com.Workspace.InitCoderAgent(context.TODO()); err != nil {
2033 cmds = append(cmds, util.ReportError(err))
1935 cmds = append(cmds, util.ReportError(err))
2034 }
1936 }
2035 } else if m.com.IsHyper() {
1937 } else if m.com.IsHyper() {
@@ -2131,12 +2033,6 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
2131 }
2033 }
2132 cmds = append(cmds, util.ReportInfo("Yolo mode "+status))
2034 cmds = append(cmds, util.ReportInfo("Yolo mode "+status))
2133 return true
2035 return true
2134− case key.Matches(msg, m.keyMap.CopyError):
2135− if m.lastError == "" {
2136− return false
2137− }
2138− cmds = append(cmds, common.CopyToClipboard(m.lastError, "Error copied to clipboard"))
2139− return true
2140 }
2036 }
2141 return false
2037 return false
2142 }
2038 }
@@ -2503,13 +2399,6 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
2503
2399
2504// drawHeader draws the header section of the UI.
2400// drawHeader draws the header section of the UI.
2505func (m *UI) drawHeader(scr uv.Screen, area uv.Rectangle) {
2401func (m *UI) drawHeader(scr uv.Screen, area uv.Rectangle) {
2506− // Sum diagnostics from the local LSP mirror instead of an
2507− // LSPGetStates RPC per frame; m.lspStates is refreshed on every LSP
2508− // event, so the total changes exactly when the header would.
2509− lspErrorCount := 0
2510− for _, info := range m.lspStates {
2511− lspErrorCount += info.DiagnosticCount
2512− }
2513 m.header.drawHeader(
2402 m.header.drawHeader(
2514 scr,
2403 scr,
2515 area,
2404 area,
@@ -2518,7 +2407,6 @@ func (m *UI) drawHeader(scr uv.Screen, area uv.Rectangle) {
2518 m.detailsOpen,
2407 m.detailsOpen,
2519 area.Dx(),
2408 area.Dx(),
2520 m.hyperCredits,
2409 m.hyperCredits,
2521− lspErrorCount,
2522 )
2410 )
2523}
2411}
2524
2412
@@ -2570,8 +2458,8 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
2570 }
2458 }
2571
2459
2572 m.chat.Draw(scr, layout.main)
2460 m.chat.Draw(scr, layout.main)
2573− if layout.pills.Dy() > 0 && m.renderCache.pillsView != "" {
2461+ if layout.pills.Dy() > 0 && m.pillsView != "" {
2574− uv.NewStyledString(m.renderCache.pillsView).Draw(scr, layout.pills)
2462+ uv.NewStyledString(m.pillsView).Draw(scr, layout.pills)
2575 }
2463 }
2576
2464
2577 editorWidth := scr.Bounds().Dx()
2465 editorWidth := scr.Bounds().Dx()
@@ -2665,36 +2553,16 @@ func (m *UI) View() tea.View {
2665 canvas := uv.NewScreenBuffer(m.width, m.height)
2553 canvas := uv.NewScreenBuffer(m.width, m.height)
2666 v.Cursor = m.Draw(canvas, canvas.Bounds())
2554 v.Cursor = m.Draw(canvas, canvas.Bounds())
2667
2555
2668− // Single pass over the rendered screen: normalize \r\n → \n and trim
2556+ content := strings.ReplaceAll(canvas.Render(), "\r\n", "\n") // normalize newlines
2669− // trailing spaces per line, writing straight into one builder instead
2557+ contentLines := strings.Split(content, "\n")
2670− // of allocating a replaced copy, a line slice, and a joined copy.
2558+ for i, line := range contentLines {
2671− rendered := canvas.Render()
2559+ // Trim trailing spaces for concise rendering
2672− var sb strings.Builder
2560+ contentLines[i] = strings.TrimRight(line, " ")
2673− sb.Grow(len(rendered))
2674− start := 0
2675− for i := 0; i <= len(rendered); i++ {
2676− if i < len(rendered) && rendered[i] != '\n' {
2677− continue
2678− }
2679− line := rendered[start:i]
2680− // Drop a single trailing '\r' immediately before the newline
2681− // (the \r\n → \n normalization).
2682− if len(line) > 0 && line[len(line)-1] == '\r' {
2683− line = line[:len(line)-1]
2684− }
2685− // Trim trailing spaces only (matches strings.TrimRight(line, " ")).
2686− end := len(line)
2687− for end > 0 && line[end-1] == ' ' {
2688− end--
2689− }
2690− sb.WriteString(line[:end])
2691− if i < len(rendered) {
2692− sb.WriteByte('\n')
2693− }
2694− start = i + 1
2695 }
2561 }
2696
2562
2697− v.Content = sb.String()
2563+ content = strings.Join(contentLines, "\n")
2564+
2565+ v.Content = content
2698 if m.progressBarEnabled && m.sendProgressBar && m.isAgentBusy() {
2566 if m.progressBarEnabled && m.sendProgressBar && m.isAgentBusy() {
2699 // HACK: use a random percentage to prevent ghostty from hiding it
2567 // HACK: use a random percentage to prevent ghostty from hiding it
2700 // after a timeout.
2568 // after a timeout.
@@ -2748,9 +2616,6 @@ func (m *UI) ShortHelp() []key.Binding {
2748 binds,
2616 binds,
2749 k.Editor.Newline,
2617 k.Editor.Newline,
2750 )
2618 )
2751− if m.textarea.Value() == "" {
2752− binds = append(binds, k.Editor.ShellCommand)
2753− }
2754 case uiFocusMain:
2619 case uiFocusMain:
2755 binds = append(
2620 binds = append(
2756 binds,
2621 binds,
@@ -2764,16 +2629,8 @@ func (m *UI) ShortHelp() []key.Binding {
2764 binds = append(binds, k.Chat.PillLeft)
2629 binds = append(binds, k.Chat.PillLeft)
2765 }
2630 }
2766 }
2631 }
2767− case uiLanding:
2768− binds = append(
2769− binds,
2770− tab,
2771− commands,
2772− k.Models,
2773− k.Editor.Newline,
2774− k.Chat.NewSession,
2775− )
2776 default:
2632 default:
2633+ // TODO: other states
2777 // if m.session == nil {
2634 // if m.session == nil {
2778 // no session selected
2635 // no session selected
2779 binds = append(
2636 binds = append(
@@ -2784,10 +2641,6 @@ func (m *UI) ShortHelp() []key.Binding {
2784 )
2641 )
2785 }
2642 }
2786
2643
2787− if m.lastError != "" {
2788− binds = append(binds, k.CopyError)
2789− }
2790−
2791 binds = append(
2644 binds = append(
2792 binds,
2645 binds,
2793 k.Quit,
2646 k.Quit,
@@ -2860,9 +2713,6 @@ func (m *UI) FullHelp() [][]key.Binding {
2860 if m.currentModelSupportsImages() {
2713 if m.currentModelSupportsImages() {
2861 editorBinds = append(editorBinds, k.Editor.AddImage, k.Editor.PasteImage)
2714 editorBinds = append(editorBinds, k.Editor.AddImage, k.Editor.PasteImage)
2862 }
2715 }
2863− if m.textarea.Value() == "" {
2864− editorBinds = append(editorBinds, k.Editor.ShellCommand)
2865− }
2866 binds = append(binds, editorBinds)
2716 binds = append(binds, editorBinds)
2867 if hasAttachments {
2717 if hasAttachments {
2868 binds = append(
2718 binds = append(
@@ -2898,38 +2748,6 @@ func (m *UI) FullHelp() [][]key.Binding {
2898 binds = append(binds, []key.Binding{k.Chat.PillLeft})
2748 binds = append(binds, []key.Binding{k.Chat.PillLeft})
2899 }
2749 }
2900 }
2750 }
2901− case uiLanding:
2902− binds = append(
2903− binds,
2904− []key.Binding{
2905− commands,
2906− k.Models,
2907− k.Sessions,
2908− k.ToggleYolo,
2909− },
2910− )
2911− editorBinds := []key.Binding{
2912− k.Editor.Newline,
2913− k.Editor.MentionFile,
2914− k.Editor.OpenEditor,
2915− }
2916− if m.currentModelSupportsImages() {
2917− editorBinds = append(editorBinds, k.Editor.AddImage, k.Editor.PasteImage)
2918− }
2919− if m.textarea.Value() == "" {
2920− editorBinds = append(editorBinds, k.Editor.ShellCommand)
2921− }
2922− binds = append(binds, editorBinds)
2923− if hasAttachments {
2924− binds = append(
2925− binds,
2926− []key.Binding{
2927− k.Editor.AttachmentDeleteMode,
2928− k.Editor.DeleteAllAttachments,
2929− k.Editor.Escape,
2930− },
2931− )
2932− }
2933 default:
2751 default:
2934 if m.session == nil {
2752 if m.session == nil {
2935 // no session selected
2753 // no session selected
@@ -2950,9 +2768,6 @@ func (m *UI) FullHelp() [][]key.Binding {
2950 if m.currentModelSupportsImages() {
2768 if m.currentModelSupportsImages() {
2951 editorBinds = append(editorBinds, k.Editor.AddImage, k.Editor.PasteImage)
2769 editorBinds = append(editorBinds, k.Editor.AddImage, k.Editor.PasteImage)
2952 }
2770 }
2953− if m.textarea.Value() == "" {
2954− editorBinds = append(editorBinds, k.Editor.ShellCommand)
2955− }
2956 binds = append(binds, editorBinds)
2771 binds = append(binds, editorBinds)
2957 if hasAttachments {
2772 if hasAttachments {
2958 binds = append(
2773 binds = append(
@@ -2967,11 +2782,13 @@ func (m *UI) FullHelp() [][]key.Binding {
2967 }
2782 }
2968 }
2783 }
2969
2784
2970− lastRow := []key.Binding{help, k.Quit}
2785+ binds = append(
2971− if m.lastError != "" {
2786+ binds,
2972− lastRow = append([]key.Binding{k.CopyError}, lastRow...)
2787+ []key.Binding{
2973− }
2788+ help,
2974− binds = append(binds, lastRow)
2789+ k.Quit,
2790+ },
2791+ )
2975
2792
2976 return binds
2793 return binds
2977}
2794}
@@ -3117,8 +2934,7 @@ func (m *UI) generateLayout(w, h int) uiLayout {
3117 appRect.Min.X += 1
2934 appRect.Min.X += 1
3118 appRect.Max.X -= 1
2935 appRect.Max.X -= 1
3119
2936
3120− switch m.state {
2937+ if slices.Contains([]uiState{uiOnboarding, uiInitialize, uiLanding}, m.state) {
3121− case uiOnboarding, uiInitialize, uiLanding:
3122 // extra padding on left and right for these states
2938 // extra padding on left and right for these states
3123 appRect.Min.X += 1
2939 appRect.Min.X += 1
3124 appRect.Max.X -= 1
2940 appRect.Max.X -= 1
@@ -3307,9 +3123,8 @@ func (m *UI) openEditor(value string) tea.Cmd {
3307 return util.ReportError(err)
3123 return util.ReportError(err)
3308 }
3124 }
3309 tmpPath := tmpfile.Name()
3125 tmpPath := tmpfile.Name()
3310− defer tmpfile.Close()
3126+ defer tmpfile.Close() //nolint:errcheck
3311 if _, err := tmpfile.WriteString(value); err != nil {
3127 if _, err := tmpfile.WriteString(value); err != nil {
3312− _ = os.Remove(tmpPath)
3313 return util.ReportError(err)
3128 return util.ReportError(err)
3314 }
3129 }
3315 cmd, err := editor.Command(
3130 cmd, err := editor.Command(
@@ -3321,7 +3136,6 @@ func (m *UI) openEditor(value string) tea.Cmd {
3321 ),
3136 ),
3322 )
3137 )
3323 if err != nil {
3138 if err != nil {
3324− _ = os.Remove(tmpPath)
3325 return util.ReportError(err)
3139 return util.ReportError(err)
3326 }
3140 }
3327 return tea.ExecProcess(cmd, func(err error) tea.Msg {
3141 return tea.ExecProcess(cmd, func(err error) tea.Msg {
@@ -3447,9 +3261,9 @@ func (m *UI) insertFileCompletion(path string) tea.Cmd {
3447
3261
3448 if m.hasSession() {
3262 if m.hasSession() {
3449 // Skip attachment if file was already read and hasn't been modified.
3263 // Skip attachment if file was already read and hasn't been modified.
3450− lastRead := m.com.Workspace.FileTrackerLastReadTime(m.ctx, m.session.ID, absPath)
3264+ lastRead := m.com.Workspace.FileTrackerLastReadTime(context.Background(), m.session.ID, absPath)
3451 if !lastRead.IsZero() {
3265 if !lastRead.IsZero() {
3452− if info, err := os.Stat(absPath); err == nil && !info.ModTime().After(lastRead) {
3266+ if info, err := os.Stat(path); err == nil && !info.ModTime().After(lastRead) {
3453 return nil
3267 return nil
3454 }
3268 }
3455 }
3269 }
@@ -3460,14 +3274,14 @@ func (m *UI) insertFileCompletion(path string) tea.Cmd {
3460 m.sessionFileReads = append(m.sessionFileReads, absPath)
3274 m.sessionFileReads = append(m.sessionFileReads, absPath)
3461
3275
3462 // Add file as attachment.
3276 // Add file as attachment.
3463− content, err := os.ReadFile(absPath)
3277+ content, err := os.ReadFile(path)
3464 if err != nil {
3278 if err != nil {
3465 // If it fails, let the LLM handle it later.
3279 // If it fails, let the LLM handle it later.
3466 return nil
3280 return nil
3467 }
3281 }
3468
3282
3469 return message.Attachment{
3283 return message.Attachment{
3470− FilePath: absPath,
3284+ FilePath: path,
3471 FileName: filepath.Base(path),
3285 FileName: filepath.Base(path),
3472 MimeType: mimeOf(content),
3286 MimeType: mimeOf(content),
3473 Content: content,
3287 Content: content,
@@ -3489,7 +3303,7 @@ func (m *UI) insertMCPResourceCompletion(item completions.ResourceCompletionValu
3489
3303
3490 resourceCmd := func() tea.Msg {
3304 resourceCmd := func() tea.Msg {
3491 contents, err := m.com.Workspace.ReadMCPResource(
3305 contents, err := m.com.Workspace.ReadMCPResource(
3492− m.ctx,
3306+ context.Background(),
3493 item.MCPName,
3307 item.MCPName,
3494 item.URI,
3308 item.URI,
3495 )
3309 )
@@ -3624,40 +3438,12 @@ func (m *UI) cacheSidebarLogo(width int) {
3624// invalidating the markdown renderer cache and re-rendering the entire
3438// invalidating the markdown renderer cache and re-rendering the entire
3625// transcript for no visible change.
3439// transcript for no visible change.
3626func (m *UI) applyThemeForProvider(providerID string) {
3440func (m *UI) applyThemeForProvider(providerID string) {
3627− dark := m.effectiveDark()
3441+ key := styles.ThemeKeyForProvider(providerID)
3628− key := styles.ThemeKeyForProviderMode(providerID, dark)
3629 if key == m.themeKey {
3442 if key == m.themeKey {
3630 return
3443 return
3631 }
3444 }
3632 m.themeKey = key
3445 m.themeKey = key
3633− m.applyTheme(styles.ThemeForProviderMode(providerID, dark))
3446+ m.applyTheme(styles.ThemeForProvider(providerID))
3634−}
3635−
3636−// effectiveDark reports whether the dark palette should be used. A "dark"
3637−// or "light" theme setting forces the mode; "auto" (the default) follows
3638−// the detected terminal background (m.dark).
3639−func (m *UI) effectiveDark() bool {
3640− if cfg := m.com.Config(); cfg != nil && cfg.Options != nil && cfg.Options.TUI != nil {
3641− switch cfg.Options.TUI.Theme {
3642− case "light":
3643− return false
3644− case "dark":
3645− return true
3646− }
3647− }
3648− return m.dark
3649−}
3650−
3651−// reconcileTheme re-selects the active theme for the current large-model
3652−// provider and background mode, applying it only when it differs from the
3653−// theme already in effect. It is safe to call on every background-color or
3654−// window-size event.
3655−func (m *UI) reconcileTheme() {
3656− providerID := ""
3657− if cfg := m.com.Config(); cfg != nil {
3658− providerID = cfg.Models[config.SelectedModelTypeLarge].Provider
3659− }
3660− m.applyThemeForProvider(providerID)
3661}
3447}
3662
3448
3663// applyTheme replaces the active styles with the given theme, drops the
3449// applyTheme replaces the active styles with the given theme, drops the
@@ -3698,7 +3484,7 @@ func (m *UI) refreshStyles() {
3698// return one.
3484// return one.
3699func (m *UI) attachSkill(skillID, name string) tea.Cmd {
3485func (m *UI) attachSkill(skillID, name string) tea.Cmd {
3700 return func() tea.Msg {
3486 return func() tea.Msg {
3701− content, result, err := m.com.Workspace.ReadSkill(m.ctx, skillID)
3487+ content, result, err := m.com.Workspace.ReadSkill(context.Background(), skillID)
3702 if err != nil {
3488 if err != nil {
3703 return util.NewErrorMsg(err)
3489 return util.NewErrorMsg(err)
3704 }
3490 }
@@ -3723,7 +3509,7 @@ func (m *UI) sendMessage(content string, attachments ...message.Attachment) tea.
3723
3509
3724 var cmds []tea.Cmd
3510 var cmds []tea.Cmd
3725 if !m.hasSession() {
3511 if !m.hasSession() {
3726− newSession, err := m.com.Workspace.CreateSession(m.ctx, "New Session")
3512+ newSession, err := m.com.Workspace.CreateSession(context.Background(), "New Session")
3727 if err != nil {
3513 if err != nil {
3728 return util.ReportError(err)
3514 return util.ReportError(err)
3729 }
3515 }
@@ -3737,7 +3523,7 @@ func (m *UI) sendMessage(content string, attachments ...message.Attachment) tea.
3737 m.setState(uiChat, m.focus)
3523 m.setState(uiChat, m.focus)
3738 }
3524 }
3739
3525
3740− ctx := m.ctx
3526+ ctx := context.Background()
3741 cmds = append(cmds, func() tea.Msg {
3527 cmds = append(cmds, func() tea.Msg {
3742 for _, path := range m.sessionFileReads {
3528 for _, path := range m.sessionFileReads {
3743 m.com.Workspace.FileTrackerRecordRead(ctx, m.session.ID, path)
3529 m.com.Workspace.FileTrackerRecordRead(ctx, m.session.ID, path)
@@ -3753,7 +3539,7 @@ func (m *UI) sendMessage(content string, attachments ...message.Attachment) tea.
3753 // been accepted (HTTP 202) or synchronously with a validation
3539 // been accepted (HTTP 202) or synchronously with a validation
3754 // or transport error. Run failures and cancellation surface
3540 // or transport error. Run failures and cancellation surface
3755 // through SSE-derived events, not this return value.
3541 // through SSE-derived events, not this return value.
3756− err := m.com.Workspace.AgentRun(m.ctx, sessionID, content, attachments...)
3542+ err := m.com.Workspace.AgentRun(context.Background(), sessionID, content, attachments...)
3757 if err != nil {
3543 if err != nil {
3758 return util.InfoMsg{
3544 return util.InfoMsg{
3759 Type: util.InfoTypeError,
3545 Type: util.InfoTypeError,
@@ -3777,7 +3563,7 @@ func (m *UI) runShellCommand(command string) tea.Cmd {
3777func (m *UI) runShellCommandInternal(command string, isFirstMessage bool) tea.Cmd {
3563func (m *UI) runShellCommandInternal(command string, isFirstMessage bool) tea.Cmd {
3778 var cmds []tea.Cmd
3564 var cmds []tea.Cmd
3779 if !m.hasSession() {
3565 if !m.hasSession() {
3780− newSession, err := m.com.Workspace.CreateSession(m.ctx, "New Session")
3566+ newSession, err := m.com.Workspace.CreateSession(context.Background(), "New Session")
3781 if err != nil {
3567 if err != nil {
3782 return util.ReportError(err)
3568 return util.ReportError(err)
3783 }
3569 }
@@ -3830,7 +3616,7 @@ func (m *UI) runShellCommandInternal(command string, isFirstMessage bool) tea.Cm
3830 return shellStreamMsg{PendingID: pendingID, Chunk: chunk, streamCh: streamCh}
3616 return shellStreamMsg{PendingID: pendingID, Chunk: chunk, streamCh: streamCh}
3831 })
3617 })
3832
3618
3833− ctx, cancel := context.WithCancel(m.ctx)
3619+ ctx, cancel := context.WithCancel(context.Background())
3834 m.bangCancel = cancel
3620 m.bangCancel = cancel
3835
3621
3836 cmds = append(cmds, func() tea.Msg {
3622 cmds = append(cmds, func() tea.Msg {
@@ -4117,8 +3903,8 @@ func (m *UI) handleAgentNotification(n notify.Notification) tea.Cmd {
4117 case notify.TypeAgentFinished:
3903 case notify.TypeAgentFinished:
4118 var cmds []tea.Cmd
3904 var cmds []tea.Cmd
4119 cmds = append(cmds, m.sendNotification(notification.Notification{
3905 cmds = append(cmds, m.sendNotification(notification.Notification{
4120− Title: "Crush finished",
3906+ Title: "Crush is waiting...",
4121− Message: fmt.Sprintf("Turn complete — %s", n.SessionTitle),
3907+ Message: fmt.Sprintf("Agent's turn completed in \"%s\"", n.SessionTitle),
4122 }))
3908 }))
4123 if m.com.IsHyper() {
3909 if m.com.IsHyper() {
4124 cmds = append(cmds, m.fetchHyperCredits())
3910 cmds = append(cmds, m.fetchHyperCredits())
@@ -4165,14 +3951,12 @@ func (m *UI) newSession() tea.Cmd {
4165 m.pillsExpanded = false
3951 m.pillsExpanded = false
4166 m.pillsAutoExpanded = false
3952 m.pillsAutoExpanded = false
4167 m.promptQueue = 0
3953 m.promptQueue = 0
4168− m.renderCache.pillsView = ""
3954+ m.pillsView = ""
4169− m.renderCache.pillsCacheKey = ""
4170− m.renderCache.hasSidebarCache = false
4171 m.historyReset()
3955 m.historyReset()
4172 agenttools.ResetCache()
3956 agenttools.ResetCache()
4173 return tea.Batch(
3957 return tea.Batch(
4174 func() tea.Msg {
3958 func() tea.Msg {
4175− m.com.Workspace.LSPStopAll(m.ctx)
3959+ m.com.Workspace.LSPStopAll(context.Background())
4176 return nil
3960 return nil
4177 },
3961 },
4178 m.loadPromptHistory(),
3962 m.loadPromptHistory(),
@@ -4467,7 +4251,8 @@ func (m *UI) runMCPPrompt(clientID, promptID string, arguments map[string]string
4467 load := func() tea.Msg {
4251 load := func() tea.Msg {
4468 prompt, err := m.com.Workspace.GetMCPPrompt(clientID, promptID, arguments)
4252 prompt, err := m.com.Workspace.GetMCPPrompt(clientID, promptID, arguments)
4469 if err != nil {
4253 if err != nil {
4470− return util.ReportError(fmt.Errorf("failed to load MCP prompt %q from %q: %w", promptID, clientID, err))()
4254+ // TODO: make this better
4255+ return util.ReportError(err)()
4471 }
4256 }
4472
4257
4473 if prompt == "" {
4258 if prompt == "" {
@@ -4491,9 +4276,7 @@ func (m *UI) runMCPPrompt(clientID, promptID string, arguments map[string]string
4491
4276
4492func (m *UI) handleStateChanged() tea.Cmd {
4277func (m *UI) handleStateChanged() tea.Cmd {
4493 return func() tea.Msg {
4278 return func() tea.Msg {
4494− if err := m.com.Workspace.UpdateAgentModel(m.ctx); err != nil {
4279+ m.com.Workspace.UpdateAgentModel(context.Background())
4495− slog.Warn("failed to update agent model", "error", err)
4496− }
4497 return mcpStateChangedMsg{
4280 return mcpStateChangedMsg{
4498 states: m.com.Workspace.MCPGetStates(),
4281 states: m.com.Workspace.MCPGetStates(),
4499 }
4282 }
@@ -4534,7 +4317,7 @@ func (m *UI) copyChatHighlight() tea.Cmd {
4534}
4317}
4535
4318
4536func (m *UI) enableDockerMCP() tea.Msg {
4319func (m *UI) enableDockerMCP() tea.Msg {
4537− ctx := m.ctx
4320+ ctx := context.Background()
4538 if err := m.com.Workspace.EnableDockerMCP(ctx); err != nil {
4321 if err := m.com.Workspace.EnableDockerMCP(ctx); err != nil {
4539 return util.ReportError(err)()
4322 return util.ReportError(err)()
4540 }
4323 }
internal/ui/styles/grad.go+2−51
@@ -2,52 +2,13 @@ package styles
2
2
3import (
3import (
4 "fmt"
4 "fmt"
5− "hash/fnv"
6 "image/color"
5 "image/color"
7 "strings"
6 "strings"
8− "sync"
9
7
10 "charm.land/lipgloss/v2"
8 "charm.land/lipgloss/v2"
11 "github.com/rivo/uniseg"
9 "github.com/rivo/uniseg"
12)
10)
13
11
14−// gradCache memoizes the per-cluster rendered output of ForegroundGrad. The
15−// function walks grapheme clusters (uniseg), blends a color ramp and renders
16−// each cluster on every call, and it is invoked every frame by dialog titles
17−// and similar constant text. Results are keyed by a fingerprint of every input
18−// that affects the output.
19−var gradCache sync.Map // map[uint64][]string
20−
21−// gradKey derives a stable cache key from all inputs that affect the rendered
22−// gradient output.
23−func gradKey(base lipgloss.Style, input string, bold bool, color1, color2 color.Color) uint64 {
24− h := fnv.New64a()
25− h.Write([]byte(input))
26− h.Write([]byte{0})
27− writeColor := func(c color.Color) {
28− var buf [8]byte
29− if c != nil {
30− r, g, b, a := c.RGBA()
31− buf[0] = byte(r >> 8)
32− buf[1] = byte(g >> 8)
33− buf[2] = byte(b >> 8)
34− buf[3] = byte(a >> 8)
35− }
36− h.Write(buf[:4])
37− }
38− writeColor(color1)
39− writeColor(color2)
40− if bold {
41− h.Write([]byte{1})
42− } else {
43− h.Write([]byte{0})
44− }
45− // base carries the remaining style attributes (e.g. background); include a
46− // fingerprint so distinct bases never collide.
47− fmt.Fprintf(h, "%v", base)
48− return h.Sum64()
49−}
50−
51// ForegroundGrad returns a slice of strings representing the input string
12// ForegroundGrad returns a slice of strings representing the input string
52// rendered with a horizontal gradient foreground from color1 to color2. Each
13// rendered with a horizontal gradient foreground from color1 to color2. Each
53// string in the returned slice corresponds to a grapheme cluster in the input
14// string in the returned slice corresponds to a grapheme cluster in the input
@@ -56,20 +17,10 @@ func ForegroundGrad(base lipgloss.Style, input string, bold bool, color1, color2
56 if input == "" {
17 if input == "" {
57 return []string{""}
18 return []string{""}
58 }
19 }
59− key := gradKey(base, input, bold, color1, color2)
60− if cached, ok := gradCache.Load(key); ok {
61− return cached.([]string)
62− }
63− result := foregroundGrad(base, input, bold, color1, color2)
64− gradCache.Store(key, result)
65− return result
66−}
67−
68−func foregroundGrad(base lipgloss.Style, input string, bold bool, color1, color2 color.Color) []string {
69 if len(input) == 1 {
20 if len(input) == 1 {
70 style := base.Foreground(color1)
21 style := base.Foreground(color1)
71 if bold {
22 if bold {
72− style = style.Bold(true)
23+ style.Bold(true)
73 }
24 }
74 return []string{style.Render(input)}
25 return []string{style.Render(input)}
75 }
26 }
@@ -83,7 +34,7 @@ func foregroundGrad(base lipgloss.Style, input string, bold bool, color1, color2
83 for i, c := range ramp {
34 for i, c := range ramp {
84 style := base.Foreground(c)
35 style := base.Foreground(c)
85 if bold {
36 if bold {
86− style = style.Bold(true)
37+ style.Bold(true)
87 }
38 }
88 clusters[i] = style.Render(clusters[i])
39 clusters[i] = style.Render(clusters[i])
89 }
40 }
internal/ui/styles/quickstyle.go+21−7
@@ -550,12 +550,26 @@ func quickStyle(o quickStyleOpts) Styles {
550 Foreground(o.fgMoreSubtle).
550 Foreground(o.fgMoreSubtle).
551 Background(o.bgBase),
551 Background(o.bgBase),
552 },
552 },
553− // Insert/delete colors are derived from the semantic palette (green
553+ InsertLine: diffview.LineStyle{
554− // for inserts, red for deletes) washed over the base background, so a
554+ LineNumber: lipgloss.NewStyle().
555− // future light theme adapts without hardcoded hex. Code foreground is
555+ Foreground(lipgloss.Color("#629657")).
556− // left unset to inherit the syntax highlighting.
556+ Background(lipgloss.Color("#2b322a")),
557− InsertLine: diffview.ChangeLineStyle(o.success, o.bgBase, nil),
557+ Symbol: lipgloss.NewStyle().
558− DeleteLine: diffview.ChangeLineStyle(o.error, o.bgBase, nil),
558+ Foreground(lipgloss.Color("#629657")).
559+ Background(lipgloss.Color("#323931")),
560+ Code: lipgloss.NewStyle().
561+ Background(lipgloss.Color("#323931")),
562+ },
563+ DeleteLine: diffview.LineStyle{
564+ LineNumber: lipgloss.NewStyle().
565+ Foreground(lipgloss.Color("#a45c59")).
566+ Background(lipgloss.Color("#312929")),
567+ Symbol: lipgloss.NewStyle().
568+ Foreground(lipgloss.Color("#a45c59")).
569+ Background(lipgloss.Color("#383030")),
570+ Code: lipgloss.NewStyle().
571+ Background(lipgloss.Color("#383030")),
572+ },
559 Filename: diffview.LineStyle{
573 Filename: diffview.LineStyle{
560 LineNumber: lipgloss.NewStyle().
574 LineNumber: lipgloss.NewStyle().
561 Foreground(o.fgSubtle).
575 Foreground(o.fgSubtle).
@@ -604,7 +618,7 @@ func quickStyle(o quickStyleOpts) Styles {
604 s.Tool.IconPending = base.Foreground(o.successMostSubtle).SetString(ToolPending)
618 s.Tool.IconPending = base.Foreground(o.successMostSubtle).SetString(ToolPending)
605 s.Tool.IconSuccess = base.Foreground(o.success).SetString(ToolSuccess)
619 s.Tool.IconSuccess = base.Foreground(o.success).SetString(ToolSuccess)
606 s.Tool.IconError = base.Foreground(o.error).SetString(ToolError)
620 s.Tool.IconError = base.Foreground(o.error).SetString(ToolError)
607− s.Tool.IconCancelled = muted.SetString(ToolCancelled)
621+ s.Tool.IconCancelled = muted.SetString(ToolPending)
608
622
609 s.Tool.NameNormal = base.Foreground(o.info)
623 s.Tool.NameNormal = base.Foreground(o.info)
610 s.Tool.NameNested = base.Foreground(o.info)
624 s.Tool.NameNested = base.Foreground(o.info)
internal/ui/styles/styles.go+3−4
@@ -25,10 +25,9 @@ const (
25
25
26 ArrowRightIcon string = "→"
26 ArrowRightIcon string = "→"
27
27
28− ToolPending string = "●"
28+ ToolPending string = "●"
29− ToolSuccess string = "✓"
29+ ToolSuccess string = "✓"
30− ToolError string = "×"
30+ ToolError string = "×"
31− ToolCancelled string = "⊘"
32
31
33 RadioOn string = "◉"
32 RadioOn string = "◉"
34 RadioOff string = "○"
33 RadioOff string = "○"
internal/ui/styles/themes.go+12−131
@@ -19,76 +19,22 @@ func ThemeKeyForProvider(providerID string) string {
19 }
19 }
20}
20}
21
21
22−// ThemeForProvider returns the dark Styles associated with the given
22+// ThemeForProvider returns the Styles associated with the given provider
23−// provider ID. Unknown or empty provider IDs yield the default Charmtone
23+// ID. Unknown or empty provider IDs yield the default Charmtone Pantera
24−// Pantera theme. Retained for callers that always want the dark theme
24+// theme.
25−// (e.g. the non-interactive CLI); the interactive TUI uses the
26−// background-aware [ThemeForProviderMode].
27func ThemeForProvider(providerID string) Styles {
25func ThemeForProvider(providerID string) Styles {
28− return ThemeForProviderMode(providerID, true)
29−}
30−
31−// ThemeKeyForProviderMode returns a stable identifier for the theme
32−// associated with the given provider ID and background mode (dark/light).
33−// The key incorporates the mode so callers can detect when a background
34−// change would actually alter the active theme and skip the rebuild
35−// otherwise.
36−func ThemeKeyForProviderMode(providerID string, dark bool) string {
37− base := ThemeKeyForProvider(providerID)
38− if dark {
39− return base + "-dark"
40− }
41− return base + "-light"
42−}
43−
44−// ThemeForProviderMode returns the Styles for the given provider ID in the
45−// requested background mode. When dark is false a light-background palette
46−// is returned so the UI stays legible on light terminals.
47−func ThemeForProviderMode(providerID string, dark bool) Styles {
48 switch ThemeKeyForProvider(providerID) {
26 switch ThemeKeyForProvider(providerID) {
49 case "hyper":
27 case "hyper":
50− if dark {
28+ return HypercrushObsidiana()
51− return HypercrushObsidiana()
52− }
53− return HypercrushLatte()
54 default:
29 default:
55− if dark {
30+ return CharmtonePantera()
56− return CharmtonePantera()
57− }
58− return CharmtoneLatte()
59 }
31 }
60}
32}
61
33
62// CharmtonePantera returns the Charmtone dark theme. It's the default style
34// CharmtonePantera returns the Charmtone dark theme. It's the default style
63// for the UI.
35// for the UI.
64func CharmtonePantera() Styles {
36func CharmtonePantera() Styles {
65− return buildCharmtone(charmtoneDarkOpts())
37+ s := quickStyle(quickStyleOpts{
66−}
67−
68−// CharmtoneLatte returns the Charmtone light theme, used on light-background
69−// terminals. It mirrors the dark theme's structure but inverts the neutral
70−// ramp and swaps the neon accent/semantic colors for darker shades that stay
71−// legible on a light background.
72−//
73−// NOTE: this palette is a first pass tuned by hand; the exact accent/semantic
74−// color choices are worth a visual review on real light terminals.
75−func CharmtoneLatte() Styles {
76− return buildCharmtone(charmtoneLightOpts())
77−}
78−
79−// HypercrushObsidiana returns the Hypercrush dark theme.
80−func HypercrushObsidiana() Styles {
81− return CharmtonePantera()
82−}
83−
84−// HypercrushLatte returns the Hypercrush light theme.
85−func HypercrushLatte() Styles {
86− return CharmtoneLatte()
87−}
88−
89−// charmtoneDarkOpts returns the color options for the Charmtone dark theme.
90−func charmtoneDarkOpts() quickStyleOpts {
91− return quickStyleOpts{
92 primary: charmtone.Charple,
38 primary: charmtone.Charple,
93 secondary: charmtone.Dolly,
39 secondary: charmtone.Dolly,
94 accent: charmtone.Bok,
40 accent: charmtone.Bok,
@@ -140,77 +86,7 @@ func charmtoneDarkOpts() quickStyleOpts {
140 ansiBrightMagenta: charmtone.Blush,
86 ansiBrightMagenta: charmtone.Blush,
141 ansiBrightCyan: charmtone.Sardine,
87 ansiBrightCyan: charmtone.Sardine,
142 ansiBrightWhite: charmtone.Salt,
88 ansiBrightWhite: charmtone.Salt,
143− }
89+ })
144−}
145−
146−// charmtoneLightOpts returns the color options for the Charmtone light theme.
147−// The neutral ramp is inverted (light backgrounds, dark foregrounds) and the
148−// neon accents/semantics from the dark theme are replaced with darker,
149−// higher-contrast shades so text stays readable on a light background.
150−func charmtoneLightOpts() quickStyleOpts {
151− return quickStyleOpts{
152− primary: charmtone.Charple, // saturated purple reads on white
153− secondary: charmtone.Grape,
154− accent: charmtone.Zinc,
155− keyword: charmtone.Prince,
156−
157− // Foregrounds: dark on light, decreasing visibility.
158− fgBase: charmtone.Pepper,
159− fgSubtle: charmtone.Iron,
160− fgMoreSubtle: charmtone.Oyster,
161− fgMostSubtle: charmtone.Steam,
162−
163− onPrimary: charmtone.Butter, // light text on the purple primary
164−
165− // Backgrounds: light base, increasingly visible (darker) greys.
166− bgBase: charmtone.Salt,
167− bgLeastVisible: charmtone.Sash,
168− bgLessVisible: charmtone.Steep,
169− bgMostVisible: charmtone.Smoke,
170−
171− separator: charmtone.Steep,
172−
173− destructive: charmtone.Coral,
174− error: charmtone.Sriracha,
175− warningSubtle: charmtone.Yam,
176− warning: charmtone.Tang,
177− denied: charmtone.Paprika,
178− busy: charmtone.Cumin,
179− info: charmtone.Damson,
180− infoMoreSubtle: charmtone.Malibu,
181− infoMostSubtle: charmtone.Anchovy,
182− success: charmtone.Pickle,
183− successMoreSubtle: charmtone.Guac,
184− successMostSubtle: charmtone.Zinc,
185−
186− // ANSI 16-color palette tuned for legibility on a light background:
187− // normal colors are darker/saturated, brights slightly lighter.
188− ansiBlack: charmtone.Pepper,
189− ansiRed: charmtone.Pom,
190− ansiGreen: charmtone.Pickle,
191− ansiYellow: charmtone.Cumin,
192− ansiBlue: charmtone.Oceania,
193− ansiMagenta: charmtone.Grape,
194− ansiCyan: charmtone.Damson,
195− ansiWhite: charmtone.Oyster,
196−
197− ansiBrightBlack: charmtone.Steam,
198− ansiBrightRed: charmtone.Sriracha,
199− ansiBrightGreen: charmtone.Guac,
200− ansiBrightYellow: charmtone.Tang,
201− ansiBrightBlue: charmtone.Sapphire,
202− ansiBrightMagenta: charmtone.Urchin,
203− ansiBrightCyan: charmtone.Zinc,
204− ansiBrightWhite: charmtone.Iron,
205− }
206−}
207−
208−// buildCharmtone builds a Styles value from the given color options and
209−// applies the shared bang-prompt and shell-bar accent overrides. These
210−// overrides use saturated purple accents that read on both light and dark
211−// backgrounds.
212−func buildCharmtone(opts quickStyleOpts) Styles {
213− s := quickStyle(opts)
214
90
215 // Bang ! prompt overrides - use Salt/Hazy/Larple colors.
91 // Bang ! prompt overrides - use Salt/Hazy/Larple colors.
216 s.Editor.PromptBangIconFocused = s.Editor.PromptBangIconFocused.
92 s.Editor.PromptBangIconFocused = s.Editor.PromptBangIconFocused.
@@ -233,3 +109,8 @@ func buildCharmtone(opts quickStyleOpts) Styles {
233
109
234 return s
110 return s
235}
111}
112+
113+// HypercrushObsidiana returns the Hypercrush dark theme.
114+func HypercrushObsidiana() Styles {
115+ return CharmtonePantera()
116+}
internal/ui/xchroma/chroma.go+19−37
@@ -13,55 +13,37 @@ import (
13// Lip Gloss for foreground styling, while keeping a forced background color.
13// Lip Gloss for foreground styling, while keeping a forced background color.
14func Formatter(bgColor color.Color, processValue func(string) string) chroma.Formatter {
14func Formatter(bgColor color.Color, processValue func(string) string) chroma.Formatter {
15 return chroma.FormatterFunc(func(w io.Writer, style *chroma.Style, it chroma.Iterator) error {
15 return chroma.FormatterFunc(func(w io.Writer, style *chroma.Style, it chroma.Iterator) error {
16− // Precompute the base background style once, then memoize the
17− // resolved lipgloss.Style per token type so the per-token work is
18− // just a map lookup + one Render.
19− baseStyle := lipgloss.NewStyle().Background(bgColor)
20−
21− type tokenEntry struct {
22− style lipgloss.Style
23− zero bool
24− }
25− palette := make(map[chroma.TokenType]tokenEntry)
26−
27 for token := it(); token != chroma.EOF; token = it() {
16 for token := it(); token != chroma.EOF; token = it() {
28 value := token.Value
17 value := token.Value
29 if processValue != nil {
18 if processValue != nil {
30 value = processValue(value)
19 value = processValue(value)
31 }
20 }
32
21
33− te, ok := palette[token.Type]
22+ entry := style.Get(token.Type)
34− if !ok {
23+ if entry.IsZero() {
35− entry := style.Get(token.Type)
36− if entry.IsZero() {
37− te = tokenEntry{zero: true}
38− } else {
39− s := baseStyle
40− if entry.Bold == chroma.Yes {
41− s = s.Bold(true)
42− }
43− if entry.Underline == chroma.Yes {
44− s = s.Underline(true)
45− }
46− if entry.Italic == chroma.Yes {
47− s = s.Italic(true)
48− }
49− if entry.Colour.IsSet() {
50− s = s.Foreground(lipgloss.Color(entry.Colour.String()))
51− }
52− te = tokenEntry{style: s}
53− }
54− palette[token.Type] = te
55− }
56−
57− if te.zero {
58 if _, err := fmt.Fprint(w, value); err != nil {
24 if _, err := fmt.Fprint(w, value); err != nil {
59 return err
25 return err
60 }
26 }
61 continue
27 continue
62 }
28 }
63
29
64− if _, err := fmt.Fprint(w, te.style.Render(value)); err != nil {
30+ s := lipgloss.NewStyle().
31+ Background(bgColor)
32+
33+ if entry.Bold == chroma.Yes {
34+ s = s.Bold(true)
35+ }
36+ if entry.Underline == chroma.Yes {
37+ s = s.Underline(true)
38+ }
39+ if entry.Italic == chroma.Yes {
40+ s = s.Italic(true)
41+ }
42+ if entry.Colour.IsSet() {
43+ s = s.Foreground(lipgloss.Color(entry.Colour.String()))
44+ }
45+
46+ if _, err := fmt.Fprint(w, s.Render(value)); err != nil {
65 return err
47 return err
66 }
48 }
67 }
49 }
internal/workspace/app_workspace.go+12−6
@@ -71,12 +71,10 @@ func (w *AppWorkspace) ParseAgentToolSessionID(sessionID string) (string, string
71 return w.app.Sessions.ParseAgentToolSessionID(sessionID)
71 return w.app.Sessions.ParseAgentToolSessionID(sessionID)
72}
72}
73
73
74−// SetCurrentSession reports the active session to herdr so the pane
74+// SetCurrentSession is a no-op in single-client local mode. The
75−// can persist a resumable reference. Multi-client presence tracking
75+// presence concept only matters when multiple clients can share a
76−// is irrelevant in single-client local mode, but herdr still needs
76+// workspace via the HTTP server.
77−// to know which session is live to support agent resume.
78func (w *AppWorkspace) SetCurrentSession(ctx context.Context, sessionID string) error {
77func (w *AppWorkspace) SetCurrentSession(ctx context.Context, sessionID string) error {
79− w.app.ReportCurrentSession(sessionID)
80 return nil
78 return nil
81}
79}
82
80
@@ -114,7 +112,15 @@ func (w *AppWorkspace) AgentRunShellCommand(ctx context.Context, sessionID, comm
114 var persist shell.PersistFunc
112 var persist shell.PersistFunc
115 if sessionID != "" {
113 if sessionID != "" {
116 persist = func(cmd, output string, exitCode int) error {
114 persist = func(cmd, output string, exitCode int) error {
117− return shell.PersistOutput(ctx, w.app.Messages, sessionID, cmd, output, exitCode)
115+ _, err := w.app.Messages.Create(ctx, sessionID, message.CreateMessageParams{
116+ Role: message.User,
117+ Parts: []message.ContentPart{message.ShellCommand{
118+ Command: cmd,
119+ Output: output,
120+ ExitCode: exitCode,
121+ }},
122+ })
123+ return err
118 }
124 }
119 }
125 }
120
126
internal/workspace/client_workspace.go+3−16
@@ -14,7 +14,6 @@ import (
14 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
14 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
15 "github.com/charmbracelet/crush/internal/client"
15 "github.com/charmbracelet/crush/internal/client"
16 "github.com/charmbracelet/crush/internal/config"
16 "github.com/charmbracelet/crush/internal/config"
17− "github.com/charmbracelet/crush/internal/herdr"
18 "github.com/charmbracelet/crush/internal/history"
17 "github.com/charmbracelet/crush/internal/history"
19 "github.com/charmbracelet/crush/internal/log"
18 "github.com/charmbracelet/crush/internal/log"
20 "github.com/charmbracelet/crush/internal/lsp"
19 "github.com/charmbracelet/crush/internal/lsp"
@@ -38,10 +37,6 @@ type ClientWorkspace struct {
38 mu sync.RWMutex
37 mu sync.RWMutex
39 ws proto.Workspace
38 ws proto.Workspace
40 skills *skills.Manager
39 skills *skills.Manager
41−
42− // herdrClient reports agent state to herdr when running inside
43− // a herdr-managed pane. Nil when not in a herdr environment.
44− herdrClient *herdr.Client
45}
40}
46
41
47// NewClientWorkspace creates a new ClientWorkspace that proxies all
42// NewClientWorkspace creates a new ClientWorkspace that proxies all
@@ -59,10 +54,9 @@ func NewClientWorkspace(c *client.Client, ws proto.Workspace) *ClientWorkspace {
59 states := protoToSkillStates(ws.Skills)
54 states := protoToSkillStates(ws.Skills)
60 mgr := skills.NewManager(nil, nil, states, skills.WithGlobalMirror())
55 mgr := skills.NewManager(nil, nil, states, skills.WithGlobalMirror())
61 return &ClientWorkspace{
56 return &ClientWorkspace{
62− client: c,
57+ client: c,
63− ws: ws,
58+ ws: ws,
64− skills: mgr,
59+ skills: mgr,
65− herdrClient: herdr.Init(),
66 }
60 }
67}
61}
68
62
@@ -153,7 +147,6 @@ func (w *ClientWorkspace) ParseAgentToolSessionID(sessionID string) (string, str
153// are propagated to the caller; the TUI logs and ignores them since
147// are propagated to the caller; the TUI logs and ignores them since
154// the presence record is a hint, not correctness-critical state.
148// the presence record is a hint, not correctness-critical state.
155func (w *ClientWorkspace) SetCurrentSession(ctx context.Context, sessionID string) error {
149func (w *ClientWorkspace) SetCurrentSession(ctx context.Context, sessionID string) error {
156− w.herdrClient.SetSessionID(sessionID)
157 return w.client.SetCurrentSession(ctx, w.workspaceID(), sessionID)
150 return w.client.SetCurrentSession(ctx, w.workspaceID(), sessionID)
158}
151}
159
152
@@ -629,11 +622,6 @@ func (w *ClientWorkspace) Subscribe(program *tea.Program) {
629// are translated into domain types and forwarded to send.
622// are translated into domain types and forwarded to send.
630func (w *ClientWorkspace) consumeEvents(evc <-chan any, send func(tea.Msg)) {
623func (w *ClientWorkspace) consumeEvents(evc <-chan any, send func(tea.Msg)) {
631 for ev := range evc {
624 for ev := range evc {
632− // Forward events to herdr if running inside a herdr pane.
633− if hev := herdr.Translate(ev); hev != nil {
634− w.herdrClient.HandleEvent(hev)
635− }
636−
637 if _, ok := ev.(pubsub.Event[proto.ConfigChanged]); ok {
625 if _, ok := ev.(pubsub.Event[proto.ConfigChanged]); ok {
638 w.refreshWorkspace()
626 w.refreshWorkspace()
639 continue
627 continue
@@ -646,7 +634,6 @@ func (w *ClientWorkspace) consumeEvents(evc <-chan any, send func(tea.Msg)) {
646}
634}
647
635
648func (w *ClientWorkspace) Shutdown() {
636func (w *ClientWorkspace) Shutdown() {
649− w.herdrClient.Close()
650 _ = w.client.DeleteWorkspace(context.Background(), w.workspaceID())
637 _ = w.client.DeleteWorkspace(context.Background(), w.workspaceID())
651}
638}
652
639
schema.json−10
@@ -748,16 +748,6 @@
748 ],
748 ],
749 "description": "Diff mode for the TUI interface"
749 "description": "Diff mode for the TUI interface"
750 },
750 },
751− "theme": {
752− "type": "string",
753− "enum": [
754− "auto",
755− "dark",
756− "light"
757− ],
758− "description": "TUI color scheme",
759− "default": "auto"
760− },
761 "completions": {
751 "completions": {
762 "$ref": "#/$defs/Completions",
752 "$ref": "#/$defs/Completions",
763 "description": "Completions UI options"
753 "description": "Completions UI options"
vendor/charm.land/bubbles/v2/LICENSEdeleted−21
@@ -1,21 +0,0 @@
1−MIT License
2−
3−Copyright (c) 2020-2026 Charmbracelet, Inc.
4−
5−Permission is hereby granted, free of charge, to any person obtaining a copy
6−of this software and associated documentation files (the "Software"), to deal
7−in the Software without restriction, including without limitation the rights
8−to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9−copies of the Software, and to permit persons to whom the Software is
10−furnished to do so, subject to the following conditions:
11−
12−The above copyright notice and this permission notice shall be included in all
13−copies or substantial portions of the Software.
14−
15−THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16−IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17−FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18−AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19−LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20−OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21−SOFTWARE.
vendor/charm.land/bubbles/v2/cursor/cursor.godeleted−241
@@ -1,241 +0,0 @@
1−// Package cursor provides a virtual cursor to support the textinput and
2−// textarea elements.
3−package cursor
4−
5−import (
6− "context"
7− "sync/atomic"
8− "time"
9−
10− tea "charm.land/bubbletea/v2"
11− "charm.land/lipgloss/v2"
12−)
13−
14−const defaultBlinkSpeed = time.Millisecond * 530
15−
16−// Internal ID management. Used during animating to ensure that frame messages
17−// are received only by spinner components that sent them.
18−var lastID int64
19−
20−func nextID() int {
21− return int(atomic.AddInt64(&lastID, 1))
22−}
23−
24−// initialBlinkMsg initializes cursor blinking.
25−type initialBlinkMsg struct{}
26−
27−// BlinkMsg signals that the cursor should blink. It contains metadata that
28−// allows us to tell if the blink message is the one we're expecting.
29−type BlinkMsg struct {
30− id int
31− tag int
32−}
33−
34−// blinkCanceled is sent when a blink operation is canceled.
35−type blinkCanceled struct{}
36−
37−// blinkCtx manages cursor blinking.
38−type blinkCtx struct {
39− ctx context.Context
40− cancel context.CancelFunc
41−}
42−
43−// Mode describes the behavior of the cursor.
44−type Mode int
45−
46−// Available cursor modes.
47−const (
48− CursorBlink Mode = iota
49− CursorStatic
50− CursorHide
51−)
52−
53−// String returns the cursor mode in a human-readable format. This method is
54−// provisional and for informational purposes only.
55−func (c Mode) String() string {
56− return [...]string{
57− "blink",
58− "static",
59− "hidden",
60− }[c]
61−}
62−
63−// Model is the Bubble Tea model for this cursor element.
64−type Model struct {
65− // Style styles the cursor block.
66− Style lipgloss.Style
67−
68− // TextStyle is the style used for the cursor when it is blinking
69− // (hidden), i.e. displaying normal text.
70− TextStyle lipgloss.Style
71−
72− // BlinkSpeed is the speed at which the cursor blinks. This has no effect
73− // unless [CursorMode] is not set to [CursorBlink].
74− BlinkSpeed time.Duration
75−
76− // IsBlinked is the state of the cursor blink. When true, the cursor is
77− // hidden.
78− IsBlinked bool
79−
80− // char is the character under the cursor
81− char string
82−
83− // The ID of this Model as it relates to other cursors
84− id int
85−
86− // focus indicates whether the containing input is focused
87− focus bool
88−
89− // Used to manage cursor blink
90− blinkCtx *blinkCtx
91−
92− // The ID of the blink message we're expecting to receive.
93− blinkTag int
94−
95− // mode determines the behavior of the cursor
96− mode Mode
97−}
98−
99−// New creates a new model with default settings.
100−func New() Model {
101− return Model{
102− id: nextID(),
103− BlinkSpeed: defaultBlinkSpeed,
104− IsBlinked: true,
105− mode: CursorBlink,
106−
107− blinkCtx: &blinkCtx{
108− ctx: context.Background(),
109− },
110− }
111−}
112−
113−// Update updates the cursor.
114−func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
115− switch msg := msg.(type) {
116− case initialBlinkMsg:
117− // We accept all initialBlinkMsgs generated by the Blink command.
118−
119− if m.mode != CursorBlink || !m.focus {
120− return m, nil
121− }
122−
123− cmd := m.Blink()
124− return m, cmd
125−
126− case tea.FocusMsg:
127− return m, m.Focus()
128−
129− case tea.BlurMsg:
130− m.Blur()
131− return m, nil
132−
133− case BlinkMsg:
134− // We're choosy about whether to accept blinkMsgs so that our cursor
135− // only exactly when it should.
136−
137− // Is this model blink-able?
138− if m.mode != CursorBlink || !m.focus {
139− return m, nil
140− }
141−
142− // Were we expecting this blink message?
143− if msg.id != m.id || msg.tag != m.blinkTag {
144− return m, nil
145− }
146−
147− var cmd tea.Cmd
148− if m.mode == CursorBlink {
149− m.IsBlinked = !m.IsBlinked
150− cmd = m.Blink()
151− }
152− return m, cmd
153−
154− case blinkCanceled: // no-op
155− return m, nil
156− }
157− return m, nil
158−}
159−
160−// Mode returns the model's cursor mode. For available cursor modes, see
161−// type Mode.
162−func (m Model) Mode() Mode {
163− return m.mode
164−}
165−
166−// SetMode sets the model's cursor mode. This method returns a command.
167−//
168−// For available cursor modes, see type CursorMode.
169−func (m *Model) SetMode(mode Mode) tea.Cmd {
170− // Adjust the mode value if it's value is out of range
171− if mode < CursorBlink || mode > CursorHide {
172− return nil
173− }
174− m.mode = mode
175− m.IsBlinked = m.mode == CursorHide || !m.focus
176− if mode == CursorBlink {
177− return Blink
178− }
179− return nil
180−}
181−
182−// Blink is a command used to manage cursor blinking.
183−func (m *Model) Blink() tea.Cmd {
184− if m.mode != CursorBlink {
185− return nil
186− }
187−
188− if m.blinkCtx != nil && m.blinkCtx.cancel != nil {
189− m.blinkCtx.cancel()
190− }
191−
192− ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed)
193− m.blinkCtx.cancel = cancel
194−
195− m.blinkTag++
196− blinkMsg := BlinkMsg{id: m.id, tag: m.blinkTag}
197−
198− return func() tea.Msg {
199− defer cancel()
200− <-ctx.Done()
201− if ctx.Err() == context.DeadlineExceeded {
202− return blinkMsg
203− }
204− return blinkCanceled{}
205− }
206−}
207−
208−// Blink is a command used to initialize cursor blinking.
209−func Blink() tea.Msg {
210− return initialBlinkMsg{}
211−}
212−
213−// Focus focuses the cursor to allow it to blink if desired.
214−func (m *Model) Focus() tea.Cmd {
215− m.focus = true
216− m.IsBlinked = m.mode == CursorHide // show the cursor unless we've explicitly hidden it
217−
218− if m.mode == CursorBlink && m.focus {
219− return m.Blink()
220− }
221− return nil
222−}
223−
224−// Blur blurs the cursor.
225−func (m *Model) Blur() {
226− m.focus = false
227− m.IsBlinked = true
228−}
229−
230−// SetChar sets the character under the cursor.
231−func (m *Model) SetChar(char string) {
232− m.char = char
233−}
234−
235−// View displays the cursor.
236−func (m Model) View() string {
237− if m.IsBlinked {
238− return m.TextStyle.Inline(true).Render(m.char)
239− }
240− return m.Style.Inline(true).Reverse(true).Render(m.char)
241−}
vendor/charm.land/bubbles/v2/filepicker/filepicker.godeleted−529
@@ -1,529 +0,0 @@
1−// Package filepicker provides a file picker component for Bubble Tea
2−// applications.
3−package filepicker
4−
5−import (
6− "fmt"
7− "os"
8− "path/filepath"
9− "sort"
10− "strconv"
11− "strings"
12− "sync/atomic"
13−
14− "charm.land/bubbles/v2/key"
15− tea "charm.land/bubbletea/v2"
16− "charm.land/lipgloss/v2"
17− "github.com/dustin/go-humanize"
18−)
19−
20−var lastID int64
21−
22−func nextID() int {
23− return int(atomic.AddInt64(&lastID, 1))
24−}
25−
26−// New returns a new filepicker model with default styling and key bindings.
27−func New() Model {
28− return Model{
29− id: nextID(),
30− CurrentDirectory: ".",
31− Cursor: ">",
32− AllowedTypes: []string{},
33− selected: 0,
34− ShowPermissions: true,
35− ShowSize: true,
36− ShowHidden: false,
37− DirAllowed: false,
38− FileAllowed: true,
39− AutoHeight: true,
40− height: 0,
41− maxIdx: 0,
42− minIdx: 0,
43− selectedStack: newStack(),
44− minStack: newStack(),
45− maxStack: newStack(),
46− KeyMap: DefaultKeyMap(),
47− Styles: DefaultStyles(),
48− }
49−}
50−
51−type errorMsg struct {
52− err error
53−}
54−
55−type readDirMsg struct {
56− id int
57− entries []os.DirEntry
58−}
59−
60−const (
61− marginBottom = 5
62− fileSizeWidth = 7
63− paddingLeft = 2
64−)
65−
66−// KeyMap defines key bindings for each user action.
67−type KeyMap struct {
68− GoToTop key.Binding
69− GoToLast key.Binding
70− Down key.Binding
71− Up key.Binding
72− PageUp key.Binding
73− PageDown key.Binding
74− Back key.Binding
75− Open key.Binding
76− Select key.Binding
77−}
78−
79−// DefaultKeyMap defines the default keybindings.
80−func DefaultKeyMap() KeyMap {
81− return KeyMap{
82− GoToTop: key.NewBinding(key.WithKeys("g"), key.WithHelp("g", "first")),
83− GoToLast: key.NewBinding(key.WithKeys("G"), key.WithHelp("G", "last")),
84− Down: key.NewBinding(key.WithKeys("j", "down", "ctrl+n"), key.WithHelp("j", "down")),
85− Up: key.NewBinding(key.WithKeys("k", "up", "ctrl+p"), key.WithHelp("k", "up")),
86− PageUp: key.NewBinding(key.WithKeys("K", "pgup"), key.WithHelp("pgup", "page up")),
87− PageDown: key.NewBinding(key.WithKeys("J", "pgdown"), key.WithHelp("pgdown", "page down")),
88− Back: key.NewBinding(key.WithKeys("h", "backspace", "left", "esc"), key.WithHelp("h", "back")),
89− Open: key.NewBinding(key.WithKeys("l", "right", "enter"), key.WithHelp("l", "open")),
90− Select: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "select")),
91− }
92−}
93−
94−// Styles defines the possible customizations for styles in the file picker.
95−type Styles struct {
96− DisabledCursor lipgloss.Style
97− Cursor lipgloss.Style
98− Symlink lipgloss.Style
99− Directory lipgloss.Style
100− File lipgloss.Style
101− DisabledFile lipgloss.Style
102− Permission lipgloss.Style
103− Selected lipgloss.Style
104− DisabledSelected lipgloss.Style
105− FileSize lipgloss.Style
106− EmptyDirectory lipgloss.Style
107−}
108−
109−// DefaultStyles defines the default styling for the file picker.
110−func DefaultStyles() Styles {
111− return Styles{
112− DisabledCursor: lipgloss.NewStyle().Foreground(lipgloss.Color("247")),
113− Cursor: lipgloss.NewStyle().Foreground(lipgloss.Color("212")),
114− Symlink: lipgloss.NewStyle().Foreground(lipgloss.Color("36")),
115− Directory: lipgloss.NewStyle().Foreground(lipgloss.Color("99")),
116− File: lipgloss.NewStyle(),
117− DisabledFile: lipgloss.NewStyle().Foreground(lipgloss.Color("243")),
118− DisabledSelected: lipgloss.NewStyle().Foreground(lipgloss.Color("247")),
119− Permission: lipgloss.NewStyle().Foreground(lipgloss.Color("244")),
120− Selected: lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Bold(true),
121− FileSize: lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Width(fileSizeWidth).Align(lipgloss.Right),
122− EmptyDirectory: lipgloss.NewStyle().Foreground(lipgloss.Color("240")).PaddingLeft(paddingLeft).SetString("Bummer. No Files Found."),
123− }
124−}
125−
126−// Model represents a file picker.
127−type Model struct {
128− id int
129−
130− // Path is the path which the user has selected with the file picker.
131− Path string
132−
133− // CurrentDirectory is the directory that the user is currently in.
134− CurrentDirectory string
135−
136− // AllowedTypes specifies which file types the user may select.
137− // If empty the user may select any file.
138− AllowedTypes []string
139−
140− KeyMap KeyMap
141− files []os.DirEntry
142− ShowPermissions bool
143− ShowSize bool
144− ShowHidden bool
145− DirAllowed bool
146− FileAllowed bool
147−
148− FileSelected string
149− selected int
150− selectedStack stack
151−
152− minIdx int
153− maxIdx int
154− maxStack stack
155− minStack stack
156−
157− height int
158− AutoHeight bool
159−
160− Cursor string
161− Styles Styles
162−}
163−
164−type stack struct {
165− Push func(int)
166− Pop func() int
167− Length func() int
168−}
169−
170−func newStack() stack {
171− slice := make([]int, 0)
172− return stack{
173− Push: func(i int) {
174− slice = append(slice, i)
175− },
176− Pop: func() int {
177− res := slice[len(slice)-1]
178− slice = slice[:len(slice)-1]
179− return res
180− },
181− Length: func() int {
182− return len(slice)
183− },
184− }
185−}
186−
187−func (m *Model) pushView(selected, minimum, maximum int) {
188− m.selectedStack.Push(selected)
189− m.minStack.Push(minimum)
190− m.maxStack.Push(maximum)
191−}
192−
193−func (m *Model) popView() (int, int, int) {
194− return m.selectedStack.Pop(), m.minStack.Pop(), m.maxStack.Pop()
195−}
196−
197−func (m Model) readDir(path string, showHidden bool) tea.Cmd {
198− return func() tea.Msg {
199− dirEntries, err := os.ReadDir(path)
200− if err != nil {
201− return errorMsg{err}
202− }
203−
204− sort.Slice(dirEntries, func(i, j int) bool {
205− if dirEntries[i].IsDir() == dirEntries[j].IsDir() {
206− return dirEntries[i].Name() < dirEntries[j].Name()
207− }
208− return dirEntries[i].IsDir()
209− })
210−
211− if showHidden {
212− return readDirMsg{id: m.id, entries: dirEntries}
213− }
214−
215− var sanitizedDirEntries []os.DirEntry
216− for _, dirEntry := range dirEntries {
217− isHidden, _ := IsHidden(dirEntry.Name())
218− if isHidden {
219− continue
220− }
221− sanitizedDirEntries = append(sanitizedDirEntries, dirEntry)
222− }
223− return readDirMsg{id: m.id, entries: sanitizedDirEntries}
224− }
225−}
226−
227−// SetHeight sets the height of the file picker.
228−func (m *Model) SetHeight(h int) {
229− m.height = h
230− if m.maxIdx > m.height-1 {
231− m.maxIdx = m.minIdx + m.height - 1
232− }
233−}
234−
235−// Height returns the height of the file picker.
236−func (m Model) Height() int {
237− return m.height
238−}
239−
240−// Init initializes the file picker model.
241−func (m Model) Init() tea.Cmd {
242− return m.readDir(m.CurrentDirectory, m.ShowHidden)
243−}
244−
245−// Update handles user interactions within the file picker model.
246−func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
247− switch msg := msg.(type) {
248− case readDirMsg:
249− if msg.id != m.id {
250− break
251− }
252− m.files = msg.entries
253− m.maxIdx = max(m.maxIdx, m.Height()-1)
254− case tea.WindowSizeMsg:
255− if m.AutoHeight {
256− m.SetHeight(msg.Height - marginBottom)
257− }
258− m.maxIdx = m.Height() - 1
259− case tea.KeyPressMsg:
260− switch {
261− case key.Matches(msg, m.KeyMap.GoToTop):
262− m.selected = 0
263− m.minIdx = 0
264− m.maxIdx = m.Height() - 1
265− case key.Matches(msg, m.KeyMap.GoToLast):
266− m.selected = len(m.files) - 1
267− m.minIdx = len(m.files) - m.Height()
268− m.maxIdx = len(m.files) - 1
269− case key.Matches(msg, m.KeyMap.Down):
270− m.selected++
271− if m.selected >= len(m.files) {
272− m.selected = len(m.files) - 1
273− }
274− if m.selected > m.maxIdx {
275− m.minIdx++
276− m.maxIdx++
277− }
278− case key.Matches(msg, m.KeyMap.Up):
279− m.selected--
280− if m.selected < 0 {
281− m.selected = 0
282− }
283− if m.selected < m.minIdx {
284− m.minIdx--
285− m.maxIdx--
286− }
287− case key.Matches(msg, m.KeyMap.PageDown):
288− m.selected += m.Height()
289− if m.selected >= len(m.files) {
290− m.selected = len(m.files) - 1
291− }
292− m.minIdx += m.Height()
293− m.maxIdx += m.Height()
294−
295− if m.maxIdx >= len(m.files) {
296− m.maxIdx = len(m.files) - 1
297− m.minIdx = m.maxIdx - m.Height()
298− }
299− case key.Matches(msg, m.KeyMap.PageUp):
300− m.selected -= m.Height()
301− if m.selected < 0 {
302− m.selected = 0
303− }
304− m.minIdx -= m.Height()
305− m.maxIdx -= m.Height()
306−
307− if m.minIdx < 0 {
308− m.minIdx = 0
309− m.maxIdx = m.minIdx + m.Height()
310− }
311− case key.Matches(msg, m.KeyMap.Back):
312− m.CurrentDirectory = filepath.Dir(m.CurrentDirectory)
313− if m.selectedStack.Length() > 0 {
314− m.selected, m.minIdx, m.maxIdx = m.popView()
315− } else {
316− m.selected = 0
317− m.minIdx = 0
318− m.maxIdx = m.Height() - 1
319− }
320− return m, m.readDir(m.CurrentDirectory, m.ShowHidden)
321− case key.Matches(msg, m.KeyMap.Open):
322− if len(m.files) == 0 {
323− break
324− }
325−
326− f := m.files[m.selected]
327− info, err := f.Info()
328− if err != nil {
329− break
330− }
331− isSymlink := info.Mode()&os.ModeSymlink != 0
332− isDir := f.IsDir()
333−
334− if isSymlink {
335− symlinkPath, _ := filepath.EvalSymlinks(filepath.Join(m.CurrentDirectory, f.Name()))
336− info, err := os.Stat(symlinkPath)
337− if err != nil {
338− break
339− }
340− if info.IsDir() {
341− isDir = true
342− }
343− }
344−
345− if (!isDir && m.FileAllowed) || (isDir && m.DirAllowed) {
346− if key.Matches(msg, m.KeyMap.Select) {
347− // Select the current path as the selection
348− m.Path = filepath.Join(m.CurrentDirectory, f.Name())
349− }
350− }
351−
352− if !isDir {
353− break
354− }
355−
356− m.CurrentDirectory = filepath.Join(m.CurrentDirectory, f.Name())
357− m.pushView(m.selected, m.minIdx, m.maxIdx)
358− m.selected = 0
359− m.minIdx = 0
360− m.maxIdx = m.Height() - 1
361− return m, m.readDir(m.CurrentDirectory, m.ShowHidden)
362− }
363− }
364− return m, nil
365−}
366−
367−// View returns the view of the file picker.
368−func (m Model) View() string {
369− if len(m.files) == 0 {
370− return m.Styles.EmptyDirectory.Height(m.Height()).MaxHeight(m.Height()).String()
371− }
372− var s strings.Builder
373−
374− for i, f := range m.files {
375− if i < m.minIdx || i > m.maxIdx {
376− continue
377− }
378−
379− var symlinkPath string
380− info, err := f.Info()
381− if err != nil {
382− continue
383− }
384− isSymlink := info.Mode()&os.ModeSymlink != 0
385− size := strings.Replace(humanize.Bytes(uint64(info.Size())), " ", "", 1) //nolint:gosec
386− name := f.Name()
387−
388− if isSymlink {
389− symlinkPath, _ = filepath.EvalSymlinks(filepath.Join(m.CurrentDirectory, name))
390− }
391−
392− disabled := !m.canSelect(name) && !f.IsDir()
393−
394− if m.selected == i { //nolint:nestif
395− selected := ""
396− if m.ShowPermissions {
397− selected += " " + info.Mode().String()
398− }
399− if m.ShowSize {
400− selected += fmt.Sprintf("%"+strconv.Itoa(m.Styles.FileSize.GetWidth())+"s", size)
401− }
402− selected += " " + name
403− if isSymlink {
404− selected += " → " + symlinkPath
405− }
406− if disabled {
407− s.WriteString(m.Styles.DisabledCursor.Render(m.Cursor) + m.Styles.DisabledSelected.Render(selected))
408− } else {
409− s.WriteString(m.Styles.Cursor.Render(m.Cursor) + m.Styles.Selected.Render(selected))
410− }
411− s.WriteRune('\n')
412− continue
413− }
414−
415− style := m.Styles.File
416− if f.IsDir() {
417− style = m.Styles.Directory
418− } else if isSymlink {
419− style = m.Styles.Symlink
420− } else if disabled {
421− style = m.Styles.DisabledFile
422− }
423−
424− fileName := style.Render(name)
425− s.WriteString(m.Styles.Cursor.Render(" "))
426− if isSymlink {
427− fileName += " → " + symlinkPath
428− }
429− if m.ShowPermissions {
430− s.WriteString(" " + m.Styles.Permission.Render(info.Mode().String()))
431− }
432− if m.ShowSize {
433− s.WriteString(m.Styles.FileSize.Render(size))
434− }
435− s.WriteString(" " + fileName)
436− s.WriteRune('\n')
437− }
438−
439− for i := lipgloss.Height(s.String()); i <= m.Height(); i++ {
440− s.WriteRune('\n')
441− }
442−
443− return s.String()
444−}
445−
446−// DidSelectFile returns whether a user has selected a file (on this msg).
447−func (m Model) DidSelectFile(msg tea.Msg) (bool, string) {
448− didSelect, path := m.didSelectFile(msg)
449− if didSelect && m.canSelect(path) {
450− return true, path
451− }
452− return false, ""
453−}
454−
455−// DidSelectDisabledFile returns whether a user tried to select a disabled file
456−// (on this msg). This is necessary only if you would like to warn the user that
457−// they tried to select a disabled file.
458−func (m Model) DidSelectDisabledFile(msg tea.Msg) (bool, string) {
459− didSelect, path := m.didSelectFile(msg)
460− if didSelect && !m.canSelect(path) {
461− return true, path
462− }
463− return false, ""
464−}
465−
466−func (m Model) didSelectFile(msg tea.Msg) (bool, string) {
467− if len(m.files) == 0 {
468− return false, ""
469− }
470− switch msg := msg.(type) {
471− case tea.KeyPressMsg:
472− // If the msg does not match the Select keymap then this could not have been a selection.
473− if !key.Matches(msg, m.KeyMap.Select) {
474− return false, ""
475− }
476−
477− // The key press was a selection, let's confirm whether the current file could
478− // be selected or used for navigating deeper into the stack.
479− f := m.files[m.selected]
480− info, err := f.Info()
481− if err != nil {
482− return false, ""
483− }
484− isSymlink := info.Mode()&os.ModeSymlink != 0
485− isDir := f.IsDir()
486−
487− if isSymlink {
488− symlinkPath, _ := filepath.EvalSymlinks(filepath.Join(m.CurrentDirectory, f.Name()))
489− info, err := os.Stat(symlinkPath)
490− if err != nil {
491− break
492− }
493− if info.IsDir() {
494− isDir = true
495− }
496− }
497−
498− if (!isDir && m.FileAllowed) || (isDir && m.DirAllowed) && m.Path != "" {
499− return true, m.Path
500− }
501−
502− // If the msg was not a KeyPressMsg, then the file could not have been selected this iteration.
503− // Only a KeyPressMsg can select a file.
504− default:
505− return false, ""
506− }
507− return false, ""
508−}
509−
510−func (m Model) canSelect(file string) bool {
511− if len(m.AllowedTypes) <= 0 {
512− return true
513− }
514−
515− for _, ext := range m.AllowedTypes {
516− if strings.HasSuffix(file, ext) {
517− return true
518− }
519− }
520− return false
521−}
522−
523−// HighlightedPath returns the path of the currently highlighted file or directory.
524−func (m Model) HighlightedPath() string {
525− if len(m.files) == 0 || m.selected < 0 || m.selected >= len(m.files) {
526− return ""
527− }
528− return filepath.Join(m.CurrentDirectory, m.files[m.selected].Name())
529−}
vendor/charm.land/bubbles/v2/filepicker/hidden_unix.godeleted−11
@@ -1,11 +0,0 @@
1−//go:build !windows
2−// +build !windows
3−
4−package filepicker
5−
6−import "strings"
7−
8−// IsHidden reports whether a file is hidden or not.
9−func IsHidden(file string) (bool, error) {
10− return strings.HasPrefix(file, "."), nil
11−}
vendor/charm.land/bubbles/v2/filepicker/hidden_windows.godeleted−21
@@ -1,21 +0,0 @@
1−//go:build windows
2−// +build windows
3−
4−package filepicker
5−
6−import (
7− "syscall"
8−)
9−
10−// IsHidden reports whether a file is hidden or not.
11−func IsHidden(file string) (bool, error) {
12− pointer, err := syscall.UTF16PtrFromString(file)
13− if err != nil {
14− return false, err //nolint:wrapcheck
15− }
16− attributes, err := syscall.GetFileAttributes(pointer)
17− if err != nil {
18− return false, err //nolint:wrapcheck
19− }
20− return attributes&syscall.FILE_ATTRIBUTE_HIDDEN != 0, nil
21−}
vendor/charm.land/bubbles/v2/help/help.godeleted−253
@@ -1,253 +0,0 @@
1−// Package help provides a simple help view for Bubble Tea applications.
2−package help
3−
4−import (
5− "strings"
6−
7− "charm.land/bubbles/v2/key"
8− tea "charm.land/bubbletea/v2"
9− "charm.land/lipgloss/v2"
10−)
11−
12−// KeyMap is a map of keybindings used to generate help. Since it's an
13−// interface it can be any type, though struct or a map[string][]key.Binding
14−// are likely candidates.
15−//
16−// Note that if a key is disabled (via key.Binding.SetEnabled) it will not be
17−// rendered in the help view, so in theory generated help should self-manage.
18−type KeyMap interface {
19− // ShortHelp returns a slice of bindings to be displayed in the short
20− // version of the help. The help bubble will render help in the order in
21− // which the help items are returned here.
22− ShortHelp() []key.Binding
23−
24− // FullHelp returns an extended group of help items, grouped by columns.
25− // The help bubble will render the help in the order in which the help
26− // items are returned here.
27− FullHelp() [][]key.Binding
28−}
29−
30−// Styles is a set of available style definitions for the Help bubble.
31−type Styles struct {
32− Ellipsis lipgloss.Style
33−
34− // Styling for the short help
35− ShortKey lipgloss.Style
36− ShortDesc lipgloss.Style
37− ShortSeparator lipgloss.Style
38−
39− // Styling for the full help
40− FullKey lipgloss.Style
41− FullDesc lipgloss.Style
42− FullSeparator lipgloss.Style
43−}
44−
45−// DefaultStyles returns a set of default styles for the help bubble. Light or
46−// dark styles can be selected by passing true or false to the isDark
47−// parameter.
48−func DefaultStyles(isDark bool) Styles {
49− lightDark := lipgloss.LightDark(isDark)
50−
51− keyStyle := lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("#909090"), lipgloss.Color("#626262")))
52− descStyle := lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("#B2B2B2"), lipgloss.Color("#4A4A4A")))
53− sepStyle := lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("#DADADA"), lipgloss.Color("#3C3C3C")))
54−
55− return Styles{
56− ShortKey: keyStyle,
57− ShortDesc: descStyle,
58− ShortSeparator: sepStyle,
59− Ellipsis: sepStyle,
60− FullKey: keyStyle,
61− FullDesc: descStyle,
62− FullSeparator: sepStyle,
63− }
64−}
65−
66−// DefaultDarkStyles returns a set of default styles for dark backgrounds.
67−func DefaultDarkStyles() Styles {
68− return DefaultStyles(true)
69−}
70−
71−// DefaultLightStyles returns a set of default styles for light backgrounds.
72−func DefaultLightStyles() Styles {
73− return DefaultStyles(false)
74−}
75−
76−// Model contains the state of the help view.
77−type Model struct {
78− ShowAll bool // if true, render the "full" help menu
79−
80− ShortSeparator string
81− FullSeparator string
82−
83− // The symbol we use in the short help when help items have been truncated
84− // due to width. Periods of ellipsis by default.
85− Ellipsis string
86−
87− Styles Styles
88−
89− width int
90−}
91−
92−// New creates a new help view with some useful defaults.
93−func New() Model {
94− return Model{
95− ShortSeparator: " • ",
96− FullSeparator: " ",
97− Ellipsis: "…",
98− Styles: DefaultDarkStyles(),
99− }
100−}
101−
102−// Update helps satisfy the Bubble Tea Model interface. It's a no-op.
103−func (m Model) Update(_ tea.Msg) (Model, tea.Cmd) {
104− return m, nil
105−}
106−
107−// View renders the help view's current state.
108−func (m Model) View(k KeyMap) string {
109− if m.ShowAll {
110− return m.FullHelpView(k.FullHelp())
111− }
112− return m.ShortHelpView(k.ShortHelp())
113−}
114−
115−// SetWidth sets the maximum width for the help view.
116−func (m *Model) SetWidth(w int) {
117− m.width = w
118−}
119−
120−// Width returns the maximum width for the help view.
121−func (m Model) Width() int {
122− return m.width
123−}
124−
125−// ShortHelpView renders a single line help view from a slice of keybindings.
126−// If the line is longer than the maximum width it will be gracefully
127−// truncated, showing only as many help items as possible.
128−func (m Model) ShortHelpView(bindings []key.Binding) string {
129− if len(bindings) == 0 {
130− return ""
131− }
132−
133− var b strings.Builder
134− var totalWidth int
135− separator := m.Styles.ShortSeparator.Inline(true).Render(m.ShortSeparator)
136−
137− for i, kb := range bindings {
138− if !kb.Enabled() {
139− continue
140− }
141−
142− // Sep
143− var sep string
144− if totalWidth > 0 && i < len(bindings) {
145− sep = separator
146− }
147−
148− // Item
149− str := sep +
150− m.Styles.ShortKey.Inline(true).Render(kb.Help().Key) + " " +
151− m.Styles.ShortDesc.Inline(true).Render(kb.Help().Desc)
152− w := lipgloss.Width(str)
153−
154− // Tail
155− if tail, ok := m.shouldAddItem(totalWidth, w); !ok {
156− if tail != "" {
157− b.WriteString(tail)
158− }
159− break
160− }
161−
162− totalWidth += w
163− b.WriteString(str)
164− }
165−
166− return b.String()
167−}
168−
169−// FullHelpView renders help columns from a slice of key binding slices. Each
170−// top level slice entry renders into a column.
171−func (m Model) FullHelpView(groups [][]key.Binding) string {
172− if len(groups) == 0 {
173− return ""
174− }
175−
176− // Linter note: at this time we don't think it's worth the additional
177− // code complexity involved in preallocating this slice.
178− var (
179− out []string
180−
181− totalWidth int
182− separator = m.Styles.FullSeparator.Inline(true).Render(m.FullSeparator)
183− )
184−
185− // Iterate over groups to build columns
186− for i, group := range groups {
187− if group == nil || !shouldRenderColumn(group) {
188− continue
189− }
190− var (
191− sep string
192− keys []string
193− descriptions []string
194− )
195−
196− // Sep
197− if totalWidth > 0 && i < len(groups) {
198− sep = separator
199− }
200−
201− // Separate keys and descriptions into different slices
202− for _, kb := range group {
203− if !kb.Enabled() {
204− continue
205− }
206− keys = append(keys, kb.Help().Key)
207− descriptions = append(descriptions, kb.Help().Desc)
208− }
209−
210− // Column
211− col := lipgloss.JoinHorizontal(lipgloss.Top,
212− sep,
213− m.Styles.FullKey.Render(strings.Join(keys, "\n")),
214− " ",
215− m.Styles.FullDesc.Render(strings.Join(descriptions, "\n")),
216− )
217− w := lipgloss.Width(col)
218−
219− // Tail
220− if tail, ok := m.shouldAddItem(totalWidth, w); !ok {
221− if tail != "" {
222− out = append(out, tail)
223− }
224− break
225− }
226−
227− totalWidth += w
228− out = append(out, col)
229− }
230−
231− return lipgloss.JoinHorizontal(lipgloss.Top, out...)
232−}
233−
234−func (m Model) shouldAddItem(totalWidth, width int) (tail string, ok bool) {
235− // If there's room for an ellipsis, print that.
236− if m.width > 0 && totalWidth+width > m.width {
237− tail = " " + m.Styles.Ellipsis.Inline(true).Render(m.Ellipsis)
238−
239− if totalWidth+lipgloss.Width(tail) < m.width {
240− return tail, false
241− }
242− }
243− return "", true
244−}
245−
246−func shouldRenderColumn(b []key.Binding) (ok bool) {
247− for _, v := range b {
248− if v.Enabled() {
249− return true
250− }
251− }
252− return false
253−}
vendor/charm.land/bubbles/v2/internal/memoization/memoization.godeleted−125
@@ -1,125 +0,0 @@
1−// Package memoization implement a simple memoization cache. It's designed to
2−// improve performance in textarea.
3−package memoization
4−
5−import (
6− "container/list"
7− "crypto/sha256"
8− "fmt"
9− "sync"
10−)
11−
12−// Hasher is an interface that requires a Hash method. The Hash method is
13−// expected to return a string representation of the hash of the object.
14−type Hasher interface {
15− Hash() string
16−}
17−
18−// entry is a struct that holds a key-value pair. It is used as an element
19−// in the evictionList of the MemoCache.
20−type entry[T any] struct {
21− key string
22− value T
23−}
24−
25−// MemoCache is a struct that represents a cache with a set capacity. It
26−// uses an LRU (Least Recently Used) eviction policy. It is safe for
27−// concurrent use.
28−type MemoCache[H Hasher, T any] struct {
29− capacity int
30− mutex sync.Mutex
31− cache map[string]*list.Element // The cache holding the results
32− evictionList *list.List // A list to keep track of the order for LRU
33− hashableItems map[string]T // This map keeps track of the original hashable items (optional)
34−}
35−
36−// NewMemoCache is a function that creates a new MemoCache with a given
37−// capacity. It returns a pointer to the created MemoCache.
38−func NewMemoCache[H Hasher, T any](capacity int) *MemoCache[H, T] {
39− return &MemoCache[H, T]{
40− capacity: capacity,
41− cache: make(map[string]*list.Element),
42− evictionList: list.New(),
43− hashableItems: make(map[string]T),
44− }
45−}
46−
47−// Capacity is a method that returns the capacity of the MemoCache.
48−func (m *MemoCache[H, T]) Capacity() int {
49− return m.capacity
50−}
51−
52−// Size is a method that returns the current size of the MemoCache. It is
53−// the number of items currently stored in the cache.
54−func (m *MemoCache[H, T]) Size() int {
55− m.mutex.Lock()
56− defer m.mutex.Unlock()
57− return m.evictionList.Len()
58−}
59−
60−// Get is a method that returns the value associated with the given
61−// hashable item in the MemoCache. If there is no corresponding value, the
62−// method returns nil.
63−func (m *MemoCache[H, T]) Get(h H) (T, bool) {
64− m.mutex.Lock()
65− defer m.mutex.Unlock()
66−
67− hashedKey := h.Hash()
68− if element, found := m.cache[hashedKey]; found {
69− m.evictionList.MoveToFront(element)
70− return element.Value.(*entry[T]).value, true
71− }
72− var result T
73− return result, false
74−}
75−
76−// Set is a method that sets the value for the given hashable item in the
77−// MemoCache. If the cache is at capacity, it evicts the least recently
78−// used item before adding the new item.
79−func (m *MemoCache[H, T]) Set(h H, value T) {
80− m.mutex.Lock()
81− defer m.mutex.Unlock()
82−
83− hashedKey := h.Hash()
84− if element, found := m.cache[hashedKey]; found {
85− m.evictionList.MoveToFront(element)
86− element.Value.(*entry[T]).value = value
87− return
88− }
89−
90− // Check if the cache is at capacity
91− if m.evictionList.Len() >= m.capacity {
92− // Evict the least recently used item from the cache
93− toEvict := m.evictionList.Back()
94− if toEvict != nil {
95− evictedEntry := m.evictionList.Remove(toEvict).(*entry[T])
96− delete(m.cache, evictedEntry.key)
97− delete(m.hashableItems, evictedEntry.key) // if you're keeping track of original items
98− }
99− }
100−
101− // Add the value to the cache and the evictionList
102− newEntry := &entry[T]{
103− key: hashedKey,
104− value: value,
105− }
106− element := m.evictionList.PushFront(newEntry)
107− m.cache[hashedKey] = element
108− m.hashableItems[hashedKey] = value // if you're keeping track of original items
109−}
110−
111−// HString is a type that implements the Hasher interface for strings.
112−type HString string
113−
114−// Hash is a method that returns the hash of the string.
115−func (h HString) Hash() string {
116− return fmt.Sprintf("%x", sha256.Sum256([]byte(h)))
117−}
118−
119−// HInt is a type that implements the Hasher interface for integers.
120−type HInt int
121−
122−// Hash is a method that returns the hash of the integer.
123−func (h HInt) Hash() string {
124− return fmt.Sprintf("%x", sha256.Sum256(fmt.Appendf(nil, "%d", h)))
125−}
vendor/charm.land/bubbles/v2/internal/runeutil/runeutil.godeleted−102
@@ -1,102 +0,0 @@
1−// Package runeutil provides utility functions for tidying up incoming runes
2−// from Key messages.
3−package runeutil
4−
5−import (
6− "unicode"
7− "unicode/utf8"
8−)
9−
10−// Sanitizer is a helper for bubble widgets that want to process
11−// Runes from input key messages.
12−type Sanitizer interface {
13− // Sanitize removes control characters from runes in a KeyRunes
14− // message, and optionally replaces newline/carriage return/tabs by a
15− // specified character.
16− //
17− // The rune array is modified in-place if possible. In that case, the
18− // returned slice is the original slice shortened after the control
19− // characters have been removed/translated.
20− Sanitize(runes []rune) []rune
21−}
22−
23−// NewSanitizer constructs a rune sanitizer.
24−func NewSanitizer(opts ...Option) Sanitizer {
25− s := sanitizer{
26− replaceNewLine: []rune("\n"),
27− replaceTab: []rune(" "),
28− }
29− for _, o := range opts {
30− s = o(s)
31− }
32− return &s
33−}
34−
35−// Option is the type of option that can be passed to Sanitize().
36−type Option func(sanitizer) sanitizer
37−
38−// ReplaceTabs replaces tabs by the specified string.
39−func ReplaceTabs(tabRepl string) Option {
40− return func(s sanitizer) sanitizer {
41− s.replaceTab = []rune(tabRepl)
42− return s
43− }
44−}
45−
46−// ReplaceNewlines replaces newline characters by the specified string.
47−func ReplaceNewlines(nlRepl string) Option {
48− return func(s sanitizer) sanitizer {
49− s.replaceNewLine = []rune(nlRepl)
50− return s
51− }
52−}
53−
54−func (s *sanitizer) Sanitize(runes []rune) []rune {
55− // dstrunes are where we are storing the result.
56− dstrunes := runes[:0:len(runes)]
57− // copied indicates whether dstrunes is an alias of runes
58− // or a copy. We need a copy when dst moves past src.
59− // We use this as an optimization to avoid allocating
60− // a new rune slice in the common case where the output
61− // is smaller or equal to the input.
62− copied := false
63−
64− for src := range runes {
65− r := runes[src]
66− switch {
67− case r == utf8.RuneError:
68− // skip
69−
70− case r == '\r' || r == '\n':
71− if len(dstrunes)+len(s.replaceNewLine) > src && !copied {
72− dst := len(dstrunes)
73− dstrunes = make([]rune, dst, len(runes)+len(s.replaceNewLine))
74− copy(dstrunes, runes[:dst])
75− copied = true
76− }
77− dstrunes = append(dstrunes, s.replaceNewLine...)
78−
79− case r == '\t':
80− if len(dstrunes)+len(s.replaceTab) > src && !copied {
81− dst := len(dstrunes)
82− dstrunes = make([]rune, dst, len(runes)+len(s.replaceTab))
83− copy(dstrunes, runes[:dst])
84− copied = true
85− }
86− dstrunes = append(dstrunes, s.replaceTab...)
87−
88− case unicode.IsControl(r):
89− // Other control characters: skip.
90−
91− default:
92− // Keep the character.
93− dstrunes = append(dstrunes, runes[src])
94− }
95− }
96− return dstrunes
97−}
98−
99−type sanitizer struct {
100− replaceNewLine []rune
101− replaceTab []rune
102−}
vendor/charm.land/bubbles/v2/key/key.godeleted−140
@@ -1,140 +0,0 @@
1−// Package key provides some types and functions for generating user-definable
2−// keymappings useful in Bubble Tea components. There are a few different ways
3−// you can define a keymapping with this package. Here's one example:
4−//
5−// type KeyMap struct {
6−// Up key.Binding
7−// Down key.Binding
8−// }
9−//
10−// var DefaultKeyMap = KeyMap{
11−// Up: key.NewBinding(
12−// key.WithKeys("k", "up"), // actual keybindings
13−// key.WithHelp("↑/k", "move up"), // corresponding help text
14−// ),
15−// Down: key.NewBinding(
16−// key.WithKeys("j", "down"),
17−// key.WithHelp("↓/j", "move down"),
18−// ),
19−// }
20−//
21−// func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
22−// switch msg := msg.(type) {
23−// case tea.KeyPressMsg:
24−// switch {
25−// case key.Matches(msg, DefaultKeyMap.Up):
26−// // The user pressed up
27−// case key.Matches(msg, DefaultKeyMap.Down):
28−// // The user pressed down
29−// }
30−// }
31−//
32−// // ...
33−// }
34−//
35−// The help information, which is not used in the example above, can be used
36−// to render help text for keystrokes in your views.
37−package key
38−
39−import "fmt"
40−
41−// Binding describes a set of keybindings and, optionally, their associated
42−// help text.
43−type Binding struct {
44− keys []string
45− help Help
46− disabled bool
47−}
48−
49−// BindingOpt is an initialization option for a keybinding. It's used as an
50−// argument to NewBinding.
51−type BindingOpt func(*Binding)
52−
53−// NewBinding returns a new keybinding from a set of BindingOpt options.
54−func NewBinding(opts ...BindingOpt) Binding {
55− b := &Binding{}
56− for _, opt := range opts {
57− opt(b)
58− }
59− return *b
60−}
61−
62−// WithKeys initializes a keybinding with the given keystrokes.
63−func WithKeys(keys ...string) BindingOpt {
64− return func(b *Binding) {
65− b.keys = keys
66− }
67−}
68−
69−// WithHelp initializes a keybinding with the given help text.
70−func WithHelp(key, desc string) BindingOpt {
71− return func(b *Binding) {
72− b.help = Help{Key: key, Desc: desc}
73− }
74−}
75−
76−// WithDisabled initializes a disabled keybinding.
77−func WithDisabled() BindingOpt {
78− return func(b *Binding) {
79− b.disabled = true
80− }
81−}
82−
83−// SetKeys sets the keys for the keybinding.
84−func (b *Binding) SetKeys(keys ...string) {
85− b.keys = keys
86−}
87−
88−// Keys returns the keys for the keybinding.
89−func (b Binding) Keys() []string {
90− return b.keys
91−}
92−
93−// SetHelp sets the help text for the keybinding.
94−func (b *Binding) SetHelp(key, desc string) {
95− b.help = Help{Key: key, Desc: desc}
96−}
97−
98−// Help returns the Help information for the keybinding.
99−func (b Binding) Help() Help {
100− return b.help
101−}
102−
103−// Enabled returns whether or not the keybinding is enabled. Disabled
104−// keybindings won't be activated and won't show up in help. Keybindings are
105−// enabled by default.
106−func (b Binding) Enabled() bool {
107− return !b.disabled && b.keys != nil
108−}
109−
110−// SetEnabled enables or disables the keybinding.
111−func (b *Binding) SetEnabled(v bool) {
112− b.disabled = !v
113−}
114−
115−// Unbind removes the keys and help from this binding, effectively nullifying
116−// it. This is a step beyond disabling it, since applications can enable
117−// or disable key bindings based on application state.
118−func (b *Binding) Unbind() {
119− b.keys = nil
120− b.help = Help{}
121−}
122−
123−// Help is help information for a given keybinding.
124−type Help struct {
125− Key string
126− Desc string
127−}
128−
129−// Matches checks if the given key matches the given bindings.
130−func Matches[Key fmt.Stringer](k Key, b ...Binding) bool {
131− keys := k.String()
132− for _, binding := range b {
133− for _, v := range binding.keys {
134− if keys == v && binding.Enabled() {
135− return true
136− }
137− }
138− }
139− return false
140−}
vendor/charm.land/bubbles/v2/spinner/spinner.godeleted−211
@@ -1,211 +0,0 @@
1−// Package spinner provides a spinner component for Bubble Tea applications.
2−package spinner
3−
4−import (
5− "sync/atomic"
6− "time"
7−
8− tea "charm.land/bubbletea/v2"
9− "charm.land/lipgloss/v2"
10−)
11−
12−// Internal ID management. Used during animating to ensure that frame messages
13−// are received only by spinner components that sent them.
14−var lastID int64
15−
16−func nextID() int {
17− return int(atomic.AddInt64(&lastID, 1))
18−}
19−
20−// Spinner is a set of frames used in animating the spinner.
21−type Spinner struct {
22− Frames []string
23− FPS time.Duration
24−}
25−
26−// Some spinners to choose from. You could also make your own.
27−var (
28− Line = Spinner{
29− Frames: []string{"|", "/", "-", "\\"},
30− FPS: time.Second / 10, //nolint:mnd
31− }
32− Dot = Spinner{
33− Frames: []string{"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "},
34− FPS: time.Second / 10, //nolint:mnd
35− }
36− MiniDot = Spinner{
37− Frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
38− FPS: time.Second / 12, //nolint:mnd
39− }
40− Jump = Spinner{
41− Frames: []string{"⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"},
42− FPS: time.Second / 10, //nolint:mnd
43− }
44− Pulse = Spinner{
45− Frames: []string{"█", "▓", "▒", "░"},
46− FPS: time.Second / 8, //nolint:mnd
47− }
48− Points = Spinner{
49− Frames: []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●"},
50− FPS: time.Second / 7, //nolint:mnd
51− }
52− Globe = Spinner{
53− Frames: []string{"🌍", "🌎", "🌏"},
54− FPS: time.Second / 4, //nolint:mnd
55− }
56− Moon = Spinner{
57− Frames: []string{"🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"},
58− FPS: time.Second / 8, //nolint:mnd
59− }
60− Monkey = Spinner{
61− Frames: []string{"🙈", "🙉", "🙊"},
62− FPS: time.Second / 3, //nolint:mnd
63− }
64− Meter = Spinner{
65− Frames: []string{
66− "▱▱▱",
67− "▰▱▱",
68− "▰▰▱",
69− "▰▰▰",
70− "▰▰▱",
71− "▰▱▱",
72− "▱▱▱",
73− },
74− FPS: time.Second / 7, //nolint:mnd
75− }
76− Hamburger = Spinner{
77− Frames: []string{"☱", "☲", "☴", "☲"},
78− FPS: time.Second / 3, //nolint:mnd
79− }
80− Ellipsis = Spinner{
81− Frames: []string{"", ".", "..", "..."},
82− FPS: time.Second / 3, //nolint:mnd
83− }
84−)
85−
86−// Model contains the state for the spinner. Use New to create new models
87−// rather than using Model as a struct literal.
88−type Model struct {
89− // Spinner settings to use. See type Spinner.
90− Spinner Spinner
91−
92− // Style sets the styling for the spinner. Most of the time you'll just
93− // want foreground and background coloring, and potentially some padding.
94− //
95− // For an introduction to styling with Lip Gloss see:
96− // https://github.com/charmbracelet/lipgloss
97− Style lipgloss.Style
98−
99− frame int
100− id int
101− tag int
102−}
103−
104−// ID returns the spinner's unique ID.
105−func (m Model) ID() int {
106− return m.id
107−}
108−
109−// New returns a model with default values.
110−func New(opts ...Option) Model {
111− m := Model{
112− Spinner: Line,
113− id: nextID(),
114− }
115−
116− for _, opt := range opts {
117− opt(&m)
118− }
119−
120− return m
121−}
122−
123−// TickMsg indicates that the timer has ticked and we should render a frame.
124−type TickMsg struct {
125− Time time.Time
126− tag int
127− ID int
128−}
129−
130−// Update is the Tea update function.
131−func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
132− switch msg := msg.(type) {
133− case TickMsg:
134− // If an ID is set, and the ID doesn't belong to this spinner, reject
135− // the message.
136− if msg.ID > 0 && msg.ID != m.id {
137− return m, nil
138− }
139−
140− // If a tag is set, and it's not the one we expect, reject the message.
141− // This prevents the spinner from receiving too many messages and
142− // thus spinning too fast.
143− if msg.tag > 0 && msg.tag != m.tag {
144− return m, nil
145− }
146−
147− m.frame++
148− if m.frame >= len(m.Spinner.Frames) {
149− m.frame = 0
150− }
151−
152− m.tag++
153− return m, m.tick(m.id, m.tag)
154− default:
155− return m, nil
156− }
157−}
158−
159−// View renders the model's view.
160−func (m Model) View() string {
161− if m.frame >= len(m.Spinner.Frames) {
162− return "(error)"
163− }
164−
165− return m.Style.Render(m.Spinner.Frames[m.frame])
166−}
167−
168−// Tick is the command used to advance the spinner one frame. Use this command
169−// to effectively start the spinner.
170−func (m Model) Tick() tea.Msg {
171− return TickMsg{
172− // The time at which the tick occurred.
173− Time: time.Now(),
174−
175− // The ID of the spinner that this message belongs to. This can be
176− // helpful when routing messages, however bear in mind that spinners
177− // will ignore messages that don't contain ID by default.
178− ID: m.id,
179−
180− tag: m.tag,
181− }
182−}
183−
184−func (m Model) tick(id, tag int) tea.Cmd {
185− return tea.Tick(m.Spinner.FPS, func(t time.Time) tea.Msg {
186− return TickMsg{
187− Time: t,
188− ID: id,
189− tag: tag,
190− }
191− })
192−}
193−
194−// Option is used to set options in New. For example:
195−//
196−// spinner := New(WithSpinner(Dot))
197−type Option func(*Model)
198−
199−// WithSpinner is an option to set the spinner. Pass this to [Spinner.New].
200−func WithSpinner(spinner Spinner) Option {
201− return func(m *Model) {
202− m.Spinner = spinner
203− }
204−}
205−
206−// WithStyle is an option to set the spinner style. Pass this to [Spinner.New].
207−func WithStyle(style lipgloss.Style) Option {
208− return func(m *Model) {
209− m.Style = style
210− }
211−}
vendor/charm.land/bubbles/v2/textarea/textarea.godeleted−1897

This diff is too big to show here.

vendor/charm.land/bubbles/v2/textinput/styles.godeleted−96

This diff is too big to show here.

vendor/charm.land/bubbles/v2/textinput/textinput.godeleted−968

This diff is too big to show here.

vendor/charm.land/bubbles/v2/viewport/highlight.godeleted−141

This diff is too big to show here.

vendor/charm.land/bubbles/v2/viewport/keymap.godeleted−58

This diff is too big to show here.

vendor/charm.land/bubbles/v2/viewport/viewport.godeleted−765

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/.gitattributesdeleted−1

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/.gitignoredeleted−23

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/.golangci.ymldeleted−47

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/.goreleaser.ymldeleted−5

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/LICENSEdeleted−21

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/README.mddeleted−402

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/Taskfile.yamldeleted−38

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/UPGRADE_GUIDE_V2.mddeleted−573

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/clipboard.godeleted−70

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/color.godeleted−91

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/commands.godeleted−175

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/cursed_renderer.godeleted−860

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/cursor.godeleted−28

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/environ.godeleted−34

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/exec.godeleted−129

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/focus.godeleted−9

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/input.godeleted−54

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/key.godeleted−371

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/keyboard.godeleted−59

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/logging.godeleted−53

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/mod.godeleted−27

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/mouse.godeleted−144

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/nil_renderer.godeleted−53

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/options.godeleted−168

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/paste.godeleted−20

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/profile.godeleted−15

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/raw.godeleted−37

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/renderer.godeleted−104

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/screen.godeleted−68

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/signals_unix.godeleted−33

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/signals_windows.godeleted−10

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/tea.godeleted−1439

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/termcap.godeleted−48

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/termios_bsd.godeleted−13

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/termios_other.godeleted−8

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/termios_unix.godeleted−14

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/termios_windows.godeleted−11

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/tty.godeleted−136

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/tty_unix.godeleted−47

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/tty_windows.godeleted−64

This diff is too big to show here.

vendor/charm.land/bubbletea/v2/xterm.godeleted−22

This diff is too big to show here.

vendor/charm.land/catwalk/LICENSEdeleted−21

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/aihubmix.jsondeleted−3746

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/alibaba-singapore.jsondeleted−141

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/alibaba-united-states.jsondeleted−45

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/anthropic.jsondeleted−183

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/avian.jsondeleted−125

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/azure.jsondeleted−225

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/baseten.jsondeleted−187

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/bedrock-europe.jsondeleted−115

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/bedrock-united-states.jsondeleted−143

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/cerebras.jsondeleted−42

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/chutes.jsondeleted−227

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/copilot.jsondeleted−310

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/cortecs.jsondeleted−1697

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/deepseek.jsondeleted−45

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/fireworks.jsondeleted−171

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/gemini.jsondeleted−91

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/groq.jsondeleted−36

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/huggingface.jsondeleted−267

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/ionet.jsondeleted−335

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/kimi.jsondeleted−23

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/minimax-china.jsondeleted−91

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/minimax.jsondeleted−91

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/moonshot.jsondeleted−131

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/nebius.jsondeleted−419

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/neuralwatt.jsondeleted−155

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/openai.jsondeleted−352

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/opencode-go.jsondeleted−351

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/opencode-zen.jsondeleted−908

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/openrouter.jsondeleted−3831

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/qiniucloud.jsondeleted−179

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/scaleway.jsondeleted−141

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/synthetic.jsondeleted−197

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/venice.jsondeleted−1173

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/vercel.jsondeleted−3072

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/vertexai.jsondeleted−170

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/xai.jsondeleted−59

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/zai.jsondeleted−136

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/zhipu-coding.jsondeleted−136

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/configs/zhipu.jsondeleted−120

This diff is too big to show here.

vendor/charm.land/catwalk/internal/providers/providers.godeleted−349

This diff is too big to show here.

vendor/charm.land/catwalk/pkg/catwalk/client.godeleted−79

This diff is too big to show here.

vendor/charm.land/catwalk/pkg/catwalk/pkg.godeleted−2

This diff is too big to show here.

vendor/charm.land/catwalk/pkg/catwalk/provider.godeleted−157

This diff is too big to show here.

vendor/charm.land/catwalk/pkg/embedded/embedded.godeleted−13

This diff is too big to show here.

vendor/charm.land/fang/v2/.gitignoredeleted−3

This diff is too big to show here.

vendor/charm.land/fang/v2/.golangci.ymldeleted−47

This diff is too big to show here.

vendor/charm.land/fang/v2/.goreleaser.ymldeleted−5

This diff is too big to show here.

vendor/charm.land/fang/v2/LICENSE.mddeleted−21

This diff is too big to show here.

vendor/charm.land/fang/v2/README.mddeleted−92

This diff is too big to show here.

vendor/charm.land/fang/v2/Taskfile.ymldeleted−7

This diff is too big to show here.

vendor/charm.land/fang/v2/UPGRADE_GUIDE_V2.mddeleted−127

This diff is too big to show here.

vendor/charm.land/fang/v2/fang.godeleted−208

This diff is too big to show here.

vendor/charm.land/fang/v2/fang_other.godeleted−10

This diff is too big to show here.

vendor/charm.land/fang/v2/fang_windows.godeleted−30

This diff is too big to show here.

vendor/charm.land/fang/v2/help.godeleted−482

This diff is too big to show here.

vendor/charm.land/fang/v2/theme.godeleted−222

This diff is too big to show here.

vendor/charm.land/fantasy/.editorconfigdeleted−18

This diff is too big to show here.

vendor/charm.land/fantasy/.gitattributesdeleted−1

This diff is too big to show here.

vendor/charm.land/fantasy/.gitignoredeleted−42

This diff is too big to show here.

vendor/charm.land/fantasy/.golangci.ymldeleted−44

This diff is too big to show here.

vendor/charm.land/fantasy/.goreleaser.ymldeleted−6

This diff is too big to show here.

vendor/charm.land/fantasy/AGENTS.mddeleted−29

This diff is too big to show here.

vendor/charm.land/fantasy/LICENSEdeleted−177

This diff is too big to show here.

vendor/charm.land/fantasy/NOTICEdeleted−13

This diff is too big to show here.

vendor/charm.land/fantasy/README.mddeleted−95

This diff is too big to show here.

vendor/charm.land/fantasy/Taskfile.yamldeleted−45

This diff is too big to show here.

vendor/charm.land/fantasy/agent.godeleted−1702

This diff is too big to show here.

vendor/charm.land/fantasy/content.godeleted−625

This diff is too big to show here.

vendor/charm.land/fantasy/content_json.godeleted−1046

This diff is too big to show here.

vendor/charm.land/fantasy/crush.jsondeleted−6

This diff is too big to show here.

vendor/charm.land/fantasy/cspell.jsondeleted−1

This diff is too big to show here.

vendor/charm.land/fantasy/doc.godeleted−2

This diff is too big to show here.

vendor/charm.land/fantasy/errors.godeleted−155

This diff is too big to show here.

vendor/charm.land/fantasy/jsonrepair/jsonrepair.godeleted−1648

This diff is too big to show here.

vendor/charm.land/fantasy/model.godeleted−261

This diff is too big to show here.

vendor/charm.land/fantasy/model_json.godeleted−156

This diff is too big to show here.

vendor/charm.land/fantasy/object.godeleted−236

This diff is too big to show here.

vendor/charm.land/fantasy/object/object.godeleted−620

This diff is too big to show here.

vendor/charm.land/fantasy/provider.godeleted−11

This diff is too big to show here.

vendor/charm.land/fantasy/provider_registry.godeleted−106

This diff is too big to show here.

vendor/charm.land/fantasy/providers/anthropic/anthropic.godeleted−1707

This diff is too big to show here.

vendor/charm.land/fantasy/providers/anthropic/bedrock.godeleted−15

This diff is too big to show here.

vendor/charm.land/fantasy/providers/anthropic/call_useragent.godeleted−14

This diff is too big to show here.

vendor/charm.land/fantasy/providers/anthropic/computer_use.godeleted−427

This diff is too big to show here.

vendor/charm.land/fantasy/providers/anthropic/error.godeleted−67

This diff is too big to show here.

vendor/charm.land/fantasy/providers/anthropic/google.godeleted−11

This diff is too big to show here.

vendor/charm.land/fantasy/providers/anthropic/provider_options.godeleted−283

This diff is too big to show here.

vendor/charm.land/fantasy/providers/anthropic/sanitize.godeleted−33

This diff is too big to show here.

vendor/charm.land/fantasy/providers/azure/README.mddeleted−6

This diff is too big to show here.

vendor/charm.land/fantasy/providers/azure/azure.godeleted−125

This diff is too big to show here.

vendor/charm.land/fantasy/providers/bedrock/README.mddeleted−10

This diff is too big to show here.

vendor/charm.land/fantasy/providers/bedrock/bedrock.godeleted−87

This diff is too big to show here.

vendor/charm.land/fantasy/providers/google/README.mddeleted−63

This diff is too big to show here.

vendor/charm.land/fantasy/providers/google/auth.godeleted−15

This diff is too big to show here.

vendor/charm.land/fantasy/providers/google/call_useragent.godeleted−53

This diff is too big to show here.

vendor/charm.land/fantasy/providers/google/error.godeleted−51

This diff is too big to show here.

vendor/charm.land/fantasy/providers/google/google.godeleted−1488

This diff is too big to show here.

vendor/charm.land/fantasy/providers/google/provider_options.godeleted−146

This diff is too big to show here.

vendor/charm.land/fantasy/providers/google/slice.godeleted−11

This diff is too big to show here.

vendor/charm.land/fantasy/providers/internal/httpheaders/httpheaders.godeleted−60

This diff is too big to show here.

vendor/charm.land/fantasy/providers/openai/call_useragent.godeleted−31

This diff is too big to show here.

vendor/charm.land/fantasy/providers/openai/error.godeleted−92

This diff is too big to show here.

vendor/charm.land/fantasy/providers/openai/language_model.godeleted−990

This diff is too big to show here.

vendor/charm.land/fantasy/providers/openai/language_model_hooks.godeleted−668

This diff is too big to show here.

vendor/charm.land/fantasy/providers/openai/openai.godeleted−220

This diff is too big to show here.

vendor/charm.land/fantasy/providers/openai/provider_options.godeleted−217

This diff is too big to show here.

vendor/charm.land/fantasy/providers/openai/responses_language_model.godeleted−1678

This diff is too big to show here.

vendor/charm.land/fantasy/providers/openai/responses_options.godeleted−395

This diff is too big to show here.

vendor/charm.land/fantasy/providers/openaicompat/language_model_hooks.godeleted−516

This diff is too big to show here.

vendor/charm.land/fantasy/providers/openaicompat/openaicompat.godeleted−140

This diff is too big to show here.

vendor/charm.land/fantasy/providers/openaicompat/provider_options.godeleted−161

This diff is too big to show here.

vendor/charm.land/fantasy/providers/openrouter/language_model_hooks.godeleted−1063

This diff is too big to show here.

vendor/charm.land/fantasy/providers/openrouter/openrouter.godeleted−121

This diff is too big to show here.

vendor/charm.land/fantasy/providers/openrouter/provider_options.godeleted−218

This diff is too big to show here.

vendor/charm.land/fantasy/providers/vercel/language_model_hooks.godeleted−1074

This diff is too big to show here.

vendor/charm.land/fantasy/providers/vercel/provider_options.godeleted−195

This diff is too big to show here.

vendor/charm.land/fantasy/providers/vercel/vercel.godeleted−122

This diff is too big to show here.

vendor/charm.land/fantasy/retry.godeleted−155

This diff is too big to show here.

vendor/charm.land/fantasy/schema/schema.godeleted−442

This diff is too big to show here.

vendor/charm.land/fantasy/tool.godeleted−176

This diff is too big to show here.

vendor/charm.land/fantasy/util.godeleted−22

This diff is too big to show here.

vendor/charm.land/fantasy/version.godeleted−12

This diff is too big to show here.

vendor/charm.land/fantasy/version.txtdeleted−1

This diff is too big to show here.

vendor/charm.land/glamour/v2/.editorconfigdeleted−18

This diff is too big to show here.

vendor/charm.land/glamour/v2/.gitattributesdeleted−2

This diff is too big to show here.

vendor/charm.land/glamour/v2/.gitignoredeleted−2

This diff is too big to show here.

vendor/charm.land/glamour/v2/.golangci.ymldeleted−44

This diff is too big to show here.

vendor/charm.land/glamour/v2/.goreleaser.ymldeleted−6

This diff is too big to show here.

vendor/charm.land/glamour/v2/CONTRIBUTING.mddeleted−23

This diff is too big to show here.

vendor/charm.land/glamour/v2/LICENSEdeleted−21

This diff is too big to show here.

vendor/charm.land/glamour/v2/README.mddeleted−129

This diff is too big to show here.

vendor/charm.land/glamour/v2/Taskfile.ymldeleted−25

This diff is too big to show here.

vendor/charm.land/glamour/v2/UPGRADE_GUIDE_V2.mddeleted−279

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/ansi.godeleted−3

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/baseelement.godeleted−157

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/blockelement.godeleted−66

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/blockstack.godeleted−95

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/codeblock.godeleted−151

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/codespan.godeleted−15

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/context.godeleted−38

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/elements.godeleted−481

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/emphasis.godeleted−47

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/heading.godeleted−80

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/image.godeleted−57

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/link.godeleted−112

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/listitem.godeleted−29

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/margin.godeleted−231

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/paragraph.godeleted−63

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/renderer.godeleted−166

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/style.godeleted−252

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/table.godeleted−206

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/table_links.godeleted−234

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/task.godeleted−27

This diff is too big to show here.

vendor/charm.land/glamour/v2/ansi/templatehelper.godeleted−86

This diff is too big to show here.

vendor/charm.land/glamour/v2/examples.shdeleted−19

This diff is too big to show here.

vendor/charm.land/glamour/v2/gallery.shdeleted−23

This diff is too big to show here.

vendor/charm.land/glamour/v2/glamour.godeleted−294

This diff is too big to show here.

vendor/charm.land/glamour/v2/internal/autolink/autolink.godeleted−55

This diff is too big to show here.

vendor/charm.land/glamour/v2/styles/README.mddeleted−565

This diff is too big to show here.

vendor/charm.land/glamour/v2/styles/ascii.jsondeleted−87

This diff is too big to show here.

vendor/charm.land/glamour/v2/styles/dark.jsondeleted−191

This diff is too big to show here.

vendor/charm.land/glamour/v2/styles/dracula.godeleted−216

This diff is too big to show here.

vendor/charm.land/glamour/v2/styles/dracula.jsondeleted−188

This diff is too big to show here.

vendor/charm.land/glamour/v2/styles/light.jsondeleted−190

This diff is too big to show here.

vendor/charm.land/glamour/v2/styles/notty.jsondeleted−87

This diff is too big to show here.

vendor/charm.land/glamour/v2/styles/pink.jsondeleted−90

This diff is too big to show here.

vendor/charm.land/glamour/v2/styles/styles.godeleted−691

This diff is too big to show here.

vendor/charm.land/glamour/v2/styles/tokyo-night.godeleted−209

This diff is too big to show here.

vendor/charm.land/glamour/v2/styles/tokyo-night.jsondeleted−182

This diff is too big to show here.

vendor/charm.land/glamour/v2/styles/tokyo_night.jsondeleted−182

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/.editorconfigdeleted−18

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/.gitattributesdeleted−1

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/.gitignoredeleted−3

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/.golangci.ymldeleted−50

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/.goreleaser.ymldeleted−5

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/LICENSEdeleted−21

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/README.mddeleted−996

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/Taskfile.yamldeleted−24

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/UPGRADE_GUIDE_V2.mddeleted−504

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/align.godeleted−82

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/ansi_unix.godeleted−8

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/ansi_windows.godeleted−30

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/blending.godeleted−196

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/borders.godeleted−587

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/canvas.godeleted−88

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/color.godeleted−359

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/get.godeleted−657

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/join.godeleted−175

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/layer.godeleted−327

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/lipgloss.godeleted−3

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/position.godeleted−134

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/query.godeleted−92

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/ranges.godeleted−48

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/runes.godeleted−43

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/set.godeleted−932

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/size.godeleted−40

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/style.godeleted−637

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/table/resizing.godeleted−489

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/table/rows.godeleted−130

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/table/table.godeleted−568

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/table/util.godeleted−44

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/terminal.godeleted−124

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/tree/children.godeleted−100

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/tree/enumerator.godeleted−74

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/tree/renderer.godeleted−165

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/tree/tree.godeleted−407

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/unset.godeleted−359

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/whitespace.godeleted−76

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/wrap.godeleted−113

This diff is too big to show here.

vendor/charm.land/lipgloss/v2/writer.godeleted−160

This diff is too big to show here.

vendor/charm.land/log/v2/.gitignoredeleted−17

This diff is too big to show here.

vendor/charm.land/log/v2/.golangci.ymldeleted−50

This diff is too big to show here.

vendor/charm.land/log/v2/.goreleaser.ymldeleted−6

This diff is too big to show here.

vendor/charm.land/log/v2/LICENSEdeleted−21

This diff is too big to show here.

vendor/charm.land/log/v2/README.mddeleted−367

This diff is too big to show here.

vendor/charm.land/log/v2/UPGRADE_GUIDE_V2.mddeleted−320

This diff is too big to show here.

vendor/charm.land/log/v2/context.godeleted−23

This diff is too big to show here.

vendor/charm.land/log/v2/doc.godeleted−3

This diff is too big to show here.

vendor/charm.land/log/v2/formatter.godeleted−27

This diff is too big to show here.

vendor/charm.land/log/v2/json.godeleted−157

This diff is too big to show here.

vendor/charm.land/log/v2/level.godeleted−65

This diff is too big to show here.

vendor/charm.land/log/v2/logfmt.godeleted−32

This diff is too big to show here.

vendor/charm.land/log/v2/logger.godeleted−413

This diff is too big to show here.

vendor/charm.land/log/v2/logger_121.godeleted−72

This diff is too big to show here.

vendor/charm.land/log/v2/logger_no121.godeleted−69

This diff is too big to show here.

vendor/charm.land/log/v2/options.godeleted−61

This diff is too big to show here.

vendor/charm.land/log/v2/pkg.godeleted−248

This diff is too big to show here.

vendor/charm.land/log/v2/stdlog.godeleted−67

This diff is too big to show here.

vendor/charm.land/log/v2/styles.godeleted−83

This diff is too big to show here.

vendor/charm.land/log/v2/text.godeleted−272

This diff is too big to show here.

vendor/charm.land/x/vcr/LICENSEdeleted−21

This diff is too big to show here.

vendor/charm.land/x/vcr/doc.godeleted−3

This diff is too big to show here.

vendor/charm.land/x/vcr/hooks.godeleted−33

This diff is too big to show here.

vendor/charm.land/x/vcr/marshaler.godeleted−19

This diff is too big to show here.

vendor/charm.land/x/vcr/matcher.godeleted−70

This diff is too big to show here.

vendor/charm.land/x/vcr/recorder.godeleted−70

This diff is too big to show here.

vendor/cloud.google.com/go/LICENSEdeleted−202

This diff is too big to show here.

vendor/cloud.google.com/go/auth/CHANGES.mddeleted−483

This diff is too big to show here.

vendor/cloud.google.com/go/auth/LICENSEdeleted−202

This diff is too big to show here.

vendor/cloud.google.com/go/auth/README.mddeleted−40

This diff is too big to show here.

vendor/cloud.google.com/go/auth/auth.godeleted−618

This diff is too big to show here.

vendor/cloud.google.com/go/auth/credentials/compute.godeleted−102

This diff is too big to show here.

vendor/cloud.google.com/go/auth/credentials/detect.godeleted−471

This diff is too big to show here.

vendor/cloud.google.com/go/auth/credentials/doc.godeleted−45

This diff is too big to show here.

vendor/cloud.google.com/go/auth/credentials/filetypes.godeleted−329

This diff is too big to show here.

vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/aws_provider.godeleted−531

This diff is too big to show here.

vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/executable_provider.godeleted−284

This diff is too big to show here.

vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/externalaccount.godeleted−431

This diff is too big to show here.

vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/file_provider.godeleted−78

This diff is too big to show here.

vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/info.godeleted−74

This diff is too big to show here.

vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/programmatic_provider.godeleted−30

This diff is too big to show here.

vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/url_provider.godeleted−93

This diff is too big to show here.

vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/x509_provider.godeleted−220

This diff is too big to show here.

vendor/cloud.google.com/go/auth/credentials/internal/externalaccountuser/externalaccountuser.godeleted−115

This diff is too big to show here.

vendor/cloud.google.com/go/auth/credentials/internal/gdch/gdch.godeleted−198

This diff is too big to show here.

vendor/cloud.google.com/go/auth/credentials/internal/impersonate/idtoken.godeleted−105

This diff is too big to show here.

vendor/cloud.google.com/go/auth/credentials/internal/impersonate/impersonate.godeleted−168

This diff is too big to show here.

vendor/cloud.google.com/go/auth/credentials/internal/stsexchange/sts_exchange.godeleted−167

This diff is too big to show here.

vendor/cloud.google.com/go/auth/credentials/selfsignedjwt.godeleted−89

This diff is too big to show here.

vendor/cloud.google.com/go/auth/grpctransport/dial_socketopt.godeleted−62

This diff is too big to show here.

vendor/cloud.google.com/go/auth/grpctransport/directpath.godeleted−177

This diff is too big to show here.

vendor/cloud.google.com/go/auth/grpctransport/grpctransport.godeleted−675

This diff is too big to show here.

vendor/cloud.google.com/go/auth/grpctransport/pool.godeleted−119

This diff is too big to show here.

vendor/cloud.google.com/go/auth/httptransport/httptransport.godeleted−254

This diff is too big to show here.

vendor/cloud.google.com/go/auth/httptransport/transport.godeleted−548

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/compute/compute.godeleted−65

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/compute/manufacturer.godeleted−22

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/compute/manufacturer_linux.godeleted−23

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/compute/manufacturer_windows.godeleted−46

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/credsfile/credsfile.godeleted−63

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/credsfile/filetype.godeleted−159

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/credsfile/parse.godeleted−99

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/internal.godeleted−285

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/jwt/jwt.godeleted−208

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/retry/retry.godeleted−117

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/transport/cba.godeleted−361

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/transport/cert/default_cert.godeleted−65

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/transport/cert/enterprise_cert.godeleted−54

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/transport/cert/secureconnect_cert.godeleted−124

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/transport/cert/workload_cert.godeleted−138

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/transport/headers/headers.godeleted−61

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/transport/s2a.godeleted−138

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/transport/transport.godeleted−137

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/trustboundary/external_accounts_config_providers.godeleted−100

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/trustboundary/trust_boundary.godeleted−392

This diff is too big to show here.

vendor/cloud.google.com/go/auth/internal/version.godeleted−20

This diff is too big to show here.

vendor/cloud.google.com/go/auth/oauth2adapt/CHANGES.mddeleted−82

This diff is too big to show here.

vendor/cloud.google.com/go/auth/oauth2adapt/LICENSEdeleted−202

This diff is too big to show here.

vendor/cloud.google.com/go/auth/oauth2adapt/oauth2adapt.godeleted−200

This diff is too big to show here.

vendor/cloud.google.com/go/auth/threelegged.godeleted−382

This diff is too big to show here.

vendor/cloud.google.com/go/civil/civil.godeleted−500

This diff is too big to show here.

vendor/cloud.google.com/go/compute/metadata/CHANGES.mddeleted−115

This diff is too big to show here.

vendor/cloud.google.com/go/compute/metadata/LICENSEdeleted−202

This diff is too big to show here.

vendor/cloud.google.com/go/compute/metadata/README.mddeleted−27

This diff is too big to show here.

vendor/cloud.google.com/go/compute/metadata/log.godeleted−149

This diff is too big to show here.

vendor/cloud.google.com/go/compute/metadata/metadata.godeleted−937

This diff is too big to show here.

vendor/cloud.google.com/go/compute/metadata/retry.godeleted−117

This diff is too big to show here.

vendor/cloud.google.com/go/compute/metadata/retry_linux.godeleted−31

This diff is too big to show here.

vendor/cloud.google.com/go/compute/metadata/syscheck.godeleted−28

This diff is too big to show here.

vendor/cloud.google.com/go/compute/metadata/syscheck_linux.godeleted−30

This diff is too big to show here.

vendor/cloud.google.com/go/compute/metadata/syscheck_windows.godeleted−39

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/.gitignoredeleted−1

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/ARCHITECTURE.mddeleted−244

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/LICENSEdeleted−61

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/README.mddeleted−86

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/constants.godeleted−65

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/internal/winrt/data/xml/dom/xmldocument.godeleted−73

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/internal/winrt/ui/notifications/toastnotification.godeleted−88

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/internal/winrt/ui/notifications/toastnotificationmanager.godeleted−53

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/internal/winrt/ui/notifications/toastnotificationmanagerforuser.godeleted−68

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/internal/winrt/ui/notifications/toastnotifier.godeleted−64

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/tmpl/powershell.go.tmpldeleted−14

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/tmpl/tmpl.godeleted−28

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/tmpl/xml.go.tmpldeleted−34

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/toast.godeleted−225

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/wintoast/bind.godeleted−86

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/wintoast/bind_noop.godeleted−19

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/wintoast/bind_windows.godeleted−174

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/wintoast/impl.godeleted−179

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/wintoast/procs.godeleted−37

This diff is too big to show here.

vendor/git.sr.ht/~jackmordaunt/go-toast/wintoast/registry.godeleted−110

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/CHANGELOG.mddeleted−849

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/LICENSE.txtdeleted−21

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/README.mddeleted−39

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/ci.ymldeleted−29

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud/cloud.godeleted−44

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud/doc.godeleted−53

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/core.godeleted−173

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/doc.godeleted−264

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/errors.godeleted−17

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/etag.godeleted−57

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported/exported.godeleted−175

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported/pipeline.godeleted−77

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported/request.godeleted−260

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported/response_error.godeleted−201

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/log/log.godeleted−50

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/async/async.godeleted−159

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/body/body.godeleted−135

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/fake/fake.godeleted−133

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/loc/loc.godeleted−123

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/op/op.godeleted−148

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/poller.godeleted−24

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/util.godeleted−212

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared/constants.godeleted−44

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared/shared.godeleted−149

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/log/doc.godeleted−10

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/log/log.godeleted−55

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/policy/doc.godeleted−10

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/policy/policy.godeleted−198

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/doc.godeleted−10

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/errors.godeleted−27

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/pager.godeleted−138

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/pipeline.godeleted−94

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_api_version.godeleted−75

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_bearer_token.godeleted−236

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_body_download.godeleted−72

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_http_header.godeleted−40

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_http_trace.godeleted−154

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_include_response.godeleted−35

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_key_credential.godeleted−64

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_logging.godeleted−264

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_request_id.godeleted−34

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_retry.godeleted−276

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_sas_credential.godeleted−55

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_telemetry.godeleted−83

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/poller.godeleted−396

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/request.godeleted−281

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/response.godeleted−109

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/transport_default_dialer_other.godeleted−15

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/transport_default_dialer_wasm.godeleted−15

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/transport_default_http_client.godeleted−48

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming/doc.godeleted−9

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming/progress.godeleted−89

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing/constants.godeleted−41

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing/tracing.godeleted−191

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/LICENSE.txtdeleted−21

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/diag/diag.godeleted−51

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/diag/doc.godeleted−7

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/errorinfo/doc.godeleted−7

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/errorinfo/errorinfo.godeleted−46

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/exported/exported.godeleted−129

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/log/doc.godeleted−7

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/log/log.godeleted−104

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/poller/util.godeleted−155

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/temporal/resource.godeleted−123

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/uuid/doc.godeleted−7

This diff is too big to show here.

vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/uuid/uuid.godeleted−76

This diff is too big to show here.

vendor/github.com/JohannesKaufmann/html-to-markdown/.gitignoredeleted−14

This diff is too big to show here.

vendor/github.com/JohannesKaufmann/html-to-markdown/CONTRIBUTING.mddeleted

No changes to the content.

vendor/github.com/JohannesKaufmann/html-to-markdown/LICENSEdeleted−21

This diff is too big to show here.

vendor/github.com/JohannesKaufmann/html-to-markdown/README.mddeleted−242

This diff is too big to show here.

vendor/github.com/JohannesKaufmann/html-to-markdown/SECURITY.mddeleted−6

This diff is too big to show here.

vendor/github.com/JohannesKaufmann/html-to-markdown/commonmark.godeleted−393

This diff is too big to show here.

vendor/github.com/JohannesKaufmann/html-to-markdown/escape/escape.godeleted−65

This diff is too big to show here.

vendor/github.com/JohannesKaufmann/html-to-markdown/from.godeleted−464

This diff is too big to show here.

vendor/github.com/JohannesKaufmann/html-to-markdown/logo_five_years.pngdeleted

Binary file not shown.

vendor/github.com/JohannesKaufmann/html-to-markdown/markdown.godeleted−212

This diff is too big to show here.

vendor/github.com/JohannesKaufmann/html-to-markdown/utils.godeleted−533

This diff is too big to show here.

vendor/github.com/KyleBanks/depth/.gitignoredeleted−16

This diff is too big to show here.

vendor/github.com/KyleBanks/depth/.travis.ymldeleted−9

This diff is too big to show here.

vendor/github.com/KyleBanks/depth/LICENSEdeleted−21

This diff is too big to show here.

vendor/github.com/KyleBanks/depth/Makefiledeleted−32

This diff is too big to show here.

vendor/github.com/KyleBanks/depth/README.mddeleted−232

This diff is too big to show here.

vendor/github.com/KyleBanks/depth/depth.godeleted−129

This diff is too big to show here.

vendor/github.com/KyleBanks/depth/pkg.godeleted−184

This diff is too big to show here.

vendor/github.com/MakeNowJust/heredoc/LICENSEdeleted−21

This diff is too big to show here.

vendor/github.com/MakeNowJust/heredoc/README.mddeleted−52

This diff is too big to show here.

vendor/github.com/MakeNowJust/heredoc/heredoc.godeleted−105

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/.gitattributesdeleted−1

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/.gitignoredeleted−10

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/.golangci.ymldeleted−147

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/CODEOWNERSdeleted−1

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/LICENSEdeleted−22

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/README.mddeleted−89

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/SECURITY.mddeleted−41

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/backup.godeleted−287

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/doc.godeleted−22

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/ea.godeleted−137

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/file.godeleted−320

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/fileinfo.godeleted−106

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/hvsock.godeleted−582

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/internal/fs/doc.godeleted−2

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/internal/fs/fs.godeleted−262

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/internal/fs/security.godeleted−12

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/internal/fs/zsyscall_windows.godeleted−61

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/internal/socket/rawaddr.godeleted−20

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/internal/socket/socket.godeleted−177

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/internal/socket/zsyscall_windows.godeleted−69

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/internal/stringbuffer/wstring.godeleted−132

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/pipe.godeleted−586

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/pkg/guid/guid.godeleted−232

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/pkg/guid/guid_nonwindows.godeleted−16

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/pkg/guid/guid_windows.godeleted−13

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/pkg/guid/variant_string.godeleted−27

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/privilege.godeleted−196

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/reparse.godeleted−131

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/sd.godeleted−133

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/syscall.godeleted−5

This diff is too big to show here.

vendor/github.com/Microsoft/go-winio/zsyscall_windows.godeleted−378

This diff is too big to show here.

vendor/github.com/PuerkitoBio/goquery/.gitattributesdeleted−1

This diff is too big to show here.

vendor/github.com/PuerkitoBio/goquery/.gitignoredeleted−16

This diff is too big to show here.

vendor/github.com/PuerkitoBio/goquery/LICENSEdeleted−12

This diff is too big to show here.

vendor/github.com/PuerkitoBio/goquery/README.mddeleted−218

This diff is too big to show here.

vendor/github.com/PuerkitoBio/goquery/array.godeleted−124

This diff is too big to show here.

vendor/github.com/PuerkitoBio/goquery/doc.godeleted−123

This diff is too big to show here.

vendor/github.com/PuerkitoBio/goquery/expand.godeleted−70

This diff is too big to show here.

vendor/github.com/PuerkitoBio/goquery/filter.godeleted−163

This diff is too big to show here.

vendor/github.com/PuerkitoBio/goquery/iteration.godeleted−61

This diff is too big to show here.

vendor/github.com/PuerkitoBio/goquery/manipulation.godeleted−679

This diff is too big to show here.

vendor/github.com/PuerkitoBio/goquery/property.godeleted−275

This diff is too big to show here.

vendor/github.com/PuerkitoBio/goquery/query.godeleted−49

This diff is too big to show here.

vendor/github.com/PuerkitoBio/goquery/traversal.godeleted−704

This diff is too big to show here.

vendor/github.com/PuerkitoBio/goquery/type.godeleted−203

This diff is too big to show here.

vendor/github.com/PuerkitoBio/goquery/utilities.godeleted−177

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/.editorconfigdeleted−17

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/.gitignoredeleted−28

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/.golangci.ymldeleted−122

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/.goreleaser.ymldeleted−34

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/AGENTS.mddeleted−11

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/BUILD.bitdeleted−93

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/COPYINGdeleted−118

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/Dockerfiledeleted−64

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/Justfiledeleted−59

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/README.mddeleted−309

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/biome.jsondeleted−6

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/chroma.jpgdeleted

Binary file not shown.

vendor/github.com/alecthomas/chroma/v2/coalesce.godeleted−35

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/colour.godeleted−192

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/delegate.godeleted−161

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/doc.godeleted−7

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/emitters.godeleted−233

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/formatter.godeleted−43

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/formatters/api.godeleted−57

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/formatters/html/html.godeleted−692

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/formatters/json.godeleted−39

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/formatters/svg/font_liberation_mono.godeleted−51

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/formatters/svg/svg.godeleted−222

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/formatters/tokens.godeleted−18

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/formatters/tty_indexed.godeleted−284

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/formatters/tty_truecolour.godeleted−76

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/iterator.godeleted−93

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexer.godeleted−179

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/README.mddeleted−46

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/caddyfile.godeleted−275

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/cl.godeleted−243

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/dns.godeleted−17

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/emacs.godeleted−533

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/abap.xmldeleted−154

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/abnf.xmldeleted−66

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/actionscript.xmldeleted−68

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/actionscript_3.xmldeleted−163

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ada.xmldeleted−321

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/agda.xmldeleted−66

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/al.xmldeleted−75

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/alloy.xmldeleted−58

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ampl.xmldeleted−98

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/angular2.xmldeleted−109

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/antlr.xmldeleted−317

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/apacheconf.xmldeleted−74

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/apl.xmldeleted−59

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/applescript.xmldeleted−151

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/arangodb_aql.xmldeleted−174

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/arduino.xmldeleted−322

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/armasm.xmldeleted−126

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/arturo.xmldeleted−119

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/atl.xmldeleted−165

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/autohotkey.xmldeleted−78

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/autoit.xmldeleted−70

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/awk.xmldeleted−95

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ballerina.xmldeleted−97

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/bash.xmldeleted−222

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/bash_session.xmldeleted−25

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/batchfile.xmldeleted−660

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/beef.xmldeleted−120

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/bibtex.xmldeleted−152

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/bicep.xmldeleted−84

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/blitzbasic.xmldeleted−141

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/bnf.xmldeleted−28

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/bqn.xmldeleted−83

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/brainfuck.xmldeleted−51

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/c#.xmldeleted−121

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/c++.xmldeleted−331

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/c.xmldeleted−260

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/c3.xmldeleted−374

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/cap_n_proto.xmldeleted−122

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/cassandra_cql.xmldeleted−137

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ceylon.xmldeleted−151

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/cfengine3.xmldeleted−197

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/cfstatement.xmldeleted−92

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/chaiscript.xmldeleted−134

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/chapel.xmldeleted−143

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/cheetah.xmldeleted−55

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/clojure.xmldeleted−71

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/cmake.xmldeleted−90

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/cobol.xmldeleted−90

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/coffeescript.xmldeleted−210

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/common_lisp.xmldeleted−184

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/coq.xmldeleted−136

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/core.xmldeleted−79

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/crystal.xmldeleted−762

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/css.xmldeleted−323

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/csv.xmldeleted−53

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/cue.xmldeleted−85

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/cython.xmldeleted−372

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/d.xmldeleted−133

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/dart.xmldeleted−213

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/dax.xmldeleted−39

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/desktop_entry.xmldeleted−17

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/devicetree.xmldeleted−251

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/diff.xmldeleted−52

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/django_jinja.xmldeleted−153

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/dns.xmldeleted−44

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/docker.xmldeleted−68

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/dtd.xmldeleted−168

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/dylan.xmldeleted−176

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ebnf.xmldeleted−90

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/elixir.xmldeleted−744

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/elm.xmldeleted−119

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/emacslisp.xmldeleted−132

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/erb.xmldeleted−37

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/erlang.xmldeleted−166

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/factor.xmldeleted−412

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/fennel.xmldeleted−68

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/fish.xmldeleted−159

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/forth.xmldeleted−78

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/fortran.xmldeleted−102

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/fortranfixed.xmldeleted−71

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/fsharp.xmldeleted−245

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/gas.xmldeleted−150

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/gdscript.xmldeleted−259

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/gdscript3.xmldeleted−270

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/gemfile_lock.xmldeleted−81

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/gettext.xmldeleted−24

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/gherkin.xmldeleted−263

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/gleam.xmldeleted−100

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/glsl.xmldeleted−65

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/gnuplot.xmldeleted−289

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/go_template.xmldeleted−114

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/graphql.xmldeleted−92

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/groff.xmldeleted−90

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/groovy.xmldeleted−135

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/handlebars.xmldeleted−147

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/hare.xmldeleted−98

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/haskell.xmldeleted−275

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/hcl.xmldeleted−143

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/hexdump.xmldeleted−189

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/hlb.xmldeleted−131

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/hlsl.xmldeleted−110

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/holyc.xmldeleted−252

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/html.xmldeleted−159

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/hy.xmldeleted−104

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/idris.xmldeleted−216

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/igor.xmldeleted−47

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ini.xmldeleted−52

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/io.xmldeleted−71

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/iscdhcpd.xmldeleted−96

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/j.xmldeleted−157

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/janet.xmldeleted−48

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/java.xmldeleted−206

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/javascript.xmldeleted−160

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/json.xmldeleted−117

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/jsonata.xmldeleted−83

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/jsonnet.xmldeleted−138

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/julia.xmldeleted−400

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/jungle.xmldeleted−98

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/kakoune.xmldeleted−96

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/kdl.xmldeleted−75

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/kotlin.xmldeleted−234

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/lateralus.xmldeleted−184

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/lean.xmldeleted−56

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/lighttpd_configuration_file.xmldeleted−42

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/lilypond.xmldeleted−133

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/llvm.xmldeleted−73

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/lox.xmldeleted−78

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/lua.xmldeleted−158

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/luau.xmldeleted−173

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/makefile.xmldeleted−131

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/mako.xmldeleted−120

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/mason.xmldeleted−89

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/materialize_sql_dialect.xmldeleted−155

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/mathematica.xmldeleted−60

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/matlab.xmldeleted−114

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/mcfunction.xmldeleted−138

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/meson.xmldeleted−86

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/metal.xmldeleted−270

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/microcad.xmldeleted−139

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/minizinc.xmldeleted−82

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/mlir.xmldeleted−73

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/modelica.xmldeleted−106

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/modula-2.xmldeleted−245

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/mojo.xmldeleted−228

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/monkeyc.xmldeleted−153

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/moonbit.xmldeleted−75

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/moonscript.xmldeleted−83

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/morrowindscript.xmldeleted−90

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/myghty.xmldeleted−77

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/mysql.xmldeleted−121

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/nasm.xmldeleted−126

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/natural.xmldeleted−143

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ndisasm.xmldeleted−123

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/newspeak.xmldeleted−121

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/nginx_configuration_file.xmldeleted−101

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/nim.xmldeleted−211

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/nix.xmldeleted−258

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/nsis.xmldeleted−59

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/nu.xmldeleted−126

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/objective-c.xmldeleted−510

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/objectpascal.xmldeleted−142

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ocaml.xmldeleted−153

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/octave.xmldeleted−101

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/odin.xmldeleted−127

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/onesenterprise.xmldeleted−92

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/openedge_abl.xmldeleted−101

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/openscad.xmldeleted−96

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/org_mode.xmldeleted−329

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/pacmanconf.xmldeleted−37

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/perl.xmldeleted−400

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/php.xmldeleted−258

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/pig.xmldeleted−105

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/pkgconfig.xmldeleted−73

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/pl_pgsql.xmldeleted−119

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/plaintext.xmldeleted−21

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/plutus_core.xmldeleted−105

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/pony.xmldeleted−135

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/postgresql_sql_dialect.xmldeleted−155

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/postscript.xmldeleted−93

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/povray.xmldeleted−58

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/powerquery.xmldeleted−51

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/powershell.xmldeleted−230

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/prolog.xmldeleted−115

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/promela.xmldeleted−119

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/promql.xmldeleted−123

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/properties.xmldeleted−45

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/protocol_buffer.xmldeleted−124

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/prql.xmldeleted−161

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/psl.xmldeleted−213

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/puppet.xmldeleted−100

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/python.xmldeleted−595

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/python_2.xmldeleted−356

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/qbasic.xmldeleted−173

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/qml.xmldeleted−113

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/r.xmldeleted−128

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/racket.xmldeleted−260

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ragel.xmldeleted−149

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/react.xmldeleted−236

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/reasonml.xmldeleted−147

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/reg.xmldeleted−68

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/rego.xmldeleted−94

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/rexx.xmldeleted−127

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/rgbasm.xmldeleted−239

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ring.xmldeleted−74

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/rpgle.xmldeleted−176

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/rpm_spec.xmldeleted−58

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ruby.xmldeleted−728

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/rust.xmldeleted−375

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/sas.xmldeleted−191

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/sass.xmldeleted−362

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/scala.xmldeleted−274

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/scdoc.xmldeleted−115

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/scheme.xmldeleted−106

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/scilab.xmldeleted−98

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/scss.xmldeleted−373

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/sed.xmldeleted−92

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/sieve.xmldeleted−61

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/smali.xmldeleted−73

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/smalltalk.xmldeleted−294

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/smarty.xmldeleted−79

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/snbt.xmldeleted−58

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/snobol.xmldeleted−95

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/solidity.xmldeleted−291

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/sourcepawn.xmldeleted−59

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/spade.xmldeleted−292

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/sparql.xmldeleted−160

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/sql.xmldeleted−90

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/squidconf.xmldeleted−63

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/standard_ml.xmldeleted−548

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/stas.xmldeleted−85

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/stylus.xmldeleted−132

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/swift.xmldeleted−207

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/systemd.xmldeleted−63

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/systemverilog.xmldeleted−181

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/tablegen.xmldeleted−69

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/tal.xmldeleted−43

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/tasm.xmldeleted−135

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/tcl.xmldeleted−272

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/tcsh.xml−121

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/templ.xml−315

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/termcap.xml−75

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/terminfo.xml−84

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/terraform.xml−149

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/tex.xml−113

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/thrift.xml−154

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/toml.xml−45

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/tradingview.xml−81

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/transact-sql.xml−137

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/turing.xml−82

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/turtle.xml−170

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/twig.xml−155

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/txtpb.xml−162

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/typescript.xml−302

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/typoscript.xml−178

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/typoscriptcssdata.xml−52

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/typoscripthtmldata.xml−52

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/typst.xml−108

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ucode.xml−147

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/v.xml−355

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/v_shell.xml−365

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/vala.xml−72

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/vb_net.xml−162

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/verilog.xml−158

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/vhdl.xml−171

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/vhs.xml−48

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/viml.xml−85

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/vue.xml−295

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/wat.xml−149

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/wdte.xml−43

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/webgpu_shading_language.xml−142

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/webvtt.xml−283

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/whiley.xml−57

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/xml.xml−96

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/xorg.xml−35

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/yaml.xml−132

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/yang.xml−99

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/z80_assembly.xml−74

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/zed.xml−51

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/embedded/zig.xml−187

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/erb.go−29

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/gemtext.go−37

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/genshi.go−118

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/go.go−81

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/haxe.go−647

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/html.go−8

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/http.go−131

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/lexers.go−85

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/markdown.go−107

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/markless.go−168

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/mysql.go−33

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/php.go−37

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/raku.go−1710

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/rst.go−89

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/svelte.go−70

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/typoscript.go−85

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/yaml_jinja.go−20

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/lexers/zed.go−24

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/mutators.go−201

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/pygments-lexers.txt−605

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/quick/quick.go−44

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/regexp.go−533

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/registry.go−228

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/remap.go−94

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/renovate.json5−25

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/serialise.go−483

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/style.go−530

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/abap.xml−11

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/algol.xml−18

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/algol_nu.xml−18

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/api.go−95

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/arduino.xml−18

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/ashen.xml−69

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/aura-theme-dark-soft.xml−107

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/aura-theme-dark.xml−107

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/autumn.xml−36

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/average.xml−74

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/base16-snazzy.xml−74

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/borland.xml−26

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/bw.xml−23

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/catppuccin-frappe.xml−83

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/catppuccin-latte.xml−83

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/catppuccin-macchiato.xml−83

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/catppuccin-mocha.xml−83

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/colorful.xml−52

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/compat.go−69

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/darcula.xml−83

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/doom-one.xml−51

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/doom-one2.xml−64

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/dracula.xml−74

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/emacs.xml−44

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/evergarden.xml−33

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/friendly.xml−44

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/fruity.xml−19

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/github-dark.xml−46

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/github.xml−39

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/gruvbox-light.xml−34

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/gruvbox.xml−34

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/hr_high_contrast.xml−12

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/hrdark.xml−10

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/igor.xml−9

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/kanagawa-dragon.xml−83

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/kanagawa-lotus.xml−83

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/kanagawa-wave.xml−83

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/lovelace.xml−53

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/manni.xml−44

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/modus-operandi.xml−13

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/modus-vivendi.xml−13

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/monokai.xml−29

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/monokailight.xml−26

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/murphy.xml−52

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/native.xml−35

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/nord.xml−46

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/nordic.xml−46

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/onedark.xml−25

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/onesenterprise.xml−10

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/paraiso-dark.xml−37

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/paraiso-light.xml−37

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/pastie.xml−45

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/perldoc.xml−37

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/pygments.xml−42

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/rainbow_dash.xml−40

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/rose-pine-dawn.xml−29

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/rose-pine-moon.xml−29

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/rose-pine.xml−29

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/rpgle.xml−30

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/rrt.xml−19

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/solarized-dark.xml−39

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/solarized-dark256.xml−41

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/solarized-light.xml−17

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/swapoff.xml−18

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/tango.xml−72

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/tokyonight-day.xml−83

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/tokyonight-moon.xml−83

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/tokyonight-night.xml−83

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/tokyonight-storm.xml−83

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/trac.xml−35

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/vim.xml−29

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/vs.xml−16

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/vulcan.xml−74

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/witchhazel.xml−31

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/xcode-dark.xml−31

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/styles/xcode.xml−22

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/table.py−31

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/test.jsonl−2

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/tokentype_enumer.go−588

This diff is too big to show here.

vendor/github.com/alecthomas/chroma/v2/types.go−357

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/.gitignore−2

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/LICENSE−19

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/README.md−19

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/backward_references.go−185

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/backward_references_hq.go−796

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/bit_cost.go−436

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/bit_reader.go−266

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/bitwriter.go−56

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/block_splitter.go−144

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/block_splitter_command.go−434

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/block_splitter_distance.go−433

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/block_splitter_literal.go−433

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/brotli_bit_stream.go−1435

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/cluster.go−30

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/cluster_command.go−164

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/cluster_distance.go−326

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/cluster_literal.go−326

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/command.go−254

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/compress_fragment.go−834

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/compress_fragment_two_pass.go−775

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/constants.go−77

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/context.go−2176

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/decode.go−2581

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/dictionary.go−122890

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/dictionary_hash.go−32779

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/encode.go−1220

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/encoder.go−177

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/encoder_dict.go−22

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/encoder_fast.go−197

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/entropy_encode.go−592

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/entropy_encode_static.go−4399

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/fast_log.go−290

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/find_match_length.go−45

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/flate/LICENSE−27

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/flate/README.md−2

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/flate/gzip.go−65

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/flate/huffman_bit_writer.go−517

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/flate/huffman_code.go−346

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/flate/token.go−80

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/flate/writer.go−66

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/h10.go−287

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/h5.go−214

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/h6.go−216

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/hash.go−342

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/hash_composite.go−93

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/hash_forgetful_chain.go−252

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/hash_longest_match_quickly.go−214

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/hash_rolling.go−168

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/histogram.go−226

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/http.go−189

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/huffman.go−653

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/literal_cost.go−182

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/matchfinder/bargain1.go−212

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/matchfinder/bargain2.go−248

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/matchfinder/bargain3.go−278

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/matchfinder/emitter.go−34

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/matchfinder/m0.go−175

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/matchfinder/m4.go−339

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/matchfinder/matchfinder.go−103

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/matchfinder/pathfinder.go−328

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/matchfinder/textencoder.go−53

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/matchfinder/trio.go−326

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/matchfinder/zdfast.go−258

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/matchfinder/zfast.go−206

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/matchfinder/zm.go−301

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/memory.go−66

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/metablock.go−574

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/metablock_command.go−165

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/metablock_distance.go−165

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/metablock_literal.go−165

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/params.go−37

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/platform.go−103

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/prefix.go−30

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/prefix_dec.go−723

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/quality.go−196

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/reader.go−111

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/ringbuffer.go−134

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/state.go−294

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/static_dict.go−662

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/static_dict_lut.go−75094

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/symbol_list.go−22

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/transform.go−641

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/utf8_util.go−70

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/util.go−7

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/write_bits.go−52

This diff is too big to show here.

vendor/github.com/andybalholm/brotli/writer.go−162

This diff is too big to show here.

vendor/github.com/andybalholm/cascadia/.travis.yml−14

This diff is too big to show here.

vendor/github.com/andybalholm/cascadia/LICENSE−24

This diff is too big to show here.

vendor/github.com/andybalholm/cascadia/README.md−144

This diff is too big to show here.

vendor/github.com/andybalholm/cascadia/parser.go−889

This diff is too big to show here.

vendor/github.com/andybalholm/cascadia/pseudo_classes.go−458

This diff is too big to show here.

vendor/github.com/andybalholm/cascadia/selector.go−586

This diff is too big to show here.

vendor/github.com/andybalholm/cascadia/serialize.go−176

This diff is too big to show here.

vendor/github.com/andybalholm/cascadia/specificity.go−26

This diff is too big to show here.

vendor/github.com/atotto/clipboard/.travis.yml−22

This diff is too big to show here.

vendor/github.com/atotto/clipboard/LICENSE−27

This diff is too big to show here.

vendor/github.com/atotto/clipboard/README.md−48

This diff is too big to show here.

vendor/github.com/atotto/clipboard/clipboard.go−20

This diff is too big to show here.

vendor/github.com/atotto/clipboard/clipboard_darwin.go−52

This diff is too big to show here.

vendor/github.com/atotto/clipboard/clipboard_plan9.go−42

This diff is too big to show here.

vendor/github.com/atotto/clipboard/clipboard_unix.go−149

This diff is too big to show here.

vendor/github.com/atotto/clipboard/clipboard_windows.go−157

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/LICENSE.txt−202

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/NOTICE.txt−3

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/accountid_endpoint_mode.go−18

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/checksum.go−33

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/config.go−254

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/context.go−22

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/credential_cache.go−235

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/credentials.go−234

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/defaults/auto.go−38

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/defaults/configuration.go−43

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/defaults/defaults.go−50

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/defaults/doc.go−2

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/defaultsmode.go−95

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/doc.go−62

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/endpoints.go−247

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/errors.go−9

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/from_ptr.go−365

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/go_module_metadata.go−6

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/logging.go−119

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/logging_generate.go−95

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/middleware/metadata.go−213

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/middleware/middleware.go−168

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/middleware/osname.go−24

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/middleware/osname_go115.go−24

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/middleware/recursion_detection.go−94

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/middleware/request_id.go−27

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/middleware/request_id_retriever.go−57

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/middleware/user_agent.go−398

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/CHANGELOG.md−203

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/LICENSE.txt−202

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/debug.go−144

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/decode.go−218

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/encode.go−167

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/error.go−23

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/eventstreamapi/headers.go−24

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/eventstreamapi/middleware.go−71

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/eventstreamapi/transport.go−13

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/eventstreamapi/transport_go117.go−12

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/go_module_metadata.go−6

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/header.go−175

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/header_value.go−521

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/message.go−99

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/query/array.go−61

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/query/encoder.go−80

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/query/map.go−78

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/query/middleware.go−62

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/query/object.go−68

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/query/value.go−117

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/restjson/decoder_util.go−85

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/xml/error_utils.go−48

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/ratelimit/none.go−20

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/ratelimit/token_bucket.go−96

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/ratelimit/token_rate_limit.go−83

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/request.go−25

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/restrict_file_permissions.go−21

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/retry/adaptive.go−156

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/retry/adaptive_ratelimit.go−158

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/retry/adaptive_token_bucket.go−83

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/retry/attempt_metrics.go−51

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/retry/doc.go−80

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/retry/errors.go−20

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/retry/jitter_backoff.go−124

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/retry/metadata.go−52

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/retry/middleware.go−487

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/retry/retry.go−103

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/retry/retryable_error.go−228

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/retry/standard.go−343

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/retry/throttle_error.go−60

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/retry/timeout_error.go−52

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/retryer.go−127

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/runtime.go−14

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4/cache.go−115

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4/const.go−40

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4/header_rules.go−82

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4/headers.go−70

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4/hmac.go−13

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4/host.go−75

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4/scope.go−13

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4/time.go−36

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4/util.go−80

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/signer/v4/middleware.go−420

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/signer/v4/presign_middleware.go−127

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/signer/v4/stream.go−86

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/signer/v4/v4.go−564

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/to_ptr.go−297

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/transport/http/client.go−355

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/transport/http/content_type.go−42

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/transport/http/response_error.go−33

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/transport/http/response_error_middleware.go−56

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/transport/http/timeout_read_closer.go−109

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/types.go−42

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/aws/version.go−8

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/CHANGELOG.md−1097

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/LICENSE.txt−202

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/auth_scheme_preference.go−19

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/config.go−237

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/defaultsmode.go−47

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/doc.go−20

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/env_config.go−966

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/generate.go−4

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/go_module_metadata.go−6

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/internal/ini/errors.go−22

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/internal/ini/ini.go−56

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/internal/ini/parse.go−109

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/internal/ini/sections.go−157

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/internal/ini/strings.go−89

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/internal/ini/token.go−32

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/internal/ini/tokenize.go−92

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/internal/ini/value.go−93

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/load_options.go−1371

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/local.go−51

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/provider.go−802

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/resolve.go−458

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/resolve_bearer_token.go−122

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/resolve_credentials.go−650

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/config/shared_config.go−1704

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/CHANGELOG.md−990

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/LICENSE.txt−202

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/doc.go−4

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds/doc.go−58

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds/provider.go−241

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/internal/client/auth.go−48

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/internal/client/client.go−165

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/internal/client/endpoints.go−20

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/internal/client/middleware.go−164

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/provider.go−207

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/go_module_metadata.go−6

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/logincreds/dpop.go−150

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/logincreds/file.go−14

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/logincreds/provider.go−184

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/logincreds/token.go−110

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/processcreds/doc.go−92

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/processcreds/provider.go−296

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/ssocreds/doc.go−81

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/ssocreds/sso_cached_token.go−232

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/ssocreds/sso_credentials_provider.go−165

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/ssocreds/sso_token_provider.go−147

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/static_provider.go−63

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/stscreds/assume_role_provider.go−338

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/credentials/stscreds/web_identity_provider.go−181

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/CHANGELOG.md−584

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/LICENSE.txt−202

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/api_client.go−358

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/api_op_GetDynamicData.go−77

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/api_op_GetIAMInfo.go−103

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/api_op_GetInstanceIdentityDocument.go−110

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/api_op_GetMetadata.go−77

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/api_op_GetRegion.go−73

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/api_op_GetToken.go−119

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/api_op_GetUserData.go−61

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/auth.go−48

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/doc.go−12

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/endpoints.go−20

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/go_module_metadata.go−6

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/internal/config/resolvers.go−114

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/request_middleware.go−313

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/token_provider.go−261

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/auth/auth.go−45

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/auth/scheme.go−191

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/auth/smithy/bearer_token_adapter.go−43

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/auth/smithy/bearer_token_signer_adapter.go−35

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/auth/smithy/credentials_adapter.go−46

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/auth/smithy/smithy.go−2

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/auth/smithy/v4signer_adapter.go−57

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/auth/smithy/v4signer_adapter_eventstream.go−51

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/CHANGELOG.md−544

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/LICENSE.txt−202

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/config.go−65

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/endpoints.go−57

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/go_module_metadata.go−6

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/context/context.go−65

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn/arn.go−94

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn/doc.go−3

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn/generate.go−7

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn/host.go−51

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn/partition.go−76

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn/partitions.go−496

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn/partitions.json−267

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/endpoints.go−201

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/CHANGELOG.md−519

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/LICENSE.txt−202

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/endpoints.go−302

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/go_module_metadata.go−6

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/rand/rand.go−33

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/sdk/interfaces.go−9

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/sdk/time.go−74

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/sdkio/byte.go−12

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/shareddefaults/shared_config.go−47

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/strings/strings.go−11

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight/LICENSE−28

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight/docs.go−7

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight/singleflight.go−210

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/timeconv/duration.go−13

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/CHANGELOG.md−486

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/LICENSE.txt−202

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/credentials.go−141

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/error.go−17

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/go_module_metadata.go−6

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/internal/crypto/compare.go−30

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/internal/crypto/ecc.go−113

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/internal/v4/const.go−36

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/internal/v4/header_rules.go−82

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/internal/v4/headers.go−68

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/internal/v4/hmac.go−13

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/internal/v4/host.go−75

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/internal/v4/time.go−36

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/internal/v4/util.go−64

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/middleware.go−118

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/presign_middleware.go−117

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/smithy.go−92

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/v4a.go−520

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/CHANGELOG.md−220

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/LICENSE.txt−202

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/accept_encoding_gzip.go−176

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/doc.go−22

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/go_module_metadata.go−6

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/CHANGELOG.md−571

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/LICENSE.txt−202

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/context.go−56

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/doc.go−3

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/go_module_metadata.go−6

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/middleware.go−110

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/CHANGELOG.md−85

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/LICENSE.txt−202

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/api_client.go−945

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/api_op_CreateOAuth2Token.go−212

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/api_op_DeleteConsoleAuthorizationConfiguration.go−168

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/api_op_DeleteResourcePermissionStatement.go−196

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/api_op_GetConsoleAuthorizationConfiguration.go−168

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/api_op_GetResourcePolicy.go−155

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/api_op_ListResourcePermissionStatements.go−261

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/api_op_PutConsoleAuthorizationConfiguration.go−168

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/api_op_PutResourcePermissionStatement.go−216

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/auth.go−361

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/deserializers.go−2474

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/doc.go−9

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/endpoints.go−854

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/generated.json−40

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/go_module_metadata.go−6

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/internal/endpoints/endpoints.go−333

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/options.go−242

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/serializers.go−724

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/types/enums.go−46

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/types/errors.go−247

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/types/types.go−162

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/signin/validators.go−111

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/CHANGELOG.md−792

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/LICENSE.txt−202

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/api_client.go−929

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/api_op_GetRoleCredentials.go−165

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/api_op_ListAccountRoles.go−263

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/api_op_ListAccounts.go−261

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/api_op_Logout.go−164

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/auth.go−379

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/deserializers.go−1171

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/doc.go−27

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/endpoints.go−570

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/generated.json−35

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/go_module_metadata.go−6

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/internal/endpoints/endpoints.go−628

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/options.go−238

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/serializers.go−309

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/types/errors.go−115

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/types/types.go−63

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sso/validators.go−175

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/CHANGELOG.md−790

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/LICENSE.txt−202

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/api_client.go−929

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/api_op_CreateToken.go−235

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/api_op_CreateTokenWithIAM.go−282

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/api_op_RegisterClient.go−206

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/api_op_StartDeviceAuthorization.go−188

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/auth.go−373

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/deserializers.go−2244

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/doc.go−49

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/endpoints.go−570

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/generated.json−36

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/go_module_metadata.go−6

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/internal/endpoints/endpoints.go−628

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/options.go−238

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/serializers.go−512

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/types/enums.go−44

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/types/errors.go−430

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/types/types.go−25

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/validators.go−184

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/CHANGELOG.md−832

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/LICENSE.txt−202

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_client.go−1115

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_AssumeRole.go−544

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_AssumeRoleWithSAML.go−455

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_AssumeRoleWithWebIdentity.go−472

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_AssumeRoot.go−229

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_DecodeAuthorizationMessage.go−189

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_GetAccessKeyInfo.go−180

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_GetCallerIdentity.go−192

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_GetDelegatedAccessToken.go−169

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_GetFederationToken.go−393

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_GetSessionToken.go−239

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_GetWebIdentityToken.go−192

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/auth.go−377

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/deserializers.go−3456

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/doc.go−13

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/endpoints.go−683

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/generated.json−45

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/go_module_metadata.go−6

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/internal/endpoints/endpoints.go−585

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/options.go−286

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/serializers.go−1198

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/types/errors.go−363

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/types/types.go−144

This diff is too big to show here.

vendor/github.com/aws/aws-sdk-go-v2/service/sts/validators.go−433

This diff is too big to show here.

vendor/github.com/aws/smithy-go/.gitignore−29

This diff is too big to show here.

vendor/github.com/aws/smithy-go/.travis.yml−28

This diff is too big to show here.

vendor/github.com/aws/smithy-go/AGENTS.md−172

This diff is too big to show here.

vendor/github.com/aws/smithy-go/CHANGELOG.md−454

This diff is too big to show here.

vendor/github.com/aws/smithy-go/CODE_OF_CONDUCT.md−4

This diff is too big to show here.

vendor/github.com/aws/smithy-go/CONTRIBUTING.md−90

This diff is too big to show here.

vendor/github.com/aws/smithy-go/LICENSE−175

This diff is too big to show here.

vendor/github.com/aws/smithy-go/Makefile−130

This diff is too big to show here.

vendor/github.com/aws/smithy-go/NOTICE−1

This diff is too big to show here.

vendor/github.com/aws/smithy-go/README.md−107

This diff is too big to show here.

vendor/github.com/aws/smithy-go/auth/auth.go−3

This diff is too big to show here.

vendor/github.com/aws/smithy-go/auth/bearer/docs.go−3

This diff is too big to show here.

vendor/github.com/aws/smithy-go/auth/bearer/middleware.go−104

This diff is too big to show here.

vendor/github.com/aws/smithy-go/auth/bearer/token.go−50

This diff is too big to show here.

vendor/github.com/aws/smithy-go/auth/bearer/token_cache.go−208

This diff is too big to show here.

vendor/github.com/aws/smithy-go/auth/identity.go−47

This diff is too big to show here.

vendor/github.com/aws/smithy-go/auth/option.go−25

This diff is too big to show here.

vendor/github.com/aws/smithy-go/auth/scheme_id.go−20

This diff is too big to show here.

vendor/github.com/aws/smithy-go/changelog-template.json−9

This diff is too big to show here.

vendor/github.com/aws/smithy-go/context/suppress_expired.go−81

This diff is too big to show here.

vendor/github.com/aws/smithy-go/doc.go−2

This diff is too big to show here.

vendor/github.com/aws/smithy-go/document.go−10

This diff is too big to show here.

vendor/github.com/aws/smithy-go/document/doc.go−12

This diff is too big to show here.

vendor/github.com/aws/smithy-go/document/document.go−239

This diff is too big to show here.

vendor/github.com/aws/smithy-go/document/errors.go−75

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/doc.go−4

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/encoding.go−40

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/httpbinding/encode.go−123

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/httpbinding/header.go−122

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/httpbinding/path_replace.go−108

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/httpbinding/query.go−107

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/httpbinding/uri.go−111

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/json/array.go−35

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/json/constants.go−15

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/json/decoder_util.go−139

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/json/encoder.go−30

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/json/escape.go−198

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/json/object.go−40

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/json/value.go−154

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/xml/array.go−49

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/xml/constants.go−10

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/xml/doc.go−49

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/xml/element.go−91

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/xml/encoder.go−51

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/xml/error_utils.go−51

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/xml/escape.go−137

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/xml/map.go−53

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/xml/value.go−302

This diff is too big to show here.

vendor/github.com/aws/smithy-go/encoding/xml/xml_decoder.go−154

This diff is too big to show here.

vendor/github.com/aws/smithy-go/endpoints/endpoint.go−23

This diff is too big to show here.

vendor/github.com/aws/smithy-go/endpoints/private/bdd/evaluate.go−35

This diff is too big to show here.

vendor/github.com/aws/smithy-go/endpoints/private/rulesfn/doc.go−4

This diff is too big to show here.

vendor/github.com/aws/smithy-go/endpoints/private/rulesfn/split.go−16

This diff is too big to show here.

vendor/github.com/aws/smithy-go/endpoints/private/rulesfn/string_slice.go−18

This diff is too big to show here.

vendor/github.com/aws/smithy-go/endpoints/private/rulesfn/strings.go−25

This diff is too big to show here.

vendor/github.com/aws/smithy-go/endpoints/private/rulesfn/uri.go−133

This diff is too big to show here.

vendor/github.com/aws/smithy-go/errors.go−137

This diff is too big to show here.

vendor/github.com/aws/smithy-go/eventstream/const.go−24

This diff is too big to show here.

vendor/github.com/aws/smithy-go/eventstream/debug.go−144

This diff is too big to show here.

vendor/github.com/aws/smithy-go/eventstream/decode.go−218

This diff is too big to show here.

vendor/github.com/aws/smithy-go/eventstream/deserializer.go−294

This diff is too big to show here.

vendor/github.com/aws/smithy-go/eventstream/encode.go−167

This diff is too big to show here.

vendor/github.com/aws/smithy-go/eventstream/error.go−23

This diff is too big to show here.

vendor/github.com/aws/smithy-go/eventstream/header.go−175

This diff is too big to show here.

vendor/github.com/aws/smithy-go/eventstream/header_value.go−521

This diff is too big to show here.

vendor/github.com/aws/smithy-go/eventstream/message.go−99

This diff is too big to show here.

vendor/github.com/aws/smithy-go/eventstream/serializer.go−228

This diff is too big to show here.

vendor/github.com/aws/smithy-go/eventstream/signer.go−82

This diff is too big to show here.

vendor/github.com/aws/smithy-go/eventstream/types.go−26

This diff is too big to show here.

vendor/github.com/aws/smithy-go/go_module_metadata.go−6

This diff is too big to show here.

vendor/github.com/aws/smithy-go/internal/sync/singleflight/LICENSE−28

This diff is too big to show here.

vendor/github.com/aws/smithy-go/internal/sync/singleflight/docs.go−8

This diff is too big to show here.

vendor/github.com/aws/smithy-go/internal/sync/singleflight/singleflight.go−210

This diff is too big to show here.

vendor/github.com/aws/smithy-go/io/byte.go−12

This diff is too big to show here.

vendor/github.com/aws/smithy-go/io/doc.go−2

This diff is too big to show here.

vendor/github.com/aws/smithy-go/io/reader.go−16

This diff is too big to show here.

vendor/github.com/aws/smithy-go/io/ringbuffer.go−94

This diff is too big to show here.

vendor/github.com/aws/smithy-go/local-mod-replace.sh−39

This diff is too big to show here.

vendor/github.com/aws/smithy-go/logging/logger.go−82

This diff is too big to show here.

vendor/github.com/aws/smithy-go/metrics/metrics.go−136

This diff is too big to show here.

vendor/github.com/aws/smithy-go/metrics/nop.go−98

This diff is too big to show here.

vendor/github.com/aws/smithy-go/middleware/context.go−41

This diff is too big to show here.

vendor/github.com/aws/smithy-go/middleware/doc.go−67

This diff is too big to show here.

vendor/github.com/aws/smithy-go/middleware/eventstream_middleware.go−21

This diff is too big to show here.

vendor/github.com/aws/smithy-go/middleware/logging.go−46

This diff is too big to show here.

vendor/github.com/aws/smithy-go/middleware/metadata.go−65

This diff is too big to show here.

vendor/github.com/aws/smithy-go/middleware/middleware.go−71

This diff is too big to show here.

vendor/github.com/aws/smithy-go/middleware/ordered_group.go−270

This diff is too big to show here.

vendor/github.com/aws/smithy-go/middleware/stack.go−209

This diff is too big to show here.

vendor/github.com/aws/smithy-go/middleware/stack_values.go−100

This diff is too big to show here.

vendor/github.com/aws/smithy-go/middleware/step_build.go−298

This diff is too big to show here.

vendor/github.com/aws/smithy-go/middleware/step_deserialize.go−300

This diff is too big to show here.

vendor/github.com/aws/smithy-go/middleware/step_finalize.go−298

This diff is too big to show here.

vendor/github.com/aws/smithy-go/middleware/step_initialize.go−299

This diff is too big to show here.

vendor/github.com/aws/smithy-go/middleware/step_serialize.go−305

This diff is too big to show here.

vendor/github.com/aws/smithy-go/modman.toml−9

This diff is too big to show here.

vendor/github.com/aws/smithy-go/private/requestcompression/gzip.go−30

This diff is too big to show here.

vendor/github.com/aws/smithy-go/private/requestcompression/middleware_capture_request_compression.go−52

This diff is too big to show here.

vendor/github.com/aws/smithy-go/private/requestcompression/request_compression.go−103

This diff is too big to show here.

vendor/github.com/aws/smithy-go/properties.go−69

This diff is too big to show here.

vendor/github.com/aws/smithy-go/ptr/doc.go−5

This diff is too big to show here.

vendor/github.com/aws/smithy-go/ptr/from_ptr.go−601

This diff is too big to show here.

vendor/github.com/aws/smithy-go/ptr/gen_scalars.go−83

This diff is too big to show here.

vendor/github.com/aws/smithy-go/ptr/to_ptr.go−499

This diff is too big to show here.

vendor/github.com/aws/smithy-go/rand/doc.go−3

This diff is too big to show here.

vendor/github.com/aws/smithy-go/rand/rand.go−31

This diff is too big to show here.

vendor/github.com/aws/smithy-go/rand/uuid.go−87

This diff is too big to show here.

vendor/github.com/aws/smithy-go/schema.go−328

This diff is too big to show here.

vendor/github.com/aws/smithy-go/schema_ext.go−37

This diff is too big to show here.

vendor/github.com/aws/smithy-go/serde.go−229

This diff is too big to show here.

vendor/github.com/aws/smithy-go/sync/error.go−53

This diff is too big to show here.

vendor/github.com/aws/smithy-go/time/time.go−134

This diff is too big to show here.

vendor/github.com/aws/smithy-go/tracing/context.go−96

This diff is too big to show here.

vendor/github.com/aws/smithy-go/tracing/nop.go−32

This diff is too big to show here.

vendor/github.com/aws/smithy-go/tracing/tracing.go−95

This diff is too big to show here.

vendor/github.com/aws/smithy-go/trait.go−21

This diff is too big to show here.

vendor/github.com/aws/smithy-go/traits/http.go−69

This diff is too big to show here.

vendor/github.com/aws/smithy-go/traits/index.go−107

This diff is too big to show here.

vendor/github.com/aws/smithy-go/traits/serde.go−56

This diff is too big to show here.

vendor/github.com/aws/smithy-go/traits/traits.go−72

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/auth.go−30

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/auth_schemes.go−45

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/checksum_middleware.go−70

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/client.go−161

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/doc.go−5

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/eventstream.go−209

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/eventstream_middleware.go−69

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/headerlist.go−163

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/host.go−89

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/interceptor.go−321

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/interceptor_middleware.go−325

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/internal/io/safe.go−75

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/md5_checksum.go−25

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/metrics.go−204

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/middleware_close_response_body.go−79

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/middleware_content_length.go−84

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/middleware_header_comment.go−81

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/middleware_headers.go−167

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/middleware_http_logging.go−75

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/middleware_metadata.go−51

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/middleware_min_proto.go−79

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/properties.go−80

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/protocol.go−27

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/request.go−188

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/response.go−34

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/time.go−13

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/url.go−44

This diff is too big to show here.

vendor/github.com/aws/smithy-go/transport/http/user_agent.go−37

This diff is too big to show here.

vendor/github.com/aws/smithy-go/type_registry.go−70

This diff is too big to show here.

vendor/github.com/aws/smithy-go/validation.go−140

This diff is too big to show here.

vendor/github.com/aymanbagabas/go-udiff/LICENSE-BSD−27

This diff is too big to show here.

vendor/github.com/aymanbagabas/go-udiff/LICENSE-MIT−21

This diff is too big to show here.

vendor/github.com/aymanbagabas/go-udiff/Makefile−4

This diff is too big to show here.

vendor/github.com/aymanbagabas/go-udiff/README.md−142

This diff is too big to show here.

vendor/github.com/aymanbagabas/go-udiff/diff.go−177

This diff is too big to show here.

vendor/github.com/aymanbagabas/go-udiff/export.go−16

This diff is too big to show here.

vendor/github.com/aymanbagabas/go-udiff/lcs/common.go−179

This diff is too big to show here.

vendor/github.com/aymanbagabas/go-udiff/lcs/doc.go−156

This diff is too big to show here.

vendor/github.com/aymanbagabas/go-udiff/lcs/export.go−11

This diff is too big to show here.

vendor/github.com/aymanbagabas/go-udiff/lcs/git.sh−33

This diff is too big to show here.

vendor/github.com/aymanbagabas/go-udiff/lcs/labels.go−55

This diff is too big to show here.

vendor/github.com/aymanbagabas/go-udiff/lcs/old.go−475

This diff is too big to show here.

vendor/github.com/aymanbagabas/go-udiff/lcs/sequence.go−70

This diff is too big to show here.

vendor/github.com/aymanbagabas/go-udiff/merge.go−81

This diff is too big to show here.

vendor/github.com/aymanbagabas/go-udiff/ndiff.go−118

This diff is too big to show here.

vendor/github.com/aymanbagabas/go-udiff/unified.go−314

This diff is too big to show here.

vendor/github.com/aymerick/douceur/LICENSE−22

This diff is too big to show here.

vendor/github.com/aymerick/douceur/css/declaration.go−60

This diff is too big to show here.

vendor/github.com/aymerick/douceur/css/rule.go−230

This diff is too big to show here.

vendor/github.com/aymerick/douceur/css/stylesheet.go−25

This diff is too big to show here.

vendor/github.com/aymerick/douceur/parser/parser.go−409

This diff is too big to show here.

vendor/github.com/bahlo/generic-list-go/LICENSE−27

This diff is too big to show here.

vendor/github.com/bahlo/generic-list-go/README.md−5

This diff is too big to show here.

vendor/github.com/bahlo/generic-list-go/list.go−235

This diff is too big to show here.

vendor/github.com/bmatcuk/doublestar/v4/.codecov.yml−10

This diff is too big to show here.

vendor/github.com/bmatcuk/doublestar/v4/.gitignore−32

This diff is too big to show here.

vendor/github.com/bmatcuk/doublestar/v4/LICENSE−22

This diff is too big to show here.

vendor/github.com/bmatcuk/doublestar/v4/README.md−495

This diff is too big to show here.

vendor/github.com/bmatcuk/doublestar/v4/UPGRADING.md−63

This diff is too big to show here.

vendor/github.com/bmatcuk/doublestar/v4/doublestar.go−13

This diff is too big to show here.

vendor/github.com/bmatcuk/doublestar/v4/glob.go−497

This diff is too big to show here.

vendor/github.com/bmatcuk/doublestar/v4/globoptions.go−188

This diff is too big to show here.

vendor/github.com/bmatcuk/doublestar/v4/globwalk.go−438

This diff is too big to show here.

vendor/github.com/bmatcuk/doublestar/v4/match.go−419

This diff is too big to show here.

vendor/github.com/bmatcuk/doublestar/v4/utils.go−165

This diff is too big to show here.

vendor/github.com/bmatcuk/doublestar/v4/utils_unix.go−19

This diff is too big to show here.

vendor/github.com/bmatcuk/doublestar/v4/utils_windows.go−43

This diff is too big to show here.

vendor/github.com/bmatcuk/doublestar/v4/validate.go−80

This diff is too big to show here.

vendor/github.com/buger/jsonparser/.gitignore−12

This diff is too big to show here.

vendor/github.com/buger/jsonparser/.travis.yml−12

This diff is too big to show here.

vendor/github.com/buger/jsonparser/Dockerfile−12

This diff is too big to show here.

vendor/github.com/buger/jsonparser/LICENSE−21

This diff is too big to show here.

vendor/github.com/buger/jsonparser/Makefile−36

This diff is too big to show here.

vendor/github.com/buger/jsonparser/README.md−361

This diff is too big to show here.

vendor/github.com/buger/jsonparser/bytes.go−47

This diff is too big to show here.

vendor/github.com/buger/jsonparser/bytes_safe.go−25

This diff is too big to show here.

vendor/github.com/buger/jsonparser/bytes_unsafe.go−44

This diff is too big to show here.

vendor/github.com/buger/jsonparser/escape.go−173

This diff is too big to show here.

vendor/github.com/buger/jsonparser/fuzz.go−117

This diff is too big to show here.

vendor/github.com/buger/jsonparser/oss-fuzz-build.sh−47

This diff is too big to show here.

vendor/github.com/buger/jsonparser/parser.go−1318

This diff is too big to show here.

vendor/github.com/cespare/xxhash/v2/LICENSE.txt−22

This diff is too big to show here.

vendor/github.com/cespare/xxhash/v2/README.md−74

This diff is too big to show here.

vendor/github.com/cespare/xxhash/v2/testall.sh−10

This diff is too big to show here.

vendor/github.com/cespare/xxhash/v2/xxhash.go−243

This diff is too big to show here.

vendor/github.com/cespare/xxhash/v2/xxhash_amd64.s−209

This diff is too big to show here.

vendor/github.com/cespare/xxhash/v2/xxhash_arm64.s−183

This diff is too big to show here.

vendor/github.com/cespare/xxhash/v2/xxhash_asm.go−15

This diff is too big to show here.

vendor/github.com/cespare/xxhash/v2/xxhash_other.go−76

This diff is too big to show here.

vendor/github.com/cespare/xxhash/v2/xxhash_safe.go−16

This diff is too big to show here.

vendor/github.com/cespare/xxhash/v2/xxhash_unsafe.go−58

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/.gitignore−2

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/LICENSE−21

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/Makefile−101

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/README.md−193

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/adapters.go−102

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/dirent.go−73

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/dirent_portable.go−134

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/dirent_unix.go−146

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/entry_filter_portable.go−37

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/entry_filter_unix.go−59

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/entry_filter_windows.go−156

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/fastwalk.go−697

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/fastwalk_darwin.go−122

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/fastwalk_portable.go−65

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/fastwalk_unix.go−131

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/go.LICENSE−27

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/internal/dirent/dirent.go−85

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/internal/dirent/dirent_aix.go−29

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/internal/dirent/dirent_dragonfly.go−54

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/internal/dirent/dirent_freebsd.go−46

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/internal/dirent/dirent_js.go−27

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/internal/dirent/dirent_linux.go−50

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/internal/dirent/dirent_netbsd.go−46

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/internal/dirent/dirent_openbsd.go−46

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/internal/dirent/dirent_solaris.go−29

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/internal/dirent/doc.go−2

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/internal/fmtdirent/fmtdirent_go120.go−31

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/internal/fmtdirent/fmtdirent_go121.go−15

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/zsyscall_darwin.go−93

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/zsyscall_darwin_amd64.1_13.s−22

This diff is too big to show here.

vendor/github.com/charlievieth/fastwalk/zsyscall_darwin_arm64.1_13.s−22

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/.gitignore−4

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/.release-please-manifest.json−3

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/.stats.yml−4

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/Brewfile−1

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/CHANGELOG.md−641

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/CONTRIBUTING.md−65

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/LICENSE−8

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/README.md−999

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/SECURITY.md−27

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/aliases.go−72

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/api.md−545

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/bedrock/bedrock.go−295

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/beta.go−374

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/betafile.go−273

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/betamessage.go−11734

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/betamessagebatch.go−925

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/betamessageutil.go−459

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/betamodel.go−151

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/betaskill.go−362

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/betaskillversion.go−373

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/betatoolrunner.go−435

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/client.go−157

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/completion.go−196

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/field.go−46

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/apierror/apierror.go−58

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/apiform/encoder.go−473

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/apiform/form.go−5

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/apiform/richparam.go−21

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/apiform/tag.go−51

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/apijson/decoder.go−709

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/apijson/encoder.go−392

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/apijson/enum.go−145

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/apijson/field.go−23

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/apijson/port.go−120

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/apijson/registry.go−59

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/apijson/subfield.go−68

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/apijson/tag.go−47

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/apijson/union.go−209

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/apiquery/encoder.go−414

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/apiquery/query.go−55

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/apiquery/richparam.go−20

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/apiquery/tag.go−44

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/encoding/json/decode.go−1325

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/encoding/json/encode.go−1396

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/encoding/json/fold.go−48

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/encoding/json/indent.go−182

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/encoding/json/scanner.go−610

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/encoding/json/sentinel/null.go−47

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/encoding/json/shims/shims.go−113

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/encoding/json/stream.go−512

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/encoding/json/tables.go−218

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/encoding/json/tags.go−38

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/encoding/json/time.go−62

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/paramutil/field.go−30

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/paramutil/union.go−49

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/requestconfig/requestconfig.go−638

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/internal/version.go−5

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/message.go−9436

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/messagebatch.go−827

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/messageutil.go−413

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/model.go−151

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/option/middleware.go−38

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/option/requestoption.go−285

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/packages/jsonl/jsonl.go−62

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/packages/pagination/pagination.go−351

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/packages/param/encoder.go−109

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/packages/param/null.go−19

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/packages/param/option.go−122

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/packages/param/param.go−189

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/packages/respjson/respjson.go−88

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/packages/ssestream/ssestream.go−198

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/release-please-config.json−68

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/schemautil.go−244

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/shared/constant/constants.go−487

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/shared/shared.go−346

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/tools.md−292

This diff is too big to show here.

vendor/github.com/charmbracelet/anthropic-sdk-go/vertex/vertex.go−134

This diff is too big to show here.

vendor/github.com/charmbracelet/colorprofile/.golangci.yml−47

This diff is too big to show here.

vendor/github.com/charmbracelet/colorprofile/.goreleaser.yml−6

This diff is too big to show here.

vendor/github.com/charmbracelet/colorprofile/LICENSE−21

This diff is too big to show here.

vendor/github.com/charmbracelet/colorprofile/README.md−109

This diff is too big to show here.

vendor/github.com/charmbracelet/colorprofile/doc.go−4

This diff is too big to show here.

vendor/github.com/charmbracelet/colorprofile/env.go−294

This diff is too big to show here.

vendor/github.com/charmbracelet/colorprofile/env_other.go−8

This diff is too big to show here.

vendor/github.com/charmbracelet/colorprofile/env_windows.go−40

This diff is too big to show here.

vendor/github.com/charmbracelet/colorprofile/profile.go−108

This diff is too big to show here.

vendor/github.com/charmbracelet/colorprofile/writer.go−171

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/.gitignore−4

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/.release-please-manifest.json−3

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/.stats.yml−4

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/Brewfile−1

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/CHANGELOG.md−1171

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/CONTRIBUTING.md−65

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/LICENSE−201

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/MIGRATION.md−284

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/README.md−1007

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/SECURITY.md−29

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/aliases.go−585

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/api.md−774

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/audio.go−61

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/audiospeech.go−179

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/audiotranscription.go−950

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/audiotranslation.go−120

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/azure/azure.go−269

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/batch.go−464

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/beta.go−36

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/betaassistant.go−2284

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/betachatkit.go−125

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/betachatkitsession.go−83

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/betachatkitthread.go−1564

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/betathread.go−1574

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/betathreadmessage.go−1717

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/betathreadrun.go−974

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/betathreadrunstep.go−1396

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/chat.go−30

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/chatcompletion.go−3521

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/chatcompletionmessage.go−100

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/client.go−185

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/completion.go−436

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/container.go−648

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/containerfile.go−287

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/containerfilecontent.go−50

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/conversations/aliases.go−585

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/conversations/api.md−31

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/conversations/conversation.go−493

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/conversations/item.go−1082

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/embedding.go−207

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/field.go−45

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/file.go−352

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/finetuning.go−35

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/finetuningalpha.go−29

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/finetuningalphagrader.go−675

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/finetuningcheckpoint.go−29

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/finetuningcheckpointpermission.go−352

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/finetuningjob.go−884

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/finetuningjobcheckpoint.go−152

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/finetuningmethod.go−1487

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/grader.go−28

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/gradergradermodel.go−1753

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/image.go−1337

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/apierror/apierror.go−58

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/apiform/encoder.go−473

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/apiform/form.go−5

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/apiform/richparam.go−20

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/apiform/tag.go−72

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/apijson/decoder.go−688

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/apijson/encoder.go−371

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/apijson/enum.go−140

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/apijson/field.go−23

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/apijson/port.go−120

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/apijson/registry.go−51

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/apijson/subfield.go−67

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/apijson/tag.go−69

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/apijson/union.go−208

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/apiquery/encoder.go−394

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/apiquery/query.go−55

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/apiquery/richparam.go−20

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/apiquery/tag.go−44

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/encoding/json/decode.go−1324

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/encoding/json/encode.go−1396

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/encoding/json/fold.go−48

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/encoding/json/indent.go−182

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/encoding/json/scanner.go−610

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/encoding/json/sentinel/null.go−46

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/encoding/json/shims/shims.go−113

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/encoding/json/stream.go−512

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/encoding/json/tables.go−218

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/encoding/json/tags.go−38

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/encoding/json/time.go−61

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/paramutil/field.go−30

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/paramutil/union.go−48

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/requestconfig/requestconfig.go−644

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/internal/version.go−5

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/model.go−137

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/moderation.go−489

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/option/middleware.go−38

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/option/requestoption.go−299

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/packages/pagination/pagination.go−329

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/packages/param/encoder.go−109

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/packages/param/null.go−19

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/packages/param/option.go−122

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/packages/param/param.go−186

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/packages/respjson/respjson.go−88

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/packages/ssestream/ssestream.go−247

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/polling.go−129

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/realtime/aliases.go−585

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/realtime/api.md−55

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/realtime/call.go−133

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/realtime/clientsecret.go−1826

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/realtime/realtime.go−1979

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/release-please-config.json−68

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/responses/aliases.go−585

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/responses/api.md−240

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/responses/inputitem.go−135

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/responses/inputtoken.go−336

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/responses/response.go−21786

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/shared/constant/constants.go−1143

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/shared/shared.go−1183

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/skill.go−292

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/skillcontent.go−46

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/skillversion.go−283

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/skillversioncontent.go−50

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/streamaccumulator.go−215

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/upload.go−238

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/uploadpart.go−115

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/vectorstore.go−911

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/vectorstorefile.go−535

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/vectorstorefilebatch.go−427

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/video.go−677

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/webhooks/aliases.go−585

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/webhooks/api.md−20

This diff is too big to show here.

vendor/github.com/charmbracelet/openai-go/webhooks/webhook.go−1306

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/.gitattributes−1

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/.golangci.yml−48

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/.goreleaser.yml−5

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/LICENSE−21

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/README.md−161

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/TUTORIAL.md−175

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/border.go−231

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/buffer.go−831

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/cancelreader_other.go−16

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/cancelreader_windows.go−139

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/cell.go−465

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/console.go−219

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/console_unix.go−13

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/console_windows.go−13

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/cursor.go−22

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/decoder.go−2102

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/doc.go−46

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/environ.go−32

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/event.go−567

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/internal/casso/math.go−172

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/internal/casso/solver.go−275

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/internal/lru/lru.go−69

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/key.go−762

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/key_table.go−664

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/layout/cache.go−29

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/layout/constraint.go−174

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/layout/flex.go−210

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/layout/layout.go−973

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/layout/padding.go−66

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/logger.go−6

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/mouse.go−150

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/poll.go−38

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/poll_bsd.go−165

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/poll_default.go−12

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/poll_fallback.go−158

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/poll_linux.go−175

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/poll_select.go−156

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/poll_solaris.go−12

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/poll_windows.go−217

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/screen/context.go−379

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/screen/screen.go−117

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/styled.go−337

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/tabstop.go−142

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/terminal.go−394

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/terminal_bsdly.go−10

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/terminal_bsdly_other.go−8

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/terminal_other.go−22

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/terminal_reader.go−503

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/terminal_reader_other.go−13

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/terminal_reader_windows.go−336

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/terminal_renderer.go−1639

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/terminal_renderer_hardscroll.go−243

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/terminal_renderer_hashmap.go−298

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/terminal_screen.go−837

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/terminal_tabdly.go−10

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/terminal_tabdly_other.go−8

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/terminal_unix.go−56

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/terminal_windows.go−77

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/tty.go−42

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/tty_other.go−26

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/tty_unix.go−39

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/tty_windows.go−39

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/utils.go−15

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/uv.go−406

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/winch.go−62

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/winch_other.go−17

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/winch_unix.go−50

This diff is too big to show here.

vendor/github.com/charmbracelet/ultraviolet/window.go−147

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/LICENSE−21

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/ansi.go−11

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/ascii.go−8

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/background.go−178

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/c0.go−79

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/c1.go−72

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/charset.go−55

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/clipboard.go−75

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/color.go−784

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/ctrl.go−156

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/cursor.go−637

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/cwd.go−26

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/doc.go−7

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/finalterm.go−67

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/focus.go−9

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/graphics.go−62

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/hyperlink.go−28

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/inband.go−24

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/iterm2.go−18

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/keypad.go−28

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/kitty.go−90

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/kitty/decoder.go−86

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/kitty/encoder.go−64

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/kitty/graphics.go−414

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/kitty/options.go−373

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/kitty/writer.go−187

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/method.go−196

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/mode.go−690

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/mode_deprecated.go−495

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/modes.go−65

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/mouse.go−172

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/notification.go−32

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/palette.go−34

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/parser.go−417

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/parser/const.go−79

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/parser/seq.go−136

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/parser/transition_table.go−273

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/parser_decode.go−525

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/parser_handler.go−60

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/parser_sync.go−29

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/passthrough.go−60

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/paste.go−7

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/progress.go−49

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/reset.go−11

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/screen.go−410

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/sgr.go−79

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/status.go−168

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/style.go−819

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/termcap.go−41

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/title.go−48

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/truncate.go−290

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/urxvt.go−17

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/util.go−92

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/width.go−107

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/winop.go−59

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/wrap.go−472

This diff is too big to show here.

vendor/github.com/charmbracelet/x/ansi/xterm.go−138

This diff is too big to show here.

vendor/github.com/charmbracelet/x/editor/LICENSE−21

This diff is too big to show here.

vendor/github.com/charmbracelet/x/editor/editor.go−140

This diff is too big to show here.

vendor/github.com/charmbracelet/x/etag/LICENSE−21

This diff is too big to show here.

vendor/github.com/charmbracelet/x/etag/etag.go−48

This diff is too big to show here.

vendor/github.com/charmbracelet/x/exp/charmtone/LICENSE−21

This diff is too big to show here.

vendor/github.com/charmbracelet/x/exp/charmtone/charmtone.go−443

This diff is too big to show here.

vendor/github.com/charmbracelet/x/exp/golden/LICENSE−21

This diff is too big to show here.

vendor/github.com/charmbracelet/x/exp/golden/golden.go−83

This diff is too big to show here.

vendor/github.com/charmbracelet/x/exp/ordered/LICENSE−21

This diff is too big to show here.

vendor/github.com/charmbracelet/x/exp/ordered/ordered.go−42

This diff is too big to show here.

vendor/github.com/charmbracelet/x/exp/slice/LICENSE−21

This diff is too big to show here.

vendor/github.com/charmbracelet/x/exp/slice/slice.go−148

This diff is too big to show here.

vendor/github.com/charmbracelet/x/exp/strings/LICENSE−21

This diff is too big to show here.

vendor/github.com/charmbracelet/x/exp/strings/contains.go−16

This diff is too big to show here.

vendor/github.com/charmbracelet/x/exp/strings/join.go−134

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/LICENSE−21

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/config/config.go−118

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/config/lsps.json−367

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/config/overrides.go−85

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/lsp/client.go−875

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/lsp/language.go−158

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/lsp/protocol.go−50

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/lsp/protocol/LICENSE−27

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/lsp/protocol/interface.go−132

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/lsp/protocol/pattern_interfaces.go−79

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/lsp/protocol/tables.go−31

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/lsp/protocol/tsdocument-changes.go−83

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/lsp/protocol/tsjson.go−3072

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/lsp/protocol/tsprotocol.go−6963

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/lsp/protocol/uri.go−192

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/lsp/sync.go−240

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/lsp/types.go−93

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/transport/connection.go−130

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/transport/router.go−107

This diff is too big to show here.

vendor/github.com/charmbracelet/x/powernap/pkg/transport/transport.go−214

This diff is too big to show here.

vendor/github.com/charmbracelet/x/term/LICENSE−21

This diff is too big to show here.

vendor/github.com/charmbracelet/x/term/term.go−51

This diff is too big to show here.

vendor/github.com/charmbracelet/x/term/term_other.go−39

This diff is too big to show here.

vendor/github.com/charmbracelet/x/term/term_plan9.go−118

This diff is too big to show here.

vendor/github.com/charmbracelet/x/term/term_unix.go−96

This diff is too big to show here.

vendor/github.com/charmbracelet/x/term/term_unix_bsd.go−11

This diff is too big to show here.

vendor/github.com/charmbracelet/x/term/term_unix_other.go−11

This diff is too big to show here.

vendor/github.com/charmbracelet/x/term/term_windows.go−87

This diff is too big to show here.

vendor/github.com/charmbracelet/x/term/terminal.go−12

This diff is too big to show here.

vendor/github.com/charmbracelet/x/term/util.go−47

This diff is too big to show here.

vendor/github.com/charmbracelet/x/termios/LICENSE−21

This diff is too big to show here.

vendor/github.com/charmbracelet/x/termios/bit_bsd.go−7

This diff is too big to show here.

vendor/github.com/charmbracelet/x/termios/bit_darwin.go−7

This diff is too big to show here.

vendor/github.com/charmbracelet/x/termios/bit_other.go−7

This diff is too big to show here.

vendor/github.com/charmbracelet/x/termios/syscalls_bsd.go−11

This diff is too big to show here.

vendor/github.com/charmbracelet/x/termios/syscalls_darwin.go−10

This diff is too big to show here.

vendor/github.com/charmbracelet/x/termios/syscalls_linux.go−14

This diff is too big to show here.

vendor/github.com/charmbracelet/x/termios/termios.go−249

This diff is too big to show here.

vendor/github.com/charmbracelet/x/termios/termios_bsd.go−13

This diff is too big to show here.

vendor/github.com/charmbracelet/x/termios/termios_linux.go−13

This diff is too big to show here.

vendor/github.com/charmbracelet/x/termios/termios_other.go−15

This diff is too big to show here.

vendor/github.com/charmbracelet/x/termios/termios_solaris.go−26

This diff is too big to show here.

vendor/github.com/charmbracelet/x/windows/LICENSE−21

This diff is too big to show here.

vendor/github.com/charmbracelet/x/windows/doc.go−4

This diff is too big to show here.

vendor/github.com/charmbracelet/x/windows/syscall_windows.go−14

This diff is too big to show here.

vendor/github.com/charmbracelet/x/windows/types.go−221

This diff is too big to show here.

vendor/github.com/charmbracelet/x/windows/types_windows.go−142

This diff is too big to show here.

vendor/github.com/charmbracelet/x/windows/zsyscall_windows.go−77

This diff is too big to show here.

vendor/github.com/clipperhouse/displaywidth/.gitignore−3

This diff is too big to show here.

vendor/github.com/clipperhouse/displaywidth/AGENTS.md−51

This diff is too big to show here.

vendor/github.com/clipperhouse/displaywidth/CHANGELOG.md−129

This diff is too big to show here.

vendor/github.com/clipperhouse/displaywidth/LICENSE−21

This diff is too big to show here.

vendor/github.com/clipperhouse/displaywidth/README.md−190

This diff is too big to show here.

vendor/github.com/clipperhouse/displaywidth/gen.go−3

This diff is too big to show here.

vendor/github.com/clipperhouse/displaywidth/graphemes.go−73

This diff is too big to show here.

vendor/github.com/clipperhouse/displaywidth/options.go−30

This diff is too big to show here.

vendor/github.com/clipperhouse/displaywidth/trie.go−1699

This diff is too big to show here.

vendor/github.com/clipperhouse/displaywidth/truncate.go−149

This diff is too big to show here.

vendor/github.com/clipperhouse/displaywidth/width.go−239

This diff is too big to show here.

vendor/github.com/clipperhouse/uax29/v2/LICENSE−21

This diff is too big to show here.

vendor/github.com/clipperhouse/uax29/v2/graphemes/README.md−120

This diff is too big to show here.

vendor/github.com/clipperhouse/uax29/v2/graphemes/ansi.go−138

This diff is too big to show here.

vendor/github.com/clipperhouse/uax29/v2/graphemes/ansi8.go−79

This diff is too big to show here.

vendor/github.com/clipperhouse/uax29/v2/graphemes/iterator.go−144

This diff is too big to show here.

vendor/github.com/clipperhouse/uax29/v2/graphemes/reader.go−25

This diff is too big to show here.

vendor/github.com/clipperhouse/uax29/v2/graphemes/splitfunc.go−205

This diff is too big to show here.

vendor/github.com/clipperhouse/uax29/v2/graphemes/trie.go−1717

This diff is too big to show here.

vendor/github.com/clipperhouse/uax29/v2/words/README.md−121

This diff is too big to show here.

vendor/github.com/clipperhouse/uax29/v2/words/bleve_compat.go−104

This diff is too big to show here.

vendor/github.com/clipperhouse/uax29/v2/words/iterator.go−126

This diff is too big to show here.

vendor/github.com/clipperhouse/uax29/v2/words/joiners.go−41

This diff is too big to show here.

vendor/github.com/clipperhouse/uax29/v2/words/reader.go−34

This diff is too big to show here.

vendor/github.com/clipperhouse/uax29/v2/words/seek.go−43

This diff is too big to show here.

vendor/github.com/clipperhouse/uax29/v2/words/splitfunc.go−269

This diff is too big to show here.

vendor/github.com/clipperhouse/uax29/v2/words/trie.go−3774

This diff is too big to show here.

vendor/github.com/cyphar/filepath-securejoin/.golangci.yml−60

This diff is too big to show here.

vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md−453

This diff is too big to show here.

vendor/github.com/cyphar/filepath-securejoin/COPYING.md−447

This diff is too big to show here.

vendor/github.com/cyphar/filepath-securejoin/LICENSE.BSD−28

This diff is too big to show here.

vendor/github.com/cyphar/filepath-securejoin/LICENSE.MPL-2.0−373

This diff is too big to show here.

vendor/github.com/cyphar/filepath-securejoin/README.md−184

This diff is too big to show here.

vendor/github.com/cyphar/filepath-securejoin/VERSION−1

This diff is too big to show here.

vendor/github.com/cyphar/filepath-securejoin/codecov.yml−29

This diff is too big to show here.

vendor/github.com/cyphar/filepath-securejoin/doc.go−47

This diff is too big to show here.

vendor/github.com/cyphar/filepath-securejoin/internal/consts/consts.go−15

This diff is too big to show here.

vendor/github.com/cyphar/filepath-securejoin/join.go−169

This diff is too big to show here.

vendor/github.com/cyphar/filepath-securejoin/vfs.go−37

This diff is too big to show here.

vendor/github.com/davecgh/go-spew/LICENSE−15

This diff is too big to show here.

vendor/github.com/davecgh/go-spew/spew/bypass.go−145

This diff is too big to show here.

vendor/github.com/davecgh/go-spew/spew/bypasssafe.go−38

This diff is too big to show here.

vendor/github.com/davecgh/go-spew/spew/common.go−341

This diff is too big to show here.

vendor/github.com/davecgh/go-spew/spew/config.go−306

This diff is too big to show here.

vendor/github.com/davecgh/go-spew/spew/doc.go−211

This diff is too big to show here.

vendor/github.com/davecgh/go-spew/spew/dump.go−509

This diff is too big to show here.

vendor/github.com/davecgh/go-spew/spew/format.go−419

This diff is too big to show here.

vendor/github.com/davecgh/go-spew/spew/spew.go−148

This diff is too big to show here.

vendor/github.com/denisbrodbeck/machineid/.gitignore−29

This diff is too big to show here.

vendor/github.com/denisbrodbeck/machineid/LICENSE.md−22

This diff is too big to show here.

vendor/github.com/denisbrodbeck/machineid/README.md−182

This diff is too big to show here.

vendor/github.com/denisbrodbeck/machineid/helper.go−36

This diff is too big to show here.

vendor/github.com/denisbrodbeck/machineid/id.go−45

This diff is too big to show here.

vendor/github.com/denisbrodbeck/machineid/id_bsd.go−42

This diff is too big to show here.

vendor/github.com/denisbrodbeck/machineid/id_darwin.go−37

This diff is too big to show here.

vendor/github.com/denisbrodbeck/machineid/id_linux.go−27

This diff is too big to show here.

vendor/github.com/denisbrodbeck/machineid/id_windows.go−23

This diff is too big to show here.

vendor/github.com/denisbrodbeck/machineid/logo.png

This diff is too big to show here.

vendor/github.com/denisbrodbeck/machineid/makefile−12

This diff is too big to show here.

vendor/github.com/disintegration/imaging/.travis.yml−12

This diff is too big to show here.

vendor/github.com/disintegration/imaging/LICENSE−21

This diff is too big to show here.

vendor/github.com/disintegration/imaging/README.md−226

This diff is too big to show here.

vendor/github.com/disintegration/imaging/adjust.go−253

This diff is too big to show here.

vendor/github.com/disintegration/imaging/convolution.go−148

This diff is too big to show here.

vendor/github.com/disintegration/imaging/doc.go−7

This diff is too big to show here.

vendor/github.com/disintegration/imaging/effects.go−169

This diff is too big to show here.

vendor/github.com/disintegration/imaging/histogram.go−52

This diff is too big to show here.

vendor/github.com/disintegration/imaging/io.go−444

This diff is too big to show here.

vendor/github.com/disintegration/imaging/resize.go−595

This diff is too big to show here.

vendor/github.com/disintegration/imaging/scanner.go−285

This diff is too big to show here.

vendor/github.com/disintegration/imaging/tools.go−249

This diff is too big to show here.

vendor/github.com/disintegration/imaging/transform.go−268

This diff is too big to show here.

vendor/github.com/disintegration/imaging/utils.go−167

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/.gitignore−29

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/ATTRIB−133

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/LICENSE−21

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/README.md−283

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/Taskfile.yml−64

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/bufferpool.go−76

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/fastclock.go−141

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/helpers/indexof.go−394

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/helpers/math.go−15

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/helpers/runes.go−58

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/helpers/searchvalues.go−180

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/match.go−455

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/options.go−157

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/regexp.go−717

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/regexp_codegen.go−70

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/replace.go−357

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/runner.go−2197

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/split.go−67

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/stringprefixfilter.go−375

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/syntax/charclass.go−1507

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/syntax/code.go−316

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/syntax/escape.go−94

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/syntax/fuzz.go−21

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/syntax/optimizations.go−672

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/syntax/parser.go−2529

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/syntax/prefix.go−943

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/syntax/prefixanalyzer.go−1491

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/syntax/replacerdata.go−87

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/syntax/tree.go−2362

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/syntax/unicode_alias_tables.go−6058

This diff is too big to show here.

vendor/github.com/dlclark/regexp2/v2/syntax/writer.go−500

This diff is too big to show here.

vendor/github.com/dustin/go-humanize/.travis.yml−21

This diff is too big to show here.

vendor/github.com/dustin/go-humanize/LICENSE−21

This diff is too big to show here.

vendor/github.com/dustin/go-humanize/README.markdown−124

This diff is too big to show here.

vendor/github.com/dustin/go-humanize/big.go−31

This diff is too big to show here.

vendor/github.com/dustin/go-humanize/bigbytes.go−189

This diff is too big to show here.

vendor/github.com/dustin/go-humanize/bytes.go−143

This diff is too big to show here.

vendor/github.com/dustin/go-humanize/comma.go−116

This diff is too big to show here.

vendor/github.com/dustin/go-humanize/commaf.go−41

This diff is too big to show here.

vendor/github.com/dustin/go-humanize/ftoa.go−49

This diff is too big to show here.

vendor/github.com/dustin/go-humanize/humanize.go−8

This diff is too big to show here.

vendor/github.com/dustin/go-humanize/number.go−192

This diff is too big to show here.

vendor/github.com/dustin/go-humanize/ordinals.go−25

This diff is too big to show here.

vendor/github.com/dustin/go-humanize/si.go−127

This diff is too big to show here.

vendor/github.com/dustin/go-humanize/times.go−117

This diff is too big to show here.

vendor/github.com/ebitengine/purego/.gitignore−1

This diff is too big to show here.

vendor/github.com/ebitengine/purego/LICENSE−201

This diff is too big to show here.

vendor/github.com/ebitengine/purego/README.md−119

This diff is too big to show here.

vendor/github.com/ebitengine/purego/abi_amd64.h−99

This diff is too big to show here.

vendor/github.com/ebitengine/purego/abi_arm64.h−39

This diff is too big to show here.

vendor/github.com/ebitengine/purego/abi_loong64.h−60

This diff is too big to show here.

vendor/github.com/ebitengine/purego/cgo.go−19

This diff is too big to show here.

vendor/github.com/ebitengine/purego/dlerror.go−17

This diff is too big to show here.

vendor/github.com/ebitengine/purego/dlfcn.go−99

This diff is too big to show here.

vendor/github.com/ebitengine/purego/dlfcn_android.go−34

This diff is too big to show here.

vendor/github.com/ebitengine/purego/dlfcn_darwin.go−20

This diff is too big to show here.

vendor/github.com/ebitengine/purego/dlfcn_freebsd.go−14

This diff is too big to show here.

vendor/github.com/ebitengine/purego/dlfcn_linux.go−16

This diff is too big to show here.

vendor/github.com/ebitengine/purego/dlfcn_netbsd.go−15

This diff is too big to show here.

vendor/github.com/ebitengine/purego/dlfcn_nocgo_freebsd.go−11

This diff is too big to show here.

vendor/github.com/ebitengine/purego/dlfcn_nocgo_linux.go−19

This diff is too big to show here.

vendor/github.com/ebitengine/purego/dlfcn_nocgo_netbsd.go−9

This diff is too big to show here.

vendor/github.com/ebitengine/purego/dlfcn_playground.go−24

This diff is too big to show here.

vendor/github.com/ebitengine/purego/dlfcn_stubs.s−22

This diff is too big to show here.

vendor/github.com/ebitengine/purego/func.go−564

This diff is too big to show here.

vendor/github.com/ebitengine/purego/gen.go−6

This diff is too big to show here.

vendor/github.com/ebitengine/purego/go_runtime.go−13

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/cgo/dlfcn_cgo_unix.go−56

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/cgo/empty.go−6

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/cgo/syscall_cgo_unix.go−55

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/abi_amd64.h−99

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/abi_arm64.h−39

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/abi_loong64.h−60

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/abi_ppc64x.h−195

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/asm_386.s−29

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/asm_amd64.s−39

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/asm_arm.s−52

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/asm_arm64.s−36

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/asm_loong64.s−40

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/asm_ppc64le.s−82

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/asm_riscv64.s−78

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/asm_s390x.s−55

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/callbacks.go−93

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/doc.go−32

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/fakecgo.go−14

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/freebsd.go−27

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/go_darwin.go−88

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/go_freebsd.go−100

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/go_libinit.go−72

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/go_linux.go−100

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/go_netbsd.go−106

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/go_setenv.go−18

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/go_util.go−38

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/iscgo.go−19

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo.go−39

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo_darwin.go−26

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo_freebsd.go−20

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo_linux.go−20

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo_netbsd.go−26

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/netbsd.go−23

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/setenv.go−19

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_386.s−107

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_amd64.s−107

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_arm.s−81

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_arm64.s−84

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_loong64.s−88

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_ppc64le.s−227

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_riscv64.s−72

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/zsymbols.go−165

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/zsymbols_darwin.go−59

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/zsymbols_freebsd.go−48

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/zsymbols_linux.go−48

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/zsymbols_netbsd.go−59

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/ztrampolines_darwin.s−19

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/ztrampolines_freebsd.s−16

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/ztrampolines_linux.s−16

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/ztrampolines_netbsd.s−19

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/fakecgo/ztrampolines_stubs.s−55

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/strings/strings.go−40

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/xreflect/reflect_go124.go−15

This diff is too big to show here.

vendor/github.com/ebitengine/purego/internal/xreflect/reflect_go125.go−12

This diff is too big to show here.

vendor/github.com/ebitengine/purego/is_ios.go−13

This diff is too big to show here.

vendor/github.com/ebitengine/purego/nocgo.go−25

This diff is too big to show here.

vendor/github.com/ebitengine/purego/objc/objc_block_darwin.go−272

This diff is too big to show here.

vendor/github.com/ebitengine/purego/objc/objc_runtime_darwin.go−709

This diff is too big to show here.

vendor/github.com/ebitengine/purego/struct_386.go−41

This diff is too big to show here.

vendor/github.com/ebitengine/purego/struct_amd64.go−286

This diff is too big to show here.

vendor/github.com/ebitengine/purego/struct_arm.go−85

This diff is too big to show here.

vendor/github.com/ebitengine/purego/struct_arm64.go−549

This diff is too big to show here.

vendor/github.com/ebitengine/purego/struct_loong64.go−213

This diff is too big to show here.

vendor/github.com/ebitengine/purego/struct_ppc64le.go−143

This diff is too big to show here.

vendor/github.com/ebitengine/purego/struct_riscv64.go−143

This diff is too big to show here.

vendor/github.com/ebitengine/purego/struct_s390x.go−143

This diff is too big to show here.

vendor/github.com/ebitengine/purego/sys_386.s−147

This diff is too big to show here.

vendor/github.com/ebitengine/purego/sys_amd64.s−170

This diff is too big to show here.

vendor/github.com/ebitengine/purego/sys_arm.s−142

This diff is too big to show here.

vendor/github.com/ebitengine/purego/sys_arm64.s−97

This diff is too big to show here.

vendor/github.com/ebitengine/purego/sys_loong64.s−96

This diff is too big to show here.

vendor/github.com/ebitengine/purego/sys_ppc64le.s−120

This diff is too big to show here.

vendor/github.com/ebitengine/purego/sys_riscv64.s−101

This diff is too big to show here.

vendor/github.com/ebitengine/purego/sys_s390x.s−114

This diff is too big to show here.

vendor/github.com/ebitengine/purego/sys_unix_386.s−226

This diff is too big to show here.

vendor/github.com/ebitengine/purego/sys_unix_arm.s−89

This diff is too big to show here.

vendor/github.com/ebitengine/purego/sys_unix_arm64.s−70

This diff is too big to show here.

vendor/github.com/ebitengine/purego/sys_unix_loong64.s−75

This diff is too big to show here.

vendor/github.com/ebitengine/purego/sys_unix_ppc64le.s−114

This diff is too big to show here.

vendor/github.com/ebitengine/purego/sys_unix_riscv64.s−79

This diff is too big to show here.

vendor/github.com/ebitengine/purego/sys_unix_s390x.s−109

This diff is too big to show here.

vendor/github.com/ebitengine/purego/syscall.go−83

This diff is too big to show here.

vendor/github.com/ebitengine/purego/syscall_32bit.go−109

This diff is too big to show here.

vendor/github.com/ebitengine/purego/syscall_cgo_linux.go−21

This diff is too big to show here.

vendor/github.com/ebitengine/purego/syscall_sysv.go−317

This diff is too big to show here.

vendor/github.com/ebitengine/purego/syscall_sysv_others.go−28

This diff is too big to show here.

vendor/github.com/ebitengine/purego/syscall_sysv_stackargs.go−33

This diff is too big to show here.

vendor/github.com/ebitengine/purego/syscall_windows.go−46

This diff is too big to show here.

vendor/github.com/ebitengine/purego/zcallback_386.s−4014

This diff is too big to show here.

vendor/github.com/ebitengine/purego/zcallback_amd64.s−2014

This diff is too big to show here.

vendor/github.com/ebitengine/purego/zcallback_arm.s−4014

This diff is too big to show here.

vendor/github.com/ebitengine/purego/zcallback_arm64.s−4014

This diff is too big to show here.

vendor/github.com/ebitengine/purego/zcallback_loong64.s−4014

This diff is too big to show here.

vendor/github.com/ebitengine/purego/zcallback_ppc64le.s−4014

This diff is too big to show here.

vendor/github.com/ebitengine/purego/zcallback_riscv64.s−4051

This diff is too big to show here.

vendor/github.com/ebitengine/purego/zcallback_s390x.s−4015

This diff is too big to show here.

vendor/github.com/esiqveland/notify/.gitignore−1

This diff is too big to show here.

vendor/github.com/esiqveland/notify/LICENSE.txt−27

This diff is too big to show here.

vendor/github.com/esiqveland/notify/README.md−54

This diff is too big to show here.

vendor/github.com/esiqveland/notify/doc.go−18

This diff is too big to show here.

vendor/github.com/esiqveland/notify/notification.go−547

This diff is too big to show here.

vendor/github.com/felixge/httpsnoop/.gitignore

This diff is too big to show here.

vendor/github.com/felixge/httpsnoop/LICENSE.txt−19

This diff is too big to show here.

vendor/github.com/felixge/httpsnoop/Makefile−10

This diff is too big to show here.

vendor/github.com/felixge/httpsnoop/README.md−95

This diff is too big to show here.

vendor/github.com/felixge/httpsnoop/capture_metrics.go−121

This diff is too big to show here.

vendor/github.com/felixge/httpsnoop/docs.go−10

This diff is too big to show here.

vendor/github.com/felixge/httpsnoop/wrap_generated.go−16179

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/.cirrus.yml−14

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/.gitignore−10

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/.mailmap−2

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/CHANGELOG.md−602

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/CONTRIBUTING.md−145

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/LICENSE−25

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/README.md−182

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/backend_fen.go−467

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/backend_inotify.go−583

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/backend_kqueue.go−705

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/backend_other.go−22

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/backend_windows.go−680

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/fsnotify.go−496

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/internal/darwin.go−39

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/internal/debug_darwin.go−57

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/internal/debug_dragonfly.go−33

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/internal/debug_freebsd.go−42

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/internal/debug_kqueue.go−32

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/internal/debug_linux.go−56

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/internal/debug_netbsd.go−25

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/internal/debug_openbsd.go−28

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/internal/debug_solaris.go−45

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/internal/debug_windows.go−40

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/internal/freebsd.go−31

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/internal/internal.go−2

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/internal/unix.go−31

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/internal/unix2.go−7

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/internal/windows.go−41

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/shared.go−64

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/staticcheck.conf−3

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/system_bsd.go−7

This diff is too big to show here.

vendor/github.com/fsnotify/fsnotify/system_darwin.go−8

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/LICENSE−23

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/README.md−41

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/alert_darwin.go−8

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/alert_js.go−12

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/alert_unix.go−17

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/alert_unsupported.go−8

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/alert_windows.go−43

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/beeep.go−55

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/beep_darwin.go−32

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/beep_js.go−39

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/beep_unix.go−100

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/beep_unsupported.go−15

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/beep_windows.go−52

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/notify_darwin.go−131

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/notify_js.go−63

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/notify_unix.go−181

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/notify_unix_nodbus.go−78

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/notify_unsupported.go−8

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/notify_windows.go−157

This diff is too big to show here.

vendor/github.com/gen2brain/beeep/notify_windows_compat.go−109

This diff is too big to show here.

vendor/github.com/go-git/gcfg/.gitignore−1

This diff is too big to show here.

vendor/github.com/go-git/gcfg/LICENSE−28

This diff is too big to show here.

vendor/github.com/go-git/gcfg/Makefile−17

This diff is too big to show here.

vendor/github.com/go-git/gcfg/README−4

This diff is too big to show here.

vendor/github.com/go-git/gcfg/doc.go−145

This diff is too big to show here.

vendor/github.com/go-git/gcfg/errors.go−41

This diff is too big to show here.

vendor/github.com/go-git/gcfg/read.go−273

This diff is too big to show here.

vendor/github.com/go-git/gcfg/scanner/errors.go−121

This diff is too big to show here.

vendor/github.com/go-git/gcfg/scanner/scanner.go−334

This diff is too big to show here.

vendor/github.com/go-git/gcfg/set.go−334

This diff is too big to show here.

vendor/github.com/go-git/gcfg/token/position.go−435

This diff is too big to show here.

vendor/github.com/go-git/gcfg/token/serialize.go−56

This diff is too big to show here.

vendor/github.com/go-git/gcfg/token/token.go−83

This diff is too big to show here.

vendor/github.com/go-git/gcfg/types/bool.go−23

This diff is too big to show here.

vendor/github.com/go-git/gcfg/types/doc.go−4

This diff is too big to show here.

vendor/github.com/go-git/gcfg/types/enum.go−44

This diff is too big to show here.

vendor/github.com/go-git/gcfg/types/int.go−86

This diff is too big to show here.

vendor/github.com/go-git/gcfg/types/scan.go−23

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/.gitignore−4

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/LICENSE−201

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/Makefile−18

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/README.md−77

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/fs.go−210

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/helper/chroot/chroot.go−444

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/helper/polyfill/polyfill.go−114

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/memfs/memory.go−428

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/memfs/storage.go−251

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/osfs/os.go−132

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/osfs/os_bound.go−351

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/osfs/os_chroot.go−126

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/osfs/os_js.go−25

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/osfs/os_options.go−3

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/osfs/os_plan9.go−91

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/osfs/os_posix.go−38

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/osfs/os_wasip1.go−34

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/osfs/os_windows.go−58

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/util/glob.go−111

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/util/util.go−282

This diff is too big to show here.

vendor/github.com/go-git/go-billy/v5/util/walk.go−72

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/LICENSE−201

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/config/branch.go−123

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/config/config.go−727

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/config/modules.go−191

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/config/optbool.go−82

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/config/refspec.go−155

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/config/url.go−81

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/internal/path_util/path_util.go−29

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/internal/pathutil/dotgit.go−21

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/internal/pathutil/hfs.go−99

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/internal/pathutil/ntfs.go−187

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/internal/pathutil/tree.go−66

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/internal/url/url.go−72

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/error.go−35

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/format/config/common.go−109

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/format/config/decoder.go−37

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/format/config/doc.go−122

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/format/config/encoder.go−82

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/format/config/format.go−53

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/format/config/option.go−127

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/format/config/section.go−181

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/format/gitignore/dir.go−148

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/format/gitignore/doc.go−70

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/format/gitignore/matcher.go−30

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/format/gitignore/pattern.go−155

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/hash.go−84

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/hash/hash.go−60

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/hash/hash_sha1.go−15

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/hash/hash_sha256.go−15

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/memory.go−72

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/object.go−111

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/reference.go−315

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/plumbing/revision.go−11

This diff is too big to show here.

vendor/github.com/go-git/go-git/v5/utils/ioutil/common.go−210

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/AUTHORS−3

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/CONTRIBUTORS−3

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/LICENSE−27

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/README.md−176

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/alias.go−943

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/api.png

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/arshal.go−569

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/arshal_any.go−289

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/arshal_default.go−2043

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/arshal_embedded.go−230

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/arshal_funcs.go−428

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/arshal_methods.go−378

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/arshal_time.go−795

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/doc.go−247

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/errors.go−465

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/fields.go−656

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/fold.go−58

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/intern.go−88

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/internal/internal.go−42

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/internal/jsonflags/flags.go−223

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/internal/jsonopts/options.go−217

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/internal/jsonopts/options_format.go−124

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/internal/jsonwire/decode.go−606

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/internal/jsonwire/encode.go−290

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/internal/jsonwire/wire.go−209

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/jsontext/alias.go−555

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/jsontext/decode.go−1218

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/jsontext/doc.go−111

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/jsontext/encode.go−993

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/jsontext/errors.go−213

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/jsontext/export.go−91

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/jsontext/options.go−304

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/jsontext/pools.go−164

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/jsontext/quote.go−41

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/jsontext/state.go−828

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/jsontext/token.go−697

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/jsontext/value.go−414

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/migrate.sh−60

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/options.go−290

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/options_format.go−122

This diff is too big to show here.

vendor/github.com/go-json-experiment/json/options_format_global.go−30

This diff is too big to show here.

vendor/github.com/go-logfmt/logfmt/.gitignore−1

This diff is too big to show here.

vendor/github.com/go-logfmt/logfmt/CHANGELOG.md−82

This diff is too big to show here.

vendor/github.com/go-logfmt/logfmt/LICENSE−22

This diff is too big to show here.

vendor/github.com/go-logfmt/logfmt/README.md−41

This diff is too big to show here.

vendor/github.com/go-logfmt/logfmt/decode.go−254

This diff is too big to show here.

vendor/github.com/go-logfmt/logfmt/doc.go−6

This diff is too big to show here.

vendor/github.com/go-logfmt/logfmt/encode.go−322

This diff is too big to show here.

vendor/github.com/go-logfmt/logfmt/jsonstring.go−277

This diff is too big to show here.

vendor/github.com/go-logr/logr/.golangci.yaml−28

This diff is too big to show here.

vendor/github.com/go-logr/logr/CHANGELOG.md−6

This diff is too big to show here.

vendor/github.com/go-logr/logr/CONTRIBUTING.md−17

This diff is too big to show here.

vendor/github.com/go-logr/logr/LICENSE−201

This diff is too big to show here.

vendor/github.com/go-logr/logr/README.md−407

This diff is too big to show here.

vendor/github.com/go-logr/logr/SECURITY.md−18

This diff is too big to show here.

vendor/github.com/go-logr/logr/context.go−33

This diff is too big to show here.

vendor/github.com/go-logr/logr/context_noslog.go−49

This diff is too big to show here.

vendor/github.com/go-logr/logr/context_slog.go−83

This diff is too big to show here.

vendor/github.com/go-logr/logr/discard.go−24

This diff is too big to show here.

vendor/github.com/go-logr/logr/funcr/funcr.go−914

This diff is too big to show here.

vendor/github.com/go-logr/logr/funcr/slogsink.go−105

This diff is too big to show here.

vendor/github.com/go-logr/logr/logr.go−520

This diff is too big to show here.

vendor/github.com/go-logr/logr/sloghandler.go−192

This diff is too big to show here.

vendor/github.com/go-logr/logr/slogr.go−100

This diff is too big to show here.

vendor/github.com/go-logr/logr/slogsink.go−120

This diff is too big to show here.

vendor/github.com/go-logr/stdr/LICENSE−201

This diff is too big to show here.

vendor/github.com/go-logr/stdr/README.md−6

This diff is too big to show here.

vendor/github.com/go-logr/stdr/stdr.go−170

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/.travis.yml−8

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/ChangeLog.md−49

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/LICENSE−21

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/README.md−46

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/SECURITY.md−13

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/appveyor.yml−68

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/com.go−386

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/com_func.go−174

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/connect.go−192

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/constants.go−153

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/error.go−51

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/error_func.go−8

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/error_windows.go−24

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/guid.go−284

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/iconnectionpoint.go−20

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/iconnectionpoint_func.go−21

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/iconnectionpoint_windows.go−43

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/iconnectionpointcontainer.go−17

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/iconnectionpointcontainer_func.go−11

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/iconnectionpointcontainer_windows.go−25

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/idispatch.go−94

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/idispatch_func.go−19

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/idispatch_windows.go−203

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/ienumvariant.go−19

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/ienumvariant_func.go−19

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/ienumvariant_windows.go−63

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/iinspectable.go−18

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/iinspectable_func.go−15

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/iinspectable_windows.go−72

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/iprovideclassinfo.go−21

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/iprovideclassinfo_func.go−7

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/iprovideclassinfo_windows.go−21

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/itypeinfo.go−34

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/itypeinfo_func.go−7

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/itypeinfo_windows.go−21

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/iunknown.go−57

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/iunknown_func.go−19

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/iunknown_windows.go−58

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/ole.go−190

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/safearray.go−27

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/safearray_func.go−211

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/safearray_windows.go−337

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/safearrayconversion.go−140

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/safearrayslices.go−33

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/utility.go−101

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/variables.go−15

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/variant.go−105

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/variant_386.go−11

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/variant_amd64.go−12

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/variant_arm.go−11

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/variant_arm64.go−13

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/variant_date_386.go−22

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/variant_date_amd64.go−20

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/variant_date_arm.go−22

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/variant_date_arm64.go−23

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/variant_ppc64le.go−12

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/variant_s390x.go−12

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/vt_string.go−58

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/winrt.go−99

This diff is too big to show here.

vendor/github.com/go-ole/go-ole/winrt_doc.go−36

This diff is too big to show here.

vendor/github.com/go-openapi/jsonpointer/.editorconfig−26

This diff is too big to show here.

vendor/github.com/go-openapi/jsonpointer/.gitignore−1

This diff is too big to show here.

vendor/github.com/go-openapi/jsonpointer/.travis.yml−15

This diff is too big to show here.

vendor/github.com/go-openapi/jsonpointer/CODE_OF_CONDUCT.md−74

This diff is too big to show here.

vendor/github.com/go-openapi/jsonpointer/LICENSE−202

This diff is too big to show here.

vendor/github.com/go-openapi/jsonpointer/README.md−15

This diff is too big to show here.

vendor/github.com/go-openapi/jsonpointer/pointer.go−390

This diff is too big to show here.

4793 more files changed are not listed.