Compare commits

..
20 changed files with 830 additions and 63 deletions
+377 -6
View File
@@ -328,15 +328,386 @@ Use nanobot for a real task, report what broke, and then pick a focused improvem
- Browse [open issues](https://github.com/HKUDS/nanobot/issues) for problems to investigate.
- Open a [pull request](https://github.com/HKUDS/nanobot/pulls) for a focused fix or integration.
## Contact
## Maintainers
Nanobot was started by [Xubin Ren](https://github.com/re-bin) as a personal open-source project and is now maintained collaboratively with contributors from the open-source community. Feel free to contact [xubinrencs@gmail.com](mailto:xubinrencs@gmail.com) for questions, ideas, or collaboration.
<table>
<tr>
<td align="center"><a href="https://github.com/re-bin"><img src="https://avatars.githubusercontent.com/u/52506698?v=4&s=80" width="80" height="80" alt="Xubin Ren"><br><strong>Xubin Ren</strong></a><br><a href="https://x.com/xubinrencs"><img src="https://img.shields.io/badge/@xubinrencs-000000?style=flat&logo=x&logoColor=white" alt="Xubin Ren on X"></a></td>
<td align="center"><a href="https://github.com/chengyongru"><img src="https://avatars.githubusercontent.com/u/61816729?v=4&s=80" width="80" height="80" alt="Yongru Chen"><br><strong>Yongru Chen</strong></a><br><a href="https://x.com/chengyongru"><img src="https://img.shields.io/badge/@chengyongru-000000?style=flat&logo=x&logoColor=white" alt="Yongru Chen on X"></a></td>
</tr>
</table>
### Contributors
## Community Contributors
<a href="https://github.com/HKUDS/nanobot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=HKUDS/nanobot&max=100&columns=12&updated=20260210" alt="Contributors" />
</a>
<!-- contributors:start -->
<p>
<a href="https://github.com/Athemis"><img src="https://avatars.githubusercontent.com/u/552653?v=4&s=48" width="48" height="48" alt="Athemis"></a>
<a href="https://github.com/axelray-dev"><img src="https://avatars.githubusercontent.com/u/110029405?v=4&s=48" width="48" height="48" alt="axelray-dev"></a>
<a href="https://github.com/yorkhellen"><img src="https://avatars.githubusercontent.com/u/8706550?v=4&s=48" width="48" height="48" alt="yorkhellen"></a>
<a href="https://github.com/04cb"><img src="https://avatars.githubusercontent.com/u/111667698?v=4&s=48" width="48" height="48" alt="04cb"></a>
<a href="https://github.com/santhreal"><img src="https://avatars.githubusercontent.com/u/64453045?v=4&s=48" width="48" height="48" alt="santhreal"></a>
<a href="https://github.com/yu-xin-c"><img src="https://avatars.githubusercontent.com/u/175149126?v=4&s=48" width="48" height="48" alt="yu-xin-c"></a>
<a href="https://github.com/xcosmosbox"><img src="https://avatars.githubusercontent.com/u/56502269?v=4&s=48" width="48" height="48" alt="xcosmosbox"></a>
<a href="https://github.com/kunalk16"><img src="https://avatars.githubusercontent.com/u/5303824?v=4&s=48" width="48" height="48" alt="kunalk16"></a>
<a href="https://github.com/chaohuang-ai"><img src="https://avatars.githubusercontent.com/u/204865953?v=4&s=48" width="48" height="48" alt="chaohuang-ai"></a>
<a href="https://github.com/zayfod"><img src="https://avatars.githubusercontent.com/u/1811339?v=4&s=48" width="48" height="48" alt="zayfod"></a>
<a href="https://github.com/nikolasdehor"><img src="https://avatars.githubusercontent.com/u/116851567?v=4&s=48" width="48" height="48" alt="nikolasdehor"></a>
<a href="https://github.com/JiajunBernoulli"><img src="https://avatars.githubusercontent.com/u/45968640?v=4&s=48" width="48" height="48" alt="JiajunBernoulli"></a>
<a href="https://github.com/flobo3"><img src="https://avatars.githubusercontent.com/u/268352850?v=4&s=48" width="48" height="48" alt="flobo3"></a>
<a href="https://github.com/hamb1y"><img src="https://avatars.githubusercontent.com/u/88080063?v=4&s=48" width="48" height="48" alt="hamb1y"></a>
<a href="https://github.com/SergioSV96"><img src="https://avatars.githubusercontent.com/u/20419761?v=4&s=48" width="48" height="48" alt="SergioSV96"></a>
<a href="https://github.com/KDB-Wind"><img src="https://avatars.githubusercontent.com/u/271925278?v=4&s=48" width="48" height="48" alt="KDB-Wind"></a>
<a href="https://github.com/morandot"><img src="https://avatars.githubusercontent.com/u/274257964?v=4&s=48" width="48" height="48" alt="morandot"></a>
<a href="https://github.com/coldxiangyu163"><img src="https://avatars.githubusercontent.com/u/134986317?v=4&s=48" width="48" height="48" alt="coldxiangyu163"></a>
<a href="https://github.com/boogieLing"><img src="https://avatars.githubusercontent.com/u/64551706?v=4&s=48" width="48" height="48" alt="boogieLing"></a>
<a href="https://github.com/michaelxer"><img src="https://avatars.githubusercontent.com/u/52305679?v=4&s=48" width="48" height="48" alt="michaelxer"></a>
<a href="https://github.com/aiguozhi123456"><img src="https://avatars.githubusercontent.com/u/126325311?v=4&s=48" width="48" height="48" alt="aiguozhi123456"></a>
<a href="https://github.com/pinhua33"><img src="https://avatars.githubusercontent.com/u/251483507?v=4&s=48" width="48" height="48" alt="pinhua33"></a>
<a href="https://github.com/pixan-ai"><img src="https://avatars.githubusercontent.com/u/218441143?v=4&s=48" width="48" height="48" alt="pixan-ai"></a>
<a href="https://github.com/hussein1362"><img src="https://avatars.githubusercontent.com/u/49703886?v=4&s=48" width="48" height="48" alt="hussein1362"></a>
<a href="https://github.com/alekwo"><img src="https://avatars.githubusercontent.com/u/24917047?v=4&s=48" width="48" height="48" alt="alekwo"></a>
<a href="https://github.com/haosenwang1018"><img src="https://avatars.githubusercontent.com/u/167664334?v=4&s=48" width="48" height="48" alt="haosenwang1018"></a>
<a href="https://github.com/IlyaGusev"><img src="https://avatars.githubusercontent.com/u/2670295?v=4&s=48" width="48" height="48" alt="IlyaGusev"></a>
<a href="https://github.com/T3chC0wb0y"><img src="https://avatars.githubusercontent.com/u/68530847?v=4&s=48" width="48" height="48" alt="T3chC0wb0y"></a>
<a href="https://github.com/VITOHJL"><img src="https://avatars.githubusercontent.com/u/166518988?v=4&s=48" width="48" height="48" alt="VITOHJL"></a>
<a href="https://github.com/macroadster"><img src="https://avatars.githubusercontent.com/u/328366?v=4&s=48" width="48" height="48" alt="macroadster"></a>
<a href="https://github.com/Hinotoi-agent"><img src="https://avatars.githubusercontent.com/u/275430060?v=4&s=48" width="48" height="48" alt="Hinotoi-agent"></a>
<a href="https://github.com/kingassune"><img src="https://avatars.githubusercontent.com/u/6126851?v=4&s=48" width="48" height="48" alt="kingassune"></a>
<a href="https://github.com/goodtiding5"><img src="https://avatars.githubusercontent.com/u/179489?v=4&s=48" width="48" height="48" alt="goodtiding5"></a>
<a href="https://github.com/kiplangatkorir"><img src="https://avatars.githubusercontent.com/u/153384040?v=4&s=48" width="48" height="48" alt="kiplangatkorir"></a>
<a href="https://github.com/elkaix"><img src="https://avatars.githubusercontent.com/u/197959891?v=4&s=48" width="48" height="48" alt="elkaix"></a>
<a href="https://github.com/KimGLee"><img src="https://avatars.githubusercontent.com/u/150593189?v=4&s=48" width="48" height="48" alt="KimGLee"></a>
<a href="https://github.com/m11y"><img src="https://avatars.githubusercontent.com/u/1625837?v=4&s=48" width="48" height="48" alt="m11y"></a>
<a href="https://github.com/LingaoM"><img src="https://avatars.githubusercontent.com/u/26378606?v=4&s=48" width="48" height="48" alt="LingaoM"></a>
<a href="https://github.com/DaryeDev"><img src="https://avatars.githubusercontent.com/u/54469750?v=4&s=48" width="48" height="48" alt="DaryeDev"></a>
<a href="https://github.com/CJWTRUST"><img src="https://avatars.githubusercontent.com/u/235565898?v=4&s=48" width="48" height="48" alt="CJWTRUST"></a>
<a href="https://github.com/xzq-xu"><img src="https://avatars.githubusercontent.com/u/53989315?v=4&s=48" width="48" height="48" alt="xzq-xu"></a>
<a href="https://github.com/pikaxinge"><img src="https://avatars.githubusercontent.com/u/68273313?v=4&s=48" width="48" height="48" alt="pikaxinge"></a>
<a href="https://github.com/arcdrake22"><img src="https://avatars.githubusercontent.com/u/204617897?v=4&s=48" width="48" height="48" alt="arcdrake22"></a>
<a href="https://github.com/JackLuguibin"><img src="https://avatars.githubusercontent.com/u/46274946?v=4&s=48" width="48" height="48" alt="JackLuguibin"></a>
<a href="https://github.com/HaisamAbbas"><img src="https://avatars.githubusercontent.com/u/95044189?v=4&s=48" width="48" height="48" alt="HaisamAbbas"></a>
<a href="https://github.com/anunay999"><img src="https://avatars.githubusercontent.com/u/16853513?v=4&s=48" width="48" height="48" alt="anunay999"></a>
<a href="https://github.com/flaviovs"><img src="https://avatars.githubusercontent.com/u/1832699?v=4&s=48" width="48" height="48" alt="flaviovs"></a>
<a href="https://github.com/C-Li"><img src="https://avatars.githubusercontent.com/u/20661667?v=4&s=48" width="48" height="48" alt="C-Li"></a>
<a href="https://github.com/Ho1yShif"><img src="https://avatars.githubusercontent.com/u/75815862?v=4&s=48" width="48" height="48" alt="Ho1yShif"></a>
<a href="https://github.com/pjhoberman"><img src="https://avatars.githubusercontent.com/u/37924?v=4&s=48" width="48" height="48" alt="pjhoberman"></a>
<a href="https://github.com/nghiahsgs"><img src="https://avatars.githubusercontent.com/u/24955327?v=4&s=48" width="48" height="48" alt="nghiahsgs"></a>
<a href="https://github.com/Bahtya"><img src="https://avatars.githubusercontent.com/u/34988899?v=4&s=48" width="48" height="48" alt="Bahtya"></a>
<a href="https://github.com/tangtaizong666"><img src="https://avatars.githubusercontent.com/u/212687958?v=4&s=48" width="48" height="48" alt="tangtaizong666"></a>
<a href="https://github.com/XJPeng12"><img src="https://avatars.githubusercontent.com/u/50786186?v=4&s=48" width="48" height="48" alt="XJPeng12"></a>
<a href="https://github.com/yanghan-cyber"><img src="https://avatars.githubusercontent.com/u/188783428?v=4&s=48" width="48" height="48" alt="yanghan-cyber"></a>
<a href="https://github.com/ZhouJ-sh"><img src="https://avatars.githubusercontent.com/u/9983860?v=4&s=48" width="48" height="48" alt="ZhouJ-sh"></a>
<a href="https://github.com/Yuxin-Lou"><img src="https://avatars.githubusercontent.com/u/117000057?v=4&s=48" width="48" height="48" alt="Yuxin-Lou"></a>
<a href="https://github.com/LeoFYH"><img src="https://avatars.githubusercontent.com/u/184173704?v=4&s=48" width="48" height="48" alt="LeoFYH"></a>
<a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude"></a>
<a href="https://github.com/chris-alexander"><img src="https://avatars.githubusercontent.com/u/2815297?v=4&s=48" width="48" height="48" alt="chris-alexander"></a>
<a href="https://github.com/benlenarts"><img src="https://avatars.githubusercontent.com/u/131161?v=4&s=48" width="48" height="48" alt="benlenarts"></a>
<a href="https://github.com/outlook84"><img src="https://avatars.githubusercontent.com/u/96007761?v=4&s=48" width="48" height="48" alt="outlook84"></a>
<a href="https://github.com/Mrart"><img src="https://avatars.githubusercontent.com/u/5235758?v=4&s=48" width="48" height="48" alt="Mrart"></a>
<a href="https://github.com/ramonpaolo"><img src="https://avatars.githubusercontent.com/u/53312850?v=4&s=48" width="48" height="48" alt="ramonpaolo"></a>
<a href="https://github.com/huhu-tiger"><img src="https://avatars.githubusercontent.com/u/76894920?v=4&s=48" width="48" height="48" alt="huhu-tiger"></a>
<a href="https://github.com/tangjiabin"><img src="https://avatars.githubusercontent.com/u/21021242?v=4&s=48" width="48" height="48" alt="tangjiabin"></a>
<a href="https://github.com/yeyitech"><img src="https://avatars.githubusercontent.com/u/231244789?v=4&s=48" width="48" height="48" alt="yeyitech"></a>
<a href="https://github.com/Flinn-X"><img src="https://avatars.githubusercontent.com/u/54433526?v=4&s=48" width="48" height="48" alt="Flinn-X"></a>
<a href="https://github.com/bingqilinweimaotai"><img src="https://avatars.githubusercontent.com/u/111987281?v=4&s=48" width="48" height="48" alt="bingqilinweimaotai"></a>
<a href="https://github.com/Qinnnnnn"><img src="https://avatars.githubusercontent.com/u/14584068?v=4&s=48" width="48" height="48" alt="Qinnnnnn"></a>
<a href="https://github.com/HengWeiBin"><img src="https://avatars.githubusercontent.com/u/45145821?v=4&s=48" width="48" height="48" alt="HengWeiBin"></a>
<a href="https://github.com/waelantar"><img src="https://avatars.githubusercontent.com/u/70063334?v=4&s=48" width="48" height="48" alt="waelantar"></a>
<a href="https://github.com/tanishra"><img src="https://avatars.githubusercontent.com/u/100482827?v=4&s=48" width="48" height="48" alt="tanishra"></a>
<a href="https://github.com/olgagaga"><img src="https://avatars.githubusercontent.com/u/75477960?v=4&s=48" width="48" height="48" alt="olgagaga"></a>
<a href="https://github.com/masterlyj"><img src="https://avatars.githubusercontent.com/u/167326996?v=4&s=48" width="48" height="48" alt="masterlyj"></a>
<a href="https://github.com/xgzlucario"><img src="https://avatars.githubusercontent.com/u/48748794?v=4&s=48" width="48" height="48" alt="xgzlucario"></a>
<a href="https://github.com/dzydzydzy7"><img src="https://avatars.githubusercontent.com/u/32220064?v=4&s=48" width="48" height="48" alt="dzydzydzy7"></a>
<a href="https://github.com/dajiaohuang"><img src="https://avatars.githubusercontent.com/u/108231307?v=4&s=48" width="48" height="48" alt="dajiaohuang"></a>
<a href="https://github.com/concertypin"><img src="https://avatars.githubusercontent.com/u/55056558?v=4&s=48" width="48" height="48" alt="concertypin"></a>
<a href="https://github.com/WangCheng0116"><img src="https://avatars.githubusercontent.com/u/111694270?v=4&s=48" width="48" height="48" alt="WangCheng0116"></a>
<a href="https://github.com/yarikoptic"><img src="https://avatars.githubusercontent.com/u/39889?v=4&s=48" width="48" height="48" alt="yarikoptic"></a>
<a href="https://github.com/lukemilby"><img src="https://avatars.githubusercontent.com/u/966940?v=4&s=48" width="48" height="48" alt="lukemilby"></a>
<a href="https://github.com/gongpx20069"><img src="https://avatars.githubusercontent.com/u/21985921?v=4&s=48" width="48" height="48" alt="gongpx20069"></a>
<a href="https://github.com/tobrien"><img src="https://avatars.githubusercontent.com/u/36787?v=4&s=48" width="48" height="48" alt="tobrien"></a>
<a href="https://github.com/Shiniese"><img src="https://avatars.githubusercontent.com/u/135589327?v=4&s=48" width="48" height="48" alt="Shiniese"></a>
<a href="https://github.com/shawnWXN"><img src="https://avatars.githubusercontent.com/u/47786182?v=4&s=48" width="48" height="48" alt="shawnWXN"></a>
<a href="https://github.com/sbyinin"><img src="https://avatars.githubusercontent.com/u/2064038?v=4&s=48" width="48" height="48" alt="sbyinin"></a>
<a href="https://github.com/nne998"><img src="https://avatars.githubusercontent.com/u/148901?v=4&s=48" width="48" height="48" alt="nne998"></a>
<a href="https://github.com/lahuman"><img src="https://avatars.githubusercontent.com/u/6156679?v=4&s=48" width="48" height="48" alt="lahuman"></a>
<a href="https://github.com/hlgone"><img src="https://avatars.githubusercontent.com/u/152462991?v=4&s=48" width="48" height="48" alt="hlgone"></a>
<a href="https://github.com/franciscomaestre"><img src="https://avatars.githubusercontent.com/u/2027043?v=4&s=48" width="48" height="48" alt="franciscomaestre"></a>
<a href="https://github.com/fat-operator"><img src="https://avatars.githubusercontent.com/u/105777951?v=4&s=48" width="48" height="48" alt="fat-operator"></a>
<a href="https://github.com/shixi-li"><img src="https://avatars.githubusercontent.com/u/40780706?v=4&s=48" width="48" height="48" alt="shixi-li"></a>
<a href="https://github.com/who96"><img src="https://avatars.githubusercontent.com/u/44131846?v=4&s=48" width="48" height="48" alt="who96"></a>
<a href="https://github.com/cyzlmh"><img src="https://avatars.githubusercontent.com/u/24603258?v=4&s=48" width="48" height="48" alt="cyzlmh"></a>
<a href="https://github.com/zhuzhh"><img src="https://avatars.githubusercontent.com/u/41102272?v=4&s=48" width="48" height="48" alt="zhuzhh"></a>
<a href="https://github.com/zpljd258"><img src="https://avatars.githubusercontent.com/u/11162658?v=4&s=48" width="48" height="48" alt="zpljd258"></a>
<a href="https://github.com/cms19859230182-lang"><img src="https://avatars.githubusercontent.com/u/276597748?v=4&s=48" width="48" height="48" alt="cms19859230182-lang"></a>
<a href="https://github.com/amplifierplus"><img src="https://avatars.githubusercontent.com/u/160200579?v=4&s=48" width="48" height="48" alt="amplifierplus"></a>
<a href="https://github.com/LZDQ"><img src="https://avatars.githubusercontent.com/u/45907809?v=4&s=48" width="48" height="48" alt="LZDQ"></a>
<a href="https://github.com/wb213"><img src="https://avatars.githubusercontent.com/u/488412?v=4&s=48" width="48" height="48" alt="wb213"></a>
<a href="https://github.com/shaun0927"><img src="https://avatars.githubusercontent.com/u/70629228?v=4&s=48" width="48" height="48" alt="shaun0927"></a>
<a href="https://github.com/wzrayyy"><img src="https://avatars.githubusercontent.com/u/143233939?v=4&s=48" width="48" height="48" alt="wzrayyy"></a>
<a href="https://github.com/LHMQ878"><img src="https://avatars.githubusercontent.com/u/205284459?v=4&s=48" width="48" height="48" alt="LHMQ878"></a>
<a href="https://github.com/Michael-lhh"><img src="https://avatars.githubusercontent.com/u/41994684?v=4&s=48" width="48" height="48" alt="Michael-lhh"></a>
<a href="https://github.com/Mizarka"><img src="https://avatars.githubusercontent.com/u/253529828?v=4&s=48" width="48" height="48" alt="Mizarka"></a>
<a href="https://github.com/rick2047"><img src="https://avatars.githubusercontent.com/u/16410?v=4&s=48" width="48" height="48" alt="rick2047"></a>
<a href="https://github.com/kuchazi-yy"><img src="https://avatars.githubusercontent.com/u/73976601?v=4&s=48" width="48" height="48" alt="kuchazi-yy"></a>
<a href="https://github.com/Protocol-zero-0"><img src="https://avatars.githubusercontent.com/u/257158451?v=4&s=48" width="48" height="48" alt="Protocol-zero-0"></a>
<a href="https://github.com/subalkum"><img src="https://avatars.githubusercontent.com/u/180379485?v=4&s=48" width="48" height="48" alt="subalkum"></a>
<a href="https://github.com/vystartasv"><img src="https://avatars.githubusercontent.com/u/34380849?v=4&s=48" width="48" height="48" alt="vystartasv"></a>
<a href="https://github.com/ZJUCQR"><img src="https://avatars.githubusercontent.com/u/138299253?v=4&s=48" width="48" height="48" alt="ZJUCQR"></a>
<a href="https://github.com/ZegWe"><img src="https://avatars.githubusercontent.com/u/22636524?v=4&s=48" width="48" height="48" alt="ZegWe"></a>
<a href="https://github.com/ZhangYuanhan-AI"><img src="https://avatars.githubusercontent.com/u/18485270?v=4&s=48" width="48" height="48" alt="ZhangYuanhan-AI"></a>
<a href="https://github.com/chtangwin"><img src="https://avatars.githubusercontent.com/u/8316617?v=4&s=48" width="48" height="48" alt="chtangwin"></a>
<a href="https://github.com/dxtime"><img src="https://avatars.githubusercontent.com/u/8173810?v=4&s=48" width="48" height="48" alt="dxtime"></a>
<a href="https://github.com/ethanclaw"><img src="https://avatars.githubusercontent.com/u/262543029?v=4&s=48" width="48" height="48" alt="ethanclaw"></a>
<a href="https://github.com/WufeiHalf"><img src="https://avatars.githubusercontent.com/u/103879607?v=4&s=48" width="48" height="48" alt="WufeiHalf"></a>
<a href="https://github.com/stutiredboy"><img src="https://avatars.githubusercontent.com/u/345208?v=4&s=48" width="48" height="48" alt="stutiredboy"></a>
<a href="https://github.com/stupidloud"><img src="https://avatars.githubusercontent.com/u/56048681?v=4&s=48" width="48" height="48" alt="stupidloud"></a>
<a href="https://github.com/asif786ka"><img src="https://avatars.githubusercontent.com/u/6130514?v=4&s=48" width="48" height="48" alt="asif786ka"></a>
<a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla"></a>
<a href="https://github.com/cypggs"><img src="https://avatars.githubusercontent.com/u/3694954?v=4&s=48" width="48" height="48" alt="cypggs"></a>
<a href="https://github.com/web-flow"><img src="https://avatars.githubusercontent.com/u/19864447?v=4&s=48" width="48" height="48" alt="web-flow"></a>
<a href="https://github.com/eliumusk"><img src="https://avatars.githubusercontent.com/u/123090877?v=4&s=48" width="48" height="48" alt="eliumusk"></a>
<a href="https://github.com/mikaku9944"><img src="https://avatars.githubusercontent.com/u/66119379?v=4&s=48" width="48" height="48" alt="mikaku9944"></a>
<a href="https://github.com/mamamiyear"><img src="https://avatars.githubusercontent.com/u/14191296?v=4&s=48" width="48" height="48" alt="mamamiyear"></a>
<a href="https://github.com/jr551"><img src="https://avatars.githubusercontent.com/u/2920328?v=4&s=48" width="48" height="48" alt="jr551"></a>
<a href="https://github.com/invictus-z"><img src="https://avatars.githubusercontent.com/u/108621936?v=4&s=48" width="48" height="48" alt="invictus-z"></a>
<a href="https://github.com/imfondof"><img src="https://avatars.githubusercontent.com/u/39022581?v=4&s=48" width="48" height="48" alt="imfondof"></a>
<a href="https://github.com/hyoukadev"><img src="https://avatars.githubusercontent.com/u/17965578?v=4&s=48" width="48" height="48" alt="hyoukadev"></a>
<a href="https://github.com/hata33"><img src="https://avatars.githubusercontent.com/u/79907651?v=4&s=48" width="48" height="48" alt="hata33"></a>
<a href="https://github.com/fengxiaohu"><img src="https://avatars.githubusercontent.com/u/23492381?v=4&s=48" width="48" height="48" alt="fengxiaohu"></a>
<a href="https://github.com/vivganes"><img src="https://avatars.githubusercontent.com/u/2035886?v=4&s=48" width="48" height="48" alt="vivganes"></a>
<a href="https://github.com/themavik"><img src="https://avatars.githubusercontent.com/u/179817126?v=4&s=48" width="48" height="48" alt="themavik"></a>
<a href="https://github.com/flyzstu"><img src="https://avatars.githubusercontent.com/u/94161727?v=4&s=48" width="48" height="48" alt="flyzstu"></a>
<a href="https://github.com/pikaqqqqqq"><img src="https://avatars.githubusercontent.com/u/20340136?v=4&s=48" width="48" height="48" alt="pikaqqqqqq"></a>
<a href="https://github.com/wyjBot"><img src="https://avatars.githubusercontent.com/u/70993189?v=4&s=48" width="48" height="48" alt="wyjBot"></a>
<a href="https://github.com/pblocz"><img src="https://avatars.githubusercontent.com/u/9288574?v=4&s=48" width="48" height="48" alt="pblocz"></a>
<a href="https://github.com/niradler"><img src="https://avatars.githubusercontent.com/u/6292980?v=4&s=48" width="48" height="48" alt="niradler"></a>
<a href="https://github.com/longle325"><img src="https://avatars.githubusercontent.com/u/140832783?v=4&s=48" width="48" height="48" alt="longle325"></a>
<a href="https://github.com/primit1v0"><img src="https://avatars.githubusercontent.com/u/119784372?v=4&s=48" width="48" height="48" alt="primit1v0"></a>
<a href="https://github.com/honjiaxuan"><img src="https://avatars.githubusercontent.com/u/13818528?v=4&s=48" width="48" height="48" alt="honjiaxuan"></a>
<a href="https://github.com/DeeJ4yNg"><img src="https://avatars.githubusercontent.com/u/99658722?v=4&s=48" width="48" height="48" alt="DeeJ4yNg"></a>
<a href="https://github.com/danielphang"><img src="https://avatars.githubusercontent.com/u/1204069?v=4&s=48" width="48" height="48" alt="danielphang"></a>
<a href="https://github.com/yanalialiuk"><img src="https://avatars.githubusercontent.com/u/193742981?v=4&s=48" width="48" height="48" alt="yanalialiuk"></a>
<a href="https://github.com/zhouzhuojie"><img src="https://avatars.githubusercontent.com/u/658840?v=4&s=48" width="48" height="48" alt="zhouzhuojie"></a>
<a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x"></a>
<a href="https://github.com/yrk111222"><img src="https://avatars.githubusercontent.com/u/185151020?v=4&s=48" width="48" height="48" alt="yrk111222"></a>
<a href="https://github.com/Xerxes-cn"><img src="https://avatars.githubusercontent.com/u/58462889?v=4&s=48" width="48" height="48" alt="Xerxes-cn"></a>
<a href="https://github.com/suger-m"><img src="https://avatars.githubusercontent.com/u/240725677?v=4&s=48" width="48" height="48" alt="suger-m"></a>
<a href="https://github.com/mengyhang"><img src="https://avatars.githubusercontent.com/u/148381938?v=4&s=48" width="48" height="48" alt="mengyhang"></a>
<a href="https://github.com/Liwx1014"><img src="https://avatars.githubusercontent.com/u/186271593?v=4&s=48" width="48" height="48" alt="Liwx1014"></a>
<a href="https://github.com/Shizoqua"><img src="https://avatars.githubusercontent.com/u/136805224?v=4&s=48" width="48" height="48" alt="Shizoqua"></a>
<a href="https://github.com/KailBug"><img src="https://avatars.githubusercontent.com/u/66873219?v=4&s=48" width="48" height="48" alt="KailBug"></a>
<a href="https://github.com/19emtuck"><img src="https://avatars.githubusercontent.com/u/956861?v=4&s=48" width="48" height="48" alt="19emtuck"></a>
<a href="https://github.com/tsubasakong"><img src="https://avatars.githubusercontent.com/u/97429702?v=4&s=48" width="48" height="48" alt="tsubasakong"></a>
<a href="https://github.com/wseng"><img src="https://avatars.githubusercontent.com/u/6572161?v=4&s=48" width="48" height="48" alt="wseng"></a>
<a href="https://github.com/3927o"><img src="https://avatars.githubusercontent.com/u/53431636?v=4&s=48" width="48" height="48" alt="3927o"></a>
<a href="https://github.com/FloRainRJY"><img src="https://avatars.githubusercontent.com/u/146079207?v=4&s=48" width="48" height="48" alt="FloRainRJY"></a>
<a href="https://github.com/agbocsardi"><img src="https://avatars.githubusercontent.com/u/17645046?v=4&s=48" width="48" height="48" alt="agbocsardi"></a>
<a href="https://github.com/JilunSun7274"><img src="https://avatars.githubusercontent.com/u/268303062?v=4&s=48" width="48" height="48" alt="JilunSun7274"></a>
<a href="https://github.com/dvejmz"><img src="https://avatars.githubusercontent.com/u/9487006?v=4&s=48" width="48" height="48" alt="dvejmz"></a>
<a href="https://github.com/ddadaal"><img src="https://avatars.githubusercontent.com/u/8363856?v=4&s=48" width="48" height="48" alt="ddadaal"></a>
<a href="https://github.com/jiehaoZ"><img src="https://avatars.githubusercontent.com/u/51368211?v=4&s=48" width="48" height="48" alt="jiehaoZ"></a>
<a href="https://github.com/Lbin91"><img src="https://avatars.githubusercontent.com/u/26209763?v=4&s=48" width="48" height="48" alt="Lbin91"></a>
<a href="https://github.com/Alex-yang00"><img src="https://avatars.githubusercontent.com/u/57132813?v=4&s=48" width="48" height="48" alt="Alex-yang00"></a>
<a href="https://github.com/xek"><img src="https://avatars.githubusercontent.com/u/107911?v=4&s=48" width="48" height="48" alt="xek"></a>
<a href="https://github.com/Harvey-Mackie"><img src="https://avatars.githubusercontent.com/u/38426388?v=4&s=48" width="48" height="48" alt="Harvey-Mackie"></a>
<a href="https://github.com/chenyahui"><img src="https://avatars.githubusercontent.com/u/6067594?v=4&s=48" width="48" height="48" alt="chenyahui"></a>
<a href="https://github.com/angleyanalbedo"><img src="https://avatars.githubusercontent.com/u/100198247?v=4&s=48" width="48" height="48" alt="angleyanalbedo"></a>
<a href="https://github.com/adabarbulescu"><img src="https://avatars.githubusercontent.com/u/94562950?v=4&s=48" width="48" height="48" alt="adabarbulescu"></a>
<a href="https://github.com/yoheinishikubo"><img src="https://avatars.githubusercontent.com/u/17715848?v=4&s=48" width="48" height="48" alt="yoheinishikubo"></a>
<a href="https://github.com/WormW"><img src="https://avatars.githubusercontent.com/u/24667814?v=4&s=48" width="48" height="48" alt="WormW"></a>
<a href="https://github.com/WhalerO"><img src="https://avatars.githubusercontent.com/u/68461696?v=4&s=48" width="48" height="48" alt="WhalerO"></a>
<a href="https://github.com/thomya"><img src="https://avatars.githubusercontent.com/u/5235056?v=4&s=48" width="48" height="48" alt="thomya"></a>
<a href="https://github.com/Tejas1Koli"><img src="https://avatars.githubusercontent.com/u/181818824?v=4&s=48" width="48" height="48" alt="Tejas1Koli"></a>
<a href="https://github.com/Seeratul"><img src="https://avatars.githubusercontent.com/u/126798754?v=4&s=48" width="48" height="48" alt="Seeratul"></a>
<a href="https://github.com/SJK-py"><img src="https://avatars.githubusercontent.com/u/201669535?v=4&s=48" width="48" height="48" alt="SJK-py"></a>
<a href="https://github.com/RongLei-intel"><img src="https://avatars.githubusercontent.com/u/81341556?v=4&s=48" width="48" height="48" alt="RongLei-intel"></a>
<a href="https://github.com/QQQ300kuai"><img src="https://avatars.githubusercontent.com/u/55626566?v=4&s=48" width="48" height="48" alt="QQQ300kuai"></a>
<a href="https://github.com/MiguelPF"><img src="https://avatars.githubusercontent.com/u/1163236?v=4&s=48" width="48" height="48" alt="MiguelPF"></a>
<a href="https://github.com/mterhar"><img src="https://avatars.githubusercontent.com/u/938684?v=4&s=48" width="48" height="48" alt="mterhar"></a>
<a href="https://github.com/Pringlas"><img src="https://avatars.githubusercontent.com/u/28577663?v=4&s=48" width="48" height="48" alt="Pringlas"></a>
<a href="https://github.com/pjbakker"><img src="https://avatars.githubusercontent.com/u/1267780?v=4&s=48" width="48" height="48" alt="pjbakker"></a>
<a href="https://github.com/luojiaaoo"><img src="https://avatars.githubusercontent.com/u/62821977?v=4&s=48" width="48" height="48" alt="luojiaaoo"></a>
<a href="https://github.com/NearlCrews"><img src="https://avatars.githubusercontent.com/u/23341701?v=4&s=48" width="48" height="48" alt="NearlCrews"></a>
<a href="https://github.com/yongPhone"><img src="https://avatars.githubusercontent.com/u/29919651?v=4&s=48" width="48" height="48" alt="yongPhone"></a>
<a href="https://github.com/ZXGERIC"><img src="https://avatars.githubusercontent.com/u/25354180?v=4&s=48" width="48" height="48" alt="ZXGERIC"></a>
<a href="https://github.com/erikmackinnon"><img src="https://avatars.githubusercontent.com/u/40612473?v=4&s=48" width="48" height="48" alt="erikmackinnon"></a>
<a href="https://github.com/rickererer"><img src="https://avatars.githubusercontent.com/u/289160634?v=4&s=48" width="48" height="48" alt="rickererer"></a>
<a href="https://github.com/ferkans-amir"><img src="https://avatars.githubusercontent.com/u/212877286?v=4&s=48" width="48" height="48" alt="ferkans-amir"></a>
<a href="https://github.com/for13to1"><img src="https://avatars.githubusercontent.com/u/115892874?v=4&s=48" width="48" height="48" alt="for13to1"></a>
<a href="https://github.com/futurist"><img src="https://avatars.githubusercontent.com/u/159167?v=4&s=48" width="48" height="48" alt="futurist"></a>
<a href="https://github.com/Maaannnn"><img src="https://avatars.githubusercontent.com/u/105716414?v=4&s=48" width="48" height="48" alt="Maaannnn"></a>
<a href="https://github.com/rubychilds"><img src="https://avatars.githubusercontent.com/u/1305077?v=4&s=48" width="48" height="48" alt="rubychilds"></a>
<a href="https://github.com/init-new-world"><img src="https://avatars.githubusercontent.com/u/36530844?v=4&s=48" width="48" height="48" alt="init-new-world"></a>
<a href="https://github.com/Idealist17"><img src="https://avatars.githubusercontent.com/u/55554642?v=4&s=48" width="48" height="48" alt="Idealist17"></a>
<a href="https://github.com/gola"><img src="https://avatars.githubusercontent.com/u/31429180?v=4&s=48" width="48" height="48" alt="gola"></a>
<a href="https://github.com/greyishsong"><img src="https://avatars.githubusercontent.com/u/49446254?v=4&s=48" width="48" height="48" alt="greyishsong"></a>
<a href="https://github.com/h4nz4"><img src="https://avatars.githubusercontent.com/u/18464660?v=4&s=48" width="48" height="48" alt="h4nz4"></a>
<a href="https://github.com/hoaresky"><img src="https://avatars.githubusercontent.com/u/25839923?v=4&s=48" width="48" height="48" alt="hoaresky"></a>
<a href="https://github.com/hcanyz"><img src="https://avatars.githubusercontent.com/u/8407922?v=4&s=48" width="48" height="48" alt="hcanyz"></a>
<a href="https://github.com/himax12"><img src="https://avatars.githubusercontent.com/u/122690580?v=4&s=48" width="48" height="48" alt="himax12"></a>
<a href="https://github.com/Zeknes"><img src="https://avatars.githubusercontent.com/u/117632598?v=4&s=48" width="48" height="48" alt="Zeknes"></a>
<a href="https://github.com/ZhihaoZhang97"><img src="https://avatars.githubusercontent.com/u/31653817?v=4&s=48" width="48" height="48" alt="ZhihaoZhang97"></a>
<a href="https://github.com/linziyanleo"><img src="https://avatars.githubusercontent.com/u/24808982?v=4&s=48" width="48" height="48" alt="linziyanleo"></a>
<a href="https://github.com/akinolur"><img src="https://avatars.githubusercontent.com/u/126256260?v=4&s=48" width="48" height="48" alt="akinolur"></a>
<a href="https://github.com/alairjt"><img src="https://avatars.githubusercontent.com/u/678781?v=4&s=48" width="48" height="48" alt="alairjt"></a>
<a href="https://github.com/noki1928"><img src="https://avatars.githubusercontent.com/u/200396425?v=4&s=48" width="48" height="48" alt="noki1928"></a>
<a href="https://github.com/barreler126"><img src="https://avatars.githubusercontent.com/u/129291861?v=4&s=48" width="48" height="48" alt="barreler126"></a>
<a href="https://github.com/bllackhu"><img src="https://avatars.githubusercontent.com/u/194945539?v=4&s=48" width="48" height="48" alt="bllackhu"></a>
<a href="https://github.com/brendanlevy-clarahealth"><img src="https://avatars.githubusercontent.com/u/262887138?v=4&s=48" width="48" height="48" alt="brendanlevy-clarahealth"></a>
<a href="https://github.com/cdkey85"><img src="https://avatars.githubusercontent.com/u/7017432?v=4&s=48" width="48" height="48" alt="cdkey85"></a>
<a href="https://github.com/ducheng121"><img src="https://avatars.githubusercontent.com/u/134901105?v=4&s=48" width="48" height="48" alt="ducheng121"></a>
<a href="https://github.com/SamZhu19921116"><img src="https://avatars.githubusercontent.com/u/39799950?v=4&s=48" width="48" height="48" alt="SamZhu19921116"></a>
<a href="https://github.com/justforyoudear"><img src="https://avatars.githubusercontent.com/u/50271514?v=4&s=48" width="48" height="48" alt="justforyoudear"></a>
<a href="https://github.com/choiking"><img src="https://avatars.githubusercontent.com/u/13400202?v=4&s=48" width="48" height="48" alt="choiking"></a>
<a href="https://github.com/cocolato"><img src="https://avatars.githubusercontent.com/u/35182391?v=4&s=48" width="48" height="48" alt="cocolato"></a>
<a href="https://github.com/luomaohao"><img src="https://avatars.githubusercontent.com/u/36148933?v=4&s=48" width="48" height="48" alt="luomaohao"></a>
<a href="https://github.com/codedragoncom"><img src="https://avatars.githubusercontent.com/u/5353092?v=4&s=48" width="48" height="48" alt="codedragoncom"></a>
<a href="https://github.com/d1ago"><img src="https://avatars.githubusercontent.com/u/143279157?v=4&s=48" width="48" height="48" alt="d1ago"></a>
<a href="https://github.com/daliu858"><img src="https://avatars.githubusercontent.com/u/213729810?v=4&s=48" width="48" height="48" alt="daliu858"></a>
<a href="https://github.com/danielyangfei"><img src="https://avatars.githubusercontent.com/u/9975680?v=4&s=48" width="48" height="48" alt="danielyangfei"></a>
<a href="https://github.com/dingyanyi2019"><img src="https://avatars.githubusercontent.com/u/230930993?v=4&s=48" width="48" height="48" alt="dingyanyi2019"></a>
<a href="https://github.com/djmaze"><img src="https://avatars.githubusercontent.com/u/7229?v=4&s=48" width="48" height="48" alt="djmaze"></a>
<a href="https://github.com/dulltackle"><img src="https://avatars.githubusercontent.com/u/45963660?v=4&s=48" width="48" height="48" alt="dulltackle"></a>
<a href="https://github.com/samyzhh"><img src="https://avatars.githubusercontent.com/u/13554741?v=4&s=48" width="48" height="48" alt="samyzhh"></a>
<a href="https://github.com/shen0122"><img src="https://avatars.githubusercontent.com/u/145903102?v=4&s=48" width="48" height="48" alt="shen0122"></a>
<a href="https://github.com/shenchengtsi"><img src="https://avatars.githubusercontent.com/u/228445050?v=4&s=48" width="48" height="48" alt="shenchengtsi"></a>
<a href="https://github.com/sidkang"><img src="https://avatars.githubusercontent.com/u/6175895?v=4&s=48" width="48" height="48" alt="sidkang"></a>
<a href="https://github.com/skiyo"><img src="https://avatars.githubusercontent.com/u/224273?v=4&s=48" width="48" height="48" alt="skiyo"></a>
<a href="https://github.com/sontianye"><img src="https://avatars.githubusercontent.com/u/162393000?v=4&s=48" width="48" height="48" alt="sontianye"></a>
<a href="https://github.com/spartan077"><img src="https://avatars.githubusercontent.com/u/118879019?v=4&s=48" width="48" height="48" alt="spartan077"></a>
<a href="https://github.com/tercerapersona"><img src="https://avatars.githubusercontent.com/u/16053355?v=4&s=48" width="48" height="48" alt="tercerapersona"></a>
<a href="https://github.com/tlguszz1010"><img src="https://avatars.githubusercontent.com/u/62739187?v=4&s=48" width="48" height="48" alt="tlguszz1010"></a>
<a href="https://github.com/vandazia"><img src="https://avatars.githubusercontent.com/u/56904192?v=4&s=48" width="48" height="48" alt="vandazia"></a>
<a href="https://github.com/vincentchen0x2-dev"><img src="https://avatars.githubusercontent.com/u/262490969?v=4&s=48" width="48" height="48" alt="vincentchen0x2-dev"></a>
<a href="https://github.com/tianrking"><img src="https://avatars.githubusercontent.com/u/10758833?v=4&s=48" width="48" height="48" alt="tianrking"></a>
<a href="https://github.com/wcmolin"><img src="https://avatars.githubusercontent.com/u/11606262?v=4&s=48" width="48" height="48" alt="wcmolin"></a>
<a href="https://github.com/dynames0098"><img src="https://avatars.githubusercontent.com/u/16553686?v=4&s=48" width="48" height="48" alt="dynames0098"></a>
<a href="https://github.com/knightconnorp"><img src="https://avatars.githubusercontent.com/u/143191129?v=4&s=48" width="48" height="48" alt="knightconnorp"></a>
<a href="https://github.com/wymcmh"><img src="https://avatars.githubusercontent.com/u/5070729?v=4&s=48" width="48" height="48" alt="wymcmh"></a>
<a href="https://github.com/weijun-xia"><img src="https://avatars.githubusercontent.com/u/293320877?v=4&s=48" width="48" height="48" alt="weijun-xia"></a>
<a href="https://github.com/yaotutu"><img src="https://avatars.githubusercontent.com/u/21394924?v=4&s=48" width="48" height="48" alt="yaotutu"></a>
<a href="https://github.com/yeounhyeok"><img src="https://avatars.githubusercontent.com/u/141844100?v=4&s=48" width="48" height="48" alt="yeounhyeok"></a>
<a href="https://github.com/Endeavour-Yuan"><img src="https://avatars.githubusercontent.com/u/50094541?v=4&s=48" width="48" height="48" alt="Endeavour-Yuan"></a>
<a href="https://github.com/ziuus"><img src="https://avatars.githubusercontent.com/u/64656661?v=4&s=48" width="48" height="48" alt="ziuus"></a>
<a href="https://github.com/dsxyy"><img src="https://avatars.githubusercontent.com/u/8911760?v=4&s=48" width="48" height="48" alt="dsxyy"></a>
<a href="https://github.com/azhengzz"><img src="https://avatars.githubusercontent.com/u/30361780?v=4&s=48" width="48" height="48" alt="azhengzz"></a>
<a href="https://github.com/jhkim43"><img src="https://avatars.githubusercontent.com/u/139941582?v=4&s=48" width="48" height="48" alt="jhkim43"></a>
<a href="https://github.com/kimkitsuragi26"><img src="https://avatars.githubusercontent.com/u/263307076?v=4&s=48" width="48" height="48" alt="kimkitsuragi26"></a>
<a href="https://github.com/kinchahoy"><img src="https://avatars.githubusercontent.com/u/6504381?v=4&s=48" width="48" height="48" alt="kinchahoy"></a>
<a href="https://github.com/A11Might"><img src="https://avatars.githubusercontent.com/u/38397074?v=4&s=48" width="48" height="48" alt="A11Might"></a>
<a href="https://github.com/kronk307"><img src="https://avatars.githubusercontent.com/u/264627887?v=4&s=48" width="48" height="48" alt="kronk307"></a>
<a href="https://github.com/lailoo"><img src="https://avatars.githubusercontent.com/u/20536249?v=4&s=48" width="48" height="48" alt="lailoo"></a>
<a href="https://github.com/lang07123"><img src="https://avatars.githubusercontent.com/u/7733095?v=4&s=48" width="48" height="48" alt="lang07123"></a>
<a href="https://github.com/tetratorus"><img src="https://avatars.githubusercontent.com/u/4226174?v=4&s=48" width="48" height="48" alt="tetratorus"></a>
<a href="https://github.com/spinvettel"><img src="https://avatars.githubusercontent.com/u/82635206?v=4&s=48" width="48" height="48" alt="spinvettel"></a>
<a href="https://github.com/li-yazhou"><img src="https://avatars.githubusercontent.com/u/17548940?v=4&s=48" width="48" height="48" alt="li-yazhou"></a>
<a href="https://github.com/ALIZE126"><img src="https://avatars.githubusercontent.com/u/79365356?v=4&s=48" width="48" height="48" alt="ALIZE126"></a>
<a href="https://github.com/Rheasilvia"><img src="https://avatars.githubusercontent.com/u/29389840?v=4&s=48" width="48" height="48" alt="Rheasilvia"></a>
<a href="https://github.com/mru4913"><img src="https://avatars.githubusercontent.com/u/31579276?v=4&s=48" width="48" height="48" alt="mru4913"></a>
<a href="https://github.com/mt-huerta"><img src="https://avatars.githubusercontent.com/u/5499466?v=4&s=48" width="48" height="48" alt="mt-huerta"></a>
<a href="https://github.com/mytechdream"><img src="https://avatars.githubusercontent.com/u/114465679?v=4&s=48" width="48" height="48" alt="mytechdream"></a>
<a href="https://github.com/nikube"><img src="https://avatars.githubusercontent.com/u/63295277?v=4&s=48" width="48" height="48" alt="nikube"></a>
<a href="https://github.com/npodbielski"><img src="https://avatars.githubusercontent.com/u/796782?v=4&s=48" width="48" height="48" alt="npodbielski"></a>
<a href="https://github.com/oriengy"><img src="https://avatars.githubusercontent.com/u/50244473?v=4&s=48" width="48" height="48" alt="oriengy"></a>
<a href="https://github.com/popcell"><img src="https://avatars.githubusercontent.com/u/70359868?v=4&s=48" width="48" height="48" alt="popcell"></a>
<a href="https://github.com/qixinbo"><img src="https://avatars.githubusercontent.com/u/6218739?v=4&s=48" width="48" height="48" alt="qixinbo"></a>
<a href="https://github.com/qulllee"><img src="https://avatars.githubusercontent.com/u/113170232?v=4&s=48" width="48" height="48" alt="qulllee"></a>
<a href="https://github.com/rav-melisono"><img src="https://avatars.githubusercontent.com/u/165779938?v=4&s=48" width="48" height="48" alt="rav-melisono"></a>
<a href="https://github.com/razzh7"><img src="https://avatars.githubusercontent.com/u/67299806?v=4&s=48" width="48" height="48" alt="razzh7"></a>
<a href="https://github.com/rise2689"><img src="https://avatars.githubusercontent.com/u/268597299?v=4&s=48" width="48" height="48" alt="rise2689"></a>
<a href="https://github.com/hlibr"><img src="https://avatars.githubusercontent.com/u/5793607?v=4&s=48" width="48" height="48" alt="hlibr"></a>
<a href="https://github.com/gthieleb"><img src="https://avatars.githubusercontent.com/u/21332468?v=4&s=48" width="48" height="48" alt="gthieleb"></a>
<a href="https://github.com/Rafa-Ross"><img src="https://avatars.githubusercontent.com/u/279471146?v=4&s=48" width="48" height="48" alt="Rafa-Ross"></a>
<a href="https://github.com/korruz"><img src="https://avatars.githubusercontent.com/u/79794883?v=4&s=48" width="48" height="48" alt="korruz"></a>
<a href="https://github.com/hyudryu"><img src="https://avatars.githubusercontent.com/u/22283864?v=4&s=48" width="48" height="48" alt="hyudryu"></a>
<a href="https://github.com/breitburg"><img src="https://avatars.githubusercontent.com/u/25728414?v=4&s=48" width="48" height="48" alt="breitburg"></a>
<a href="https://github.com/IlyaSemenov"><img src="https://avatars.githubusercontent.com/u/128121?v=4&s=48" width="48" height="48" alt="IlyaSemenov"></a>
<a href="https://github.com/Tevkanbot"><img src="https://avatars.githubusercontent.com/u/143351134?v=4&s=48" width="48" height="48" alt="Tevkanbot"></a>
<a href="https://github.com/JakeRowe19"><img src="https://avatars.githubusercontent.com/u/117069245?v=4&s=48" width="48" height="48" alt="JakeRowe19"></a>
<a href="https://github.com/JamesWrigley"><img src="https://avatars.githubusercontent.com/u/5361518?v=4&s=48" width="48" height="48" alt="JamesWrigley"></a>
<a href="https://github.com/La-Volpe"><img src="https://avatars.githubusercontent.com/u/5852615?v=4&s=48" width="48" height="48" alt="La-Volpe"></a>
<a href="https://github.com/JavisPeng"><img src="https://avatars.githubusercontent.com/u/18676680?v=4&s=48" width="48" height="48" alt="JavisPeng"></a>
<a href="https://github.com/Jefsky"><img src="https://avatars.githubusercontent.com/u/7386165?v=4&s=48" width="48" height="48" alt="Jefsky"></a>
<a href="https://github.com/letzdoo-js"><img src="https://avatars.githubusercontent.com/u/12003829?v=4&s=48" width="48" height="48" alt="letzdoo-js"></a>
<a href="https://github.com/95256155o"><img src="https://avatars.githubusercontent.com/u/74103710?v=4&s=48" width="48" height="48" alt="95256155o"></a>
<a href="https://github.com/joel611"><img src="https://avatars.githubusercontent.com/u/5180124?v=4&s=48" width="48" height="48" alt="joel611"></a>
<a href="https://github.com/NiceLargeHuo"><img src="https://avatars.githubusercontent.com/u/306099191?v=4&s=48" width="48" height="48" alt="NiceLargeHuo"></a>
<a href="https://github.com/kamalakarrao"><img src="https://avatars.githubusercontent.com/u/15045455?v=4&s=48" width="48" height="48" alt="kamalakarrao"></a>
<a href="https://github.com/KEEPSLAMDUNK"><img src="https://avatars.githubusercontent.com/u/155275575?v=4&s=48" width="48" height="48" alt="KEEPSLAMDUNK"></a>
<a href="https://github.com/krisLu"><img src="https://avatars.githubusercontent.com/u/92515202?v=4&s=48" width="48" height="48" alt="krisLu"></a>
<a href="https://github.com/Krislu1221"><img src="https://avatars.githubusercontent.com/u/258380416?v=4&s=48" width="48" height="48" alt="Krislu1221"></a>
<a href="https://github.com/kyya"><img src="https://avatars.githubusercontent.com/u/13448248?v=4&s=48" width="48" height="48" alt="kyya"></a>
<a href="https://github.com/rreben"><img src="https://avatars.githubusercontent.com/u/4026131?v=4&s=48" width="48" height="48" alt="rreben"></a>
<a href="https://github.com/ATECHPCS"><img src="https://avatars.githubusercontent.com/u/125108010?v=4&s=48" width="48" height="48" alt="ATECHPCS"></a>
<a href="https://github.com/adrianhoehne"><img src="https://avatars.githubusercontent.com/u/19731088?v=4&s=48" width="48" height="48" alt="adrianhoehne"></a>
<a href="https://github.com/Aisht669"><img src="https://avatars.githubusercontent.com/u/36147411?v=4&s=48" width="48" height="48" alt="Aisht669"></a>
<a href="https://github.com/AlbertWang688"><img src="https://avatars.githubusercontent.com/u/36430404?v=4&s=48" width="48" height="48" alt="AlbertWang688"></a>
<a href="https://github.com/AlexanderMerkel"><img src="https://avatars.githubusercontent.com/u/105279319?v=4&s=48" width="48" height="48" alt="AlexanderMerkel"></a>
<a href="https://github.com/khmylov"><img src="https://avatars.githubusercontent.com/u/1044282?v=4&s=48" width="48" height="48" alt="khmylov"></a>
<a href="https://github.com/karimluna"><img src="https://avatars.githubusercontent.com/u/195384419?v=4&s=48" width="48" height="48" alt="karimluna"></a>
<a href="https://github.com/Bayern4ever-dot"><img src="https://avatars.githubusercontent.com/u/67447782?v=4&s=48" width="48" height="48" alt="Bayern4ever-dot"></a>
<a href="https://github.com/abhinavaditya811"><img src="https://avatars.githubusercontent.com/u/40894851?v=4&s=48" width="48" height="48" alt="abhinavaditya811"></a>
<a href="https://github.com/bjoshuanoah"><img src="https://avatars.githubusercontent.com/u/1885253?v=4&s=48" width="48" height="48" alt="bjoshuanoah"></a>
<a href="https://github.com/hanouticelina"><img src="https://avatars.githubusercontent.com/u/36770234?v=4&s=48" width="48" height="48" alt="hanouticelina"></a>
<a href="https://github.com/quanmou"><img src="https://avatars.githubusercontent.com/u/7821404?v=4&s=48" width="48" height="48" alt="quanmou"></a>
<a href="https://github.com/ClaytonWWilson"><img src="https://avatars.githubusercontent.com/u/31804874?v=4&s=48" width="48" height="48" alt="ClaytonWWilson"></a>
<a href="https://github.com/kaseru"><img src="https://avatars.githubusercontent.com/u/5975972?v=4&s=48" width="48" height="48" alt="kaseru"></a>
<a href="https://github.com/danielemden"><img src="https://avatars.githubusercontent.com/u/265470?v=4&s=48" width="48" height="48" alt="danielemden"></a>
<a href="https://github.com/dmarkey"><img src="https://avatars.githubusercontent.com/u/1159924?v=4&s=48" width="48" height="48" alt="dmarkey"></a>
<a href="https://github.com/desmondsow"><img src="https://avatars.githubusercontent.com/u/7720601?v=4&s=48" width="48" height="48" alt="desmondsow"></a>
<a href="https://github.com/intelliot"><img src="https://avatars.githubusercontent.com/u/81505?v=4&s=48" width="48" height="48" alt="intelliot"></a>
<a href="https://github.com/eugenechae"><img src="https://avatars.githubusercontent.com/u/1910247?v=4&s=48" width="48" height="48" alt="eugenechae"></a>
<a href="https://github.com/ehs208"><img src="https://avatars.githubusercontent.com/u/109217208?v=4&s=48" width="48" height="48" alt="ehs208"></a>
<a href="https://github.com/Felix8568"><img src="https://avatars.githubusercontent.com/u/86166271?v=4&s=48" width="48" height="48" alt="Felix8568"></a>
<a href="https://github.com/GabrielWithTina"><img src="https://avatars.githubusercontent.com/u/2384004?v=4&s=48" width="48" height="48" alt="GabrielWithTina"></a>
<a href="https://github.com/georgeatparallel"><img src="https://avatars.githubusercontent.com/u/297992784?v=4&s=48" width="48" height="48" alt="georgeatparallel"></a>
<a href="https://github.com/SHLE1"><img src="https://avatars.githubusercontent.com/u/101321085?v=4&s=48" width="48" height="48" alt="SHLE1"></a>
<a href="https://github.com/lzmjlrt"><img src="https://avatars.githubusercontent.com/u/62170398?v=4&s=48" width="48" height="48" alt="lzmjlrt"></a>
<a href="https://github.com/saimonventura"><img src="https://avatars.githubusercontent.com/u/3719710?v=4&s=48" width="48" height="48" alt="saimonventura"></a>
<a href="https://github.com/SIDD-KIDD"><img src="https://avatars.githubusercontent.com/u/162047739?v=4&s=48" width="48" height="48" alt="SIDD-KIDD"></a>
<a href="https://github.com/sihyeonn"><img src="https://avatars.githubusercontent.com/u/24850223?v=4&s=48" width="48" height="48" alt="sihyeonn"></a>
<a href="https://github.com/Seym0n"><img src="https://avatars.githubusercontent.com/u/119116740?v=4&s=48" width="48" height="48" alt="Seym0n"></a>
<a href="https://github.com/fyhertz"><img src="https://avatars.githubusercontent.com/u/2746007?v=4&s=48" width="48" height="48" alt="fyhertz"></a>
<a href="https://github.com/sohamb117"><img src="https://avatars.githubusercontent.com/u/36938330?v=4&s=48" width="48" height="48" alt="sohamb117"></a>
<a href="https://github.com/Solaris-star"><img src="https://avatars.githubusercontent.com/u/67425364?v=4&s=48" width="48" height="48" alt="Solaris-star"></a>
<a href="https://github.com/Syoc"><img src="https://avatars.githubusercontent.com/u/9057210?v=4&s=48" width="48" height="48" alt="Syoc"></a>
<a href="https://github.com/tedyyan"><img src="https://avatars.githubusercontent.com/u/2662290?v=4&s=48" width="48" height="48" alt="tedyyan"></a>
<a href="https://github.com/xuayan-nokia"><img src="https://avatars.githubusercontent.com/u/87028154?v=4&s=48" width="48" height="48" alt="xuayan-nokia"></a>
<a href="https://github.com/TheAutomatic"><img src="https://avatars.githubusercontent.com/u/5350578?v=4&s=48" width="48" height="48" alt="TheAutomatic"></a>
<a href="https://github.com/TomLisankie"><img src="https://avatars.githubusercontent.com/u/92654?v=4&s=48" width="48" height="48" alt="TomLisankie"></a>
<a href="https://github.com/tamvicky"><img src="https://avatars.githubusercontent.com/u/9824871?v=4&s=48" width="48" height="48" alt="tamvicky"></a>
<a href="https://github.com/MVS-source"><img src="https://avatars.githubusercontent.com/u/72023257?v=4&s=48" width="48" height="48" alt="MVS-source"></a>
<a href="https://github.com/wingkwong"><img src="https://avatars.githubusercontent.com/u/35857179?v=4&s=48" width="48" height="48" alt="wingkwong"></a>
<a href="https://github.com/wenjielei1990"><img src="https://avatars.githubusercontent.com/u/182426847?v=4&s=48" width="48" height="48" alt="wenjielei1990"></a>
<a href="https://github.com/Wenzhang-Chen"><img src="https://avatars.githubusercontent.com/u/212304734?v=4&s=48" width="48" height="48" alt="Wenzhang-Chen"></a>
<a href="https://github.com/wesleyzhangwq"><img src="https://avatars.githubusercontent.com/u/275724973?v=4&s=48" width="48" height="48" alt="wesleyzhangwq"></a>
<a href="https://github.com/XiaoHuo888-hue"><img src="https://avatars.githubusercontent.com/u/315183888?v=4&s=48" width="48" height="48" alt="XiaoHuo888-hue"></a>
<a href="https://github.com/Lyt060814"><img src="https://avatars.githubusercontent.com/u/182195098?v=4&s=48" width="48" height="48" alt="Lyt060814"></a>
<a href="https://github.com/limdingwen"><img src="https://avatars.githubusercontent.com/u/1744967?v=4&s=48" width="48" height="48" alt="limdingwen"></a>
<a href="https://github.com/luc-nguyen-cake"><img src="https://avatars.githubusercontent.com/u/308145655?v=4&s=48" width="48" height="48" alt="luc-nguyen-cake"></a>
<a href="https://github.com/maciejwojcik86"><img src="https://avatars.githubusercontent.com/u/122781058?v=4&s=48" width="48" height="48" alt="maciejwojcik86"></a>
<a href="https://github.com/mvanhorn"><img src="https://avatars.githubusercontent.com/u/455140?v=4&s=48" width="48" height="48" alt="mvanhorn"></a>
<a href="https://github.com/twiddles"><img src="https://avatars.githubusercontent.com/u/242461?v=4&s=48" width="48" height="48" alt="twiddles"></a>
<a href="https://github.com/maxmilian"><img src="https://avatars.githubusercontent.com/u/3001335?v=4&s=48" width="48" height="48" alt="maxmilian"></a>
<a href="https://github.com/mrbob-git"><img src="https://avatars.githubusercontent.com/u/202024716?v=4&s=48" width="48" height="48" alt="mrbob-git"></a>
<a href="https://github.com/MuataSr"><img src="https://avatars.githubusercontent.com/u/177951810?v=4&s=48" width="48" height="48" alt="MuataSr"></a>
<a href="https://github.com/Neutralmilkzzz"><img src="https://avatars.githubusercontent.com/u/216463318?v=4&s=48" width="48" height="48" alt="Neutralmilkzzz"></a>
<a href="https://github.com/nblondiau"><img src="https://avatars.githubusercontent.com/u/6884594?v=4&s=48" width="48" height="48" alt="nblondiau"></a>
<a href="https://github.com/omdv"><img src="https://avatars.githubusercontent.com/u/4576131?v=4&s=48" width="48" height="48" alt="omdv"></a>
<a href="https://github.com/rbankole"><img src="https://avatars.githubusercontent.com/u/25436617?v=4&s=48" width="48" height="48" alt="rbankole"></a>
<a href="https://github.com/orrinwitt"><img src="https://avatars.githubusercontent.com/u/9917194?v=4&s=48" width="48" height="48" alt="orrinwitt"></a>
<a href="https://github.com/pjperez"><img src="https://avatars.githubusercontent.com/u/747936?v=4&s=48" width="48" height="48" alt="pjperez"></a>
<a href="https://github.com/pve"><img src="https://avatars.githubusercontent.com/u/37116?v=4&s=48" width="48" height="48" alt="pve"></a>
<a href="https://github.com/PeterDaveHello"><img src="https://avatars.githubusercontent.com/u/3691490?v=4&s=48" width="48" height="48" alt="PeterDaveHello"></a>
<a href="https://github.com/Molunerfinn"><img src="https://avatars.githubusercontent.com/u/12621342?v=4&s=48" width="48" height="48" alt="Molunerfinn"></a>
<a href="https://github.com/power88"><img src="https://avatars.githubusercontent.com/u/24859241?v=4&s=48" width="48" height="48" alt="power88"></a>
<a href="https://github.com/srajasimman"><img src="https://avatars.githubusercontent.com/u/15092596?v=4&s=48" width="48" height="48" alt="srajasimman"></a>
<a href="https://github.com/groudas"><img src="https://avatars.githubusercontent.com/u/18154989?v=4&s=48" width="48" height="48" alt="groudas"></a>
<a href="https://github.com/RohitDayanand"><img src="https://avatars.githubusercontent.com/u/66650100?v=4&s=48" width="48" height="48" alt="RohitDayanand"></a>
<a href="https://github.com/katafractari"><img src="https://avatars.githubusercontent.com/u/1299228?v=4&s=48" width="48" height="48" alt="katafractari"></a>
<a href="https://github.com/rudy-of-the-corner"><img src="https://avatars.githubusercontent.com/u/188991374?v=4&s=48" width="48" height="48" alt="rudy-of-the-corner"></a>
</p>
<!-- contributors:end -->
<p align="center">
<em> Thanks for visiting ✨ nanobot!</em><br><br>
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""Refresh the native contributor avatar wall in README.md."""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
from typing import TypedDict, cast
from urllib.request import Request, urlopen
REPOSITORY = "HKUDS/nanobot"
README = Path(__file__).resolve().parents[1] / "README.md"
START = "<!-- contributors:start -->"
END = "<!-- contributors:end -->"
PER_PAGE = 100
MAINTAINERS = {"re-bin", "chengyongru"}
class Contributor(TypedDict):
login: str
type: str
html_url: str
avatar_url: str
def fetch_contributors() -> list[Contributor]:
headers = {
"Accept": "application/vnd.github+json",
"User-Agent": "nanobot-readme",
"X-GitHub-Api-Version": "2022-11-28",
}
if token := os.environ.get("GITHUB_TOKEN"):
headers["Authorization"] = f"Bearer {token}"
contributors: list[Contributor] = []
page = 1
while True:
url = f"https://api.github.com/repos/{REPOSITORY}/contributors?per_page={PER_PAGE}&page={page}"
with urlopen(Request(url, headers=headers), timeout=30) as response: # noqa: S310
batch = cast(list[Contributor], json.load(response))
contributors.extend(batch)
if len(batch) < PER_PAGE:
break
page += 1
return [
contributor
for contributor in contributors
if contributor.get("login")
and contributor.get("type") != "Bot"
and not contributor["login"].lower().endswith("[bot]")
and contributor["login"].lower() not in MAINTAINERS
]
def render_wall(contributors: list[Contributor]) -> str:
avatars = [
(
f'<a href="{contributor["html_url"]}">'
f'<img src="{contributor["avatar_url"]}&s=48" '
f'width="48" height="48" alt="{contributor["login"]}"></a>'
)
for contributor in contributors
]
wall = "\n".join(avatars)
return f"{START}\n<p>\n{wall}\n</p>\n{END}"
def update_readme(*, check: bool) -> bool:
current = README.read_text()
before, separator, tail = current.partition(START)
if not separator or END not in tail:
raise SystemExit("README contributor markers are missing")
_, _, after = tail.partition(END)
updated = f"{before}{render_wall(fetch_contributors())}{after}"
if updated == current:
return False
if check:
raise SystemExit("README contributor wall is out of date")
README.write_text(updated)
return True
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true", help="fail when README.md is out of date")
args = parser.parse_args()
print("Updated README.md" if update_readme(check=args.check) else "README.md is current")
+77 -21
View File
@@ -65,6 +65,8 @@ interface MessageBubbleProps {
temporary?: boolean;
/** When false, hide this message's copy button. Default true. */
showCopyAction?: boolean;
/** Show opt-in per-turn token usage for completed assistant messages. */
showTurnUsage?: boolean;
cliApps?: CliAppInfo[];
mcpPresets?: McpPresetInfo[];
slashCommands?: SlashCommand[];
@@ -189,31 +191,65 @@ function compactDuration(milliseconds: number): string {
return `${minutes}m ${Math.round(seconds % 60)}s`;
}
function TurnUsageMeta({
usage,
latencyMs,
}: {
usage: TurnUsage;
latencyMs?: number;
}) {
function TurnUsageMeta({ usage }: { usage: TurnUsage }) {
const { t } = useTranslation();
const context = usage.context_tokens;
const prompt = usage.prompt_tokens;
const completion = usage.completion_tokens;
const approximate = (usage.estimated_tokens ?? 0) > 0 ? "~" : "";
const formatTokens = (tokens: number) => `${approximate}${formatCompactTokenCount(tokens)}`;
const parts: string[] = [];
if (typeof prompt === "number") parts.push(`${approximate}${formatCompactTokenCount(prompt)} in`);
if (typeof completion === "number") parts.push(`${approximate}${formatCompactTokenCount(completion)} out`);
const details: string[] = [];
if (typeof context === "number" && context >= 0) {
parts.push(t("message.usage.context", {
tokens: formatTokens(context),
defaultValue: "{{tokens}} context",
}));
if (typeof prompt === "number") {
details.push(t("message.usage.turnInput", {
tokens: formatTokens(prompt),
defaultValue: "{{tokens}} turn input",
}));
}
} else if (typeof prompt === "number") {
parts.push(t("message.usage.turnInput", {
tokens: formatTokens(prompt),
defaultValue: "{{tokens}} turn input",
}));
}
if (typeof usage.request_count === "number" && usage.request_count > 0) {
const requestKey = usage.request_count === 1
? "message.usage.request"
: "message.usage.requests";
details.push(t(requestKey, {
count: usage.request_count,
defaultValue: usage.request_count === 1 ? "{{count}} request" : "{{count}} requests",
}));
}
if (typeof completion === "number") {
const output = t("message.usage.output", {
tokens: formatTokens(completion),
defaultValue: "{{tokens}} out",
});
if (parts.length > 0) details.push(output);
else parts.push(output);
}
if (
typeof usage.cached_tokens === "number"
&& typeof prompt === "number"
&& prompt > 0
) {
parts.push(`${Math.round(Math.min(1, usage.cached_tokens / prompt) * 100)}% cached`);
details.push(t("message.usage.cached", {
percent: Math.round(Math.min(1, usage.cached_tokens / prompt) * 100),
defaultValue: "{{percent}}% cached",
}));
}
if (typeof latencyMs === "number" && latencyMs >= 0) parts.push(compactDuration(latencyMs));
if (parts.length === 0) return null;
const details: string[] = [];
if (parts.length === 0) return null;
if (approximate) {
details.push(t("message.usage.estimated", { defaultValue: "Includes estimated usage" }));
}
@@ -246,6 +282,17 @@ function TurnUsageMeta({
);
}
function TurnLatencyMeta({ latencyMs }: { latencyMs: number }) {
return (
<span
data-turn-latency
className="text-[11px] leading-none text-muted-foreground/70 tabular-nums"
>
{compactDuration(latencyMs)}
</span>
);
}
function deliveryErrorCopy(
kind: MessageDeliveryErrorKind | undefined,
t: (key: string) => string,
@@ -394,6 +441,7 @@ export function MessageBubble({
isTurnStreaming = false,
temporary = false,
showCopyAction = true,
showTurnUsage = false,
cliApps = [],
mcpPresets = [],
slashCommands = [],
@@ -549,9 +597,21 @@ export function MessageBubble({
&& (!empty || hasReasoning || media.length > 0);
const assistantTimestampTitle = showAssistantTimestamp ? fmtDateTime(assistantTimestamp) : "";
const showAutomationTrigger = showAssistantTimestamp && automationSourceLabel.length > 0;
const showUsage = message.role === "assistant" && !!message.usage && !message.isStreaming;
const showUsage = (
showTurnUsage
&& message.role === "assistant"
&& !!message.usage
&& !message.isStreaming
);
const showLatency = (
message.role === "assistant"
&& !!message.usage
&& !message.isStreaming
&& typeof message.latencyMs === "number"
&& message.latencyMs >= 0
);
const showAssistantFooterRow =
showCopyButton || showForkButton || showAssistantTimestamp || showUsage;
showCopyButton || showForkButton || showAssistantTimestamp || showUsage || showLatency;
const showAssistantFooterSlot =
message.role === "assistant"
&& (!empty || hasReasoning || media.length > 0);
@@ -617,12 +677,8 @@ export function MessageBubble({
<TooltipContent side="top" align="center">{forkLabel}</TooltipContent>
</Tooltip>
) : null}
{showUsage ? (
<TurnUsageMeta
usage={message.usage!}
latencyMs={message.latencyMs}
/>
) : null}
{showUsage ? <TurnUsageMeta usage={message.usage!} /> : null}
{showLatency ? <TurnLatencyMeta latencyMs={message.latencyMs!} /> : null}
{showAssistantTimestamp ? (
<MessageTimestamp
{...(showCompletedAt ? { "data-assistant-completed-at": true } : {})}
@@ -407,6 +407,22 @@ export function AppearanceSettings({
label={localPrefs.codeWrap ? tx("settings.values.on", "On") : tx("settings.values.off", "Off")}
/>
</SettingsRow>
<SettingsRow
title={tx("settings.rows.showTurnUsage", "Show per-turn token usage")}
description={tx(
"settings.help.showTurnUsage",
"Show context, aggregate input, output, cache rate, and model calls under assistant messages. Stored only in this browser.",
)}
>
<ToggleButton
checked={localPrefs.showTurnUsage}
onChange={(showTurnUsage) => (
onChangeLocalPrefs((prev) => ({ ...prev, showTurnUsage }))
)}
ariaLabel={tx("settings.rows.showTurnUsage", "Show per-turn token usage")}
label={localPrefs.showTurnUsage ? tx("settings.values.on", "On") : tx("settings.values.off", "Off")}
/>
</SettingsRow>
<SettingsRow
title={tx("settings.rows.brandLogos", "Brand logos")}
description={tx(
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
import { MessageBubble } from "@/components/MessageBubble";
import { AgentActivityCluster } from "@/components/thread/AgentActivityCluster";
import { AssistantSelectionAction } from "@/components/thread/AssistantSelectionAction";
import { useShowTurnUsage } from "@/hooks/useShowTurnUsage";
import { projectActivityTimeline, type TurnUnit } from "@/lib/activity-timeline";
import type { CliAppInfo, McpPresetInfo, SlashCommand, UIMessage } from "@/lib/types";
@@ -67,6 +68,7 @@ export function ThreadMessages({
onQuoteSelection,
}: ThreadMessagesProps) {
const { t } = useTranslation();
const showTurnUsage = useShowTurnUsage();
const messageListRef = useRef<HTMLDivElement>(null);
const units = useMemo(
() => buildDisplayUnits(messages, isStreaming, activeTurnId),
@@ -158,6 +160,7 @@ export function ThreadMessages({
showForkBoundary={index === forkBoundaryAfterUnitIndex}
forkBoundaryLabel={t("thread.forkedFromHistory")}
temporary={temporary}
showTurnUsage={showTurnUsage}
cliApps={cliApps}
mcpPresets={mcpPresets}
slashCommands={slashCommands}
@@ -239,6 +242,7 @@ interface ThreadDisplayUnitProps {
showForkBoundary: boolean;
forkBoundaryLabel: string;
temporary: boolean;
showTurnUsage: boolean;
cliApps: CliAppInfo[];
mcpPresets: McpPresetInfo[];
slashCommands: SlashCommand[];
@@ -257,6 +261,7 @@ const ThreadDisplayUnit = memo(function ThreadDisplayUnit({
showForkBoundary,
forkBoundaryLabel,
temporary,
showTurnUsage,
cliApps,
mcpPresets,
slashCommands,
@@ -295,6 +300,7 @@ const ThreadDisplayUnit = memo(function ThreadDisplayUnit({
message={unit.message}
isTurnStreaming={isTurnStreaming}
temporary={temporary}
showTurnUsage={showTurnUsage}
cliApps={cliApps}
mcpPresets={mcpPresets}
slashCommands={slashCommands}
@@ -323,6 +329,7 @@ function threadDisplayUnitPropsEqual(
&& previous.showForkBoundary === next.showForkBoundary
&& previous.forkBoundaryLabel === next.forkBoundaryLabel
&& previous.temporary === next.temporary
&& previous.showTurnUsage === next.showTurnUsage
&& previous.cliApps === next.cliApps
&& previous.mcpPresets === next.mcpPresets
&& previous.slashCommands === next.slashCommands
+33
View File
@@ -0,0 +1,33 @@
import { useEffect, useState } from "react";
import {
LOCAL_PREFS_CHANGED_EVENT,
readLocalPreferences,
type LocalPreferences,
} from "@/lib/local-preferences";
export function useShowTurnUsage(): boolean {
const [showTurnUsage, setShowTurnUsage] = useState(
() => readLocalPreferences().showTurnUsage,
);
useEffect(() => {
const refresh = () => setShowTurnUsage(readLocalPreferences().showTurnUsage);
const refreshFromLocalPreferenceEvent = (event: Event) => {
const detail = (event as CustomEvent<Partial<LocalPreferences> | undefined>).detail;
setShowTurnUsage(
detail ? detail.showTurnUsage === true : readLocalPreferences().showTurnUsage,
);
};
window.addEventListener("storage", refresh);
window.addEventListener("focus", refresh);
window.addEventListener(LOCAL_PREFS_CHANGED_EVENT, refreshFromLocalPreferenceEvent);
return () => {
window.removeEventListener("storage", refresh);
window.removeEventListener("focus", refresh);
window.removeEventListener(LOCAL_PREFS_CHANGED_EVENT, refreshFromLocalPreferenceEvent);
};
}, []);
return showTurnUsage;
}
+8 -2
View File
@@ -196,6 +196,7 @@
"density": "Density",
"activityMode": "Activity detail",
"fileEditDisplay": "File edit display",
"showTurnUsage": "Show per-turn token usage",
"codeWrap": "Code wrapping",
"brandLogos": "Brand logos",
"maxResults": "Max results",
@@ -241,6 +242,7 @@
"density": "Stored only in this browser.",
"activityMode": "Choose how much agent activity chrome to show by default.",
"fileEditDisplay": "Choose whether file edit activity opens as line counts or a diff.",
"showTurnUsage": "Show context, aggregate input, output, cache rate, and model calls under assistant messages. Stored only in this browser.",
"codeWrap": "Keep long code lines readable on smaller screens.",
"brandLogos": "Show third-party provider and CLI logos in Settings.",
"maxResults": "Results returned by each web_search call.",
@@ -1431,8 +1433,12 @@
"fileEditOpenFile": "Open file",
"fileEditDiffTruncated": "Diff truncated. Open the file for the full change.",
"usage": {
"context": "Context now: {{tokens}}{{capacity}}",
"requests": "{{count}} calls this turn",
"context": "{{tokens}} context",
"turnInput": "{{tokens}} turn input",
"request": "{{count}} request",
"requests": "{{count}} requests",
"output": "{{tokens}} out",
"cached": "{{percent}}% cached",
"estimated": "Includes estimated usage"
}
},
+8 -2
View File
@@ -142,6 +142,7 @@
"density": "Densidad",
"activityMode": "Detalle de actividad",
"fileEditDisplay": "Vista de edición de archivos",
"showTurnUsage": "Mostrar uso de tokens por turno",
"codeWrap": "Ajuste de código",
"maxResults": "Resultados máximos",
"timeout": "Tiempo de espera",
@@ -185,6 +186,7 @@
"density": "Solo se guarda en este navegador.",
"activityMode": "Elige cuánto detalle de actividad del agente se muestra por defecto.",
"fileEditDisplay": "Elige si la actividad de edición muestra recuentos de líneas o diferencias.",
"showTurnUsage": "Muestra el contexto, la entrada acumulada, la salida, la tasa de caché y las llamadas al modelo debajo de los mensajes del asistente. Solo se guarda en este navegador.",
"codeWrap": "Mantiene legibles las líneas largas de código en pantallas pequeñas.",
"maxResults": "Resultados devueltos por cada llamada web_search.",
"timeout": "Segundos antes de que una solicitud de búsqueda expire.",
@@ -1401,8 +1403,12 @@
"fileEditOpenFile": "Abrir archivo",
"fileEditDiffTruncated": "Diferencias truncadas. Abre el archivo para ver el cambio completo.",
"usage": {
"context": "Contexto más reciente: {{tokens}}{{capacity}} tokens",
"requests": "{{count}} solicitudes al modelo",
"context": "{{tokens}} de contexto",
"turnInput": "{{tokens}} de entrada en el turno",
"request": "{{count}} solicitud",
"requests": "{{count}} solicitudes",
"output": "{{tokens}} de salida",
"cached": "{{percent}}% en caché",
"estimated": "Incluye uso estimado"
},
"activityThinkingFor": "Pensando durante {{duration}}",
+8 -2
View File
@@ -142,6 +142,7 @@
"density": "Densité",
"activityMode": "Détail dactivité",
"fileEditDisplay": "Affichage des modifications de fichiers",
"showTurnUsage": "Afficher lutilisation des tokens par tour",
"codeWrap": "Retour à la ligne du code",
"maxResults": "Résultats max.",
"timeout": "Délai dattente",
@@ -185,6 +186,7 @@
"density": "Enregistré seulement dans ce navigateur.",
"activityMode": "Choisissez le niveau de détail de lactivité de lagent affiché par défaut.",
"fileEditDisplay": "Choisissez si lactivité de modification affiche le nombre de lignes ou les différences.",
"showTurnUsage": "Affiche le contexte, lentrée cumulée, la sortie, le taux de cache et le nombre dappels au modèle sous les messages de lassistant. Enregistré uniquement dans ce navigateur.",
"codeWrap": "Garde les longues lignes de code lisibles sur les petits écrans.",
"maxResults": "Résultats renvoyés par chaque appel web_search.",
"timeout": "Nombre de secondes avant lexpiration dune requête de recherche.",
@@ -1400,8 +1402,12 @@
"fileEditOpenFile": "Ouvrir le fichier",
"fileEditDiffTruncated": "Différences tronquées. Ouvrez le fichier pour voir la modification complète.",
"usage": {
"context": "Dernier contexte : {{tokens}}{{capacity}} tokens",
"requests": "{{count}} requêtes au modèle",
"context": "{{tokens}} de contexte",
"turnInput": "{{tokens}} en entrée sur ce tour",
"request": "{{count}} requête",
"requests": "{{count}} requêtes",
"output": "{{tokens}} en sortie",
"cached": "{{percent}} % en cache",
"estimated": "Inclut une estimation de lutilisation"
},
"activityThinkingFor": "Réflexion pendant {{duration}}",
+8 -2
View File
@@ -142,6 +142,7 @@
"density": "Kerapatan",
"activityMode": "Detail aktivitas",
"fileEditDisplay": "Tampilan perubahan file",
"showTurnUsage": "Tampilkan penggunaan token per giliran",
"codeWrap": "Bungkus kode",
"maxResults": "Hasil maksimum",
"timeout": "Batas waktu",
@@ -185,6 +186,7 @@
"density": "Hanya disimpan di browser ini.",
"activityMode": "Pilih seberapa banyak detail aktivitas agen yang ditampilkan secara default.",
"fileEditDisplay": "Pilih apakah aktivitas perubahan file ditampilkan sebagai jumlah baris atau perbedaan.",
"showTurnUsage": "Tampilkan konteks, input kumulatif, output, rasio cache, dan jumlah panggilan model di bawah pesan asisten. Hanya disimpan di browser ini.",
"codeWrap": "Menjaga baris kode panjang tetap terbaca di layar kecil.",
"maxResults": "Hasil yang dikembalikan oleh setiap panggilan web_search.",
"timeout": "Detik sebelum permintaan penyedia pencarian mencapai batas waktu.",
@@ -1400,8 +1402,12 @@
"fileEditOpenFile": "Buka file",
"fileEditDiffTruncated": "Perbedaan dipotong. Buka file untuk melihat perubahan lengkap.",
"usage": {
"context": "Konteks terbaru: {{tokens}}{{capacity}} token",
"requests": "{{count}} permintaan model",
"context": "Konteks {{tokens}}",
"turnInput": "Input giliran {{tokens}}",
"request": "{{count}} permintaan",
"requests": "{{count}} permintaan",
"output": "Output {{tokens}}",
"cached": "{{percent}}% di-cache",
"estimated": "Termasuk penggunaan perkiraan"
},
"activityThinkingFor": "Berpikir selama {{duration}}",
+8 -2
View File
@@ -142,6 +142,7 @@
"density": "表示密度",
"activityMode": "アクティビティ詳細",
"fileEditDisplay": "ファイル編集表示",
"showTurnUsage": "ターンごとのトークン使用量を表示",
"codeWrap": "コードの折り返し",
"maxResults": "最大結果数",
"timeout": "タイムアウト",
@@ -185,6 +186,7 @@
"density": "このブラウザーにのみ保存されます。",
"activityMode": "既定で表示するエージェントアクティビティの詳細量を選択します。",
"fileEditDisplay": "ファイル編集アクティビティを行数または差分で表示します。",
"showTurnUsage": "アシスタントのメッセージの下に、コンテキスト、累積入力、出力、キャッシュ率、モデル呼び出し回数を表示します。このブラウザーにのみ保存されます。",
"codeWrap": "小さな画面でも長いコード行を読みやすくします。",
"maxResults": "各 web_search 呼び出しで返す結果数です。",
"timeout": "検索プロバイダーのリクエストがタイムアウトするまでの秒数です。",
@@ -1400,8 +1402,12 @@
"fileEditOpenFile": "ファイルを開く",
"fileEditDiffTruncated": "差分は切り詰められました。完全な変更はファイルを開いて確認してください。",
"usage": {
"context": "最新のコンテキスト: {{tokens}}{{capacity}} トークン",
"requests": "モデルリクエスト {{count}} 回",
"context": "コンテキスト {{tokens}}",
"turnInput": "ターン入力 {{tokens}}",
"request": "リクエスト {{count}} 回",
"requests": "リクエスト {{count}} 回",
"output": "出力 {{tokens}}",
"cached": "キャッシュ {{percent}}%",
"estimated": "推定使用量を含みます"
},
"activityThinkingFor": "{{duration}}考えています",
+8 -2
View File
@@ -142,6 +142,7 @@
"density": "밀도",
"activityMode": "활동 상세",
"fileEditDisplay": "파일 편집 표시",
"showTurnUsage": "턴별 토큰 사용량 표시",
"codeWrap": "코드 줄바꿈",
"maxResults": "최대 결과 수",
"timeout": "타임아웃",
@@ -185,6 +186,7 @@
"density": "이 브라우저에만 저장됩니다.",
"activityMode": "기본으로 표시할 에이전트 활동 세부 수준을 선택합니다.",
"fileEditDisplay": "파일 편집 활동을 줄 수 또는 변경 사항으로 표시할지 선택합니다.",
"showTurnUsage": "어시스턴트 메시지 아래에 컨텍스트, 누적 입력, 출력, 캐시 비율, 모델 호출 횟수를 표시합니다. 이 브라우저에만 저장됩니다.",
"codeWrap": "작은 화면에서도 긴 코드 줄을 읽기 쉽게 유지합니다.",
"maxResults": "각 web_search 호출에서 반환되는 결과 수입니다.",
"timeout": "검색 제공자 요청이 타임아웃되기 전의 초입니다.",
@@ -1400,8 +1402,12 @@
"fileEditOpenFile": "파일 열기",
"fileEditDiffTruncated": "변경 사항이 잘렸습니다. 전체 변경은 파일을 열어 확인하세요.",
"usage": {
"context": "최근 컨텍스트: {{tokens}}{{capacity}} 토큰",
"requests": "모델 요청 {{count}}",
"context": "컨텍스트 {{tokens}}",
"turnInput": "턴 입력 {{tokens}}",
"request": "요청 {{count}}회",
"requests": "요청 {{count}}회",
"output": "출력 {{tokens}}",
"cached": "캐시됨 {{percent}}%",
"estimated": "예상 사용량 포함"
},
"activityThinkingFor": "{{duration}} 동안 생각 중",
+8 -2
View File
@@ -196,6 +196,7 @@
"density": "Densidade",
"activityMode": "Detalhe de atividade",
"fileEditDisplay": "Exibição de edição de arquivo",
"showTurnUsage": "Mostrar uso de tokens por turno",
"codeWrap": "Quebra de linha no código",
"brandLogos": "Logos de marca",
"maxResults": "Máx. de resultados",
@@ -241,6 +242,7 @@
"density": "Armazenado apenas neste navegador.",
"activityMode": "Escolha quanto detalhe de atividade do agente é exibido por padrão.",
"fileEditDisplay": "Escolha se a atividade de edição de arquivo é exibida como contagem de linhas ou como diferenças.",
"showTurnUsage": "Mostra contexto, entrada acumulada, saída, taxa de cache e chamadas do modelo abaixo das mensagens do assistente. Salvo somente neste navegador.",
"codeWrap": "Mantém linhas longas de código legíveis em telas menores.",
"brandLogos": "Mostra logotipos de provedores terceiros e de CLIs em Configurações.",
"maxResults": "Resultados retornados por cada chamada de web_search.",
@@ -1431,8 +1433,12 @@
"fileEditOpenFile": "Abrir arquivo",
"fileEditDiffTruncated": "Diferenças truncadas. Abra o arquivo para ver a alteração completa.",
"usage": {
"context": "Contexto atual: {{tokens}}{{capacity}}",
"requests": "{{count}} chamadas nesta rodada",
"context": "{{tokens}} de contexto",
"turnInput": "{{tokens}} de entrada na rodada",
"request": "{{count}} chamada",
"requests": "{{count}} chamadas",
"output": "{{tokens}} de saída",
"cached": "{{percent}}% em cache",
"estimated": "Inclui uso estimado"
}
},
+8 -2
View File
@@ -142,6 +142,7 @@
"density": "Mật độ",
"activityMode": "Chi tiết hoạt động",
"fileEditDisplay": "Hiển thị sửa tệp",
"showTurnUsage": "Hiển thị mức dùng token theo lượt",
"codeWrap": "Xuống dòng mã",
"maxResults": "Kết quả tối đa",
"timeout": "Thời gian chờ",
@@ -185,6 +186,7 @@
"density": "Chỉ lưu trong trình duyệt này.",
"activityMode": "Chọn mức chi tiết hoạt động của tác nhân hiển thị mặc định.",
"fileEditDisplay": "Chọn hoạt động sửa tệp hiển thị số dòng hay khác biệt.",
"showTurnUsage": "Hiển thị ngữ cảnh, đầu vào tích lũy, đầu ra, tỷ lệ bộ nhớ đệm và số lần gọi mô hình bên dưới tin nhắn của trợ lý. Chỉ được lưu trong trình duyệt này.",
"codeWrap": "Giữ các dòng mã dài dễ đọc trên màn hình nhỏ.",
"maxResults": "Số kết quả được trả về sau mỗi lần gọi web_search.",
"timeout": "Số giây trước khi yêu cầu của nhà cung cấp tìm kiếm hết thời gian.",
@@ -1400,8 +1402,12 @@
"fileEditOpenFile": "Mở tệp",
"fileEditDiffTruncated": "Khác biệt đã bị cắt bớt. Mở tệp để xem toàn bộ thay đổi.",
"usage": {
"context": "Ngữ cảnh gần nhất: {{tokens}}{{capacity}} token",
"requests": "{{count}} yêu cầu mô hình",
"context": "Ngữ cảnh {{tokens}}",
"turnInput": "Đầu vào lượt {{tokens}}",
"request": "{{count}} yêu cầu",
"requests": "{{count}} yêu cầu",
"output": "Đầu ra {{tokens}}",
"cached": "{{percent}}% đã lưu đệm",
"estimated": "Bao gồm mức sử dụng ước tính"
},
"activityThinkingFor": "Đang suy nghĩ trong {{duration}}",
+8 -2
View File
@@ -196,6 +196,7 @@
"density": "密度",
"activityMode": "活动详情",
"fileEditDisplay": "文件编辑显示",
"showTurnUsage": "显示每轮 Token 用量",
"codeWrap": "代码换行",
"brandLogos": "品牌 Logo",
"maxResults": "最大结果数",
@@ -241,6 +242,7 @@
"density": "只保存在此浏览器中。",
"activityMode": "选择默认显示多少智能体活动详情。",
"fileEditDisplay": "选择文件编辑活动默认显示行数还是差异。",
"showTurnUsage": "在助手消息下显示上下文、累计输入、输出、缓存率和模型调用次数;仅保存在此浏览器。",
"codeWrap": "让长代码行在小屏幕上也易读。",
"brandLogos": "在设置中显示第三方提供商和 CLI 图标。",
"maxResults": "每次 web_search 调用返回的结果数。",
@@ -1431,8 +1433,12 @@
"fileEditOpenFile": "打开文件",
"fileEditDiffTruncated": "差异内容已截断。打开文件可查看完整更改。",
"usage": {
"context": "当前上下文{{tokens}}{{capacity}}",
"requests": "本轮 {{count}} 次模型调用",
"context": "上下文 {{tokens}}",
"turnInput": "本轮输入 {{tokens}}",
"request": "{{count}} 次模型调用",
"requests": "{{count}} 次模型调用",
"output": "输出 {{tokens}}",
"cached": "缓存 {{percent}}%",
"estimated": "包含估算用量"
}
},
+7 -1
View File
@@ -142,6 +142,7 @@
"density": "密度",
"activityMode": "活動細節",
"fileEditDisplay": "檔案編輯顯示",
"showTurnUsage": "顯示每輪 Token 用量",
"codeWrap": "程式碼換行",
"maxResults": "最大結果數",
"timeout": "逾時",
@@ -185,6 +186,7 @@
"density": "只儲存在此瀏覽器中。",
"activityMode": "選擇預設顯示多少智能體活動細節。",
"fileEditDisplay": "選擇檔案編輯活動預設顯示行數或差異。",
"showTurnUsage": "在助理訊息下顯示上下文、累計輸入、輸出、快取率和模型呼叫次數;僅儲存在此瀏覽器。",
"codeWrap": "讓長程式碼行在小螢幕上也易讀。",
"maxResults": "每次呼叫 web_search 所回傳的結果數。",
"timeout": "搜尋供應商請求逾時前的秒數。",
@@ -1399,8 +1401,12 @@
"fileEditOpenFile": "開啟檔案",
"fileEditDiffTruncated": "差異內容已截斷。請開啟檔案以檢視完整變更。",
"usage": {
"context": "最近一次上下文{{tokens}}{{capacity}} tokens",
"context": "上下文 {{tokens}}",
"turnInput": "本輪輸入 {{tokens}}",
"request": "{{count}} 次模型請求",
"requests": "{{count}} 次模型請求",
"output": "輸出 {{tokens}}",
"cached": "快取 {{percent}}%",
"estimated": "包含估算用量"
},
"activityThinkingFor": "思考中,已 {{duration}}",
+3
View File
@@ -8,6 +8,7 @@ export interface LocalPreferences {
codeWrap: boolean;
brandLogos: boolean;
fileEditDisplayMode: FileEditDisplayMode;
showTurnUsage: boolean;
}
export const LOCAL_PREFS_STORAGE_KEY = "nanobot-webui.settings-preferences";
@@ -19,6 +20,7 @@ export const DEFAULT_LOCAL_PREFS: LocalPreferences = {
codeWrap: true,
brandLogos: false,
fileEditDisplayMode: "summary",
showTurnUsage: false,
};
export function normalizeFileEditDisplayMode(value: unknown): FileEditDisplayMode {
@@ -36,6 +38,7 @@ export function readLocalPreferences(): LocalPreferences {
codeWrap: parsed.codeWrap !== false,
brandLogos: parsed.brandLogos === true,
fileEditDisplayMode: normalizeFileEditDisplayMode(parsed.fileEditDisplayMode),
showTurnUsage: parsed.showTurnUsage === true,
};
} catch {
return DEFAULT_LOCAL_PREFS;
+9
View File
@@ -160,6 +160,7 @@ const LOCALIZED_SETTINGS_COPY_KEYS = [
"settings.rows.activityMode",
"settings.rows.fileEditDisplay",
"settings.rows.codeWrap",
"settings.rows.showTurnUsage",
"settings.rows.brandLogos",
"settings.rows.currentModel",
"settings.rows.localServiceAccess",
@@ -171,6 +172,7 @@ const LOCALIZED_SETTINGS_COPY_KEYS = [
"settings.help.activityMode",
"settings.help.fileEditDisplay",
"settings.help.codeWrap",
"settings.help.showTurnUsage",
"settings.help.brandLogos",
"settings.help.currentModel",
"settings.help.localServiceAccess",
@@ -291,6 +293,13 @@ const LOCALIZED_NEW_SURFACE_KEYS = [
"message.openLink",
"message.openAttachment",
"message.skill",
"message.usage.context",
"message.usage.turnInput",
"message.usage.request",
"message.usage.requests",
"message.usage.output",
"message.usage.cached",
"message.usage.estimated",
"settings.channels.connectionChecks",
"settings.channels.open",
];
+98 -16
View File
@@ -1,8 +1,14 @@
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { MessageBubble } from "@/components/MessageBubble";
import { useShowTurnUsage } from "@/hooks/useShowTurnUsage";
import { fmtDateTime, formatMessageEndTime } from "@/lib/format";
import {
DEFAULT_LOCAL_PREFS,
LOCAL_PREFS_STORAGE_KEY,
writeLocalPreferences,
} from "@/lib/local-preferences";
import type {
CliAppInfo,
McpPresetInfo,
@@ -67,6 +73,11 @@ const MCP_PRESETS: McpPresetInfo[] = [
},
];
function PreferenceAwareMessageBubble({ message }: { message: UIMessage }) {
const showTurnUsage = useShowTurnUsage();
return <MessageBubble message={message} showTurnUsage={showTurnUsage} />;
}
const SLASH_COMMANDS: SlashCommand[] = [
{
command: "/model",
@@ -95,6 +106,10 @@ const SLASH_COMMANDS: SlashCommand[] = [
];
describe("MessageBubble", () => {
afterEach(() => {
localStorage.removeItem(LOCAL_PREFS_STORAGE_KEY);
});
it("renders user messages as right-aligned pills", () => {
const message: UIMessage = {
id: "u1",
@@ -1013,32 +1028,95 @@ describe("MessageBubble", () => {
expect(screen.queryByLabelText("File attachment")).not.toBeInTheDocument();
});
it("keeps turn usage focused on the completed reply", () => {
it("hides turn usage by default and leaves latency visible when the preference changes", async () => {
const message: UIMessage = {
id: "a-usage",
id: "turn-usage-preference",
role: "assistant",
content: "done",
createdAt: Date.now(),
latencyMs: 18_200,
contextWindowTokens: 128_000,
usage: {
prompt_tokens: 12_400,
completion_tokens: 823,
cached_tokens: 9_672,
context_tokens: 8_100,
request_count: 3,
request_count: 1,
},
};
render(<MessageBubble message={message} />);
const { container } = render(<PreferenceAwareMessageBubble message={message} />);
const usage = screen.getByText("12.4K in · 823 out · 78% cached · 18s");
expect(usage).toHaveAttribute("data-turn-usage");
expect(usage).not.toHaveAttribute("tabindex");
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
expect(container.querySelector("[data-turn-usage]")).not.toBeInTheDocument();
expect(container.querySelector("[data-turn-latency]")).toHaveTextContent("18s");
act(() => {
writeLocalPreferences({ ...DEFAULT_LOCAL_PREFS, showTurnUsage: true });
});
expect(await screen.findByText("8.1K context")).toHaveAttribute("data-turn-usage");
act(() => {
writeLocalPreferences({ ...DEFAULT_LOCAL_PREFS, showTurnUsage: false });
});
await waitFor(() => {
expect(container.querySelector("[data-turn-usage]")).not.toBeInTheDocument();
});
expect(container.querySelector("[data-turn-latency]")).toHaveTextContent("18s");
});
it("marks estimated usage and omits cache when the provider did not report it", () => {
it("shows consecutive contexts while keeping each turn's aggregate cost in details", async () => {
const turns: UIMessage[] = [
{
id: "turn-a",
role: "assistant",
content: "first reply",
createdAt: Date.now(),
usage: {
prompt_tokens: 111_000,
completion_tokens: 333,
cached_tokens: 55_500,
context_tokens: 56_000,
request_count: 2,
},
},
{
id: "turn-b",
role: "assistant",
content: "second reply",
createdAt: Date.now(),
usage: {
prompt_tokens: 57_000,
completion_tokens: 192,
cached_tokens: 55_290,
context_tokens: 57_000,
request_count: 1,
},
},
];
render(<>{turns.map((message) => (
<MessageBubble key={message.id} message={message} showTurnUsage />
))}</>);
const turnAUsage = screen.getByText("56K context");
const turnBUsage = screen.getByText("57K context");
expect(turnAUsage).toHaveAttribute("data-turn-usage");
expect(turnBUsage).toHaveAttribute("data-turn-usage");
expect(screen.queryByText(/111K turn input/)).not.toBeInTheDocument();
fireEvent.focus(turnAUsage);
expect(screen.getByRole("tooltip")).toHaveTextContent(
"111K turn input · 2 requests · 333 out · 50% cached",
);
fireEvent.blur(turnAUsage);
await waitFor(() => expect(screen.queryByRole("tooltip")).not.toBeInTheDocument());
fireEvent.focus(turnBUsage);
expect(screen.getByRole("tooltip")).toHaveTextContent(
"57K turn input · 1 request · 192 out · 97% cached",
);
});
it("falls back to clearly labeled estimated turn input when context is unavailable", () => {
const message: UIMessage = {
id: "a-estimated-usage",
role: "assistant",
@@ -1047,15 +1125,19 @@ describe("MessageBubble", () => {
usage: {
prompt_tokens: 1_250,
completion_tokens: 90,
request_count: 1,
estimated_tokens: 1_340,
},
};
render(<MessageBubble message={message} />);
render(<MessageBubble message={message} showTurnUsage />);
const usage = screen.getByText("~1.3K in · ~90 out");
expect(usage).not.toHaveTextContent("cached");
const usage = screen.getByText("~1.3K turn input");
expect(usage).not.toHaveTextContent("context");
fireEvent.focus(usage);
expect(screen.getByRole("tooltip")).toHaveTextContent("Includes estimated usage");
expect(screen.getByRole("tooltip")).toHaveTextContent(
"1 request · ~90 out · Includes estimated usage",
);
expect(screen.getByRole("tooltip")).not.toHaveTextContent("cached");
});
});
+40 -1
View File
@@ -1,5 +1,6 @@
import { act, fireEvent, screen, waitFor, within } from "@testing-library/react";
import { act, cleanup, fireEvent, screen, waitFor, within } from "@testing-library/react";
import { expect, it, vi } from "vitest";
import { LOCAL_PREFS_STORAGE_KEY } from "@/lib/local-preferences";
import type { SettingsPayload } from "@/lib/types";
import { jsonResponse, settingsPayload, renderSettingsView, installSettingsViewTestHooks } from "@/tests/settings-test-utils";
@@ -10,6 +11,44 @@ describe("Settings overview and appearance", () => {
installSettingsViewTestHooks();
it("defaults per-turn token usage off and persists changes across remounts", async () => {
const renderAppearance = () => renderSettingsView({
initialSection: "appearance",
initialSettings: settingsPayload(),
showSidebar: true,
});
renderAppearance();
const title = "Show per-turn token usage";
const description = (
"Show context, aggregate input, output, cache rate, and model calls under assistant messages. "
+ "Stored only in this browser."
);
expect(screen.getByText(description)).toBeInTheDocument();
expect(screen.getByRole("switch", { name: title })).toHaveAttribute("aria-checked", "false");
fireEvent.click(screen.getByRole("switch", { name: title }));
await waitFor(() => {
const saved = JSON.parse(localStorage.getItem(LOCAL_PREFS_STORAGE_KEY) || "{}");
expect(saved.showTurnUsage).toBe(true);
});
cleanup();
renderAppearance();
expect(screen.getByRole("switch", { name: title })).toHaveAttribute("aria-checked", "true");
fireEvent.click(screen.getByRole("switch", { name: title }));
await waitFor(() => {
const saved = JSON.parse(localStorage.getItem(LOCAL_PREFS_STORAGE_KEY) || "{}");
expect(saved.showTurnUsage).toBe(false);
});
cleanup();
renderAppearance();
expect(screen.getByRole("switch", { name: title })).toHaveAttribute("aria-checked", "false");
});
it("persists the file edit display local preference", async () => {
renderSettingsView({
initialSection: "appearance",