on main help sign in
Merge branch 'main' into ui
commite671774217927f2d15c0b5335a897b6dadc24f78 authorAyman Bagabas <ayman.bagabas@gmail.com> dateMon 5 Jan 2026, 16:30 (1 year ago) parent909244e56f1b37
12 files changed+300−118
.github/cla-signatures.json+8 .goreleaser.yml+8−1 go.mod+23−22 go.sum+61−48 internal/agent/agent.go+73−28 internal/agent/agentic_fetch_tool.go+1−1 internal/agent/coordinator.go+20−10 internal/cmd/root.go+2−4 internal/event/event.go+1−1 internal/oauth/copilot/client.go+78 internal/skills/skills.go+21−2 internal/tui/page/chat/chat.go+4−1
.github/cla-signatures.json+8
@@ -999,6 +999,14 @@
999999 "created_at": "2025-12-27T06:01:58Z",
10001000 "repoId": 987670088,
10011001 "pullRequestNo": 1723
1002+ },
1003+ {
1004+ "name": "nikolayk812",
1005+ "id": 23297850,
1006+ "comment_id": 3704102069,
1007+ "created_at": "2026-01-01T21:00:07Z",
1008+ "repoId": 987670088,
1009+ "pullRequestNo": 1748
10021010 }
10031011 ]
10041012}
.goreleaser.yml+8−1
@@ -152,7 +152,14 @@ aurs:
152152 conflicts:
153153 - crush
154154 package: |-
155− cd "${srcdir}/crush_${pkgver}_Linux_${CARCH}"
155+ case "$CARCH" in
156+ aarch64)
157+ cd "${srcdir}/crush_${pkgver}_Linux_arm64"
158+ ;;
159+ *)
160+ cd "${srcdir}/crush_${pkgver}_Linux_${CARCH}"
161+ ;;
162+ esac
156163 # bin
157164 install -Dm755 "./crush" "${pkgdir}/usr/bin/crush"
158165 # license
go.mod+23−22
@@ -5,7 +5,7 @@ go 1.25.5
55require (
66 charm.land/bubbles/v2 v2.0.0-rc.1
77 charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251216153312-819e2e89c62e
8− charm.land/fantasy v0.5.5
8+ charm.land/fantasy v0.6.0
99 charm.land/glamour/v2 v2.0.0-20251110203732-69649f93d3b1
1010 charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251205162909-7869489d8971
1111 charm.land/log/v2 v2.0.0-20251110204020-529bb77f35da
@@ -18,7 +18,7 @@ require (
1818 github.com/aymanbagabas/go-udiff v0.3.1
1919 github.com/bmatcuk/doublestar/v4 v4.9.1
2020 github.com/charlievieth/fastwalk v1.0.14
21− github.com/charmbracelet/catwalk v0.11.2
21+ github.com/charmbracelet/catwalk v0.12.0
2222 github.com/charmbracelet/colorprofile v0.4.1
2323 github.com/charmbracelet/fang v0.4.4
2424 github.com/charmbracelet/ultraviolet v0.0.0-20251212194010-b927aa605560
@@ -37,14 +37,14 @@ require (
3737 github.com/invopop/jsonschema v0.13.0
3838 github.com/joho/godotenv v1.5.1
3939 github.com/lucasb-eyer/go-colorful v1.3.0
40− github.com/modelcontextprotocol/go-sdk v1.1.0
40+ github.com/modelcontextprotocol/go-sdk v1.2.0
4141 github.com/muesli/termenv v0.16.0
4242 github.com/ncruces/go-sqlite3 v0.30.4
4343 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
4444 github.com/nxadm/tail v1.4.11
4545 github.com/openai/openai-go/v2 v2.7.1
4646 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
47− github.com/posthog/posthog-go v1.6.13
47+ github.com/posthog/posthog-go v1.8.2
4848 github.com/pressly/goose/v3 v3.26.0
4949 github.com/qjebbs/go-jsons v1.0.0-alpha.4
5050 github.com/rivo/uniseg v0.4.7
@@ -69,26 +69,27 @@ require (
6969
7070require (
7171 cloud.google.com/go v0.116.0 // indirect
72− cloud.google.com/go/auth v0.17.0 // indirect
72+ cloud.google.com/go/auth v0.18.0 // indirect
7373 cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
74− cloud.google.com/go/compute/metadata v0.8.0 // indirect
74+ cloud.google.com/go/compute/metadata v0.9.0 // indirect
7575 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
7676 github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
7777 github.com/RealAlexandreAI/json-repair v0.0.14 // indirect
7878 github.com/andybalholm/cascadia v1.3.3 // indirect
7979 github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect
8080 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
81− github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
82− github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
83− github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
84− github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
85− github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
86− github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
87− github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
88− github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
89− github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
90− github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
91− github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
81+ github.com/aws/aws-sdk-go-v2/config v1.32.6 // indirect
82+ github.com/aws/aws-sdk-go-v2/credentials v1.19.6 // indirect
83+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
84+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
85+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
86+ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
87+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
88+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
89+ github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
90+ github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
91+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
92+ github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
9293 github.com/aws/smithy-go v1.24.0 // indirect
9394 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
9495 github.com/aymerick/douceur v0.2.0 // indirect
@@ -115,7 +116,7 @@ require (
115116 github.com/google/go-cmp v0.7.0 // indirect
116117 github.com/google/jsonschema-go v0.3.0 // indirect
117118 github.com/google/s2a-go v0.1.9 // indirect
118− github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
119+ github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
119120 github.com/googleapis/gax-go/v2 v2.15.0 // indirect
120121 github.com/gorilla/css v1.0.1 // indirect
121122 github.com/gorilla/websocket v1.5.3 // indirect
@@ -170,11 +171,11 @@ require (
170171 golang.org/x/oauth2 v0.34.0 // indirect
171172 golang.org/x/sys v0.39.0 // indirect
172173 golang.org/x/term v0.38.0 // indirect
173− golang.org/x/time v0.12.0 // indirect
174+ golang.org/x/time v0.14.0 // indirect
174175 google.golang.org/api v0.239.0 // indirect
175− google.golang.org/genai v1.39.0 // indirect
176− google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
177− google.golang.org/grpc v1.74.2 // indirect
176+ google.golang.org/genai v1.40.0 // indirect
177+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
178+ google.golang.org/grpc v1.76.0 // indirect
178179 google.golang.org/protobuf v1.36.10 // indirect
179180 gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20251110073552-01de4eb40290 // indirect
180181 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
go.sum+61−48
@@ -2,8 +2,8 @@ charm.land/bubbles/v2 v2.0.0-rc.1 h1:EiIFVAc3Zi/yY86td+79mPhHR7AqZ1OxF+6ztpOCRaM
22charm.land/bubbles/v2 v2.0.0-rc.1/go.mod h1:5AbN6cEd/47gkEf8TgiQ2O3RZ5QxMS14l9W+7F9fPC4=
33charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251216153312-819e2e89c62e h1:tXwTmgGpwZT7ParKF5xbEQBVjM2e1uKhKi/GpfU3mYQ=
44charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251216153312-819e2e89c62e/go.mod h1:pDM18flq3Z4njKZPA3zCvyVSSIJbMcoqlE82BdGUtL8=
5−charm.land/fantasy v0.5.5 h1:Dw/NBLH9HLX/ouCz604RXGD7BYzr0lT56/B4ylMGZjg=
6−charm.land/fantasy v0.5.5/go.mod h1:QyJLJGissYdBifvitgAxFcYhNACSr0G1faC75CIESUk=
5+charm.land/fantasy v0.6.0 h1:0PZfZ/w6c70UdlumGGFW6s9zTV6f4xAV/bXo6vGuZsc=
6+charm.land/fantasy v0.6.0/go.mod h1:hUyklhBbCtnVeMAWGXHbMD4A+5B8dHbYHGZDfOYpzzw=
77charm.land/glamour/v2 v2.0.0-20251110203732-69649f93d3b1 h1:9q4+yyU7105T3OrOx0csMyKnw89yMSijJ+rVld/Z2ek=
88charm.land/glamour/v2 v2.0.0-20251110203732-69649f93d3b1/go.mod h1:J3kVhY6oHXZq5f+8vC3hmDO95fEvbqj3z7xDwxrfzU8=
99charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251205162909-7869489d8971 h1:xZFcNsJMiIDbFtWRyDmkKNk1sjojfaom4Zoe0cyH/8c=
@@ -14,12 +14,12 @@ charm.land/x/vcr v0.1.1 h1:PXCFMUG0rPtyk35rhfzYCJEduOzWXCIbrXTFq4OF/9Q=
1414charm.land/x/vcr v0.1.1/go.mod h1:eByq2gqzWvcct/8XE2XO5KznoWEBiXH56+y2gphbltM=
1515cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
1616cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
17−cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
18−cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
17+cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
18+cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
1919cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
2020cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
21−cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
22−cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
21+cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
22+cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
2323github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
2424github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
2525github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
@@ -52,28 +52,30 @@ github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgP
5252github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
5353github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
5454github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
55−github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
56−github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
57−github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
58−github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
59−github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
60−github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
61−github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
62−github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
63−github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
64−github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
65−github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
66−github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
67−github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
68−github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
69−github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
70−github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
71−github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
72−github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
73−github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
74−github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
75−github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
76−github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
55+github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
56+github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
57+github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
58+github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
59+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
60+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
61+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
62+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
63+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
64+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
65+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
66+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
67+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
68+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
69+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
70+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
71+github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
72+github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
73+github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
74+github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
75+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
76+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
77+github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
78+github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
7779github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
7880github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
7981github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
@@ -92,8 +94,8 @@ github.com/charlievieth/fastwalk v1.0.14 h1:3Eh5uaFGwHZd8EGwTjJnSpBkfwfsak9h6ICg
9294github.com/charlievieth/fastwalk v1.0.14/go.mod h1:diVcUreiU1aQ4/Wu3NbxxH4/KYdKpLDojrQ1Bb2KgNY=
9395github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251024181547-21d6f3d9a904 h1:rwLdEpG9wE6kL69KkEKDiWprO8pQOZHZXeod6+9K+mw=
9496github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251024181547-21d6f3d9a904/go.mod h1:8TIYxZxsuCqqeJ0lga/b91tBwrbjoHDC66Sq5t8N2R4=
95−github.com/charmbracelet/catwalk v0.11.2 h1:m+eE7yv/uIrKW95FpFeGDMFrAugotylX89XzpkZwlLk=
96−github.com/charmbracelet/catwalk v0.11.2/go.mod h1:qg+Yl9oaZTkTvRscqbxfttzOFQ4v0pOT5XwC7b5O0NQ=
97+github.com/charmbracelet/catwalk v0.12.0 h1:CCxbZpgMPyZNtnaRGvL//BgPkvOWOYVFhRf925Dfrdg=
98+github.com/charmbracelet/catwalk v0.12.0/go.mod h1:qg+Yl9oaZTkTvRscqbxfttzOFQ4v0pOT5XwC7b5O0NQ=
9799github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
98100github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
99101github.com/charmbracelet/fang v0.4.4 h1:G4qKxF6or/eTPgmAolwPuRNyuci3hTUGGX1rj1YkHJY=
@@ -128,6 +130,8 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa
128130github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
129131github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
130132github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
133+github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
134+github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
131135github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
132136github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
133137github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
@@ -144,6 +148,11 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
144148github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
145149github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
146150github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
151+github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
152+github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
153+github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
154+github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
155+github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
147156github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
148157github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
149158github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
@@ -177,8 +186,8 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
177186github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
178187github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
179188github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
180−github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
181−github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
189+github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
190+github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
182191github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
183192github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
184193github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
@@ -234,8 +243,8 @@ github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwX
234243github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
235244github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
236245github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
237−github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA=
238−github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
246+github.com/modelcontextprotocol/go-sdk v1.2.0 h1:Y23co09300CEk8iZ/tMxIX1dVmKZkzoSBZOpJwUnc/s=
247+github.com/modelcontextprotocol/go-sdk v1.2.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
239248github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
240249github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
241250github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI=
@@ -267,10 +276,12 @@ github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFu
267276github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
268277github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
269278github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
279+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
280+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
270281github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
271282github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
272−github.com/posthog/posthog-go v1.6.13 h1:4t9j0VOIJBgITm4v5rLsLy3IKUkU9dn2VusMNzZXScw=
273−github.com/posthog/posthog-go v1.6.13/go.mod h1:LcC1Nu4AgvV22EndTtrMXTy+7RGVC0MhChSw7Qk5XkY=
283+github.com/posthog/posthog-go v1.8.2 h1:v/ajsM8lq+2Z3OlQbTVWqiHI+hyh9Cd4uiQt1wFlehE=
284+github.com/posthog/posthog-go v1.8.2/go.mod h1:ueZiJCmHezyDHI/swIR1RmOfktLehnahJnFxEvQ9mnQ=
274285github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM=
275286github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
276287github.com/qjebbs/go-jsons v1.0.0-alpha.4 h1:Qsb4ohRUHQODIUAsJKdKJ/SIDbsO7oGOzsfy+h1yQZs=
@@ -352,10 +363,10 @@ go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
352363go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
353364go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
354365go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
355−go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
356−go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
357−go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
358−go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
366+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
367+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
368+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
369+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
359370go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
360371go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
361372go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -449,8 +460,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
449460golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
450461golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
451462golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
452−golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
453−golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
463+golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
464+golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
454465golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
455466golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
456467golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@@ -460,14 +471,16 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
460471golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
461472golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
462473golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
474+gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
475+gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
463476google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo=
464477google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
465−google.golang.org/genai v1.39.0 h1:80I1sYFGROliWNxEgPWDklNYVO8xq/bNvw70BFh6XmA=
466−google.golang.org/genai v1.39.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
467−google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
468−google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
469−google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
470−google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
478+google.golang.org/genai v1.40.0 h1:kYxyQSH+vsib8dvsgyLJzsVEIv5k3ZmHJyVqdvGncmc=
479+google.golang.org/genai v1.40.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
480+google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
481+google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
482+google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
483+google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
471484google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
472485google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
473486gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
internal/agent/agent.go+73−28
@@ -16,6 +16,7 @@ import (
1616 "fmt"
1717 "log/slog"
1818 "os"
19+ "regexp"
1920 "strconv"
2021 "strings"
2122 "sync"
@@ -39,12 +40,17 @@ import (
3940 "github.com/charmbracelet/crush/internal/stringext"
4041)
4142
43+const defaultSessionName = "Untitled Session"
44+
4245//go:embed templates/title.md
4346var titlePrompt []byte
4447
4548//go:embed templates/summary.md
4649var summaryPrompt []byte
4750
51+// Used to remove <think> tags from generated titles.
52+var thinkTagRegex = regexp.MustCompile(`<think>.*?</think>`)
53+
4854type SessionAgentCall struct {
4955 SessionID string
5056 Prompt string
@@ -466,7 +472,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
466472 currentAssistant.AddFinish(
467473 message.FinishReasonError,
468474 "Copilot model not enabled",
469− fmt.Sprintf("%q is not enabled in Copilot. Go to the following page to enable it. Then, wait a minute before trying again. %s", a.largeModel.CatwalkCfg.Name, link),
475+ fmt.Sprintf("%q is not enabled in Copilot. Go to the following page to enable it. Then, wait 5 minutes before trying again. %s", a.largeModel.CatwalkCfg.Name, link),
470476 )
471477 } else {
472478 currentAssistant.AddFinish(message.FinishReasonError, cmp.Or(stringext.Capitalize(providerErr.Title), defaultTitle), providerErr.Message)
@@ -725,49 +731,87 @@ func (a *sessionAgent) getSessionMessages(ctx context.Context, session session.S
725731 return msgs, nil
726732}
727733
728−func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, prompt string) {
729− if prompt == "" {
734+// generateTitle generates a session titled based on the initial prompt.
735+func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, userPrompt string) {
736+ if userPrompt == "" {
730737 return
731738 }
732739
733− var maxOutput int64 = 40
740+ var maxOutputTokens int64 = 40
734741 if a.smallModel.CatwalkCfg.CanReason {
735− maxOutput = a.smallModel.CatwalkCfg.DefaultMaxTokens
742+ maxOutputTokens = a.smallModel.CatwalkCfg.DefaultMaxTokens
736743 }
737744
738− agent := fantasy.NewAgent(a.smallModel.Model,
739− fantasy.WithSystemPrompt(string(titlePrompt)+"\n /no_think"),
740− fantasy.WithMaxOutputTokens(maxOutput),
741− )
745+ newAgent := func(m fantasy.LanguageModel, p []byte, tok int64) fantasy.Agent {
746+ return fantasy.NewAgent(m,
747+ fantasy.WithSystemPrompt(string(p)+"\n /no_think"),
748+ fantasy.WithMaxOutputTokens(tok),
749+ )
750+ }
742751
743− resp, err := agent.Stream(ctx, fantasy.AgentStreamCall{
744− Prompt: fmt.Sprintf("Generate a concise title for the following content:\n\n%s\n <think>\n\n</think>", prompt),
745− PrepareStep: func(callContext context.Context, options fantasy.PrepareStepFunctionOptions) (_ context.Context, prepared fantasy.PrepareStepResult, err error) {
746− prepared.Messages = options.Messages
752+ streamCall := fantasy.AgentStreamCall{
753+ Prompt: fmt.Sprintf("Generate a concise title for the following content:\n\n%s\n <think>\n\n</think>", userPrompt),
754+ PrepareStep: func(callCtx context.Context, opts fantasy.PrepareStepFunctionOptions) (_ context.Context, prepared fantasy.PrepareStepResult, err error) {
755+ prepared.Messages = opts.Messages
747756 if a.systemPromptPrefix != "" {
748− prepared.Messages = append([]fantasy.Message{fantasy.NewSystemMessage(a.systemPromptPrefix)}, prepared.Messages...)
757+ prepared.Messages = append([]fantasy.Message{
758+ fantasy.NewSystemMessage(a.systemPromptPrefix),
759+ }, prepared.Messages...)
749760 }
750− return callContext, prepared, nil
761+ return callCtx, prepared, nil
751762 },
752− })
753− if err != nil {
754− slog.Error("error generating title", "err", err)
755− return
756763 }
757764
758− title := resp.Response.Content.Text()
765+ // Use the small model to generate the title.
766+ model := &a.smallModel
767+ agent := newAgent(model.Model, titlePrompt, maxOutputTokens)
768+ resp, err := agent.Stream(ctx, streamCall)
769+ if err == nil {
770+ // We successfully generated a title with the small model.
771+ slog.Info("generated title with small model")
772+ } else {
773+ // It didn't work. Let's try with the big model.
774+ slog.Error("error generating title with small model; trying big model", "err", err)
775+ model = &a.largeModel
776+ agent = newAgent(model.Model, titlePrompt, maxOutputTokens)
777+ resp, err = agent.Stream(ctx, streamCall)
778+ if err == nil {
779+ slog.Info("generated title with large model")
780+ } else {
781+ // Welp, the large model didn't work either. Use the default
782+ // session name and return.
783+ slog.Error("error generating title with large model", "err", err)
784+ saveErr := a.sessions.UpdateTitleAndUsage(ctx, sessionID, defaultSessionName, 0, 0, 0)
785+ if saveErr != nil {
786+ slog.Error("failed to save session title and usage", "error", saveErr)
787+ }
788+ return
789+ }
790+ }
791+
792+ if resp == nil {
793+ // Actually, we didn't get a response so we can't. Use the default
794+ // session name and return.
795+ slog.Error("response is nil; can't generate title")
796+ saveErr := a.sessions.UpdateTitleAndUsage(ctx, sessionID, defaultSessionName, 0, 0, 0)
797+ if saveErr != nil {
798+ slog.Error("failed to save session title and usage", "error", saveErr)
799+ }
800+ return
801+ }
759802
760− title = strings.ReplaceAll(title, "\n", " ")
803+ // Clean up title.
804+ var title string
805+ title = strings.ReplaceAll(resp.Response.Content.Text(), "\n", " ")
806+ slog.Info("generated title", "title", title)
761807
762808 // Remove thinking tags if present.
763− if idx := strings.Index(title, "</think>"); idx > 0 {
764− title = title[idx+len("</think>"):]
765− }
809+ title = thinkTagRegex.ReplaceAllString(title, "")
766810
767811 title = strings.TrimSpace(title)
768812 if title == "" {
769− slog.Warn("failed to generate title", "warn", "empty title")
770− return
813+ slog.Warn("empty title; using fallback")
814+ title = defaultSessionName
771815 }
772816
773817 // Calculate usage and cost.
@@ -783,7 +827,7 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, prom
783827 }
784828 }
785829
786− modelConfig := a.smallModel.CatwalkCfg
830+ modelConfig := model.CatwalkCfg
787831 cost := modelConfig.CostPer1MInCached/1e6*float64(resp.TotalUsage.CacheCreationTokens) +
788832 modelConfig.CostPer1MOutCached/1e6*float64(resp.TotalUsage.CacheReadTokens) +
789833 modelConfig.CostPer1MIn/1e6*float64(resp.TotalUsage.InputTokens) +
@@ -805,7 +849,7 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, prom
805849 // concurrent session updates.
806850 saveErr := a.sessions.UpdateTitleAndUsage(ctx, sessionID, title, promptTokens, completionTokens, cost)
807851 if saveErr != nil {
808− slog.Error("failed to save session title & usage", "error", saveErr)
852+ slog.Error("failed to save session title and usage", "error", saveErr)
809853 return
810854 }
811855}
@@ -947,6 +991,7 @@ func (a *sessionAgent) promptPrefix() string {
947991 return a.systemPromptPrefix
948992}
949993
994+// XXX: this should be generalized to cover other subscription plans, like Copilot.
950995func (a *sessionAgent) isClaudeCode() bool {
951996 cfg := config.Get()
952997 pc, ok := cfg.Providers.Get(a.largeModel.ModelCfg.Provider)
internal/agent/agentic_fetch_tool.go+1−1
@@ -143,7 +143,7 @@ func (c *coordinator) agenticFetchTool(_ context.Context, client *http.Client) (
143143 return fantasy.ToolResponse{}, fmt.Errorf("error creating prompt: %s", err)
144144 }
145145
146− _, small, err := c.buildAgentModels(ctx)
146+ _, small, err := c.buildAgentModels(ctx, true)
147147 if err != nil {
148148 return fantasy.ToolResponse{}, fmt.Errorf("error building models: %s", err)
149149 }
internal/agent/coordinator.go+20−10
@@ -26,6 +26,7 @@ import (
2626 "github.com/charmbracelet/crush/internal/log"
2727 "github.com/charmbracelet/crush/internal/lsp"
2828 "github.com/charmbracelet/crush/internal/message"
29+ "github.com/charmbracelet/crush/internal/oauth/copilot"
2930 "github.com/charmbracelet/crush/internal/permission"
3031 "github.com/charmbracelet/crush/internal/session"
3132 "golang.org/x/sync/errgroup"
@@ -316,7 +317,7 @@ func mergeCallOptions(model Model, cfg config.ProviderConfig) (fantasy.ProviderO
316317}
317318
318319func (c *coordinator) buildAgent(ctx context.Context, prompt *prompt.Prompt, agent config.Agent, isSubAgent bool) (SessionAgent, error) {
319− large, small, err := c.buildAgentModels(ctx)
320+ large, small, err := c.buildAgentModels(ctx, isSubAgent)
320321 if err != nil {
321322 return nil, err
322323 }
@@ -440,7 +441,7 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fan
440441}
441442
442443// TODO: when we support multiple agents we need to change this so that we pass in the agent specific model config
443−func (c *coordinator) buildAgentModels(ctx context.Context) (Model, Model, error) {
444+func (c *coordinator) buildAgentModels(ctx context.Context, isSubAgent bool) (Model, Model, error) {
444445 largeModelCfg, ok := c.cfg.Models[config.SelectedModelTypeLarge]
445446 if !ok {
446447 return Model{}, Model{}, errors.New("large model not selected")
@@ -455,7 +456,7 @@ func (c *coordinator) buildAgentModels(ctx context.Context) (Model, Model, error
455456 return Model{}, Model{}, errors.New("large model provider not configured")
456457 }
457458
458− largeProvider, err := c.buildProvider(largeProviderCfg, largeModelCfg)
459+ largeProvider, err := c.buildProvider(largeProviderCfg, largeModelCfg, isSubAgent)
459460 if err != nil {
460461 return Model{}, Model{}, err
461462 }
@@ -465,7 +466,7 @@ func (c *coordinator) buildAgentModels(ctx context.Context) (Model, Model, error
465466 return Model{}, Model{}, errors.New("large model provider not configured")
466467 }
467468
468− smallProvider, err := c.buildProvider(smallProviderCfg, largeModelCfg)
469+ smallProvider, err := c.buildProvider(smallProviderCfg, largeModelCfg, true)
469470 if err != nil {
470471 return Model{}, Model{}, err
471472 }
@@ -582,15 +583,24 @@ func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[stri
582583 return openrouter.New(opts...)
583584}
584585
585−func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string, extraBody map[string]any) (fantasy.Provider, error) {
586+func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string, extraBody map[string]any, providerID string, isSubAgent bool) (fantasy.Provider, error) {
586587 opts := []openaicompat.Option{
587588 openaicompat.WithBaseURL(baseURL),
588589 openaicompat.WithAPIKey(apiKey),
589590 }
590− if c.cfg.Options.Debug {
591− httpClient := log.NewHTTPClient()
591+
592+ // Set HTTP client based on provider and debug mode.
593+ var httpClient *http.Client
594+ if providerID == string(catwalk.InferenceProviderCopilot) {
595+ opts = append(opts, openaicompat.WithUseResponsesAPI())
596+ httpClient = copilot.NewClient(isSubAgent, c.cfg.Options.Debug)
597+ } else if c.cfg.Options.Debug {
598+ httpClient = log.NewHTTPClient()
599+ }
600+ if httpClient != nil {
592601 opts = append(opts, openaicompat.WithHTTPClient(httpClient))
593602 }
603+
594604 if len(headers) > 0 {
595605 opts = append(opts, openaicompat.WithHeaders(headers))
596606 }
@@ -705,7 +715,7 @@ func (c *coordinator) isAnthropicThinking(model config.SelectedModel) bool {
705715 return false
706716}
707717
708−func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel) (fantasy.Provider, error) {
718+func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel, isSubAgent bool) (fantasy.Provider, error) {
709719 headers := maps.Clone(providerCfg.ExtraHeaders)
710720 if headers == nil {
711721 headers = make(map[string]string)
@@ -745,7 +755,7 @@ func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model con
745755 }
746756 providerCfg.ExtraBody["tool_stream"] = true
747757 }
748− return c.buildOpenaiCompatProvider(baseURL, apiKey, headers, providerCfg.ExtraBody)
758+ return c.buildOpenaiCompatProvider(baseURL, apiKey, headers, providerCfg.ExtraBody, providerCfg.ID, isSubAgent)
749759 case hyper.Name:
750760 return c.buildHyperProvider(baseURL, apiKey)
751761 default:
@@ -790,7 +800,7 @@ func (c *coordinator) Model() Model {
790800
791801func (c *coordinator) UpdateModels(ctx context.Context) error {
792802 // build the models again so we make sure we get the latest config
793− large, small, err := c.buildAgentModels(ctx)
803+ large, small, err := c.buildAgentModels(ctx, false)
794804 if err != nil {
795805 return err
796806 }
internal/cmd/root.go+2−4
@@ -53,10 +53,8 @@ func init() {
5353
5454var rootCmd = &cobra.Command{
5555 Use: "crush",
56− Short: "Terminal-based AI assistant for software development",
57− Long: `Crush is a powerful terminal-based AI assistant that helps with software development tasks.
58−It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
59−to assist developers in writing, debugging, and understanding code directly from the terminal.`,
56+ Short: "An AI assistant for software development",
57+ Long: "An AI assistant for software development and similar tasks with direct access to the terminal",
6058 Example: `
6159# Run in interactive mode
6260crush
internal/event/event.go+1−1
@@ -31,7 +31,7 @@ var (
3131)
3232
3333func SetInteractive(interactive bool) {
34− baseProps = baseProps.Set("interactive", interactive)
34+ baseProps = baseProps.Set("Interactive", interactive)
3535}
3636
3737func Init() {
internal/oauth/copilot/client.gonew file+78
@@ -0,0 +1,78 @@
1+// Package copilot provides GitHub Copilot integration.
2+package copilot
3+
4+import (
5+ "bytes"
6+ "fmt"
7+ "io"
8+ "log/slog"
9+ "net/http"
10+ "regexp"
11+
12+ "github.com/charmbracelet/crush/internal/log"
13+)
14+
15+// NewClient creates a new HTTP client with a custom transport that adds the
16+// X-Initiator header based on message history in the request body.
17+func NewClient(isSubAgent, debug bool) *http.Client {
18+ return &http.Client{
19+ Transport: &initiatorTransport{debug: debug, isSubAgent: isSubAgent},
20+ }
21+}
22+
23+type initiatorTransport struct {
24+ debug bool
25+ isSubAgent bool
26+}
27+
28+func (t *initiatorTransport) RoundTrip(req *http.Request) (*http.Response, error) {
29+ const (
30+ xInitiatorHeader = "X-Initiator"
31+ userInitiator = "user"
32+ agentInitiator = "agent"
33+ )
34+
35+ if req == nil {
36+ return nil, fmt.Errorf("HTTP request is nil")
37+ }
38+ if req.Body == http.NoBody {
39+ // No body to inspect; default to user.
40+ req.Header.Set(xInitiatorHeader, userInitiator)
41+ slog.Debug("Setting X-Initiator header to user (no request body)")
42+ return t.roundTrip(req)
43+ }
44+
45+ // Clone request to avoid modifying the original.
46+ req = req.Clone(req.Context())
47+
48+ // Read the original body into bytes so we can examine it.
49+ bodyBytes, err := io.ReadAll(req.Body)
50+ if err != nil {
51+ return nil, fmt.Errorf("failed to read request body: %w", err)
52+ }
53+ defer req.Body.Close()
54+
55+ // Restore the original body using the preserved bytes.
56+ req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
57+
58+ // Check for assistant messages using regex to handle whitespace
59+ // variations in the JSON while avoiding full unmarshalling overhead.
60+ initiator := userInitiator
61+ assistantRolePattern := regexp.MustCompile(`"role"\s*:\s*"assistant"`)
62+ if assistantRolePattern.Match(bodyBytes) || t.isSubAgent {
63+ slog.Debug("Setting X-Initiator header to agent (found assistant messages in history)")
64+ initiator = agentInitiator
65+ } else {
66+ slog.Debug("Setting X-Initiator header to user (no assistant messages)")
67+ }
68+ req.Header.Set(xInitiatorHeader, initiator)
69+
70+ return t.roundTrip(req)
71+}
72+
73+func (t *initiatorTransport) roundTrip(req *http.Request) (*http.Response, error) {
74+ if t.debug {
75+ return log.NewHTTPClient().Transport.RoundTrip(req)
76+ }
77+ return http.DefaultTransport.RoundTrip(req)
78+}
internal/skills/skills.go+21−2
@@ -10,7 +10,9 @@ import (
1010 "path/filepath"
1111 "regexp"
1212 "strings"
13+ "sync"
1314
15+ "github.com/charlievieth/fastwalk"
1416 "gopkg.in/yaml.v3"
1517)
1618
@@ -110,17 +112,32 @@ func splitFrontmatter(content string) (frontmatter, body string, err error) {
110112// Discover finds all valid skills in the given paths.
111113func Discover(paths []string) []*Skill {
112114 var skills []*Skill
115+ var mu sync.Mutex
113116 seen := make(map[string]bool)
114117
115118 for _, base := range paths {
116− filepath.WalkDir(base, func(path string, d os.DirEntry, err error) error {
119+ // We use fastwalk with Follow: true instead of filepath.WalkDir because
120+ // WalkDir doesn't follow symlinked directories at any depth—only entry
121+ // points. This ensures skills in symlinked subdirectories are discovered.
122+ // fastwalk is concurrent, so we protect shared state (seen, skills) with mu.
123+ conf := fastwalk.Config{
124+ Follow: true,
125+ ToSlash: fastwalk.DefaultToSlash(),
126+ }
127+ fastwalk.Walk(&conf, base, func(path string, d os.DirEntry, err error) error {
117128 if err != nil {
118129 return nil
119130 }
120− if d.IsDir() || d.Name() != SkillFileName || seen[path] {
131+ if d.IsDir() || d.Name() != SkillFileName {
132+ return nil
133+ }
134+ mu.Lock()
135+ if seen[path] {
136+ mu.Unlock()
121137 return nil
122138 }
123139 seen[path] = true
140+ mu.Unlock()
124141 skill, err := Parse(path)
125142 if err != nil {
126143 slog.Warn("Failed to parse skill file", "path", path, "error", err)
@@ -131,7 +148,9 @@ func Discover(paths []string) []*Skill {
131148 return nil
132149 }
133150 slog.Info("Successfully loaded skill", "name", skill.Name, "path", path)
151+ mu.Lock()
134152 skills = append(skills, skill)
153+ mu.Unlock()
135154 return nil
136155 })
137156 }
internal/tui/page/chat/chat.go+4−1
@@ -961,7 +961,10 @@ func (p *chatPage) sendMessage(text string, attachments []message.Attachment) te
961961 session := p.session
962962 var cmds []tea.Cmd
963963 if p.session.ID == "" {
964− newSession, err := p.app.Sessions.Create(context.Background(), "New Session")
964+ // XXX: The second argument here is the session name, which we leave
965+ // blank as it will be auto-generated. Ideally, we remove the need for
966+ // that argument entirely.
967+ newSession, err := p.app.Sessions.Create(context.Background(), "")
965968 if err != nil {
966969 return util.ReportError(err)
967970 }