on main help sign in
Merge branch 'main' into ui
b9a0cdcAyman Bagabas
go.mod+7−7
@@ -55,10 +55,10 @@ require (
55 github.com/stretchr/testify v1.11.1
55 github.com/stretchr/testify v1.11.1
56 github.com/tidwall/sjson v1.2.5
56 github.com/tidwall/sjson v1.2.5
57 github.com/zeebo/xxh3 v1.0.2
57 github.com/zeebo/xxh3 v1.0.2
58− golang.org/x/mod v0.30.0
58+ golang.org/x/mod v0.31.0
59− golang.org/x/net v0.47.0
59+ golang.org/x/net v0.48.0
60− golang.org/x/sync v0.18.0
60+ golang.org/x/sync v0.19.0
61− golang.org/x/text v0.31.0
61+ golang.org/x/text v0.32.0
62 gopkg.in/natefinch/lumberjack.v2 v2.2.1
62 gopkg.in/natefinch/lumberjack.v2 v2.2.1
63 mvdan.cc/sh/moreinterp v0.0.0-20250902163504-3cf4fd5717a5
63 mvdan.cc/sh/moreinterp v0.0.0-20250902163504-3cf4fd5717a5
64 mvdan.cc/sh/v3 v3.12.1-0.20250902163504-3cf4fd5717a5
64 mvdan.cc/sh/v3 v3.12.1-0.20250902163504-3cf4fd5717a5
@@ -163,12 +163,12 @@ require (
163 go.opentelemetry.io/otel/trace v1.37.0 // indirect
163 go.opentelemetry.io/otel/trace v1.37.0 // indirect
164 go.uber.org/multierr v1.11.0 // indirect
164 go.uber.org/multierr v1.11.0 // indirect
165 go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect
165 go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect
166− golang.org/x/crypto v0.45.0 // indirect
166+ golang.org/x/crypto v0.46.0 // indirect
167 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
167 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
168 golang.org/x/image v0.27.0 // indirect
168 golang.org/x/image v0.27.0 // indirect
169 golang.org/x/oauth2 v0.33.0 // indirect
169 golang.org/x/oauth2 v0.33.0 // indirect
170− golang.org/x/sys v0.38.0 // indirect
170+ golang.org/x/sys v0.39.0 // indirect
171− golang.org/x/term v0.37.0 // indirect
171+ golang.org/x/term v0.38.0 // indirect
172 golang.org/x/time v0.12.0 // indirect
172 golang.org/x/time v0.12.0 // indirect
173 google.golang.org/api v0.239.0 // indirect
173 google.golang.org/api v0.239.0 // indirect
174 google.golang.org/genai v1.37.0 // indirect
174 google.golang.org/genai v1.37.0 // indirect
go.sum+16−16
@@ -370,8 +370,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
370golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
370golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
371golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
371golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
372golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
372golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
373−golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
373+golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
374−golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
374+golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
375golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
375golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
376golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
376golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
377golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
377golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
@@ -381,8 +381,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
381golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
381golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
382golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
382golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
383golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
383golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
384−golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
384+golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
385−golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
385+golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
386golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
386golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
387golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
387golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
388golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
388golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -394,8 +394,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
394golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
394golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
395golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
395golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
396golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
396golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
397−golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
397+golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
398−golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
398+golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
399golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
399golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
400golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
400golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
401golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
401golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -405,8 +405,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
405golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
405golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
406golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
406golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
407golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
407golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
408−golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
408+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
409−golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
409+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
410golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
410golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
411golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
411golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
412golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
412golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -423,8 +423,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
423golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
423golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
424golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
424golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
425golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
425golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
426−golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
426+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
427−golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
427+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
428golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
428golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
429golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
429golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
430golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
430golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -436,8 +436,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
436golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
436golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
437golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
437golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
438golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
438golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
439−golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
439+golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
440−golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
440+golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
441golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
441golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
442golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
442golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
443golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
443golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -447,8 +447,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
447golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
447golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
448golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
448golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
449golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
449golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
450−golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
450+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
451−golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
451+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
452golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
452golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
453golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
453golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
454golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
454golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -457,8 +457,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
457golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
457golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
458golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
458golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
459golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
459golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
460−golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
460+golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
461−golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
461+golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
462golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
462golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
463google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo=
463google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo=
464google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
464google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
internal/agent/tools/download.go+4−16
@@ -114,12 +114,6 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
114 return fantasy.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
114 return fantasy.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
115 }
115 }
116
116
117− // Check content length if available
118− maxSize := int64(100 * 1024 * 1024) // 100MB
119− if resp.ContentLength > maxSize {
120− return fantasy.NewTextErrorResponse(fmt.Sprintf("File too large: %d bytes (max %d bytes)", resp.ContentLength, maxSize)), nil
121− }
122−
123 // Create parent directories if they don't exist
117 // Create parent directories if they don't exist
124 if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil {
118 if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil {
125 return fantasy.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
119 return fantasy.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
@@ -132,20 +126,14 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
132 }
126 }
133 defer outFile.Close()
127 defer outFile.Close()
134
128
135− // Copy data with size limit
129+ // Copy data without an explicit size limit.
136− limitedReader := io.LimitReader(resp.Body, maxSize)
130+ // The overall download is still constrained by the HTTP client's timeout
131+ // and any upstream server limits.
137− bytesWritten, err := io.Copy(outFile, limitedReader)
132+ bytesWritten, err := io.Copy(outFile, resp.Body)
138 if err != nil {
133 if err != nil {
139 return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
134 return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
140 }
135 }
141
136
142− // Check if we hit the size limit
143− if bytesWritten == maxSize {
144− // Clean up the file since it might be incomplete
145− os.Remove(filePath)
146− return fantasy.NewTextErrorResponse(fmt.Sprintf("File too large: exceeded %d bytes limit", maxSize)), nil
147− }
148−
149 contentType := resp.Header.Get("Content-Type")
137 contentType := resp.Header.Get("Content-Type")
150 responseMsg := fmt.Sprintf("Successfully downloaded %d bytes to %s", bytesWritten, relPath)
138 responseMsg := fmt.Sprintf("Successfully downloaded %d bytes to %s", bytesWritten, relPath)
151 if contentType != "" {
139 if contentType != "" {
internal/agent/tools/search.go+8−2
@@ -38,6 +38,10 @@ func searchDuckDuckGo(ctx context.Context, client *http.Client, query string, ma
38
38
39 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
39 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
40 req.Header.Set("User-Agent", BrowserUserAgent)
40 req.Header.Set("User-Agent", BrowserUserAgent)
41+ req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
42+ req.Header.Set("Accept-Language", "en-US,en;q=0.5")
43+ req.Header.Set("Accept-Encoding", "gzip, deflate")
44+ req.Header.Set("Referer", "https://duckduckgo.com/")
41
45
42 resp, err := client.Do(req)
46 resp, err := client.Do(req)
43 if err != nil {
47 if err != nil {
@@ -45,8 +49,10 @@ func searchDuckDuckGo(ctx context.Context, client *http.Client, query string, ma
45 }
49 }
46 defer resp.Body.Close()
50 defer resp.Body.Close()
47
51
52+ // Accept both 200 (OK) and 202 (Accepted).
53+ // DuckDuckGo may still return 202 for rate limiting or bot detection.
48− if resp.StatusCode != http.StatusOK {
54+ if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
49− return nil, fmt.Errorf("search failed with status code: %d", resp.StatusCode)
55+ return nil, fmt.Errorf("search failed with status code: %d (DuckDuckGo may be rate limiting requests)", resp.StatusCode)
50 }
56 }
51
57
52 body, err := io.ReadAll(resp.Body)
58 body, err := io.ReadAll(resp.Body)
internal/cmd/projects.gonew file+78
@@ -0,0 +1,78 @@
1+package cmd
2+
3+import (
4+ "encoding/json"
5+ "os"
6+
7+ "charm.land/lipgloss/v2"
8+ "charm.land/lipgloss/v2/table"
9+ "github.com/charmbracelet/crush/internal/projects"
10+ "github.com/charmbracelet/x/term"
11+ "github.com/spf13/cobra"
12+)
13+
14+var projectsCmd = &cobra.Command{
15+ Use: "projects",
16+ Short: "List all tracked project directories",
17+ Long: `List all directories where Crush has been used.
18+This includes the working directory, data directory path, and last accessed time.`,
19+ Example: `
20+# List all projects in a table
21+crush projects
22+
23+# Output as JSON
24+crush projects --json
25+ `,
26+ RunE: func(cmd *cobra.Command, args []string) error {
27+ jsonOutput, _ := cmd.Flags().GetBool("json")
28+
29+ projectList, err := projects.List()
30+ if err != nil {
31+ return err
32+ }
33+
34+ if jsonOutput {
35+ output := struct {
36+ Projects []projects.Project `json:"projects"`
37+ }{Projects: projectList}
38+
39+ data, err := json.Marshal(output)
40+ if err != nil {
41+ return err
42+ }
43+ cmd.Println(string(data))
44+ return nil
45+ }
46+
47+ if len(projectList) == 0 {
48+ cmd.Println("No projects tracked yet.")
49+ return nil
50+ }
51+
52+ if term.IsTerminal(os.Stdout.Fd()) {
53+ // We're in a TTY: make it fancy.
54+ t := table.New().
55+ Border(lipgloss.RoundedBorder()).
56+ StyleFunc(func(row, col int) lipgloss.Style {
57+ return lipgloss.NewStyle().Padding(0, 2)
58+ }).
59+ Headers("Path", "Data Dir", "Last Accessed")
60+
61+ for _, p := range projectList {
62+ t.Row(p.Path, p.DataDir, p.LastAccessed.Local().Format("2006-01-02 15:04"))
63+ }
64+ lipgloss.Println(t)
65+ return nil
66+ }
67+
68+ // Not a TTY: plain output
69+ for _, p := range projectList {
70+ cmd.Printf("%s\t%s\t%s\n", p.Path, p.DataDir, p.LastAccessed.Format("2006-01-02T15:04:05Z07:00"))
71+ }
72+ return nil
73+ },
74+}
75+
76+func init() {
77+ projectsCmd.Flags().Bool("json", false, "Output as JSON")
78+}
internal/cmd/projects_test.gonew file+56
@@ -0,0 +1,56 @@
1+package cmd
2+
3+import (
4+ "bytes"
5+ "encoding/json"
6+ "testing"
7+
8+ "github.com/charmbracelet/crush/internal/projects"
9+ "github.com/stretchr/testify/require"
10+)
11+
12+func TestProjectsEmpty(t *testing.T) {
13+ // Use a temp directory for projects.json
14+ tmpDir := t.TempDir()
15+ t.Setenv("XDG_DATA_HOME", tmpDir)
16+
17+ var b bytes.Buffer
18+ projectsCmd.SetOut(&b)
19+ projectsCmd.SetErr(&b)
20+ projectsCmd.SetIn(bytes.NewReader(nil))
21+ err := projectsCmd.RunE(projectsCmd, nil)
22+ require.NoError(t, err)
23+ require.Equal(t, "No projects tracked yet.\n", b.String())
24+}
25+
26+func TestProjectsJSON(t *testing.T) {
27+ tmpDir := t.TempDir()
28+ t.Setenv("XDG_DATA_HOME", tmpDir)
29+
30+ // Register a project
31+ err := projects.Register("/test/project", "/test/project/.crush")
32+ require.NoError(t, err)
33+
34+ var b bytes.Buffer
35+ projectsCmd.SetOut(&b)
36+ projectsCmd.SetErr(&b)
37+ projectsCmd.SetIn(bytes.NewReader(nil))
38+
39+ // Set the --json flag
40+ projectsCmd.Flags().Set("json", "true")
41+ defer projectsCmd.Flags().Set("json", "false")
42+
43+ err = projectsCmd.RunE(projectsCmd, nil)
44+ require.NoError(t, err)
45+
46+ // Parse the JSON output
47+ var result struct {
48+ Projects []projects.Project `json:"projects"`
49+ }
50+ err = json.Unmarshal(b.Bytes(), &result)
51+ require.NoError(t, err)
52+
53+ require.Len(t, result.Projects, 1)
54+ require.Equal(t, "/test/project", result.Projects[0].Path)
55+ require.Equal(t, "/test/project/.crush", result.Projects[0].DataDir)
56+}
internal/cmd/root.go+8
@@ -19,6 +19,7 @@ import (
19 "github.com/charmbracelet/crush/internal/config"
19 "github.com/charmbracelet/crush/internal/config"
20 "github.com/charmbracelet/crush/internal/db"
20 "github.com/charmbracelet/crush/internal/db"
21 "github.com/charmbracelet/crush/internal/event"
21 "github.com/charmbracelet/crush/internal/event"
22+ "github.com/charmbracelet/crush/internal/projects"
22 "github.com/charmbracelet/crush/internal/stringext"
23 "github.com/charmbracelet/crush/internal/stringext"
23 "github.com/charmbracelet/crush/internal/tui"
24 "github.com/charmbracelet/crush/internal/tui"
24 "github.com/charmbracelet/crush/internal/ui/common"
25 "github.com/charmbracelet/crush/internal/ui/common"
@@ -42,6 +43,7 @@ func init() {
42 rootCmd.AddCommand(
43 rootCmd.AddCommand(
43 runCmd,
44 runCmd,
44 dirsCmd,
45 dirsCmd,
46+ projectsCmd,
45 updateProvidersCmd,
47 updateProvidersCmd,
46 logsCmd,
48 logsCmd,
47 schemaCmd,
49 schemaCmd,
@@ -201,6 +203,12 @@ func setupApp(cmd *cobra.Command) (*app.App, error) {
201 return nil, err
203 return nil, err
202 }
204 }
203
205
206+ // Register this project in the centralized projects list.
207+ if err := projects.Register(cwd, cfg.Options.DataDirectory); err != nil {
208+ slog.Warn("Failed to register project", "error", err)
209+ // Non-fatal: continue even if registration fails
210+ }
211+
204 // Connect to DB; this will also run migrations.
212 // Connect to DB; this will also run migrations.
205 conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
213 conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
206 if err != nil {
214 if err != nil {
internal/cmd/run.go+7
@@ -8,6 +8,7 @@ import (
8 "os/signal"
8 "os/signal"
9 "strings"
9 "strings"
10
10
11+ "github.com/charmbracelet/crush/internal/event"
11 "github.com/spf13/cobra"
12 "github.com/spf13/cobra"
12)
13)
13
14
@@ -58,8 +59,14 @@ crush run --quiet "Generate a README for this project"
58 return fmt.Errorf("no prompt provided")
59 return fmt.Errorf("no prompt provided")
59 }
60 }
60
61
62+ event.SetInteractive(true)
63+ event.AppInitialized()
64+
61 return app.RunNonInteractive(ctx, os.Stdout, prompt, quiet)
65 return app.RunNonInteractive(ctx, os.Stdout, prompt, quiet)
62 },
66 },
67+ PostRun: func(cmd *cobra.Command, args []string) {
68+ event.AppExited()
69+ },
63}
70}
64
71
65func init() {
72func init() {
internal/event/event.go+6−1
@@ -26,9 +26,14 @@ var (
26 Set("TERM", os.Getenv("TERM")).
26 Set("TERM", os.Getenv("TERM")).
27 Set("SHELL", filepath.Base(os.Getenv("SHELL"))).
27 Set("SHELL", filepath.Base(os.Getenv("SHELL"))).
28 Set("Version", version.Version).
28 Set("Version", version.Version).
29− Set("GoVersion", runtime.Version())
29+ Set("GoVersion", runtime.Version()).
30+ Set("Interactive", false)
30)
31)
31
32
33+func SetInteractive(interactive bool) {
34+ baseProps = baseProps.Set("interactive", interactive)
35+}
36+
32func Init() {
37func Init() {
33 c, err := posthog.NewWithConfig(key, posthog.Config{
38 c, err := posthog.NewWithConfig(key, posthog.Config{
34 Endpoint: endpoint,
39 Endpoint: endpoint,
internal/projects/projects.gonew file+126
@@ -0,0 +1,126 @@
1+package projects
2+
3+import (
4+ "encoding/json"
5+ "os"
6+ "path/filepath"
7+ "slices"
8+ "sync"
9+ "time"
10+
11+ "github.com/charmbracelet/crush/internal/config"
12+)
13+
14+const projectsFileName = "projects.json"
15+
16+// Project represents a tracked project directory.
17+type Project struct {
18+ Path string `json:"path"`
19+ DataDir string `json:"data_dir"`
20+ LastAccessed time.Time `json:"last_accessed"`
21+}
22+
23+// ProjectList holds the list of tracked projects.
24+type ProjectList struct {
25+ Projects []Project `json:"projects"`
26+}
27+
28+var mu sync.Mutex
29+
30+// projectsFilePath returns the path to the projects.json file.
31+func projectsFilePath() string {
32+ return filepath.Join(filepath.Dir(config.GlobalConfigData()), projectsFileName)
33+}
34+
35+// Load reads the projects list from disk.
36+func Load() (*ProjectList, error) {
37+ mu.Lock()
38+ defer mu.Unlock()
39+
40+ path := projectsFilePath()
41+ data, err := os.ReadFile(path)
42+ if err != nil {
43+ if os.IsNotExist(err) {
44+ return &ProjectList{Projects: []Project{}}, nil
45+ }
46+ return nil, err
47+ }
48+
49+ var list ProjectList
50+ if err := json.Unmarshal(data, &list); err != nil {
51+ return nil, err
52+ }
53+
54+ return &list, nil
55+}
56+
57+// Save writes the projects list to disk.
58+func Save(list *ProjectList) error {
59+ mu.Lock()
60+ defer mu.Unlock()
61+
62+ path := projectsFilePath()
63+
64+ // Ensure directory exists
65+ if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
66+ return err
67+ }
68+
69+ data, err := json.MarshalIndent(list, "", " ")
70+ if err != nil {
71+ return err
72+ }
73+
74+ return os.WriteFile(path, data, 0o600)
75+}
76+
77+// Register adds or updates a project in the list.
78+func Register(workingDir, dataDir string) error {
79+ list, err := Load()
80+ if err != nil {
81+ return err
82+ }
83+
84+ now := time.Now().UTC()
85+
86+ // Check if project already exists
87+ found := false
88+ for i, p := range list.Projects {
89+ if p.Path == workingDir {
90+ list.Projects[i].DataDir = dataDir
91+ list.Projects[i].LastAccessed = now
92+ found = true
93+ break
94+ }
95+ }
96+
97+ if !found {
98+ list.Projects = append(list.Projects, Project{
99+ Path: workingDir,
100+ DataDir: dataDir,
101+ LastAccessed: now,
102+ })
103+ }
104+
105+ // Sort by last accessed (most recent first)
106+ slices.SortFunc(list.Projects, func(a, b Project) int {
107+ if a.LastAccessed.After(b.LastAccessed) {
108+ return -1
109+ }
110+ if a.LastAccessed.Before(b.LastAccessed) {
111+ return 1
112+ }
113+ return 0
114+ })
115+
116+ return Save(list)
117+}
118+
119+// List returns all tracked projects sorted by last accessed.
120+func List() ([]Project, error) {
121+ list, err := Load()
122+ if err != nil {
123+ return nil, err
124+ }
125+ return list.Projects, nil
126+}
internal/projects/projects_test.gonew file+180
@@ -0,0 +1,180 @@
1+package projects
2+
3+import (
4+ "path/filepath"
5+ "testing"
6+ "time"
7+)
8+
9+func TestRegisterAndList(t *testing.T) {
10+ // Create a temporary directory for the test
11+ tmpDir := t.TempDir()
12+
13+ // Override the projects file path for testing
14+ t.Setenv("XDG_DATA_HOME", tmpDir)
15+
16+ // Test registering a project
17+ err := Register("/home/user/project1", "/home/user/project1/.crush")
18+ if err != nil {
19+ t.Fatalf("Register failed: %v", err)
20+ }
21+
22+ // List projects
23+ projects, err := List()
24+ if err != nil {
25+ t.Fatalf("List failed: %v", err)
26+ }
27+
28+ if len(projects) != 1 {
29+ t.Fatalf("Expected 1 project, got %d", len(projects))
30+ }
31+
32+ if projects[0].Path != "/home/user/project1" {
33+ t.Errorf("Expected path /home/user/project1, got %s", projects[0].Path)
34+ }
35+
36+ if projects[0].DataDir != "/home/user/project1/.crush" {
37+ t.Errorf("Expected data_dir /home/user/project1/.crush, got %s", projects[0].DataDir)
38+ }
39+
40+ // Register another project
41+ err = Register("/home/user/project2", "/home/user/project2/.crush")
42+ if err != nil {
43+ t.Fatalf("Register failed: %v", err)
44+ }
45+
46+ projects, err = List()
47+ if err != nil {
48+ t.Fatalf("List failed: %v", err)
49+ }
50+
51+ if len(projects) != 2 {
52+ t.Fatalf("Expected 2 projects, got %d", len(projects))
53+ }
54+
55+ // Most recent should be first
56+ if projects[0].Path != "/home/user/project2" {
57+ t.Errorf("Expected most recent project first, got %s", projects[0].Path)
58+ }
59+}
60+
61+func TestRegisterUpdatesExisting(t *testing.T) {
62+ tmpDir := t.TempDir()
63+ t.Setenv("XDG_DATA_HOME", tmpDir)
64+
65+ // Register a project
66+ err := Register("/home/user/project1", "/home/user/project1/.crush")
67+ if err != nil {
68+ t.Fatalf("Register failed: %v", err)
69+ }
70+
71+ projects, _ := List()
72+ firstAccess := projects[0].LastAccessed
73+
74+ // Wait a bit and re-register
75+ time.Sleep(10 * time.Millisecond)
76+
77+ err = Register("/home/user/project1", "/home/user/project1/.crush-new")
78+ if err != nil {
79+ t.Fatalf("Register failed: %v", err)
80+ }
81+
82+ projects, _ = List()
83+
84+ if len(projects) != 1 {
85+ t.Fatalf("Expected 1 project after update, got %d", len(projects))
86+ }
87+
88+ if projects[0].DataDir != "/home/user/project1/.crush-new" {
89+ t.Errorf("Expected updated data_dir, got %s", projects[0].DataDir)
90+ }
91+
92+ if !projects[0].LastAccessed.After(firstAccess) {
93+ t.Error("Expected LastAccessed to be updated")
94+ }
95+}
96+
97+func TestLoadEmptyFile(t *testing.T) {
98+ tmpDir := t.TempDir()
99+ t.Setenv("XDG_DATA_HOME", tmpDir)
100+
101+ // List before any projects exist
102+ projects, err := List()
103+ if err != nil {
104+ t.Fatalf("List failed: %v", err)
105+ }
106+
107+ if len(projects) != 0 {
108+ t.Errorf("Expected 0 projects, got %d", len(projects))
109+ }
110+}
111+
112+func TestProjectsFilePath(t *testing.T) {
113+ tmpDir := t.TempDir()
114+ t.Setenv("XDG_DATA_HOME", tmpDir)
115+
116+ expected := filepath.Join(tmpDir, "crush", "projects.json")
117+ actual := projectsFilePath()
118+
119+ if actual != expected {
120+ t.Errorf("Expected %s, got %s", expected, actual)
121+ }
122+}
123+
124+func TestRegisterWithParentDataDir(t *testing.T) {
125+ tmpDir := t.TempDir()
126+ t.Setenv("XDG_DATA_HOME", tmpDir)
127+
128+ // Register a project where .crush is in a parent directory.
129+ // e.g., working in /home/user/monorepo/packages/app but .crush is at /home/user/monorepo/.crush
130+ err := Register("/home/user/monorepo/packages/app", "/home/user/monorepo/.crush")
131+ if err != nil {
132+ t.Fatalf("Register failed: %v", err)
133+ }
134+
135+ projects, err := List()
136+ if err != nil {
137+ t.Fatalf("List failed: %v", err)
138+ }
139+
140+ if len(projects) != 1 {
141+ t.Fatalf("Expected 1 project, got %d", len(projects))
142+ }
143+
144+ if projects[0].Path != "/home/user/monorepo/packages/app" {
145+ t.Errorf("Expected path /home/user/monorepo/packages/app, got %s", projects[0].Path)
146+ }
147+
148+ if projects[0].DataDir != "/home/user/monorepo/.crush" {
149+ t.Errorf("Expected data_dir /home/user/monorepo/.crush, got %s", projects[0].DataDir)
150+ }
151+}
152+
153+func TestRegisterWithExternalDataDir(t *testing.T) {
154+ tmpDir := t.TempDir()
155+ t.Setenv("XDG_DATA_HOME", tmpDir)
156+
157+ // Register a project where .crush is in a completely different location.
158+ // e.g., project at /home/user/project but data stored at /var/data/crush/myproject
159+ err := Register("/home/user/project", "/var/data/crush/myproject")
160+ if err != nil {
161+ t.Fatalf("Register failed: %v", err)
162+ }
163+
164+ projects, err := List()
165+ if err != nil {
166+ t.Fatalf("List failed: %v", err)
167+ }
168+
169+ if len(projects) != 1 {
170+ t.Fatalf("Expected 1 project, got %d", len(projects))
171+ }
172+
173+ if projects[0].Path != "/home/user/project" {
174+ t.Errorf("Expected path /home/user/project, got %s", projects[0].Path)
175+ }
176+
177+ if projects[0].DataDir != "/var/data/crush/myproject" {
178+ t.Errorf("Expected data_dir /var/data/crush/myproject, got %s", projects[0].DataDir)
179+ }
180+}
internal/tui/components/chat/messages/messages.go+1−1
@@ -311,7 +311,7 @@ func (m *messageCmp) renderThinkingContent() string {
311 }
311 }
312 }
312 }
313 lineStyle := t.S().Subtle.Background(t.BgBaseLighter)
313 lineStyle := t.S().Subtle.Background(t.BgBaseLighter)
314− result := lineStyle.Width(m.textWidth()).Padding(0, 1).Render(m.thinkingViewport.View())
314+ result := lineStyle.Width(m.textWidth()).Padding(0, 1, 0, 0).Render(m.thinkingViewport.View())
315 if footer != "" {
315 if footer != "" {
316 result += "\n\n" + footer
316 result += "\n\n" + footer
317 }
317 }
internal/tui/components/dialogs/commands/arguments.go+3−12
@@ -11,6 +11,7 @@ import (
11 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
11 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
12 "github.com/charmbracelet/crush/internal/tui/styles"
12 "github.com/charmbracelet/crush/internal/tui/styles"
13 "github.com/charmbracelet/crush/internal/tui/util"
13 "github.com/charmbracelet/crush/internal/tui/util"
14+ "github.com/charmbracelet/crush/internal/uicmd"
14)
15)
15
16
16const (
17const (
@@ -18,20 +19,10 @@ const (
18)
19)
19
20
20// ShowArgumentsDialogMsg is a message that is sent to show the arguments dialog.
21// ShowArgumentsDialogMsg is a message that is sent to show the arguments dialog.
21−type ShowArgumentsDialogMsg struct {
22+type ShowArgumentsDialogMsg = uicmd.ShowArgumentsDialogMsg
22− CommandID string
23− Description string
24− ArgNames []string
25− OnSubmit func(args map[string]string) tea.Cmd
26−}
27
23
28// CloseArgumentsDialogMsg is a message that is sent when the arguments dialog is closed.
24// CloseArgumentsDialogMsg is a message that is sent when the arguments dialog is closed.
29−type CloseArgumentsDialogMsg struct {
25+type CloseArgumentsDialogMsg = uicmd.CloseArgumentsDialogMsg
30− Submit bool
31− CommandID string
32− Content string
33− Args map[string]string
34−}
35
26
36// CommandArgumentsDialog represents the commands dialog.
27// CommandArgumentsDialog represents the commands dialog.
37type CommandArgumentsDialog interface {
28type CommandArgumentsDialog interface {
internal/tui/components/dialogs/commands/commands.go+9−10
@@ -23,6 +23,7 @@ import (
23 "github.com/charmbracelet/crush/internal/tui/exp/list"
23 "github.com/charmbracelet/crush/internal/tui/exp/list"
24 "github.com/charmbracelet/crush/internal/tui/styles"
24 "github.com/charmbracelet/crush/internal/tui/styles"
25 "github.com/charmbracelet/crush/internal/tui/util"
25 "github.com/charmbracelet/crush/internal/tui/util"
26+ "github.com/charmbracelet/crush/internal/uicmd"
26)
27)
27
28
28const (
29const (
@@ -44,13 +45,11 @@ const (
44type listModel = list.FilterableList[list.CompletionItem[Command]]
45type listModel = list.FilterableList[list.CompletionItem[Command]]
45
46
46// Command represents a command that can be executed
47// Command represents a command that can be executed
47−type Command struct {
48+type (
48− ID string
49+ Command = uicmd.Command
49− Title string
50+ CommandRunCustomMsg = uicmd.CommandRunCustomMsg
50− Description string
51+ ShowMCPPromptArgumentsDialogMsg = uicmd.ShowMCPPromptArgumentsDialogMsg
51− Shortcut string // Optional shortcut for the command
52+)
52− Handler func(cmd Command) tea.Cmd
53−}
54
53
55// CommandsDialog represents the commands dialog.
54// CommandsDialog represents the commands dialog.
56type CommandsDialog interface {
55type CommandsDialog interface {
@@ -121,12 +120,12 @@ func NewCommandDialog(sessionID string) CommandsDialog {
121}
120}
122
121
123func (c *commandDialogCmp) Init() tea.Cmd {
122func (c *commandDialogCmp) Init() tea.Cmd {
124− commands, err := LoadCustomCommands()
123+ commands, err := uicmd.LoadCustomCommands()
125 if err != nil {
124 if err != nil {
126 return util.ReportError(err)
125 return util.ReportError(err)
127 }
126 }
128 c.userCommands = commands
127 c.userCommands = commands
129− c.mcpPrompts.SetSlice(loadMCPPrompts())
128+ c.mcpPrompts.SetSlice(uicmd.LoadMCPPrompts())
130 return c.setCommandType(c.selected)
129 return c.setCommandType(c.selected)
131}
130}
132
131
@@ -142,7 +141,7 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
142 case pubsub.Event[mcp.Event]:
141 case pubsub.Event[mcp.Event]:
143 // Reload MCP prompts when MCP state changes
142 // Reload MCP prompts when MCP state changes
144 if msg.Type == pubsub.UpdatedEvent {
143 if msg.Type == pubsub.UpdatedEvent {
145− c.mcpPrompts.SetSlice(loadMCPPrompts())
144+ c.mcpPrompts.SetSlice(uicmd.LoadMCPPrompts())
146 // If we're currently viewing MCP prompts, refresh the list
145 // If we're currently viewing MCP prompts, refresh the list
147 if c.selected == MCPPrompts {
146 if c.selected == MCPPrompts {
148 return c, c.setCommandType(MCPPrompts)
147 return c, c.setCommandType(MCPPrompts)
internal/tui/components/dialogs/commands/loader.go → internal/uicmd/uicmd.gorenamed+34−3
@@ -1,4 +1,7 @@
1−package commands
1+// Package uicmd provides functionality to load and handle custom commands
2+// from markdown files and MCP prompts.
3+// TODO: Move this into internal/ui after refactoring.
4+package uicmd
2
5
3import (
6import (
4 "cmp"
7 "cmp"
@@ -18,6 +21,31 @@ import (
18 "github.com/charmbracelet/crush/internal/tui/util"
21 "github.com/charmbracelet/crush/internal/tui/util"
19)
22)
20
23
24+// Command represents a command that can be executed
25+type Command struct {
26+ ID string
27+ Title string
28+ Description string
29+ Shortcut string // Optional shortcut for the command
30+ Handler func(cmd Command) tea.Cmd
31+}
32+
33+// ShowArgumentsDialogMsg is a message that is sent to show the arguments dialog.
34+type ShowArgumentsDialogMsg struct {
35+ CommandID string
36+ Description string
37+ ArgNames []string
38+ OnSubmit func(args map[string]string) tea.Cmd
39+}
40+
41+// CloseArgumentsDialogMsg is a message that is sent when the arguments dialog is closed.
42+type CloseArgumentsDialogMsg struct {
43+ Submit bool
44+ CommandID string
45+ Content string
46+ Args map[string]string
47+}
48+
21const (
49const (
22 userCommandPrefix = "user:"
50 userCommandPrefix = "user:"
23 projectCommandPrefix = "project:"
51 projectCommandPrefix = "project:"
@@ -35,7 +63,10 @@ type commandSource struct {
35}
63}
36
64
37func LoadCustomCommands() ([]Command, error) {
65func LoadCustomCommands() ([]Command, error) {
38− cfg := config.Get()
66+ return LoadCustomCommandsFromConfig(config.Get())
67+}
68+
69+func LoadCustomCommandsFromConfig(cfg *config.Config) ([]Command, error) {
39 if cfg == nil {
70 if cfg == nil {
40 return nil, fmt.Errorf("config not loaded")
71 return nil, fmt.Errorf("config not loaded")
41 }
72 }
@@ -221,7 +252,7 @@ type CommandRunCustomMsg struct {
221 Content string
252 Content string
222}
253}
223
254
224−func loadMCPPrompts() []Command {
255+func LoadMCPPrompts() []Command {
225 var commands []Command
256 var commands []Command
226 for mcpName, prompts := range mcp.Prompts() {
257 for mcpName, prompts := range mcp.Prompts() {
227 for _, prompt := range prompts {
258 for _, prompt := range prompts {