feat: add swagger api docs generation

Ayman Bagabas created

Change summary

Taskfile.yaml              |  13 +
go.mod                     |  16 +
go.sum                     |  28 ++
internal/proto/requests.go |  92 ++++++++
internal/server/config.go  | 257 ++++++++++++++++++-----
internal/server/proto.go   | 430 +++++++++++++++++++++++++++++++++++++++
internal/server/server.go  |   3 
main.go                    |  10 
8 files changed, 781 insertions(+), 68 deletions(-)

Detailed changes

Taskfile.yaml 🔗

@@ -185,6 +185,19 @@ tasks:
       - go get charm.land/catwalk
       - go mod tidy
 
+  swag:
+    desc: Generate OpenAPI spec from swag annotations
+    cmds:
+      - go run github.com/swaggo/swag/cmd/swag@v1.16.6 init --generalInfo main.go --dir . --output docs --parseDependency --parseInternal --parseDepth 5
+    sources:
+      - internal/server/*.go
+      - internal/proto/*.go
+      - main.go
+    generates:
+      - docs/docs.go
+      - docs/swagger.json
+      - docs/swagger.yaml
+
   sqlc:
     desc: Generate code using SQLC
     cmds:

go.mod 🔗

@@ -14,6 +14,7 @@ require (
 	charm.land/x/vcr v0.1.1
 	github.com/JohannesKaufmann/html-to-markdown v1.6.0
 	github.com/MakeNowJust/heredoc v1.0.0
+	github.com/Microsoft/go-winio v0.6.2
 	github.com/PuerkitoBio/goquery v1.12.0
 	github.com/alecthomas/chroma/v2 v2.23.1
 	github.com/atotto/clipboard v0.1.4
@@ -59,12 +60,15 @@ require (
 	github.com/sourcegraph/jsonrpc2 v0.2.1
 	github.com/spf13/cobra v1.10.2
 	github.com/stretchr/testify v1.11.1
+	github.com/swaggo/http-swagger/v2 v2.0.2
+	github.com/swaggo/swag v1.16.6
 	github.com/tidwall/gjson v1.18.0
 	github.com/tidwall/sjson v1.2.5
 	github.com/zeebo/xxh3 v1.1.0
 	go.uber.org/goleak v1.3.0
 	golang.org/x/net v0.52.0
 	golang.org/x/sync v0.20.0
+	golang.org/x/sys v0.42.0
 	golang.org/x/text v0.35.0
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 	gopkg.in/yaml.v3 v3.0.1
@@ -81,7 +85,7 @@ require (
 	git.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
-	github.com/Microsoft/go-winio v0.6.2 // indirect
+	github.com/KyleBanks/depth v1.2.1 // indirect
 	github.com/andybalholm/cascadia v1.3.3 // indirect
 	github.com/aws/aws-sdk-go-v2 v1.41.3 // indirect
 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
@@ -119,6 +123,10 @@ require (
 	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-ole/go-ole v1.3.0 // indirect
+	github.com/go-openapi/jsonpointer v0.19.5 // indirect
+	github.com/go-openapi/jsonreference v0.20.0 // indirect
+	github.com/go-openapi/spec v0.20.6 // indirect
+	github.com/go-openapi/swag v0.19.15 // indirect
 	github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
 	github.com/goccy/go-json v0.10.5 // indirect
 	github.com/goccy/go-yaml v1.19.2 // indirect
@@ -135,6 +143,7 @@ require (
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jackmordaunt/icns/v3 v3.0.1 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
+	github.com/josharian/intern v1.0.0 // indirect
 	github.com/kaptinlin/go-i18n v0.2.12 // indirect
 	github.com/kaptinlin/jsonpointer v0.4.17 // indirect
 	github.com/kaptinlin/jsonschema v0.7.5 // indirect
@@ -164,6 +173,7 @@ require (
 	github.com/sergeymakinen/go-ico v1.0.0-beta.0 // indirect
 	github.com/sethvargo/go-retry v0.3.0 // indirect
 	github.com/spf13/pflag v1.0.9 // indirect
+	github.com/swaggo/files/v2 v2.0.0 // indirect
 	github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
 	github.com/tetratelabs/wazero v1.11.0 // indirect
 	github.com/tidwall/match v1.1.1 // indirect
@@ -186,10 +196,11 @@ require (
 	golang.org/x/crypto v0.49.0 // indirect
 	golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
 	golang.org/x/image v0.36.0 // indirect
+	golang.org/x/mod v0.33.0 // indirect
 	golang.org/x/oauth2 v0.36.0 // indirect
-	golang.org/x/sys v0.42.0 // indirect
 	golang.org/x/term v0.41.0 // indirect
 	golang.org/x/time v0.14.0 // indirect
+	golang.org/x/tools v0.42.0 // indirect
 	google.golang.org/api v0.269.0 // indirect
 	google.golang.org/genai v1.49.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
@@ -198,6 +209,7 @@ require (
 	gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20251110073552-01de4eb40290 // indirect
 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
 	modernc.org/libc v1.68.0 // indirect
 	modernc.org/mathutil v1.7.1 // indirect
 	modernc.org/memory v1.11.0 // indirect

go.sum 🔗

@@ -36,6 +36,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mx
 github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/JohannesKaufmann/html-to-markdown v1.6.0 h1:04VXMiE50YYfCfLboJCLcgqF5x+rHJnb1ssNmqpLH/k=
 github.com/JohannesKaufmann/html-to-markdown v1.6.0/go.mod h1:NUI78lGg/a7vpEJTz/0uOcYMaibytE4BUOQS8k78yPQ=
+github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
+github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
 github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
 github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
@@ -139,6 +141,7 @@ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJ
 github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
 github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
 github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -188,6 +191,16 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
 github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
+github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
+github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
+github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
+github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
+github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
 github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
 github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
 github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
@@ -241,6 +254,7 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
 github.com/jordanella/go-ansi-paintbrush v0.0.0-20240728195301-b7ad996ecf3d h1:on25kP+Sx7sxUMRQiA8gdcToAGet4DK/EIA30mXre+4=
 github.com/jordanella/go-ansi-paintbrush v0.0.0-20240728195301-b7ad996ecf3d/go.mod h1:SV0W0APWP9MZ1/gfDQ/NzzTlWdIgYZ/ZbpN4d/UXRYw=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/kaptinlin/go-i18n v0.2.12 h1:ywDsvb4KDFddMC2dpI/rrIzGU2mWUSvHmWUm9BMsdl4=
 github.com/kaptinlin/go-i18n v0.2.12/go.mod h1:pVcu9qsW5pOIOoZFJXesRYmLos1vMQrby70JPAoWmJU=
@@ -267,6 +281,9 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
 github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -299,6 +316,7 @@ github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt
 github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
 github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
 github.com/openai/openai-go/v3 v3.28.0 h1:2+FfrCVMdGXSQrBv1tLWtokm+BU7+3hJ/8rAHPQ63KM=
@@ -359,11 +377,18 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
+github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
+github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg=
+github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
+github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
+github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
 github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
 github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
 github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
@@ -532,7 +557,9 @@ google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhH
 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
 google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20251110073552-01de4eb40290 h1:g3ah7zaWmw41EtOgBNXpx8zk4HYuH3OMwB+qh1Dt834=
@@ -547,6 +574,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=

internal/proto/requests.go 🔗

@@ -0,0 +1,92 @@
+package proto
+
+import "github.com/charmbracelet/crush/internal/config"
+
+// ConfigSetRequest represents a request to set a config field.
+type ConfigSetRequest struct {
+	Scope config.Scope `json:"scope"`
+	Key   string       `json:"key"`
+	Value any          `json:"value"`
+}
+
+// ConfigRemoveRequest represents a request to remove a config field.
+type ConfigRemoveRequest struct {
+	Scope config.Scope `json:"scope"`
+	Key   string       `json:"key"`
+}
+
+// ConfigModelRequest represents a request to update the preferred model.
+type ConfigModelRequest struct {
+	Scope     config.Scope             `json:"scope"`
+	ModelType config.SelectedModelType `json:"model_type"`
+	Model     config.SelectedModel     `json:"model"`
+}
+
+// ConfigCompactRequest represents a request to set compact mode.
+type ConfigCompactRequest struct {
+	Scope   config.Scope `json:"scope"`
+	Enabled bool         `json:"enabled"`
+}
+
+// ConfigProviderKeyRequest represents a request to set a provider API key.
+type ConfigProviderKeyRequest struct {
+	Scope      config.Scope `json:"scope"`
+	ProviderID string       `json:"provider_id"`
+	APIKey     any          `json:"api_key"`
+}
+
+// ConfigRefreshOAuthRequest represents a request to refresh an OAuth token.
+type ConfigRefreshOAuthRequest struct {
+	Scope      config.Scope `json:"scope"`
+	ProviderID string       `json:"provider_id"`
+}
+
+// ImportCopilotResponse represents the response from importing Copilot credentials.
+type ImportCopilotResponse struct {
+	Token   any  `json:"token"`
+	Success bool `json:"success"`
+}
+
+// ProjectNeedsInitResponse represents whether a project needs initialization.
+type ProjectNeedsInitResponse struct {
+	NeedsInit bool `json:"needs_init"`
+}
+
+// ProjectInitPromptResponse represents the project initialization prompt.
+type ProjectInitPromptResponse struct {
+	Prompt string `json:"prompt"`
+}
+
+// LSPStartRequest represents a request to start an LSP for a path.
+type LSPStartRequest struct {
+	Path string `json:"path"`
+}
+
+// FileTrackerReadRequest represents a request to record a file read.
+type FileTrackerReadRequest struct {
+	SessionID string `json:"session_id"`
+	Path      string `json:"path"`
+}
+
+// MCPNameRequest represents a request targeting a named MCP server.
+type MCPNameRequest struct {
+	Name string `json:"name"`
+}
+
+// MCPReadResourceRequest represents a request to read an MCP resource.
+type MCPReadResourceRequest struct {
+	Name string `json:"name"`
+	URI  string `json:"uri"`
+}
+
+// MCPGetPromptRequest represents a request to get an MCP prompt.
+type MCPGetPromptRequest struct {
+	ClientID string            `json:"client_id"`
+	PromptID string            `json:"prompt_id"`
+	Args     map[string]string `json:"args"`
+}
+
+// MCPGetPromptResponse represents the response from getting an MCP prompt.
+type MCPGetPromptResponse struct {
+	Prompt string `json:"prompt"`
+}

internal/server/config.go 🔗

@@ -4,18 +4,25 @@ import (
 	"encoding/json"
 	"net/http"
 
-	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/proto"
 )
 
+// handlePostWorkspaceConfigSet sets a configuration field.
+//
+//	@Summary		Set a config field
+//	@Tags			config
+//	@Accept			json
+//	@Param			id		path	string					true	"Workspace ID"
+//	@Param			request	body	proto.ConfigSetRequest	true	"Config set request"
+//	@Success		200
+//	@Failure		400	{object}	proto.Error
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/config/set [post]
 func (c *controllerV1) handlePostWorkspaceConfigSet(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 
-	var req struct {
-		Scope config.Scope `json:"scope"`
-		Key   string       `json:"key"`
-		Value any          `json:"value"`
-	}
+	var req proto.ConfigSetRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		c.server.logError(r, "Failed to decode request", "error", err)
 		jsonError(w, http.StatusBadRequest, "failed to decode request")
@@ -29,13 +36,22 @@ func (c *controllerV1) handlePostWorkspaceConfigSet(w http.ResponseWriter, r *ht
 	w.WriteHeader(http.StatusOK)
 }
 
+// handlePostWorkspaceConfigRemove removes a configuration field.
+//
+//	@Summary		Remove a config field
+//	@Tags			config
+//	@Accept			json
+//	@Param			id		path	string						true	"Workspace ID"
+//	@Param			request	body	proto.ConfigRemoveRequest	true	"Config remove request"
+//	@Success		200
+//	@Failure		400	{object}	proto.Error
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/config/remove [post]
 func (c *controllerV1) handlePostWorkspaceConfigRemove(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 
-	var req struct {
-		Scope config.Scope `json:"scope"`
-		Key   string       `json:"key"`
-	}
+	var req proto.ConfigRemoveRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		c.server.logError(r, "Failed to decode request", "error", err)
 		jsonError(w, http.StatusBadRequest, "failed to decode request")
@@ -49,14 +65,22 @@ func (c *controllerV1) handlePostWorkspaceConfigRemove(w http.ResponseWriter, r
 	w.WriteHeader(http.StatusOK)
 }
 
+// handlePostWorkspaceConfigModel updates the preferred model.
+//
+//	@Summary		Set the preferred model
+//	@Tags			config
+//	@Accept			json
+//	@Param			id		path	string						true	"Workspace ID"
+//	@Param			request	body	proto.ConfigModelRequest	true	"Config model request"
+//	@Success		200
+//	@Failure		400	{object}	proto.Error
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/config/model [post]
 func (c *controllerV1) handlePostWorkspaceConfigModel(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 
-	var req struct {
-		Scope     config.Scope             `json:"scope"`
-		ModelType config.SelectedModelType `json:"model_type"`
-		Model     config.SelectedModel     `json:"model"`
-	}
+	var req proto.ConfigModelRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		c.server.logError(r, "Failed to decode request", "error", err)
 		jsonError(w, http.StatusBadRequest, "failed to decode request")
@@ -70,13 +94,22 @@ func (c *controllerV1) handlePostWorkspaceConfigModel(w http.ResponseWriter, r *
 	w.WriteHeader(http.StatusOK)
 }
 
+// handlePostWorkspaceConfigCompact sets compact mode.
+//
+//	@Summary		Set compact mode
+//	@Tags			config
+//	@Accept			json
+//	@Param			id		path	string						true	"Workspace ID"
+//	@Param			request	body	proto.ConfigCompactRequest	true	"Config compact request"
+//	@Success		200
+//	@Failure		400	{object}	proto.Error
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/config/compact [post]
 func (c *controllerV1) handlePostWorkspaceConfigCompact(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 
-	var req struct {
-		Scope   config.Scope `json:"scope"`
-		Enabled bool         `json:"enabled"`
-	}
+	var req proto.ConfigCompactRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		c.server.logError(r, "Failed to decode request", "error", err)
 		jsonError(w, http.StatusBadRequest, "failed to decode request")
@@ -90,14 +123,22 @@ func (c *controllerV1) handlePostWorkspaceConfigCompact(w http.ResponseWriter, r
 	w.WriteHeader(http.StatusOK)
 }
 
+// handlePostWorkspaceConfigProviderKey sets a provider API key.
+//
+//	@Summary		Set provider API key
+//	@Tags			config
+//	@Accept			json
+//	@Param			id		path	string							true	"Workspace ID"
+//	@Param			request	body	proto.ConfigProviderKeyRequest	true	"Config provider key request"
+//	@Success		200
+//	@Failure		400	{object}	proto.Error
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/config/provider-key [post]
 func (c *controllerV1) handlePostWorkspaceConfigProviderKey(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 
-	var req struct {
-		Scope      config.Scope `json:"scope"`
-		ProviderID string       `json:"provider_id"`
-		APIKey     any          `json:"api_key"`
-	}
+	var req proto.ConfigProviderKeyRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		c.server.logError(r, "Failed to decode request", "error", err)
 		jsonError(w, http.StatusBadRequest, "failed to decode request")
@@ -111,6 +152,16 @@ func (c *controllerV1) handlePostWorkspaceConfigProviderKey(w http.ResponseWrite
 	w.WriteHeader(http.StatusOK)
 }
 
+// handlePostWorkspaceConfigImportCopilot imports Copilot credentials.
+//
+//	@Summary		Import Copilot credentials
+//	@Tags			config
+//	@Produce		json
+//	@Param			id	path		string						true	"Workspace ID"
+//	@Success		200	{object}	proto.ImportCopilotResponse
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/config/import-copilot [post]
 func (c *controllerV1) handlePostWorkspaceConfigImportCopilot(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	token, ok, err := c.backend.ImportCopilot(id)
@@ -118,19 +169,25 @@ func (c *controllerV1) handlePostWorkspaceConfigImportCopilot(w http.ResponseWri
 		c.handleError(w, r, err)
 		return
 	}
-	jsonEncode(w, struct {
-		Token   any  `json:"token"`
-		Success bool `json:"success"`
-	}{Token: token, Success: ok})
+	jsonEncode(w, proto.ImportCopilotResponse{Token: token, Success: ok})
 }
 
+// handlePostWorkspaceConfigRefreshOAuth refreshes an OAuth token for a provider.
+//
+//	@Summary		Refresh OAuth token
+//	@Tags			config
+//	@Accept			json
+//	@Param			id		path	string							true	"Workspace ID"
+//	@Param			request	body	proto.ConfigRefreshOAuthRequest	true	"Refresh OAuth request"
+//	@Success		200
+//	@Failure		400	{object}	proto.Error
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/config/refresh-oauth [post]
 func (c *controllerV1) handlePostWorkspaceConfigRefreshOAuth(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 
-	var req struct {
-		Scope      config.Scope `json:"scope"`
-		ProviderID string       `json:"provider_id"`
-	}
+	var req proto.ConfigRefreshOAuthRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		c.server.logError(r, "Failed to decode request", "error", err)
 		jsonError(w, http.StatusBadRequest, "failed to decode request")
@@ -144,6 +201,16 @@ func (c *controllerV1) handlePostWorkspaceConfigRefreshOAuth(w http.ResponseWrit
 	w.WriteHeader(http.StatusOK)
 }
 
+// handleGetWorkspaceProjectNeedsInit reports whether a project needs initialization.
+//
+//	@Summary		Check if project needs initialization
+//	@Tags			project
+//	@Produce		json
+//	@Param			id	path		string							true	"Workspace ID"
+//	@Success		200	{object}	proto.ProjectNeedsInitResponse
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/project/needs-init [get]
 func (c *controllerV1) handleGetWorkspaceProjectNeedsInit(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	needs, err := c.backend.ProjectNeedsInitialization(id)
@@ -151,11 +218,18 @@ func (c *controllerV1) handleGetWorkspaceProjectNeedsInit(w http.ResponseWriter,
 		c.handleError(w, r, err)
 		return
 	}
-	jsonEncode(w, struct {
-		NeedsInit bool `json:"needs_init"`
-	}{NeedsInit: needs})
+	jsonEncode(w, proto.ProjectNeedsInitResponse{NeedsInit: needs})
 }
 
+// handlePostWorkspaceProjectInit marks the project as initialized.
+//
+//	@Summary		Mark project as initialized
+//	@Tags			project
+//	@Param			id	path	string	true	"Workspace ID"
+//	@Success		200
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/project/init [post]
 func (c *controllerV1) handlePostWorkspaceProjectInit(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	if err := c.backend.MarkProjectInitialized(id); err != nil {
@@ -165,6 +239,16 @@ func (c *controllerV1) handlePostWorkspaceProjectInit(w http.ResponseWriter, r *
 	w.WriteHeader(http.StatusOK)
 }
 
+// handleGetWorkspaceProjectInitPrompt returns the project initialization prompt.
+//
+//	@Summary		Get project initialization prompt
+//	@Tags			project
+//	@Produce		json
+//	@Param			id	path		string							true	"Workspace ID"
+//	@Success		200	{object}	proto.ProjectInitPromptResponse
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/project/init-prompt [get]
 func (c *controllerV1) handleGetWorkspaceProjectInitPrompt(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	prompt, err := c.backend.InitializePrompt(id)
@@ -172,17 +256,25 @@ func (c *controllerV1) handleGetWorkspaceProjectInitPrompt(w http.ResponseWriter
 		c.handleError(w, r, err)
 		return
 	}
-	jsonEncode(w, struct {
-		Prompt string `json:"prompt"`
-	}{Prompt: prompt})
+	jsonEncode(w, proto.ProjectInitPromptResponse{Prompt: prompt})
 }
 
+// handlePostWorkspaceMCPRefreshTools refreshes tools for a named MCP server.
+//
+//	@Summary		Refresh MCP tools
+//	@Tags			mcp
+//	@Accept			json
+//	@Param			id		path	string					true	"Workspace ID"
+//	@Param			request	body	proto.MCPNameRequest	true	"MCP name request"
+//	@Success		200
+//	@Failure		400	{object}	proto.Error
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/mcp/refresh-tools [post]
 func (c *controllerV1) handlePostWorkspaceMCPRefreshTools(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 
-	var req struct {
-		Name string `json:"name"`
-	}
+	var req proto.MCPNameRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		c.server.logError(r, "Failed to decode request", "error", err)
 		jsonError(w, http.StatusBadRequest, "failed to decode request")
@@ -196,13 +288,23 @@ func (c *controllerV1) handlePostWorkspaceMCPRefreshTools(w http.ResponseWriter,
 	w.WriteHeader(http.StatusOK)
 }
 
+// handlePostWorkspaceMCPReadResource reads a resource from an MCP server.
+//
+//	@Summary		Read MCP resource
+//	@Tags			mcp
+//	@Accept			json
+//	@Produce		json
+//	@Param			id		path		string						true	"Workspace ID"
+//	@Param			request	body		proto.MCPReadResourceRequest	true	"MCP read resource request"
+//	@Success		200		{object}	object
+//	@Failure		400		{object}	proto.Error
+//	@Failure		404		{object}	proto.Error
+//	@Failure		500		{object}	proto.Error
+//	@Router			/workspaces/{id}/mcp/read-resource [post]
 func (c *controllerV1) handlePostWorkspaceMCPReadResource(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 
-	var req struct {
-		Name string `json:"name"`
-		URI  string `json:"uri"`
-	}
+	var req proto.MCPReadResourceRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		c.server.logError(r, "Failed to decode request", "error", err)
 		jsonError(w, http.StatusBadRequest, "failed to decode request")
@@ -217,14 +319,23 @@ func (c *controllerV1) handlePostWorkspaceMCPReadResource(w http.ResponseWriter,
 	jsonEncode(w, contents)
 }
 
+// handlePostWorkspaceMCPGetPrompt retrieves a prompt from an MCP server.
+//
+//	@Summary		Get MCP prompt
+//	@Tags			mcp
+//	@Accept			json
+//	@Produce		json
+//	@Param			id		path		string						true	"Workspace ID"
+//	@Param			request	body		proto.MCPGetPromptRequest	true	"MCP get prompt request"
+//	@Success		200		{object}	proto.MCPGetPromptResponse
+//	@Failure		400		{object}	proto.Error
+//	@Failure		404		{object}	proto.Error
+//	@Failure		500		{object}	proto.Error
+//	@Router			/workspaces/{id}/mcp/get-prompt [post]
 func (c *controllerV1) handlePostWorkspaceMCPGetPrompt(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 
-	var req struct {
-		ClientID string            `json:"client_id"`
-		PromptID string            `json:"prompt_id"`
-		Args     map[string]string `json:"args"`
-	}
+	var req proto.MCPGetPromptRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		c.server.logError(r, "Failed to decode request", "error", err)
 		jsonError(w, http.StatusBadRequest, "failed to decode request")
@@ -236,11 +347,19 @@ func (c *controllerV1) handlePostWorkspaceMCPGetPrompt(w http.ResponseWriter, r
 		c.handleError(w, r, err)
 		return
 	}
-	jsonEncode(w, struct {
-		Prompt string `json:"prompt"`
-	}{Prompt: prompt})
+	jsonEncode(w, proto.MCPGetPromptResponse{Prompt: prompt})
 }
 
+// handleGetWorkspaceMCPStates returns the state of all MCP clients.
+//
+//	@Summary		Get MCP client states
+//	@Tags			mcp
+//	@Produce		json
+//	@Param			id	path		string						true	"Workspace ID"
+//	@Success		200	{object}	map[string]proto.MCPClientInfo
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/mcp/states [get]
 func (c *controllerV1) handleGetWorkspaceMCPStates(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	states := c.backend.MCPGetStates(id)
@@ -259,12 +378,22 @@ func (c *controllerV1) handleGetWorkspaceMCPStates(w http.ResponseWriter, r *htt
 	jsonEncode(w, result)
 }
 
+// handlePostWorkspaceMCPRefreshPrompts refreshes prompts for a named MCP server.
+//
+//	@Summary		Refresh MCP prompts
+//	@Tags			mcp
+//	@Accept			json
+//	@Param			id		path	string					true	"Workspace ID"
+//	@Param			request	body	proto.MCPNameRequest	true	"MCP name request"
+//	@Success		200
+//	@Failure		400	{object}	proto.Error
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/mcp/refresh-prompts [post]
 func (c *controllerV1) handlePostWorkspaceMCPRefreshPrompts(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 
-	var req struct {
-		Name string `json:"name"`
-	}
+	var req proto.MCPNameRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		c.server.logError(r, "Failed to decode request", "error", err)
 		jsonError(w, http.StatusBadRequest, "failed to decode request")
@@ -275,12 +404,22 @@ func (c *controllerV1) handlePostWorkspaceMCPRefreshPrompts(w http.ResponseWrite
 	w.WriteHeader(http.StatusOK)
 }
 
+// handlePostWorkspaceMCPRefreshResources refreshes resources for a named MCP server.
+//
+//	@Summary		Refresh MCP resources
+//	@Tags			mcp
+//	@Accept			json
+//	@Param			id		path	string					true	"Workspace ID"
+//	@Param			request	body	proto.MCPNameRequest	true	"MCP name request"
+//	@Success		200
+//	@Failure		400	{object}	proto.Error
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/mcp/refresh-resources [post]
 func (c *controllerV1) handlePostWorkspaceMCPRefreshResources(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 
-	var req struct {
-		Name string `json:"name"`
-	}
+	var req proto.MCPNameRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		c.server.logError(r, "Failed to decode request", "error", err)
 		jsonError(w, http.StatusBadRequest, "failed to decode request")

internal/server/proto.go 🔗

@@ -16,14 +16,36 @@ type controllerV1 struct {
 	server  *Server
 }
 
+// handleGetHealth checks server health.
+//
+//	@Summary		Health check
+//	@Tags			system
+//	@Success		200
+//	@Router			/health [get]
 func (c *controllerV1) handleGetHealth(w http.ResponseWriter, _ *http.Request) {
 	w.WriteHeader(http.StatusOK)
 }
 
+// handleGetVersion returns server version information.
+//
+//	@Summary		Get server version
+//	@Tags			system
+//	@Produce		json
+//	@Success		200	{object}	proto.VersionInfo
+//	@Router			/version [get]
 func (c *controllerV1) handleGetVersion(w http.ResponseWriter, _ *http.Request) {
 	jsonEncode(w, c.backend.VersionInfo())
 }
 
+// handlePostControl sends a control command to the server.
+//
+//	@Summary		Send server control command
+//	@Tags			system
+//	@Accept			json
+//	@Param			request	body	proto.ServerControl	true	"Control command (e.g. shutdown)"
+//	@Success		200
+//	@Failure		400	{object}	proto.Error
+//	@Router			/control [post]
 func (c *controllerV1) handlePostControl(w http.ResponseWriter, r *http.Request) {
 	var req proto.ServerControl
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -42,14 +64,38 @@ func (c *controllerV1) handlePostControl(w http.ResponseWriter, r *http.Request)
 	}
 }
 
+// handleGetConfig returns global server configuration.
+//
+//	@Summary		Get server config
+//	@Tags			system
+//	@Produce		json
+//	@Success		200	{object}	object
+//	@Router			/config [get]
 func (c *controllerV1) handleGetConfig(w http.ResponseWriter, _ *http.Request) {
 	jsonEncode(w, c.backend.Config())
 }
 
+// handleGetWorkspaces lists all workspaces.
+//
+//	@Summary		List workspaces
+//	@Tags			workspaces
+//	@Produce		json
+//	@Success		200	{array}		proto.Workspace
+//	@Router			/workspaces [get]
 func (c *controllerV1) handleGetWorkspaces(w http.ResponseWriter, _ *http.Request) {
 	jsonEncode(w, c.backend.ListWorkspaces())
 }
 
+// handleGetWorkspace returns a single workspace by ID.
+//
+//	@Summary		Get workspace
+//	@Tags			workspaces
+//	@Produce		json
+//	@Param			id	path		string	true	"Workspace ID"
+//	@Success		200	{object}	proto.Workspace
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id} [get]
 func (c *controllerV1) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	ws, err := c.backend.GetWorkspaceProto(id)
@@ -60,6 +106,17 @@ func (c *controllerV1) handleGetWorkspace(w http.ResponseWriter, r *http.Request
 	jsonEncode(w, ws)
 }
 
+// handlePostWorkspaces creates a new workspace.
+//
+//	@Summary		Create workspace
+//	@Tags			workspaces
+//	@Accept			json
+//	@Produce		json
+//	@Param			request	body		proto.Workspace	true	"Workspace creation params"
+//	@Success		200		{object}	proto.Workspace
+//	@Failure		400		{object}	proto.Error
+//	@Failure		500		{object}	proto.Error
+//	@Router			/workspaces [post]
 func (c *controllerV1) handlePostWorkspaces(w http.ResponseWriter, r *http.Request) {
 	var args proto.Workspace
 	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
@@ -76,11 +133,29 @@ func (c *controllerV1) handlePostWorkspaces(w http.ResponseWriter, r *http.Reque
 	jsonEncode(w, result)
 }
 
+// handleDeleteWorkspaces deletes a workspace.
+//
+//	@Summary		Delete workspace
+//	@Tags			workspaces
+//	@Param			id	path	string	true	"Workspace ID"
+//	@Success		200
+//	@Failure		404	{object}	proto.Error
+//	@Router			/workspaces/{id} [delete]
 func (c *controllerV1) handleDeleteWorkspaces(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	c.backend.DeleteWorkspace(id)
 }
 
+// handleGetWorkspaceConfig returns workspace configuration.
+//
+//	@Summary		Get workspace config
+//	@Tags			workspaces
+//	@Produce		json
+//	@Param			id	path		string	true	"Workspace ID"
+//	@Success		200	{object}	object
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/config [get]
 func (c *controllerV1) handleGetWorkspaceConfig(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	cfg, err := c.backend.GetWorkspaceConfig(id)
@@ -91,6 +166,16 @@ func (c *controllerV1) handleGetWorkspaceConfig(w http.ResponseWriter, r *http.R
 	jsonEncode(w, cfg)
 }
 
+// handleGetWorkspaceProviders lists available providers for a workspace.
+//
+//	@Summary		Get workspace providers
+//	@Tags			workspaces
+//	@Produce		json
+//	@Param			id	path		string	true	"Workspace ID"
+//	@Success		200	{object}	object
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/providers [get]
 func (c *controllerV1) handleGetWorkspaceProviders(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	providers, err := c.backend.GetWorkspaceProviders(id)
@@ -101,6 +186,16 @@ func (c *controllerV1) handleGetWorkspaceProviders(w http.ResponseWriter, r *htt
 	jsonEncode(w, providers)
 }
 
+// handleGetWorkspaceEvents streams workspace events as Server-Sent Events.
+//
+//	@Summary		Stream workspace events (SSE)
+//	@Tags			workspaces
+//	@Produce		text/event-stream
+//	@Param			id	path	string	true	"Workspace ID"
+//	@Success		200
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/events [get]
 func (c *controllerV1) handleGetWorkspaceEvents(w http.ResponseWriter, r *http.Request) {
 	flusher := http.NewResponseController(w)
 	id := r.PathValue("id")
@@ -140,6 +235,16 @@ func (c *controllerV1) handleGetWorkspaceEvents(w http.ResponseWriter, r *http.R
 	}
 }
 
+// handleGetWorkspaceLSPs lists LSP clients for a workspace.
+//
+//	@Summary		List LSP clients
+//	@Tags			lsp
+//	@Produce		json
+//	@Param			id	path		string							true	"Workspace ID"
+//	@Success		200	{object}	map[string]proto.LSPClientInfo
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/lsps [get]
 func (c *controllerV1) handleGetWorkspaceLSPs(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	states, err := c.backend.GetLSPStates(id)
@@ -160,6 +265,17 @@ func (c *controllerV1) handleGetWorkspaceLSPs(w http.ResponseWriter, r *http.Req
 	jsonEncode(w, result)
 }
 
+// handleGetWorkspaceLSPDiagnostics returns diagnostics for an LSP client.
+//
+//	@Summary		Get LSP diagnostics
+//	@Tags			lsp
+//	@Produce		json
+//	@Param			id	path		string	true	"Workspace ID"
+//	@Param			lsp	path		string	true	"LSP client name"
+//	@Success		200	{object}	object
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/lsps/{lsp}/diagnostics [get]
 func (c *controllerV1) handleGetWorkspaceLSPDiagnostics(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	lspName := r.PathValue("lsp")
@@ -171,6 +287,16 @@ func (c *controllerV1) handleGetWorkspaceLSPDiagnostics(w http.ResponseWriter, r
 	jsonEncode(w, diagnostics)
 }
 
+// handleGetWorkspaceSessions lists sessions for a workspace.
+//
+//	@Summary		List sessions
+//	@Tags			sessions
+//	@Produce		json
+//	@Param			id	path		string			true	"Workspace ID"
+//	@Success		200	{array}		proto.Session
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/sessions [get]
 func (c *controllerV1) handleGetWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	sessions, err := c.backend.ListSessions(r.Context(), id)
@@ -181,6 +307,19 @@ func (c *controllerV1) handleGetWorkspaceSessions(w http.ResponseWriter, r *http
 	jsonEncode(w, sessions)
 }
 
+// handlePostWorkspaceSessions creates a new session in a workspace.
+//
+//	@Summary		Create session
+//	@Tags			sessions
+//	@Accept			json
+//	@Produce		json
+//	@Param			id		path		string			true	"Workspace ID"
+//	@Param			request	body		proto.Session	true	"Session creation params (title)"
+//	@Success		200		{object}	proto.Session
+//	@Failure		400		{object}	proto.Error
+//	@Failure		404		{object}	proto.Error
+//	@Failure		500		{object}	proto.Error
+//	@Router			/workspaces/{id}/sessions [post]
 func (c *controllerV1) handlePostWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 
@@ -199,6 +338,17 @@ func (c *controllerV1) handlePostWorkspaceSessions(w http.ResponseWriter, r *htt
 	jsonEncode(w, sess)
 }
 
+// handleGetWorkspaceSession returns a single session.
+//
+//	@Summary		Get session
+//	@Tags			sessions
+//	@Produce		json
+//	@Param			id	path		string	true	"Workspace ID"
+//	@Param			sid	path		string	true	"Session ID"
+//	@Success		200	{object}	proto.Session
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/sessions/{sid} [get]
 func (c *controllerV1) handleGetWorkspaceSession(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	sid := r.PathValue("sid")
@@ -210,6 +360,17 @@ func (c *controllerV1) handleGetWorkspaceSession(w http.ResponseWriter, r *http.
 	jsonEncode(w, sess)
 }
 
+// handleGetWorkspaceSessionHistory returns the history for a session.
+//
+//	@Summary		Get session history
+//	@Tags			sessions
+//	@Produce		json
+//	@Param			id	path		string		true	"Workspace ID"
+//	@Param			sid	path		string		true	"Session ID"
+//	@Success		200	{array}		proto.File
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/sessions/{sid}/history [get]
 func (c *controllerV1) handleGetWorkspaceSessionHistory(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	sid := r.PathValue("sid")
@@ -221,6 +382,17 @@ func (c *controllerV1) handleGetWorkspaceSessionHistory(w http.ResponseWriter, r
 	jsonEncode(w, history)
 }
 
+// handleGetWorkspaceSessionMessages returns all messages for a session.
+//
+//	@Summary		Get session messages
+//	@Tags			sessions
+//	@Produce		json
+//	@Param			id	path		string			true	"Workspace ID"
+//	@Param			sid	path		string			true	"Session ID"
+//	@Success		200	{array}		proto.Message
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/sessions/{sid}/messages [get]
 func (c *controllerV1) handleGetWorkspaceSessionMessages(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	sid := r.PathValue("sid")
@@ -232,6 +404,20 @@ func (c *controllerV1) handleGetWorkspaceSessionMessages(w http.ResponseWriter,
 	jsonEncode(w, messagesToProto(messages))
 }
 
+// handlePutWorkspaceSession updates a session.
+//
+//	@Summary		Update session
+//	@Tags			sessions
+//	@Accept			json
+//	@Produce		json
+//	@Param			id		path		string			true	"Workspace ID"
+//	@Param			sid		path		string			true	"Session ID"
+//	@Param			request	body		proto.Session	true	"Updated session"
+//	@Success		200		{object}	proto.Session
+//	@Failure		400		{object}	proto.Error
+//	@Failure		404		{object}	proto.Error
+//	@Failure		500		{object}	proto.Error
+//	@Router			/workspaces/{id}/sessions/{sid} [put]
 func (c *controllerV1) handlePutWorkspaceSession(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 
@@ -250,6 +436,16 @@ func (c *controllerV1) handlePutWorkspaceSession(w http.ResponseWriter, r *http.
 	jsonEncode(w, saved)
 }
 
+// handleDeleteWorkspaceSession deletes a session.
+//
+//	@Summary		Delete session
+//	@Tags			sessions
+//	@Param			id	path	string	true	"Workspace ID"
+//	@Param			sid	path	string	true	"Session ID"
+//	@Success		200
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/sessions/{sid} [delete]
 func (c *controllerV1) handleDeleteWorkspaceSession(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	sid := r.PathValue("sid")
@@ -260,6 +456,17 @@ func (c *controllerV1) handleDeleteWorkspaceSession(w http.ResponseWriter, r *ht
 	w.WriteHeader(http.StatusOK)
 }
 
+// handleGetWorkspaceSessionUserMessages returns user messages for a session.
+//
+//	@Summary		Get user messages for session
+//	@Tags			sessions
+//	@Produce		json
+//	@Param			id	path		string			true	"Workspace ID"
+//	@Param			sid	path		string			true	"Session ID"
+//	@Success		200	{array}		proto.Message
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/sessions/{sid}/messages/user [get]
 func (c *controllerV1) handleGetWorkspaceSessionUserMessages(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	sid := r.PathValue("sid")
@@ -271,6 +478,16 @@ func (c *controllerV1) handleGetWorkspaceSessionUserMessages(w http.ResponseWrit
 	jsonEncode(w, messagesToProto(messages))
 }
 
+// handleGetWorkspaceAllUserMessages returns all user messages across sessions.
+//
+//	@Summary		Get all user messages for workspace
+//	@Tags			workspaces
+//	@Produce		json
+//	@Param			id	path		string			true	"Workspace ID"
+//	@Success		200	{array}		proto.Message
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/messages/user [get]
 func (c *controllerV1) handleGetWorkspaceAllUserMessages(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	messages, err := c.backend.ListAllUserMessages(r.Context(), id)
@@ -281,6 +498,17 @@ func (c *controllerV1) handleGetWorkspaceAllUserMessages(w http.ResponseWriter,
 	jsonEncode(w, messagesToProto(messages))
 }
 
+// handleGetWorkspaceSessionFileTrackerFiles lists files read in a session.
+//
+//	@Summary		List tracked files for session
+//	@Tags			filetracker
+//	@Produce		json
+//	@Param			id	path		string		true	"Workspace ID"
+//	@Param			sid	path		string		true	"Session ID"
+//	@Success		200	{array}		string
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/sessions/{sid}/filetracker/files [get]
 func (c *controllerV1) handleGetWorkspaceSessionFileTrackerFiles(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	sid := r.PathValue("sid")
@@ -292,13 +520,22 @@ func (c *controllerV1) handleGetWorkspaceSessionFileTrackerFiles(w http.Response
 	jsonEncode(w, files)
 }
 
+// handlePostWorkspaceFileTrackerRead records a file read event.
+//
+//	@Summary		Record file read
+//	@Tags			filetracker
+//	@Accept			json
+//	@Param			id		path	string							true	"Workspace ID"
+//	@Param			request	body	proto.FileTrackerReadRequest	true	"File tracker read request"
+//	@Success		200
+//	@Failure		400	{object}	proto.Error
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/filetracker/read [post]
 func (c *controllerV1) handlePostWorkspaceFileTrackerRead(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 
-	var req struct {
-		SessionID string `json:"session_id"`
-		Path      string `json:"path"`
-	}
+	var req proto.FileTrackerReadRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		c.server.logError(r, "Failed to decode request", "error", err)
 		jsonError(w, http.StatusBadRequest, "failed to decode request")
@@ -312,6 +549,18 @@ func (c *controllerV1) handlePostWorkspaceFileTrackerRead(w http.ResponseWriter,
 	w.WriteHeader(http.StatusOK)
 }
 
+// handleGetWorkspaceFileTrackerLastRead returns the last read time for a file.
+//
+//	@Summary		Get last read time for file
+//	@Tags			filetracker
+//	@Produce		json
+//	@Param			id			path		string	true	"Workspace ID"
+//	@Param			session_id	query		string	false	"Session ID"
+//	@Param			path		query		string	true	"File path"
+//	@Success		200			{object}	object
+//	@Failure		404			{object}	proto.Error
+//	@Failure		500			{object}	proto.Error
+//	@Router			/workspaces/{id}/filetracker/lastread [get]
 func (c *controllerV1) handleGetWorkspaceFileTrackerLastRead(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	sid := r.URL.Query().Get("session_id")
@@ -325,12 +574,22 @@ func (c *controllerV1) handleGetWorkspaceFileTrackerLastRead(w http.ResponseWrit
 	jsonEncode(w, t)
 }
 
+// handlePostWorkspaceLSPStart starts an LSP server for a path.
+//
+//	@Summary		Start LSP server
+//	@Tags			lsp
+//	@Accept			json
+//	@Param			id		path	string					true	"Workspace ID"
+//	@Param			request	body	proto.LSPStartRequest	true	"LSP start request"
+//	@Success		200
+//	@Failure		400	{object}	proto.Error
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/lsps/start [post]
 func (c *controllerV1) handlePostWorkspaceLSPStart(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 
-	var req struct {
-		Path string `json:"path"`
-	}
+	var req proto.LSPStartRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		c.server.logError(r, "Failed to decode request", "error", err)
 		jsonError(w, http.StatusBadRequest, "failed to decode request")
@@ -344,6 +603,15 @@ func (c *controllerV1) handlePostWorkspaceLSPStart(w http.ResponseWriter, r *htt
 	w.WriteHeader(http.StatusOK)
 }
 
+// handlePostWorkspaceLSPStopAll stops all LSP servers.
+//
+//	@Summary		Stop all LSP servers
+//	@Tags			lsp
+//	@Param			id	path	string	true	"Workspace ID"
+//	@Success		200
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/lsps/stop [post]
 func (c *controllerV1) handlePostWorkspaceLSPStopAll(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	if err := c.backend.LSPStopAll(r.Context(), id); err != nil {
@@ -353,6 +621,16 @@ func (c *controllerV1) handlePostWorkspaceLSPStopAll(w http.ResponseWriter, r *h
 	w.WriteHeader(http.StatusOK)
 }
 
+// handleGetWorkspaceAgent returns agent info for a workspace.
+//
+//	@Summary		Get agent info
+//	@Tags			agent
+//	@Produce		json
+//	@Param			id	path		string			true	"Workspace ID"
+//	@Success		200	{object}	proto.AgentInfo
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/agent [get]
 func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	info, err := c.backend.GetAgentInfo(id)
@@ -363,6 +641,18 @@ func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Re
 	jsonEncode(w, info)
 }
 
+// handlePostWorkspaceAgent sends a message to the agent.
+//
+//	@Summary		Send message to agent
+//	@Tags			agent
+//	@Accept			json
+//	@Param			id		path	string				true	"Workspace ID"
+//	@Param			request	body	proto.AgentMessage	true	"Agent message"
+//	@Success		200
+//	@Failure		400	{object}	proto.Error
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/agent [post]
 func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 
@@ -380,6 +670,15 @@ func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.R
 	w.WriteHeader(http.StatusOK)
 }
 
+// handlePostWorkspaceAgentInit initializes the agent for a workspace.
+//
+//	@Summary		Initialize agent
+//	@Tags			agent
+//	@Param			id	path	string	true	"Workspace ID"
+//	@Success		200
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/agent/init [post]
 func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	if err := c.backend.InitAgent(r.Context(), id); err != nil {
@@ -389,6 +688,15 @@ func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *ht
 	w.WriteHeader(http.StatusOK)
 }
 
+// handlePostWorkspaceAgentUpdate updates the agent for a workspace.
+//
+//	@Summary		Update agent
+//	@Tags			agent
+//	@Param			id	path	string	true	"Workspace ID"
+//	@Success		200
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/agent/update [post]
 func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	if err := c.backend.UpdateAgent(r.Context(), id); err != nil {
@@ -398,6 +706,17 @@ func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *
 	w.WriteHeader(http.StatusOK)
 }
 
+// handleGetWorkspaceAgentSession returns a specific agent session.
+//
+//	@Summary		Get agent session
+//	@Tags			agent
+//	@Produce		json
+//	@Param			id	path		string				true	"Workspace ID"
+//	@Param			sid	path		string				true	"Session ID"
+//	@Success		200	{object}	proto.AgentSession
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/agent/sessions/{sid} [get]
 func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	sid := r.PathValue("sid")
@@ -409,6 +728,16 @@ func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r *
 	jsonEncode(w, agentSession)
 }
 
+// handlePostWorkspaceAgentSessionCancel cancels a running agent session.
+//
+//	@Summary		Cancel agent session
+//	@Tags			agent
+//	@Param			id	path	string	true	"Workspace ID"
+//	@Param			sid	path	string	true	"Session ID"
+//	@Success		200
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/agent/sessions/{sid}/cancel [post]
 func (c *controllerV1) handlePostWorkspaceAgentSessionCancel(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	sid := r.PathValue("sid")
@@ -419,6 +748,17 @@ func (c *controllerV1) handlePostWorkspaceAgentSessionCancel(w http.ResponseWrit
 	w.WriteHeader(http.StatusOK)
 }
 
+// handleGetWorkspaceAgentSessionPromptQueued returns whether a queued prompt exists.
+//
+//	@Summary		Get queued prompt status
+//	@Tags			agent
+//	@Produce		json
+//	@Param			id	path		string	true	"Workspace ID"
+//	@Param			sid	path		string	true	"Session ID"
+//	@Success		200	{object}	object
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/agent/sessions/{sid}/prompts/queued [get]
 func (c *controllerV1) handleGetWorkspaceAgentSessionPromptQueued(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	sid := r.PathValue("sid")
@@ -430,6 +770,16 @@ func (c *controllerV1) handleGetWorkspaceAgentSessionPromptQueued(w http.Respons
 	jsonEncode(w, queued)
 }
 
+// handlePostWorkspaceAgentSessionPromptClear clears the prompt queue for a session.
+//
+//	@Summary		Clear prompt queue
+//	@Tags			agent
+//	@Param			id	path	string	true	"Workspace ID"
+//	@Param			sid	path	string	true	"Session ID"
+//	@Success		200
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/agent/sessions/{sid}/prompts/clear [post]
 func (c *controllerV1) handlePostWorkspaceAgentSessionPromptClear(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	sid := r.PathValue("sid")
@@ -440,6 +790,16 @@ func (c *controllerV1) handlePostWorkspaceAgentSessionPromptClear(w http.Respons
 	w.WriteHeader(http.StatusOK)
 }
 
+// handlePostWorkspaceAgentSessionSummarize summarizes a session.
+//
+//	@Summary		Summarize session
+//	@Tags			agent
+//	@Param			id	path	string	true	"Workspace ID"
+//	@Param			sid	path	string	true	"Session ID"
+//	@Success		200
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/agent/sessions/{sid}/summarize [post]
 func (c *controllerV1) handlePostWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	sid := r.PathValue("sid")
@@ -450,6 +810,17 @@ func (c *controllerV1) handlePostWorkspaceAgentSessionSummarize(w http.ResponseW
 	w.WriteHeader(http.StatusOK)
 }
 
+// handleGetWorkspaceAgentSessionPromptList returns the list of queued prompts.
+//
+//	@Summary		List queued prompts
+//	@Tags			agent
+//	@Produce		json
+//	@Param			id	path		string		true	"Workspace ID"
+//	@Param			sid	path		string		true	"Session ID"
+//	@Success		200	{array}		string
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/agent/sessions/{sid}/prompts/list [get]
 func (c *controllerV1) handleGetWorkspaceAgentSessionPromptList(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	sid := r.PathValue("sid")
@@ -461,6 +832,17 @@ func (c *controllerV1) handleGetWorkspaceAgentSessionPromptList(w http.ResponseW
 	jsonEncode(w, prompts)
 }
 
+// handleGetWorkspaceAgentDefaultSmallModel returns the default small model for a provider.
+//
+//	@Summary		Get default small model
+//	@Tags			agent
+//	@Produce		json
+//	@Param			id			path		string	true	"Workspace ID"
+//	@Param			provider_id	query		string	false	"Provider ID"
+//	@Success		200			{object}	object
+//	@Failure		404			{object}	proto.Error
+//	@Failure		500			{object}	proto.Error
+//	@Router			/workspaces/{id}/agent/default-small-model [get]
 func (c *controllerV1) handleGetWorkspaceAgentDefaultSmallModel(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	providerID := r.URL.Query().Get("provider_id")
@@ -472,6 +854,18 @@ func (c *controllerV1) handleGetWorkspaceAgentDefaultSmallModel(w http.ResponseW
 	jsonEncode(w, model)
 }
 
+// handlePostWorkspacePermissionsGrant grants a permission request.
+//
+//	@Summary		Grant permission
+//	@Tags			permissions
+//	@Accept			json
+//	@Param			id		path	string				true	"Workspace ID"
+//	@Param			request	body	proto.PermissionGrant	true	"Permission grant"
+//	@Success		200
+//	@Failure		400	{object}	proto.Error
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/permissions/grant [post]
 func (c *controllerV1) handlePostWorkspacePermissionsGrant(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 
@@ -489,6 +883,18 @@ func (c *controllerV1) handlePostWorkspacePermissionsGrant(w http.ResponseWriter
 	w.WriteHeader(http.StatusOK)
 }
 
+// handlePostWorkspacePermissionsSkip sets whether to skip permission prompts.
+//
+//	@Summary		Set skip permissions
+//	@Tags			permissions
+//	@Accept			json
+//	@Param			id		path	string						true	"Workspace ID"
+//	@Param			request	body	proto.PermissionSkipRequest	true	"Permission skip request"
+//	@Success		200
+//	@Failure		400	{object}	proto.Error
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/permissions/skip [post]
 func (c *controllerV1) handlePostWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 
@@ -505,6 +911,16 @@ func (c *controllerV1) handlePostWorkspacePermissionsSkip(w http.ResponseWriter,
 	}
 }
 
+// handleGetWorkspacePermissionsSkip returns whether permission prompts are skipped.
+//
+//	@Summary		Get skip permissions status
+//	@Tags			permissions
+//	@Produce		json
+//	@Param			id	path		string						true	"Workspace ID"
+//	@Success		200	{object}	proto.PermissionSkipRequest
+//	@Failure		404	{object}	proto.Error
+//	@Failure		500	{object}	proto.Error
+//	@Router			/workspaces/{id}/permissions/skip [get]
 func (c *controllerV1) handleGetWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
 	id := r.PathValue("id")
 	skip, err := c.backend.GetPermissionsSkip(id)

internal/server/server.go 🔗

@@ -11,8 +11,10 @@ import (
 	"runtime"
 	"strings"
 
+	_ "github.com/charmbracelet/crush/docs"
 	"github.com/charmbracelet/crush/internal/backend"
 	"github.com/charmbracelet/crush/internal/config"
+	httpswagger "github.com/swaggo/http-swagger/v2"
 )
 
 // ErrServerClosed is returned when the server is closed.
@@ -161,6 +163,7 @@ func NewServer(cfg *config.ConfigStore, network, address string) *Server {
 	mux.HandleFunc("GET /v1/workspaces/{id}/mcp/states", c.handleGetWorkspaceMCPStates)
 	mux.HandleFunc("POST /v1/workspaces/{id}/mcp/refresh-prompts", c.handlePostWorkspaceMCPRefreshPrompts)
 	mux.HandleFunc("POST /v1/workspaces/{id}/mcp/refresh-resources", c.handlePostWorkspaceMCPRefreshResources)
+	mux.Handle("/v1/docs/", httpswagger.WrapHandler)
 	s.h = &http.Server{
 		Protocols: &p,
 		Handler:   s.loggingHandler(mux),

main.go 🔗

@@ -1,3 +1,13 @@
+// Package main is the entry point for the Crush CLI.
+//
+//	@title			Crush API
+//	@version		1.0
+//	@description	Crush is a terminal-based AI coding assistant. This API is served over a Unix socket (or Windows named pipe) and provides programmatic access to workspaces, sessions, agents, LSP, MCP, and more.
+//	@contact.name	Charm
+//	@contact.url	https://charm.sh
+//	@license.name	MIT
+//	@license.url	https://github.com/charmbracelet/crush/blob/main/LICENSE
+//	@BasePath		/v1
 package main
 
 import (