Merge branch 'dev' into fix-ai-message-issue
commit
4d91552be3
403
STATS.md
403
STATS.md
|
|
@ -1,203 +1,204 @@
|
|||
# Download Stats
|
||||
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | -------------------- | ------------------- | -------------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
||||
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
||||
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
||||
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
||||
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
||||
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
||||
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
|
||||
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
|
||||
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
|
||||
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
|
||||
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
|
||||
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
|
||||
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
|
||||
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
|
||||
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
|
||||
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
|
||||
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
|
||||
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
|
||||
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
|
||||
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
|
||||
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
|
||||
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
|
||||
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
|
||||
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
|
||||
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
|
||||
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
|
||||
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
|
||||
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
|
||||
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
|
||||
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
|
||||
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
|
||||
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
|
||||
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
|
||||
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
|
||||
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
|
||||
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
|
||||
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
|
||||
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
|
||||
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
|
||||
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
|
||||
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
|
||||
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
|
||||
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
|
||||
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
|
||||
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
|
||||
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
|
||||
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
|
||||
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
|
||||
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
|
||||
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
|
||||
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
|
||||
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
|
||||
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
|
||||
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
|
||||
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
|
||||
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
|
||||
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
|
||||
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
|
||||
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
|
||||
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
|
||||
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
|
||||
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
|
||||
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
|
||||
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
|
||||
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
|
||||
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
|
||||
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
|
||||
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
|
||||
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
|
||||
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
|
||||
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
|
||||
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
|
||||
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
|
||||
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
|
||||
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
|
||||
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
|
||||
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
|
||||
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
|
||||
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
|
||||
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
|
||||
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
|
||||
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
|
||||
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
|
||||
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
|
||||
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
|
||||
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
|
||||
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
|
||||
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
|
||||
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
|
||||
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
|
||||
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
|
||||
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
|
||||
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
|
||||
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
|
||||
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
|
||||
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
|
||||
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
|
||||
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
|
||||
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
|
||||
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
|
||||
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
|
||||
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
|
||||
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
|
||||
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
|
||||
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
||||
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
|
||||
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
||||
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
|
||||
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
|
||||
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
|
||||
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
|
||||
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
|
||||
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
|
||||
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
|
||||
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
|
||||
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
|
||||
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
|
||||
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
|
||||
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
|
||||
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
|
||||
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
|
||||
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
|
||||
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
|
||||
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
|
||||
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
|
||||
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
|
||||
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
|
||||
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
|
||||
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
|
||||
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
|
||||
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
|
||||
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
|
||||
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
|
||||
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
|
||||
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
|
||||
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
|
||||
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
|
||||
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
|
||||
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
|
||||
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
|
||||
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
|
||||
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
|
||||
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
|
||||
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
|
||||
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
|
||||
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
|
||||
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
|
||||
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |
|
||||
| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) |
|
||||
| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) |
|
||||
| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) |
|
||||
| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) |
|
||||
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
|
||||
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
|
||||
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
|
||||
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
|
||||
| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
|
||||
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
|
||||
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
|
||||
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
|
||||
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
|
||||
| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) |
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | -------------------- | -------------------- | -------------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
||||
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
||||
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
||||
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
||||
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
||||
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
||||
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
|
||||
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
|
||||
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
|
||||
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
|
||||
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
|
||||
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
|
||||
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
|
||||
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
|
||||
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
|
||||
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
|
||||
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
|
||||
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
|
||||
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
|
||||
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
|
||||
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
|
||||
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
|
||||
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
|
||||
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
|
||||
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
|
||||
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
|
||||
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
|
||||
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
|
||||
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
|
||||
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
|
||||
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
|
||||
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
|
||||
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
|
||||
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
|
||||
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
|
||||
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
|
||||
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
|
||||
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
|
||||
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
|
||||
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
|
||||
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
|
||||
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
|
||||
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
|
||||
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
|
||||
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
|
||||
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
|
||||
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
|
||||
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
|
||||
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
|
||||
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
|
||||
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
|
||||
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
|
||||
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
|
||||
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
|
||||
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
|
||||
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
|
||||
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
|
||||
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
|
||||
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
|
||||
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
|
||||
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
|
||||
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
|
||||
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
|
||||
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
|
||||
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
|
||||
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
|
||||
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
|
||||
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
|
||||
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
|
||||
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
|
||||
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
|
||||
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
|
||||
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
|
||||
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
|
||||
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
|
||||
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
|
||||
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
|
||||
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
|
||||
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
|
||||
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
|
||||
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
|
||||
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
|
||||
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
|
||||
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
|
||||
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
|
||||
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
|
||||
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
|
||||
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
|
||||
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
|
||||
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
|
||||
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
|
||||
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
|
||||
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
|
||||
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
|
||||
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
|
||||
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
|
||||
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
|
||||
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
|
||||
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
|
||||
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
|
||||
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
|
||||
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
|
||||
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
|
||||
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
|
||||
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
||||
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
|
||||
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
||||
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
|
||||
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
|
||||
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
|
||||
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
|
||||
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
|
||||
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
|
||||
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
|
||||
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
|
||||
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
|
||||
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
|
||||
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
|
||||
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
|
||||
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
|
||||
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
|
||||
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
|
||||
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
|
||||
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
|
||||
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
|
||||
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
|
||||
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
|
||||
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
|
||||
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
|
||||
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
|
||||
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
|
||||
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
|
||||
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
|
||||
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
|
||||
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
|
||||
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
|
||||
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
|
||||
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
|
||||
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
|
||||
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
|
||||
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
|
||||
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
|
||||
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
|
||||
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
|
||||
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
|
||||
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
|
||||
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
|
||||
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |
|
||||
| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) |
|
||||
| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) |
|
||||
| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) |
|
||||
| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) |
|
||||
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
|
||||
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
|
||||
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
|
||||
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
|
||||
| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
|
||||
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
|
||||
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
|
||||
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
|
||||
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
|
||||
| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) |
|
||||
| 2026-01-16 | 4,121,550 (+552,622) | 1,754,418 (+109,056) | 5,875,968 (+661,678) |
|
||||
|
|
|
|||
24
bun.lock
24
bun.lock
|
|
@ -379,7 +379,7 @@
|
|||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.1.23",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.88.1",
|
||||
"@hey-api/openapi-ts": "0.90.4",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
|
|
@ -922,11 +922,11 @@
|
|||
|
||||
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
|
||||
|
||||
"@hey-api/codegen-core": ["@hey-api/codegen-core@0.3.3", "", { "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-vArVDtrvdzFewu1hnjUm4jX1NBITlSCeO81EdWq676MxQbyxsGcDPAgohaSA+Wvr4HjPSvsg2/1s2zYxUtXebg=="],
|
||||
"@hey-api/codegen-core": ["@hey-api/codegen-core@0.5.2", "", { "dependencies": { "ansi-colors": "4.1.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-88cqrrB2cLXN8nMOHidQTcVOnZsJ5kebEbBefjMCifaUCwTA30ouSSWvTZqrOX4O104zjJyu7M8Gcv/NNYQuaA=="],
|
||||
|
||||
"@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.2", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA=="],
|
||||
|
||||
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.88.1", "", { "dependencies": { "@hey-api/codegen-core": "^0.3.3", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.2", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-x/nDTupOnV9VuSeNIiJpgIpc915GHduhyseJeMTnI0JMsXaObmpa0rgPr3ASVEYMLgpvqozIEG1RTOOnal6zLQ=="],
|
||||
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.90.4", "", { "dependencies": { "@hey-api/codegen-core": "^0.5.2", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-9l++kjcb0ui4JqPlueZ6OZ9zKn6eK/8//Z2jHcIXb5MRwDRgubOOSpTU5llEv3uvWfT10VzcMp99dySWq0AASw=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
|
||||
|
||||
|
|
@ -2094,7 +2094,7 @@
|
|||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"c12": ["c12@3.3.2", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-QkikB2X5voO1okL3QsES0N690Sn/K9WokXqUsDQsWy5SnYb+psYQFGA10iy1bZHj3fjISKsI67Q90gruvWWM3A=="],
|
||||
"c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
|
|
@ -3494,7 +3494,7 @@
|
|||
|
||||
"selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="],
|
||||
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
|
||||
|
||||
|
|
@ -4280,6 +4280,8 @@
|
|||
|
||||
"astro/diff": ["diff@5.2.0", "", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="],
|
||||
|
||||
"astro/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"astro/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="],
|
||||
|
||||
"astro/unstorage": ["unstorage@1.17.3", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^4.0.3", "destr": "^2.0.5", "h3": "^1.15.4", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.1" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q=="],
|
||||
|
|
@ -4302,6 +4304,8 @@
|
|||
|
||||
"bun-webgpu/@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
|
||||
|
||||
"c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
|
@ -4318,6 +4322,8 @@
|
|||
|
||||
"editorconfig/minimatch": ["minimatch@9.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w=="],
|
||||
|
||||
"editorconfig/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"engine.io-client/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
|
||||
"es-get-iterator/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
|
@ -4344,6 +4350,8 @@
|
|||
|
||||
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
|
||||
"gel/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||
|
||||
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
|
@ -4358,6 +4366,8 @@
|
|||
|
||||
"jsonwebtoken/jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
|
||||
|
||||
"jsonwebtoken/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
||||
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
|
@ -4442,6 +4452,8 @@
|
|||
|
||||
"sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"sharp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"shiki/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
|
||||
|
||||
"shiki/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
|
||||
|
|
@ -4920,6 +4932,8 @@
|
|||
|
||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="],
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-qjXrRkNAJsarbUBMiEL18lGkr65w74YvCsFVjrSCQHI=",
|
||||
"x86_64-linux": "sha256-07XxcHLuToM4QfWVyaPLACxjPZ93ZM7gtpX2o08Lp18=",
|
||||
"aarch64-linux": "sha256-E6lyYFApS1cw3jE7ISx5QZxDDJ9V3HU0ICYFdY+aIBw=",
|
||||
"aarch64-darwin": "sha256-7UajHu40n7JKqurU/+CGlitErsVFA2qDneUytI8+/zQ=",
|
||||
"aarch64-darwin": "sha256-U2UvE70nM0OI0VhIku8qnX+ptPbA+Q/y1BGXbFMcyt4=",
|
||||
"x86_64-darwin": "sha256-LxBsYdq5AzInQJzF89taXvS2vigew5C5hjaIEH8rTb8="
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,17 +54,22 @@ export function SessionHeader() {
|
|||
<Portal mount={mount()}>
|
||||
<button
|
||||
type="button"
|
||||
class="hidden md:flex w-[320px] h-8 p-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
onClick={() => command.trigger("file.open")}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="magnifying-glass" size="normal" class="icon-base" />
|
||||
<span class="flex-1 min-w-0 text-14-regular text-text-weak truncate">Search {name()}</span>
|
||||
<span class="flex-1 min-w-0 text-14-regular text-text-weak truncate" style={{ "line-height": 1 }}>
|
||||
Search {name()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Show when={hotkey()}>
|
||||
{(keybind) => (
|
||||
<span class="shrink-0 flex items-center justify-center h-5 px-2 rounded-[2px] border border-border-weak-base bg-surface-base text-12-medium text-text-weak">
|
||||
<span
|
||||
class="shrink-0 flex items-center justify-center h-5 px-2 rounded-[2px] bg-surface-base text-12-medium text-text-weak"
|
||||
style={{ "box-shadow": "var(--shadow-xxs-border)" }}
|
||||
>
|
||||
{keybind()}
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ export function SortableTerminalTab(props: { terminal: LocalPTY }): JSX.Element
|
|||
<Tabs.Trigger
|
||||
value={props.terminal.id}
|
||||
closeButton={
|
||||
terminal.all().length > 1 && (
|
||||
<IconButton icon="close" variant="ghost" onClick={() => terminal.close(props.terminal.id)} />
|
||||
terminal.tabs().length > 1 && (
|
||||
<IconButton icon="close" variant="ghost" onClick={() => terminal.closeTab(props.terminal.tabId)} />
|
||||
)
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,322 @@
|
|||
import { For, Show, createMemo, createSignal, onCleanup } from "solid-js"
|
||||
import { Terminal } from "./terminal"
|
||||
import { useTerminal, type Panel } from "@/context/terminal"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
|
||||
export interface TerminalSplitProps {
|
||||
tabId: string
|
||||
}
|
||||
|
||||
function computeLayout(
|
||||
panels: Record<string, Panel>,
|
||||
panelId: string,
|
||||
bounds: { top: number; left: number; width: number; height: number },
|
||||
): Map<string, { top: number; left: number; width: number; height: number }> {
|
||||
const result = new Map<string, { top: number; left: number; width: number; height: number }>()
|
||||
const panel = panels[panelId]
|
||||
if (!panel) return result
|
||||
|
||||
if (panel.ptyId) {
|
||||
result.set(panel.ptyId, bounds)
|
||||
} else if (panel.children && panel.children.length === 2) {
|
||||
const [leftId, rightId] = panel.children
|
||||
const sizes = panel.sizes ?? [50, 50]
|
||||
|
||||
if (panel.direction === "horizontal") {
|
||||
const topHeight = (bounds.height * sizes[0]) / 100
|
||||
const topBounds = { ...bounds, height: topHeight }
|
||||
const bottomBounds = { ...bounds, top: bounds.top + topHeight, height: bounds.height - topHeight }
|
||||
for (const [k, v] of computeLayout(panels, leftId, topBounds)) result.set(k, v)
|
||||
for (const [k, v] of computeLayout(panels, rightId, bottomBounds)) result.set(k, v)
|
||||
} else {
|
||||
const leftWidth = (bounds.width * sizes[0]) / 100
|
||||
const leftBounds = { ...bounds, width: leftWidth }
|
||||
const rightBounds = { ...bounds, left: bounds.left + leftWidth, width: bounds.width - leftWidth }
|
||||
for (const [k, v] of computeLayout(panels, leftId, leftBounds)) result.set(k, v)
|
||||
for (const [k, v] of computeLayout(panels, rightId, rightBounds)) result.set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function findPanelForPty(panels: Record<string, Panel>, ptyId: string): string | undefined {
|
||||
for (const [id, panel] of Object.entries(panels)) {
|
||||
if (panel.ptyId === ptyId) return id
|
||||
}
|
||||
}
|
||||
|
||||
export function TerminalSplit(props: TerminalSplitProps) {
|
||||
const terminal = useTerminal()
|
||||
const pane = createMemo(() => terminal.pane(props.tabId))
|
||||
const terminals = createMemo(() => terminal.all().filter((t) => t.tabId === props.tabId))
|
||||
const [containerFocused, setContainerFocused] = createSignal(true)
|
||||
|
||||
const layout = createMemo(() => {
|
||||
const p = pane()
|
||||
if (!p) {
|
||||
const single = terminals()[0]
|
||||
if (!single) return new Map()
|
||||
return new Map([[single.id, { top: 0, left: 0, width: 100, height: 100 }]])
|
||||
}
|
||||
return computeLayout(p.panels, p.root, { top: 0, left: 0, width: 100, height: 100 })
|
||||
})
|
||||
|
||||
const focused = createMemo(() => {
|
||||
const p = pane()
|
||||
if (!p) return props.tabId
|
||||
const focusedPanel = p.panels[p.focused ?? ""]
|
||||
return focusedPanel?.ptyId ?? props.tabId
|
||||
})
|
||||
|
||||
const handleFocus = (ptyId: string) => {
|
||||
const p = pane()
|
||||
if (!p) return
|
||||
const panelId = findPanelForPty(p.panels, ptyId)
|
||||
if (panelId) terminal.focus(props.tabId, panelId)
|
||||
}
|
||||
|
||||
const handleClose = (ptyId: string) => {
|
||||
const pty = terminal.all().find((t) => t.id === ptyId)
|
||||
if (!pty) return
|
||||
|
||||
const p = pane()
|
||||
if (!p) {
|
||||
if (pty.tabId === props.tabId) {
|
||||
terminal.closeTab(props.tabId)
|
||||
}
|
||||
return
|
||||
}
|
||||
const panelId = findPanelForPty(p.panels, ptyId)
|
||||
if (panelId) terminal.closeSplit(props.tabId, panelId)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
class="relative size-full"
|
||||
data-terminal-split-container
|
||||
onFocusIn={() => setContainerFocused(true)}
|
||||
onFocusOut={(e) => {
|
||||
const related = e.relatedTarget as Node | null
|
||||
if (!related || !e.currentTarget.contains(related)) {
|
||||
setContainerFocused(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<For each={terminals()}>
|
||||
{(pty) => {
|
||||
const bounds = createMemo(() => layout().get(pty.id) ?? { top: 0, left: 0, width: 100, height: 100 })
|
||||
const isFocused = createMemo(() => focused() === pty.id)
|
||||
const hasSplits = createMemo(() => !!pane())
|
||||
|
||||
return (
|
||||
<div
|
||||
class="absolute flex flex-col min-h-0"
|
||||
classList={{
|
||||
"ring-1 ring-inset ring-border-strong-base": containerFocused() && isFocused(),
|
||||
"border-l border-border-weak-base": bounds().left > 0,
|
||||
"border-t border-border-weak-base": bounds().top > 0,
|
||||
}}
|
||||
style={{
|
||||
top: `${bounds().top}%`,
|
||||
left: `${bounds().left}%`,
|
||||
width: `${bounds().width}%`,
|
||||
height: `${bounds().height}%`,
|
||||
}}
|
||||
onClick={() => handleFocus(pty.id)}
|
||||
>
|
||||
<Show when={pane()}>
|
||||
<div class="absolute top-1 right-1 z-10 opacity-0 hover:opacity-100 transition-opacity">
|
||||
<IconButton
|
||||
icon="close"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleClose(pty.id)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
class="flex-1 min-h-0"
|
||||
classList={{ "opacity-50": !containerFocused() || (hasSplits() && !isFocused()) }}
|
||||
>
|
||||
<Terminal
|
||||
pty={pty}
|
||||
focused={isFocused()}
|
||||
onCleanup={terminal.update}
|
||||
onConnectError={() => terminal.clone(pty.id)}
|
||||
onExit={() => handleClose(pty.id)}
|
||||
class="size-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<ResizeHandles tabId={props.tabId} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ResizeHandles(props: { tabId: string }) {
|
||||
const terminal = useTerminal()
|
||||
const pane = createMemo(() => terminal.pane(props.tabId))
|
||||
|
||||
const splits = createMemo(() => {
|
||||
const p = pane()
|
||||
if (!p) return []
|
||||
return Object.values(p.panels).filter((panel) => panel.children && panel.children.length === 2)
|
||||
})
|
||||
|
||||
return <For each={splits()}>{(panel) => <ResizeHandle tabId={props.tabId} panelId={panel.id} />}</For>
|
||||
}
|
||||
|
||||
function ResizeHandle(props: { tabId: string; panelId: string }) {
|
||||
const terminal = useTerminal()
|
||||
const pane = createMemo(() => terminal.pane(props.tabId))
|
||||
const panel = createMemo(() => pane()?.panels[props.panelId])
|
||||
|
||||
let cleanup: VoidFunction | undefined
|
||||
|
||||
onCleanup(() => cleanup?.())
|
||||
|
||||
const position = createMemo(() => {
|
||||
const p = pane()
|
||||
if (!p) return null
|
||||
const pan = panel()
|
||||
if (!pan?.children || pan.children.length !== 2) return null
|
||||
|
||||
const bounds = computePanelBounds(p.panels, p.root, props.panelId, {
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
})
|
||||
if (!bounds) return null
|
||||
|
||||
const sizes = pan.sizes ?? [50, 50]
|
||||
|
||||
if (pan.direction === "horizontal") {
|
||||
return {
|
||||
horizontal: true,
|
||||
top: bounds.top + (bounds.height * sizes[0]) / 100,
|
||||
left: bounds.left,
|
||||
size: bounds.width,
|
||||
}
|
||||
}
|
||||
return {
|
||||
horizontal: false,
|
||||
top: bounds.top,
|
||||
left: bounds.left + (bounds.width * sizes[0]) / 100,
|
||||
size: bounds.height,
|
||||
}
|
||||
})
|
||||
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
const pos = position()
|
||||
if (!pos) return
|
||||
|
||||
const container = (e.target as HTMLElement).closest("[data-terminal-split-container]") as HTMLElement
|
||||
if (!container) return
|
||||
|
||||
const rect = container.getBoundingClientRect()
|
||||
const pan = panel()
|
||||
if (!pan) return
|
||||
|
||||
const p = pane()
|
||||
if (!p) return
|
||||
const panelBounds = computePanelBounds(p.panels, p.root, props.panelId, {
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
})
|
||||
if (!panelBounds) return
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (pan.direction === "horizontal") {
|
||||
const totalPx = (rect.height * panelBounds.height) / 100
|
||||
const topPx = (rect.height * panelBounds.top) / 100
|
||||
const posPx = e.clientY - rect.top - topPx
|
||||
const percent = Math.max(10, Math.min(90, (posPx / totalPx) * 100))
|
||||
terminal.resizeSplit(props.tabId, props.panelId, [percent, 100 - percent])
|
||||
} else {
|
||||
const totalPx = (rect.width * panelBounds.width) / 100
|
||||
const leftPx = (rect.width * panelBounds.left) / 100
|
||||
const posPx = e.clientX - rect.left - leftPx
|
||||
const percent = Math.max(10, Math.min(90, (posPx / totalPx) * 100))
|
||||
terminal.resizeSplit(props.tabId, props.panelId, [percent, 100 - percent])
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove)
|
||||
document.removeEventListener("mouseup", handleMouseUp)
|
||||
cleanup = undefined
|
||||
}
|
||||
|
||||
cleanup = handleMouseUp
|
||||
document.addEventListener("mousemove", handleMouseMove)
|
||||
document.addEventListener("mouseup", handleMouseUp)
|
||||
}
|
||||
|
||||
return (
|
||||
<Show when={position()}>
|
||||
{(pos) => (
|
||||
<div
|
||||
data-component="resize-handle"
|
||||
data-direction={pos().horizontal ? "vertical" : "horizontal"}
|
||||
class="absolute"
|
||||
style={{
|
||||
top: `${pos().top}%`,
|
||||
left: `${pos().left}%`,
|
||||
width: pos().horizontal ? `${pos().size}%` : "8px",
|
||||
height: pos().horizontal ? "8px" : `${pos().size}%`,
|
||||
transform: pos().horizontal ? "translateY(-50%)" : "translateX(-50%)",
|
||||
cursor: pos().horizontal ? "row-resize" : "col-resize",
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function computePanelBounds(
|
||||
panels: Record<string, Panel>,
|
||||
currentId: string,
|
||||
targetId: string,
|
||||
bounds: { top: number; left: number; width: number; height: number },
|
||||
): { top: number; left: number; width: number; height: number } | null {
|
||||
if (currentId === targetId) return bounds
|
||||
|
||||
const panel = panels[currentId]
|
||||
if (!panel?.children || panel.children.length !== 2) return null
|
||||
|
||||
const [leftId, rightId] = panel.children
|
||||
const sizes = panel.sizes ?? [50, 50]
|
||||
const horizontal = panel.direction === "horizontal"
|
||||
|
||||
if (horizontal) {
|
||||
const topHeight = (bounds.height * sizes[0]) / 100
|
||||
const bottomHeight = bounds.height - topHeight
|
||||
const topBounds = { ...bounds, height: topHeight }
|
||||
const bottomBounds = { ...bounds, top: bounds.top + topHeight, height: bottomHeight }
|
||||
return (
|
||||
computePanelBounds(panels, leftId, targetId, topBounds) ??
|
||||
computePanelBounds(panels, rightId, targetId, bottomBounds)
|
||||
)
|
||||
}
|
||||
|
||||
const leftWidth = (bounds.width * sizes[0]) / 100
|
||||
const rightWidth = bounds.width - leftWidth
|
||||
const leftBounds = { ...bounds, width: leftWidth }
|
||||
const rightBounds = { ...bounds, left: bounds.left + leftWidth, width: rightWidth }
|
||||
return (
|
||||
computePanelBounds(panels, leftId, targetId, leftBounds) ??
|
||||
computePanelBounds(panels, rightId, targetId, rightBounds)
|
||||
)
|
||||
}
|
||||
|
|
@ -7,9 +7,11 @@ import { resolveThemeVariant, useTheme, withAlpha, type HexColor } from "@openco
|
|||
|
||||
export interface TerminalProps extends ComponentProps<"div"> {
|
||||
pty: LocalPTY
|
||||
focused?: boolean
|
||||
onSubmit?: () => void
|
||||
onCleanup?: (pty: LocalPTY) => void
|
||||
onConnectError?: (error: unknown) => void
|
||||
onExit?: () => void
|
||||
}
|
||||
|
||||
type TerminalColors = {
|
||||
|
|
@ -38,7 +40,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||
const sdk = useSDK()
|
||||
const theme = useTheme()
|
||||
let container!: HTMLDivElement
|
||||
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
|
||||
const [local, others] = splitProps(props, ["pty", "focused", "class", "classList", "onConnectError"])
|
||||
let ws: WebSocket | undefined
|
||||
let term: Term | undefined
|
||||
let ghostty: Ghostty
|
||||
|
|
@ -49,6 +51,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||
let handleTextareaBlur: () => void
|
||||
let reconnect: number | undefined
|
||||
let disposed = false
|
||||
let cleaning = false
|
||||
|
||||
const getTerminalColors = (): TerminalColors => {
|
||||
const mode = theme.mode()
|
||||
|
|
@ -88,6 +91,11 @@ export const Terminal = (props: TerminalProps) => {
|
|||
t.focus()
|
||||
setTimeout(() => t.textarea?.focus(), 0)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (local.focused) focusTerminal()
|
||||
})
|
||||
|
||||
const handlePointerDown = () => {
|
||||
const activeElement = document.activeElement
|
||||
if (activeElement instanceof HTMLElement && activeElement !== container) {
|
||||
|
|
@ -166,6 +174,11 @@ export const Terminal = (props: TerminalProps) => {
|
|||
return true
|
||||
}
|
||||
|
||||
// allow cmd+d and cmd+shift+d for terminal splitting
|
||||
if (event.metaKey && key === "d") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
|
|
@ -231,7 +244,6 @@ export const Terminal = (props: TerminalProps) => {
|
|||
// console.log("Scroll position:", ydisp)
|
||||
// })
|
||||
socket.addEventListener("open", () => {
|
||||
console.log("WebSocket connected")
|
||||
sdk.client.pty
|
||||
.update({
|
||||
ptyID: local.pty.id,
|
||||
|
|
@ -250,7 +262,9 @@ export const Terminal = (props: TerminalProps) => {
|
|||
props.onConnectError?.(error)
|
||||
})
|
||||
socket.addEventListener("close", () => {
|
||||
console.log("WebSocket disconnected")
|
||||
if (!cleaning) {
|
||||
props.onExit?.()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -274,6 +288,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||
})
|
||||
}
|
||||
|
||||
cleaning = true
|
||||
ws?.close()
|
||||
t?.dispose()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -81,13 +81,15 @@ export function Titlebar() {
|
|||
>
|
||||
<Show when={mac()}>
|
||||
<div class="w-[72px] h-full shrink-0" data-tauri-drag-region />
|
||||
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
|
||||
<IconButton icon="menu" variant="ghost" class="size-8 rounded-md" onClick={layout.mobileSidebar.toggle} />
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!mac()}>
|
||||
<div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
|
||||
<IconButton icon="menu" variant="ghost" class="size-8 rounded-md" onClick={layout.mobileSidebar.toggle} />
|
||||
</div>
|
||||
</Show>
|
||||
<IconButton
|
||||
icon="menu"
|
||||
variant="ghost"
|
||||
class="xl:hidden size-8 rounded-md"
|
||||
onClick={layout.mobileSidebar.toggle}
|
||||
/>
|
||||
<TooltipKeybind
|
||||
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0"}
|
||||
placement="bottom"
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||
createStore({
|
||||
sidebar: {
|
||||
opened: false,
|
||||
width: 280,
|
||||
width: 344,
|
||||
workspaces: {} as Record<string, boolean>,
|
||||
workspacesDefault: false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,12 +9,31 @@ export type LocalPTY = {
|
|||
id: string
|
||||
title: string
|
||||
titleNumber: number
|
||||
tabId: string
|
||||
rows?: number
|
||||
cols?: number
|
||||
buffer?: string
|
||||
scrollY?: number
|
||||
}
|
||||
|
||||
export type SplitDirection = "horizontal" | "vertical"
|
||||
|
||||
export type Panel = {
|
||||
id: string
|
||||
parentId?: string
|
||||
ptyId?: string
|
||||
direction?: SplitDirection
|
||||
children?: [string, string]
|
||||
sizes?: [number, number]
|
||||
}
|
||||
|
||||
export type TabPane = {
|
||||
id: string
|
||||
root: string
|
||||
panels: Record<string, Panel>
|
||||
focused?: string
|
||||
}
|
||||
|
||||
const WORKSPACE_KEY = "__workspace__"
|
||||
const MAX_TERMINAL_SESSIONS = 20
|
||||
|
||||
|
|
@ -25,6 +44,10 @@ type TerminalCacheEntry = {
|
|||
dispose: VoidFunction
|
||||
}
|
||||
|
||||
function generateId() {
|
||||
return Math.random().toString(36).slice(2, 10)
|
||||
}
|
||||
|
||||
function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, id: string | undefined) {
|
||||
const legacy = `${dir}/terminal${id ? "/" + id : ""}.v1`
|
||||
|
||||
|
|
@ -33,47 +56,102 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, id:
|
|||
createStore<{
|
||||
active?: string
|
||||
all: LocalPTY[]
|
||||
panes: Record<string, TabPane>
|
||||
}>({
|
||||
all: [],
|
||||
panes: {},
|
||||
}),
|
||||
)
|
||||
|
||||
const getNextTitleNumber = () => {
|
||||
const existing = new Set(store.all.filter((p) => p.tabId === p.id).map((pty) => pty.titleNumber))
|
||||
let next = 1
|
||||
while (existing.has(next)) next++
|
||||
return next
|
||||
}
|
||||
|
||||
const createPty = async (tabId?: string): Promise<LocalPTY | undefined> => {
|
||||
const tab = tabId ? store.all.find((p) => p.id === tabId) : undefined
|
||||
const num = tab?.titleNumber ?? getNextTitleNumber()
|
||||
const title = tab?.title ?? `Terminal ${num}`
|
||||
const pty = await sdk.client.pty.create({ title }).catch((e) => {
|
||||
console.error("Failed to create terminal", e)
|
||||
return undefined
|
||||
})
|
||||
if (!pty?.data?.id) return undefined
|
||||
return {
|
||||
id: pty.data.id,
|
||||
title,
|
||||
titleNumber: num,
|
||||
tabId: tabId ?? pty.data.id,
|
||||
}
|
||||
}
|
||||
|
||||
const getAllPtyIds = (pane: TabPane, panelId: string): string[] => {
|
||||
const panel = pane.panels[panelId]
|
||||
if (!panel) return []
|
||||
if (panel.ptyId) return [panel.ptyId]
|
||||
if (panel.children && panel.children.length === 2) {
|
||||
return [...getAllPtyIds(pane, panel.children[0]), ...getAllPtyIds(pane, panel.children[1])]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const getFirstLeaf = (pane: TabPane, panelId: string): string | undefined => {
|
||||
const panel = pane.panels[panelId]
|
||||
if (!panel) return undefined
|
||||
if (panel.ptyId) return panelId
|
||||
if (panel.children?.[0]) return getFirstLeaf(pane, panel.children[0])
|
||||
return undefined
|
||||
}
|
||||
|
||||
const migrate = (terminals: LocalPTY[]) =>
|
||||
terminals.map((p) => ((p as { tabId?: string }).tabId ? p : { ...p, tabId: p.id }))
|
||||
|
||||
const tabCache = new Map<string, LocalPTY>()
|
||||
const tabs = createMemo(() => {
|
||||
const migrated = migrate(store.all)
|
||||
const seen = new Set<string>()
|
||||
const result: LocalPTY[] = []
|
||||
for (const p of migrated) {
|
||||
if (!seen.has(p.tabId)) {
|
||||
seen.add(p.tabId)
|
||||
const cached = tabCache.get(p.tabId)
|
||||
if (cached) {
|
||||
cached.title = p.title
|
||||
cached.titleNumber = p.titleNumber
|
||||
result.push(cached)
|
||||
} else {
|
||||
const tab = { ...p, id: p.tabId }
|
||||
tabCache.set(p.tabId, tab)
|
||||
result.push(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key of tabCache.keys()) {
|
||||
if (!seen.has(key)) tabCache.delete(key)
|
||||
}
|
||||
return result
|
||||
})
|
||||
const all = createMemo(() => migrate(store.all))
|
||||
|
||||
return {
|
||||
ready,
|
||||
all: createMemo(() => Object.values(store.all)),
|
||||
active: createMemo(() => store.active),
|
||||
new() {
|
||||
const existingTitleNumbers = new Set(
|
||||
store.all.map((pty) => {
|
||||
const match = pty.titleNumber
|
||||
return match
|
||||
}),
|
||||
)
|
||||
tabs,
|
||||
all,
|
||||
active: () => store.active,
|
||||
panes: () => store.panes,
|
||||
pane: (tabId: string) => store.panes[tabId],
|
||||
panel: (tabId: string, panelId: string) => store.panes[tabId]?.panels[panelId],
|
||||
focused: (tabId: string) => store.panes[tabId]?.focused,
|
||||
|
||||
let nextNumber = 1
|
||||
while (existingTitleNumbers.has(nextNumber)) {
|
||||
nextNumber++
|
||||
}
|
||||
|
||||
sdk.client.pty
|
||||
.create({ title: `Terminal ${nextNumber}` })
|
||||
.then((pty) => {
|
||||
const id = pty.data?.id
|
||||
if (!id) return
|
||||
setStore("all", [
|
||||
...store.all,
|
||||
{
|
||||
id,
|
||||
title: pty.data?.title ?? "Terminal",
|
||||
titleNumber: nextNumber,
|
||||
},
|
||||
])
|
||||
setStore("active", id)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("Failed to create terminal", e)
|
||||
})
|
||||
async new() {
|
||||
const pty = await createPty()
|
||||
if (!pty) return
|
||||
setStore("all", [...store.all, pty])
|
||||
setStore("active", pty.tabId)
|
||||
},
|
||||
|
||||
update(pty: Partial<LocalPTY> & { id: string }) {
|
||||
setStore("all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x)))
|
||||
sdk.client.pty
|
||||
|
|
@ -86,46 +164,82 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, id:
|
|||
console.error("Failed to update terminal", e)
|
||||
})
|
||||
},
|
||||
|
||||
async clone(id: string) {
|
||||
const index = store.all.findIndex((x) => x.id === id)
|
||||
const pty = store.all[index]
|
||||
if (!pty) return
|
||||
const clone = await sdk.client.pty
|
||||
.create({
|
||||
title: pty.title,
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("Failed to clone terminal", e)
|
||||
return undefined
|
||||
})
|
||||
if (!clone?.data) return
|
||||
setStore("all", index, {
|
||||
...pty,
|
||||
...clone.data,
|
||||
const clone = await sdk.client.pty.create({ title: pty.title }).catch((e) => {
|
||||
console.error("Failed to clone terminal", e)
|
||||
return undefined
|
||||
})
|
||||
if (store.active === pty.id) {
|
||||
setStore("active", clone.data.id)
|
||||
if (!clone?.data) return
|
||||
setStore("all", index, { ...pty, ...clone.data })
|
||||
if (store.active === pty.tabId) {
|
||||
setStore("active", pty.tabId)
|
||||
}
|
||||
},
|
||||
|
||||
open(id: string) {
|
||||
setStore("active", id)
|
||||
},
|
||||
|
||||
async close(id: string) {
|
||||
batch(() => {
|
||||
setStore(
|
||||
"all",
|
||||
store.all.filter((x) => x.id !== id),
|
||||
)
|
||||
if (store.active === id) {
|
||||
const index = store.all.findIndex((f) => f.id === id)
|
||||
const previous = store.all[Math.max(0, index - 1)]
|
||||
setStore("active", previous?.id)
|
||||
const pty = store.all.find((x) => x.id === id)
|
||||
if (!pty) return
|
||||
|
||||
const pane = store.panes[pty.tabId]
|
||||
if (pane) {
|
||||
const panelId = Object.keys(pane.panels).find((key) => pane.panels[key].ptyId === id)
|
||||
if (panelId) {
|
||||
await this.closeSplit(pty.tabId, panelId)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (store.active === pty.tabId) {
|
||||
const remaining = store.all.filter((p) => p.tabId === p.id && p.id !== id)
|
||||
setStore("active", remaining[0]?.tabId)
|
||||
}
|
||||
|
||||
setStore(
|
||||
"all",
|
||||
store.all.filter((x) => x.id !== id),
|
||||
)
|
||||
|
||||
await sdk.client.pty.remove({ ptyID: id }).catch((e) => {
|
||||
console.error("Failed to close terminal", e)
|
||||
})
|
||||
},
|
||||
|
||||
async closeTab(tabId: string) {
|
||||
const pane = store.panes[tabId]
|
||||
const terminalsInTab = store.all.filter((p) => p.tabId === tabId)
|
||||
const ptyIds = pane ? getAllPtyIds(pane, pane.root) : terminalsInTab.map((p) => p.id)
|
||||
|
||||
const remainingTabs = store.all.filter((p) => p.tabId !== tabId)
|
||||
const uniqueTabIds = [...new Set(remainingTabs.map((p) => p.tabId))]
|
||||
|
||||
setStore(
|
||||
"all",
|
||||
store.all.filter((x) => !ptyIds.includes(x.id)),
|
||||
)
|
||||
setStore(
|
||||
"panes",
|
||||
produce((panes) => {
|
||||
delete panes[tabId]
|
||||
}),
|
||||
)
|
||||
if (store.active === tabId) {
|
||||
setStore("active", uniqueTabIds[0])
|
||||
}
|
||||
for (const ptyId of ptyIds) {
|
||||
await sdk.client.pty.remove({ ptyID: ptyId }).catch((e) => {
|
||||
console.error("Failed to close terminal", e)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
move(id: string, to: number) {
|
||||
const index = store.all.findIndex((f) => f.id === id)
|
||||
if (index === -1) return
|
||||
|
|
@ -136,6 +250,159 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, id:
|
|||
}),
|
||||
)
|
||||
},
|
||||
|
||||
async split(tabId: string, direction: SplitDirection) {
|
||||
const pane = store.panes[tabId]
|
||||
const newPty = await createPty(tabId)
|
||||
if (!newPty) return
|
||||
|
||||
setStore("all", [...store.all, newPty])
|
||||
|
||||
if (!pane) {
|
||||
const rootId = generateId()
|
||||
const leftId = generateId()
|
||||
const rightId = generateId()
|
||||
|
||||
setStore("panes", tabId, {
|
||||
id: tabId,
|
||||
root: rootId,
|
||||
panels: {
|
||||
[rootId]: {
|
||||
id: rootId,
|
||||
direction,
|
||||
children: [leftId, rightId],
|
||||
sizes: [50, 50],
|
||||
},
|
||||
[leftId]: {
|
||||
id: leftId,
|
||||
parentId: rootId,
|
||||
ptyId: tabId,
|
||||
},
|
||||
[rightId]: {
|
||||
id: rightId,
|
||||
parentId: rootId,
|
||||
ptyId: newPty.id,
|
||||
},
|
||||
},
|
||||
focused: rightId,
|
||||
})
|
||||
} else {
|
||||
const focusedPanelId = pane.focused
|
||||
if (!focusedPanelId) return
|
||||
|
||||
const focusedPanel = pane.panels[focusedPanelId]
|
||||
if (!focusedPanel?.ptyId) return
|
||||
|
||||
const oldPtyId = focusedPanel.ptyId
|
||||
const newSplitId = generateId()
|
||||
const newTerminalId = generateId()
|
||||
|
||||
setStore("panes", tabId, "panels", newSplitId, {
|
||||
id: newSplitId,
|
||||
parentId: focusedPanelId,
|
||||
ptyId: oldPtyId,
|
||||
})
|
||||
setStore("panes", tabId, "panels", newTerminalId, {
|
||||
id: newTerminalId,
|
||||
parentId: focusedPanelId,
|
||||
ptyId: newPty.id,
|
||||
})
|
||||
setStore("panes", tabId, "panels", focusedPanelId, "ptyId", undefined)
|
||||
setStore("panes", tabId, "panels", focusedPanelId, "direction", direction)
|
||||
setStore("panes", tabId, "panels", focusedPanelId, "children", [newSplitId, newTerminalId])
|
||||
setStore("panes", tabId, "panels", focusedPanelId, "sizes", [50, 50])
|
||||
setStore("panes", tabId, "focused", newTerminalId)
|
||||
}
|
||||
},
|
||||
|
||||
focus(tabId: string, panelId: string) {
|
||||
if (store.panes[tabId]) {
|
||||
setStore("panes", tabId, "focused", panelId)
|
||||
}
|
||||
},
|
||||
|
||||
async closeSplit(tabId: string, panelId: string) {
|
||||
const pane = store.panes[tabId]
|
||||
if (!pane) return
|
||||
|
||||
const panel = pane.panels[panelId]
|
||||
if (!panel) return
|
||||
|
||||
const ptyId = panel.ptyId
|
||||
if (!ptyId) return
|
||||
|
||||
if (!panel.parentId) {
|
||||
await this.closeTab(tabId)
|
||||
return
|
||||
}
|
||||
|
||||
const parentPanel = pane.panels[panel.parentId]
|
||||
if (!parentPanel?.children || parentPanel.children.length !== 2) return
|
||||
|
||||
const siblingId = parentPanel.children[0] === panelId ? parentPanel.children[1] : parentPanel.children[0]
|
||||
const sibling = pane.panels[siblingId]
|
||||
if (!sibling) return
|
||||
|
||||
const newFocused = sibling.ptyId ? panel.parentId! : (getFirstLeaf(pane, sibling.children![0]) ?? panel.parentId!)
|
||||
|
||||
batch(() => {
|
||||
setStore(
|
||||
"panes",
|
||||
tabId,
|
||||
"panels",
|
||||
produce((panels) => {
|
||||
const parent = panels[panel.parentId!]
|
||||
if (!parent) return
|
||||
|
||||
if (sibling.ptyId) {
|
||||
parent.ptyId = sibling.ptyId
|
||||
parent.direction = undefined
|
||||
parent.children = undefined
|
||||
parent.sizes = undefined
|
||||
} else if (sibling.children && sibling.children.length === 2) {
|
||||
parent.ptyId = undefined
|
||||
parent.direction = sibling.direction
|
||||
parent.children = sibling.children
|
||||
parent.sizes = sibling.sizes
|
||||
panels[sibling.children[0]].parentId = panel.parentId!
|
||||
panels[sibling.children[1]].parentId = panel.parentId!
|
||||
}
|
||||
|
||||
delete panels[panelId]
|
||||
delete panels[siblingId]
|
||||
}),
|
||||
)
|
||||
|
||||
setStore("panes", tabId, "focused", newFocused)
|
||||
|
||||
setStore(
|
||||
"all",
|
||||
store.all.filter((x) => x.id !== ptyId),
|
||||
)
|
||||
})
|
||||
|
||||
const remainingPanels = Object.values(store.panes[tabId]?.panels ?? {})
|
||||
const shouldCleanupPane = remainingPanels.length === 1 && remainingPanels[0]?.ptyId
|
||||
|
||||
if (shouldCleanupPane) {
|
||||
setStore(
|
||||
"panes",
|
||||
produce((panes) => {
|
||||
delete panes[tabId]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
await sdk.client.pty.remove({ ptyID: ptyId }).catch((e) => {
|
||||
console.error("Failed to close terminal", e)
|
||||
})
|
||||
},
|
||||
|
||||
resizeSplit(tabId: string, panelId: string, sizes: [number, number]) {
|
||||
if (store.panes[tabId]?.panels[panelId]) {
|
||||
setStore("panes", tabId, "panels", panelId, "sizes", sizes)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -189,14 +456,25 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
|||
|
||||
return {
|
||||
ready: () => session().ready(),
|
||||
tabs: () => session().tabs(),
|
||||
all: () => session().all(),
|
||||
active: () => session().active(),
|
||||
panes: () => session().panes(),
|
||||
pane: (tabId: string) => session().pane(tabId),
|
||||
panel: (tabId: string, panelId: string) => session().panel(tabId, panelId),
|
||||
focused: (tabId: string) => session().focused(tabId),
|
||||
new: () => session().new(),
|
||||
update: (pty: Partial<LocalPTY> & { id: string }) => session().update(pty),
|
||||
clone: (id: string) => session().clone(id),
|
||||
open: (id: string) => session().open(id),
|
||||
close: (id: string) => session().close(id),
|
||||
closeTab: (tabId: string) => session().closeTab(tabId),
|
||||
move: (id: string, to: number) => session().move(id, to),
|
||||
split: (tabId: string, direction: SplitDirection) => session().split(tabId, direction),
|
||||
focus: (tabId: string, panelId: string) => session().focus(tabId, panelId),
|
||||
closeSplit: (tabId: string, panelId: string) => session().closeSplit(tabId, panelId),
|
||||
resizeSplit: (tabId: string, panelId: string, sizes: [number, number]) =>
|
||||
session().resizeSplit(tabId, panelId, sizes),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,3 +9,16 @@
|
|||
*[data-tauri-drag-region] {
|
||||
app-region: drag;
|
||||
}
|
||||
|
||||
/* Terminal split resize handles */
|
||||
[data-terminal-split-container] [data-component="resize-handle"] {
|
||||
inset: unset;
|
||||
|
||||
&[data-direction="horizontal"] {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&[data-direction="vertical"] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ import { useServer } from "@/context/server"
|
|||
|
||||
export default function Layout(props: ParentProps) {
|
||||
const [store, setStore, , ready] = persisted(
|
||||
Persist.global("layout", ["layout.v6"]),
|
||||
Persist.global("layout.page", ["layout.page.v1"]),
|
||||
createStore({
|
||||
lastSession: {} as { [directory: string]: string },
|
||||
activeProject: undefined as string | undefined,
|
||||
|
|
@ -74,6 +74,8 @@ export default function Layout(props: ParentProps) {
|
|||
}),
|
||||
)
|
||||
|
||||
const pageReady = createMemo(() => ready())
|
||||
|
||||
let scrollContainerRef: HTMLDivElement | undefined
|
||||
const xlQuery = window.matchMedia("(min-width: 1280px)")
|
||||
const [isLargeViewport, setIsLargeViewport] = createSignal(xlQuery.matches)
|
||||
|
|
@ -85,6 +87,7 @@ export default function Layout(props: ParentProps) {
|
|||
const globalSDK = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
const layout = useLayout()
|
||||
const layoutReady = createMemo(() => layout.ready())
|
||||
const platform = usePlatform()
|
||||
const server = useServer()
|
||||
const notification = useNotification()
|
||||
|
|
@ -293,7 +296,8 @@ export default function Layout(props: ParentProps) {
|
|||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!ready()) return
|
||||
if (!pageReady()) return
|
||||
if (!layoutReady()) return
|
||||
const project = currentProject()
|
||||
if (!project) return
|
||||
|
||||
|
|
@ -318,6 +322,16 @@ export default function Layout(props: ParentProps) {
|
|||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!pageReady()) return
|
||||
if (!layoutReady()) return
|
||||
for (const [directory, expanded] of Object.entries(store.workspaceExpanded)) {
|
||||
if (layout.sidebar.workspaces(directory)()) continue
|
||||
if (!expanded) continue
|
||||
setStore("workspaceExpanded", directory, false)
|
||||
}
|
||||
})
|
||||
|
||||
const currentSessions = createMemo(() => {
|
||||
const project = currentProject()
|
||||
if (!project) return [] as Session[]
|
||||
|
|
@ -708,6 +722,7 @@ export default function Layout(props: ParentProps) {
|
|||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!pageReady()) return
|
||||
if (!params.dir || !params.id) return
|
||||
const directory = base64Decode(params.dir)
|
||||
const id = params.id
|
||||
|
|
@ -885,7 +900,7 @@ export default function Layout(props: ParentProps) {
|
|||
return (
|
||||
<div
|
||||
data-session-id={props.session.id}
|
||||
class="group/session relative w-full rounded-md cursor-default transition-colors px-3
|
||||
class="group/session relative w-full rounded-md cursor-default transition-colors pl-2 pr-3
|
||||
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
|
||||
>
|
||||
<Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={16} openDelay={1000}>
|
||||
|
|
@ -900,7 +915,7 @@ export default function Layout(props: ParentProps) {
|
|||
class="shrink-0 size-6 flex items-center justify-center"
|
||||
style={{ color: tint() ?? "var(--icon-interactive-base)" }}
|
||||
>
|
||||
<Switch>
|
||||
<Switch fallback={<Icon name="dash" size="small" class="text-icon-weak" />}>
|
||||
<Match when={isWorking()}>
|
||||
<Spinner class="size-[15px]" />
|
||||
</Match>
|
||||
|
|
@ -993,9 +1008,10 @@ export default function Layout(props: ParentProps) {
|
|||
<button
|
||||
type="button"
|
||||
classList={{
|
||||
"flex items-center justify-center size-10 p-1 rounded-lg border transition-colors cursor-default": true,
|
||||
"bg-transparent border-icon-strong-base hover:bg-surface-base-hover": selected(),
|
||||
"bg-transparent border-transparent hover:bg-surface-base-hover hover:border-border-weak-base": !selected(),
|
||||
"flex items-center justify-center size-10 p-1 rounded-lg overflow-hidden transition-colors cursor-default": true,
|
||||
"bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover": selected(),
|
||||
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
|
||||
!selected(),
|
||||
}}
|
||||
onClick={() => navigateToProject(props.project.worktree)}
|
||||
>
|
||||
|
|
@ -1048,7 +1064,7 @@ export default function Layout(props: ParentProps) {
|
|||
<div class="px-2 py-2 border-t border-border-weak-base">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="flex w-full text-left justify-start text-text-base px-2"
|
||||
class="flex w-full text-left justify-start text-text-base px-2 hover:bg-transparent active:bg-transparent"
|
||||
onClick={() => {
|
||||
layout.sidebar.open()
|
||||
navigateToProject(props.project.worktree)
|
||||
|
|
@ -1135,7 +1151,7 @@ export default function Layout(props: ParentProps) {
|
|||
>
|
||||
<div class="px-2 py-1">
|
||||
<div class="group/trigger relative">
|
||||
<Collapsible.Trigger class="flex items-center justify-between w-full pl-2 pr-16 py-1.5 rounded-md hover:bg-surface-raised-base-hover">
|
||||
<Collapsible.Trigger class="flex items-center justify-between w-full pl-2 pr-2 py-1.5 rounded-md hover:bg-surface-raised-base-hover transition-all group-hover/trigger:pr-16 group-focus-within/trigger:pr-16">
|
||||
<div class="flex items-center gap-1 min-w-0">
|
||||
<div class="flex items-center justify-center shrink-0 size-6">
|
||||
<Icon name="branch" size="small" />
|
||||
|
|
@ -1188,7 +1204,7 @@ export default function Layout(props: ParentProps) {
|
|||
<div class="relative w-full py-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="flex w-full text-left justify-start text-14-regular text-text-weak px-10"
|
||||
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-9 pr-10"
|
||||
size="large"
|
||||
onClick={(e: MouseEvent) => {
|
||||
loadMore()
|
||||
|
|
@ -1240,7 +1256,7 @@ export default function Layout(props: ParentProps) {
|
|||
<div class="relative w-full py-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="flex w-full text-left justify-start text-14-regular text-text-weak px-10"
|
||||
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-9 pr-10"
|
||||
size="large"
|
||||
onClick={(e: MouseEvent) => {
|
||||
loadMore()
|
||||
|
|
@ -1408,7 +1424,7 @@ export default function Layout(props: ParentProps) {
|
|||
<Button
|
||||
size="large"
|
||||
icon="plus-small"
|
||||
class="w-full"
|
||||
class="w-full max-w-[256px]"
|
||||
onClick={() => {
|
||||
navigate(`/${base64Encode(p.worktree)}/session`)
|
||||
layout.mobileSidebar.hide()
|
||||
|
|
@ -1425,7 +1441,7 @@ export default function Layout(props: ParentProps) {
|
|||
>
|
||||
<>
|
||||
<div class="py-4 px-3">
|
||||
<Button size="large" icon="plus-small" class="w-full" onClick={createWorkspace}>
|
||||
<Button size="large" icon="plus-small" class="w-full max-w-[256px]" onClick={createWorkspace}>
|
||||
New workspace
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -1496,7 +1512,7 @@ export default function Layout(props: ParentProps) {
|
|||
"hidden xl:block": true,
|
||||
"relative shrink-0": true,
|
||||
}}
|
||||
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : "64px" }}
|
||||
style={{ width: layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "64px" }}
|
||||
>
|
||||
<div class="@container w-full h-full contain-strict">
|
||||
<SidebarContent />
|
||||
|
|
@ -1505,9 +1521,9 @@ export default function Layout(props: ParentProps) {
|
|||
<ResizeHandle
|
||||
direction="horizontal"
|
||||
size={layout.sidebar.width()}
|
||||
min={214}
|
||||
min={244}
|
||||
max={window.innerWidth * 0.3 + 64}
|
||||
collapseThreshold={144}
|
||||
collapseThreshold={244}
|
||||
onResize={layout.sidebar.resize}
|
||||
onCollapse={layout.sidebar.close}
|
||||
/>
|
||||
|
|
@ -1516,7 +1532,7 @@ export default function Layout(props: ParentProps) {
|
|||
<div class="xl:hidden">
|
||||
<div
|
||||
classList={{
|
||||
"fixed inset-0 z-40 transition-opacity duration-200": true,
|
||||
"fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true,
|
||||
"opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
|
||||
"opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
|
||||
}}
|
||||
|
|
@ -1526,7 +1542,7 @@ export default function Layout(props: ParentProps) {
|
|||
/>
|
||||
<div
|
||||
classList={{
|
||||
"@container fixed inset-y-0 left-0 z-50 w-72 bg-background-base transition-transform duration-200 ease-out": true,
|
||||
"@container fixed top-10 bottom-0 left-0 z-50 w-72 bg-background-base transition-transform duration-200 ease-out": true,
|
||||
"translate-x-0": layout.mobileSidebar.opened(),
|
||||
"-translate-x-full": !layout.mobileSidebar.opened(),
|
||||
}}
|
||||
|
|
@ -1539,7 +1555,7 @@ export default function Layout(props: ParentProps) {
|
|||
<main
|
||||
classList={{
|
||||
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base": true,
|
||||
"border-l rounded-tl-sm": !layout.sidebar.opened(),
|
||||
"xl:border-l xl:rounded-tl-sm": !layout.sidebar.opened(),
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { useSync } from "@/context/sync"
|
|||
import { useTerminal, type LocalPTY } from "@/context/terminal"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { Terminal } from "@/components/terminal"
|
||||
import { TerminalSplit } from "@/components/terminal-split"
|
||||
import { checksum, base64Encode, base64Decode } from "@opencode-ai/util/encode"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { DialogSelectFile } from "@/components/dialog-select-file"
|
||||
|
|
@ -170,6 +171,7 @@ export default function Page() {
|
|||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||
const view = createMemo(() => layout.view(sessionKey()))
|
||||
const activeTerminal = createMemo(() => terminal.active())
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
createEffect(
|
||||
|
|
@ -380,7 +382,7 @@ export default function Page() {
|
|||
createEffect(() => {
|
||||
if (!view().terminal.opened()) return
|
||||
if (!terminal.ready()) return
|
||||
if (terminal.all().length !== 0) return
|
||||
if (terminal.tabs().length !== 0) return
|
||||
terminal.new()
|
||||
})
|
||||
|
||||
|
|
@ -459,6 +461,30 @@ export default function Page() {
|
|||
keybind: "ctrl+shift+`",
|
||||
onSelect: () => terminal.new(),
|
||||
},
|
||||
{
|
||||
id: "terminal.split.vertical",
|
||||
title: "Split terminal right",
|
||||
description: "Split the current terminal vertically",
|
||||
category: "Terminal",
|
||||
keybind: "mod+d",
|
||||
disabled: !terminal.active(),
|
||||
onSelect: () => {
|
||||
const active = terminal.active()
|
||||
if (active) terminal.split(active, "vertical")
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "terminal.split.horizontal",
|
||||
title: "Split terminal down",
|
||||
description: "Split the current terminal horizontally",
|
||||
category: "Terminal",
|
||||
keybind: "mod+shift+d",
|
||||
disabled: !terminal.active(),
|
||||
onSelect: () => {
|
||||
const active = terminal.active()
|
||||
if (active) terminal.split(active, "horizontal")
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "steps.toggle",
|
||||
title: "Toggle steps",
|
||||
|
|
@ -707,7 +733,7 @@ export default function Page() {
|
|||
const handleTerminalDragOver = (event: DragEvent) => {
|
||||
const { draggable, droppable } = event
|
||||
if (draggable && droppable) {
|
||||
const terminals = terminal.all()
|
||||
const terminals = terminal.tabs()
|
||||
const fromIndex = terminals.findIndex((t: LocalPTY) => t.id === draggable.id.toString())
|
||||
const toIndex = terminals.findIndex((t: LocalPTY) => t.id === droppable.id.toString())
|
||||
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
|
||||
|
|
@ -1009,7 +1035,7 @@ export default function Page() {
|
|||
|
||||
createEffect(() => {
|
||||
if (!terminal.ready()) return
|
||||
handoff.terminals = terminal.all().map((t) => t.title)
|
||||
handoff.terminals = terminal.tabs().map((t) => t.title)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
|
|
@ -1666,10 +1692,10 @@ export default function Page() {
|
|||
>
|
||||
<DragDropSensors />
|
||||
<ConstrainDragYAxis />
|
||||
<Tabs variant="alt" value={terminal.active()} onChange={terminal.open}>
|
||||
<Tabs variant="alt" value={activeTerminal()} onChange={terminal.open}>
|
||||
<Tabs.List class="h-10">
|
||||
<SortableProvider ids={terminal.all().map((t: LocalPTY) => t.id)}>
|
||||
<For each={terminal.all()}>{(pty) => <SortableTerminalTab terminal={pty} />}</For>
|
||||
<SortableProvider ids={terminal.tabs().map((t: LocalPTY) => t.id)}>
|
||||
<For each={terminal.tabs()}>{(pty) => <SortableTerminalTab terminal={pty} />}</For>
|
||||
</SortableProvider>
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<TooltipKeybind
|
||||
|
|
@ -1681,10 +1707,10 @@ export default function Page() {
|
|||
</TooltipKeybind>
|
||||
</div>
|
||||
</Tabs.List>
|
||||
<For each={terminal.all()}>
|
||||
<For each={terminal.tabs()}>
|
||||
{(pty) => (
|
||||
<Tabs.Content value={pty.id}>
|
||||
<Terminal pty={pty} onCleanup={terminal.update} onConnectError={() => terminal.clone(pty.id)} />
|
||||
<Tabs.Content value={pty.id} class="h-[calc(100%-2.5rem)]">
|
||||
<TerminalSplit tabId={pty.id} />
|
||||
</Tabs.Content>
|
||||
)}
|
||||
</For>
|
||||
|
|
@ -1692,7 +1718,7 @@ export default function Page() {
|
|||
<DragOverlay>
|
||||
<Show when={store.activeTerminalDraggable}>
|
||||
{(draggedId) => {
|
||||
const pty = createMemo(() => terminal.all().find((t: LocalPTY) => t.id === draggedId()))
|
||||
const pty = createMemo(() => terminal.tabs().find((t: LocalPTY) => t.id === draggedId()))
|
||||
return (
|
||||
<Show when={pty()}>
|
||||
{(t) => (
|
||||
|
|
|
|||
|
|
@ -1,186 +0,0 @@
|
|||
.light-rays-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.light-rays-container canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.light-rays-controls {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 9999;
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.light-rays-controls-toggle {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.light-rays-controls-toggle:hover {
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.light-rays-controls-panel {
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 240px;
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow-y: auto;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.control-group.checkbox {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.control-group.checkbox label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.control-group input[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.control-group input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
.control-group input[type="range"]::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.control-group input[type="range"]::-moz-range-thumb {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.control-group input[type="color"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.control-group input[type="color"]::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.control-group input[type="color"]::-webkit-color-swatch {
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.control-group select {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.control-group select:hover {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.control-group select option {
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.control-group input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin-top: 4px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.reset-button:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
color: #fff;
|
||||
}
|
||||
|
|
@ -1,924 +0,0 @@
|
|||
import { createSignal, createEffect, onMount, onCleanup, Show, For, Accessor, Setter } from "solid-js"
|
||||
import "./light-rays.css"
|
||||
|
||||
export type RaysOrigin =
|
||||
| "top-center"
|
||||
| "top-left"
|
||||
| "top-right"
|
||||
| "right"
|
||||
| "left"
|
||||
| "bottom-center"
|
||||
| "bottom-right"
|
||||
| "bottom-left"
|
||||
|
||||
export interface LightRaysConfig {
|
||||
raysOrigin: RaysOrigin
|
||||
raysColor: string
|
||||
raysSpeed: number
|
||||
lightSpread: number
|
||||
rayLength: number
|
||||
sourceWidth: number
|
||||
pulsating: boolean
|
||||
pulsatingMin: number
|
||||
pulsatingMax: number
|
||||
fadeDistance: number
|
||||
saturation: number
|
||||
followMouse: boolean
|
||||
mouseInfluence: number
|
||||
noiseAmount: number
|
||||
distortion: number
|
||||
opacity: number
|
||||
}
|
||||
|
||||
export const defaultConfig: LightRaysConfig = {
|
||||
raysOrigin: "top-center",
|
||||
raysColor: "#ffffff",
|
||||
raysSpeed: 1.0,
|
||||
lightSpread: 1.2,
|
||||
rayLength: 4.5,
|
||||
sourceWidth: 0.1,
|
||||
pulsating: true,
|
||||
pulsatingMin: 0.9,
|
||||
pulsatingMax: 1.05,
|
||||
fadeDistance: 1.25,
|
||||
saturation: 0.35,
|
||||
followMouse: false,
|
||||
mouseInfluence: 0.05,
|
||||
noiseAmount: 0.5,
|
||||
distortion: 0.0,
|
||||
opacity: 0.35,
|
||||
}
|
||||
|
||||
export interface LightRaysAnimationState {
|
||||
time: number
|
||||
intensity: number
|
||||
pulseValue: number
|
||||
}
|
||||
|
||||
interface LightRaysProps {
|
||||
config: Accessor<LightRaysConfig>
|
||||
class?: string
|
||||
onAnimationFrame?: (state: LightRaysAnimationState) => void
|
||||
}
|
||||
|
||||
const hexToRgb = (hex: string): [number, number, number] => {
|
||||
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [1, 1, 1]
|
||||
}
|
||||
|
||||
const getAnchorAndDir = (
|
||||
origin: RaysOrigin,
|
||||
w: number,
|
||||
h: number,
|
||||
): { anchor: [number, number]; dir: [number, number] } => {
|
||||
const outside = 0.2
|
||||
switch (origin) {
|
||||
case "top-left":
|
||||
return { anchor: [0, -outside * h], dir: [0, 1] }
|
||||
case "top-right":
|
||||
return { anchor: [w, -outside * h], dir: [0, 1] }
|
||||
case "left":
|
||||
return { anchor: [-outside * w, 0.5 * h], dir: [1, 0] }
|
||||
case "right":
|
||||
return { anchor: [(1 + outside) * w, 0.5 * h], dir: [-1, 0] }
|
||||
case "bottom-left":
|
||||
return { anchor: [0, (1 + outside) * h], dir: [0, -1] }
|
||||
case "bottom-center":
|
||||
return { anchor: [0.5 * w, (1 + outside) * h], dir: [0, -1] }
|
||||
case "bottom-right":
|
||||
return { anchor: [w, (1 + outside) * h], dir: [0, -1] }
|
||||
default: // "top-center"
|
||||
return { anchor: [0.5 * w, -outside * h], dir: [0, 1] }
|
||||
}
|
||||
}
|
||||
|
||||
interface UniformData {
|
||||
iTime: number
|
||||
iResolution: [number, number]
|
||||
rayPos: [number, number]
|
||||
rayDir: [number, number]
|
||||
raysColor: [number, number, number]
|
||||
raysSpeed: number
|
||||
lightSpread: number
|
||||
rayLength: number
|
||||
sourceWidth: number
|
||||
pulsating: number
|
||||
pulsatingMin: number
|
||||
pulsatingMax: number
|
||||
fadeDistance: number
|
||||
saturation: number
|
||||
mousePos: [number, number]
|
||||
mouseInfluence: number
|
||||
noiseAmount: number
|
||||
distortion: number
|
||||
}
|
||||
|
||||
const WGSL_SHADER = `
|
||||
struct Uniforms {
|
||||
iTime: f32,
|
||||
_pad0: f32,
|
||||
iResolution: vec2<f32>,
|
||||
rayPos: vec2<f32>,
|
||||
rayDir: vec2<f32>,
|
||||
raysColor: vec3<f32>,
|
||||
raysSpeed: f32,
|
||||
lightSpread: f32,
|
||||
rayLength: f32,
|
||||
sourceWidth: f32,
|
||||
pulsating: f32,
|
||||
pulsatingMin: f32,
|
||||
pulsatingMax: f32,
|
||||
fadeDistance: f32,
|
||||
saturation: f32,
|
||||
mousePos: vec2<f32>,
|
||||
mouseInfluence: f32,
|
||||
noiseAmount: f32,
|
||||
distortion: f32,
|
||||
_pad1: f32,
|
||||
_pad2: f32,
|
||||
_pad3: f32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) vUv: vec2<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
||||
var positions = array<vec2<f32>, 3>(
|
||||
vec2<f32>(-1.0, -1.0),
|
||||
vec2<f32>(3.0, -1.0),
|
||||
vec2<f32>(-1.0, 3.0)
|
||||
);
|
||||
|
||||
var output: VertexOutput;
|
||||
let pos = positions[vertexIndex];
|
||||
output.position = vec4<f32>(pos, 0.0, 1.0);
|
||||
output.vUv = pos * 0.5 + 0.5;
|
||||
return output;
|
||||
}
|
||||
|
||||
fn noise(st: vec2<f32>) -> f32 {
|
||||
return fract(sin(dot(st, vec2<f32>(12.9898, 78.233))) * 43758.5453123);
|
||||
}
|
||||
|
||||
fn rayStrength(raySource: vec2<f32>, rayRefDirection: vec2<f32>, coord: vec2<f32>,
|
||||
seedA: f32, seedB: f32, speed: f32) -> f32 {
|
||||
let sourceToCoord = coord - raySource;
|
||||
let dirNorm = normalize(sourceToCoord);
|
||||
let cosAngle = dot(dirNorm, rayRefDirection);
|
||||
|
||||
let distortedAngle = cosAngle + uniforms.distortion * sin(uniforms.iTime * 2.0 + length(sourceToCoord) * 0.01) * 0.2;
|
||||
|
||||
let spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / max(uniforms.lightSpread, 0.001));
|
||||
|
||||
let distance = length(sourceToCoord);
|
||||
let maxDistance = uniforms.iResolution.x * uniforms.rayLength;
|
||||
let lengthFalloff = clamp((maxDistance - distance) / maxDistance, 0.0, 1.0);
|
||||
|
||||
let fadeFalloff = clamp((uniforms.iResolution.x * uniforms.fadeDistance - distance) / (uniforms.iResolution.x * uniforms.fadeDistance), 0.5, 1.0);
|
||||
let pulseCenter = (uniforms.pulsatingMin + uniforms.pulsatingMax) * 0.5;
|
||||
let pulseAmplitude = (uniforms.pulsatingMax - uniforms.pulsatingMin) * 0.5;
|
||||
var pulse: f32;
|
||||
if (uniforms.pulsating > 0.5) {
|
||||
pulse = pulseCenter + pulseAmplitude * sin(uniforms.iTime * speed * 3.0);
|
||||
} else {
|
||||
pulse = 1.0;
|
||||
}
|
||||
|
||||
let baseStrength = clamp(
|
||||
(0.45 + 0.15 * sin(distortedAngle * seedA + uniforms.iTime * speed)) +
|
||||
(0.3 + 0.2 * cos(-distortedAngle * seedB + uniforms.iTime * speed)),
|
||||
0.0, 1.0
|
||||
);
|
||||
|
||||
return baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragmentMain(@builtin(position) fragCoord: vec4<f32>, @location(0) vUv: vec2<f32>) -> @location(0) vec4<f32> {
|
||||
let coord = vec2<f32>(fragCoord.x, fragCoord.y);
|
||||
|
||||
let normalizedX = (coord.x / uniforms.iResolution.x) - 0.5;
|
||||
let widthOffset = -normalizedX * uniforms.sourceWidth * uniforms.iResolution.x;
|
||||
|
||||
let perpDir = vec2<f32>(-uniforms.rayDir.y, uniforms.rayDir.x);
|
||||
let adjustedRayPos = uniforms.rayPos + perpDir * widthOffset;
|
||||
|
||||
var finalRayDir = uniforms.rayDir;
|
||||
if (uniforms.mouseInfluence > 0.0) {
|
||||
let mouseScreenPos = uniforms.mousePos * uniforms.iResolution;
|
||||
let mouseDirection = normalize(mouseScreenPos - adjustedRayPos);
|
||||
finalRayDir = normalize(mix(uniforms.rayDir, mouseDirection, uniforms.mouseInfluence));
|
||||
}
|
||||
|
||||
let rays1 = vec4<f32>(1.0) *
|
||||
rayStrength(adjustedRayPos, finalRayDir, coord, 36.2214, 21.11349,
|
||||
1.5 * uniforms.raysSpeed);
|
||||
let rays2 = vec4<f32>(1.0) *
|
||||
rayStrength(adjustedRayPos, finalRayDir, coord, 22.3991, 18.0234,
|
||||
1.1 * uniforms.raysSpeed);
|
||||
|
||||
var fragColor = rays1 * 0.5 + rays2 * 0.4;
|
||||
|
||||
if (uniforms.noiseAmount > 0.0) {
|
||||
let n = noise(coord * 0.01 + uniforms.iTime * 0.1);
|
||||
fragColor = vec4<f32>(fragColor.rgb * (1.0 - uniforms.noiseAmount + uniforms.noiseAmount * n), fragColor.a);
|
||||
}
|
||||
|
||||
let brightness = 1.0 - (coord.y / uniforms.iResolution.y);
|
||||
fragColor.x = fragColor.x * (0.1 + brightness * 0.8);
|
||||
fragColor.y = fragColor.y * (0.3 + brightness * 0.6);
|
||||
fragColor.z = fragColor.z * (0.5 + brightness * 0.5);
|
||||
|
||||
if (uniforms.saturation != 1.0) {
|
||||
let gray = dot(fragColor.rgb, vec3<f32>(0.299, 0.587, 0.114));
|
||||
fragColor = vec4<f32>(mix(vec3<f32>(gray), fragColor.rgb, uniforms.saturation), fragColor.a);
|
||||
}
|
||||
|
||||
fragColor = vec4<f32>(fragColor.rgb * uniforms.raysColor, fragColor.a);
|
||||
|
||||
return fragColor;
|
||||
}
|
||||
`
|
||||
|
||||
const UNIFORM_BUFFER_SIZE = 96
|
||||
|
||||
function createUniformBuffer(data: UniformData): Float32Array {
|
||||
const buffer = new Float32Array(24)
|
||||
buffer[0] = data.iTime
|
||||
buffer[1] = 0
|
||||
buffer[2] = data.iResolution[0]
|
||||
buffer[3] = data.iResolution[1]
|
||||
buffer[4] = data.rayPos[0]
|
||||
buffer[5] = data.rayPos[1]
|
||||
buffer[6] = data.rayDir[0]
|
||||
buffer[7] = data.rayDir[1]
|
||||
buffer[8] = data.raysColor[0]
|
||||
buffer[9] = data.raysColor[1]
|
||||
buffer[10] = data.raysColor[2]
|
||||
buffer[11] = data.raysSpeed
|
||||
buffer[12] = data.lightSpread
|
||||
buffer[13] = data.rayLength
|
||||
buffer[14] = data.sourceWidth
|
||||
buffer[15] = data.pulsating
|
||||
buffer[16] = data.pulsatingMin
|
||||
buffer[17] = data.pulsatingMax
|
||||
buffer[18] = data.fadeDistance
|
||||
buffer[19] = data.saturation
|
||||
buffer[20] = data.mousePos[0]
|
||||
buffer[21] = data.mousePos[1]
|
||||
buffer[22] = data.mouseInfluence
|
||||
buffer[23] = data.noiseAmount
|
||||
return buffer
|
||||
}
|
||||
|
||||
const UNIFORM_BUFFER_SIZE_CORRECTED = 112
|
||||
|
||||
function createUniformBufferCorrected(data: UniformData): Float32Array {
|
||||
const buffer = new Float32Array(28)
|
||||
buffer[0] = data.iTime
|
||||
buffer[1] = 0
|
||||
buffer[2] = data.iResolution[0]
|
||||
buffer[3] = data.iResolution[1]
|
||||
buffer[4] = data.rayPos[0]
|
||||
buffer[5] = data.rayPos[1]
|
||||
buffer[6] = data.rayDir[0]
|
||||
buffer[7] = data.rayDir[1]
|
||||
buffer[8] = data.raysColor[0]
|
||||
buffer[9] = data.raysColor[1]
|
||||
buffer[10] = data.raysColor[2]
|
||||
buffer[11] = data.raysSpeed
|
||||
buffer[12] = data.lightSpread
|
||||
buffer[13] = data.rayLength
|
||||
buffer[14] = data.sourceWidth
|
||||
buffer[15] = data.pulsating
|
||||
buffer[16] = data.pulsatingMin
|
||||
buffer[17] = data.pulsatingMax
|
||||
buffer[18] = data.fadeDistance
|
||||
buffer[19] = data.saturation
|
||||
buffer[20] = data.mousePos[0]
|
||||
buffer[21] = data.mousePos[1]
|
||||
buffer[22] = data.mouseInfluence
|
||||
buffer[23] = data.noiseAmount
|
||||
buffer[24] = data.distortion
|
||||
buffer[25] = 0
|
||||
buffer[26] = 0
|
||||
buffer[27] = 0
|
||||
return buffer
|
||||
}
|
||||
|
||||
export default function LightRays(props: LightRaysProps) {
|
||||
let containerRef: HTMLDivElement | undefined
|
||||
let canvasRef: HTMLCanvasElement | null = null
|
||||
let deviceRef: GPUDevice | null = null
|
||||
let contextRef: GPUCanvasContext | null = null
|
||||
let pipelineRef: GPURenderPipeline | null = null
|
||||
let uniformBufferRef: GPUBuffer | null = null
|
||||
let bindGroupRef: GPUBindGroup | null = null
|
||||
let animationIdRef: number | null = null
|
||||
let cleanupFunctionRef: (() => void) | null = null
|
||||
let uniformDataRef: UniformData | null = null
|
||||
|
||||
const mouseRef = { x: 0.5, y: 0.5 }
|
||||
const smoothMouseRef = { x: 0.5, y: 0.5 }
|
||||
|
||||
const [isVisible, setIsVisible] = createSignal(false)
|
||||
|
||||
onMount(() => {
|
||||
if (!containerRef) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const entry = entries[0]
|
||||
setIsVisible(entry.isIntersecting)
|
||||
},
|
||||
{ threshold: 0.1 },
|
||||
)
|
||||
|
||||
observer.observe(containerRef)
|
||||
|
||||
onCleanup(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const visible = isVisible()
|
||||
const config = props.config()
|
||||
if (!visible || !containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
if (cleanupFunctionRef) {
|
||||
cleanupFunctionRef()
|
||||
cleanupFunctionRef = null
|
||||
}
|
||||
|
||||
const initializeWebGPU = async () => {
|
||||
if (!containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
if (!containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!navigator.gpu) {
|
||||
console.warn("WebGPU is not supported in this browser")
|
||||
return
|
||||
}
|
||||
|
||||
const adapter = await navigator.gpu.requestAdapter()
|
||||
if (!adapter) {
|
||||
console.warn("Failed to get WebGPU adapter")
|
||||
return
|
||||
}
|
||||
|
||||
const device = await adapter.requestDevice()
|
||||
deviceRef = device
|
||||
|
||||
const canvas = document.createElement("canvas")
|
||||
canvas.style.width = "100%"
|
||||
canvas.style.height = "100%"
|
||||
canvasRef = canvas
|
||||
|
||||
while (containerRef.firstChild) {
|
||||
containerRef.removeChild(containerRef.firstChild)
|
||||
}
|
||||
containerRef.appendChild(canvas)
|
||||
|
||||
const context = canvas.getContext("webgpu")
|
||||
if (!context) {
|
||||
console.warn("Failed to get WebGPU context")
|
||||
return
|
||||
}
|
||||
contextRef = context
|
||||
|
||||
const presentationFormat = navigator.gpu.getPreferredCanvasFormat()
|
||||
context.configure({
|
||||
device,
|
||||
format: presentationFormat,
|
||||
alphaMode: "premultiplied",
|
||||
})
|
||||
|
||||
const shaderModule = device.createShaderModule({
|
||||
code: WGSL_SHADER,
|
||||
})
|
||||
|
||||
const uniformBuffer = device.createBuffer({
|
||||
size: UNIFORM_BUFFER_SIZE_CORRECTED,
|
||||
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
||||
})
|
||||
uniformBufferRef = uniformBuffer
|
||||
|
||||
const bindGroupLayout = device.createBindGroupLayout({
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
||||
buffer: { type: "uniform" },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const bindGroup = device.createBindGroup({
|
||||
layout: bindGroupLayout,
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
resource: { buffer: uniformBuffer },
|
||||
},
|
||||
],
|
||||
})
|
||||
bindGroupRef = bindGroup
|
||||
|
||||
const pipelineLayout = device.createPipelineLayout({
|
||||
bindGroupLayouts: [bindGroupLayout],
|
||||
})
|
||||
|
||||
const pipeline = device.createRenderPipeline({
|
||||
layout: pipelineLayout,
|
||||
vertex: {
|
||||
module: shaderModule,
|
||||
entryPoint: "vertexMain",
|
||||
},
|
||||
fragment: {
|
||||
module: shaderModule,
|
||||
entryPoint: "fragmentMain",
|
||||
targets: [
|
||||
{
|
||||
format: presentationFormat,
|
||||
blend: {
|
||||
color: {
|
||||
srcFactor: "src-alpha",
|
||||
dstFactor: "one-minus-src-alpha",
|
||||
operation: "add",
|
||||
},
|
||||
alpha: {
|
||||
srcFactor: "one",
|
||||
dstFactor: "one-minus-src-alpha",
|
||||
operation: "add",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
primitive: {
|
||||
topology: "triangle-list",
|
||||
},
|
||||
})
|
||||
pipelineRef = pipeline
|
||||
|
||||
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
|
||||
const dpr = Math.min(window.devicePixelRatio, 2)
|
||||
const w = wCSS * dpr
|
||||
const h = hCSS * dpr
|
||||
const { anchor, dir } = getAnchorAndDir(config.raysOrigin, w, h)
|
||||
|
||||
uniformDataRef = {
|
||||
iTime: 0,
|
||||
iResolution: [w, h],
|
||||
rayPos: anchor,
|
||||
rayDir: dir,
|
||||
raysColor: hexToRgb(config.raysColor),
|
||||
raysSpeed: config.raysSpeed,
|
||||
lightSpread: config.lightSpread,
|
||||
rayLength: config.rayLength,
|
||||
sourceWidth: config.sourceWidth,
|
||||
pulsating: config.pulsating ? 1.0 : 0.0,
|
||||
pulsatingMin: config.pulsatingMin,
|
||||
pulsatingMax: config.pulsatingMax,
|
||||
fadeDistance: config.fadeDistance,
|
||||
saturation: config.saturation,
|
||||
mousePos: [0.5, 0.5],
|
||||
mouseInfluence: config.mouseInfluence,
|
||||
noiseAmount: config.noiseAmount,
|
||||
distortion: config.distortion,
|
||||
}
|
||||
|
||||
const updatePlacement = () => {
|
||||
if (!containerRef || !canvasRef || !uniformDataRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const dpr = Math.min(window.devicePixelRatio, 2)
|
||||
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
|
||||
const w = Math.floor(wCSS * dpr)
|
||||
const h = Math.floor(hCSS * dpr)
|
||||
|
||||
canvasRef.width = w
|
||||
canvasRef.height = h
|
||||
|
||||
uniformDataRef.iResolution = [w, h]
|
||||
|
||||
const currentConfig = props.config()
|
||||
const { anchor, dir } = getAnchorAndDir(currentConfig.raysOrigin, w, h)
|
||||
uniformDataRef.rayPos = anchor
|
||||
uniformDataRef.rayDir = dir
|
||||
}
|
||||
|
||||
const loop = (t: number) => {
|
||||
if (!deviceRef || !contextRef || !pipelineRef || !uniformBufferRef || !bindGroupRef || !uniformDataRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentConfig = props.config()
|
||||
const timeSeconds = t * 0.001
|
||||
uniformDataRef.iTime = timeSeconds
|
||||
|
||||
if (currentConfig.followMouse && currentConfig.mouseInfluence > 0.0) {
|
||||
const smoothing = 0.92
|
||||
|
||||
smoothMouseRef.x = smoothMouseRef.x * smoothing + mouseRef.x * (1 - smoothing)
|
||||
smoothMouseRef.y = smoothMouseRef.y * smoothing + mouseRef.y * (1 - smoothing)
|
||||
|
||||
uniformDataRef.mousePos = [smoothMouseRef.x, smoothMouseRef.y]
|
||||
}
|
||||
|
||||
if (props.onAnimationFrame) {
|
||||
const pulseCenter = (currentConfig.pulsatingMin + currentConfig.pulsatingMax) * 0.5
|
||||
const pulseAmplitude = (currentConfig.pulsatingMax - currentConfig.pulsatingMin) * 0.5
|
||||
const pulseValue = currentConfig.pulsating
|
||||
? pulseCenter + pulseAmplitude * Math.sin(timeSeconds * currentConfig.raysSpeed * 3.0)
|
||||
: 1.0
|
||||
|
||||
const baseIntensity1 = 0.45 + 0.15 * Math.sin(timeSeconds * currentConfig.raysSpeed * 1.5)
|
||||
const baseIntensity2 = 0.3 + 0.2 * Math.cos(timeSeconds * currentConfig.raysSpeed * 1.1)
|
||||
const intensity = (baseIntensity1 + baseIntensity2) * pulseValue
|
||||
|
||||
props.onAnimationFrame({
|
||||
time: timeSeconds,
|
||||
intensity,
|
||||
pulseValue,
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const uniformData = createUniformBufferCorrected(uniformDataRef)
|
||||
deviceRef.queue.writeBuffer(uniformBufferRef, 0, uniformData.buffer)
|
||||
|
||||
const commandEncoder = deviceRef.createCommandEncoder()
|
||||
|
||||
const textureView = contextRef.getCurrentTexture().createView()
|
||||
|
||||
const renderPass = commandEncoder.beginRenderPass({
|
||||
colorAttachments: [
|
||||
{
|
||||
view: textureView,
|
||||
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
||||
loadOp: "clear",
|
||||
storeOp: "store",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
renderPass.setPipeline(pipelineRef)
|
||||
renderPass.setBindGroup(0, bindGroupRef)
|
||||
renderPass.draw(3)
|
||||
renderPass.end()
|
||||
|
||||
deviceRef.queue.submit([commandEncoder.finish()])
|
||||
|
||||
animationIdRef = requestAnimationFrame(loop)
|
||||
} catch (error) {
|
||||
console.warn("WebGPU rendering error:", error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", updatePlacement)
|
||||
updatePlacement()
|
||||
animationIdRef = requestAnimationFrame(loop)
|
||||
|
||||
cleanupFunctionRef = () => {
|
||||
if (animationIdRef) {
|
||||
cancelAnimationFrame(animationIdRef)
|
||||
animationIdRef = null
|
||||
}
|
||||
|
||||
window.removeEventListener("resize", updatePlacement)
|
||||
|
||||
if (uniformBufferRef) {
|
||||
uniformBufferRef.destroy()
|
||||
uniformBufferRef = null
|
||||
}
|
||||
|
||||
if (deviceRef) {
|
||||
deviceRef.destroy()
|
||||
deviceRef = null
|
||||
}
|
||||
|
||||
if (canvasRef && canvasRef.parentNode) {
|
||||
canvasRef.parentNode.removeChild(canvasRef)
|
||||
}
|
||||
|
||||
canvasRef = null
|
||||
contextRef = null
|
||||
pipelineRef = null
|
||||
bindGroupRef = null
|
||||
uniformDataRef = null
|
||||
}
|
||||
}
|
||||
|
||||
initializeWebGPU()
|
||||
|
||||
onCleanup(() => {
|
||||
if (cleanupFunctionRef) {
|
||||
cleanupFunctionRef()
|
||||
cleanupFunctionRef = null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!uniformDataRef || !containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const config = props.config()
|
||||
|
||||
uniformDataRef.raysColor = hexToRgb(config.raysColor)
|
||||
uniformDataRef.raysSpeed = config.raysSpeed
|
||||
uniformDataRef.lightSpread = config.lightSpread
|
||||
uniformDataRef.rayLength = config.rayLength
|
||||
uniformDataRef.sourceWidth = config.sourceWidth
|
||||
uniformDataRef.pulsating = config.pulsating ? 1.0 : 0.0
|
||||
uniformDataRef.pulsatingMin = config.pulsatingMin
|
||||
uniformDataRef.pulsatingMax = config.pulsatingMax
|
||||
uniformDataRef.fadeDistance = config.fadeDistance
|
||||
uniformDataRef.saturation = config.saturation
|
||||
uniformDataRef.mouseInfluence = config.mouseInfluence
|
||||
uniformDataRef.noiseAmount = config.noiseAmount
|
||||
uniformDataRef.distortion = config.distortion
|
||||
|
||||
const dpr = Math.min(window.devicePixelRatio, 2)
|
||||
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
|
||||
const { anchor, dir } = getAnchorAndDir(config.raysOrigin, wCSS * dpr, hCSS * dpr)
|
||||
uniformDataRef.rayPos = anchor
|
||||
uniformDataRef.rayDir = dir
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const config = props.config()
|
||||
if (!config.followMouse) {
|
||||
return
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!containerRef) {
|
||||
return
|
||||
}
|
||||
const rect = containerRef.getBoundingClientRect()
|
||||
const x = (e.clientX - rect.left) / rect.width
|
||||
const y = (e.clientY - rect.top) / rect.height
|
||||
mouseRef.x = x
|
||||
mouseRef.y = y
|
||||
}
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove)
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("mousemove", handleMouseMove)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
class={`light-rays-container ${props.class ?? ""}`.trim()}
|
||||
style={{ opacity: props.config().opacity }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface LightRaysControlsProps {
|
||||
config: Accessor<LightRaysConfig>
|
||||
setConfig: Setter<LightRaysConfig>
|
||||
}
|
||||
|
||||
export function LightRaysControls(props: LightRaysControlsProps) {
|
||||
const [isOpen, setIsOpen] = createSignal(true)
|
||||
|
||||
const updateConfig = <K extends keyof LightRaysConfig>(key: K, value: LightRaysConfig[K]) => {
|
||||
props.setConfig((prev) => ({ ...prev, [key]: value }))
|
||||
}
|
||||
|
||||
const origins: RaysOrigin[] = [
|
||||
"top-center",
|
||||
"top-left",
|
||||
"top-right",
|
||||
"left",
|
||||
"right",
|
||||
"bottom-center",
|
||||
"bottom-left",
|
||||
"bottom-right",
|
||||
]
|
||||
|
||||
return (
|
||||
<div class="light-rays-controls">
|
||||
<button class="light-rays-controls-toggle" onClick={() => setIsOpen(!isOpen())}>
|
||||
{isOpen() ? "▼" : "▶"} Light Rays
|
||||
</button>
|
||||
<Show when={isOpen()}>
|
||||
<div class="light-rays-controls-panel">
|
||||
<div class="control-group">
|
||||
<label>Origin</label>
|
||||
<select
|
||||
value={props.config().raysOrigin}
|
||||
onChange={(e) => updateConfig("raysOrigin", e.currentTarget.value as RaysOrigin)}
|
||||
>
|
||||
<For each={origins}>{(origin) => <option value={origin}>{origin}</option>}</For>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Color</label>
|
||||
<input
|
||||
type="color"
|
||||
value={props.config().raysColor}
|
||||
onInput={(e) => updateConfig("raysColor", e.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Speed: {props.config().raysSpeed.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="3"
|
||||
step="0.01"
|
||||
value={props.config().raysSpeed}
|
||||
onInput={(e) => updateConfig("raysSpeed", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Light Spread: {props.config().lightSpread.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="5"
|
||||
step="0.01"
|
||||
value={props.config().lightSpread}
|
||||
onInput={(e) => updateConfig("lightSpread", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Ray Length: {props.config().rayLength.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="5"
|
||||
step="0.01"
|
||||
value={props.config().rayLength}
|
||||
onInput={(e) => updateConfig("rayLength", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Source Width: {props.config().sourceWidth.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.01"
|
||||
value={props.config().sourceWidth}
|
||||
onInput={(e) => updateConfig("sourceWidth", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Fade Distance: {props.config().fadeDistance.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="3"
|
||||
step="0.01"
|
||||
value={props.config().fadeDistance}
|
||||
onInput={(e) => updateConfig("fadeDistance", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Saturation: {props.config().saturation.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.01"
|
||||
value={props.config().saturation}
|
||||
onInput={(e) => updateConfig("saturation", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Mouse Influence: {props.config().mouseInfluence.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={props.config().mouseInfluence}
|
||||
onInput={(e) => updateConfig("mouseInfluence", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Noise: {props.config().noiseAmount.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={props.config().noiseAmount}
|
||||
onInput={(e) => updateConfig("noiseAmount", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Distortion: {props.config().distortion.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.01"
|
||||
value={props.config().distortion}
|
||||
onInput={(e) => updateConfig("distortion", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Opacity: {props.config().opacity.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={props.config().opacity}
|
||||
onInput={(e) => updateConfig("opacity", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group checkbox">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.config().pulsating}
|
||||
onChange={(e) => updateConfig("pulsating", e.currentTarget.checked)}
|
||||
/>
|
||||
Pulsating
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Show when={props.config().pulsating}>
|
||||
<div class="control-group">
|
||||
<label>Pulse Min: {props.config().pulsatingMin.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={props.config().pulsatingMin}
|
||||
onInput={(e) => updateConfig("pulsatingMin", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Pulse Max: {props.config().pulsatingMax.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.01"
|
||||
value={props.config().pulsatingMax}
|
||||
onInput={(e) => updateConfig("pulsatingMax", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="control-group checkbox">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.config().followMouse}
|
||||
onChange={(e) => updateConfig("followMouse", e.currentTarget.checked)}
|
||||
/>
|
||||
Follow Mouse
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="reset-button" onClick={() => props.setConfig(defaultConfig)}>
|
||||
Reset to Defaults
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
.spotlight-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 50dvh;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spotlight-container canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,820 @@
|
|||
import { createSignal, createEffect, onMount, onCleanup, Accessor } from "solid-js"
|
||||
import "./spotlight.css"
|
||||
|
||||
export interface ParticlesConfig {
|
||||
enabled: boolean
|
||||
amount: number
|
||||
size: [number, number]
|
||||
speed: number
|
||||
opacity: number
|
||||
drift: number
|
||||
}
|
||||
|
||||
export interface SpotlightConfig {
|
||||
placement: [number, number]
|
||||
color: string
|
||||
speed: number
|
||||
spread: number
|
||||
length: number
|
||||
width: number
|
||||
pulsating: false | [number, number]
|
||||
distance: number
|
||||
saturation: number
|
||||
noiseAmount: number
|
||||
distortion: number
|
||||
opacity: number
|
||||
particles: ParticlesConfig
|
||||
}
|
||||
|
||||
export const defaultConfig: SpotlightConfig = {
|
||||
placement: [0.5, -0.15],
|
||||
color: "#ffffff",
|
||||
speed: 0.8,
|
||||
spread: 0.5,
|
||||
length: 4.0,
|
||||
width: 0.15,
|
||||
pulsating: [0.95, 1.1],
|
||||
distance: 3.5,
|
||||
saturation: 0.35,
|
||||
noiseAmount: 0.15,
|
||||
distortion: 0.05,
|
||||
opacity: 0.325,
|
||||
particles: {
|
||||
enabled: true,
|
||||
amount: 70,
|
||||
size: [1.25, 1.5],
|
||||
speed: 0.75,
|
||||
opacity: 0.9,
|
||||
drift: 1.5,
|
||||
},
|
||||
}
|
||||
|
||||
export interface SpotlightAnimationState {
|
||||
time: number
|
||||
intensity: number
|
||||
pulseValue: number
|
||||
}
|
||||
|
||||
interface SpotlightProps {
|
||||
config: Accessor<SpotlightConfig>
|
||||
class?: string
|
||||
onAnimationFrame?: (state: SpotlightAnimationState) => void
|
||||
}
|
||||
|
||||
const hexToRgb = (hex: string): [number, number, number] => {
|
||||
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [1, 1, 1]
|
||||
}
|
||||
|
||||
const getAnchorAndDir = (
|
||||
placement: [number, number],
|
||||
w: number,
|
||||
h: number,
|
||||
): { anchor: [number, number]; dir: [number, number] } => {
|
||||
const [px, py] = placement
|
||||
const outside = 0.2
|
||||
|
||||
let anchorX = px * w
|
||||
let anchorY = py * h
|
||||
let dirX = 0
|
||||
let dirY = 0
|
||||
|
||||
const centerX = 0.5
|
||||
const centerY = 0.5
|
||||
|
||||
if (py <= 0.25) {
|
||||
anchorY = -outside * h + py * h
|
||||
dirY = 1
|
||||
dirX = (centerX - px) * 0.5
|
||||
} else if (py >= 0.75) {
|
||||
anchorY = (1 + outside) * h - (1 - py) * h
|
||||
dirY = -1
|
||||
dirX = (centerX - px) * 0.5
|
||||
} else if (px <= 0.25) {
|
||||
anchorX = -outside * w + px * w
|
||||
dirX = 1
|
||||
dirY = (centerY - py) * 0.5
|
||||
} else if (px >= 0.75) {
|
||||
anchorX = (1 + outside) * w - (1 - px) * w
|
||||
dirX = -1
|
||||
dirY = (centerY - py) * 0.5
|
||||
} else {
|
||||
dirY = 1
|
||||
}
|
||||
|
||||
const len = Math.sqrt(dirX * dirX + dirY * dirY)
|
||||
if (len > 0) {
|
||||
dirX /= len
|
||||
dirY /= len
|
||||
}
|
||||
|
||||
return { anchor: [anchorX, anchorY], dir: [dirX, dirY] }
|
||||
}
|
||||
|
||||
interface UniformData {
|
||||
iTime: number
|
||||
iResolution: [number, number]
|
||||
lightPos: [number, number]
|
||||
lightDir: [number, number]
|
||||
color: [number, number, number]
|
||||
speed: number
|
||||
lightSpread: number
|
||||
lightLength: number
|
||||
sourceWidth: number
|
||||
pulsating: number
|
||||
pulsatingMin: number
|
||||
pulsatingMax: number
|
||||
fadeDistance: number
|
||||
saturation: number
|
||||
noiseAmount: number
|
||||
distortion: number
|
||||
particlesEnabled: number
|
||||
particleAmount: number
|
||||
particleSizeMin: number
|
||||
particleSizeMax: number
|
||||
particleSpeed: number
|
||||
particleOpacity: number
|
||||
particleDrift: number
|
||||
}
|
||||
|
||||
const WGSL_SHADER = `
|
||||
struct Uniforms {
|
||||
iTime: f32,
|
||||
_pad0: f32,
|
||||
iResolution: vec2<f32>,
|
||||
lightPos: vec2<f32>,
|
||||
lightDir: vec2<f32>,
|
||||
color: vec3<f32>,
|
||||
speed: f32,
|
||||
lightSpread: f32,
|
||||
lightLength: f32,
|
||||
sourceWidth: f32,
|
||||
pulsating: f32,
|
||||
pulsatingMin: f32,
|
||||
pulsatingMax: f32,
|
||||
fadeDistance: f32,
|
||||
saturation: f32,
|
||||
noiseAmount: f32,
|
||||
distortion: f32,
|
||||
particlesEnabled: f32,
|
||||
particleAmount: f32,
|
||||
particleSizeMin: f32,
|
||||
particleSizeMax: f32,
|
||||
particleSpeed: f32,
|
||||
particleOpacity: f32,
|
||||
particleDrift: f32,
|
||||
_pad1: f32,
|
||||
_pad2: f32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) vUv: vec2<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
||||
var positions = array<vec2<f32>, 3>(
|
||||
vec2<f32>(-1.0, -1.0),
|
||||
vec2<f32>(3.0, -1.0),
|
||||
vec2<f32>(-1.0, 3.0)
|
||||
);
|
||||
|
||||
var output: VertexOutput;
|
||||
let pos = positions[vertexIndex];
|
||||
output.position = vec4<f32>(pos, 0.0, 1.0);
|
||||
output.vUv = pos * 0.5 + 0.5;
|
||||
return output;
|
||||
}
|
||||
|
||||
fn hash(p: vec2<f32>) -> f32 {
|
||||
let p3 = fract(p.xyx * 0.1031);
|
||||
return fract((p3.x + p3.y) * p3.z + dot(p3, p3.yzx + 33.33));
|
||||
}
|
||||
|
||||
fn hash2(p: vec2<f32>) -> vec2<f32> {
|
||||
let n = sin(dot(p, vec2<f32>(41.0, 289.0)));
|
||||
return fract(vec2<f32>(n * 262144.0, n * 32768.0));
|
||||
}
|
||||
|
||||
fn fastNoise(st: vec2<f32>) -> f32 {
|
||||
return fract(sin(dot(st, vec2<f32>(12.9898, 78.233))) * 43758.5453);
|
||||
}
|
||||
|
||||
fn lightStrengthCombined(lightSource: vec2<f32>, lightRefDirection: vec2<f32>, coord: vec2<f32>) -> f32 {
|
||||
let sourceToCoord = coord - lightSource;
|
||||
let distSq = dot(sourceToCoord, sourceToCoord);
|
||||
let distance = sqrt(distSq);
|
||||
|
||||
let baseSize = min(uniforms.iResolution.x, uniforms.iResolution.y);
|
||||
let maxDistance = max(baseSize * uniforms.lightLength, 0.001);
|
||||
if (distance > maxDistance) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let invDist = 1.0 / max(distance, 0.001);
|
||||
let dirNorm = sourceToCoord * invDist;
|
||||
let cosAngle = dot(dirNorm, lightRefDirection);
|
||||
|
||||
if (cosAngle < 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let side = dot(dirNorm, vec2<f32>(-lightRefDirection.y, lightRefDirection.x));
|
||||
let time = uniforms.iTime;
|
||||
let speed = uniforms.speed;
|
||||
|
||||
let asymNoise = fastNoise(vec2<f32>(side * 6.0 + time * 0.12, distance * 0.004 + cosAngle * 2.0));
|
||||
let asymShift = (asymNoise - 0.5) * uniforms.distortion * 0.6;
|
||||
|
||||
let distortPhase = time * 1.4 + distance * 0.006 + cosAngle * 4.5 + side * 1.7;
|
||||
let distortedAngle = cosAngle + uniforms.distortion * sin(distortPhase) * 0.22 + asymShift;
|
||||
|
||||
let flickerSeed = cosAngle * 9.0 + side * 4.0 + time * speed * 0.35;
|
||||
let flicker = 0.86 + fastNoise(vec2<f32>(flickerSeed, distance * 0.01)) * 0.28;
|
||||
|
||||
let asymSpread = max(uniforms.lightSpread * (0.9 + (asymNoise - 0.5) * 0.25), 0.001);
|
||||
let spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / asymSpread);
|
||||
let lengthFalloff = clamp(1.0 - distance / maxDistance, 0.0, 1.0);
|
||||
|
||||
let fadeMaxDist = max(baseSize * uniforms.fadeDistance, 0.001);
|
||||
let fadeFalloff = clamp((fadeMaxDist - distance) / fadeMaxDist, 0.0, 1.0);
|
||||
|
||||
var pulse: f32 = 1.0;
|
||||
if (uniforms.pulsating > 0.5) {
|
||||
let pulseCenter = (uniforms.pulsatingMin + uniforms.pulsatingMax) * 0.5;
|
||||
let pulseAmplitude = (uniforms.pulsatingMax - uniforms.pulsatingMin) * 0.5;
|
||||
pulse = pulseCenter + pulseAmplitude * sin(time * speed * 3.0);
|
||||
}
|
||||
|
||||
let timeSpeed = time * speed;
|
||||
let wave = 0.5
|
||||
+ 0.25 * sin(cosAngle * 28.0 + side * 8.0 + timeSpeed * 1.2)
|
||||
+ 0.18 * cos(cosAngle * 22.0 - timeSpeed * 0.95 + side * 6.0)
|
||||
+ 0.12 * sin(cosAngle * 35.0 + timeSpeed * 1.6 + asymNoise * 3.0);
|
||||
let minStrength = 0.14 + asymNoise * 0.06;
|
||||
let baseStrength = max(clamp(wave * (0.85 + asymNoise * 0.3), 0.0, 1.0), minStrength);
|
||||
|
||||
let lightStrength = baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse * flicker;
|
||||
let ambientLight = (0.06 + asymNoise * 0.04) * lengthFalloff * fadeFalloff * spreadFactor;
|
||||
|
||||
return max(lightStrength, ambientLight);
|
||||
}
|
||||
|
||||
fn particle(coord: vec2<f32>, particlePos: vec2<f32>, size: f32) -> f32 {
|
||||
let delta = coord - particlePos;
|
||||
let distSq = dot(delta, delta);
|
||||
let sizeSq = size * size;
|
||||
|
||||
if (distSq > sizeSq * 9.0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let d = sqrt(distSq);
|
||||
let core = smoothstep(size, size * 0.35, d);
|
||||
let glow = smoothstep(size * 3.0, 0.0, d) * 0.55;
|
||||
return core + glow;
|
||||
}
|
||||
|
||||
fn renderParticles(coord: vec2<f32>, lightSource: vec2<f32>, lightDir: vec2<f32>) -> f32 {
|
||||
if (uniforms.particlesEnabled < 0.5 || uniforms.particleAmount < 1.0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
var particleSum: f32 = 0.0;
|
||||
let particleCount = i32(uniforms.particleAmount);
|
||||
let time = uniforms.iTime * uniforms.particleSpeed;
|
||||
let perpDir = vec2<f32>(-lightDir.y, lightDir.x);
|
||||
let baseSize = min(uniforms.iResolution.x, uniforms.iResolution.y);
|
||||
let maxDist = max(baseSize * uniforms.lightLength, 1.0);
|
||||
let spreadScale = uniforms.lightSpread * baseSize * 0.65;
|
||||
let coneHalfWidth = uniforms.lightSpread * baseSize * 0.55;
|
||||
|
||||
for (var i: i32 = 0; i < particleCount; i = i + 1) {
|
||||
let fi = f32(i);
|
||||
let seed = vec2<f32>(fi * 127.1, fi * 311.7);
|
||||
let rnd = hash2(seed);
|
||||
|
||||
let lifeDuration = 2.0 + hash(seed + vec2<f32>(19.0, 73.0)) * 3.0;
|
||||
let lifeOffset = hash(seed + vec2<f32>(91.0, 37.0)) * lifeDuration;
|
||||
let lifeProgress = fract((time + lifeOffset) / lifeDuration);
|
||||
|
||||
let fadeIn = smoothstep(0.0, 0.2, lifeProgress);
|
||||
let fadeOut = 1.0 - smoothstep(0.8, 1.0, lifeProgress);
|
||||
let lifeFade = fadeIn * fadeOut;
|
||||
if (lifeFade < 0.01) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let alongLight = rnd.x * maxDist * 0.8;
|
||||
let perpOffset = (rnd.y - 0.5) * spreadScale;
|
||||
|
||||
let floatPhase = rnd.y * 6.28318 + fi * 0.37;
|
||||
let floatSpeed = 0.35 + rnd.x * 0.9;
|
||||
let drift = vec2<f32>(
|
||||
sin(time * floatSpeed + floatPhase),
|
||||
cos(time * floatSpeed * 0.85 + floatPhase * 1.3)
|
||||
) * uniforms.particleDrift * baseSize * 0.08;
|
||||
|
||||
let wobble = vec2<f32>(
|
||||
sin(time * 1.4 + floatPhase * 2.1),
|
||||
cos(time * 1.1 + floatPhase * 1.6)
|
||||
) * uniforms.particleDrift * baseSize * 0.03;
|
||||
|
||||
let flowOffset = (rnd.x - 0.5) * baseSize * 0.12 + fract(time * 0.06 + rnd.y) * baseSize * 0.1;
|
||||
|
||||
let basePos = lightSource + lightDir * (alongLight + flowOffset) + perpDir * perpOffset + drift + wobble;
|
||||
|
||||
let toParticle = basePos - lightSource;
|
||||
let projLen = dot(toParticle, lightDir);
|
||||
if (projLen < 0.0 || projLen > maxDist) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let sideDist = abs(dot(toParticle, perpDir));
|
||||
if (sideDist > coneHalfWidth) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let size = mix(uniforms.particleSizeMin, uniforms.particleSizeMax, rnd.x);
|
||||
let twinkle = 0.7 + 0.3 * sin(time * (1.5 + rnd.y * 2.0) + floatPhase);
|
||||
let distFade = 1.0 - smoothstep(maxDist * 0.2, maxDist * 0.95, projLen);
|
||||
if (distFade < 0.01) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let p = particle(coord, basePos, size);
|
||||
if (p > 0.0) {
|
||||
particleSum = particleSum + p * lifeFade * twinkle * distFade * uniforms.particleOpacity;
|
||||
if (particleSum >= 1.0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return min(particleSum, 1.0);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragmentMain(@builtin(position) fragCoord: vec4<f32>, @location(0) vUv: vec2<f32>) -> @location(0) vec4<f32> {
|
||||
let coord = vec2<f32>(fragCoord.x, fragCoord.y);
|
||||
|
||||
let normalizedX = (coord.x / uniforms.iResolution.x) - 0.5;
|
||||
let widthOffset = -normalizedX * uniforms.sourceWidth * uniforms.iResolution.x;
|
||||
|
||||
let perpDir = vec2<f32>(-uniforms.lightDir.y, uniforms.lightDir.x);
|
||||
let adjustedLightPos = uniforms.lightPos + perpDir * widthOffset;
|
||||
|
||||
let lightValue = lightStrengthCombined(adjustedLightPos, uniforms.lightDir, coord);
|
||||
|
||||
if (lightValue < 0.001) {
|
||||
let particles = renderParticles(coord, adjustedLightPos, uniforms.lightDir);
|
||||
if (particles < 0.001) {
|
||||
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
||||
}
|
||||
let particleBrightness = particles * 1.8;
|
||||
return vec4<f32>(uniforms.color * particleBrightness, particles * 0.9);
|
||||
}
|
||||
|
||||
var fragColor = vec4<f32>(lightValue, lightValue, lightValue, lightValue);
|
||||
|
||||
if (uniforms.noiseAmount > 0.01) {
|
||||
let n = fastNoise(coord * 0.5 + uniforms.iTime * 0.5);
|
||||
let grain = mix(1.0, n, uniforms.noiseAmount * 0.5);
|
||||
fragColor = vec4<f32>(fragColor.rgb * grain, fragColor.a);
|
||||
}
|
||||
|
||||
let brightness = 1.0 - (coord.y / uniforms.iResolution.y);
|
||||
fragColor = vec4<f32>(
|
||||
fragColor.x * (0.15 + brightness * 0.85),
|
||||
fragColor.y * (0.35 + brightness * 0.65),
|
||||
fragColor.z * (0.55 + brightness * 0.45),
|
||||
fragColor.a
|
||||
);
|
||||
|
||||
if (abs(uniforms.saturation - 1.0) > 0.01) {
|
||||
let gray = dot(fragColor.rgb, vec3<f32>(0.299, 0.587, 0.114));
|
||||
fragColor = vec4<f32>(mix(vec3<f32>(gray), fragColor.rgb, uniforms.saturation), fragColor.a);
|
||||
}
|
||||
|
||||
fragColor = vec4<f32>(fragColor.rgb * uniforms.color, fragColor.a);
|
||||
|
||||
let particles = renderParticles(coord, adjustedLightPos, uniforms.lightDir);
|
||||
if (particles > 0.001) {
|
||||
let particleBrightness = particles * 1.8;
|
||||
fragColor = vec4<f32>(fragColor.rgb + uniforms.color * particleBrightness, max(fragColor.a, particles * 0.9));
|
||||
}
|
||||
|
||||
return fragColor;
|
||||
}
|
||||
`
|
||||
|
||||
const UNIFORM_BUFFER_SIZE = 144
|
||||
|
||||
function updateUniformBuffer(buffer: Float32Array, data: UniformData): void {
|
||||
buffer[0] = data.iTime
|
||||
buffer[2] = data.iResolution[0]
|
||||
buffer[3] = data.iResolution[1]
|
||||
buffer[4] = data.lightPos[0]
|
||||
buffer[5] = data.lightPos[1]
|
||||
buffer[6] = data.lightDir[0]
|
||||
buffer[7] = data.lightDir[1]
|
||||
buffer[8] = data.color[0]
|
||||
buffer[9] = data.color[1]
|
||||
buffer[10] = data.color[2]
|
||||
buffer[11] = data.speed
|
||||
buffer[12] = data.lightSpread
|
||||
buffer[13] = data.lightLength
|
||||
buffer[14] = data.sourceWidth
|
||||
buffer[15] = data.pulsating
|
||||
buffer[16] = data.pulsatingMin
|
||||
buffer[17] = data.pulsatingMax
|
||||
buffer[18] = data.fadeDistance
|
||||
buffer[19] = data.saturation
|
||||
buffer[20] = data.noiseAmount
|
||||
buffer[21] = data.distortion
|
||||
buffer[22] = data.particlesEnabled
|
||||
buffer[23] = data.particleAmount
|
||||
buffer[24] = data.particleSizeMin
|
||||
buffer[25] = data.particleSizeMax
|
||||
buffer[26] = data.particleSpeed
|
||||
buffer[27] = data.particleOpacity
|
||||
buffer[28] = data.particleDrift
|
||||
}
|
||||
|
||||
export default function Spotlight(props: SpotlightProps) {
|
||||
let containerRef: HTMLDivElement | undefined
|
||||
let canvasRef: HTMLCanvasElement | null = null
|
||||
let deviceRef: GPUDevice | null = null
|
||||
let contextRef: GPUCanvasContext | null = null
|
||||
let pipelineRef: GPURenderPipeline | null = null
|
||||
let uniformBufferRef: GPUBuffer | null = null
|
||||
let bindGroupRef: GPUBindGroup | null = null
|
||||
let animationIdRef: number | null = null
|
||||
let cleanupFunctionRef: (() => void) | null = null
|
||||
let uniformDataRef: UniformData | null = null
|
||||
let uniformArrayRef: Float32Array | null = null
|
||||
let configRef: SpotlightConfig = props.config()
|
||||
let frameCount = 0
|
||||
|
||||
const [isVisible, setIsVisible] = createSignal(false)
|
||||
|
||||
createEffect(() => {
|
||||
configRef = props.config()
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
if (!containerRef) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const entry = entries[0]
|
||||
setIsVisible(entry.isIntersecting)
|
||||
},
|
||||
{ threshold: 0.1 },
|
||||
)
|
||||
|
||||
observer.observe(containerRef)
|
||||
|
||||
onCleanup(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const visible = isVisible()
|
||||
const config = props.config()
|
||||
if (!visible || !containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
if (cleanupFunctionRef) {
|
||||
cleanupFunctionRef()
|
||||
cleanupFunctionRef = null
|
||||
}
|
||||
|
||||
const initializeWebGPU = async () => {
|
||||
if (!containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
if (!containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!navigator.gpu) {
|
||||
console.warn("WebGPU is not supported in this browser")
|
||||
return
|
||||
}
|
||||
|
||||
const adapter = await navigator.gpu.requestAdapter({
|
||||
powerPreference: "high-performance",
|
||||
})
|
||||
if (!adapter) {
|
||||
console.warn("Failed to get WebGPU adapter")
|
||||
return
|
||||
}
|
||||
|
||||
const device = await adapter.requestDevice()
|
||||
deviceRef = device
|
||||
|
||||
const canvas = document.createElement("canvas")
|
||||
canvas.style.width = "100%"
|
||||
canvas.style.height = "100%"
|
||||
canvasRef = canvas
|
||||
|
||||
while (containerRef.firstChild) {
|
||||
containerRef.removeChild(containerRef.firstChild)
|
||||
}
|
||||
containerRef.appendChild(canvas)
|
||||
|
||||
const context = canvas.getContext("webgpu")
|
||||
if (!context) {
|
||||
console.warn("Failed to get WebGPU context")
|
||||
return
|
||||
}
|
||||
contextRef = context
|
||||
|
||||
const presentationFormat = navigator.gpu.getPreferredCanvasFormat()
|
||||
context.configure({
|
||||
device,
|
||||
format: presentationFormat,
|
||||
alphaMode: "premultiplied",
|
||||
})
|
||||
|
||||
const shaderModule = device.createShaderModule({
|
||||
code: WGSL_SHADER,
|
||||
})
|
||||
|
||||
const uniformBuffer = device.createBuffer({
|
||||
size: UNIFORM_BUFFER_SIZE,
|
||||
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
||||
})
|
||||
uniformBufferRef = uniformBuffer
|
||||
|
||||
const bindGroupLayout = device.createBindGroupLayout({
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
||||
buffer: { type: "uniform" },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const bindGroup = device.createBindGroup({
|
||||
layout: bindGroupLayout,
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
resource: { buffer: uniformBuffer },
|
||||
},
|
||||
],
|
||||
})
|
||||
bindGroupRef = bindGroup
|
||||
|
||||
const pipelineLayout = device.createPipelineLayout({
|
||||
bindGroupLayouts: [bindGroupLayout],
|
||||
})
|
||||
|
||||
const pipeline = device.createRenderPipeline({
|
||||
layout: pipelineLayout,
|
||||
vertex: {
|
||||
module: shaderModule,
|
||||
entryPoint: "vertexMain",
|
||||
},
|
||||
fragment: {
|
||||
module: shaderModule,
|
||||
entryPoint: "fragmentMain",
|
||||
targets: [
|
||||
{
|
||||
format: presentationFormat,
|
||||
blend: {
|
||||
color: {
|
||||
srcFactor: "src-alpha",
|
||||
dstFactor: "one-minus-src-alpha",
|
||||
operation: "add",
|
||||
},
|
||||
alpha: {
|
||||
srcFactor: "one",
|
||||
dstFactor: "one-minus-src-alpha",
|
||||
operation: "add",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
primitive: {
|
||||
topology: "triangle-list",
|
||||
},
|
||||
})
|
||||
pipelineRef = pipeline
|
||||
|
||||
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
|
||||
const dpr = Math.min(window.devicePixelRatio, 2)
|
||||
const w = wCSS * dpr
|
||||
const h = hCSS * dpr
|
||||
const { anchor, dir } = getAnchorAndDir(config.placement, w, h)
|
||||
|
||||
uniformDataRef = {
|
||||
iTime: 0,
|
||||
iResolution: [w, h],
|
||||
lightPos: anchor,
|
||||
lightDir: dir,
|
||||
color: hexToRgb(config.color),
|
||||
speed: config.speed,
|
||||
lightSpread: config.spread,
|
||||
lightLength: config.length,
|
||||
sourceWidth: config.width,
|
||||
pulsating: config.pulsating !== false ? 1.0 : 0.0,
|
||||
pulsatingMin: config.pulsating !== false ? config.pulsating[0] : 1.0,
|
||||
pulsatingMax: config.pulsating !== false ? config.pulsating[1] : 1.0,
|
||||
fadeDistance: config.distance,
|
||||
saturation: config.saturation,
|
||||
noiseAmount: config.noiseAmount,
|
||||
distortion: config.distortion,
|
||||
particlesEnabled: config.particles.enabled ? 1.0 : 0.0,
|
||||
particleAmount: config.particles.amount,
|
||||
particleSizeMin: config.particles.size[0],
|
||||
particleSizeMax: config.particles.size[1],
|
||||
particleSpeed: config.particles.speed,
|
||||
particleOpacity: config.particles.opacity,
|
||||
particleDrift: config.particles.drift,
|
||||
}
|
||||
|
||||
const updatePlacement = () => {
|
||||
if (!containerRef || !canvasRef || !uniformDataRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const dpr = Math.min(window.devicePixelRatio, 2)
|
||||
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
|
||||
const w = Math.floor(wCSS * dpr)
|
||||
const h = Math.floor(hCSS * dpr)
|
||||
|
||||
canvasRef.width = w
|
||||
canvasRef.height = h
|
||||
|
||||
uniformDataRef.iResolution = [w, h]
|
||||
|
||||
const { anchor, dir } = getAnchorAndDir(configRef.placement, w, h)
|
||||
uniformDataRef.lightPos = anchor
|
||||
uniformDataRef.lightDir = dir
|
||||
}
|
||||
|
||||
const loop = (t: number) => {
|
||||
if (!deviceRef || !contextRef || !pipelineRef || !uniformBufferRef || !bindGroupRef || !uniformDataRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeSeconds = t * 0.001
|
||||
uniformDataRef.iTime = timeSeconds
|
||||
frameCount++
|
||||
|
||||
if (props.onAnimationFrame && frameCount % 2 === 0) {
|
||||
const pulsatingMin = configRef.pulsating !== false ? configRef.pulsating[0] : 1.0
|
||||
const pulsatingMax = configRef.pulsating !== false ? configRef.pulsating[1] : 1.0
|
||||
const pulseCenter = (pulsatingMin + pulsatingMax) * 0.5
|
||||
const pulseAmplitude = (pulsatingMax - pulsatingMin) * 0.5
|
||||
const pulseValue =
|
||||
configRef.pulsating !== false
|
||||
? pulseCenter + pulseAmplitude * Math.sin(timeSeconds * configRef.speed * 3.0)
|
||||
: 1.0
|
||||
|
||||
const baseIntensity1 = 0.45 + 0.15 * Math.sin(timeSeconds * configRef.speed * 1.5)
|
||||
const baseIntensity2 = 0.3 + 0.2 * Math.cos(timeSeconds * configRef.speed * 1.1)
|
||||
const intensity = Math.max((baseIntensity1 + baseIntensity2) * pulseValue, 0.55)
|
||||
|
||||
props.onAnimationFrame({
|
||||
time: timeSeconds,
|
||||
intensity,
|
||||
pulseValue: Math.max(pulseValue, 0.9),
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
if (!uniformArrayRef) {
|
||||
uniformArrayRef = new Float32Array(36)
|
||||
}
|
||||
updateUniformBuffer(uniformArrayRef, uniformDataRef)
|
||||
deviceRef.queue.writeBuffer(uniformBufferRef, 0, uniformArrayRef.buffer)
|
||||
|
||||
const commandEncoder = deviceRef.createCommandEncoder()
|
||||
|
||||
const textureView = contextRef.getCurrentTexture().createView()
|
||||
|
||||
const renderPass = commandEncoder.beginRenderPass({
|
||||
colorAttachments: [
|
||||
{
|
||||
view: textureView,
|
||||
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
||||
loadOp: "clear",
|
||||
storeOp: "store",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
renderPass.setPipeline(pipelineRef)
|
||||
renderPass.setBindGroup(0, bindGroupRef)
|
||||
renderPass.draw(3)
|
||||
renderPass.end()
|
||||
|
||||
deviceRef.queue.submit([commandEncoder.finish()])
|
||||
|
||||
animationIdRef = requestAnimationFrame(loop)
|
||||
} catch (error) {
|
||||
console.warn("WebGPU rendering error:", error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", updatePlacement)
|
||||
updatePlacement()
|
||||
animationIdRef = requestAnimationFrame(loop)
|
||||
|
||||
cleanupFunctionRef = () => {
|
||||
if (animationIdRef) {
|
||||
cancelAnimationFrame(animationIdRef)
|
||||
animationIdRef = null
|
||||
}
|
||||
|
||||
window.removeEventListener("resize", updatePlacement)
|
||||
|
||||
if (uniformBufferRef) {
|
||||
uniformBufferRef.destroy()
|
||||
uniformBufferRef = null
|
||||
}
|
||||
|
||||
if (deviceRef) {
|
||||
deviceRef.destroy()
|
||||
deviceRef = null
|
||||
}
|
||||
|
||||
if (canvasRef && canvasRef.parentNode) {
|
||||
canvasRef.parentNode.removeChild(canvasRef)
|
||||
}
|
||||
|
||||
canvasRef = null
|
||||
contextRef = null
|
||||
pipelineRef = null
|
||||
bindGroupRef = null
|
||||
uniformDataRef = null
|
||||
}
|
||||
}
|
||||
|
||||
initializeWebGPU()
|
||||
|
||||
onCleanup(() => {
|
||||
if (cleanupFunctionRef) {
|
||||
cleanupFunctionRef()
|
||||
cleanupFunctionRef = null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!uniformDataRef || !containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const config = props.config()
|
||||
|
||||
uniformDataRef.color = hexToRgb(config.color)
|
||||
uniformDataRef.speed = config.speed
|
||||
uniformDataRef.lightSpread = config.spread
|
||||
uniformDataRef.lightLength = config.length
|
||||
uniformDataRef.sourceWidth = config.width
|
||||
uniformDataRef.pulsating = config.pulsating !== false ? 1.0 : 0.0
|
||||
uniformDataRef.pulsatingMin = config.pulsating !== false ? config.pulsating[0] : 1.0
|
||||
uniformDataRef.pulsatingMax = config.pulsating !== false ? config.pulsating[1] : 1.0
|
||||
uniformDataRef.fadeDistance = config.distance
|
||||
uniformDataRef.saturation = config.saturation
|
||||
uniformDataRef.noiseAmount = config.noiseAmount
|
||||
uniformDataRef.distortion = config.distortion
|
||||
uniformDataRef.particlesEnabled = config.particles.enabled ? 1.0 : 0.0
|
||||
uniformDataRef.particleAmount = config.particles.amount
|
||||
uniformDataRef.particleSizeMin = config.particles.size[0]
|
||||
uniformDataRef.particleSizeMax = config.particles.size[1]
|
||||
uniformDataRef.particleSpeed = config.particles.speed
|
||||
uniformDataRef.particleOpacity = config.particles.opacity
|
||||
uniformDataRef.particleDrift = config.particles.drift
|
||||
|
||||
const dpr = Math.min(window.devicePixelRatio, 2)
|
||||
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
|
||||
const { anchor, dir } = getAnchorAndDir(config.placement, wCSS * dpr, hCSS * dpr)
|
||||
uniformDataRef.lightPos = anchor
|
||||
uniformDataRef.lightDir = dir
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
class={`spotlight-container ${props.class ?? ""}`.trim()}
|
||||
style={{ opacity: props.config().opacity }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import { Title, Meta, Link } from "@solidjs/meta"
|
|||
import { createMemo, createSignal } from "solid-js"
|
||||
import { github } from "~/lib/github"
|
||||
import { config } from "~/config"
|
||||
import LightRays, { defaultConfig, type LightRaysConfig, type LightRaysAnimationState } from "~/component/light-rays"
|
||||
import Spotlight, { defaultConfig, type SpotlightAnimationState } from "~/component/spotlight"
|
||||
import "./black.css"
|
||||
|
||||
export default function BlackLayout(props: RouteSectionProps) {
|
||||
|
|
@ -17,15 +17,14 @@ export default function BlackLayout(props: RouteSectionProps) {
|
|||
: config.github.starsFormatted.compact,
|
||||
)
|
||||
|
||||
const [lightRaysConfig, setLightRaysConfig] = createSignal<LightRaysConfig>(defaultConfig)
|
||||
const [rayAnimationState, setRayAnimationState] = createSignal<LightRaysAnimationState>({
|
||||
const [spotlightAnimationState, setSpotlightAnimationState] = createSignal<SpotlightAnimationState>({
|
||||
time: 0,
|
||||
intensity: 0.5,
|
||||
pulseValue: 1,
|
||||
})
|
||||
|
||||
const svgLightingValues = createMemo(() => {
|
||||
const state = rayAnimationState()
|
||||
const state = spotlightAnimationState()
|
||||
const t = state.time
|
||||
|
||||
const wave1 = Math.sin(t * 1.5) * 0.5 + 0.5
|
||||
|
|
@ -33,11 +32,11 @@ export default function BlackLayout(props: RouteSectionProps) {
|
|||
const wave3 = Math.sin(t * 0.8 + 2.5) * 0.5 + 0.5
|
||||
|
||||
const shimmerPos = Math.sin(t * 0.7) * 0.5 + 0.5
|
||||
const glowIntensity = state.intensity * state.pulseValue * 0.35
|
||||
const fillOpacity = 0.1 + wave1 * 0.08 * state.pulseValue
|
||||
const strokeBrightness = 55 + wave2 * 25 * state.pulseValue
|
||||
const glowIntensity = Math.max(state.intensity * state.pulseValue * 0.35, 0.15)
|
||||
const fillOpacity = Math.max(0.1 + wave1 * 0.08 * state.pulseValue, 0.12)
|
||||
const strokeBrightness = Math.max(55 + wave2 * 25 * state.pulseValue, 60)
|
||||
|
||||
const shimmerIntensity = wave3 * 0.15 * state.pulseValue
|
||||
const shimmerIntensity = Math.max(wave3 * 0.15 * state.pulseValue, 0.08)
|
||||
|
||||
return {
|
||||
glowIntensity,
|
||||
|
|
@ -56,10 +55,12 @@ export default function BlackLayout(props: RouteSectionProps) {
|
|||
} as Record<string, string>
|
||||
})
|
||||
|
||||
const handleAnimationFrame = (state: LightRaysAnimationState) => {
|
||||
setRayAnimationState(state)
|
||||
const handleAnimationFrame = (state: SpotlightAnimationState) => {
|
||||
setSpotlightAnimationState(state)
|
||||
}
|
||||
|
||||
const spotlightConfig = () => defaultConfig
|
||||
|
||||
return (
|
||||
<div data-page="black">
|
||||
<Title>OpenCode Black | Access all the world's best coding models</Title>
|
||||
|
|
@ -84,7 +85,7 @@ export default function BlackLayout(props: RouteSectionProps) {
|
|||
/>
|
||||
<Meta name="twitter:image" content="/social-share-black.png" />
|
||||
|
||||
<LightRays config={lightRaysConfig} class="header-light-rays" onAnimationFrame={handleAnimationFrame} />
|
||||
<Spotlight config={spotlightConfig} class="header-spotlight" onAnimationFrame={handleAnimationFrame} />
|
||||
|
||||
<header data-component="header">
|
||||
<A href="/" data-component="header-logo">
|
||||
|
|
|
|||
|
|
@ -64,23 +64,21 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) =>
|
|||
newBuffer.set(value, buffer.length)
|
||||
buffer = newBuffer
|
||||
|
||||
if (buffer.length < 4) return
|
||||
// The first 4 bytes are the total length (big-endian).
|
||||
const totalLength = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength).getUint32(0, false)
|
||||
const messages = []
|
||||
|
||||
// If we don't have the full message yet, wait for more chunks.
|
||||
if (buffer.length < totalLength) return
|
||||
while (buffer.length >= 4) {
|
||||
// first 4 bytes are the total length (big-endian)
|
||||
const totalLength = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength).getUint32(0, false)
|
||||
|
||||
try {
|
||||
// Decode exactly the sub-slice for this event.
|
||||
const subView = buffer.subarray(0, totalLength)
|
||||
const decoded = codec.decode(subView)
|
||||
// wait for more chunks
|
||||
if (buffer.length < totalLength) break
|
||||
|
||||
// Slice the used bytes out of the buffer, removing this message.
|
||||
buffer = buffer.slice(totalLength)
|
||||
try {
|
||||
const subView = buffer.subarray(0, totalLength)
|
||||
const decoded = codec.decode(subView)
|
||||
buffer = buffer.slice(totalLength)
|
||||
|
||||
// Process message
|
||||
/* Example of Bedrock data
|
||||
/* Example of Bedrock data
|
||||
```
|
||||
{
|
||||
bytes: 'eyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtb3B1cy00LTUtMjAyNTExMDEiLCJpZCI6Im1zZ19iZHJrXzAxMjVGdHRGb2lkNGlwWmZ4SzZMbktxeCIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjo0LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjEsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjoxMTk2MywiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MSwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjF9fX0=',
|
||||
|
|
@ -112,22 +110,28 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) =>
|
|||
```
|
||||
*/
|
||||
|
||||
/* Example of Anthropic data
|
||||
/* Example of Anthropic data
|
||||
```
|
||||
event: message_delta
|
||||
data: {"type":"message_start","message":{"model":"claude-opus-4-5-20251101","id":"msg_01ETvwVWSKULxzPdkQ1xAnk2","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":11543,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":11543,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}}}
|
||||
```
|
||||
*/
|
||||
if (decoded.headers[":message-type"]?.value !== "event") return
|
||||
const data = decoder.decode(decoded.body, { stream: true })
|
||||
if (decoded.headers[":message-type"]?.value === "event") {
|
||||
const data = decoder.decode(decoded.body, { stream: true })
|
||||
|
||||
const parsedDataResult = JSON.parse(data)
|
||||
delete parsedDataResult.p
|
||||
const utf8 = atob(parsedDataResult.bytes)
|
||||
return encoder.encode(["event: message_start", "\n", "data: " + utf8, "\n\n"].join(""))
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
const parsedDataResult = JSON.parse(data)
|
||||
delete parsedDataResult.p
|
||||
const bytes = atob(parsedDataResult.bytes)
|
||||
const eventName = JSON.parse(bytes).type
|
||||
messages.push([`event: ${eventName}`, "\n", `data: ${bytes}`, "\n\n"].join(""))
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("@@@EE@@@")
|
||||
console.log(e)
|
||||
break
|
||||
}
|
||||
}
|
||||
return encoder.encode(messages.join(""))
|
||||
}
|
||||
},
|
||||
streamSeparator: "\n\n",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import { Installation } from "@/installation"
|
|||
import { ConfigMarkdown } from "./markdown"
|
||||
import { existsSync } from "fs"
|
||||
import { Bus } from "@/bus"
|
||||
import { Session } from "@/session"
|
||||
|
||||
export namespace Config {
|
||||
const log = Log.create({ service: "config" })
|
||||
|
|
@ -233,10 +232,11 @@ export namespace Config {
|
|||
dot: true,
|
||||
cwd: dir,
|
||||
})) {
|
||||
const md = await ConfigMarkdown.parse(item).catch((err) => {
|
||||
const md = await ConfigMarkdown.parse(item).catch(async (err) => {
|
||||
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
|
||||
? err.data.message
|
||||
: `Failed to parse command ${item}`
|
||||
const { Session } = await import("@/session")
|
||||
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
|
||||
log.error("failed to load command", { command: item, err })
|
||||
return undefined
|
||||
|
|
@ -272,10 +272,11 @@ export namespace Config {
|
|||
dot: true,
|
||||
cwd: dir,
|
||||
})) {
|
||||
const md = await ConfigMarkdown.parse(item).catch((err) => {
|
||||
const md = await ConfigMarkdown.parse(item).catch(async (err) => {
|
||||
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
|
||||
? err.data.message
|
||||
: `Failed to parse agent ${item}`
|
||||
const { Session } = await import("@/session")
|
||||
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
|
||||
log.error("failed to load agent", { agent: item, err })
|
||||
return undefined
|
||||
|
|
@ -310,10 +311,11 @@ export namespace Config {
|
|||
dot: true,
|
||||
cwd: dir,
|
||||
})) {
|
||||
const md = await ConfigMarkdown.parse(item).catch((err) => {
|
||||
const md = await ConfigMarkdown.parse(item).catch(async (err) => {
|
||||
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
|
||||
? err.data.message
|
||||
: `Failed to parse mode ${item}`
|
||||
const { Session } = await import("@/session")
|
||||
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
|
||||
log.error("failed to load mode", { mode: item, err })
|
||||
return undefined
|
||||
|
|
@ -942,7 +944,7 @@ export namespace Config {
|
|||
})
|
||||
.catchall(Agent)
|
||||
.optional()
|
||||
.describe("Agent configuration, see https://opencode.ai/docs/agent"),
|
||||
.describe("Agent configuration, see https://opencode.ai/docs/agents"),
|
||||
provider: z
|
||||
.record(z.string(), Provider)
|
||||
.optional()
|
||||
|
|
|
|||
|
|
@ -24,15 +24,23 @@ export namespace ProviderTransform {
|
|||
// Strip openai itemId metadata following what codex does
|
||||
if (model.api.npm === "@ai-sdk/openai" || options.store === false) {
|
||||
msgs = msgs.map((msg) => {
|
||||
if (msg.providerOptions?.openai) {
|
||||
delete msg.providerOptions.openai["itemId"]
|
||||
if (msg.providerOptions) {
|
||||
for (const options of Object.values(msg.providerOptions)) {
|
||||
if (options && typeof options === "object") {
|
||||
delete options["itemId"]
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(msg.content)) {
|
||||
return msg
|
||||
}
|
||||
const content = msg.content.map((part) => {
|
||||
if (part.providerOptions?.openai) {
|
||||
delete part.providerOptions.openai["itemId"]
|
||||
if (part.providerOptions) {
|
||||
for (const options of Object.values(part.providerOptions)) {
|
||||
if (options && typeof options === "object") {
|
||||
delete options["itemId"]
|
||||
}
|
||||
}
|
||||
}
|
||||
return part
|
||||
})
|
||||
|
|
|
|||
|
|
@ -146,6 +146,10 @@ export namespace Pty {
|
|||
ptyProcess.onExit(({ exitCode }) => {
|
||||
log.info("session exited", { id, exitCode })
|
||||
session.info.status = "exited"
|
||||
for (const ws of session.subscribers) {
|
||||
ws.close()
|
||||
}
|
||||
session.subscribers.clear()
|
||||
Bus.publish(Event.Exited, { id, exitCode })
|
||||
state().delete(id)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,14 +1,7 @@
|
|||
import { BusEvent } from "@/bus/bus-event"
|
||||
import z from "zod"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import {
|
||||
APICallError,
|
||||
convertToModelMessages,
|
||||
LoadAPIKeyError,
|
||||
type ModelMessage,
|
||||
type UIMessage,
|
||||
type ToolSet,
|
||||
} from "ai"
|
||||
import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
|
||||
import { Identifier } from "../id/id"
|
||||
import { LSP } from "../lsp"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
|
|
@ -439,7 +432,7 @@ export namespace MessageV2 {
|
|||
})
|
||||
export type WithParts = z.infer<typeof WithParts>
|
||||
|
||||
export function toModelMessage(input: WithParts[], options?: { tools?: ToolSet }): ModelMessage[] {
|
||||
export function toModelMessage(input: WithParts[]): ModelMessage[] {
|
||||
const result: UIMessage[] = []
|
||||
|
||||
for (const msg of input) {
|
||||
|
|
@ -510,6 +503,24 @@ export namespace MessageV2 {
|
|||
})
|
||||
if (part.type === "tool") {
|
||||
if (part.state.status === "completed") {
|
||||
if (part.state.attachments?.length) {
|
||||
result.push({
|
||||
id: Identifier.ascending("message"),
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Tool ${part.tool} returned an attachment:`,
|
||||
},
|
||||
...part.state.attachments.map((attachment) => ({
|
||||
type: "file" as const,
|
||||
url: attachment.url,
|
||||
mediaType: attachment.mime,
|
||||
filename: attachment.filename,
|
||||
})),
|
||||
],
|
||||
})
|
||||
}
|
||||
assistantMessage.parts.push({
|
||||
type: ("tool-" + part.tool) as `tool-${string}`,
|
||||
state: "output-available",
|
||||
|
|
@ -558,12 +569,7 @@ export namespace MessageV2 {
|
|||
}
|
||||
}
|
||||
|
||||
return convertToModelMessages(
|
||||
result.filter((msg) => msg.parts.some((part) => part.type !== "step-start")),
|
||||
{
|
||||
tools: options?.tools,
|
||||
},
|
||||
)
|
||||
return convertToModelMessages(result.filter((msg) => msg.parts.some((part) => part.type !== "step-start")))
|
||||
}
|
||||
|
||||
export const stream = fn(Identifier.schema("session"), async function* (sessionID) {
|
||||
|
|
|
|||
|
|
@ -597,7 +597,7 @@ export namespace SessionPrompt {
|
|||
sessionID,
|
||||
system: [...(await SystemPrompt.environment()), ...(await SystemPrompt.custom())],
|
||||
messages: [
|
||||
...MessageV2.toModelMessage(sessionMessages, { tools }),
|
||||
...MessageV2.toModelMessage(sessionMessages),
|
||||
...(isLastStep
|
||||
? [
|
||||
{
|
||||
|
|
@ -721,15 +721,8 @@ export namespace SessionPrompt {
|
|||
if (typeof result === "string") return { type: "text", value: result }
|
||||
if (!result.attachments?.length) return { type: "text", value: result.output }
|
||||
return {
|
||||
type: "content",
|
||||
value: [
|
||||
{ type: "text", text: result.output },
|
||||
...result.attachments.map((a) => ({
|
||||
type: "media" as const,
|
||||
data: a.url.slice(a.url.indexOf(",") + 1),
|
||||
mediaType: a.mime,
|
||||
})),
|
||||
],
|
||||
type: "text",
|
||||
value: result.output,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -821,15 +814,8 @@ export namespace SessionPrompt {
|
|||
if (typeof result === "string") return { type: "text", value: result }
|
||||
if (!result.attachments?.length) return { type: "text", value: result.output }
|
||||
return {
|
||||
type: "content",
|
||||
value: [
|
||||
{ type: "text", text: result.output },
|
||||
...result.attachments.map((a) => ({
|
||||
type: "media" as const,
|
||||
data: a.url.slice(a.url.indexOf(",") + 1),
|
||||
mediaType: a.mime,
|
||||
})),
|
||||
],
|
||||
type: "text",
|
||||
value: result.output,
|
||||
}
|
||||
}
|
||||
tools[key] = item
|
||||
|
|
|
|||
|
|
@ -805,6 +805,82 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
|
|||
expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
|
||||
})
|
||||
|
||||
test("strips metadata using providerID key when store is false", () => {
|
||||
const opencodeModel = {
|
||||
...openaiModel,
|
||||
providerID: "opencode",
|
||||
api: {
|
||||
id: "opencode-test",
|
||||
url: "https://api.opencode.ai",
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
},
|
||||
}
|
||||
const msgs = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Hello",
|
||||
providerOptions: {
|
||||
opencode: {
|
||||
itemId: "msg_123",
|
||||
otherOption: "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as any[]
|
||||
|
||||
const result = ProviderTransform.message(msgs, opencodeModel, { store: false }) as any[]
|
||||
|
||||
expect(result[0].content[0].providerOptions?.opencode?.itemId).toBeUndefined()
|
||||
expect(result[0].content[0].providerOptions?.opencode?.otherOption).toBe("value")
|
||||
})
|
||||
|
||||
test("strips itemId across all providerOptions keys", () => {
|
||||
const opencodeModel = {
|
||||
...openaiModel,
|
||||
providerID: "opencode",
|
||||
api: {
|
||||
id: "opencode-test",
|
||||
url: "https://api.opencode.ai",
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
},
|
||||
}
|
||||
const msgs = [
|
||||
{
|
||||
role: "assistant",
|
||||
providerOptions: {
|
||||
openai: { itemId: "msg_root" },
|
||||
opencode: { itemId: "msg_opencode" },
|
||||
extra: { itemId: "msg_extra" },
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Hello",
|
||||
providerOptions: {
|
||||
openai: { itemId: "msg_openai_part" },
|
||||
opencode: { itemId: "msg_opencode_part" },
|
||||
extra: { itemId: "msg_extra_part" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as any[]
|
||||
|
||||
const result = ProviderTransform.message(msgs, opencodeModel, { store: false }) as any[]
|
||||
|
||||
expect(result[0].providerOptions?.openai?.itemId).toBeUndefined()
|
||||
expect(result[0].providerOptions?.opencode?.itemId).toBeUndefined()
|
||||
expect(result[0].providerOptions?.extra?.itemId).toBeUndefined()
|
||||
expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
|
||||
expect(result[0].content[0].providerOptions?.opencode?.itemId).toBeUndefined()
|
||||
expect(result[0].content[0].providerOptions?.extra?.itemId).toBeUndefined()
|
||||
})
|
||||
|
||||
test("does not strip metadata for non-openai packages when store is not false", () => {
|
||||
const anthropicModel = {
|
||||
...openaiModel,
|
||||
|
|
|
|||
|
|
@ -264,6 +264,18 @@ describe("session.message-v2.toModelMessage", () => {
|
|||
role: "user",
|
||||
content: [{ type: "text", text: "run tool" }],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Tool bash returned an attachment:" },
|
||||
{
|
||||
type: "file",
|
||||
mediaType: "image/png",
|
||||
filename: "attachment.png",
|
||||
data: "https://example.com/attachment.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
|
|
@ -285,21 +297,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||
type: "tool-result",
|
||||
toolCallId: "call-1",
|
||||
toolName: "bash",
|
||||
output: {
|
||||
type: "json",
|
||||
value: {
|
||||
output: "ok",
|
||||
attachments: [
|
||||
{
|
||||
...basePart(assistantID, "file-1"),
|
||||
type: "file",
|
||||
mime: "image/png",
|
||||
filename: "attachment.png",
|
||||
url: "https://example.com/attachment.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
output: { type: "text", value: "ok" },
|
||||
providerOptions: { openai: { tool: "meta" } },
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
"dist"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.88.1",
|
||||
"@hey-api/openapi-ts": "0.90.4",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
|
|
|
|||
|
|
@ -162,10 +162,16 @@ export const createClient = (config: Config = {}): Client => {
|
|||
case "arrayBuffer":
|
||||
case "blob":
|
||||
case "formData":
|
||||
case "json":
|
||||
case "text":
|
||||
data = await response[parseAs]()
|
||||
break
|
||||
case "json": {
|
||||
// Some servers return 200 with no Content-Length and empty body.
|
||||
// response.json() would throw; read as text and parse if non-empty.
|
||||
const text = await response.text()
|
||||
data = text ? JSON.parse(text) : {}
|
||||
break
|
||||
}
|
||||
case "stream":
|
||||
return opts.responseStyle === "data"
|
||||
? response.body
|
||||
|
|
@ -244,6 +250,7 @@ export const createClient = (config: Config = {}): Client => {
|
|||
}
|
||||
return request
|
||||
},
|
||||
serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined,
|
||||
url,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,6 +151,8 @@ export const createSseClient = <TData = unknown>({
|
|||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buffer += value
|
||||
// Normalize line endings: CRLF -> LF, then CR -> LF
|
||||
buffer = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n")
|
||||
|
||||
const chunks = buffer.split("\n\n")
|
||||
buffer = chunks.pop() ?? ""
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type {
|
|||
AppAgentsResponses,
|
||||
AppLogErrors,
|
||||
AppLogResponses,
|
||||
Auth as Auth2,
|
||||
Auth as Auth3,
|
||||
AuthSetErrors,
|
||||
AuthSetResponses,
|
||||
CommandListResponses,
|
||||
|
|
@ -2023,7 +2023,10 @@ export class Provider extends HeyApiClient {
|
|||
})
|
||||
}
|
||||
|
||||
oauth = new Oauth({ client: this.client })
|
||||
private _oauth?: Oauth
|
||||
get oauth(): Oauth {
|
||||
return (this._oauth ??= new Oauth({ client: this.client }))
|
||||
}
|
||||
}
|
||||
|
||||
export class Find extends HeyApiClient {
|
||||
|
|
@ -2398,43 +2401,6 @@ export class Auth extends HeyApiClient {
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set auth credentials
|
||||
*
|
||||
* Set authentication credentials
|
||||
*/
|
||||
public set<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
providerID: string
|
||||
directory?: string
|
||||
auth?: Auth2
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "path", key: "providerID" },
|
||||
{ in: "query", key: "directory" },
|
||||
{ key: "auth", map: "body" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).put<AuthSetResponses, AuthSetErrors, ThrowOnError>({
|
||||
url: "/auth/{providerID}",
|
||||
...options,
|
||||
...params,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
...params.headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Mcp extends HeyApiClient {
|
||||
|
|
@ -2550,7 +2516,10 @@ export class Mcp extends HeyApiClient {
|
|||
})
|
||||
}
|
||||
|
||||
auth = new Auth({ client: this.client })
|
||||
private _auth?: Auth
|
||||
get auth(): Auth {
|
||||
return (this._auth ??= new Auth({ client: this.client }))
|
||||
}
|
||||
}
|
||||
|
||||
export class Resource extends HeyApiClient {
|
||||
|
|
@ -2575,7 +2544,10 @@ export class Resource extends HeyApiClient {
|
|||
}
|
||||
|
||||
export class Experimental extends HeyApiClient {
|
||||
resource = new Resource({ client: this.client })
|
||||
private _resource?: Resource
|
||||
get resource(): Resource {
|
||||
return (this._resource ??= new Resource({ client: this.client }))
|
||||
}
|
||||
}
|
||||
|
||||
export class Lsp extends HeyApiClient {
|
||||
|
|
@ -2952,7 +2924,49 @@ export class Tui extends HeyApiClient {
|
|||
})
|
||||
}
|
||||
|
||||
control = new Control({ client: this.client })
|
||||
private _control?: Control
|
||||
get control(): Control {
|
||||
return (this._control ??= new Control({ client: this.client }))
|
||||
}
|
||||
}
|
||||
|
||||
export class Auth2 extends HeyApiClient {
|
||||
/**
|
||||
* Set auth credentials
|
||||
*
|
||||
* Set authentication credentials
|
||||
*/
|
||||
public set<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
providerID: string
|
||||
directory?: string
|
||||
auth?: Auth3
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "path", key: "providerID" },
|
||||
{ in: "query", key: "directory" },
|
||||
{ key: "auth", map: "body" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).put<AuthSetResponses, AuthSetErrors, ThrowOnError>({
|
||||
url: "/auth/{providerID}",
|
||||
...options,
|
||||
...params,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
...params.headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Event extends HeyApiClient {
|
||||
|
|
@ -2984,53 +2998,128 @@ export class OpencodeClient extends HeyApiClient {
|
|||
OpencodeClient.__registry.set(this, args?.key)
|
||||
}
|
||||
|
||||
global = new Global({ client: this.client })
|
||||
private _global?: Global
|
||||
get global(): Global {
|
||||
return (this._global ??= new Global({ client: this.client }))
|
||||
}
|
||||
|
||||
project = new Project({ client: this.client })
|
||||
private _project?: Project
|
||||
get project(): Project {
|
||||
return (this._project ??= new Project({ client: this.client }))
|
||||
}
|
||||
|
||||
pty = new Pty({ client: this.client })
|
||||
private _pty?: Pty
|
||||
get pty(): Pty {
|
||||
return (this._pty ??= new Pty({ client: this.client }))
|
||||
}
|
||||
|
||||
config = new Config({ client: this.client })
|
||||
private _config?: Config
|
||||
get config(): Config {
|
||||
return (this._config ??= new Config({ client: this.client }))
|
||||
}
|
||||
|
||||
tool = new Tool({ client: this.client })
|
||||
private _tool?: Tool
|
||||
get tool(): Tool {
|
||||
return (this._tool ??= new Tool({ client: this.client }))
|
||||
}
|
||||
|
||||
instance = new Instance({ client: this.client })
|
||||
private _instance?: Instance
|
||||
get instance(): Instance {
|
||||
return (this._instance ??= new Instance({ client: this.client }))
|
||||
}
|
||||
|
||||
path = new Path({ client: this.client })
|
||||
private _path?: Path
|
||||
get path(): Path {
|
||||
return (this._path ??= new Path({ client: this.client }))
|
||||
}
|
||||
|
||||
worktree = new Worktree({ client: this.client })
|
||||
private _worktree?: Worktree
|
||||
get worktree(): Worktree {
|
||||
return (this._worktree ??= new Worktree({ client: this.client }))
|
||||
}
|
||||
|
||||
vcs = new Vcs({ client: this.client })
|
||||
private _vcs?: Vcs
|
||||
get vcs(): Vcs {
|
||||
return (this._vcs ??= new Vcs({ client: this.client }))
|
||||
}
|
||||
|
||||
session = new Session({ client: this.client })
|
||||
private _session?: Session
|
||||
get session(): Session {
|
||||
return (this._session ??= new Session({ client: this.client }))
|
||||
}
|
||||
|
||||
part = new Part({ client: this.client })
|
||||
private _part?: Part
|
||||
get part(): Part {
|
||||
return (this._part ??= new Part({ client: this.client }))
|
||||
}
|
||||
|
||||
permission = new Permission({ client: this.client })
|
||||
private _permission?: Permission
|
||||
get permission(): Permission {
|
||||
return (this._permission ??= new Permission({ client: this.client }))
|
||||
}
|
||||
|
||||
question = new Question({ client: this.client })
|
||||
private _question?: Question
|
||||
get question(): Question {
|
||||
return (this._question ??= new Question({ client: this.client }))
|
||||
}
|
||||
|
||||
command = new Command({ client: this.client })
|
||||
private _command?: Command
|
||||
get command(): Command {
|
||||
return (this._command ??= new Command({ client: this.client }))
|
||||
}
|
||||
|
||||
provider = new Provider({ client: this.client })
|
||||
private _provider?: Provider
|
||||
get provider(): Provider {
|
||||
return (this._provider ??= new Provider({ client: this.client }))
|
||||
}
|
||||
|
||||
find = new Find({ client: this.client })
|
||||
private _find?: Find
|
||||
get find(): Find {
|
||||
return (this._find ??= new Find({ client: this.client }))
|
||||
}
|
||||
|
||||
file = new File({ client: this.client })
|
||||
private _file?: File
|
||||
get file(): File {
|
||||
return (this._file ??= new File({ client: this.client }))
|
||||
}
|
||||
|
||||
app = new App({ client: this.client })
|
||||
private _app?: App
|
||||
get app(): App {
|
||||
return (this._app ??= new App({ client: this.client }))
|
||||
}
|
||||
|
||||
mcp = new Mcp({ client: this.client })
|
||||
private _mcp?: Mcp
|
||||
get mcp(): Mcp {
|
||||
return (this._mcp ??= new Mcp({ client: this.client }))
|
||||
}
|
||||
|
||||
experimental = new Experimental({ client: this.client })
|
||||
private _experimental?: Experimental
|
||||
get experimental(): Experimental {
|
||||
return (this._experimental ??= new Experimental({ client: this.client }))
|
||||
}
|
||||
|
||||
lsp = new Lsp({ client: this.client })
|
||||
private _lsp?: Lsp
|
||||
get lsp(): Lsp {
|
||||
return (this._lsp ??= new Lsp({ client: this.client }))
|
||||
}
|
||||
|
||||
formatter = new Formatter({ client: this.client })
|
||||
private _formatter?: Formatter
|
||||
get formatter(): Formatter {
|
||||
return (this._formatter ??= new Formatter({ client: this.client }))
|
||||
}
|
||||
|
||||
tui = new Tui({ client: this.client })
|
||||
private _tui?: Tui
|
||||
get tui(): Tui {
|
||||
return (this._tui ??= new Tui({ client: this.client }))
|
||||
}
|
||||
|
||||
auth = new Auth({ client: this.client })
|
||||
private _auth?: Auth2
|
||||
get auth(): Auth2 {
|
||||
return (this._auth ??= new Auth2({ client: this.client }))
|
||||
}
|
||||
|
||||
event = new Event({ client: this.client })
|
||||
private _event?: Event
|
||||
get event(): Event {
|
||||
return (this._event ??= new Event({ client: this.client }))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1666,7 +1666,7 @@ export type Config = {
|
|||
[key: string]: AgentConfig | undefined
|
||||
}
|
||||
/**
|
||||
* Agent configuration, see https://opencode.ai/docs/agent
|
||||
* Agent configuration, see https://opencode.ai/docs/agents
|
||||
*/
|
||||
agent?: {
|
||||
plan?: AgentConfig
|
||||
|
|
|
|||
|
|
@ -9316,7 +9316,7 @@
|
|||
}
|
||||
},
|
||||
"agent": {
|
||||
"description": "Agent configuration, see https://opencode.ai/docs/agent",
|
||||
"description": "Agent configuration, see https://opencode.ai/docs/agents",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"plan": {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export function Avatar(props: AvatarProps) {
|
|||
}}
|
||||
>
|
||||
<Show when={src} fallback={split.fallback?.[0]}>
|
||||
{(src) => <img src={src()} draggable={false} class="size-full object-cover" />}
|
||||
{(src) => <img src={src()} draggable={false} class="size-full object-cover rounded-[inherit]" />}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ const icons = {
|
|||
edit: `<path d="M17.0832 17.0807V17.5807H17.5832V17.0807H17.0832ZM2.9165 17.0807H2.4165V17.5807H2.9165V17.0807ZM2.9165 2.91406V2.41406H2.4165V2.91406H2.9165ZM9.58317 3.41406H10.0832V2.41406H9.58317V2.91406V3.41406ZM17.5832 10.4141V9.91406H16.5832V10.4141H17.0832H17.5832ZM6.24984 11.2474L5.89628 10.8938L5.74984 11.0403V11.2474H6.24984ZM6.24984 13.7474H5.74984V14.2474H6.24984V13.7474ZM8.74984 13.7474V14.2474H8.95694L9.10339 14.101L8.74984 13.7474ZM15.2082 2.28906L15.5617 1.93551L15.2082 1.58196L14.8546 1.93551L15.2082 2.28906ZM17.7082 4.78906L18.0617 5.14262L18.4153 4.78906L18.0617 4.43551L17.7082 4.78906ZM17.0832 17.0807V16.5807H2.9165V17.0807V17.5807H17.0832V17.0807ZM2.9165 17.0807H3.4165V2.91406H2.9165H2.4165V17.0807H2.9165ZM2.9165 2.91406V3.41406H9.58317V2.91406V2.41406H2.9165V2.91406ZM17.0832 10.4141H16.5832V17.0807H17.0832H17.5832V10.4141H17.0832ZM6.24984 11.2474H5.74984V13.7474H6.24984H6.74984V11.2474H6.24984ZM6.24984 13.7474V14.2474H8.74984V13.7474V13.2474H6.24984V13.7474ZM6.24984 11.2474L6.60339 11.6009L15.5617 2.64262L15.2082 2.28906L14.8546 1.93551L5.89628 10.8938L6.24984 11.2474ZM15.2082 2.28906L14.8546 2.64262L17.3546 5.14262L17.7082 4.78906L18.0617 4.43551L15.5617 1.93551L15.2082 2.28906ZM17.7082 4.78906L17.3546 4.43551L8.39628 13.3938L8.74984 13.7474L9.10339 14.101L18.0617 5.14262L17.7082 4.78906Z" fill="currentColor"/>`,
|
||||
help: `<path d="M7.91683 7.91927V6.2526H12.0835V8.7526L10.0002 10.0026V12.0859M10.0002 13.7526V13.7609M17.9168 10.0026C17.9168 14.3749 14.3724 17.9193 10.0002 17.9193C5.62791 17.9193 2.0835 14.3749 2.0835 10.0026C2.0835 5.63035 5.62791 2.08594 10.0002 2.08594C14.3724 2.08594 17.9168 5.63035 17.9168 10.0026Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"settings-gear": `<path d="M7.62516 4.46094L5.05225 3.86719L3.86475 5.05469L4.4585 7.6276L2.0835 9.21094V10.7943L4.4585 12.3776L3.86475 14.9505L5.05225 16.138L7.62516 15.5443L9.2085 17.9193H10.7918L12.3752 15.5443L14.9481 16.138L16.1356 14.9505L15.5418 12.3776L17.9168 10.7943V9.21094L15.5418 7.6276L16.1356 5.05469L14.9481 3.86719L12.3752 4.46094L10.7918 2.08594H9.2085L7.62516 4.46094Z" stroke="currentColor"/><path d="M12.5002 10.0026C12.5002 11.3833 11.3809 12.5026 10.0002 12.5026C8.61945 12.5026 7.50016 11.3833 7.50016 10.0026C7.50016 8.62189 8.61945 7.5026 10.0002 7.5026C11.3809 7.5026 12.5002 8.62189 12.5002 10.0026Z" stroke="currentColor"/>`,
|
||||
dash: `<rect x="5" y="9.5" width="10" height="1" fill="currentColor"/>`,
|
||||
}
|
||||
|
||||
export interface IconProps extends ComponentProps<"svg"> {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@
|
|||
0 16px 48px -6px light-dark(hsl(0 0% 0% / 0.05), hsl(0 0% 0% / 0.15)),
|
||||
0 6px 12px -2px light-dark(hsl(0 0% 0% / 0.025), hsl(0 0% 0% / 0.1)),
|
||||
0 1px 2.5px light-dark(hsl(0 0% 0% / 0.025), hsl(0 0% 0% / 0.1));
|
||||
--shadow-xxs-border: 0 0 0 0.5px var(--border-weak-base, rgba(0, 0, 0, 0.07));
|
||||
--shadow-xs-border:
|
||||
0 0 0 1px var(--border-base, rgba(11, 6, 0, 0.2)), 0 1px 2px -1px rgba(19, 16, 16, 0.04),
|
||||
0 1px 2px 0 rgba(19, 16, 16, 0.06), 0 1px 3px 0 rgba(19, 16, 16, 0.08);
|
||||
|
|
|
|||
Loading…
Reference in New Issue