diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index 44c8d4a583..4c75ad2e04 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -29,5 +29,6 @@ jobs: uses: sst/opencode/github@latest env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + OPENCODE_PERMISSION: '{"bash": "deny"}' with: model: opencode/claude-haiku-4-5 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index eea0e21aec..add68dc629 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -55,7 +55,7 @@ jobs: - name: Install OpenCode if: inputs.bump || inputs.version - run: curl -fsSL https://opencode.ai/install | bash + run: bun i -g opencode-ai@1.0.143 - name: Login to GitHub Container Registry uses: docker/login-action@v3 @@ -70,8 +70,8 @@ jobs: registry-url: "https://registry.npmjs.org" - name: Publish - run: | - ./script/publish.ts + id: publish + run: ./script/publish.ts env: OPENCODE_BUMP: ${{ inputs.bump }} OPENCODE_VERSION: ${{ inputs.version }} @@ -79,9 +79,12 @@ jobs: AUR_KEY: ${{ secrets.AUR_KEY }} GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }} NPM_CONFIG_PROVENANCE: false + outputs: + releaseId: ${{ steps.publish.outputs.releaseId }} + tagName: ${{ steps.publish.outputs.tagName }} publish-tauri: - if: false # inputs.bump || inputs.version + needs: publish continue-on-error: true strategy: fail-fast: false @@ -91,9 +94,9 @@ jobs: target: x86_64-apple-darwin - host: macos-latest target: aarch64-apple-darwin - - host: windows-latest + - host: blacksmith-4vcpu-windows-2025 target: x86_64-pc-windows-msvc - - host: ubuntu-24.04 + - host: blacksmith-4vcpu-ubuntu-2404 target: x86_64-unknown-linux-gnu runs-on: ${{ matrix.settings.host }} steps: @@ -126,7 +129,7 @@ jobs: - uses: ./.github/actions/setup-bun - name: install dependencies (ubuntu only) - if: startsWith(matrix.settings.host, 'ubuntu') + if: contains(matrix.settings.host, 'ubuntu') run: | sudo apt-get update sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf @@ -158,7 +161,7 @@ jobs: # Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released - run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage - if: startsWith(matrix.settings.host, 'ubuntu') + if: contains(matrix.settings.host, 'ubuntu') - name: Build and upload artifacts uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a @@ -176,7 +179,9 @@ jobs: with: projectPath: packages/tauri uploadWorkflowArtifacts: true - tauriScript: ${{ (startsWith(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }} + tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }} args: --target ${{ matrix.settings.target }} updaterJsonPreferNsis: true - # releaseId: TODO + releaseId: ${{ needs.publish.outputs.releaseId }} + tagName: ${{ needs.publish.outputs.tagName }} + releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext] diff --git a/.github/workflows/sync-zed-extension.yml b/.github/workflows/sync-zed-extension.yml index 445adbc530..a504582c3c 100644 --- a/.github/workflows/sync-zed-extension.yml +++ b/.github/workflows/sync-zed-extension.yml @@ -2,8 +2,8 @@ name: "sync-zed-extension" on: workflow_dispatch: - release: - types: [published] + # release: + # types: [published] jobs: zed: diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index 88adf3762b..d5d97f4c9d 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -1,6 +1,6 @@ { "$schema": "https://opencode.ai/config.json", - "plugin": ["opencode-openai-codex-auth"], + // "plugin": ["opencode-openai-codex-auth"], // "enterprise": { // "url": "https://enterprise.dev.opencode.ai", // }, diff --git a/README.md b/README.md index 799cf00a2a..eb0295c9c8 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

-

The AI coding agent built for the terminal.

+

The open source AI coding agent.

Discord npm @@ -30,7 +30,7 @@ scoop bucket add extras; scoop install extras/opencode # Windows choco install opencode # Windows brew install opencode # macOS and Linux paru -S opencode-bin # Arch Linux -mise use --pin -g ubi:sst/opencode # Any OS +mise use -g ubi:sst/opencode # Any OS nix run nixpkgs#opencode # or github:sst/opencode for latest dev branch ``` diff --git a/STATS.md b/STATS.md index 59d0c93a1c..67f236ebe2 100644 --- a/STATS.md +++ b/STATS.md @@ -1,167 +1,169 @@ # 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) | +| 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) | diff --git a/bun.lock b/bun.lock index 10feb9b186..5b3d864de5 100644 --- a/bun.lock +++ b/bun.lock @@ -20,7 +20,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.0.141", + "version": "1.0.150", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -48,7 +48,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.0.141", + "version": "1.0.150", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -75,7 +75,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.0.141", + "version": "1.0.150", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -99,7 +99,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.0.141", + "version": "1.0.150", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -123,7 +123,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.0.141", + "version": "1.0.150", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -168,7 +168,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.0.141", + "version": "1.0.150", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -179,6 +179,7 @@ "aws4fetch": "^1.0.20", "hono": "catalog:", "hono-openapi": "catalog:", + "js-base64": "3.7.7", "luxon": "catalog:", "nitro": "3.0.1-alpha.1", "solid-js": "catalog:", @@ -196,7 +197,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.0.141", + "version": "1.0.150", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "22.0.0", @@ -212,7 +213,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.0.141", + "version": "1.0.150", "bin": { "opencode": "./bin/opencode", }, @@ -241,9 +242,9 @@ "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", - "@openrouter/ai-sdk-provider": "1.2.8", - "@opentui/core": "0.1.59", - "@opentui/solid": "0.1.59", + "@openrouter/ai-sdk-provider": "1.5.2", + "@opentui/core": "0.0.0-20251211-4403a69a", + "@opentui/solid": "0.0.0-20251211-4403a69a", "@parcel/watcher": "2.5.1", "@pierre/precision-diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -304,7 +305,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.0.141", + "version": "1.0.150", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -324,7 +325,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.0.141", + "version": "1.0.150", "devDependencies": { "@hey-api/openapi-ts": "0.88.1", "@tsconfig/node22": "catalog:", @@ -335,7 +336,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.0.141", + "version": "1.0.150", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -348,7 +349,7 @@ }, "packages/tauri": { "name": "@opencode-ai/tauri", - "version": "1.0.141", + "version": "1.0.150", "dependencies": { "@opencode-ai/desktop": "workspace:*", "@tauri-apps/api": "^2", @@ -370,7 +371,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.0.141", + "version": "1.0.150", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -402,7 +403,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.0.141", + "version": "1.0.150", "dependencies": { "zod": "catalog:", }, @@ -413,7 +414,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.0.141", + "version": "1.0.150", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -462,7 +463,7 @@ "@hono/zod-validator": "0.4.2", "@kobalte/core": "0.13.11", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/precision-diffs": "0.6.0-beta.10", + "@pierre/precision-diffs": "0.6.1", "@solidjs/meta": "0.29.4", "@solidjs/router": "0.15.4", "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020", @@ -1141,27 +1142,27 @@ "@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"], - "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.2.8", "", { "dependencies": { "@openrouter/sdk": "^0.1.8" }, "peerDependencies": { "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" } }, "sha512-pQT8AzZBKg9f4bkt4doF486ZlhK0XjKkevrLkiqYgfh1Jplovieu28nK4Y+xy3sF18/mxjqh9/2y6jh01qzLrA=="], + "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.5.2", "", { "dependencies": { "@openrouter/sdk": "^0.1.27" }, "peerDependencies": { "@toon-format/toon": "^2.0.0", "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" }, "optionalPeers": ["@toon-format/toon"] }, "sha512-3Th0vmJ9pjnwcPc2H1f59Mb0LFvwaREZAScfOQIpUxAHjZ7ZawVKDP27qgsteZPmMYqccNMy4r4Y3kgUnNcKAg=="], "@openrouter/sdk": ["@openrouter/sdk@0.1.27", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RH//L10bSmc81q25zAZudiI4kNkLgxF2E+WU42vghp3N6TEvZ6F0jK7uT3tOxkEn91gzmMw9YVmDENy7SJsajQ=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentui/core": ["@opentui/core@0.1.59", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.59", "@opentui/core-darwin-x64": "0.1.59", "@opentui/core-linux-arm64": "0.1.59", "@opentui/core-linux-x64": "0.1.59", "@opentui/core-win32-arm64": "0.1.59", "@opentui/core-win32-x64": "0.1.59", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-vOtEvIulvfCOWJy0EfKAPzAMtDTmC+S0boGYrefjLqIp7tp+bbVJuXVh/8bz6GQTPmbQC6MIk6bv/ij3pdUVkA=="], + "@opentui/core": ["@opentui/core@0.0.0-20251211-4403a69a", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20251211-4403a69a", "@opentui/core-darwin-x64": "0.0.0-20251211-4403a69a", "@opentui/core-linux-arm64": "0.0.0-20251211-4403a69a", "@opentui/core-linux-x64": "0.0.0-20251211-4403a69a", "@opentui/core-win32-arm64": "0.0.0-20251211-4403a69a", "@opentui/core-win32-x64": "0.0.0-20251211-4403a69a", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-wTZKcokyU9yiDqyC0Pvf9eRSdT73s4Ynerkit/z8Af++tynqrTlZHZCXK3o42Ff7itCSILmijcTU94n69aEypA=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.59", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JQWq7W/wkmTujW/2/Ig0d7S+701rul87LSW5txQ+GM4o6EWchqHrELwo6jcZpczsyOEj4fXxI2O8l4OVYyMa9A=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20251211-4403a69a", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VAYjTa+Eiauy8gETXadD8y0PE6ppnKasDK1X354VoexZiWFR3r7rkL+TfDfk7whhqXDYyT44JDT1QmCAhVXRzQ=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.59", "", { "os": "darwin", "cpu": "x64" }, "sha512-GzafWzMP9Lt4AzUwQAk02lxgITgfvvo33OLCN265LtQBO8w23u0eB7Fjs9W+nmtcvzXtB9q6HuA0PvP9a3OioA=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20251211-4403a69a", "", { "os": "darwin", "cpu": "x64" }, "sha512-n9oVMpsojlILj1soORZzZ2Mjh8Zl73ZNcY7ot0iRmOjBDccrjDTsqKfxoGjKNd/xJSphLeu1LYGlcI5O5OczWQ=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.59", "", { "os": "linux", "cpu": "arm64" }, "sha512-QMMFg3dr2v43g3jICgzNFYQyU4YL3zHw733MVJINC+c882+qiQ8l0utTFoVEx/iRYeBzFvMVrKZ4f6G8fFrtrw=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20251211-4403a69a", "", { "os": "linux", "cpu": "arm64" }, "sha512-vf4eUjPMI4ANitK4MpTGenZFddKgQD/K21aN6cZjusnH3mTEJAoIR7GbNtMdz3qclU43ajpzTID9sAwhshwdVQ=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.59", "", { "os": "linux", "cpu": "x64" }, "sha512-XSblVjhW/7+Xs+/o+xJHwHn74nw9j69mnPAFiNdH0d8ilP4j09nUYHZOvQ89sHZaMYeSIuJEciHnh/qP0n5QXQ=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20251211-4403a69a", "", { "os": "linux", "cpu": "x64" }, "sha512-61635Up0YvVJ8gZ2eMiL1c8OfA+U6wAzT++LoaurNjbmsUAlKHws6MZdqTLw7aspJJVGsRFbA6d1Y+gXFxbDrQ=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.59", "", { "os": "win32", "cpu": "arm64" }, "sha512-GU5pPUcTpYmeOUYKpQgAPx0VKBMrfz5LNZlK8gm/jlo2CbLrIW7QLMWCoxncVZmNYqYJeG+KUZkmXYe5KLPXCQ=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20251211-4403a69a", "", { "os": "win32", "cpu": "arm64" }, "sha512-3lUddTJGKZ6uU388eU79MY//IEbgGENCITetDrrRp7v9L1AxMntE1ihf6HniziwBvKKJcsUfqLiJWcq0WPZw2w=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.59", "", { "os": "win32", "cpu": "x64" }, "sha512-InIawEI0TOG8MBBpavMq31WBRBjJ6XPuqFcsDnjqDJcXrRbNkguRW3PNXEwlyaU4tXHfYOsdlPpRtsysS8X/bQ=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20251211-4403a69a", "", { "os": "win32", "cpu": "x64" }, "sha512-Xwc1gqYsn8UZNTzNKkigZozAhBNBGbfX2B/I/aSbyqL0h8+XIInOodI0urzJWc0B6aEv/IDiT6Rm3coXFikLIg=="], - "@opentui/solid": ["@opentui/solid@0.1.59", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.59", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-O88a/+YHkHlDC4IxbrfWD2ZWlpkpu4oXC2FCLTK8taaUAnLYoybxdrMpv1+o8u8KoWXOoZmEHdntdO9O4abHnQ=="], + "@opentui/solid": ["@opentui/solid@0.0.0-20251211-4403a69a", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20251211-4403a69a", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-vuLppAdd1Qgaqhie3q2TuEr+8udjT4d8uVg5arvCe1AUDVs19I8kvadVCfzGUVmtXgFIOEakbiv6AxDq5v9Zig=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], @@ -1277,7 +1278,7 @@ "@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="], - "@pierre/precision-diffs": ["@pierre/precision-diffs@0.6.0-beta.10", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/transformers": "3.15.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.15.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-2rdd1Q1xJbB0Z4oUbm0Ybrr2gLFEdvNetZLadJboZSFL7Q4gFujdQZfXfV3vB9X+esjt++v0nzb3mioW25BOTA=="], + "@pierre/precision-diffs": ["@pierre/precision-diffs@0.6.1", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/transformers": "3.15.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.15.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-HXafRSOly6B0rRt6fuP0yy1MimHJMQ2NNnBGcIHhHwsgK4WWs+SBWRWt1usdgz0NIuSgXdIyQn8HY3F1jKyDBQ=="], "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], @@ -4277,6 +4278,10 @@ "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], + "opentui-spinner/@opentui/core": ["@opentui/core@0.1.60", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.60", "@opentui/core-darwin-x64": "0.1.60", "@opentui/core-linux-arm64": "0.1.60", "@opentui/core-linux-x64": "0.1.60", "@opentui/core-win32-arm64": "0.1.60", "@opentui/core-win32-x64": "0.1.60", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-28jphd0AJo48uvEuKXcT9pJhgAu8I2rEJhPt25cc5ipJ2iw/eDk1uoxrbID80MPDqgOEzN21vXmzXwCd6ao+hg=="], + + "opentui-spinner/@opentui/solid": ["@opentui/solid@0.1.60", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.60", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-pn91stzAHNGWaNL6h39q55bq3G1/DLqxKtT3wVsRAV68dHfPpwmqikX1nEJZK8OU84ZTPS9Ly9fz8po2Mot2uQ=="], + "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "parse-bmfont-xml/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], @@ -4853,6 +4858,22 @@ "opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], + "opentui-spinner/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.60", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N4feqnOBDA4O4yocpat5vOiV06HqJVwJGx8rEZE9DiOtl1i+1cPQ1Lx6+zWdLhbrVBJ0ENhb7Azox8sXkm/+5Q=="], + + "opentui-spinner/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.60", "", { "os": "darwin", "cpu": "x64" }, "sha512-+z3q4WaoIs7ANU8+eTFlvnfCjAS81rk81TOdZm4TJ53Ti3/B+yheWtnV/mLpLLhvZDz2VUVxxRmfDrGMnJb4fQ=="], + + "opentui-spinner/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.60", "", { "os": "linux", "cpu": "arm64" }, "sha512-/Q65sjqVGB9ygJ6lStI8n1X6RyfmJZC8XofRGEuFiMLiWcWC/xoBtztdL8LAIvHQy42y2+pl9zIiW0fWSQ0wjw=="], + + "opentui-spinner/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.60", "", { "os": "linux", "cpu": "x64" }, "sha512-AegF+g7OguIpjZKN+PS55sc3ZFY6fj+fLwfETbSRGw6NqX+aiwpae0Y3gXX1s298Yq5yQEzMXnARTCJTGH4uzg=="], + + "opentui-spinner/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.60", "", { "os": "win32", "cpu": "arm64" }, "sha512-fbkq8MOZJgT3r9q3JWqsfVxRpQ1SlbmhmvB35BzukXnZBK8eA178wbSadGH6irMDrkSIYye9WYddHI/iXjmgVQ=="], + + "opentui-spinner/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.60", "", { "os": "win32", "cpu": "x64" }, "sha512-OebCL7f9+CKodBw0G+NvKIcc74bl6/sBEHfb73cACdJDJKh+T3C3Vt9H3kQQ0m1C8wRAqX6rh706OArk1pUb2A=="], + + "opentui-spinner/@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], + + "opentui-spinner/@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.9", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.8" }, "optionalPeers": ["solid-js"] }, "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw=="], + "parse-bmfont-xml/xml2js/sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="], "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], @@ -5031,6 +5052,8 @@ "opencontrol/@modelcontextprotocol/sdk/raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "opentui-spinner/@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], diff --git a/github/action.yml b/github/action.yml index d22d19990a..f52f14d80e 100644 --- a/github/action.yml +++ b/github/action.yml @@ -20,10 +20,29 @@ inputs: runs: using: "composite" steps: + - name: Get opencode version + id: version + shell: bash + run: | + VERSION=$(curl -sf https://api.github.com/repos/sst/opencode/releases/latest | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4) + echo "version=${VERSION:-latest}" >> $GITHUB_OUTPUT + + - name: Cache opencode + id: cache + uses: actions/cache@v4 + with: + path: ~/.opencode/bin + key: opencode-${{ runner.os }}-${{ runner.arch }}-${{ steps.version.outputs.version }} + - name: Install opencode + if: steps.cache.outputs.cache-hit != 'true' shell: bash run: curl -fsSL https://opencode.ai/install | bash + - name: Add opencode to PATH + shell: bash + run: echo "$HOME/.opencode/bin" >> $GITHUB_PATH + - name: Run opencode shell: bash id: run_opencode diff --git a/infra/enterprise.ts b/infra/enterprise.ts index 70693846a1..22b4c6f44e 100644 --- a/infra/enterprise.ts +++ b/infra/enterprise.ts @@ -1,10 +1,10 @@ import { SECRET } from "./secret" -import { domain } from "./stage" +import { domain, shortDomain } from "./stage" const storage = new sst.cloudflare.Bucket("EnterpriseStorage") -const enterprise = new sst.cloudflare.x.SolidStart("Enterprise", { - domain: "enterprise." + domain, +const teams = new sst.cloudflare.x.SolidStart("Teams", { + domain: shortDomain, path: "packages/enterprise", buildCommand: "bun run build:cloudflare", environment: { diff --git a/infra/stage.ts b/infra/stage.ts index 729422905d..f9a6fd7552 100644 --- a/infra/stage.ts +++ b/infra/stage.ts @@ -11,3 +11,9 @@ new cloudflare.RegionalHostname("RegionalHostname", { regionKey: "us", zoneId: zoneID, }) + +export const shortDomain = (() => { + if ($app.stage === "production") return "opncd.ai" + if ($app.stage === "dev") return "dev.opncd.ai" + return `${$app.stage}.dev.opncd.ai` +})() diff --git a/nix/hashes.json b/nix/hashes.json index 852504297c..53a696f851 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,3 @@ { - "nodeModules": "sha256-lM/7mkrPHz5E6YOMjWspfRhKjwav9ANrLt9kYlpPkEI=" + "nodeModules": "sha256-3GaqUwomnIUW8MqUi1jDVPHQ/C5Z+D9wMR//tAGxvSQ=" } diff --git a/package.json b/package.json index b866c9bdf0..39733b931a 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@tsconfig/bun": "1.0.9", "@cloudflare/workers-types": "4.20251008.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/precision-diffs": "0.6.0-beta.10", + "@pierre/precision-diffs": "0.6.1", "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", "ai": "5.0.97", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index cd8c0308ad..9831346f21 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.0.141", + "version": "1.0.150", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/console/app/src/app.tsx b/packages/console/app/src/app.tsx index bc94b443e9..cde2f01876 100644 --- a/packages/console/app/src/app.tsx +++ b/packages/console/app/src/app.tsx @@ -3,6 +3,7 @@ import { Router } from "@solidjs/router" import { FileRoutes } from "@solidjs/start/router" import { Suspense } from "solid-js" import { Favicon } from "@opencode-ai/ui/favicon" +import { Font } from "@opencode-ai/ui/font" import "@ibm/plex/css/ibm-plex.css" import "./app.css" @@ -13,8 +14,9 @@ export default function App() { root={(props) => ( opencode - + + {props.children} )} diff --git a/packages/console/app/src/asset/lander/desktop-app-icon.png b/packages/console/app/src/asset/lander/desktop-app-icon.png new file mode 100644 index 0000000000..a35c28f516 Binary files /dev/null and b/packages/console/app/src/asset/lander/desktop-app-icon.png differ diff --git a/packages/console/app/src/asset/lander/opencode-desktop-icon.png b/packages/console/app/src/asset/lander/opencode-desktop-icon.png new file mode 100644 index 0000000000..f2c8d4f5a3 Binary files /dev/null and b/packages/console/app/src/asset/lander/opencode-desktop-icon.png differ diff --git a/packages/console/app/src/asset/lander/opencode-min.mp4 b/packages/console/app/src/asset/lander/opencode-min.mp4 index 47468bedfa..ffd6c4f7af 100644 Binary files a/packages/console/app/src/asset/lander/opencode-min.mp4 and b/packages/console/app/src/asset/lander/opencode-min.mp4 differ diff --git a/packages/console/app/src/component/email-signup.tsx b/packages/console/app/src/component/email-signup.tsx index 4943921e75..65f81b5fc6 100644 --- a/packages/console/app/src/component/email-signup.tsx +++ b/packages/console/app/src/component/email-signup.tsx @@ -25,11 +25,8 @@ export function EmailSignup() { const submission = useSubmission(emailSignup) return (

-
- -
-

OpenCode will be available on desktop soon

+

Be the first to know when we release new products

Join the waitlist for early access.

diff --git a/packages/console/app/src/component/header.tsx b/packages/console/app/src/component/header.tsx index 06e710a187..39e8339735 100644 --- a/packages/console/app/src/component/header.tsx +++ b/packages/console/app/src/component/header.tsx @@ -34,7 +34,7 @@ const fetchSvgContent = async (svgPath: string): Promise => { } } -export function Header(props: { zen?: boolean }) { +export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) { const navigate = useNavigate() const githubData = createAsync(() => github()) const starCount = createMemo(() => @@ -243,6 +243,13 @@ export function Header(props: { zen?: boolean }) { + +
  • + + Get started for free + +
  • +
    diff --git a/packages/console/app/src/config.ts b/packages/console/app/src/config.ts index a058f6829a..e8a2ed252b 100644 --- a/packages/console/app/src/config.ts +++ b/packages/console/app/src/config.ts @@ -9,8 +9,8 @@ export const config = { github: { repoUrl: "https://github.com/sst/opencode", starsFormatted: { - compact: "35K", - full: "35,000", + compact: "38K", + full: "38,000", }, }, @@ -22,8 +22,8 @@ export const config = { // Static stats (used on landing page) stats: { - contributors: "350", - commits: "5,000", + contributors: "375", + commits: "5,250", monthlyUsers: "400,000", }, } as const diff --git a/packages/console/app/src/routes/brand/index.css b/packages/console/app/src/routes/brand/index.css index d3c0d05237..b7c76f5bbf 100644 --- a/packages/console/app/src/routes/brand/index.css +++ b/packages/console/app/src/routes/brand/index.css @@ -84,7 +84,16 @@ ul { display: flex; justify-content: space-between; + align-items: center; gap: 48px; + + @media (max-width: 55rem) { + gap: 32px; + } + + @media (max-width: 48rem) { + gap: 24px; + } li { display: inline-block; a { @@ -98,6 +107,22 @@ text-underline-offset: 2px; text-decoration-thickness: 1px; } + [data-slot="cta-button"] { + background: var(--color-background-strong); + color: var(--color-text-inverted); + padding: 8px 16px; + border-radius: 4px; + font-weight: 500; + text-decoration: none; + + @media (max-width: 55rem) { + display: none; + } + } + [data-slot="cta-button"]:hover { + background: var(--color-background-strong-hover); + text-decoration: none; + } } } @@ -266,7 +291,7 @@ h1 { font-size: 1.5rem; - font-weight: 500; + font-weight: 700; color: var(--color-text-strong); margin-bottom: 1rem; } diff --git a/packages/console/app/src/routes/download/index.css b/packages/console/app/src/routes/download/index.css new file mode 100644 index 0000000000..5178a6e55b --- /dev/null +++ b/packages/console/app/src/routes/download/index.css @@ -0,0 +1,751 @@ +::selection { + background: var(--color-background-interactive); + color: var(--color-text-strong); + + @media (prefers-color-scheme: dark) { + background: var(--color-background-interactive); + color: var(--color-text-inverted); + } +} + +[data-page="download"] { + --color-background: hsl(0, 20%, 99%); + --color-background-weak: hsl(0, 8%, 97%); + --color-background-weak-hover: hsl(0, 8%, 94%); + --color-background-strong: hsl(0, 5%, 12%); + --color-background-strong-hover: hsl(0, 5%, 18%); + --color-background-interactive: hsl(62, 84%, 88%); + --color-background-interactive-weaker: hsl(64, 74%, 95%); + + --color-text: hsl(0, 1%, 39%); + --color-text-weak: hsl(0, 1%, 60%); + --color-text-weaker: hsl(30, 2%, 81%); + --color-text-strong: hsl(0, 5%, 12%); + --color-text-inverted: hsl(0, 20%, 99%); + --color-text-success: hsl(119, 100%, 35%); + + --color-border: hsl(30, 2%, 81%); + --color-border-weak: hsl(0, 1%, 85%); + + --color-icon: hsl(0, 1%, 55%); + --color-success: hsl(142, 76%, 36%); + + background: var(--color-background); + font-family: var(--font-mono); + color: var(--color-text); + padding-bottom: 5rem; + overflow-x: hidden; + + @media (prefers-color-scheme: dark) { + --color-background: hsl(0, 9%, 7%); + --color-background-weak: hsl(0, 6%, 10%); + --color-background-weak-hover: hsl(0, 6%, 15%); + --color-background-strong: hsl(0, 15%, 94%); + --color-background-strong-hover: hsl(0, 15%, 97%); + --color-background-interactive: hsl(62, 100%, 90%); + --color-background-interactive-weaker: hsl(60, 20%, 8%); + + --color-text: hsl(0, 4%, 71%); + --color-text-weak: hsl(0, 2%, 49%); + --color-text-weaker: hsl(0, 3%, 28%); + --color-text-strong: hsl(0, 15%, 94%); + --color-text-inverted: hsl(0, 9%, 7%); + --color-text-success: hsl(119, 60%, 72%); + + --color-border: hsl(0, 3%, 28%); + --color-border-weak: hsl(0, 4%, 23%); + + --color-icon: hsl(10, 3%, 43%); + --color-success: hsl(142, 76%, 46%); + } + + /* Header and Footer styles - copied from enterprise */ + [data-component="top"] { + padding: 24px 5rem; + height: 80px; + position: sticky; + top: 0; + display: flex; + justify-content: space-between; + align-items: center; + background: var(--color-background); + border-bottom: 1px solid var(--color-border-weak); + z-index: 10; + + @media (max-width: 60rem) { + padding: 24px 1.5rem; + } + + img { + height: 34px; + width: auto; + } + + [data-component="nav-desktop"] { + ul { + display: flex; + justify-content: space-between; + align-items: center; + gap: 48px; + + @media (max-width: 55rem) { + gap: 32px; + } + + @media (max-width: 48rem) { + gap: 24px; + } + li { + display: inline-block; + a { + text-decoration: none; + span { + color: var(--color-text-weak); + } + } + a:hover { + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-thickness: 1px; + } + [data-slot="cta-button"] { + background: var(--color-background-strong); + color: var(--color-text-inverted); + padding: 8px 16px; + border-radius: 4px; + font-weight: 500; + text-decoration: none; + + @media (max-width: 55rem) { + display: none; + } + } + [data-slot="cta-button"]:hover { + background: var(--color-background-strong-hover); + text-decoration: none; + } + } + } + + @media (max-width: 40rem) { + display: none; + } + } + + [data-component="nav-mobile"] { + button > svg { + color: var(--color-icon); + } + } + + [data-component="nav-mobile-toggle"] { + border: none; + background: none; + outline: none; + height: 40px; + width: 40px; + cursor: pointer; + margin-right: -8px; + } + + [data-component="nav-mobile-toggle"]:hover { + background: var(--color-background-weak); + } + + [data-component="nav-mobile"] { + display: none; + + @media (max-width: 40rem) { + display: block; + + [data-component="nav-mobile-icon"] { + cursor: pointer; + height: 40px; + width: 40px; + display: flex; + align-items: center; + justify-content: center; + } + + [data-component="nav-mobile-menu-list"] { + position: fixed; + background: var(--color-background); + top: 80px; + left: 0; + right: 0; + height: 100vh; + + ul { + list-style: none; + padding: 20px 0; + + li { + a { + text-decoration: none; + padding: 20px; + display: block; + + span { + color: var(--color-text-weak); + } + } + + a:hover { + background: var(--color-background-weak); + } + } + } + } + } + } + + [data-slot="logo dark"] { + display: none; + } + + @media (prefers-color-scheme: dark) { + [data-slot="logo light"] { + display: none; + } + [data-slot="logo dark"] { + display: block; + } + } + } + + [data-component="footer"] { + border-top: 1px solid var(--color-border-weak); + display: flex; + flex-direction: row; + + @media (max-width: 65rem) { + border-bottom: 1px solid var(--color-border-weak); + } + + [data-slot="cell"] { + flex: 1; + text-align: center; + + a { + text-decoration: none; + padding: 2rem 0; + width: 100%; + display: block; + + span { + color: var(--color-text-weak); + + @media (max-width: 40rem) { + display: none; + } + } + } + + a:hover { + background: var(--color-background-weak); + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-thickness: 1px; + } + } + + [data-slot="cell"] + [data-slot="cell"] { + border-left: 1px solid var(--color-border-weak); + + @media (max-width: 40rem) { + border-left: none; + } + } + + @media (max-width: 25rem) { + flex-wrap: wrap; + + [data-slot="cell"] { + flex: 1 0 100%; + border-left: none; + border-top: 1px solid var(--color-border-weak); + } + + [data-slot="cell"]:nth-child(1) { + border-top: none; + } + } + } + + [data-component="container"] { + max-width: 67.5rem; + margin: 0 auto; + border: 1px solid var(--color-border-weak); + border-top: none; + + @media (max-width: 65rem) { + border: none; + } + } + + [data-component="content"] { + padding: 6rem 5rem; + + @media (max-width: 60rem) { + padding: 4rem 1.5rem; + } + } + + [data-component="legal"] { + color: var(--color-text-weak); + text-align: center; + padding: 2rem 5rem; + display: flex; + gap: 32px; + justify-content: center; + + @media (max-width: 60rem) { + padding: 2rem 1.5rem; + } + + a { + color: var(--color-text-weak); + text-decoration: none; + } + + a:hover { + color: var(--color-text); + text-decoration: underline; + } + } + + /* Download Hero Section */ + [data-component="download-hero"] { + display: grid; + grid-template-columns: 260px 1fr; + gap: 4rem; + padding-bottom: 2rem; + margin-bottom: 4rem; + + @media (max-width: 50rem) { + grid-template-columns: 1fr; + gap: 1.5rem; + padding-bottom: 2rem; + margin-bottom: 2rem; + } + + [data-component="hero-icon"] { + display: flex; + justify-content: flex-end; + align-items: center; + + @media (max-width: 40rem) { + display: none; + } + + [data-slot="icon-placeholder"] { + width: 120px; + height: 120px; + background: var(--color-background-weak); + border: 1px solid var(--color-border-weak); + border-radius: 24px; + + @media (max-width: 50rem) { + width: 80px; + height: 80px; + } + } + + img { + width: 120px; + height: 120px; + border-radius: 24px; + box-shadow: + 0 1.467px 2.847px 0 rgba(0, 0, 0, 0.42), + 0 0.779px 1.512px 0 rgba(0, 0, 0, 0.34), + 0 0.324px 0.629px 0 rgba(0, 0, 0, 0.24); + + @media (max-width: 50rem) { + width: 80px; + height: 80px; + border-radius: 16px; + } + } + + @media (max-width: 50rem) { + justify-content: flex-start; + } + } + + [data-component="hero-text"] { + display: flex; + flex-direction: column; + justify-content: center; + + h1 { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-text-strong); + margin-bottom: 4px; + + @media (max-width: 40rem) { + margin-bottom: 1rem; + } + } + + p { + color: var(--color-text); + margin-bottom: 12px; + + @media (max-width: 40rem) { + margin-bottom: 2.5rem; + line-height: 1.6; + } + } + + [data-component="download-button"] { + padding: 8px 20px 8px 16px; + background: var(--color-background-strong); + color: var(--color-text-inverted); + border: none; + border-radius: 4px; + font-weight: 500; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 10px; + transition: all 0.2s ease; + text-decoration: none; + width: fit-content; + + &:hover:not(:disabled) { + background: var(--color-background-strong-hover); + } + + &:active { + transform: scale(0.98); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + } + } + } + + /* Download Sections */ + [data-component="download-section"] { + display: grid; + grid-template-columns: 260px 1fr; + gap: 4rem; + margin-bottom: 4rem; + + @media (max-width: 50rem) { + grid-template-columns: 1fr; + gap: 1rem; + margin-bottom: 3rem; + } + + &:last-child { + margin-bottom: 0; + } + + [data-component="section-label"] { + font-weight: 500; + color: var(--color-text-strong); + padding-top: 1rem; + + span { + color: var(--color-text-weaker); + } + + @media (max-width: 50rem) { + padding-top: 0; + padding-bottom: 0.5rem; + } + } + + [data-component="section-content"] { + display: flex; + flex-direction: column; + gap: 0; + } + } + + /* CLI Rows */ + button[data-component="cli-row"] { + display: flex; + align-items: center; + gap: 12px; + padding: 1rem 0.5rem 1rem 1.5rem; + margin: 0 -0.5rem 0 -1.5rem; + background: none; + border: none; + border-radius: 4px; + width: calc(100% + 2rem); + text-align: left; + cursor: pointer; + transition: background 0.15s ease; + + &:hover { + background: var(--color-background-weak); + } + + code { + font-family: var(--font-mono); + color: var(--color-text-weak); + + strong { + color: var(--color-text-strong); + font-weight: 500; + } + } + + [data-component="copy-status"] { + display: flex; + align-items: center; + opacity: 0; + transition: opacity 0.15s ease; + color: var(--color-icon); + + svg { + width: 18px; + height: 18px; + } + + [data-slot="copy"] { + display: block; + } + + [data-slot="check"] { + display: none; + } + } + + &:hover [data-component="copy-status"] { + opacity: 1; + } + + &[data-copied] [data-component="copy-status"] { + opacity: 1; + + [data-slot="copy"] { + display: none; + } + + [data-slot="check"] { + display: block; + } + } + } + + /* Download Rows */ + [data-component="download-row"] { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 0.5rem 0.75rem 1.5rem; + margin: 0 -0.5rem 0 -1.5rem; + border-radius: 4px; + transition: background 0.15s ease; + + &:hover { + background: var(--color-background-weak); + } + + [data-component="download-info"] { + display: flex; + align-items: center; + gap: 0.75rem; + + [data-slot="icon"] { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + color: var(--color-icon); + + svg { + width: 20px; + height: 20px; + } + + img { + width: 20px; + height: 20px; + } + } + + span { + color: var(--color-text); + } + } + + [data-component="action-button"] { + padding: 6px 16px; + background: var(--color-background); + color: var(--color-text); + border: 1px solid var(--color-border); + border-radius: 4px; + font-weight: 500; + cursor: pointer; + text-decoration: none; + transition: all 0.2s ease; + + &:hover { + background: var(--color-background-weak); + border-color: var(--color-border); + text-decoration: none; + } + + &:active { + transform: scale(0.98); + } + } + } + + a { + color: var(--color-text-strong); + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-thickness: 1px; + + &:hover { + text-decoration-thickness: 2px; + } + } + + /* Narrow screen font sizes */ + @media (max-width: 40rem) { + [data-component="download-section"] { + [data-component="section-label"] { + font-size: 14px; + } + } + + button[data-component="cli-row"] { + margin: 0; + padding: 1rem 0; + width: 100%; + overflow: hidden; + + code { + font-size: 14px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; + max-width: calc(100vw - 80px); + } + + [data-component="copy-status"] { + opacity: 1 !important; + flex-shrink: 0; + } + } + + [data-component="download-row"] { + margin: 0; + padding: 0.75rem 0; + + [data-component="download-info"] span { + font-size: 14px; + } + + [data-component="action-button"] { + font-size: 14px; + padding-left: 8px; + padding-right: 8px; + } + } + } + + @media (max-width: 22.5rem) { + [data-slot="hide-narrow"] { + display: none; + } + } + + /* FAQ Section */ + [data-component="faq"] { + border-top: 1px solid var(--color-border-weak); + padding: 4rem 5rem; + margin-top: 4rem; + + @media (max-width: 60rem) { + padding: 3rem 1.5rem; + margin-top: 3rem; + } + + [data-slot="section-title"] { + margin-bottom: 24px; + + h3 { + font-size: 16px; + font-weight: 700; + color: var(--color-text-strong); + margin-bottom: 12px; + } + } + + ul { + padding: 0; + + li { + list-style: none; + margin-bottom: 24px; + line-height: 200%; + } + } + + [data-slot="faq-question"] { + display: flex; + gap: 16px; + margin-bottom: 8px; + color: var(--color-text-strong); + font-weight: 500; + cursor: pointer; + background: none; + border: none; + padding: 0; + align-items: start; + min-height: 24px; + + svg { + margin-top: 2px; + } + + [data-slot="faq-icon-plus"] { + flex-shrink: 0; + color: var(--color-text-weak); + margin-top: 2px; + + [data-closed] & { + display: block; + } + [data-expanded] & { + display: none; + } + } + [data-slot="faq-icon-minus"] { + flex-shrink: 0; + color: var(--color-text-weak); + margin-top: 2px; + + [data-closed] & { + display: none; + } + [data-expanded] & { + display: block; + } + } + [data-slot="faq-question-text"] { + flex-grow: 1; + text-align: left; + } + } + + [data-slot="faq-answer"] { + margin-left: 40px; + margin-bottom: 32px; + line-height: 200%; + } + } +} diff --git a/packages/console/app/src/routes/download/index.tsx b/packages/console/app/src/routes/download/index.tsx new file mode 100644 index 0000000000..2616b7ea13 --- /dev/null +++ b/packages/console/app/src/routes/download/index.tsx @@ -0,0 +1,416 @@ +import "./index.css" +import { Title, Meta, Link } from "@solidjs/meta" +import { A, createAsync, query } from "@solidjs/router" +import { Header } from "~/component/header" +import { Footer } from "~/component/footer" +import { IconCopy, IconCheck } from "~/component/icon" +import { Faq } from "~/component/faq" +import desktopAppIcon from "../../asset/lander/opencode-desktop-icon.png" +import { Legal } from "~/component/legal" +import { config } from "~/config" + +const getLatestRelease = query(async () => { + const response = await fetch("https://api.github.com/repos/sst/opencode/releases/latest") + if (!response.ok) return null + const data = await response.json() + return data.tag_name as string +}, "latest-release") + +function CopyStatus() { + return ( + + + + + ) +} + +export default function Download() { + const release = createAsync(() => getLatestRelease(), { + deferStream: true, + }) + const download = () => { + const version = release() + if (!version) return null + return `https://github.com/sst/opencode/releases/download/${version}` + } + const handleCopyClick = (command: string) => (event: Event) => { + const button = event.currentTarget as HTMLButtonElement + navigator.clipboard.writeText(command) + button.setAttribute("data-copied", "") + setTimeout(() => { + button.removeAttribute("data-copied") + }, 1500) + } + return ( +
    + OpenCode | Download + + +
    +
    + +
    +
    +
    + OpenCode Desktop +
    +
    +

    Download OpenCode

    +

    Available in Beta for macOS, Windows, and Linux

    +
    +
    + +
    +
    + [1] OpenCode Terminal +
    +
    + + + + + +
    +
    + +
    +
    + [2] OpenCode Desktop (Beta) +
    +
    +
    +
    + + + + + + + macOS (Apple Silicon) + +
    + + Download + +
    +
    +
    + + + + + + macOS (Intel) +
    + + Download + +
    +
    +
    + + + + + + + + + + + + + Windows (x64) +
    + + Download + +
    +
    +
    + + + + + + Linux (.deb) +
    + + Download + +
    +
    +
    + + + + + + Linux (.rpm) +
    + + Download + +
    +
    +
    + +
    +
    + [3] OpenCode Extensions +
    +
    +
    +
    + + + + + + + + + + + + + VS Code +
    + + Install + +
    + +
    +
    + + + + + + + + + + + + + Cursor +
    + + Install + +
    + +
    +
    + + + + + + Zed +
    + + Install + +
    + +
    +
    + + + + + + Windsurf +
    + + Install + +
    + +
    +
    + + + + + + VSCodium +
    + + Install + +
    +
    +
    + +
    +
    + [4] OpenCode Integrations +
    +
    +
    +
    + + + + + + GitHub +
    + + Install + +
    + +
    +
    + + + + + + GitLab +
    + + Install + +
    +
    +
    +
    + +
    +
    +

    FAQ

    +
    +
      +
    • + + OpenCode is an open source agent that helps you write and run code with any AI model. It's available as + a terminal-based interface, desktop app, or IDE extension. + +
    • +
    • + + The easiest way to get started is to read the intro. + +
    • +
    • + + Not necessarily, but probably. You'll need an AI subscription if you want to connect OpenCode to a paid + provider, although you can work with{" "} + + local models + {" "} + for free. While we encourage users to use Zen, OpenCode works with all popular + providers such as OpenAI, Anthropic, xAI etc. + +
    • +
    • + + Not anymore! OpenCode is now available as an app for your desktop. + +
    • +
    • + + OpenCode is 100% free to use. Any additional costs will come from your subscription to a model provider. + While OpenCode works with any model provider, we recommend using Zen. + +
    • +
    • + + Your data and information is only stored when you create sharable links in OpenCode. Learn more about{" "} + share pages. + +
    • +
    • + + Yes, OpenCode is fully open source. The source code is public on{" "} + + GitHub + {" "} + under the{" "} + + MIT License + + , meaning anyone can use, modify, or contribute to its development. Anyone from the community can file + issues, submit pull requests, and extend functionality. + +
    • +
    +
    + +
    +
    + +
    + ) +} diff --git a/packages/console/app/src/routes/enterprise/index.css b/packages/console/app/src/routes/enterprise/index.css index 0178e40a27..496a886ebe 100644 --- a/packages/console/app/src/routes/enterprise/index.css +++ b/packages/console/app/src/routes/enterprise/index.css @@ -84,7 +84,16 @@ ul { display: flex; justify-content: space-between; + align-items: center; gap: 48px; + + @media (max-width: 55rem) { + gap: 32px; + } + + @media (max-width: 48rem) { + gap: 24px; + } li { display: inline-block; a { @@ -98,6 +107,22 @@ text-underline-offset: 2px; text-decoration-thickness: 1px; } + [data-slot="cta-button"] { + background: var(--color-background-strong); + color: var(--color-text-inverted); + padding: 8px 16px; + border-radius: 4px; + font-weight: 500; + text-decoration: none; + + @media (max-width: 55rem) { + display: none; + } + } + [data-slot="cta-button"]:hover { + background: var(--color-background-strong-hover); + text-decoration: none; + } } } @@ -289,7 +314,7 @@ [data-component="enterprise-column-1"] { h1 { font-size: 1.5rem; - font-weight: 500; + font-weight: 700; color: var(--color-text-strong); margin-bottom: 1rem; } @@ -441,7 +466,7 @@ h3 { font-size: 16px; - font-weight: 500; + font-weight: 700; color: var(--color-text-strong); margin-bottom: 12px; } diff --git a/packages/console/app/src/routes/index.css b/packages/console/app/src/routes/index.css index 92de172e12..ae329b98bf 100644 --- a/packages/console/app/src/routes/index.css +++ b/packages/console/app/src/routes/index.css @@ -16,6 +16,8 @@ --color-background-strong-hover: hsl(0, 5%, 18%); --color-background-interactive: hsl(62, 84%, 88%); --color-background-interactive-weaker: hsl(64, 74%, 95%); + --color-surface-raised-base: hsla(0, 100%, 3%, 0.01); + --color-surface-raised-base-active: hsla(0, 100%, 17%, 0.06); --color-text: hsl(0, 1%, 39%); --color-text-weak: hsl(0, 1%, 60%); @@ -24,7 +26,7 @@ --color-text-inverted: hsl(0, 20%, 99%); --color-border: hsl(30, 2%, 81%); - --color-border-weak: hsl(0, 1%, 85%); + --color-border-weak: hsla(0, 100%, 3%, 0.12); --color-icon: hsl(0, 1%, 55%); } @@ -62,6 +64,14 @@ body { } } +[data-slot="br"] { + display: block; + + @media (max-width: 60rem) { + display: none; + } +} + [data-page="opencode"] { background: var(--color-background); --padding: 5rem; @@ -215,7 +225,16 @@ body { ul { display: flex; justify-content: space-between; + align-items: center; gap: 48px; + + @media (max-width: 55rem) { + gap: 32px; + } + + @media (max-width: 48rem) { + gap: 24px; + } li { display: inline-block; a { @@ -229,6 +248,25 @@ body { text-underline-offset: var(--space-1); text-decoration-thickness: 1px; } + [data-slot="cta-button"] { + background: var(--color-background-strong); + color: var(--color-text-inverted); + padding: 8px 16px 8px 10px; + border-radius: 4px; + font-weight: 500; + text-decoration: none; + display: flex; + align-items: center; + gap: 8px; + + @media (max-width: 55rem) { + display: none; + } + } + [data-slot="cta-button"]:hover { + background: var(--color-background-strong-hover); + text-decoration: none; + } } } @@ -322,7 +360,7 @@ body { display: flex; flex-direction: column; max-width: 100%; - padding: calc(var(--vertical-padding) * 2) var(--padding); + padding: calc(var(--vertical-padding) * 1.5) var(--padding); @media (max-width: 30rem) { padding: var(--vertical-padding) var(--padding); @@ -426,7 +464,7 @@ body { cursor: pointer; align-items: center; color: var(--color-text); - gap: var(--space-1); + gap: 16px; color: var(--color-text); padding: 8px 16px 8px 8px; border-radius: 4px; @@ -465,6 +503,77 @@ body { } } + [data-component="desktop-app-banner"] { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 32px; + + [data-slot="badge"] { + background: var(--color-background-strong); + color: var(--color-text-inverted); + font-weight: 500; + padding: 4px 8px; + line-height: 1; + flex-shrink: 0; + } + + [data-slot="content"] { + display: flex; + align-items: center; + gap: 4px; + } + + [data-slot="text"] { + color: var(--color-text-strong); + line-height: 1.4; + + @media (max-width: 30.625rem) { + display: none; + } + } + + [data-slot="platforms"] { + @media (max-width: 49.125rem) { + display: none; + } + } + + [data-slot="link"] { + color: var(--color-text-weak); + white-space: nowrap; + text-decoration: none; + + @media (max-width: 30.625rem) { + display: none; + } + } + + [data-slot="link"]:hover { + color: var(--color-text); + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-thickness: 1px; + } + + [data-slot="link-mobile"] { + display: none; + color: var(--color-text-strong); + white-space: nowrap; + text-decoration: none; + + @media (max-width: 30.625rem) { + display: inline; + } + } + + [data-slot="link-mobile"]:hover { + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-thickness: 1px; + } + } + [data-slot="hero-copy"] { [data-slot="releases"] { background: none; @@ -492,7 +601,7 @@ body { h1 { font-size: 38px; color: var(--color-text-strong); - font-weight: 500; + font-weight: 700; margin-bottom: 8px; @media (max-width: 60rem) { @@ -502,7 +611,7 @@ body { p { color: var(--color-text); - margin-bottom: 40px; + margin-bottom: 32px; max-width: 82%; @media (max-width: 50rem) { @@ -518,7 +627,6 @@ body { border-radius: 4px; font-weight: 500; cursor: pointer; - margin-bottom: 80px; display: flex; width: fit-content; gap: 12px; @@ -596,7 +704,7 @@ body { h3 { font-size: 16px; - font-weight: 500; + font-weight: 700; color: var(--color-text-strong); margin-bottom: 12px; } @@ -701,7 +809,7 @@ body { [data-slot="privacy-title"] { h3 { font-size: 16px; - font-weight: 500; + font-weight: 700; color: var(--color-text-strong); margin-bottom: 12px; } @@ -727,7 +835,7 @@ body { [data-slot="zen-cta-copy"] { strong { color: var(--color-text-strong); - font-weight: 500; + font-weight: 700; margin-bottom: 16px; display: block; } diff --git a/packages/console/app/src/routes/index.tsx b/packages/console/app/src/routes/index.tsx index 56f0785622..9948551e4c 100644 --- a/packages/console/app/src/routes/index.tsx +++ b/packages/console/app/src/routes/index.tsx @@ -53,16 +53,17 @@ export default function Home() {
    - - What’s new in {release()?.name ?? "the latest release"} - -

    The open source coding agent

    + {/**/} + {/* What’s new in {release()?.name ?? "the latest release"}*/} + {/**/} +

    The open source AI coding agent

    - OpenCode includes free models or connect from any provider to
    - use other models, including Claude, GPT, Gemini and more. + Free models included or connect any model from any provider, including + Claude, GPT, Gemini and more.

    -

    Install and use. No account, no email, and no credit card.

    -

    - Available in terminal, web, and desktop (coming soon). -
    - Extensions for VS Code, Cursor, Windsurf, and more. -

    @@ -157,15 +153,9 @@ export default function Home() {

    What is OpenCode?

    -

    OpenCode is an open source agent that helps you write and run code directly from the terminal.

    +

    OpenCode is an open source agent that helps you write code in your terminal, IDE, or desktop.

      -
    • - [*] -
      - Native TUI A responsive, native, themeable terminal UI -
      -
    • [*]
      @@ -199,7 +189,7 @@ export default function Home() {
    • [*]
      - Any editor OpenCode runs in your terminal, pair it with any IDE + Any editor Available as a terminal interface, desktop app, and IDE extension
    @@ -651,9 +641,8 @@ export default function Home() {
    • - OpenCode is an open source agent that helps you write and run code directly from the terminal. You can - pair OpenCode with any AI model, and because it’s terminal-based you can pair it with your preferred - code editor. + OpenCode is an open source agent that helps you write and run code with any AI model. It's available + as a terminal-based interface, desktop app, or IDE extension.
    • @@ -663,29 +652,38 @@ export default function Home() {
    • - Not necessarily, but probably. You’ll need an AI subscription if you want to connect OpenCode to a - paid provider, although you can work with{" "} + Not necessarily, OpenCode comes with a set of free models that you can use without creating an + account. Aside from these, you can use any of the popular coding models by creating a{" "} + Zen account. While we encourage users to use Zen, OpenCode also works with all + popular providers such as OpenAI, Anthropic, xAI etc. You can even connect your{" "} local models - {" "} - for free. While we encourage users to use Zen, OpenCode works with all popular - providers such as OpenAI, Anthropic, xAI etc. + + . + +
    • +
    • + + Yes, OpenCode supports subscription plans from all major providers. You can use your Claude Pro/Max, + ChatGPT Plus/Pro, or GitHub Copilot subscriptions. Learn more + .
    • - Yes, for now. We are actively working on a desktop app. Join the waitlist for early access. + Not anymore! OpenCode is now available as an app for your desktop.
    • - OpenCode is 100% free to use. Any additional costs will come from your subscription to a model - provider. While OpenCode works with any model provider, we recommend using Zen. + OpenCode is 100% free to use. It also comes with a set of free models. There might be additional costs + if you connect any other provider.
    • - Your data and information is only stored when you create sharable links in OpenCode. Learn more about{" "} + Your data and information is only stored when you use our free models or create sharable links. Learn + more about our models and{" "} share pages.
    • @@ -745,6 +743,17 @@ export default function Home() { />
    +
    + + + +
    +
    + + + +
    Learn about Zen diff --git a/packages/console/app/src/routes/t/[...path].tsx b/packages/console/app/src/routes/t/[...path].tsx new file mode 100644 index 0000000000..b877a8d58a --- /dev/null +++ b/packages/console/app/src/routes/t/[...path].tsx @@ -0,0 +1,20 @@ +import type { APIEvent } from "@solidjs/start/server" + +async function handler(evt: APIEvent) { + const req = evt.request.clone() + const url = new URL(req.url) + const targetUrl = `https://enterprise.opencode.ai/${url.pathname}${url.search}` + const response = await fetch(targetUrl, { + method: req.method, + headers: req.headers, + body: req.body, + }) + return response +} + +export const GET = handler +export const POST = handler +export const PUT = handler +export const DELETE = handler +export const OPTIONS = handler +export const PATCH = handler diff --git a/packages/console/app/src/routes/zen/index.css b/packages/console/app/src/routes/zen/index.css index fbdd15306b..5055bac2ac 100644 --- a/packages/console/app/src/routes/zen/index.css +++ b/packages/console/app/src/routes/zen/index.css @@ -147,7 +147,16 @@ body { ul { display: flex; justify-content: space-between; + align-items: center; gap: 48px; + + @media (max-width: 55rem) { + gap: 32px; + } + + @media (max-width: 48rem) { + gap: 24px; + } li { display: inline-block; a { @@ -161,6 +170,22 @@ body { text-underline-offset: var(--space-1); text-decoration-thickness: 1px; } + [data-slot="cta-button"] { + background: var(--color-background-strong); + color: var(--color-text-inverted); + padding: 8px 16px; + border-radius: 4px; + font-weight: 500; + text-decoration: none; + + @media (max-width: 55rem) { + display: none; + } + } + [data-slot="cta-button"]:hover { + background: var(--color-background-strong-hover); + text-decoration: none; + } } } @@ -280,7 +305,7 @@ body { h1 { font-size: 28px; color: var(--color-text-strong); - font-weight: 500; + font-weight: 700; margin-bottom: 16px; display: block; @@ -369,7 +394,7 @@ body { h3 { font-size: 16px; - font-weight: 500; + font-weight: 700; color: var(--color-text-strong); margin-bottom: 12px; } @@ -442,7 +467,7 @@ body { [data-slot="privacy-title"] { h3 { font-size: 16px; - font-weight: 500; + font-weight: 700; color: var(--color-text); margin-bottom: 12px; } diff --git a/packages/console/app/src/routes/zen/index.tsx b/packages/console/app/src/routes/zen/index.tsx index 7fd393962d..6b163315c6 100644 --- a/packages/console/app/src/routes/zen/index.tsx +++ b/packages/console/app/src/routes/zen/index.tsx @@ -38,7 +38,7 @@ export default function Home() {
    -
    +
    diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index e34704f98e..7d7767b8df 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -588,7 +588,7 @@ export async function handler( tx .update(KeyTable) .set({ timeUsed: sql`now()` }) - .where(eq(KeyTable.id, authInfo.apiKeyId)), + .where(and(eq(KeyTable.workspaceID, authInfo.workspaceID), eq(KeyTable.id, authInfo.apiKeyId))), ) } diff --git a/packages/console/app/src/style/token/font.css b/packages/console/app/src/style/token/font.css index 67143e6626..844677b5fa 100644 --- a/packages/console/app/src/style/token/font.css +++ b/packages/console/app/src/style/token/font.css @@ -15,6 +15,7 @@ body { --font-size-9xl: 8rem; --font-mono: - "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + "Berkeley Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; --font-sans: var(--font-mono); } diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 2b7332207d..86a59d6bbf 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.0.141", + "version": "1.0.150", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 3f8fb578ef..d32bde30c7 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.0.141", + "version": "1.0.150", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 33dcd8331b..764daf918c 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.0.141", + "version": "1.0.150", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 96532245c4..1d12a9cb9f 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/desktop", - "version": "1.0.141", + "version": "1.0.150", "description": "", "type": "module", "exports": { diff --git a/packages/desktop/src/app.tsx b/packages/desktop/src/app.tsx index 0ca4d5e6be..a1ff90d269 100644 --- a/packages/desktop/src/app.tsx +++ b/packages/desktop/src/app.tsx @@ -15,8 +15,14 @@ import { GlobalSDKProvider } from "./context/global-sdk" import { SessionProvider } from "./context/session" import { Show } from "solid-js" +declare global { + interface Window { + __OPENCODE__?: { updaterEnabled?: boolean; port?: number } + } +} + const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1" -const port = import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096" +const port = window.__OPENCODE__?.port ?? import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096" const url = new URLSearchParams(document.location.search).get("url") || diff --git a/packages/desktop/src/components/link.tsx b/packages/desktop/src/components/link.tsx new file mode 100644 index 0000000000..e13c313304 --- /dev/null +++ b/packages/desktop/src/components/link.tsx @@ -0,0 +1,17 @@ +import { ComponentProps, splitProps } from "solid-js" +import { usePlatform } from "@/context/platform" + +export interface LinkProps extends ComponentProps<"button"> { + href: string +} + +export function Link(props: LinkProps) { + const platform = usePlatform() + const [local, rest] = splitProps(props, ["href", "children"]) + + return ( + + ) +} diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index 6590b6d182..70ee0a7397 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -1,9 +1,20 @@ import { useFilteredList } from "@opencode-ai/ui/hooks" -import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match, createSignal } from "solid-js" +import { + createEffect, + on, + Component, + Show, + For, + onMount, + onCleanup, + Switch, + Match, + createSignal, + createMemo, +} from "solid-js" import { createStore } from "solid-js/store" import { createFocusSignal } from "@solid-primitives/active-element" import { useLocal } from "@/context/local" -import { DateTime } from "luxon" import { ContentPart, DEFAULT_PROMPT, isPromptEqual, Prompt, useSession } from "@/context/session" import { useSDK } from "@/context/sdk" import { useNavigate } from "@solidjs/router" @@ -14,10 +25,16 @@ import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { Tooltip } from "@opencode-ai/ui/tooltip" import { IconButton } from "@opencode-ai/ui/icon-button" -import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { Select } from "@opencode-ai/ui/select" +import { Tag } from "@opencode-ai/ui/tag" import { getDirectory, getFilename } from "@opencode-ai/util/path" -import { type IconName } from "@opencode-ai/ui/icons/provider" +import { useLayout } from "@/context/layout" +import { popularProviders, useProviders } from "@/hooks/use-providers" +import { Dialog } from "@opencode-ai/ui/dialog" +import { List, ListRef } from "@opencode-ai/ui/list" +import { iife } from "@opencode-ai/util/iife" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import { IconName } from "@opencode-ai/ui/icons/provider" interface PromptInputProps { class?: string @@ -58,6 +75,8 @@ export const PromptInput: Component = (props) => { const sync = useSync() const local = useLocal() const session = useSession() + const layout = useLayout() + const providers = useProviders() let editorRef!: HTMLDivElement const [store, setStore] = createStore<{ @@ -455,55 +474,207 @@ export const PromptInput: Component = (props) => { class="capitalize" variant="ghost" /> - `${x.provider.id}:${x.id}`} - items={local.model.list()} - current={local.model.current()} - filterKeys={["provider.name", "name", "id"]} - groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)} - sortGroupsBy={(a, b) => { - const order = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"] - if (a.category === "Recent" && b.category !== "Recent") return -1 - if (b.category === "Recent" && a.category !== "Recent") return 1 - const aProvider = a.items[0].provider.id - const bProvider = b.items[0].provider.id - if (order.includes(aProvider) && !order.includes(bProvider)) return -1 - if (!order.includes(aProvider) && order.includes(bProvider)) return 1 - return order.indexOf(aProvider) - order.indexOf(bProvider) - }} - onSelect={(x) => - local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true }) - } - trigger={ - - } - > - {(i) => ( -
    -
    - -
    - {i.name} - - - {DateTime.fromFormat("unknown", "yyyy-MM-dd").toFormat("LLL yyyy")} - - -
    -
    - -
    Free
    -
    -
    - )} -
    + + + + 0}> + {iife(() => { + const models = createMemo(() => + local.model + .list() + .filter((m) => + layout.connect.state() === "complete" ? m.provider.id === layout.connect.provider() : true, + ), + ) + return ( + { + if (open) { + layout.dialog.open("model") + } else { + layout.dialog.close("model") + } + }} + title="Select model" + placeholder="Search models" + emptyMessage="No model results" + key={(x) => `${x.provider.id}:${x.id}`} + items={models} + current={local.model.current()} + filterKeys={["provider.name", "name", "id"]} + // groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)} + groupBy={(x) => x.provider.name} + sortGroupsBy={(a, b) => { + if (a.category === "Recent" && b.category !== "Recent") return -1 + if (b.category === "Recent" && a.category !== "Recent") return 1 + const aProvider = a.items[0].provider.id + const bProvider = b.items[0].provider.id + if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1 + if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1 + return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider) + }} + onSelect={(x) => + local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { + recent: true, + }) + } + actions={ + + } + > + {(i) => ( +
    + {i.name} + + Free + + + Latest + +
    + )} +
    + ) + })} +
    + + {iife(() => { + let listRef: ListRef | undefined + const handleKey = (e: KeyboardEvent) => { + if (e.key === "Escape") return + listRef?.onKeyDown(e) + } + + onMount(() => { + document.addEventListener("keydown", handleKey) + onCleanup(() => { + document.removeEventListener("keydown", handleKey) + }) + }) + + return ( + { + if (open) { + layout.dialog.open("model") + } else { + layout.dialog.close("model") + } + }} + > + + Select model + + + +
    +
    Free models provided by OpenCode
    + (listRef = ref)} + items={local.model.list} + current={local.model.current()} + key={(x) => `${x.provider.id}:${x.id}`} + onSelect={(x) => { + local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { + recent: true, + }) + layout.dialog.close("model") + }} + > + {(i) => ( +
    + {i.name} + Free + + Latest + +
    + )} +
    +
    +
    +
    +
    +
    +
    +
    + Add more models from popular providers +
    +
    + x?.id} + items={providers.popular} + activeIcon="plus-small" + sortBy={(a, b) => { + if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) + return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) + return a.name.localeCompare(b.name) + }} + onSelect={(x) => { + if (!x) return + layout.dialog.connect(x.id) + }} + > + {(i) => ( +
    + + {i.name} + + Recommended + + +
    + Connect with Claude Pro/Max or API key +
    +
    +
    + )} +
    + +
    +
    +
    +
    + +
    + ) + })} +
    +
    +
    ) { - const available = PASTEL_COLORS.filter((c) => !usedColors.has(c)) - if (available.length === 0) return PASTEL_COLORS[Math.floor(Math.random() * PASTEL_COLORS.length)] - return available[Math.floor(Math.random() * available.length)] -} - -async function ensureProjectColor( - project: Project, - sdk: ReturnType, - usedColors: Set, -): Promise { - if (project.icon?.color) return project - const color = pickAvailableColor(usedColors) - usedColors.add(color) - const updated = { ...project, icon: { ...project.icon, color } } - sdk.client.project.update({ projectID: project.id, icon: { color } }) - return updated -} +import { onMount } from "solid-js" type State = { ready: boolean - provider: Provider[] agent: Agent[] project: string + provider: ProviderListResponse config: Config path: Path session: Session[] @@ -81,26 +52,58 @@ type State = { export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimpleContext({ name: "GlobalSync", init: () => { + const globalSDK = useGlobalSDK() const [globalStore, setGlobalStore] = createStore<{ ready: boolean - projects: Project[] + project: Project[] + provider: ProviderListResponse + provider_auth: ProviderAuthResponse children: Record }>({ ready: false, - projects: [], + project: [], + provider: { all: [], connected: [], default: {} }, + provider_auth: {}, children: {}, }) + async function bootstrapInstance(directory: string) { + const [store, setStore] = child(directory) + const sdk = createOpencodeClient({ + baseUrl: globalSDK.url, + directory, + }) + const load = { + project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)), + provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)), + path: () => sdk.path.get().then((x) => setStore("path", x.data!)), + agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])), + session: () => + sdk.session.list().then((x) => { + const sessions = (x.data ?? []) + .slice() + .sort((a, b) => a.id.localeCompare(b.id)) + .slice(0, store.limit) + setStore("session", sessions) + }), + status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)), + config: () => sdk.config.get().then((x) => setStore("config", x.data!)), + changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)), + node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)), + } + await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true)) + } + const children: Record>> = {} function child(directory: string) { if (!children[directory]) { setGlobalStore("children", directory, { project: "", + provider: { all: [], connected: [], default: {} }, config: {}, - path: { state: "", config: "", worktree: "", directory: "" }, + path: { state: "", config: "", worktree: "", directory: "", home: "" }, ready: false, agent: [], - provider: [], session: [], session_status: {}, session_diff: {}, @@ -112,32 +115,33 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple changes: [], }) children[directory] = createStore(globalStore.children[directory]) + bootstrapInstance(directory) } return children[directory] } - const sdk = useGlobalSDK() - sdk.event.listen((e) => { + globalSDK.event.listen((e) => { const directory = e.name const event = e.details if (directory === "global") { switch (event.type) { + case "global.disposed": { + bootstrap() + break + } case "project.updated": { - const usedColors = new Set(globalStore.projects.map((p) => p.icon?.color).filter(Boolean) as string[]) - ensureProjectColor(event.properties, sdk, usedColors).then((project) => { - const result = Binary.search(globalStore.projects, project.id, (s) => s.id) - if (result.found) { - setGlobalStore("projects", result.index, reconcile(project)) - return - } - setGlobalStore( - "projects", - produce((draft) => { - draft.splice(result.index, 0, project) - }), - ) - }) + const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id) + if (result.found) { + setGlobalStore("project", result.index, reconcile(event.properties)) + return + } + setGlobalStore( + "project", + produce((draft) => { + draft.splice(result.index, 0, event.properties) + }), + ) break } } @@ -146,6 +150,10 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple const [store, setStore] = child(directory) switch (event.type) { + case "server.instance.disposed": { + bootstrapInstance(directory) + break + } case "session.updated": { const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) if (result.found) { @@ -214,17 +222,28 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple } }) - Promise.all([ - sdk.client.project.list().then(async (x) => { - const filtered = x.data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs) - const usedColors = new Set(filtered.map((p) => p.icon?.color).filter(Boolean) as string[]) - const projects = await Promise.all(filtered.map((p) => ensureProjectColor(p, sdk, usedColors))) - setGlobalStore( - "projects", - projects.sort((a, b) => a.id.localeCompare(b.id)), - ) - }), - ]).then(() => setGlobalStore("ready", true)) + async function bootstrap() { + return Promise.all([ + globalSDK.client.project.list().then(async (x) => { + setGlobalStore( + "project", + x + .data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs) + .sort((a, b) => a.id.localeCompare(b.id)), + ) + }), + globalSDK.client.provider.list().then((x) => { + setGlobalStore("provider", x.data ?? {}) + }), + globalSDK.client.provider.auth().then((x) => { + setGlobalStore("provider_auth", x.data ?? {}) + }), + ]).then(() => setGlobalStore("ready", true)) + } + + onMount(() => { + bootstrap() + }) return { data: globalStore, @@ -232,6 +251,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple return globalStore.ready }, child, + bootstrap, } }, }) diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx index b7d1fabb58..9cafdce961 100644 --- a/packages/desktop/src/context/layout.tsx +++ b/packages/desktop/src/context/layout.tsx @@ -1,9 +1,33 @@ -import { createStore } from "solid-js/store" -import { createMemo, onMount } from "solid-js" +import { createStore, produce } from "solid-js/store" +import { batch, createMemo, onMount } from "solid-js" import { createSimpleContext } from "@opencode-ai/ui/context" import { makePersisted } from "@solid-primitives/storage" import { useGlobalSync } from "./global-sync" import { useGlobalSDK } from "./global-sdk" +import { Project } from "@opencode-ai/sdk/v2" + +const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const + +export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number] + +export function isAvatarColorKey(value: string): value is AvatarColorKey { + return AVATAR_COLOR_KEYS.includes(value as AvatarColorKey) +} + +export function getAvatarColors(key?: string) { + if (key && isAvatarColorKey(key)) { + return { + background: `var(--avatar-background-${key})`, + foreground: `var(--avatar-text-${key})`, + } + } + return { + background: "var(--surface-info-base)", + foreground: "var(--text-base)", + } +} + +type Dialog = "provider" | "model" | "connect" export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({ name: "Layout", @@ -26,9 +50,52 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, }), { - name: "default-layout.v6", + name: "default-layout.v7", }, ) + const [ephemeral, setEphemeral] = createStore<{ + connect: { + provider?: string + state?: "pending" | "complete" | "error" + error?: string + } + dialog: { + open?: Dialog + } + }>({ + connect: {}, + dialog: {}, + }) + const usedColors = new Set() + + function pickAvailableColor(): AvatarColorKey { + const available = AVATAR_COLOR_KEYS.filter((c) => !usedColors.has(c)) + if (available.length === 0) return AVATAR_COLOR_KEYS[Math.floor(Math.random() * AVATAR_COLOR_KEYS.length)] + return available[Math.floor(Math.random() * available.length)] + } + + function enrich(project: { worktree: string; expanded: boolean }) { + const metadata = globalSync.data.project.find((x) => x.worktree === project.worktree) + if (!metadata) return [] + return [ + { + ...project, + ...metadata, + }, + ] + } + + function colorize(project: Project & { expanded: boolean }) { + if (project.icon?.color) return project + const color = pickAvailableColor() + usedColors.add(color) + project.icon = { ...project.icon, color } + globalSdk.client.project.update({ projectID: project.id, icon: { color } }) + return project + } + + const enriched = createMemo(() => store.projects.flatMap(enrich)) + const list = createMemo(() => enriched().flatMap(colorize)) async function loadProjectSessions(directory: string) { const [, setStore] = globalSync.child(directory) @@ -43,30 +110,19 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( onMount(() => { Promise.all( - store.projects.map(({ worktree }) => { - return loadProjectSessions(worktree) + store.projects.map((project) => { + return loadProjectSessions(project.worktree) }), ) }) - function enrich(project: { worktree: string; expanded: boolean }) { - const metadata = globalSync.data.projects.find((x) => x.worktree === project.worktree) - if (!metadata) return [] - return [ - { - ...project, - ...metadata, - }, - ] - } - return { projects: { - list: createMemo(() => store.projects.flatMap(enrich)), + list, open(directory: string) { if (store.projects.find((x) => x.worktree === directory)) return loadProjectSessions(directory) - setStore("projects", (x) => [...x, { worktree: directory, expanded: true }]) + setStore("projects", (x) => [{ worktree: directory, expanded: true }, ...x]) }, close(directory: string) { setStore("projects", (x) => x.filter((x) => x.worktree !== directory)) @@ -129,6 +185,58 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( setStore("review", "state", "tab") }, }, + dialog: { + opened: createMemo(() => ephemeral.dialog?.open), + open(dialog: Dialog) { + batch(() => { + // if (dialog !== "connect") { + // setEphemeral("connect", {}) + // } + setEphemeral("dialog", "open", dialog) + }) + }, + close(dialog: Dialog) { + if (ephemeral.dialog.open === dialog) { + setEphemeral( + produce((state) => { + state.dialog.open = undefined + state.connect = {} + }), + ) + } + }, + connect(provider: string) { + setEphemeral( + produce((state) => { + state.dialog.open = "connect" + state.connect = { provider, state: "pending" } + }), + ) + }, + }, + connect: { + provider: createMemo(() => ephemeral.connect.provider), + state: createMemo(() => ephemeral.connect.state), + complete() { + setEphemeral( + produce((state) => { + state.dialog.open = "model" + state.connect.state = "complete" + }), + ) + }, + error(message: string) { + setEphemeral( + produce((state) => { + state.connect.state = "error" + state.connect.error = message + }), + ) + }, + clear() { + setEphemeral("connect", {}) + }, + }, } }, }) diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx index 8223a36b9c..39fd1f9874 100644 --- a/packages/desktop/src/context/local.tsx +++ b/packages/desktop/src/context/local.tsx @@ -6,6 +6,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context" import { useSDK } from "./sdk" import { useSync } from "./sync" import { base64Encode } from "@opencode-ai/util/encode" +import { useProviders } from "@/hooks/use-providers" export type LocalFile = FileNode & Partial<{ @@ -25,6 +26,7 @@ export type View = LocalFile["view"] export type LocalModel = Omit & { provider: Provider + latest?: boolean } export type ModelKey = { providerID: string; modelID: string } @@ -36,10 +38,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ init: () => { const sdk = useSDK() const sync = useSync() + const providers = useProviders() function isModelValid(model: ModelKey) { - const provider = sync.data.provider.find((x) => x.id === model.providerID) - return !!provider?.models[model.modelID] + const provider = providers.all().find((x) => x.id === model.providerID) + return ( + !!provider?.models[model.modelID] && + providers + .connected() + .map((p) => p.id) + .includes(model.providerID) + ) } function getFirstValidModel(...modelFns: (() => ModelKey | undefined)[]) { @@ -114,7 +123,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }) const list = createMemo(() => - sync.data.provider.flatMap((p) => Object.values(p.models).map((m) => ({ ...m, provider: p }) as LocalModel)), + providers.connected().flatMap((p) => + Object.values(p.models).map((m) => ({ + ...m, + name: m.name.replace("(latest)", "").trim(), + provider: p, + latest: m.name.includes("(latest)"), + })), + ), ) const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID) @@ -134,12 +150,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ return item } } - const provider = sync.data.provider[0] - const model = Object.values(provider.models)[0] - return { - providerID: provider.id, - modelID: model.id, + + for (const p of providers.connected()) { + if (p.id in providers.default()) { + return { + providerID: p.id, + modelID: providers.default()[p.id], + } + } } + + throw new Error("No default model found") }) const currentModel = createMemo(() => { diff --git a/packages/desktop/src/context/session.tsx b/packages/desktop/src/context/session.tsx index 31004811bc..860c1a14f8 100644 --- a/packages/desktop/src/context/session.tsx +++ b/packages/desktop/src/context/session.tsx @@ -62,10 +62,10 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex const userMessages = createMemo(() => messages() .filter((m) => m.role === "user") - .sort((a, b) => b.id.localeCompare(a.id)), + .sort((a, b) => a.id.localeCompare(b.id)), ) const lastUserMessage = createMemo(() => { - return userMessages()?.at(0) + return userMessages()?.at(-1) }) const activeMessage = createMemo(() => { if (!store.messageId) return lastUserMessage() @@ -94,7 +94,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex () => messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage, ) const model = createMemo(() => - last() ? sync.data.provider.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined, + last() ? sync.data.provider.all.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined, ) const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : [])) diff --git a/packages/desktop/src/context/sync.tsx b/packages/desktop/src/context/sync.tsx index 85986c3271..85758c5b6c 100644 --- a/packages/desktop/src/context/sync.tsx +++ b/packages/desktop/src/context/sync.tsx @@ -11,28 +11,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const globalSync = useGlobalSync() const sdk = useSDK() const [store, setStore] = globalSync.child(sdk.directory) - - const load = { - project: () => sdk.client.project.current().then((x) => setStore("project", x.data!.id)), - provider: () => sdk.client.config.providers().then((x) => setStore("provider", x.data!.providers)), - path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)), - agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])), - session: () => - sdk.client.session.list().then((x) => { - const sessions = (x.data ?? []) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)) - .slice(0, store.limit) - setStore("session", sessions) - }), - status: () => sdk.client.session.status().then((x) => setStore("session_status", x.data!)), - config: () => sdk.client.config.get().then((x) => setStore("config", x.data!)), - changes: () => sdk.client.file.status().then((x) => setStore("changes", x.data!)), - node: () => sdk.client.file.list({ path: "/" }).then((x) => setStore("node", x.data!)), - } - - Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true)) - const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/") return { @@ -42,8 +20,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ return store.ready }, get project() { - const match = Binary.search(globalSync.data.projects, store.project, (p) => p.id) - if (match.found) return globalSync.data.projects[match.index] + const match = Binary.search(globalSync.data.project, store.project, (p) => p.id) + if (match.found) return globalSync.data.project[match.index] return undefined }, session: { @@ -78,11 +56,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ }, fetch: async (count = 10) => { setStore("limit", (x) => x + count) - await load.session() + await sdk.client.session.list().then((x) => { + const sessions = (x.data ?? []) + .slice() + .sort((a, b) => a.id.localeCompare(b.id)) + .slice(0, store.limit) + setStore("session", sessions) + }) }, more: createMemo(() => store.session.length >= store.limit), }, - load, absolute, get directory() { return store.path.directory diff --git a/packages/desktop/src/hooks/use-providers.ts b/packages/desktop/src/hooks/use-providers.ts new file mode 100644 index 0000000000..501ff9d0c3 --- /dev/null +++ b/packages/desktop/src/hooks/use-providers.ts @@ -0,0 +1,31 @@ +import { useGlobalSync } from "@/context/global-sync" +import { base64Decode } from "@opencode-ai/util/encode" +import { useParams } from "@solidjs/router" +import { createMemo } from "solid-js" + +export const popularProviders = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"] + +export function useProviders() { + const params = useParams() + const globalSync = useGlobalSync() + const currentDirectory = createMemo(() => base64Decode(params.dir ?? "")) + const providers = createMemo(() => { + if (currentDirectory()) { + const [projectStore] = globalSync.child(currentDirectory()) + return projectStore.provider + } + return globalSync.data.provider + }) + const connected = createMemo(() => providers().all.filter((p) => providers().connected.includes(p.id))) + const paid = createMemo(() => + connected().filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input)), + ) + const popular = createMemo(() => providers().all.filter((p) => popularProviders.includes(p.id))) + return { + all: createMemo(() => providers().all), + default: createMemo(() => providers().default), + popular, + connected, + paid, + } +} diff --git a/packages/desktop/src/pages/home.tsx b/packages/desktop/src/pages/home.tsx index 4aac241e13..205ffd8157 100644 --- a/packages/desktop/src/pages/home.tsx +++ b/packages/desktop/src/pages/home.tsx @@ -38,7 +38,7 @@ export default function Home() {
    - 0}> + 0}>
    Recent projects
    @@ -50,7 +50,7 @@ export default function Home() {
      (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)) .slice(0, 5)} > diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index 3ff3abb0ee..70764292fe 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -1,7 +1,7 @@ -import { createEffect, createMemo, For, Match, ParentProps, Show, Switch, type JSX } from "solid-js" +import { createEffect, createMemo, For, Match, onCleanup, onMount, ParentProps, Show, Switch, type JSX } from "solid-js" import { DateTime } from "luxon" import { A, useNavigate, useParams } from "@solidjs/router" -import { useLayout } from "@/context/layout" +import { useLayout, getAvatarColors } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" import { base64Decode, base64Encode } from "@opencode-ai/util/encode" import { Mark } from "@opencode-ai/ui/logo" @@ -9,6 +9,7 @@ import { Avatar } from "@opencode-ai/ui/avatar" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip } from "@opencode-ai/ui/tooltip" import { Collapsible } from "@opencode-ai/ui/collapsible" @@ -16,9 +17,9 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes" import { getFilename } from "@opencode-ai/util/path" import { Select } from "@opencode-ai/ui/select" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" -import { Session, Project } from "@opencode-ai/sdk/v2/client" +import { Session, Project, ProviderAuthMethod, ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client" import { usePlatform } from "@/context/platform" -import { createStore } from "solid-js/store" +import { createStore, produce } from "solid-js/store" import { DragDropProvider, DragDropSensors, @@ -29,6 +30,18 @@ import { useDragDropContext, } from "@thisbeyond/solid-dnd" import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd" +import { SelectDialog } from "@opencode-ai/ui/select-dialog" +import { Tag } from "@opencode-ai/ui/tag" +import { IconName } from "@opencode-ai/ui/icons/provider" +import { popularProviders, useProviders } from "@/hooks/use-providers" +import { Dialog } from "@opencode-ai/ui/dialog" +import { iife } from "@opencode-ai/util/iife" +import { Link } from "@/components/link" +import { List, ListRef } from "@opencode-ai/ui/list" +import { TextField } from "@opencode-ai/ui/text-field" +import { showToast, Toast } from "@opencode-ai/ui/toast" +import { useGlobalSDK } from "@/context/global-sdk" +import { Spinner } from "@opencode-ai/ui/spinner" export default function Layout(props: ParentProps) { const [store, setStore] = createStore({ @@ -37,6 +50,7 @@ export default function Layout(props: ParentProps) { }) const params = useParams() + const globalSDK = useGlobalSDK() const globalSync = useGlobalSync() const layout = useLayout() const platform = usePlatform() @@ -44,6 +58,7 @@ export default function Layout(props: ParentProps) { const currentDirectory = createMemo(() => base64Decode(params.dir ?? "")) const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? []) const currentSession = createMemo(() => sessions().find((s) => s.id === params.id)) + const providers = useProviders() function navigateToProject(directory: string | undefined) { if (!directory) return @@ -82,12 +97,21 @@ export default function Layout(props: ParentProps) { } } + async function connectProvider() { + layout.dialog.open("provider") + } + createEffect(() => { if (!params.dir || !params.id) return const directory = base64Decode(params.dir) setStore("lastSession", directory, params.id) }) + createEffect(() => { + const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48 + document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`) + }) + function getDraggableId(event: unknown): string | undefined { if (typeof event !== "object" || event === null) return undefined if (!("draggable" in event)) return undefined @@ -156,7 +180,7 @@ export default function Layout(props: ParentProps) {
    @@ -176,7 +200,7 @@ export default function Layout(props: ParentProps) {
    @@ -207,7 +231,7 @@ export default function Layout(props: ParentProps) {
    @@ -465,10 +489,44 @@ export default function Layout(props: ParentProps) {
    + + +
    +
    +
    Getting started
    +
    OpenCode includes free models so you can start immediately.
    +
    Connect any provider to use models, inc. Claude, GPT, Gemini etc.
    +
    + + + +
    +
    + + + + + +
    - + {/* */} + {/* */} + {/* */}
    {props.children}
    + + x?.id} + items={providers.all} + filterKeys={["id", "name"]} + groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")} + sortBy={(a, b) => { + if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) + return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) + return a.name.localeCompare(b.name) + }} + sortGroupsBy={(a, b) => { + if (a.category === "Popular" && b.category !== "Popular") return -1 + if (b.category === "Popular" && a.category !== "Popular") return 1 + return 0 + }} + onSelect={(x) => { + if (!x) return + layout.dialog.connect(x.id) + }} + onOpenChange={(open) => { + if (open) { + layout.dialog.open("provider") + } else { + layout.dialog.close("provider") + } + }} + > + {(i) => ( +
    + + {i.name} + + Recommended + + +
    Connect with Claude Pro/Max or API key
    +
    +
    + )} +
    +
    + + {iife(() => { + const providerID = createMemo(() => layout.connect.provider()!) + const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === providerID())!) + const methods = createMemo( + () => + globalSync.data.provider_auth[providerID()] ?? [ + { + type: "api", + label: "API key", + }, + ], + ) + const [store, setStore] = createStore({ + method: undefined as undefined | ProviderAuthMethod, + authorization: undefined as undefined | ProviderAuthAuthorization, + state: "pending" as undefined | "pending" | "complete" | "error", + error: undefined as string | undefined, + }) + + const methodIndex = createMemo(() => methods().findIndex((x) => x.label === store.method?.label)) + + async function selectMethod(index: number) { + const method = methods()[index] + setStore( + produce((draft) => { + draft.method = method + draft.authorization = undefined + draft.state = undefined + draft.error = undefined + }), + ) + + if (method.type === "oauth") { + setStore("state", "pending") + const start = Date.now() + await globalSDK.client.provider.oauth + .authorize( + { + providerID: providerID(), + method: index, + }, + { throwOnError: true }, + ) + .then((x) => { + const elapsed = Date.now() - start + const delay = 1000 - elapsed + + if (delay > 0) { + setTimeout(() => { + setStore("state", "complete") + setStore("authorization", x.data!) + }, delay) + return + } + setStore("state", "complete") + setStore("authorization", x.data!) + }) + .catch((e) => { + setStore("state", "error") + setStore("error", String(e)) + }) + } + } + + let listRef: ListRef | undefined + function handleKey(e: KeyboardEvent) { + if (e.key === "Enter" && e.target instanceof HTMLInputElement) { + return + } + if (e.key === "Escape") return + listRef?.onKeyDown(e) + } + + onMount(() => { + if (methods().length === 1) { + selectMethod(0) + } + + document.addEventListener("keydown", handleKey) + onCleanup(() => { + document.removeEventListener("keydown", handleKey) + }) + }) + + async function complete() { + await globalSDK.client.global.dispose() + setTimeout(() => { + showToast({ + variant: "success", + icon: "circle-check", + title: `${provider().name} connected`, + description: `${provider().name} models are now available to use.`, + }) + layout.connect.complete() + }, 500) + } + + return ( + { + if (open) { + layout.dialog.open("connect") + } else { + layout.dialog.close("connect") + } + }} + > + + + { + if (methods().length === 1) { + layout.dialog.open("provider") + return + } + if (store.authorization) { + setStore("authorization", undefined) + setStore("method", undefined) + return + } + if (store.method) { + setStore("method", undefined) + return + } + layout.dialog.open("provider") + }} + /> + + + + +
    +
    + +
    + + + Login with Claude Pro/Max + + Connect {provider().name} + +
    +
    +
    + + +
    Select login method for {provider().name}.
    +
    + (listRef = ref)} + items={methods} + key={(m) => m?.label} + onSelect={async (method, index) => { + if (!method) return + selectMethod(index) + }} + > + {(i) => ( +
    +
    + + {i.label} +
    + )} + +
    + + +
    +
    + + Authorization in progress... +
    +
    +
    + +
    +
    + + Authorization failed: {store.error} +
    +
    +
    + + {iife(() => { + const [formStore, setFormStore] = createStore({ + value: "", + error: undefined as string | undefined, + }) + + async function handleSubmit(e: SubmitEvent) { + e.preventDefault() + + const form = e.currentTarget as HTMLFormElement + const formData = new FormData(form) + const apiKey = formData.get("apiKey") as string + + if (!apiKey?.trim()) { + setFormStore("error", "API key is required") + return + } + + setFormStore("error", undefined) + await globalSDK.client.auth.set({ + providerID: providerID(), + auth: { + type: "api", + key: apiKey, + }, + }) + await complete() + } + + return ( +
    + + +
    +
    + OpenCode Zen gives you access to a curated set of reliable optimized models for + coding agents. +
    +
    + With a single API key you’ll get access to models such as Claude, GPT, Gemini, + GLM and more. +
    +
    + Visit{" "} + + opencode.ai/zen + {" "} + to collect your API key. +
    +
    +
    + +
    + Enter your {provider().name} API key to connect your account and use{" "} + {provider().name} models in OpenCode. +
    +
    +
    + + + + +
    + ) + })} +
    + + + + {iife(() => { + const [formStore, setFormStore] = createStore({ + value: "", + error: undefined as string | undefined, + }) + + onMount(() => { + if (store.authorization?.method === "code" && store.authorization?.url) { + platform.openLink(store.authorization.url) + } + }) + + async function handleSubmit(e: SubmitEvent) { + e.preventDefault() + + const form = e.currentTarget as HTMLFormElement + const formData = new FormData(form) + const code = formData.get("code") as string + + if (!code?.trim()) { + setFormStore("error", "Authorization code is required") + return + } + + setFormStore("error", undefined) + const { error } = await globalSDK.client.provider.oauth.callback({ + providerID: providerID(), + method: methodIndex(), + code, + }) + if (!error) { + await complete() + return + } + setFormStore("error", "Invalid authorization code") + } + + return ( +
    +
    + Visit this link to collect your + authorization code to connect your account and use {provider().name} models in + OpenCode. +
    +
    + + + +
    + ) + })} +
    + + {iife(() => { + const code = createMemo(() => { + const instructions = store.authorization?.instructions + if (instructions?.includes(":")) { + return instructions?.split(":")[1]?.trim() + } + return instructions + }) + + onMount(async () => { + const result = await globalSDK.client.provider.oauth.callback({ + providerID: providerID(), + method: methodIndex(), + }) + if (result.error) { + // TODO: show error + layout.dialog.close("connect") + return + } + await complete() + }) + + return ( +
    +
    + Visit this link and enter the code + below to connect your account and use {provider().name} models in OpenCode. +
    + +
    + + Waiting for authorization... +
    +
    + ) + })} +
    +
    +
    + +
    +
    + +
    + ) + })} +
    + ) } diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index 890401723e..5dae4ce55d 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -415,7 +415,6 @@ export default function Page() { messages={session.messages.user()} current={session.messages.active()} onMessageSelect={session.messages.setActive} - working={session.working()} wide={wide()} /> ( OpenCode - - {assets} diff --git a/packages/enterprise/src/routes/index.tsx b/packages/enterprise/src/routes/index.tsx new file mode 100644 index 0000000000..5a743b0396 --- /dev/null +++ b/packages/enterprise/src/routes/index.tsx @@ -0,0 +1,3 @@ +export default function () { + return
    Hello World
    +} diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index 15a36b2ff4..7cce15906f 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -23,6 +23,8 @@ import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/precis import { Diff as SSRDiff } from "@opencode-ai/ui/diff-ssr" import { clientOnly } from "@solidjs/start" import { type IconName } from "@opencode-ai/ui/icons/provider" +import { Meta } from "@solidjs/meta" +import { Base64 } from "js-base64" const ClientOnlyDiff = clientOnly(() => import("@opencode-ai/ui/diff").then((m) => ({ default: m.Diff }))) @@ -41,6 +43,7 @@ const getData = query(async (shareID) => { const data = await Share.data(shareID) const result: { sessionID: string + shareID: string session: Session[] session_diff: { [sessionID: string]: FileDiff[] @@ -65,6 +68,7 @@ const getData = query(async (shareID) => { } } = { sessionID: share.sessionID, + shareID, session: [], session_diff: { [share.sessionID]: [], @@ -134,10 +138,18 @@ const getData = query(async (shareID) => { export default function () { const params = useParams() - const data = createAsync(async () => { - if (!params.shareID) throw new Error("Missing shareID") - return getData(params.shareID) - }) + const data = createAsync( + async () => { + if (!params.shareID) throw new Error("Missing shareID") + const now = Date.now() + const data = getData(params.shareID) + console.log("getData", Date.now() - now) + return data + }, + { + deferStream: true, + }, + ) createEffect(() => { console.log(data()) @@ -153,244 +165,277 @@ export default function () { ) }} > + {(data) => { const match = createMemo(() => Binary.search(data().session, data().sessionID, (s) => s.id)) if (!match().found) throw new Error(`Session ${data().sessionID} not found`) const info = createMemo(() => data().session[match().index]) + const ogImage = createMemo(() => { + const models = new Set() + const messages = data().message[data().sessionID] ?? [] + for (const msg of messages) { + if (msg.role === "assistant" && msg.modelID) { + models.add(msg.modelID) + } + } + const modelIDs = Array.from(models) + const encodedTitle = encodeURIComponent(Base64.encode(encodeURIComponent(info().title.substring(0, 700)))) + let modelParam: string + if (modelIDs.length === 1) { + modelParam = modelIDs[0] + } else if (modelIDs.length === 2) { + modelParam = encodeURIComponent(`${modelIDs[0]} & ${modelIDs[1]}`) + } else if (modelIDs.length > 2) { + modelParam = encodeURIComponent(`${modelIDs[0]} & ${modelIDs.length - 1} others`) + } else { + modelParam = "unknown" + } + const version = `v${info().version}` + return `https://social-cards.sst.dev/opencode-share/${encodedTitle}.png?model=${modelParam}&version=${version}&id=${data().shareID}` + }) return ( - - - {iife(() => { - const [store, setStore] = createStore({ - messageId: undefined as string | undefined, - }) - const messages = createMemo(() => - data().sessionID - ? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort( - (a, b) => b.time.created - a.time.created, - ) - : [], - ) - const firstUserMessage = createMemo(() => messages().at(0)) - const activeMessage = createMemo( - () => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(), - ) - function setActiveMessage(message: UserMessage | undefined) { - if (message) { - setStore("messageId", message.id) - } else { - setStore("messageId", undefined) + <> + + + + + + {iife(() => { + const [store, setStore] = createStore({ + messageId: undefined as string | undefined, + }) + const messages = createMemo(() => + data().sessionID + ? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort( + (a, b) => a.time.created - b.time.created, + ) + : [], + ) + const firstUserMessage = createMemo(() => messages().at(0)) + const activeMessage = createMemo( + () => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(), + ) + function setActiveMessage(message: UserMessage | undefined) { + if (message) { + setStore("messageId", message.id) + } else { + setStore("messageId", undefined) + } } - } - const provider = createMemo(() => activeMessage()?.model?.providerID) - const modelID = createMemo(() => activeMessage()?.model?.modelID) - const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID())) - const diffs = createMemo(() => { - const diffs = data().session_diff[data().sessionID] ?? [] - const preloaded = data().session_diff_preload[data().sessionID] ?? [] - return diffs.map((diff) => ({ - ...diff, - preloaded: preloaded.find((d) => d.newFile.name === diff.file), - })) - }) - const splitDiffs = createMemo(() => { - const diffs = data().session_diff[data().sessionID] ?? [] - const preloaded = data().session_diff_preload_split[data().sessionID] ?? [] - return diffs.map((diff) => ({ - ...diff, - preloaded: preloaded.find((d) => d.newFile.name === diff.file), - })) - }) + const provider = createMemo(() => activeMessage()?.model?.providerID) + const modelID = createMemo(() => activeMessage()?.model?.modelID) + const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID())) + const diffs = createMemo(() => { + const diffs = data().session_diff[data().sessionID] ?? [] + const preloaded = data().session_diff_preload[data().sessionID] ?? [] + return diffs.map((diff) => ({ + ...diff, + preloaded: preloaded.find((d) => d.newFile.name === diff.file), + })) + }) + const splitDiffs = createMemo(() => { + const diffs = data().session_diff[data().sessionID] ?? [] + const preloaded = data().session_diff_preload_split[data().sessionID] ?? [] + return diffs.map((diff) => ({ + ...diff, + preloaded: preloaded.find((d) => d.newFile.name === diff.file), + })) + }) - const title = () => ( -
    -
    -
    - -
    v{info().version}
    + const title = () => ( +
    +
    +
    + +
    v{info().version}
    +
    +
    + +
    {model()?.name ?? modelID()}
    +
    +
    + {DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")} +
    -
    - -
    {model()?.name ?? modelID()}
    +
    {info().title}
    +
    + ) + + const turns = () => ( +
    +
    {title()}
    +
    + + {(message) => ( + + )} +
    -
    - {DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")} +
    +
    -
    {info().title}
    -
    - ) + ) - const turns = () => ( -
    -
    {title()}
    -
    - - {(message) => ( - diffs().length === 0) + + return ( + - ) - - const wide = createMemo(() => diffs().length === 0) - - return ( -
    -
    - -
    - - -
    -
    -
    -
    + +
    + +
    1, - "px-6": !wide() && messages().length === 1, + "@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full": true, + "mx-auto max-w-146": !wide(), }} > - {title()} -
    -
    - - 1 ? "pr-6 pl-18" : "px-6"), +
    1, + "px-6": !wide() && messages().length === 1, }} > -
    - -
    - -
    -
    - 0}> - -
    - -
    +
    + + 1 + ? "pr-6 pl-18" + : "px-6"), + }} + > +
    + +
    +
    -
    -
    -
    - - 0}> - - - - Session - - - {diffs().length} Files Changed - - - - {turns()} - - - - - -
    - {turns()}
    -
    -
    + 0}> + +
    + +
    +
    +
    +
    + + 0}> + + + + Session + + + {diffs().length} Files Changed + + + + {turns()} + + + + + +
    + {turns()} +
    +
    +
    +
    -
    - ) - })} - - + ) + })} + + + ) }} diff --git a/packages/enterprise/vite.config.ts b/packages/enterprise/vite.config.ts index fb51d750c1..11ca1729df 100644 --- a/packages/enterprise/vite.config.ts +++ b/packages/enterprise/vite.config.ts @@ -18,7 +18,14 @@ const nitroConfig: any = (() => { })() export default defineConfig({ - plugins: [tailwindcss(), solidStart() as PluginOption, nitro(nitroConfig)], + plugins: [ + tailwindcss(), + solidStart() as PluginOption, + nitro({ + ...nitroConfig, + baseURL: process.env.OPENCODE_BASE_URL, + }), + ], server: { host: "0.0.0.0", allowedHosts: true, diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index ed884ec102..e7cb19debd 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.0.141" +version = "1.0.150" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/sst/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.141/opencode-darwin-arm64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.150/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.141/opencode-darwin-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.150/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.141/opencode-linux-arm64.tar.gz" +archive = "https://github.com/sst/opencode/releases/download/v1.0.150/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.141/opencode-linux-x64.tar.gz" +archive = "https://github.com/sst/opencode/releases/download/v1.0.150/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.141/opencode-windows-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.150/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index f0033ade05..591dcfb3c4 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.0.141", + "version": "1.0.150", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/Dockerfile b/packages/opencode/Dockerfile index fbbeacf044..99f593581c 100644 --- a/packages/opencode/Dockerfile +++ b/packages/opencode/Dockerfile @@ -4,7 +4,7 @@ FROM alpine # On ephemeral containers, the cache is not useful ARG BUN_RUNTIME_TRANSPILER_CACHE_PATH=0 ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=${BUN_RUNTIME_TRANSPILER_CACHE_PATH} -RUN apk add libgcc libstdc++ +RUN apk add libgcc libstdc++ ripgrep ADD ./dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode RUN opencode --version ENTRYPOINT ["opencode"] diff --git a/packages/opencode/package.json b/packages/opencode/package.json index ca3af5810b..972568983c 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.0.141", + "version": "1.0.150", "name": "opencode", "type": "module", "private": true, @@ -70,9 +70,9 @@ "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", - "@openrouter/ai-sdk-provider": "1.2.8", - "@opentui/core": "0.1.59", - "@opentui/solid": "0.1.59", + "@openrouter/ai-sdk-provider": "1.5.2", + "@opentui/core": "0.0.0-20251211-4403a69a", + "@opentui/solid": "0.0.0-20251211-4403a69a", "@parcel/watcher": "2.5.1", "@pierre/precision-diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index ae4798d963..a1e45e1d21 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -28,7 +28,7 @@ import { Config } from "@/config/config" import { Todo } from "@/session/todo" import { z } from "zod" import { LoadAPIKeyError } from "ai" -import type { OpencodeClient } from "@opencode-ai/sdk/v2" +import type { OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2" export namespace ACP { const log = Log.create({ service: "acp-agent" }) @@ -386,7 +386,7 @@ export namespace ACP { log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length }) - const load = await this.loadSession({ + const load = await this.loadSessionMode({ cwd: directory, mcpServers: params.mcpServers, sessionId, @@ -412,6 +412,242 @@ export namespace ACP { } async loadSession(params: LoadSessionRequest) { + const directory = params.cwd + const sessionId = params.sessionId + + try { + const model = await defaultModel(this.config, directory) + + // Store ACP session state + const state = await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model) + + log.info("load_session", { sessionId, mcpServers: params.mcpServers.length }) + + const mode = await this.loadSessionMode({ + cwd: directory, + mcpServers: params.mcpServers, + sessionId, + }) + + this.setupEventSubscriptions(state) + + // Replay session history + const messages = await this.sdk.session + .messages( + { + sessionID: sessionId, + directory, + }, + { throwOnError: true }, + ) + .then((x) => x.data) + .catch((err) => { + log.error("unexpected error when fetching message", { error: err }) + return undefined + }) + + for (const msg of messages ?? []) { + log.debug("replay message", msg) + await this.processMessage(msg) + } + + return mode + } catch (e) { + const error = MessageV2.fromError(e, { + providerID: this.config.defaultModel?.providerID ?? "unknown", + }) + if (LoadAPIKeyError.isInstance(error)) { + throw RequestError.authRequired() + } + throw e + } + } + + private async processMessage(message: SessionMessageResponse) { + log.debug("process message", message) + if (message.info.role !== "assistant" && message.info.role !== "user") return + const sessionId = message.info.sessionID + + for (const part of message.parts) { + if (part.type === "tool") { + switch (part.state.status) { + case "pending": + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call", + toolCallId: part.callID, + title: part.tool, + kind: toToolKind(part.tool), + status: "pending", + locations: [], + rawInput: {}, + }, + }) + .catch((err) => { + log.error("failed to send tool pending to ACP", { error: err }) + }) + break + case "running": + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: part.callID, + status: "in_progress", + locations: toLocations(part.tool, part.state.input), + rawInput: part.state.input, + }, + }) + .catch((err) => { + log.error("failed to send tool in_progress to ACP", { error: err }) + }) + break + case "completed": + const kind = toToolKind(part.tool) + const content: ToolCallContent[] = [ + { + type: "content", + content: { + type: "text", + text: part.state.output, + }, + }, + ] + + if (kind === "edit") { + const input = part.state.input + const filePath = typeof input["filePath"] === "string" ? input["filePath"] : "" + const oldText = typeof input["oldString"] === "string" ? input["oldString"] : "" + const newText = + typeof input["newString"] === "string" + ? input["newString"] + : typeof input["content"] === "string" + ? input["content"] + : "" + content.push({ + type: "diff", + path: filePath, + oldText, + newText, + }) + } + + if (part.tool === "todowrite") { + const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output)) + if (parsedTodos.success) { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "plan", + entries: parsedTodos.data.map((todo) => { + const status: PlanEntry["status"] = + todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"]) + return { + priority: "medium", + status, + content: todo.content, + } + }), + }, + }) + .catch((err) => { + log.error("failed to send session update for todo", { error: err }) + }) + } else { + log.error("failed to parse todo output", { error: parsedTodos.error }) + } + } + + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: part.callID, + status: "completed", + kind, + content, + title: part.state.title, + rawOutput: { + output: part.state.output, + metadata: part.state.metadata, + }, + }, + }) + .catch((err) => { + log.error("failed to send tool completed to ACP", { error: err }) + }) + break + case "error": + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: part.callID, + status: "failed", + content: [ + { + type: "content", + content: { + type: "text", + text: part.state.error, + }, + }, + ], + rawOutput: { + error: part.state.error, + }, + }, + }) + .catch((err) => { + log.error("failed to send tool error to ACP", { error: err }) + }) + break + } + } else if (part.type === "text") { + if (part.text) { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk", + content: { + type: "text", + text: part.text, + }, + }, + }) + .catch((err) => { + log.error("failed to send text to ACP", { error: err }) + }) + } + } else if (part.type === "reasoning") { + if (part.text) { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "agent_thought_chunk", + content: { + type: "text", + text: part.text, + }, + }, + }) + .catch((err) => { + log.error("failed to send reasoning to ACP", { error: err }) + }) + } + } + } + } + + private async loadSessionMode(params: LoadSessionRequest) { const directory = params.cwd const model = await defaultModel(this.config, directory) const sessionId = params.sessionId diff --git a/packages/opencode/src/acp/session.ts b/packages/opencode/src/acp/session.ts index 6658e42037..70b6583470 100644 --- a/packages/opencode/src/acp/session.ts +++ b/packages/opencode/src/acp/session.ts @@ -40,6 +40,37 @@ export class ACPSessionManager { return state } + async load( + sessionId: string, + cwd: string, + mcpServers: McpServer[], + model?: ACPSessionState["model"], + ): Promise { + const session = await this.sdk.session + .get( + { + sessionID: sessionId, + directory: cwd, + }, + { throwOnError: true }, + ) + .then((x) => x.data!) + + const resolvedModel = model + + const state: ACPSessionState = { + id: sessionId, + cwd, + mcpServers, + createdAt: new Date(session.time.created), + model: resolvedModel, + } + log.info("loading_session", { state }) + + this.sessions.set(sessionId, state) + return state + } + get(sessionId: string): ACPSessionState { const session = this.sessions.get(sessionId) if (!session) { diff --git a/packages/opencode/src/bus/index.ts b/packages/opencode/src/bus/index.ts index b6ab73e511..edb093f197 100644 --- a/packages/opencode/src/bus/index.ts +++ b/packages/opencode/src/bus/index.ts @@ -7,7 +7,13 @@ import { GlobalBus } from "./global" export namespace Bus { const log = Log.create({ service: "bus" }) type Subscription = (event: any) => void - const disposedEventType = "server.instance.disposed" + + export const InstanceDisposed = BusEvent.define( + "server.instance.disposed", + z.object({ + directory: z.string(), + }), + ) const state = Instance.state( () => { @@ -21,7 +27,7 @@ export namespace Bus { const wildcard = entry.subscriptions.get("*") if (!wildcard) return const event = { - type: disposedEventType, + type: InstanceDisposed.type, properties: { directory: Instance.directory, }, @@ -32,13 +38,6 @@ export namespace Bus { }, ) - export const InstanceDisposed = BusEvent.define( - disposedEventType, - z.object({ - directory: z.string(), - }), - ) - export async function publish( def: Definition, properties: z.output, diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index 61fe4e5bde..658329fb6e 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -10,6 +10,154 @@ import { Config } from "../../config/config" import { Global } from "../../global" import { Plugin } from "../../plugin" import { Instance } from "../../project/instance" +import type { Hooks } from "@opencode-ai/plugin" + +type PluginAuth = NonNullable + +/** + * Handle plugin-based authentication flow. + * Returns true if auth was handled, false if it should fall through to default handling. + */ +async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): Promise { + let index = 0 + if (plugin.auth.methods.length > 1) { + const method = await prompts.select({ + message: "Login method", + options: [ + ...plugin.auth.methods.map((x, index) => ({ + label: x.label, + value: index.toString(), + })), + ], + }) + if (prompts.isCancel(method)) throw new UI.CancelledError() + index = parseInt(method) + } + const method = plugin.auth.methods[index] + + // Handle prompts for all auth types + await new Promise((resolve) => setTimeout(resolve, 10)) + const inputs: Record = {} + if (method.prompts) { + for (const prompt of method.prompts) { + if (prompt.condition && !prompt.condition(inputs)) { + continue + } + if (prompt.type === "select") { + const value = await prompts.select({ + message: prompt.message, + options: prompt.options, + }) + if (prompts.isCancel(value)) throw new UI.CancelledError() + inputs[prompt.key] = value + } else { + const value = await prompts.text({ + message: prompt.message, + placeholder: prompt.placeholder, + validate: prompt.validate ? (v) => prompt.validate!(v ?? "") : undefined, + }) + if (prompts.isCancel(value)) throw new UI.CancelledError() + inputs[prompt.key] = value + } + } + } + + if (method.type === "oauth") { + const authorize = await method.authorize(inputs) + + if (authorize.url) { + prompts.log.info("Go to: " + authorize.url) + } + + if (authorize.method === "auto") { + if (authorize.instructions) { + prompts.log.info(authorize.instructions) + } + const spinner = prompts.spinner() + spinner.start("Waiting for authorization...") + const result = await authorize.callback() + if (result.type === "failed") { + spinner.stop("Failed to authorize", 1) + } + if (result.type === "success") { + const saveProvider = result.provider ?? provider + if ("refresh" in result) { + const { type: _, provider: __, refresh, access, expires, ...extraFields } = result + await Auth.set(saveProvider, { + type: "oauth", + refresh, + access, + expires, + ...extraFields, + }) + } + if ("key" in result) { + await Auth.set(saveProvider, { + type: "api", + key: result.key, + }) + } + spinner.stop("Login successful") + } + } + + if (authorize.method === "code") { + const code = await prompts.text({ + message: "Paste the authorization code here: ", + validate: (x) => (x && x.length > 0 ? undefined : "Required"), + }) + if (prompts.isCancel(code)) throw new UI.CancelledError() + const result = await authorize.callback(code) + if (result.type === "failed") { + prompts.log.error("Failed to authorize") + } + if (result.type === "success") { + const saveProvider = result.provider ?? provider + if ("refresh" in result) { + const { type: _, provider: __, refresh, access, expires, ...extraFields } = result + await Auth.set(saveProvider, { + type: "oauth", + refresh, + access, + expires, + ...extraFields, + }) + } + if ("key" in result) { + await Auth.set(saveProvider, { + type: "api", + key: result.key, + }) + } + prompts.log.success("Login successful") + } + } + + prompts.outro("Done") + return true + } + + if (method.type === "api") { + if (method.authorize) { + const result = await method.authorize(inputs) + if (result.type === "failed") { + prompts.log.error("Failed to authorize") + } + if (result.type === "success") { + const saveProvider = result.provider ?? provider + await Auth.set(saveProvider, { + type: "api", + key: result.key, + }) + prompts.log.success("Login successful") + } + prompts.outro("Done") + return true + } + } + + return false +} export const AuthCommand = cmd({ command: "auth", @@ -160,142 +308,8 @@ export const AuthLoginCommand = cmd({ const plugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider)) if (plugin && plugin.auth) { - let index = 0 - if (plugin.auth.methods.length > 1) { - const method = await prompts.select({ - message: "Login method", - options: [ - ...plugin.auth.methods.map((x, index) => ({ - label: x.label, - value: index.toString(), - })), - ], - }) - if (prompts.isCancel(method)) throw new UI.CancelledError() - index = parseInt(method) - } - const method = plugin.auth.methods[index] - - // Handle prompts for all auth types - await new Promise((resolve) => setTimeout(resolve, 10)) - const inputs: Record = {} - if (method.prompts) { - for (const prompt of method.prompts) { - if (prompt.condition && !prompt.condition(inputs)) { - continue - } - if (prompt.type === "select") { - const value = await prompts.select({ - message: prompt.message, - options: prompt.options, - }) - if (prompts.isCancel(value)) throw new UI.CancelledError() - inputs[prompt.key] = value - } else { - const value = await prompts.text({ - message: prompt.message, - placeholder: prompt.placeholder, - validate: prompt.validate ? (v) => prompt.validate!(v ?? "") : undefined, - }) - if (prompts.isCancel(value)) throw new UI.CancelledError() - inputs[prompt.key] = value - } - } - } - - if (method.type === "oauth") { - const authorize = await method.authorize(inputs) - - if (authorize.url) { - prompts.log.info("Go to: " + authorize.url) - } - - if (authorize.method === "auto") { - if (authorize.instructions) { - prompts.log.info(authorize.instructions) - } - const spinner = prompts.spinner() - spinner.start("Waiting for authorization...") - const result = await authorize.callback() - if (result.type === "failed") { - spinner.stop("Failed to authorize", 1) - } - if (result.type === "success") { - const saveProvider = result.provider ?? provider - if ("refresh" in result) { - const { type: _, provider: __, refresh, access, expires, ...extraFields } = result - await Auth.set(saveProvider, { - type: "oauth", - refresh, - access, - expires, - ...extraFields, - }) - } - if ("key" in result) { - await Auth.set(saveProvider, { - type: "api", - key: result.key, - }) - } - spinner.stop("Login successful") - } - } - - if (authorize.method === "code") { - const code = await prompts.text({ - message: "Paste the authorization code here: ", - validate: (x) => (x && x.length > 0 ? undefined : "Required"), - }) - if (prompts.isCancel(code)) throw new UI.CancelledError() - const result = await authorize.callback(code) - if (result.type === "failed") { - prompts.log.error("Failed to authorize") - } - if (result.type === "success") { - const saveProvider = result.provider ?? provider - if ("refresh" in result) { - const { type: _, provider: __, refresh, access, expires, ...extraFields } = result - await Auth.set(saveProvider, { - type: "oauth", - refresh, - access, - expires, - ...extraFields, - }) - } - if ("key" in result) { - await Auth.set(saveProvider, { - type: "api", - key: result.key, - }) - } - prompts.log.success("Login successful") - } - } - - prompts.outro("Done") - return - } - - if (method.type === "api") { - if (method.authorize) { - const result = await method.authorize(inputs) - if (result.type === "failed") { - prompts.log.error("Failed to authorize") - } - if (result.type === "success") { - const saveProvider = result.provider ?? provider - await Auth.set(saveProvider, { - type: "api", - key: result.key, - }) - prompts.log.success("Login successful") - } - prompts.outro("Done") - return - } - } + const handled = await handlePluginAuth({ auth: plugin.auth }, provider) + if (handled) return } if (provider === "other") { @@ -306,6 +320,14 @@ export const AuthLoginCommand = cmd({ if (prompts.isCancel(provider)) throw new UI.CancelledError() provider = provider.replace(/^@ai-sdk\//, "") if (prompts.isCancel(provider)) throw new UI.CancelledError() + + // Check if a plugin provides auth for this custom provider + const customPlugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider)) + if (customPlugin && customPlugin.auth) { + const handled = await handlePluginAuth({ auth: customPlugin.auth }, provider) + if (handled) return + } + prompts.log.warn( `This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`, ) diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index 99bbb8cc49..55d9fb19de 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -124,6 +124,8 @@ type IssueQueryResponse = { } } +const AGENT_USERNAME = "opencode-agent[bot]" +const AGENT_REACTION = "eyes" const WORKFLOW_FILE = ".github/workflows/opencode.yml" export const GithubCommand = cmd({ @@ -403,12 +405,12 @@ export const GithubRunCommand = cmd({ let appToken: string let octoRest: Octokit let octoGraph: typeof graphql - let commentId: number let gitConfig: string let session: { id: string; title: string; version: string } let shareId: string | undefined let exitCode = 0 type PromptFiles = Awaited>["promptFiles"] + const triggerCommentId = payload.comment.id try { const actionToken = isMock ? args.token! : await getOidcToken() @@ -422,8 +424,7 @@ export const GithubRunCommand = cmd({ await configureGit(appToken) await assertPermissions() - const comment = await createComment() - commentId = comment.data.id + await addReaction() // Setup opencode session const repoData = await fetchRepo() @@ -455,7 +456,8 @@ export const GithubRunCommand = cmd({ await pushToLocalBranch(summary, uncommittedChanges) } const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`)) - await updateComment(`${response}${footer({ image: !hasShared })}`) + await createComment(`${response}${footer({ image: !hasShared })}`) + await removeReaction() } // Fork PR else { @@ -469,7 +471,8 @@ export const GithubRunCommand = cmd({ await pushToForkBranch(summary, prData, uncommittedChanges) } const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`)) - await updateComment(`${response}${footer({ image: !hasShared })}`) + await createComment(`${response}${footer({ image: !hasShared })}`) + await removeReaction() } } // Issue @@ -489,9 +492,11 @@ export const GithubRunCommand = cmd({ summary, `${response}\n\nCloses #${issueId}${footer({ image: true })}`, ) - await updateComment(`Created PR #${pr}${footer({ image: true })}`) + await createComment(`Created PR #${pr}${footer({ image: true })}`) + await removeReaction() } else { - await updateComment(`${response}${footer({ image: true })}`) + await createComment(`${response}${footer({ image: true })}`) + await removeReaction() } } } catch (e: any) { @@ -503,7 +508,8 @@ export const GithubRunCommand = cmd({ } else if (e instanceof Error) { msg = e.message } - await updateComment(`${msg}${footer()}`) + await createComment(`${msg}${footer()}`) + await removeReaction() core.setFailed(msg) // Also output the clean error message for the action to capture //core.setOutput("prepare_error", e.message); @@ -808,8 +814,8 @@ export const GithubRunCommand = cmd({ await $`git config --local --unset-all ${config}` await $`git config --local ${config} "AUTHORIZATION: basic ${newCredentials}"` - await $`git config --global user.name "opencode-agent[bot]"` - await $`git config --global user.email "opencode-agent[bot]@users.noreply.github.com"` + await $`git config --global user.name "${AGENT_USERNAME}"` + await $`git config --global user.email "${AGENT_USERNAME}@users.noreply.github.com"` } async function restoreGitConfig() { @@ -931,24 +937,42 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"` if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`) } - async function createComment() { + async function addReaction() { + console.log("Adding reaction...") + return await octoRest.rest.reactions.createForIssueComment({ + owner, + repo, + comment_id: triggerCommentId, + content: AGENT_REACTION, + }) + } + + async function removeReaction() { + console.log("Removing reaction...") + const reactions = await octoRest.rest.reactions.listForIssueComment({ + owner, + repo, + comment_id: triggerCommentId, + content: AGENT_REACTION, + }) + + const eyesReaction = reactions.data.find((r) => r.user?.login === AGENT_USERNAME) + if (!eyesReaction) return + + await octoRest.rest.reactions.deleteForIssueComment({ + owner, + repo, + comment_id: triggerCommentId, + reaction_id: eyesReaction.id, + }) + } + + async function createComment(body: string) { console.log("Creating comment...") return await octoRest.rest.issues.createComment({ owner, repo, issue_number: issueId, - body: `[Working...](${runUrl})`, - }) - } - - async function updateComment(body: string) { - if (!commentId) return - - console.log("Updating comment...") - return await octoRest.rest.issues.updateComment({ - owner, - repo, - comment_id: commentId, body, }) } @@ -1029,7 +1053,7 @@ query($owner: String!, $repo: String!, $number: Int!) { const comments = (issue.comments?.nodes || []) .filter((c) => { const id = parseInt(c.databaseId) - return id !== commentId && id !== payload.comment.id + return id !== payload.comment.id }) .map((c) => ` - ${c.author.login} at ${c.createdAt}: ${c.body}`) @@ -1148,7 +1172,7 @@ query($owner: String!, $repo: String!, $number: Int!) { const comments = (pr.comments?.nodes || []) .filter((c) => { const id = parseInt(c.databaseId) - return id !== commentId && id !== payload.comment.id + return id !== payload.comment.id }) .map((c) => `- ${c.author.login} at ${c.createdAt}: ${c.body}`) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 226de4796b..28e8411224 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -107,7 +107,9 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise { return ( - }> + } + > @@ -144,7 +146,7 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise { if (route.data.type === "home") { - renderer.setTerminalTitle("opencode") + renderer.setTerminalTitle("OpenCode") return } if (route.data.type === "session") { const session = sync.session.get(route.data.sessionID) if (!session || SessionApi.isDefaultTitle(session.title)) { - renderer.setTerminalTitle("opencode") + renderer.setTerminalTitle("OpenCode") return } // Truncate title to 40 chars max const title = session.title.length > 40 ? session.title.slice(0, 37) + "..." : session.title - renderer.setTerminalTitle(`oc | ${title}`) + renderer.setTerminalTitle(`OC | ${title}`) } }) @@ -536,7 +538,12 @@ function App() { ) } -function ErrorComponent(props: { error: Error; reset: () => void; onExit: () => Promise }) { +function ErrorComponent(props: { + error: Error + reset: () => void + onExit: () => Promise + mode?: "dark" | "light" +}) { const term = useTerminalDimensions() useKeyboard((evt) => { if (evt.ctrl && evt.name === "c") { @@ -547,6 +554,15 @@ function ErrorComponent(props: { error: Error; reset: () => void; onExit: () => const issueURL = new URL("https://github.com/sst/opencode/issues/new?template=bug-report.yml") + // Choose safe fallback colors per mode since theme context may not be available + const isLight = props.mode === "light" + const colors = { + bg: isLight ? "#ffffff" : "#0a0a0a", + text: isLight ? "#1a1a1a" : "#eeeeee", + muted: isLight ? "#8a8a8a" : "#808080", + primary: isLight ? "#3b7dd8" : "#fab283", + } + if (props.error.message) { issueURL.searchParams.set("title", `opentui: fatal: ${props.error.message}`) } @@ -567,27 +583,31 @@ function ErrorComponent(props: { error: Error; reset: () => void; onExit: () => } return ( - + - Please report an issue. - - Copy issue URL (exception info pre-filled) + + Please report an issue. + + + + Copy issue URL (exception info pre-filled) + - {copied() && Successfully copied} + {copied() && Successfully copied} - A fatal error occurred! - - Reset TUI + A fatal error occurred! + + Reset TUI - - Exit + + Exit - {props.error.stack} + {props.error.stack} - {props.error.message} + {props.error.message} ) } diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index 0ea4cbd68a..38fd574585 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -199,7 +199,7 @@ export function DialogModel(props: { providerID?: string }) { ) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index 0af7034db9..5cc114f92f 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -122,7 +122,9 @@ function AutoMethod(props: AutoMethodProps) { return ( - {props.title} + + {props.title} + esc @@ -198,7 +200,7 @@ function ApiMethod(props: ApiMethodProps) { OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API key. - + Go to https://opencode.ai/zen to get a key diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 9610ca6d3a..f5e0efa493 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -8,6 +8,7 @@ import { Keybind } from "@/util/keybind" import { useTheme } from "../context/theme" import { useSDK } from "../context/sdk" import { DialogSessionRename } from "./dialog-session-rename" +import "opentui-spinner/solid" export function DialogSessionList() { const dialog = useDialog() @@ -22,6 +23,8 @@ export function DialogSessionList() { const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined)) + const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] + const options = createMemo(() => { const today = new Date().toDateString() return sync.data.session @@ -34,12 +37,15 @@ export function DialogSessionList() { category = "Today" } const isDeleting = toDelete() === x.id + const status = sync.data.session_status[x.id] + const isWorking = status?.type === "busy" return { title: isDeleting ? `Press ${deleteKeybind} again to confirm` : x.title, bg: isDeleting ? theme.error : undefined, value: x.id, category, footer: Locale.time(x.time.updated), + gutter: isWorking ? : undefined, } }) .slice(0, 150) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx index f3ce4d4dea..4e485b0338 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx @@ -19,7 +19,7 @@ export function DialogStatus() { esc - 0} fallback={No MCP Servers}> + 0} fallback={No MCP Servers}> {Object.keys(sync.data.mcp).length} MCP Servers diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 157a9c9469..669ed18979 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -10,6 +10,7 @@ import { useSync } from "@tui/context/sync" import { Identifier } from "@/id/id" import { createStore, produce } from "solid-js/store" import { useKeybind } from "@tui/context/keybind" +import { Keybind } from "@/util/keybind" import { usePromptHistory, type PromptInfo } from "./history" import { type AutocompleteRef, Autocomplete } from "./autocomplete" import { useCommandDialog } from "../dialog-command" @@ -24,6 +25,7 @@ import { Locale } from "@/util/locale" import { createColors, createFrames } from "../../ui/spinner.ts" import { useDialog } from "@tui/ui/dialog" import { DialogProvider as DialogProviderConnect } from "../dialog-provider" +import { DialogAlert } from "../../ui/dialog-alert" import { useToast } from "../../ui/toast" export type PromptProps = { @@ -46,6 +48,61 @@ export type PromptRef = { const PLACEHOLDERS = ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"] +const TEXTAREA_ACTIONS = [ + "submit", + "newline", + "move-left", + "move-right", + "move-up", + "move-down", + "select-left", + "select-right", + "select-up", + "select-down", + "line-home", + "line-end", + "select-line-home", + "select-line-end", + "visual-line-home", + "visual-line-end", + "select-visual-line-home", + "select-visual-line-end", + "buffer-home", + "buffer-end", + "select-buffer-home", + "select-buffer-end", + "delete-line", + "delete-to-line-end", + "delete-to-line-start", + "backspace", + "delete", + "undo", + "redo", + "word-forward", + "word-backward", + "select-word-forward", + "select-word-backward", + "delete-word-forward", + "delete-word-backward", +] as const + +function mapTextareaKeybindings( + keybinds: Record, + action: (typeof TEXTAREA_ACTIONS)[number], +): KeyBinding[] { + const configKey = `input_${action.replace(/-/g, "_")}` + const bindings = keybinds[configKey] + if (!bindings) return [] + return bindings.map((binding) => ({ + name: binding.name, + ctrl: binding.ctrl || undefined, + meta: binding.meta || undefined, + shift: binding.shift || undefined, + super: binding.super || undefined, + action, + })) +} + export function Prompt(props: PromptProps) { let input: TextareaRenderable let anchor: BoxRenderable @@ -76,26 +133,12 @@ export function Prompt(props: PromptProps) { } const textareaKeybindings = createMemo(() => { - const newlineBindings = keybind.all.input_newline || [] - const submitBindings = keybind.all.input_submit || [] + const keybinds = keybind.all return [ { name: "return", action: "submit" }, { name: "return", meta: true, action: "newline" }, - ...newlineBindings.map((binding) => ({ - name: binding.name, - ctrl: binding.ctrl || undefined, - meta: binding.meta || undefined, - shift: binding.shift || undefined, - action: "newline" as const, - })), - ...submitBindings.map((binding) => ({ - name: binding.name, - ctrl: binding.ctrl || undefined, - meta: binding.meta || undefined, - shift: binding.shift || undefined, - action: "submit" as const, - })), + ...TEXTAREA_ACTIONS.flatMap((action) => mapTextareaKeybindings(keybinds, action)), ] satisfies KeyBinding[] }) @@ -199,7 +242,7 @@ export function Prompt(props: PromptProps) { const content = await Editor.open({ value, renderer }) if (!content) return - input.setText(content, { history: false }) + input.setText(content) // Update positions for nonTextParts based on their location in new content // Filter out parts whose virtual text was deleted @@ -390,7 +433,7 @@ export function Prompt(props: PromptProps) { input.blur() }, set(prompt) { - input.setText(prompt.input, { history: false }) + input.setText(prompt.input) setStore("prompt", prompt) restoreExtmarksFromParts(prompt.parts) input.gotoBufferEnd() @@ -410,6 +453,11 @@ export function Prompt(props: PromptProps) { if (props.disabled) return if (autocomplete.visible) return if (!store.prompt.input) return + const trimmed = store.prompt.input.trim() + if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") { + exit() + return + } const selectedModel = local.model.current() if (!selectedModel) { promptModelWarning() @@ -683,17 +731,6 @@ export function Prompt(props: PromptProps) { setStore("extmarkToPartIndex", new Map()) return } - if (keybind.match("input_forward_delete", e) && store.prompt.input !== "") { - const cursorOffset = input.cursorOffset - if (cursorOffset < input.plainText.length) { - const text = input.plainText - const newText = text.slice(0, cursorOffset) + text.slice(cursorOffset + 1) - input.setText(newText) - input.cursorOffset = cursorOffset - } - e.preventDefault() - return - } if (keybind.match("app_exit", e)) { await exit() return @@ -720,7 +757,7 @@ export function Prompt(props: PromptProps) { const item = history.move(direction, input.plainText) if (item) { - input.setText(item.input, { history: false }) + input.setText(item.input) setStore("prompt", item) restoreExtmarksFromParts(item.parts) e.preventDefault() @@ -872,9 +909,14 @@ export function Prompt(props: PromptProps) { if (!r) return if (r.message.includes("exceeded your current quota") && r.message.includes("gemini")) return "gemini is way too hot right now" - if (r.message.length > 50) return r.message.slice(0, 50) + "..." + if (r.message.length > 80) return r.message.slice(0, 80) + "..." return r.message }) + const isTruncated = createMemo(() => { + const r = retry() + if (!r) return false + return r.message.length > 120 + }) const [seconds, setSeconds] = createSignal(0) onMount(() => { const timer = setInterval(() => { @@ -886,12 +928,28 @@ export function Prompt(props: PromptProps) { clearInterval(timer) }) }) + const handleMessageClick = () => { + const r = retry() + if (!r) return + if (isTruncated()) { + DialogAlert.show(dialog, "Retry Error", r.message) + } + } + + const retryText = () => { + const r = retry() + if (!r) return "" + const baseMessage = message() + const truncatedHint = isTruncated() ? " (click to expand)" : "" + const retryInfo = ` [retrying ${seconds() > 0 ? `in ${seconds()}s ` : ""}attempt #${r.attempt}]` + return baseMessage + truncatedHint + retryInfo + } + return ( - - {message()} [retrying {seconds() > 0 ? `in ${seconds()}s ` : ""} - attempt #{retry()!.attempt}] - + + {retryText()} + ) })()} diff --git a/packages/opencode/src/cli/cmd/tui/context/directory.ts b/packages/opencode/src/cli/cmd/tui/context/directory.ts index 2ea8cf007d..17e5c180a1 100644 --- a/packages/opencode/src/cli/cmd/tui/context/directory.ts +++ b/packages/opencode/src/cli/cmd/tui/context/directory.ts @@ -5,7 +5,8 @@ import { Global } from "@/global" export function useDirectory() { const sync = useSync() return createMemo(() => { - const result = process.cwd().replace(Global.Path.home, "~") + const directory = sync.data.path.directory || process.cwd() + const result = directory.replace(Global.Path.home, "~") if (sync.data.vcs?.branch) return result + ":" + sync.data.vcs.branch return result }) diff --git a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx index 50a29d2c5e..4c82e594c3 100644 --- a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx @@ -73,21 +73,11 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex return store.leader }, parse(evt: ParsedKey): Keybind.Info { - if (evt.name === "\x1F") - return { - ctrl: true, - name: "_", - shift: false, - leader: false, - meta: false, - } - return { - ctrl: evt.ctrl, - name: evt.name, - shift: evt.shift, - leader: store.leader, - meta: evt.meta, + // Handle special case for Ctrl+Underscore (represented as \x1F) + if (evt.name === "\x1F") { + return Keybind.fromParsedKey({ ...evt, name: "_", ctrl: true }, store.leader) } + return Keybind.fromParsedKey(evt, store.leader) }, match(key: keyof KeybindsConfig, evt: ParsedKey) { const keybind = keybinds()[key] diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 28ea60a67f..f74f787db8 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -24,6 +24,7 @@ import type { Snapshot } from "@/snapshot" import { useExit } from "./exit" import { batch, onMount } from "solid-js" import { Log } from "@/util/log" +import type { Path } from "@opencode-ai/sdk" export const { use: useSync, provider: SyncProvider } = createSimpleContext({ name: "Sync", @@ -62,6 +63,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ } formatter: FormatterStatus[] vcs: VcsInfo | undefined + path: Path }>({ provider_next: { all: [], @@ -86,6 +88,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ mcp: {}, formatter: [], vcs: undefined, + path: { state: "", config: "", worktree: "", directory: "" }, }) const sdk = useSDK() @@ -286,6 +289,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ sdk.client.session.status().then((x) => setStore("session_status", x.data!)), sdk.client.provider.auth().then((x) => setStore("provider_auth", x.data ?? {})), sdk.client.vcs.get().then((x) => setStore("vcs", x.data)), + sdk.client.path.get().then((x) => setStore("path", x.data!)), ]).then(() => { setStore("status", "complete") }) diff --git a/packages/opencode/src/cli/cmd/tui/context/theme/orng.json b/packages/opencode/src/cli/cmd/tui/context/theme/orng.json index 407016ac5b..1228f102f3 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme/orng.json +++ b/packages/opencode/src/cli/cmd/tui/context/theme/orng.json @@ -17,7 +17,7 @@ "darkAccent": "#FFF7F1", "darkRed": "#e06c75", "darkOrange": "#EC5B2B", - "darkGreen": "#7fd88f", + "darkBlue": "#6ba1e6", "darkCyan": "#56b6c2", "darkYellow": "#e5c07b", "lightStep1": "#ffffff", @@ -36,7 +36,7 @@ "lightAccent": "#c94d24", "lightRed": "#d1383d", "lightOrange": "#EC5B2B", - "lightGreen": "#3d9a57", + "lightBlue": "#0062d1", "lightCyan": "#318795", "lightYellow": "#b0851f" }, @@ -62,8 +62,8 @@ "light": "lightOrange" }, "success": { - "dark": "darkGreen", - "light": "lightGreen" + "dark": "darkBlue", + "light": "lightBlue" }, "info": { "dark": "darkCyan", @@ -102,8 +102,8 @@ "light": "lightStep6" }, "diffAdded": { - "dark": "#4fd6be", - "light": "#1e725c" + "dark": "#6ba1e6", + "light": "#0062d1" }, "diffRemoved": { "dark": "#c53b53", @@ -118,16 +118,16 @@ "light": "#7086b5" }, "diffHighlightAdded": { - "dark": "#b8db87", - "light": "#4db380" + "dark": "#6ba1e6", + "light": "#0062d1" }, "diffHighlightRemoved": { "dark": "#e26a75", "light": "#f52a65" }, "diffAddedBg": { - "dark": "#20303b", - "light": "#d5e5d5" + "dark": "#1a2a3d", + "light": "#e0edfa" }, "diffRemovedBg": { "dark": "#37222c", @@ -142,8 +142,8 @@ "light": "lightStep3" }, "diffAddedLineNumberBg": { - "dark": "#1b2b34", - "light": "#c5d5c5" + "dark": "#162535", + "light": "#d0e5f5" }, "diffRemovedLineNumberBg": { "dark": "#2d1f26", @@ -166,8 +166,8 @@ "light": "lightCyan" }, "markdownCode": { - "dark": "darkGreen", - "light": "lightGreen" + "dark": "darkBlue", + "light": "lightBlue" }, "markdownBlockQuote": { "dark": "#FFF7F1", @@ -222,8 +222,8 @@ "light": "lightRed" }, "syntaxString": { - "dark": "darkGreen", - "light": "lightGreen" + "dark": "darkBlue", + "light": "lightBlue" }, "syntaxNumber": { "dark": "#FFF7F1", diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx index 8c8576cc09..da868221e4 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx @@ -5,8 +5,13 @@ import type { TextPart } from "@opencode-ai/sdk/v2" import { Locale } from "@/util/locale" import { DialogMessage } from "./dialog-message" import { useDialog } from "../../ui/dialog" +import type { PromptInfo } from "../../component/prompt/history" -export function DialogTimeline(props: { sessionID: string; onMove: (messageID: string) => void }) { +export function DialogTimeline(props: { + sessionID: string + onMove: (messageID: string) => void + setPrompt?: (prompt: PromptInfo) => void +}) { const sync = useSync() const dialog = useDialog() @@ -26,10 +31,13 @@ export function DialogTimeline(props: { sessionID: string; onMove: (messageID: s value: message.id, footer: Locale.time(message.time.created), onSelect: (dialog) => { - dialog.replace(() => ) + dialog.replace(() => ( + + )) }, }) } + result.reverse() return result }) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx index e889373e6f..69082c870b 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx @@ -10,7 +10,7 @@ export function Footer() { const { theme } = useTheme() const sync = useSync() const route = useRoute() - const mcp = createMemo(() => Object.keys(sync.data.mcp)) + const mcp = createMemo(() => Object.values(sync.data.mcp).filter((x) => x.status === "connected").length) const mcpError = createMemo(() => Object.values(sync.data.mcp).some((x) => x.status === "failed")) const lsp = createMemo(() => Object.keys(sync.data.lsp)) const permissions = createMemo(() => { @@ -66,7 +66,7 @@ export function Footer() { {lsp().length} LSP - + @@ -76,7 +76,7 @@ export function Footer() { - {mcp().length} MCP + {mcp()} MCP /status diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 628235afd0..1c1e4b65ec 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -289,6 +289,7 @@ export function Session() { if (child) scroll.scrollBy(child.y - scroll.y - 1) }} sessionID={route.sessionID} + setPrompt={(promptInfo) => prompt.set(promptInfo)} /> )) }, @@ -894,7 +895,7 @@ export function Session() { {(file) => ( - + {file.filename} 0}> +{file.additions} @@ -1503,11 +1504,15 @@ ToolRegistry.register({ - {(task) => ( - - ∟ {Locale.titlecase(task.tool)} {task.state.status === "completed" ? task.state.title : ""} - - )} + {(task, index) => { + const summary = props.metadata.summary ?? [] + return ( + + {index() === summary.length - 1 ? "└" : "├"} {Locale.titlecase(task.tool)}{" "} + {task.state.status === "completed" ? task.state.title : ""} + + ) + }} diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index 508d10838e..c1c29a7316 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -259,9 +259,11 @@ export function Sidebar(props: { sessionID: string }) { flexDirection="row" gap={1} > - + + ⬖ + - + Getting started OpenCode includes free models so you can start immediately. @@ -269,7 +271,7 @@ export function Sidebar(props: { sessionID: string }) { Connect from 75+ providers to use other models, including Claude, GPT, Gemini etc - Connect provider + Connect provider /connect diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx index 96ef982d7f..45e946fa7c 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx @@ -22,7 +22,9 @@ export function DialogAlert(props: DialogAlertProps) { return ( - {props.title} + + {props.title} + esc diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx index 9d0e7d2c74..8431a39461 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx @@ -34,7 +34,9 @@ export function DialogConfirm(props: DialogConfirmProps) { return ( - {props.title} + + {props.title} + esc diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx index db9648f2c7..056ce41dac 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx @@ -18,7 +18,9 @@ export function DialogHelp() { return ( - Help + + Help + esc/enter diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx index 4b4c635a52..1b9acb5898 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx @@ -35,7 +35,9 @@ export function DialogPrompt(props: DialogPromptProps) { return ( - {props.title} + + {props.title} + esc diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index b6c5b5f8b9..3f49a7c321 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -36,6 +36,7 @@ export interface DialogSelectOption { category?: string disabled?: boolean bg?: RGBA + gutter?: JSX.Element onSelect?: (ctx: DialogContext, trigger?: "prompt") => void } @@ -239,7 +240,7 @@ export function DialogSelect(props: DialogSelectProps) { moveTo(index) }} backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)} - paddingLeft={current() ? 1 : 3} + paddingLeft={current() || option.gutter ? 1 : 3} paddingRight={3} gap={1} > @@ -249,6 +250,7 @@ export function DialogSelect(props: DialogSelectProps) { description={option.description !== category ? option.description : undefined} active={active()} current={current()} + gutter={option.gutter} /> ) @@ -282,6 +284,7 @@ function Option(props: { active?: boolean current?: boolean footer?: JSX.Element | string + gutter?: JSX.Element onMouseOver?: () => void }) { const { theme } = useTheme() @@ -294,6 +297,11 @@ function Option(props: { ● + + + {props.gutter} + + right").describe("Next child session"), diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index f0044607cb..2dcf112ae9 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -11,9 +11,12 @@ export namespace Flag { export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS") export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT") export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"] + export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli" // Experimental export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL") + export const OPENCODE_EXPERIMENTAL_ICON_DISCOVERY = + OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY") export const OPENCODE_EXPERIMENTAL_WATCHER = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WATCHER") export const OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT = truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT") export const OPENCODE_ENABLE_EXA = diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index d630c3f93a..0359c16fe3 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -6,6 +6,7 @@ import z from "zod" import { NamedError } from "@opencode-ai/util/error" import { Log } from "../util/log" import { iife } from "@/util/iife" +import { Flag } from "../flag/flag" declare global { const OPENCODE_VERSION: string @@ -162,7 +163,7 @@ export namespace Installation { export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local" export const CHANNEL = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local" - export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}` + export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}/${Flag.OPENCODE_CLIENT}` export async function latest(installMethod?: Method) { const detectedMethod = installMethod || (await method()) diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index 31b8ff7119..ce426cf622 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -1,6 +1,7 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import path from "path" +import { pathToFileURL, fileURLToPath } from "url" import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node" import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types" import { Log } from "../util/log" @@ -46,7 +47,7 @@ export namespace LSPClient { const diagnostics = new Map() connection.onNotification("textDocument/publishDiagnostics", (params) => { - const path = new URL(params.uri).pathname + const path = fileURLToPath(params.uri) l.info("textDocument/publishDiagnostics", { path, }) @@ -68,7 +69,7 @@ export namespace LSPClient { connection.onRequest("workspace/workspaceFolders", async () => [ { name: "workspace", - uri: "file://" + input.root, + uri: pathToFileURL(input.root).href, }, ]) connection.listen() @@ -76,12 +77,12 @@ export namespace LSPClient { l.info("sending initialize") await withTimeout( connection.sendRequest("initialize", { - rootUri: "file://" + input.root, + rootUri: pathToFileURL(input.root).href, processId: input.server.process.pid, workspaceFolders: [ { name: "workspace", - uri: "file://" + input.root, + uri: pathToFileURL(input.root).href, }, ], initializationOptions: { @@ -154,7 +155,7 @@ export namespace LSPClient { }) await connection.sendNotification("textDocument/didChange", { textDocument: { - uri: `file://` + input.path, + uri: pathToFileURL(input.path).href, version: next, }, contentChanges: [{ text }], @@ -166,7 +167,7 @@ export namespace LSPClient { diagnostics.delete(input.path) await connection.sendNotification("textDocument/didOpen", { textDocument: { - uri: `file://` + input.path, + uri: pathToFileURL(input.path).href, languageId, version: 0, text, diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 096c57a6db..764c91fcc8 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -3,6 +3,7 @@ import { Bus } from "@/bus" import { Log } from "../util/log" import { LSPClient } from "./client" import path from "path" +import { pathToFileURL } from "url" import { LSPServer } from "./server" import z from "zod" import { Config } from "../config/config" @@ -270,7 +271,7 @@ export namespace LSP { return run((client) => { return client.connection.sendRequest("textDocument/hover", { textDocument: { - uri: `file://${input.file}`, + uri: pathToFileURL(input.file).href, }, position: { line: input.line, diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index b45ea912d4..b492c7179e 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -61,14 +61,10 @@ export namespace Plugin { for (const hook of await state().then((x) => x.hooks)) { const fn = hook[name] if (!fn) continue - try { - // @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you - // give up. - // try-counter: 2 - await fn(input, output) - } catch (e) { - log.error("failed to trigger hook", { name, error: e }) - } + // @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you + // give up. + // try-counter: 2 + await fn(input, output) } return output } diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index 4defefa515..5291995a30 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -3,6 +3,7 @@ import { Context } from "../util/context" import { Project } from "./project" import { State } from "./state" import { iife } from "@/util/iife" +import { GlobalBus } from "@/bus/global" interface Context { directory: string @@ -52,6 +53,15 @@ export const Instance = { Log.Default.info("disposing instance", { directory: Instance.directory }) await State.dispose(Instance.directory) cache.delete(Instance.directory) + GlobalBus.emit("event", { + directory: Instance.directory, + payload: { + type: "server.instance.disposed", + properties: { + directory: Instance.directory, + }, + }, + }) }, async disposeAll() { Log.Default.info("disposing all instances") diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 62459cc288..80c7126057 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -107,7 +107,7 @@ export namespace Project { await migrateFromGlobal(id, worktree) } } - if (Flag.OPENCODE_EXPERIMENTAL) discover(existing) + if (Flag.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY) discover(existing) const result: Info = { ...existing, worktree, diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 0587937b5c..c523725ec7 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -12,6 +12,7 @@ export namespace ModelsDev { export const Model = z.object({ id: z.string(), name: z.string(), + family: z.string().optional(), release_date: z.string(), attachment: z.boolean(), reasoning: z.boolean(), diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 112a617931..d4755af17a 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -318,6 +318,16 @@ export namespace Provider { }, } }, + cerebras: async () => { + return { + autoload: false, + options: { + headers: { + "X-Cerebras-3rd-Party-Integration": "opencode", + }, + }, + } + }, } export const Model = z @@ -330,6 +340,7 @@ export namespace Provider { npm: z.string(), }), name: z.string(), + family: z.string().optional(), capabilities: z.object({ temperature: z.boolean(), reasoning: z.boolean(), @@ -407,6 +418,7 @@ export namespace Provider { id: model.id, providerID: provider.id, name: model.name, + family: model.family, api: { id: model.id, url: provider.api!, diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 891025cde4..c0ee452365 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -74,23 +74,28 @@ export namespace ProviderTransform { return result } - // DeepSeek: Handle reasoning_content for tool call continuations - // - With tool calls: Include reasoning_content in providerOptions so model can continue reasoning - // - Without tool calls: Strip reasoning (new turn doesn't need previous reasoning) - // See: https://api-docs.deepseek.com/guides/thinking_mode - if (model.providerID === "deepseek" || model.api.id.toLowerCase().includes("deepseek")) { + // TODO: rm later + const bugged = + (model.id === "kimi-k2-thinking" && model.providerID === "opencode") || + (model.id === "moonshotai/Kimi-K2-Thinking" && model.providerID === "baseten") + if ( + model.providerID === "deepseek" || + model.api.id.toLowerCase().includes("deepseek") || + (model.capabilities.interleaved && + typeof model.capabilities.interleaved === "object" && + model.capabilities.interleaved.field === "reasoning_content" && + !bugged) + ) { return msgs.map((msg) => { if (msg.role === "assistant" && Array.isArray(msg.content)) { const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning") - const hasToolCalls = msg.content.some((part: any) => part.type === "tool-call") const reasoningText = reasoningParts.map((part: any) => part.text).join("") // Filter out reasoning parts from content const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning") - // If this message has tool calls and reasoning, include reasoning_content - // so DeepSeek can continue reasoning after tool execution - if (hasToolCalls && reasoningText) { + // Include reasoning_content directly on the message for all assistant messages + if (reasoningText) { return { ...msg, content: filteredContent, @@ -104,12 +109,12 @@ export namespace ProviderTransform { } } - // For final answers (no tool calls), just strip reasoning return { ...msg, content: filteredContent, } } + return msg }) } @@ -212,24 +217,33 @@ export namespace ProviderTransform { ): Record { const result: Record = {} - // switch to providerID later, for now use this if (model.api.npm === "@openrouter/ai-sdk-provider") { result["usage"] = { include: true, } + if (model.api.id.includes("gemini-3")) { + result["reasoning"] = { effort: "high" } + } + } + + if ( + model.providerID === "baseten" || + (model.providerID === "opencode" && ["kimi-k2-thinking", "glm-4.6"].includes(model.api.id)) + ) { + result["chat_template_args"] = { enable_thinking: true } } if (model.providerID === "openai" || providerOptions?.setCacheKey) { result["promptCacheKey"] = sessionID } - if ( - model.providerID === "google" || - (model.providerID.startsWith("opencode") && model.api.id.includes("gemini-3")) - ) { + if (model.api.npm === "@ai-sdk/google" || model.api.npm === "@ai-sdk/google-vertex") { result["thinkingConfig"] = { includeThoughts: true, } + if (model.api.id.includes("gemini-3")) { + result["thinkingConfig"]["thinkingLevel"] = "high" + } } if (model.api.id.includes("gpt-5") && !model.api.id.includes("gpt-5-chat")) { @@ -273,23 +287,7 @@ export namespace ProviderTransform { return options } - export function providerOptions(model: Provider.Model, options: { [x: string]: any }, messages: ModelMessage[]) { - if (model.capabilities.interleaved && typeof model.capabilities.interleaved === "object") { - const cot = [] - const assistantMessages = messages.filter((msg) => msg.role === "assistant") - for (const msg of assistantMessages) { - for (const part of msg.content) { - if (typeof part === "string") { - continue - } - if (part.type === "reasoning") { - cot.push(part) - } - } - } - options[model.capabilities.interleaved.field] = cot - } - + export function providerOptions(model: Provider.Model, options: { [x: string]: any }) { switch (model.api.npm) { case "@ai-sdk/openai": case "@ai-sdk/azure": diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index e1e3367c6b..f1485ec015 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -10,7 +10,7 @@ import { proxy } from "hono/proxy" import { Session } from "../session" import z from "zod" import { Provider } from "../provider/provider" -import { mapValues } from "remeda" +import { filter, mapValues, sortBy, pipe } from "remeda" import { NamedError } from "@opencode-ai/util/error" import { ModelsDev } from "../provider/models" import { Ripgrep } from "../file/ripgrep" @@ -56,6 +56,7 @@ export namespace Server { export const Event = { Connected: BusEvent.define("server.connected", z.object({})), + Disposed: BusEvent.define("global.disposed", z.object({})), } const app = new Hono() @@ -140,6 +141,35 @@ export namespace Server { }) }, ) + .post( + "/global/dispose", + describeRoute({ + summary: "Dispose instance", + description: "Clean up and dispose all OpenCode instances, releasing all resources.", + operationId: "global.dispose", + responses: { + 200: { + description: "Global disposed", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + }, + }), + async (c) => { + await Instance.disposeAll() + GlobalBus.emit("event", { + directory: "global", + payload: { + type: Event.Disposed.type, + properties: {}, + }, + }) + return c.json(true) + }, + ) .use(async (c, next) => { const directory = c.req.query("directory") ?? c.req.header("x-opencode-directory") ?? process.cwd() return Instance.provide({ @@ -483,6 +513,7 @@ export namespace Server { schema: resolver( z .object({ + home: z.string(), state: z.string(), config: z.string(), worktree: z.string(), @@ -499,6 +530,7 @@ export namespace Server { }), async (c) => { return c.json({ + home: Global.Path.home, state: Global.Path.state, config: Global.Path.config, worktree: Instance.worktree, @@ -549,7 +581,11 @@ export namespace Server { }), async (c) => { const sessions = await Array.fromAsync(Session.list()) - sessions.sort((a, b) => b.time.updated - a.time.updated) + pipe( + await Array.fromAsync(Session.list()), + filter((s) => !s.time.archived), + sortBy((s) => s.time.updated), + ) return c.json(sessions) }, ) @@ -755,6 +791,11 @@ export namespace Server { "json", z.object({ title: z.string().optional(), + time: z + .object({ + archived: z.number().optional(), + }) + .optional(), }), ), async (c) => { @@ -765,6 +806,7 @@ export namespace Server { if (updates.title !== undefined) { session.title = updates.title } + if (updates.time?.archived !== undefined) session.time.archived = updates.time.archived }) return c.json(updatedSession) @@ -1460,12 +1502,15 @@ export namespace Server { } } - const providers = mapValues(filteredProviders, (x) => Provider.fromModelsDevProvider(x)) - const connected = await Provider.list().then((x) => Object.keys(x)) + const connected = await Provider.list() + const providers = Object.assign( + mapValues(filteredProviders, (x) => Provider.fromModelsDevProvider(x)), + connected, + ) return c.json({ all: Object.values(providers), default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id), - connected, + connected: Object.keys(connected), }) }, ) diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index de3c3dca32..602b7f77b6 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -142,7 +142,7 @@ export namespace SessionCompaction { content: [ { type: "text", - text: "Summarize our conversation above. This summary will be the only context available when the conversation continues, so preserve critical information including: what was accomplished, current work in progress, files involved, next steps, and any key user requests or constraints. Be concise but detailed enough that work can continue seamlessly.", + text: "Provide a detailed prompt for continuing our conversation above. Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next considering new session will not have access to our conversation.", }, ], }, diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index a3369eb545..bf31352845 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -60,6 +60,7 @@ export namespace Session { created: z.number(), updated: z.number(), compacting: z.number().optional(), + archived: z.number().optional(), }), revert: z .object({ @@ -222,34 +223,13 @@ export namespace Session { if (cfg.share === "disabled") { throw new Error("Sharing is disabled in configuration") } - - if (cfg.enterprise?.url) { - const { ShareNext } = await import("@/share/share-next") - const share = await ShareNext.create(id) - await update(id, (draft) => { - draft.share = { - url: share.url, - } - }) - } - - const session = await get(id) - if (session.share) return session.share - const { Share } = await import("../share/share") - const share = await Share.create(id) + const { ShareNext } = await import("@/share/share-next") + const share = await ShareNext.create(id) await update(id, (draft) => { draft.share = { url: share.url, } }) - await Storage.write(["share", id], share) - await Share.sync("session/info/" + id, session) - for (const msg of await messages({ sessionID: id })) { - await Share.sync("session/message/" + id + "/" + msg.info.id, msg.info) - for (const part of msg.parts) { - await Share.sync("session/part/" + id + "/" + msg.info.id + "/" + part.id, part) - } - } return share }) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 8c4f4ecbad..9ae54326d8 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -5,6 +5,7 @@ import z from "zod" import { Identifier } from "../id/id" import { MessageV2 } from "./message-v2" import { Log } from "../util/log" +import { Flag } from "../flag/flag" import { SessionRevert } from "./revert" import { Session } from "." import { Agent } from "../agent/agent" @@ -21,7 +22,7 @@ import PROMPT_PLAN from "../session/prompt/plan.txt" import BUILD_SWITCH from "../session/prompt/build-switch.txt" import MAX_STEPS from "../session/prompt/max-steps.txt" import { defer } from "../util/defer" -import { mergeDeep, pipe } from "remeda" +import { clone, mergeDeep, pipe } from "remeda" import { ToolRegistry } from "../tool/registry" import { Wildcard } from "../util/wildcard" import { MCP } from "../mcp" @@ -331,6 +332,7 @@ export namespace SessionPrompt { }, }, })) as MessageV2.ToolPart + let executionError: Error | undefined const result = await taskTool .execute( { @@ -355,7 +357,11 @@ export namespace SessionPrompt { }, }, ) - .catch(() => {}) + .catch((error) => { + executionError = error + log.error("subtask execution failed", { error, agent: task.agent, description: task.description }) + return undefined + }) assistantMessage.finish = "tool-calls" assistantMessage.time.completed = Date.now() await Session.updateMessage(assistantMessage) @@ -381,7 +387,7 @@ export namespace SessionPrompt { ...part, state: { status: "error", - error: "Tool execution failed", + error: executionError ? `Tool execution failed: ${executionError.message}` : "Tool execution failed", time: { start: part.state.status === "running" ? part.state.time.start : Date.now(), end: Date.now(), @@ -1088,8 +1094,8 @@ export namespace SessionPrompt { }, } await Session.updatePart(part) - const shell = process.env["SHELL"] ?? "bash" - const shellName = path.basename(shell) + const shell = process.env["SHELL"] ?? (process.platform === "win32" ? process.env["COMSPEC"] || "cmd.exe" : "bash") + const shellName = path.basename(shell).toLowerCase() const invocations: Record = { nu: { @@ -1119,6 +1125,14 @@ export namespace SessionPrompt { `, ], }, + // Windows cmd.exe + "cmd.exe": { + args: ["/c", input.command], + }, + // Windows PowerShell + "powershell.exe": { + args: ["-NoProfile", "-Command", input.command], + }, // Fallback: any shell that doesn't match those above "": { args: ["-c", "-l", `${input.command}`], @@ -1130,7 +1144,7 @@ export namespace SessionPrompt { const proc = spawn(shell, args, { cwd: Instance.directory, - detached: true, + detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, @@ -1308,6 +1322,7 @@ export namespace SessionPrompt { input.history.filter((m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic)) .length === 1 if (!isFirst) return +<<<<<<< HEAD const agent = await Agent.get("summary") if (!agent) return const result = await LLM.stream({ @@ -1325,6 +1340,23 @@ export namespace SessionPrompt { abort: new AbortController().signal, sessionID: input.session.id, retries: 2, +======= + const cfg = await Config.get() + const small = + (await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID)) + const language = await Provider.getLanguage(small) + const provider = await Provider.getProvider(small.providerID) + const options = pipe( + {}, + mergeDeep(ProviderTransform.options(small, input.session.id, provider?.options)), + mergeDeep(ProviderTransform.smallOptions(small)), + mergeDeep(small.options), + ) + await generateText({ + // use higher # for reasoning models since reasoning tokens eat up a lot of the budget + maxOutputTokens: small.capabilities.reasoning ? 3000 : 20, + providerOptions: ProviderTransform.providerOptions(small, options), +>>>>>>> dev messages: [ { role: "user", diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index 09cdeb23a8..ab6a986862 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -91,7 +91,7 @@ export namespace SessionSummary { if (textPart && !userMsg.summary?.title) { const result = await generateText({ maxOutputTokens: small.capabilities.reasoning ? 1500 : 20, - providerOptions: ProviderTransform.providerOptions(small, options, []), + providerOptions: ProviderTransform.providerOptions(small, options), messages: [ ...SystemPrompt.title(small.providerID).map( (x): ModelMessage => ({ @@ -144,7 +144,7 @@ export namespace SessionSummary { const result = await generateText({ model: language, maxOutputTokens: 100, - providerOptions: ProviderTransform.providerOptions(small, options, []), + providerOptions: ProviderTransform.providerOptions(small, options), messages: [ ...SystemPrompt.summarize(small.providerID).map( (x): ModelMessage => ({ diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index 5196aeb989..fea9c3bb9e 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -11,9 +11,11 @@ import type * as SDK from "@opencode-ai/sdk/v2" export namespace ShareNext { const log = Log.create({ service: "share-next" }) + async function url() { + return Config.get().then((x) => x.enterprise?.url ?? "https://opncd.ai") + } + export async function init() { - const config = await Config.get() - if (!config.enterprise) return Bus.subscribe(Session.Event.Updated, async (evt) => { await sync(evt.properties.info.id, [ { @@ -62,8 +64,7 @@ export namespace ShareNext { export async function create(sessionID: string) { log.info("creating share", { sessionID }) - const url = await Config.get().then((x) => x.enterprise!.url) - const result = await fetch(`${url}/api/share`, { + const result = await fetch(`${await url()}/api/share`, { method: "POST", headers: { "Content-Type": "application/json", @@ -126,11 +127,10 @@ export namespace ShareNext { const queued = queue.get(sessionID) if (!queued) return queue.delete(sessionID) - const url = await Config.get().then((x) => x.enterprise!.url) const share = await get(sessionID) if (!share) return - await fetch(`${url}/api/share/${share.id}/sync`, { + await fetch(`${await url()}/api/share/${share.id}/sync`, { method: "POST", headers: { "Content-Type": "application/json", @@ -146,10 +146,9 @@ export namespace ShareNext { export async function remove(sessionID: string) { log.info("removing share", { sessionID }) - const url = await Config.get().then((x) => x.enterprise!.url) const share = await get(sessionID) if (!share) return - await fetch(`${url}/api/share/${share.id}`, { + await fetch(`${await url()}/api/share/${share.id}`, { method: "DELETE", headers: { "Content-Type": "application/json", diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index cd04814bfe..f7d08f81ab 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -83,7 +83,7 @@ export const BashTool = Tool.define("bash", async () => { log.info("bash tool using shell", { shell }) return { - description: DESCRIPTION, + description: DESCRIPTION.replaceAll("${directory}", Instance.directory), parameters: z.object({ command: z.string().describe("The command to execute"), timeout: z.number().describe("Optional timeout in milliseconds").optional(), @@ -189,7 +189,7 @@ export const BashTool = Tool.define("bash", async () => { const action = Wildcard.allStructured({ head: command[0], tail: command.slice(1) }, permissions) if (action === "deny") { throw new Error( - `The user has specifically restricted access to this command, you are not allowed to execute it. Here is the configuration: ${JSON.stringify(permissions)}`, + `The user has specifically restricted access to this command: "${command.join(" ")}", you are not allowed to execute it. The user has these settings configured: ${JSON.stringify(permissions)}`, ) } if (action === "ask") { diff --git a/packages/opencode/src/tool/bash.txt b/packages/opencode/src/tool/bash.txt index b0f68d2b75..eff52b1d30 100644 --- a/packages/opencode/src/tool/bash.txt +++ b/packages/opencode/src/tool/bash.txt @@ -1,5 +1,7 @@ Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures. +All commands run in ${directory} by default. Use the `workdir` parameter if you need to run a command in a different directory. + Before executing the command, please follow these steps: 1. Directory Verification: @@ -17,14 +19,47 @@ Before executing the command, please follow these steps: - Capture the output of the command. Usage notes: - - The command argument is required. - - You can specify an optional timeout in milliseconds. If not specified, commands will timeout after 120000ms (2 minutes). Use the `timeout` parameter to control execution time. - - The `workdir` parameter specifies the working directory for the command. Defaults to the current working directory. Prefer setting `workdir` over using `cd` in your commands. - - It is very helpful if you write a clear, concise description of what this command does in 5-10 words. - - If the output exceeds 30000 characters, output will be truncated before being returned to you. - - VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and List to read files. - - If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all opencode users have pre-installed. - - When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings). + - The command argument is required. + - You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). + If not specified, commands will timeout after 120000ms (2 minutes). + - The description argument is required. You must write a clear, concise description of what this command does in 5-10 words. + - If the output exceeds 30000 characters, output will be truncated before being + returned to you. + - You can use the `run_in_background` parameter to run the command in the background, + which allows you to continue working while the command runs. You can monitor the output + using the Bash tool as it becomes available. You do not need to use '&' at the end of + the command when using this parameter. + + - Avoid using Bash with the `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or + `echo` commands, unless explicitly instructed or when these commands are truly necessary + for the task. Instead, always prefer using the dedicated tools for these commands: + - File search: Use Glob (NOT find or ls) + - Content search: Use Grep (NOT grep or rg) + - Read files: Use Read (NOT cat/head/tail) + - Edit files: Use Edit (NOT sed/awk) + - Write files: Use Write (NOT echo >/cat < + pytest /foo/bar/tests + + + cd /foo/bar && pytest tests + # Working Directory diff --git a/packages/opencode/src/tool/grep.txt b/packages/opencode/src/tool/grep.txt index d964a3d1f4..6067ef27b9 100644 --- a/packages/opencode/src/tool/grep.txt +++ b/packages/opencode/src/tool/grep.txt @@ -2,7 +2,7 @@ - Searches file contents using regular expressions - Supports full regex syntax (eg. "log.*Error", "function\s+\w+", etc.) - Filter files by pattern with the include parameter (eg. "*.js", "*.{ts,tsx}") -- Returns file paths with at least one match sorted by modification time +- Returns file paths and line numbers with at least one match sorted by modification time - Use this tool when you need to find files containing specific patterns - If you need to identify/count the number of matches within files, use the Bash tool with `rg` (ripgrep) directly. Do NOT use `grep`. - When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Task tool instead diff --git a/packages/opencode/src/tool/webfetch.txt b/packages/opencode/src/tool/webfetch.txt index c1217f57bd..c5d1e7da23 100644 --- a/packages/opencode/src/tool/webfetch.txt +++ b/packages/opencode/src/tool/webfetch.txt @@ -11,4 +11,3 @@ Usage notes: - The prompt should describe what information you want to extract from the page - This tool is read-only and does not modify any files - Results may be summarized if the content is very large - - Includes a self-cleaning 15-minute cache for faster responses when repeatedly accessing the same URL diff --git a/packages/opencode/src/util/keybind.ts b/packages/opencode/src/util/keybind.ts index 5beaf9aab0..69fef28f0d 100644 --- a/packages/opencode/src/util/keybind.ts +++ b/packages/opencode/src/util/keybind.ts @@ -1,16 +1,35 @@ import { isDeepEqual } from "remeda" +import type { ParsedKey } from "@opentui/core" export namespace Keybind { - export type Info = { - ctrl: boolean - meta: boolean - shift: boolean - leader: boolean - name: string + /** + * Keybind info derived from OpenTUI's ParsedKey with our custom `leader` field. + * This ensures type compatibility and catches missing fields at compile time. + */ + export type Info = Pick & { + leader: boolean // our custom field } export function match(a: Info, b: Info): boolean { - return isDeepEqual(a, b) + // Normalize super field (undefined and false are equivalent) + const normalizedA = { ...a, super: a.super ?? false } + const normalizedB = { ...b, super: b.super ?? false } + return isDeepEqual(normalizedA, normalizedB) + } + + /** + * Convert OpenTUI's ParsedKey to our Keybind.Info format. + * This helper ensures all required fields are present and avoids manual object creation. + */ + export function fromParsedKey(key: ParsedKey, leader = false): Info { + return { + name: key.name, + ctrl: key.ctrl, + meta: key.meta, + shift: key.shift, + super: key.super ?? false, + leader, + } } export function toString(info: Info): string { @@ -18,6 +37,7 @@ export namespace Keybind { if (info.ctrl) parts.push("ctrl") if (info.meta) parts.push("alt") + if (info.super) parts.push("super") if (info.shift) parts.push("shift") if (info.name) { if (info.name === "delete") parts.push("del") @@ -58,6 +78,9 @@ export namespace Keybind { case "option": info.meta = true break + case "super": + info.super = true + break case "shift": info.shift = true break diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index 209f730327..6941310bbb 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -50,7 +50,10 @@ export namespace Log { export function file() { return logpath } - let write = (msg: any) => Bun.stderr.write(msg) + let write = (msg: any) => { + process.stderr.write(msg) + return msg.length + } export async function init(options: Options) { if (options.level) level = options.level diff --git a/packages/opencode/test/keybind.test.ts b/packages/opencode/test/keybind.test.ts index c09d6cbd37..4ca1f1697e 100644 --- a/packages/opencode/test/keybind.test.ts +++ b/packages/opencode/test/keybind.test.ts @@ -68,6 +68,31 @@ describe("Keybind.toString", () => { const info: Keybind.Info = { ctrl: false, meta: false, shift: false, leader: true, name: "" } expect(Keybind.toString(info)).toBe("") }) + + test("should convert super modifier to string", () => { + const info: Keybind.Info = { ctrl: false, meta: false, shift: false, super: true, leader: false, name: "z" } + expect(Keybind.toString(info)).toBe("super+z") + }) + + test("should convert super+shift modifier to string", () => { + const info: Keybind.Info = { ctrl: false, meta: false, shift: true, super: true, leader: false, name: "z" } + expect(Keybind.toString(info)).toBe("super+shift+z") + }) + + test("should handle super with ctrl modifier", () => { + const info: Keybind.Info = { ctrl: true, meta: false, shift: false, super: true, leader: false, name: "a" } + expect(Keybind.toString(info)).toBe("ctrl+super+a") + }) + + test("should handle super with all modifiers", () => { + const info: Keybind.Info = { ctrl: true, meta: true, shift: true, super: true, leader: false, name: "x" } + expect(Keybind.toString(info)).toBe("ctrl+alt+super+shift+x") + }) + + test("should handle undefined super field (omitted)", () => { + const info: Keybind.Info = { ctrl: true, meta: false, shift: false, leader: false, name: "c" } + expect(Keybind.toString(info)).toBe("ctrl+c") + }) }) describe("Keybind.match", () => { @@ -118,6 +143,36 @@ describe("Keybind.match", () => { const b: Keybind.Info = { ctrl: false, meta: false, shift: false, leader: false, name: "a" } expect(Keybind.match(a, b)).toBe(true) }) + + test("should match super modifier keybinds", () => { + const a: Keybind.Info = { ctrl: false, meta: false, shift: false, super: true, leader: false, name: "z" } + const b: Keybind.Info = { ctrl: false, meta: false, shift: false, super: true, leader: false, name: "z" } + expect(Keybind.match(a, b)).toBe(true) + }) + + test("should not match super vs non-super", () => { + const a: Keybind.Info = { ctrl: false, meta: false, shift: false, super: true, leader: false, name: "z" } + const b: Keybind.Info = { ctrl: false, meta: false, shift: false, super: false, leader: false, name: "z" } + expect(Keybind.match(a, b)).toBe(false) + }) + + test("should match undefined super with false super", () => { + const a: Keybind.Info = { ctrl: true, meta: false, shift: false, leader: false, name: "c" } + const b: Keybind.Info = { ctrl: true, meta: false, shift: false, super: false, leader: false, name: "c" } + expect(Keybind.match(a, b)).toBe(true) + }) + + test("should match super+shift combination", () => { + const a: Keybind.Info = { ctrl: false, meta: false, shift: true, super: true, leader: false, name: "z" } + const b: Keybind.Info = { ctrl: false, meta: false, shift: true, super: true, leader: false, name: "z" } + expect(Keybind.match(a, b)).toBe(true) + }) + + test("should not match when only super differs", () => { + const a: Keybind.Info = { ctrl: true, meta: true, shift: true, super: true, leader: false, name: "a" } + const b: Keybind.Info = { ctrl: true, meta: true, shift: true, super: false, leader: false, name: "a" } + expect(Keybind.match(a, b)).toBe(false) + }) }) describe("Keybind.parse", () => { @@ -314,4 +369,53 @@ describe("Keybind.parse", () => { }, ]) }) + + test("should parse super modifier", () => { + const result = Keybind.parse("super+z") + expect(result).toEqual([ + { + ctrl: false, + meta: false, + shift: false, + super: true, + leader: false, + name: "z", + }, + ]) + }) + + test("should parse super with shift modifier", () => { + const result = Keybind.parse("super+shift+z") + expect(result).toEqual([ + { + ctrl: false, + meta: false, + shift: true, + super: true, + leader: false, + name: "z", + }, + ]) + }) + + test("should parse multiple keybinds with super", () => { + const result = Keybind.parse("ctrl+-,super+z") + expect(result).toEqual([ + { + ctrl: true, + meta: false, + shift: false, + leader: false, + name: "-", + }, + { + ctrl: false, + meta: false, + shift: false, + super: true, + leader: false, + name: "z", + }, + ]) + }) }) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index da473252df..4e202a63cb 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -158,54 +158,6 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("Let me think about this...") }) - test("DeepSeek without tool calls strips reasoning from content", () => { - const msgs = [ - { - role: "assistant", - content: [ - { type: "reasoning", text: "Let me think about this..." }, - { type: "text", text: "Final answer" }, - ], - }, - ] as any[] - - const result = ProviderTransform.message(msgs, { - id: "deepseek/deepseek-chat", - providerID: "deepseek", - api: { - id: "deepseek-chat", - url: "https://api.deepseek.com", - npm: "@ai-sdk/openai-compatible", - }, - name: "DeepSeek Chat", - capabilities: { - temperature: true, - reasoning: true, - attachment: false, - toolcall: true, - input: { text: true, audio: false, image: false, video: false, pdf: false }, - output: { text: true, audio: false, image: false, video: false, pdf: false }, - interleaved: false, - }, - cost: { - input: 0.001, - output: 0.002, - cache: { read: 0.0001, write: 0.0002 }, - }, - limit: { - context: 128000, - output: 8192, - }, - status: "active", - options: {}, - headers: {}, - }) - - expect(result).toHaveLength(1) - expect(result[0].content).toEqual([{ type: "text", text: "Final answer" }]) - expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined() - }) - test("DeepSeek model ID containing 'deepseek' matches (case insensitive)", () => { const msgs = [ { diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index 0116f47cff..9ef7dfb9d8 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -3,11 +3,12 @@ import path from "path" import { BashTool } from "../../src/tool/bash" import { Instance } from "../../src/project/instance" import { Permission } from "../../src/permission" +import { tmpdir } from "../fixture/fixture" const ctx = { sessionID: "test", messageID: "", - toolCallID: "", + callID: "", agent: "build", abort: AbortSignal.any([]), metadata: () => {}, @@ -33,23 +34,401 @@ describe("tool.bash", () => { }, }) }) - - // TODO: better test - // test("cd ../ should ask for permission for external directory", async () => { - // await Instance.provide({ - // directory: projectRoot, - // fn: async () => { - // bash.execute( - // { - // command: "cd ../", - // description: "Try to cd to parent directory", - // }, - // ctx, - // ) - // // Give time for permission to be asked - // await new Promise((resolve) => setTimeout(resolve, 1000)) - // expect(Permission.pending()[ctx.sessionID]).toBeDefined() - // }, - // }) - // }) +}) + +describe("tool.bash permissions", () => { + test("allows command matching allow pattern", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + permission: { + bash: { + "echo *": "allow", + "*": "deny", + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const bash = await BashTool.init() + const result = await bash.execute( + { + command: "echo hello", + description: "Echo hello", + }, + ctx, + ) + expect(result.metadata.exit).toBe(0) + expect(result.metadata.output).toContain("hello") + }, + }) + }) + + test("denies command matching deny pattern", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + permission: { + bash: { + "curl *": "deny", + "*": "allow", + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const bash = await BashTool.init() + await expect( + bash.execute( + { + command: "curl https://example.com", + description: "Fetch URL", + }, + ctx, + ), + ).rejects.toThrow("restricted") + }, + }) + }) + + test("denies all commands with wildcard deny", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + permission: { + bash: { + "*": "deny", + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const bash = await BashTool.init() + await expect( + bash.execute( + { + command: "ls", + description: "List files", + }, + ctx, + ), + ).rejects.toThrow("restricted") + }, + }) + }) + + test("more specific pattern overrides general pattern", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + permission: { + bash: { + "*": "deny", + "ls *": "allow", + "pwd*": "allow", + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const bash = await BashTool.init() + // ls should be allowed + const result = await bash.execute( + { + command: "ls -la", + description: "List files", + }, + ctx, + ) + expect(result.metadata.exit).toBe(0) + + // pwd should be allowed + const pwd = await bash.execute( + { + command: "pwd", + description: "Print working directory", + }, + ctx, + ) + expect(pwd.metadata.exit).toBe(0) + + // cat should be denied + await expect( + bash.execute( + { + command: "cat /etc/passwd", + description: "Read file", + }, + ctx, + ), + ).rejects.toThrow("restricted") + }, + }) + }) + + test("denies dangerous subcommands while allowing safe ones", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + permission: { + bash: { + "find *": "allow", + "find * -delete*": "deny", + "find * -exec*": "deny", + "*": "deny", + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const bash = await BashTool.init() + // Basic find should work + const result = await bash.execute( + { + command: "find . -name '*.ts'", + description: "Find typescript files", + }, + ctx, + ) + expect(result.metadata.exit).toBe(0) + + // find -delete should be denied + await expect( + bash.execute( + { + command: "find . -name '*.tmp' -delete", + description: "Delete temp files", + }, + ctx, + ), + ).rejects.toThrow("restricted") + + // find -exec should be denied + await expect( + bash.execute( + { + command: "find . -name '*.ts' -exec cat {} \\;", + description: "Find and cat files", + }, + ctx, + ), + ).rejects.toThrow("restricted") + }, + }) + }) + + test("allows git read commands while denying writes", async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + permission: { + bash: { + "git status*": "allow", + "git log*": "allow", + "git diff*": "allow", + "git branch": "allow", + "git commit *": "deny", + "git push *": "deny", + "*": "deny", + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const bash = await BashTool.init() + // git status should work + const status = await bash.execute( + { + command: "git status", + description: "Git status", + }, + ctx, + ) + expect(status.metadata.exit).toBe(0) + + // git log should work + const log = await bash.execute( + { + command: "git log --oneline -5", + description: "Git log", + }, + ctx, + ) + expect(log.metadata.exit).toBe(0) + + // git commit should be denied + await expect( + bash.execute( + { + command: "git commit -m 'test'", + description: "Git commit", + }, + ctx, + ), + ).rejects.toThrow("restricted") + + // git push should be denied + await expect( + bash.execute( + { + command: "git push origin main", + description: "Git push", + }, + ctx, + ), + ).rejects.toThrow("restricted") + }, + }) + }) + + test("denies external directory access when permission is deny", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + permission: { + external_directory: "deny", + bash: { + "*": "allow", + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const bash = await BashTool.init() + // Should deny cd to parent directory (cd is checked for external paths) + await expect( + bash.execute( + { + command: "cd ../", + description: "Change to parent directory", + }, + ctx, + ), + ).rejects.toThrow() + }, + }) + }) + + test("denies workdir outside project when external_directory is deny", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + permission: { + external_directory: "deny", + bash: { + "*": "allow", + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const bash = await BashTool.init() + await expect( + bash.execute( + { + command: "ls", + workdir: "/tmp", + description: "List /tmp", + }, + ctx, + ), + ).rejects.toThrow() + }, + }) + }) + + test("handles multiple commands in sequence", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + permission: { + bash: { + "echo *": "allow", + "curl *": "deny", + "*": "deny", + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const bash = await BashTool.init() + // echo && echo should work + const result = await bash.execute( + { + command: "echo foo && echo bar", + description: "Echo twice", + }, + ctx, + ) + expect(result.metadata.output).toContain("foo") + expect(result.metadata.output).toContain("bar") + + // echo && curl should fail (curl is denied) + await expect( + bash.execute( + { + command: "echo hi && curl https://example.com", + description: "Echo then curl", + }, + ctx, + ), + ).rejects.toThrow("restricted") + }, + }) + }) }) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index c5d961f81c..3256079a56 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.0.141", + "version": "1.0.150", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index f00e90482d..57ca75d604 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -6,6 +6,7 @@ import type { Provider, Permission, UserMessage, + Message, Part, Auth, Config, @@ -175,6 +176,15 @@ export interface Hooks { metadata: any }, ) => Promise + "experimental.chat.messages.transform"?: ( + input: {}, + output: { + messages: { + info: Message + parts: Part[] + }[] + }, + ) => Promise "experimental.text.complete"?: ( input: { sessionID: string; messageID: string; partID: string }, output: { text: string }, diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 181344cc72..0ff29129e0 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.0.141", + "version": "1.0.150", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 3df3f62b7a..90df76c223 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -28,6 +28,7 @@ import type { FindSymbolsResponses, FindTextResponses, FormatterStatusResponses, + GlobalDisposeResponses, GlobalEventResponses, InstanceDisposeResponses, LspStatusResponses, @@ -193,6 +194,18 @@ export class Global extends HeyApiClient { ...options, }) } + + /** + * Dispose instance + * + * Clean up and dispose all OpenCode instances, releasing all resources. + */ + public dispose(options?: Options) { + return (options?.client ?? this.client).post({ + url: "/global/dispose", + ...options, + }) + } } export class Project extends HeyApiClient { @@ -812,6 +825,9 @@ export class Session extends HeyApiClient { sessionID: string directory?: string title?: string + time?: { + archived?: number + } }, options?: Options, ) { @@ -823,6 +839,7 @@ export class Session extends HeyApiClient { { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "body", key: "title" }, + { in: "body", key: "time" }, ], }, ], diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index aaa6257d12..9d0bbcc92c 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -575,6 +575,7 @@ export type Session = { created: number updated: number compacting?: number + archived?: number } revert?: { messageID: string @@ -724,6 +725,13 @@ export type EventServerConnected = { } } +export type EventGlobalDisposed = { + type: "global.disposed" + properties: { + [key: string]: unknown + } +} + export type Event = | EventInstallationUpdated | EventInstallationUpdateAvailable @@ -758,6 +766,7 @@ export type Event = | EventPtyExited | EventPtyDeleted | EventServerConnected + | EventGlobalDisposed export type GlobalEvent = { directory: string @@ -927,10 +936,6 @@ export type KeybindsConfig = { * Clear input field */ input_clear?: string - /** - * Forward delete - */ - input_forward_delete?: string /** * Paste from clipboard */ @@ -943,6 +948,138 @@ export type KeybindsConfig = { * Insert newline in input */ input_newline?: string + /** + * Move cursor left in input + */ + input_move_left?: string + /** + * Move cursor right in input + */ + input_move_right?: string + /** + * Move cursor up in input + */ + input_move_up?: string + /** + * Move cursor down in input + */ + input_move_down?: string + /** + * Select left in input + */ + input_select_left?: string + /** + * Select right in input + */ + input_select_right?: string + /** + * Select up in input + */ + input_select_up?: string + /** + * Select down in input + */ + input_select_down?: string + /** + * Move to start of line in input + */ + input_line_home?: string + /** + * Move to end of line in input + */ + input_line_end?: string + /** + * Select to start of line in input + */ + input_select_line_home?: string + /** + * Select to end of line in input + */ + input_select_line_end?: string + /** + * Move to start of visual line in input + */ + input_visual_line_home?: string + /** + * Move to end of visual line in input + */ + input_visual_line_end?: string + /** + * Select to start of visual line in input + */ + input_select_visual_line_home?: string + /** + * Select to end of visual line in input + */ + input_select_visual_line_end?: string + /** + * Move to start of buffer in input + */ + input_buffer_home?: string + /** + * Move to end of buffer in input + */ + input_buffer_end?: string + /** + * Select to start of buffer in input + */ + input_select_buffer_home?: string + /** + * Select to end of buffer in input + */ + input_select_buffer_end?: string + /** + * Delete line in input + */ + input_delete_line?: string + /** + * Delete to end of line in input + */ + input_delete_to_line_end?: string + /** + * Delete to start of line in input + */ + input_delete_to_line_start?: string + /** + * Backspace in input + */ + input_backspace?: string + /** + * Delete character in input + */ + input_delete?: string + /** + * Undo in input + */ + input_undo?: string + /** + * Redo in input + */ + input_redo?: string + /** + * Move word forward in input + */ + input_word_forward?: string + /** + * Move word backward in input + */ + input_word_backward?: string + /** + * Select word forward in input + */ + input_select_word_forward?: string + /** + * Select word backward in input + */ + input_select_word_backward?: string + /** + * Delete word forward in input + */ + input_delete_word_forward?: string + /** + * Delete word backward in input + */ + input_delete_word_backward?: string /** * Previous history item */ @@ -1039,6 +1176,7 @@ export type ProviderConfig = { [key: string]: { id?: string name?: string + family?: string release_date?: string attachment?: boolean reasoning?: boolean @@ -1394,6 +1532,7 @@ export type ToolListItem = { export type ToolList = Array export type Path = { + home: string state: string config: string worktree: string @@ -1465,6 +1604,7 @@ export type Model = { npm: string } name: string + family?: string capabilities: { temperature: boolean reasoning: boolean @@ -1700,6 +1840,22 @@ export type GlobalEventResponses = { export type GlobalEventResponse = GlobalEventResponses[keyof GlobalEventResponses] +export type GlobalDisposeData = { + body?: never + path?: never + query?: never + url: "/global/dispose" +} + +export type GlobalDisposeResponses = { + /** + * Global disposed + */ + 200: boolean +} + +export type GlobalDisposeResponse = GlobalDisposeResponses[keyof GlobalDisposeResponses] + export type ProjectListData = { body?: never path?: never @@ -2251,6 +2407,9 @@ export type SessionGetResponse = SessionGetResponses[keyof SessionGetResponses] export type SessionUpdateData = { body?: { title?: string + time?: { + archived?: number + } } path: { sessionID: string @@ -3031,6 +3190,7 @@ export type ProviderListResponses = { [key: string]: { id: string name: string + family?: string release_date: string attachment: boolean reasoning: boolean diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 55c9af7325..98c8b3586a 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -31,6 +31,31 @@ ] } }, + "/global/dispose": { + "post": { + "operationId": "global.dispose", + "summary": "Dispose instance", + "description": "Clean up and dispose all OpenCode instances, releasing all resources.", + "responses": { + "200": { + "description": "Global disposed", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.dispose({\n ...\n})" + } + ] + } + }, "/project": { "get": { "operationId": "project.list", @@ -1156,6 +1181,14 @@ "properties": { "title": { "type": "string" + }, + "time": { + "type": "object", + "properties": { + "archived": { + "type": "number" + } + } } } } @@ -2788,6 +2821,9 @@ "name": { "type": "string" }, + "family": { + "type": "string" + }, "release_date": { "type": "string" }, @@ -6379,6 +6415,9 @@ }, "compacting": { "type": "number" + }, + "archived": { + "type": "number" } }, "required": ["created", "updated"] @@ -6795,6 +6834,20 @@ }, "required": ["type", "properties"] }, + "Event.global.disposed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "global.disposed" + }, + "properties": { + "type": "object", + "properties": {} + } + }, + "required": ["type", "properties"] + }, "Event": { "anyOf": [ { @@ -6895,6 +6948,9 @@ }, { "$ref": "#/components/schemas/Event.server.connected" + }, + { + "$ref": "#/components/schemas/Event.global.disposed" } ] }, @@ -7134,11 +7190,6 @@ "default": "ctrl+c", "type": "string" }, - "input_forward_delete": { - "description": "Forward delete", - "default": "ctrl+d", - "type": "string" - }, "input_paste": { "description": "Paste from clipboard", "default": "ctrl+v", @@ -7151,7 +7202,172 @@ }, "input_newline": { "description": "Insert newline in input", - "default": "shift+return,ctrl+j", + "default": "shift+return,ctrl+return,alt+return,ctrl+j", + "type": "string" + }, + "input_move_left": { + "description": "Move cursor left in input", + "default": "left,ctrl+b", + "type": "string" + }, + "input_move_right": { + "description": "Move cursor right in input", + "default": "right,ctrl+f", + "type": "string" + }, + "input_move_up": { + "description": "Move cursor up in input", + "default": "up", + "type": "string" + }, + "input_move_down": { + "description": "Move cursor down in input", + "default": "down", + "type": "string" + }, + "input_select_left": { + "description": "Select left in input", + "default": "shift+left", + "type": "string" + }, + "input_select_right": { + "description": "Select right in input", + "default": "shift+right", + "type": "string" + }, + "input_select_up": { + "description": "Select up in input", + "default": "shift+up", + "type": "string" + }, + "input_select_down": { + "description": "Select down in input", + "default": "shift+down", + "type": "string" + }, + "input_line_home": { + "description": "Move to start of line in input", + "default": "ctrl+a", + "type": "string" + }, + "input_line_end": { + "description": "Move to end of line in input", + "default": "ctrl+e", + "type": "string" + }, + "input_select_line_home": { + "description": "Select to start of line in input", + "default": "ctrl+shift+a", + "type": "string" + }, + "input_select_line_end": { + "description": "Select to end of line in input", + "default": "ctrl+shift+e", + "type": "string" + }, + "input_visual_line_home": { + "description": "Move to start of visual line in input", + "default": "alt+a", + "type": "string" + }, + "input_visual_line_end": { + "description": "Move to end of visual line in input", + "default": "alt+e", + "type": "string" + }, + "input_select_visual_line_home": { + "description": "Select to start of visual line in input", + "default": "alt+shift+a", + "type": "string" + }, + "input_select_visual_line_end": { + "description": "Select to end of visual line in input", + "default": "alt+shift+e", + "type": "string" + }, + "input_buffer_home": { + "description": "Move to start of buffer in input", + "default": "home", + "type": "string" + }, + "input_buffer_end": { + "description": "Move to end of buffer in input", + "default": "end", + "type": "string" + }, + "input_select_buffer_home": { + "description": "Select to start of buffer in input", + "default": "shift+home", + "type": "string" + }, + "input_select_buffer_end": { + "description": "Select to end of buffer in input", + "default": "shift+end", + "type": "string" + }, + "input_delete_line": { + "description": "Delete line in input", + "default": "ctrl+shift+d", + "type": "string" + }, + "input_delete_to_line_end": { + "description": "Delete to end of line in input", + "default": "ctrl+k", + "type": "string" + }, + "input_delete_to_line_start": { + "description": "Delete to start of line in input", + "default": "ctrl+u", + "type": "string" + }, + "input_backspace": { + "description": "Backspace in input", + "default": "backspace,shift+backspace", + "type": "string" + }, + "input_delete": { + "description": "Delete character in input", + "default": "ctrl+d,delete,shift+delete", + "type": "string" + }, + "input_undo": { + "description": "Undo in input", + "default": "ctrl+-,super+z", + "type": "string" + }, + "input_redo": { + "description": "Redo in input", + "default": "ctrl+.,super+shift+z", + "type": "string" + }, + "input_word_forward": { + "description": "Move word forward in input", + "default": "alt+f,alt+right,ctrl+right", + "type": "string" + }, + "input_word_backward": { + "description": "Move word backward in input", + "default": "alt+b,alt+left,ctrl+left", + "type": "string" + }, + "input_select_word_forward": { + "description": "Select word forward in input", + "default": "alt+shift+f,alt+shift+right", + "type": "string" + }, + "input_select_word_backward": { + "description": "Select word backward in input", + "default": "alt+shift+b,alt+shift+left", + "type": "string" + }, + "input_delete_word_forward": { + "description": "Delete word forward in input", + "default": "alt+d,alt+delete,ctrl+delete", + "type": "string" + }, + "input_delete_word_backward": { + "description": "Delete word backward in input", + "default": "ctrl+w,ctrl+backspace,alt+backspace", "type": "string" }, "history_previous": { @@ -7305,6 +7521,9 @@ "name": { "type": "string" }, + "family": { + "type": "string" + }, "release_date": { "type": "string" }, @@ -8094,6 +8313,9 @@ "Path": { "type": "object", "properties": { + "home": { + "type": "string" + }, "state": { "type": "string" }, @@ -8107,7 +8329,7 @@ "type": "string" } }, - "required": ["state", "config", "worktree", "directory"] + "required": ["home", "state", "config", "worktree", "directory"] }, "VcsInfo": { "type": "object", @@ -8292,6 +8514,9 @@ "name": { "type": "string" }, + "family": { + "type": "string" + }, "capabilities": { "type": "object", "properties": { diff --git a/packages/slack/package.json b/packages/slack/package.json index cae7da4d43..ab046fc403 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.0.141", + "version": "1.0.150", "type": "module", "scripts": { "dev": "bun run src/index.ts", diff --git a/packages/tauri/package.json b/packages/tauri/package.json index 326a261b9e..fa98238b85 100644 --- a/packages/tauri/package.json +++ b/packages/tauri/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/tauri", "private": true, - "version": "1.0.141", + "version": "1.0.150", "type": "module", "scripts": { "typecheck": "tsgo -b", diff --git a/packages/tauri/scripts/predev.ts b/packages/tauri/scripts/predev.ts index bd9320a4a1..6b69a3ae5c 100644 --- a/packages/tauri/scripts/predev.ts +++ b/packages/tauri/scripts/predev.ts @@ -9,9 +9,6 @@ const sidecarConfig = getCurrentSidecar(RUST_TARGET) const binaryPath = `../opencode/dist/${sidecarConfig.ocBinary}/bin/opencode` -if (!(await fs.exists(binaryPath))) { - console.log("opencode binary not found, building...") - await $`cd ../opencode && bun run build --single` -} +await $`cd ../opencode && bun run build --single` await copyBinaryToSidecarFolder(binaryPath, RUST_TARGET) diff --git a/packages/tauri/src-tauri/capabilities/default.json b/packages/tauri/src-tauri/capabilities/default.json index df40065ee8..ef5a207b4e 100644 --- a/packages/tauri/src-tauri/capabilities/default.json +++ b/packages/tauri/src-tauri/capabilities/default.json @@ -7,6 +7,7 @@ "core:default", "opener:default", "core:window:allow-start-dragging", + "core:webview:allow-set-webview-zoom", "shell:default", "updater:default", "dialog:default", diff --git a/packages/tauri/src-tauri/icons/128x128.png b/packages/tauri/src-tauri/icons/128x128.png index 57d061b44c..caf7b02eb3 100644 Binary files a/packages/tauri/src-tauri/icons/128x128.png and b/packages/tauri/src-tauri/icons/128x128.png differ diff --git a/packages/tauri/src-tauri/icons/128x128@2x.png b/packages/tauri/src-tauri/icons/128x128@2x.png index a87a4c3cc5..47fe4c61ea 100644 Binary files a/packages/tauri/src-tauri/icons/128x128@2x.png and b/packages/tauri/src-tauri/icons/128x128@2x.png differ diff --git a/packages/tauri/src-tauri/icons/32x32.png b/packages/tauri/src-tauri/icons/32x32.png index 8383b47300..5868bcc933 100644 Binary files a/packages/tauri/src-tauri/icons/32x32.png and b/packages/tauri/src-tauri/icons/32x32.png differ diff --git a/packages/tauri/src-tauri/icons/64x64.png b/packages/tauri/src-tauri/icons/64x64.png index ef3b811792..1ed7425d85 100644 Binary files a/packages/tauri/src-tauri/icons/64x64.png and b/packages/tauri/src-tauri/icons/64x64.png differ diff --git a/packages/tauri/src-tauri/icons/Square107x107Logo.png b/packages/tauri/src-tauri/icons/Square107x107Logo.png index c80eb89093..1db249bf72 100644 Binary files a/packages/tauri/src-tauri/icons/Square107x107Logo.png and b/packages/tauri/src-tauri/icons/Square107x107Logo.png differ diff --git a/packages/tauri/src-tauri/icons/Square142x142Logo.png b/packages/tauri/src-tauri/icons/Square142x142Logo.png index bb767ed976..1961c34081 100644 Binary files a/packages/tauri/src-tauri/icons/Square142x142Logo.png and b/packages/tauri/src-tauri/icons/Square142x142Logo.png differ diff --git a/packages/tauri/src-tauri/icons/Square150x150Logo.png b/packages/tauri/src-tauri/icons/Square150x150Logo.png index 15cc8e0d29..abc507347e 100644 Binary files a/packages/tauri/src-tauri/icons/Square150x150Logo.png and b/packages/tauri/src-tauri/icons/Square150x150Logo.png differ diff --git a/packages/tauri/src-tauri/icons/Square284x284Logo.png b/packages/tauri/src-tauri/icons/Square284x284Logo.png index 5d78834e0e..51e2a1b9fe 100644 Binary files a/packages/tauri/src-tauri/icons/Square284x284Logo.png and b/packages/tauri/src-tauri/icons/Square284x284Logo.png differ diff --git a/packages/tauri/src-tauri/icons/Square30x30Logo.png b/packages/tauri/src-tauri/icons/Square30x30Logo.png index f78bf4c5a5..066a1fd0c8 100644 Binary files a/packages/tauri/src-tauri/icons/Square30x30Logo.png and b/packages/tauri/src-tauri/icons/Square30x30Logo.png differ diff --git a/packages/tauri/src-tauri/icons/Square310x310Logo.png b/packages/tauri/src-tauri/icons/Square310x310Logo.png index 2419f9209d..2a85c8e952 100644 Binary files a/packages/tauri/src-tauri/icons/Square310x310Logo.png and b/packages/tauri/src-tauri/icons/Square310x310Logo.png differ diff --git a/packages/tauri/src-tauri/icons/Square44x44Logo.png b/packages/tauri/src-tauri/icons/Square44x44Logo.png index 3f665e55a2..c855b80632 100644 Binary files a/packages/tauri/src-tauri/icons/Square44x44Logo.png and b/packages/tauri/src-tauri/icons/Square44x44Logo.png differ diff --git a/packages/tauri/src-tauri/icons/Square71x71Logo.png b/packages/tauri/src-tauri/icons/Square71x71Logo.png index f4d9d20d8a..c8168f7111 100644 Binary files a/packages/tauri/src-tauri/icons/Square71x71Logo.png and b/packages/tauri/src-tauri/icons/Square71x71Logo.png differ diff --git a/packages/tauri/src-tauri/icons/Square89x89Logo.png b/packages/tauri/src-tauri/icons/Square89x89Logo.png index 07be723db6..19ec1777de 100644 Binary files a/packages/tauri/src-tauri/icons/Square89x89Logo.png and b/packages/tauri/src-tauri/icons/Square89x89Logo.png differ diff --git a/packages/tauri/src-tauri/icons/StoreLogo.png b/packages/tauri/src-tauri/icons/StoreLogo.png index 3e78e8d3e4..3fd053d349 100644 Binary files a/packages/tauri/src-tauri/icons/StoreLogo.png and b/packages/tauri/src-tauri/icons/StoreLogo.png differ diff --git a/packages/tauri/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/packages/tauri/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png index 6e500bb7e5..4f3ea0e367 100644 Binary files a/packages/tauri/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png and b/packages/tauri/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/tauri/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/packages/tauri/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png index 23b5818acc..7db80699bc 100644 Binary files a/packages/tauri/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png and b/packages/tauri/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/packages/tauri/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/packages/tauri/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png index 69d1023ec5..a54ebe6528 100644 Binary files a/packages/tauri/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png and b/packages/tauri/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/packages/tauri/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/packages/tauri/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png index e670311710..9337ccfa3f 100644 Binary files a/packages/tauri/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png and b/packages/tauri/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/tauri/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/packages/tauri/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png index d274f1c9d3..0bfc1082e6 100644 Binary files a/packages/tauri/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png and b/packages/tauri/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/packages/tauri/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/packages/tauri/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png index 167af3864b..5b02ec732e 100644 Binary files a/packages/tauri/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png and b/packages/tauri/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/packages/tauri/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/packages/tauri/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png index 237b8fb293..322aeaeaaa 100644 Binary files a/packages/tauri/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png and b/packages/tauri/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/tauri/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/packages/tauri/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png index d6299e8074..ca1e336cc3 100644 Binary files a/packages/tauri/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png and b/packages/tauri/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/packages/tauri/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/packages/tauri/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png index 999e006389..f711107992 100644 Binary files a/packages/tauri/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png and b/packages/tauri/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/packages/tauri/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/packages/tauri/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png index 2626673743..287a6b500b 100644 Binary files a/packages/tauri/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png and b/packages/tauri/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/tauri/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/packages/tauri/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png index 4e04fbca40..9d3d06a867 100644 Binary files a/packages/tauri/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png and b/packages/tauri/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/packages/tauri/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/packages/tauri/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png index cda7c3e6d2..d4b6fde1b8 100644 Binary files a/packages/tauri/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png and b/packages/tauri/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/packages/tauri/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/packages/tauri/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png index cb9d5ef522..bde8d75967 100644 Binary files a/packages/tauri/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png and b/packages/tauri/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/tauri/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/packages/tauri/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png index 557ddef408..03df7809da 100644 Binary files a/packages/tauri/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png and b/packages/tauri/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/packages/tauri/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/packages/tauri/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png index df0abee517..62363be047 100644 Binary files a/packages/tauri/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png and b/packages/tauri/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/packages/tauri/src-tauri/icons/icon.icns b/packages/tauri/src-tauri/icons/icon.icns index 0e47ddc163..be910ad5f9 100644 Binary files a/packages/tauri/src-tauri/icons/icon.icns and b/packages/tauri/src-tauri/icons/icon.icns differ diff --git a/packages/tauri/src-tauri/icons/icon.ico b/packages/tauri/src-tauri/icons/icon.ico index 7749a74b11..ff88d21e4c 100644 Binary files a/packages/tauri/src-tauri/icons/icon.ico and b/packages/tauri/src-tauri/icons/icon.ico differ diff --git a/packages/tauri/src-tauri/icons/icon.png b/packages/tauri/src-tauri/icons/icon.png index ae5fdabbe0..0ecbb6d5f8 100644 Binary files a/packages/tauri/src-tauri/icons/icon.png and b/packages/tauri/src-tauri/icons/icon.png differ diff --git a/packages/tauri/src-tauri/icons/ios/AppIcon-20x20@1x.png b/packages/tauri/src-tauri/icons/ios/AppIcon-20x20@1x.png index 36d7ee3886..eb137e164a 100644 Binary files a/packages/tauri/src-tauri/icons/ios/AppIcon-20x20@1x.png and b/packages/tauri/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/packages/tauri/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/packages/tauri/src-tauri/icons/ios/AppIcon-20x20@2x-1.png index 871f8e39d3..aa76ab10ba 100644 Binary files a/packages/tauri/src-tauri/icons/ios/AppIcon-20x20@2x-1.png and b/packages/tauri/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/packages/tauri/src-tauri/icons/ios/AppIcon-20x20@2x.png b/packages/tauri/src-tauri/icons/ios/AppIcon-20x20@2x.png index 871f8e39d3..aa76ab10ba 100644 Binary files a/packages/tauri/src-tauri/icons/ios/AppIcon-20x20@2x.png and b/packages/tauri/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/packages/tauri/src-tauri/icons/ios/AppIcon-20x20@3x.png b/packages/tauri/src-tauri/icons/ios/AppIcon-20x20@3x.png index 007046c4a4..c58ea3d49b 100644 Binary files a/packages/tauri/src-tauri/icons/ios/AppIcon-20x20@3x.png and b/packages/tauri/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/packages/tauri/src-tauri/icons/ios/AppIcon-29x29@1x.png b/packages/tauri/src-tauri/icons/ios/AppIcon-29x29@1x.png index 288506f614..0eeb4d9bf9 100644 Binary files a/packages/tauri/src-tauri/icons/ios/AppIcon-29x29@1x.png and b/packages/tauri/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/packages/tauri/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/packages/tauri/src-tauri/icons/ios/AppIcon-29x29@2x-1.png index 013281df87..32601c70a1 100644 Binary files a/packages/tauri/src-tauri/icons/ios/AppIcon-29x29@2x-1.png and b/packages/tauri/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/packages/tauri/src-tauri/icons/ios/AppIcon-29x29@2x.png b/packages/tauri/src-tauri/icons/ios/AppIcon-29x29@2x.png index 013281df87..32601c70a1 100644 Binary files a/packages/tauri/src-tauri/icons/ios/AppIcon-29x29@2x.png and b/packages/tauri/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/packages/tauri/src-tauri/icons/ios/AppIcon-29x29@3x.png b/packages/tauri/src-tauri/icons/ios/AppIcon-29x29@3x.png index b042fbddac..a372c4a111 100644 Binary files a/packages/tauri/src-tauri/icons/ios/AppIcon-29x29@3x.png and b/packages/tauri/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/packages/tauri/src-tauri/icons/ios/AppIcon-40x40@1x.png b/packages/tauri/src-tauri/icons/ios/AppIcon-40x40@1x.png index 871f8e39d3..aa76ab10ba 100644 Binary files a/packages/tauri/src-tauri/icons/ios/AppIcon-40x40@1x.png and b/packages/tauri/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/packages/tauri/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/packages/tauri/src-tauri/icons/ios/AppIcon-40x40@2x-1.png index a78ec73340..e82ce2765f 100644 Binary files a/packages/tauri/src-tauri/icons/ios/AppIcon-40x40@2x-1.png and b/packages/tauri/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/packages/tauri/src-tauri/icons/ios/AppIcon-40x40@2x.png b/packages/tauri/src-tauri/icons/ios/AppIcon-40x40@2x.png index a78ec73340..e82ce2765f 100644 Binary files a/packages/tauri/src-tauri/icons/ios/AppIcon-40x40@2x.png and b/packages/tauri/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/packages/tauri/src-tauri/icons/ios/AppIcon-40x40@3x.png b/packages/tauri/src-tauri/icons/ios/AppIcon-40x40@3x.png index 9de8403a8c..15ad593628 100644 Binary files a/packages/tauri/src-tauri/icons/ios/AppIcon-40x40@3x.png and b/packages/tauri/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/packages/tauri/src-tauri/icons/ios/AppIcon-512@2x.png b/packages/tauri/src-tauri/icons/ios/AppIcon-512@2x.png index 348f719a17..2260671c00 100644 Binary files a/packages/tauri/src-tauri/icons/ios/AppIcon-512@2x.png and b/packages/tauri/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/packages/tauri/src-tauri/icons/ios/AppIcon-60x60@2x.png b/packages/tauri/src-tauri/icons/ios/AppIcon-60x60@2x.png index 9de8403a8c..15ad593628 100644 Binary files a/packages/tauri/src-tauri/icons/ios/AppIcon-60x60@2x.png and b/packages/tauri/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/packages/tauri/src-tauri/icons/ios/AppIcon-60x60@3x.png b/packages/tauri/src-tauri/icons/ios/AppIcon-60x60@3x.png index b06c67dd84..5c66bd3b18 100644 Binary files a/packages/tauri/src-tauri/icons/ios/AppIcon-60x60@3x.png and b/packages/tauri/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/packages/tauri/src-tauri/icons/ios/AppIcon-76x76@1x.png b/packages/tauri/src-tauri/icons/ios/AppIcon-76x76@1x.png index 903dc4d3e3..a5b05f3b50 100644 Binary files a/packages/tauri/src-tauri/icons/ios/AppIcon-76x76@1x.png and b/packages/tauri/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/packages/tauri/src-tauri/icons/ios/AppIcon-76x76@2x.png b/packages/tauri/src-tauri/icons/ios/AppIcon-76x76@2x.png index c2f796c30f..9c0615d411 100644 Binary files a/packages/tauri/src-tauri/icons/ios/AppIcon-76x76@2x.png and b/packages/tauri/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/packages/tauri/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/packages/tauri/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png index fd49a44f35..6b792b36ad 100644 Binary files a/packages/tauri/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png and b/packages/tauri/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/packages/tauri/src-tauri/src/lib.rs b/packages/tauri/src-tauri/src/lib.rs index afb34094f6..5c4304d51a 100644 --- a/packages/tauri/src-tauri/src/lib.rs +++ b/packages/tauri/src-tauri/src/lib.rs @@ -1,5 +1,5 @@ use std::{ - net::SocketAddr, + net::{SocketAddr, TcpListener}, process::Command, sync::{Arc, Mutex}, time::{Duration, Instant}, @@ -18,7 +18,13 @@ fn get_sidecar_port() -> u16 { .map(|s| s.to_string()) .or_else(|| std::env::var("OPENCODE_PORT").ok()) .and_then(|port_str| port_str.parse().ok()) - .unwrap_or(4096) + .unwrap_or_else(|| { + TcpListener::bind("127.0.0.1:0") + .expect("Failed to bind to find free port") + .local_addr() + .expect("Failed to get local address") + .port() + }) } fn find_and_kill_process_on_port(port: u16) -> Result<(), Box> { @@ -60,6 +66,8 @@ fn spawn_sidecar(app: &AppHandle, port: u16) -> CommandChild { .shell() .sidecar("opencode") .unwrap() + .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true") + .env("OPENCODE_CLIENT", "desktop") .args(["serve", &format!("--port={port}")]) .spawn() .expect("Failed to spawn opencode"); @@ -168,7 +176,8 @@ pub fn run() { .initialization_script(format!( r#" window.__OPENCODE__ ??= {{}}; - window.__OPENCODE__.updaterEnabled = {updater_enabled} + window.__OPENCODE__.updaterEnabled = {updater_enabled}; + window.__OPENCODE__.port = {port}; "# )); diff --git a/packages/tauri/src-tauri/tauri.conf.json b/packages/tauri/src-tauri/tauri.conf.json index d8a48c976d..94ac84c64b 100644 --- a/packages/tauri/src-tauri/tauri.conf.json +++ b/packages/tauri/src-tauri/tauri.conf.json @@ -19,7 +19,7 @@ }, "bundle": { "active": true, - "targets": ["deb", "rpm", "appimage", "dmg", "app", "nsis"], + "targets": ["deb", "rpm", "dmg", "nsis"], "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"], "externalBin": ["sidecars/opencode"], "createUpdaterArtifacts": true, diff --git a/packages/tauri/src/index.tsx b/packages/tauri/src/index.tsx index 6b9ce88e01..c72805fe64 100644 --- a/packages/tauri/src/index.tsx +++ b/packages/tauri/src/index.tsx @@ -47,12 +47,6 @@ const platform: Platform = { }, } -declare global { - interface Window { - __OPENCODE__?: { updaterEnabled?: boolean } - } -} - render(() => { onMount(() => { if (window.__OPENCODE__?.updaterEnabled) runUpdater() diff --git a/packages/tauri/tsconfig.json b/packages/tauri/tsconfig.json index b21529450c..e7f5c5c279 100644 --- a/packages/tauri/tsconfig.json +++ b/packages/tauri/tsconfig.json @@ -1,7 +1,19 @@ { - "extends": "../desktop/tsconfig.json", "compilerOptions": { - "outDir": "ts-dist" + "target": "ESNext", + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowJs": true, + "strict": true, + "isolatedModules": true, + "noEmit": true, + "emitDeclarationOnly": false, + "outDir": "node_modules/.ts-dist" }, "references": [{ "path": "../desktop" }], "include": ["src"] diff --git a/packages/ui/index.html b/packages/ui/index.html deleted file mode 100644 index 7697a5f962..0000000000 --- a/packages/ui/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - OpenCode UI - - - -
    - - - diff --git a/packages/ui/package.json b/packages/ui/package.json index e772c613b5..e7bcbbf790 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.0.141", + "version": "1.0.150", "type": "module", "exports": { "./*": "./src/components/*.tsx", @@ -17,7 +17,6 @@ "scripts": { "typecheck": "tsgo --noEmit", "dev": "vite", - "build": "vite build", "generate:tailwind": "bun run script/tailwind.ts" }, "devDependencies": { diff --git a/packages/ui/src/assets/images/social-share.png b/packages/ui/src/assets/images/social-share.png index be360c43f3..e3d5267a92 100644 Binary files a/packages/ui/src/assets/images/social-share.png and b/packages/ui/src/assets/images/social-share.png differ diff --git a/packages/ui/src/components/avatar.css b/packages/ui/src/components/avatar.css index 4e42e6f991..87be9a50ac 100644 --- a/packages/ui/src/components/avatar.css +++ b/packages/ui/src/components/avatar.css @@ -1,5 +1,6 @@ [data-component="avatar"] { --avatar-bg: var(--color-surface-info-base); + --avatar-fg: var(--color-text-base); display: flex; align-items: center; justify-content: center; @@ -10,7 +11,7 @@ font-weight: 500; text-transform: uppercase; background-color: var(--avatar-bg); - color: oklch(from var(--avatar-bg) calc(l * 0.72) calc(c * 8) h); + color: var(--avatar-fg); } [data-component="avatar"][data-has-image] { diff --git a/packages/ui/src/components/avatar.tsx b/packages/ui/src/components/avatar.tsx index 1ff3008eeb..ab7b0d0e2a 100644 --- a/packages/ui/src/components/avatar.tsx +++ b/packages/ui/src/components/avatar.tsx @@ -4,27 +4,39 @@ export interface AvatarProps extends ComponentProps<"div"> { fallback: string src?: string background?: string + foreground?: string size?: "small" | "normal" | "large" } export function Avatar(props: AvatarProps) { - const [split, rest] = splitProps(props, ["fallback", "src", "background", "size", "class", "classList", "style"]) + const [split, rest] = splitProps(props, [ + "fallback", + "src", + "background", + "foreground", + "size", + "class", + "classList", + "style", + ]) + const src = split.src // did this so i can zero it out to test fallback return (
    - + {(src) => }
    diff --git a/packages/ui/src/components/button.css b/packages/ui/src/components/button.css index 192c7b60ca..3a32672fea 100644 --- a/packages/ui/src/components/button.css +++ b/packages/ui/src/components/button.css @@ -11,27 +11,29 @@ outline: none; &[data-variant="primary"] { - border-color: var(--border-base); - background-color: var(--surface-brand-base); - color: var(--text-on-brand-strong); + background-color: var(--icon-strong-base); + border-color: var(--border-weak-base); + color: var(--icon-invert-base); + + [data-slot="icon-svg"] { + color: var(--icon-invert-base); + } &:hover:not(:disabled) { - border-color: var(--border-hover); - background-color: var(--surface-brand-hover); + background-color: var(--icon-strong-hover); } &:focus:not(:disabled) { - border-color: var(--border-focus); - background-color: var(--surface-brand-focus); + background-color: var(--icon-strong-focus); } &:active:not(:disabled) { - border-color: var(--border-active); - background-color: var(--surface-brand-active); + background-color: var(--icon-strong-active); } &:disabled { - border-color: var(--border-disabled); - background-color: var(--surface-disabled); - color: var(--text-weak); - cursor: not-allowed; + background-color: var(--icon-strong-disabled); + + [data-slot="icon-svg"] { + color: var(--icon-invert-base); + } } } @@ -102,23 +104,12 @@ height: 24px; padding: 0 6px; &[data-icon] { - padding: 0 8px 0 6px; + padding: 0 12px 0 4px; } font-size: var(--font-size-small); line-height: var(--line-height-large); gap: 6px; - } - - &[data-size="large"] { - height: 32px; - padding: 0 8px; - - &[data-icon] { - padding: 0 8px 0 6px; - } - - gap: 8px; /* text-12-medium */ font-family: var(--font-family-sans); @@ -129,6 +120,25 @@ letter-spacing: var(--letter-spacing-normal); } + &[data-size="large"] { + height: 32px; + padding: 6px 12px; + + &[data-icon] { + padding: 0 12px 0 8px; + } + + gap: 4px; + + /* text-14-medium */ + font-family: var(--font-family-sans); + font-size: 14px; + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); /* 142.857% */ + letter-spacing: var(--letter-spacing-normal); + } + &:focus { outline: none; } diff --git a/packages/ui/src/components/dialog.css b/packages/ui/src/components/dialog.css index 2ac0709ddd..979906e262 100644 --- a/packages/ui/src/components/dialog.css +++ b/packages/ui/src/components/dialog.css @@ -16,6 +16,7 @@ [data-component="dialog"] { position: fixed; inset: 0; + margin-left: var(--dialog-left-margin); z-index: 50; display: flex; align-items: center; @@ -24,7 +25,7 @@ [data-slot="dialog-container"] { position: relative; z-index: 50; - width: min(calc(100vw - 16px), 624px); + width: min(calc(100vw - 16px), 480px); height: min(calc(100vh - 16px), 512px); display: flex; flex-direction: column; @@ -36,14 +37,14 @@ flex-direction: column; align-items: flex-start; align-self: stretch; - gap: 8px; width: 100%; max-height: 100%; + min-height: 280px; /* padding: 8px; */ - padding: 8px 8px 0 8px; + /* padding: 8px 8px 0 8px; */ border: 1px solid var(--border-base); - border-radius: var(--radius-md); + border-radius: var(--radius-xl); background: var(--surface-raised-stronger-non-alpha); box-shadow: 0 15px 45px 0 rgba(19, 16, 16, 0.22), @@ -58,8 +59,9 @@ [data-slot="dialog-header"] { display: flex; - height: 40px; - padding: 4px 4px 4px 8px; + /* height: 40px; */ + /* padding: 4px 4px 4px 8px; */ + padding: 20px; justify-content: space-between; align-items: center; flex-shrink: 0; @@ -86,7 +88,18 @@ flex-direction: column; flex: 1; overflow-y: auto; + + &:focus-visible { + outline: none; + } } + &:focus-visible { + outline: none; + } + } + + &:focus-visible { + outline: none; } } } diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx index 4625482b54..56053278d0 100644 --- a/packages/ui/src/components/dialog.tsx +++ b/packages/ui/src/components/dialog.tsx @@ -5,7 +5,7 @@ import { DialogCloseButtonProps, DialogDescriptionProps, } from "@kobalte/core/dialog" -import { ComponentProps, type JSX, onCleanup, Show, splitProps } from "solid-js" +import { ComponentProps, type JSX, onCleanup, onMount, Show, splitProps } from "solid-js" import { IconButton } from "./icon-button" export interface DialogProps extends DialogRootProps { @@ -35,6 +35,11 @@ export function DialogRoot(props: DialogProps) { }) } + onMount(() => { + // @ts-ignore + document?.activeElement?.blur?.() + }) + return ( @@ -79,7 +84,7 @@ function DialogDescription(props: DialogDescriptionProps & ComponentProps<"p">) } function DialogCloseButton(props: DialogCloseButtonProps & ComponentProps<"button">) { - return + return } export const Dialog = Object.assign(DialogRoot, { diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index 8c83b41ce0..ce4bf7556a 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -1,206 +1,63 @@ import { splitProps, type ComponentProps } from "solid-js" -// prettier-ignore const icons = { - close: '', - menu: ' ', - "chevron-right": '', - "chevron-left": '', - "chevron-down": '', - "chevron-up": '', - "chevron-down-square": '', - "chevron-up-square": '', - "chevron-right-square": '', - "chevron-left-square": '', - settings: '', - globe: '', - github: '', - hammer: '', - "avatar-square": '', - slash: '', - robot: '', - cloud: '', - "file-text": '', - file: '', - "file-checkmark": '', - "file-code": '', - "file-important": '', - "file-minus": '', - "file-plus": '', - files: '', - "file-zip": '', - jpg: '', - pdf: '', - png: '', - gif: '', - archive: '', - sun: '', - moon: '', - monitor: '', - command: '', - link: '', - share: '', - branch: '', - logout: '', - login: '', - keys: '', - key: '', - info: '', - warning: '', - checkmark: '', - "checkmark-square": '', - plus: '', - minus: '', - undo: '', - merge: '', - redo: '', - refresh: '', - rotate: '', - "arrow-left": '', - "arrow-down": '', - "arrow-right": '', - "arrow-up": '', - enter: '', - trash: '', - package: '', - box: '', - lock: '', - unlocked: '', - activity: '', - asterisk: '', - bell: '', - "bell-off": '', - bolt: '', - bookmark: '', - brain: '', - browser: '', - "browser-cursor": '', - bug: '', - "carat-down": '', - "carat-left": '', - "carat-right": '', - "carat-up": '', - cards: '', - chart: '', - "check-circle": '', - checklist: '', - "checklist-cards": '', - lab: '', - circle: '', - "circle-dotted": '', - clipboard: '', - clock: '', - "close-circle": '', - terminal: '', - code: '', - components: '', - copy: '', - cpu: '', - dashboard: '', - transfer: '', - devices: '', - diamond: '', - dice: '', - discord: '', - dots: '', - expand: '', - droplet: '', - "chevron-double-down": '', - "chevron-double-left": '', - "chevron-double-right": '', - "chevron-double-up": '', - "speech-bubble": '', - message: '', - annotation: '', - square: '', - "pull-request": '', - pencil: '', - sparkles: '', - photo: '', - columns: '', - "open-pane": '', - "close-pane": '', - "file-search": '', - "folder-search": '', - search: '', - "web-search": '', - loading: '', - mic: '', -} as const - -const newIcons = { - "circle-x": ``, - "magnifying-glass": ``, - "plus-small": ``, + "align-right": ``, + "arrow-up": ``, + "arrow-left": ``, + "bubble-5": ``, + "bullet-list": ``, + "check-small": ``, "chevron-down": ``, "chevron-right": ``, - "arrow-up": ``, - "check-small": ``, - "edit-small-2": ``, - folder: ``, - "pencil-line": ``, "chevron-grabber-vertical": ``, + "circle-x": ``, + close: ``, + checklist: ``, + console: ``, + expand: ``, + collapse: ``, + "code-lines": ``, + "circle-ban-sign": ``, + "edit-small-2": ``, + enter: ``, + folder: ``, + "magnifying-glass": ``, + "plus-small": ``, + "pencil-line": ``, mcp: ``, glasses: ``, - "bullet-list": ``, "magnifying-glass-menu": ``, "window-cursor": ``, task: ``, - checklist: ``, - console: ``, - "code-lines": ``, - "square-arrow-top-right": ``, - "circle-ban-sign": ``, stop: ``, - enter: ``, "layout-left": ``, "layout-left-partial": ``, "layout-left-full": ``, "layout-right": ``, "layout-right-partial": ``, "layout-right-full": ``, + "square-arrow-top-right": ``, "speech-bubble": ``, - "align-right": ``, - expand: ``, - collapse: ``, "folder-add-left": ``, "settings-gear": ``, - "bubble-5": ``, github: ``, discord: ``, "layout-bottom": ``, "layout-bottom-partial": ``, "layout-bottom-full": ``, "dot-grid": ``, + "circle-check": ``, + copy: ``, + check: ``, } export interface IconProps extends ComponentProps<"svg"> { - name: keyof typeof icons | keyof typeof newIcons + name: keyof typeof icons size?: "small" | "normal" | "large" } export function Icon(props: IconProps) { const [local, others] = splitProps(props, ["name", "size", "class", "classList"]) - - if (local.name in newIcons) { - return ( -
    - -
    - ) - } - return (
    + 0} + fallback={ +
    +
    + {props.emptyMessage ?? "No results"} for "{filter()}" +
    +
    + } + > + + {(group) => ( +
    + +
    {group.category}
    +
    +
    + + {(item, i) => ( + + )} + +
    +
    + )} +
    +
    +
    + ) +} diff --git a/packages/ui/src/components/message-nav.tsx b/packages/ui/src/components/message-nav.tsx index 29b465c8cf..7416cfd939 100644 --- a/packages/ui/src/components/message-nav.tsx +++ b/packages/ui/src/components/message-nav.tsx @@ -1,7 +1,6 @@ import { UserMessage } from "@opencode-ai/sdk/v2" -import { ComponentProps, createMemo, For, Match, Show, splitProps, Switch } from "solid-js" +import { ComponentProps, For, Match, Show, splitProps, Switch } from "solid-js" import { DiffChanges } from "./diff-changes" -import { Spinner } from "./spinner" import { Tooltip } from "@kobalte/core/tooltip" export function MessageNav( @@ -9,20 +8,15 @@ export function MessageNav( messages: UserMessage[] current?: UserMessage size: "normal" | "compact" - working?: boolean onMessageSelect: (message: UserMessage) => void }, ) { - const [local, others] = splitProps(props, ["messages", "current", "size", "working", "onMessageSelect"]) - const lastUserMessage = createMemo(() => { - return local.messages?.at(0) - }) + const [local, others] = splitProps(props, ["messages", "current", "size", "onMessageSelect"]) const content = () => (
      {(message) => { - const messageWorking = createMemo(() => message.id === lastUserMessage()?.id && local.working) const handleClick = () => local.onMessageSelect(message) return ( @@ -35,14 +29,7 @@ export function MessageNav( - )} - -
    -
    - )} - - - ) } diff --git a/packages/ui/src/components/session-message-rail.tsx b/packages/ui/src/components/session-message-rail.tsx index 132b813d2c..1935a4f930 100644 --- a/packages/ui/src/components/session-message-rail.tsx +++ b/packages/ui/src/components/session-message-rail.tsx @@ -6,21 +6,12 @@ import "./session-message-rail.css" export interface SessionMessageRailProps extends ComponentProps<"div"> { messages: UserMessage[] current?: UserMessage - working?: boolean wide?: boolean onMessageSelect: (message: UserMessage) => void } export function SessionMessageRail(props: SessionMessageRailProps) { - const [local, others] = splitProps(props, [ - "messages", - "current", - "working", - "wide", - "onMessageSelect", - "class", - "classList", - ]) + const [local, others] = splitProps(props, ["messages", "current", "wide", "onMessageSelect", "class", "classList"]) return ( 1}> @@ -39,7 +30,6 @@ export function SessionMessageRail(props: SessionMessageRailProps) { current={local.current} onMessageSelect={local.onMessageSelect} size="compact" - working={local.working} />
    @@ -48,7 +38,6 @@ export function SessionMessageRail(props: SessionMessageRailProps) { current={local.current} onMessageSelect={local.onMessageSelect} size={local.wide ? "normal" : "compact"} - working={local.working} />
    diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 5e73c6772f..f97a3224cd 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -42,10 +42,10 @@ export function SessionTurn( const userMessages = createMemo(() => messages() .filter((m) => m.role === "user") - .sort((a, b) => b.id.localeCompare(a.id)), + .sort((a, b) => a.id.localeCompare(b.id)), ) const lastUserMessage = createMemo(() => { - return userMessages()?.at(0) + return userMessages()?.at(-1) }) const message = createMemo(() => userMessages()?.find((m) => m.id === props.messageID)) diff --git a/packages/ui/src/components/tag.css b/packages/ui/src/components/tag.css new file mode 100644 index 0000000000..0e8b7b9f10 --- /dev/null +++ b/packages/ui/src/components/tag.css @@ -0,0 +1,37 @@ +[data-component="tag"] { + display: inline-flex; + align-items: center; + justify-content: center; + user-select: none; + + border-radius: var(--radius-xs); + border: 0.5px solid var(--border-weak-base); + background: var(--surface-raised-base); + color: var(--text-base); + + &[data-size="normal"] { + height: 18px; + padding: 0 6px; + + /* text-12-medium */ + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); /* 166.667% */ + letter-spacing: var(--letter-spacing-normal); + } + + &[data-size="large"] { + height: 22px; + padding: 0 8px; + + /* text-14-medium */ + font-family: var(--font-family-sans); + font-size: 14px; + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); /* 142.857% */ + letter-spacing: var(--letter-spacing-normal); + } +} diff --git a/packages/ui/src/components/tag.tsx b/packages/ui/src/components/tag.tsx new file mode 100644 index 0000000000..428eedd0f3 --- /dev/null +++ b/packages/ui/src/components/tag.tsx @@ -0,0 +1,22 @@ +import { type ComponentProps, splitProps } from "solid-js" + +export interface TagProps extends ComponentProps<"span"> { + size?: "normal" | "large" +} + +export function Tag(props: TagProps) { + const [split, rest] = splitProps(props, ["size", "class", "classList", "children"]) + return ( + + {split.children} + + ) +} diff --git a/packages/ui/src/components/text-field.css b/packages/ui/src/components/text-field.css new file mode 100644 index 0000000000..897050a634 --- /dev/null +++ b/packages/ui/src/components/text-field.css @@ -0,0 +1,125 @@ +[data-component="input"] { + width: 100%; + + [data-slot="input-input"] { + width: 100%; + color: var(--text-strong); + + /* text-14-regular */ + font-family: var(--font-family-sans); + font-size: 14px; + font-style: normal; + font-weight: var(--font-weight-regular); + line-height: var(--line-height-large); /* 142.857% */ + letter-spacing: var(--letter-spacing-normal); + + &:focus { + outline: none; + } + + &::placeholder { + color: var(--text-weak); + } + } + + &[data-variant="normal"] { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + + [data-slot="input-label"] { + color: var(--text-weak); + + /* text-12-medium */ + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 18px; /* 150% */ + letter-spacing: var(--letter-spacing-normal); + } + + [data-slot="input-wrapper"] { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding-right: 4px; + + border-radius: var(--radius-md); + border: 1px solid var(--border-weak-base); + background: var(--input-base); + + &:focus-within { + /* border/shadow-xs/select */ + box-shadow: + 0 0 0 3px var(--border-weak-selected), + 0 0 0 1px var(--border-selected), + 0 1px 2px -1px rgba(19, 16, 16, 0.25), + 0 1px 2px 0 rgba(19, 16, 16, 0.08), + 0 1px 3px 0 rgba(19, 16, 16, 0.12); + } + + &:has([data-invalid]) { + background: var(--surface-critical-weak); + border: 1px solid var(--border-critical-selected); + } + + &:not(:has([data-slot="input-copy-button"])) { + padding-right: 0; + } + } + + [data-slot="input-input"] { + color: var(--text-strong); + + display: flex; + height: 32px; + padding: 2px 12px; + align-items: center; + flex: 1; + min-width: 0; + + background: transparent; + border: none; + + /* text-14-regular */ + font-family: var(--font-family-sans); + font-size: 14px; + font-style: normal; + font-weight: var(--font-weight-regular); + line-height: var(--line-height-large); /* 142.857% */ + letter-spacing: var(--letter-spacing-normal); + + &:focus { + outline: none; + } + + &::placeholder { + color: var(--text-weak); + } + } + + [data-slot="input-copy-button"] { + flex-shrink: 0; + color: var(--icon-base); + + &:hover { + color: var(--icon-strong-base); + } + } + + [data-slot="input-error"] { + color: var(--text-on-critical-base); + + /* text-12-medium */ + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 18px; /* 150% */ + letter-spacing: var(--letter-spacing-normal); + } + } +} diff --git a/packages/ui/src/components/text-field.tsx b/packages/ui/src/components/text-field.tsx new file mode 100644 index 0000000000..63ffb25943 --- /dev/null +++ b/packages/ui/src/components/text-field.tsx @@ -0,0 +1,103 @@ +import { TextField as Kobalte } from "@kobalte/core/text-field" +import { createSignal, Show, splitProps } from "solid-js" +import type { ComponentProps } from "solid-js" +import { IconButton } from "./icon-button" +import { Tooltip } from "./tooltip" + +export interface TextFieldProps + extends ComponentProps, + Partial< + Pick< + ComponentProps, + | "name" + | "defaultValue" + | "value" + | "onChange" + | "onKeyDown" + | "validationState" + | "required" + | "disabled" + | "readOnly" + > + > { + label?: string + hideLabel?: boolean + description?: string + error?: string + variant?: "normal" | "ghost" + copyable?: boolean +} + +export function TextField(props: TextFieldProps) { + const [local, others] = splitProps(props, [ + "name", + "defaultValue", + "value", + "onChange", + "onKeyDown", + "validationState", + "required", + "disabled", + "readOnly", + "class", + "label", + "hideLabel", + "description", + "error", + "variant", + "copyable", + ]) + const [copied, setCopied] = createSignal(false) + + async function handleCopy() { + const value = local.value ?? local.defaultValue ?? "" + await navigator.clipboard.writeText(value) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + return ( + + + + {local.label} + + +
    + + + + + + +
    + + {local.description} + + {local.error} +
    + ) +} + +/** @deprecated Use TextField instead */ +export const Input = TextField +/** @deprecated Use TextFieldProps instead */ +export type InputProps = TextFieldProps diff --git a/packages/ui/src/components/toast.css b/packages/ui/src/components/toast.css new file mode 100644 index 0000000000..fbc84f13c9 --- /dev/null +++ b/packages/ui/src/components/toast.css @@ -0,0 +1,203 @@ +[data-component="toast-region"] { + position: fixed; + bottom: 32px; + right: 32px; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 8px; + max-width: 400px; + width: 100%; + pointer-events: none; + + [data-slot="toast-list"] { + display: flex; + flex-direction: column; + gap: 8px; + list-style: none; + margin: 0; + padding: 0; + } +} + +[data-component="toast"] { + display: flex; + align-items: flex-start; + gap: 20px; + padding: 16px 20px; + pointer-events: auto; + transition: all 150ms ease-out; + + border-radius: var(--radius-lg); + border: 1px solid var(--border-weak-base); + background: var(--surface-float-base); + color: var(--text-inverted-base); + box-shadow: var(--shadow-md); + + [data-slot="toast-inner"] { + display: flex; + align-items: flex-start; + gap: 10px; + } + + &[data-opened] { + animation: toastPopIn 150ms ease-out; + } + + &[data-closed] { + animation: toastPopOut 100ms ease-in forwards; + } + + &[data-swipe="move"] { + transform: translateX(var(--kb-toast-swipe-move-x)); + } + + &[data-swipe="cancel"] { + transform: translateX(0); + transition: transform 200ms ease-out; + } + + &[data-swipe="end"] { + animation: toastSwipeOut 100ms ease-out forwards; + } + + /* &[data-variant="success"] { */ + /* border-color: var(--color-semantic-positive); */ + /* } */ + /**/ + /* &[data-variant="error"] { */ + /* border-color: var(--color-semantic-danger); */ + /* } */ + /**/ + /* &[data-variant="loading"] { */ + /* border-color: var(--color-semantic-info); */ + /* } */ + + [data-slot="toast-icon"] { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + + [data-component="icon"] { + color: rgba(253, 252, 252, 0.94); + } + } + + [data-slot="toast-content"] { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + } + + [data-slot="toast-title"] { + color: var(--text-inverted-strong); + + /* text-14-medium */ + font-family: var(--font-family-sans); + font-size: 14px; + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); /* 142.857% */ + letter-spacing: var(--letter-spacing-normal); + + margin: 0; + } + + [data-slot="toast-description"] { + color: var(--text-inverted-base); + + /* text-14-regular */ + font-family: var(--font-family-sans); + font-size: var(--font-size-base); + font-style: normal; + font-weight: var(--font-weight-regular); + line-height: var(--line-height-x-large); /* 171.429% */ + letter-spacing: var(--letter-spacing-normal); + + margin: 0; + } + + [data-slot="toast-actions"] { + display: flex; + gap: 16px; + margin-top: 8px; + } + + [data-slot="toast-action"] { + background: none; + border: none; + padding: 0; + cursor: pointer; + + color: var(--text-inverted-strong); + font-family: var(--font-family-sans); + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); + letter-spacing: var(--letter-spacing-normal); + + &:hover { + text-decoration: underline; + } + + &:last-child { + color: var(--text-inverted-weak); + } + } + + [data-slot="toast-close-button"] { + flex-shrink: 0; + } + + [data-slot="toast-progress-track"] { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background-color: var(--surface-base); + border-radius: 0 0 var(--radius-lg) var(--radius-lg); + overflow: hidden; + } + + [data-slot="toast-progress-fill"] { + height: 100%; + width: var(--kb-toast-progress-fill-width); + background-color: var(--color-primary); + transition: width 250ms linear; + } +} + +@keyframes toastPopIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes toastPopOut { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(20px); + } +} + +@keyframes toastSwipeOut { + from { + transform: translateX(var(--kb-toast-swipe-end-x)); + } + to { + transform: translateX(100%); + } +} diff --git a/packages/ui/src/components/toast.tsx b/packages/ui/src/components/toast.tsx new file mode 100644 index 0000000000..5869f8a6b2 --- /dev/null +++ b/packages/ui/src/components/toast.tsx @@ -0,0 +1,160 @@ +import { Toast as Kobalte, toaster } from "@kobalte/core/toast" +import type { ToastRootProps, ToastCloseButtonProps, ToastTitleProps, ToastDescriptionProps } from "@kobalte/core/toast" +import type { ComponentProps, JSX } from "solid-js" +import { Show } from "solid-js" +import { Portal } from "solid-js/web" +import { Icon, type IconProps } from "./icon" +import { IconButton } from "./icon-button" + +export interface ToastRegionProps extends ComponentProps {} + +function ToastRegion(props: ToastRegionProps) { + return ( + + + + + + ) +} + +export interface ToastRootComponentProps extends ToastRootProps { + class?: string + classList?: ComponentProps<"li">["classList"] + children?: JSX.Element +} + +function ToastRoot(props: ToastRootComponentProps) { + return ( + + ) +} + +function ToastIcon(props: { name: IconProps["name"] }) { + return ( +
    + +
    + ) +} + +function ToastContent(props: ComponentProps<"div">) { + return
    +} + +function ToastTitle(props: ToastTitleProps & ComponentProps<"div">) { + return +} + +function ToastDescription(props: ToastDescriptionProps & ComponentProps<"div">) { + return +} + +function ToastActions(props: ComponentProps<"div">) { + return
    +} + +function ToastCloseButton(props: ToastCloseButtonProps & ComponentProps<"button">) { + return +} + +function ToastProgressTrack(props: ComponentProps) { + return +} + +function ToastProgressFill(props: ComponentProps) { + return +} + +export const Toast = Object.assign(ToastRoot, { + Region: ToastRegion, + Icon: ToastIcon, + Content: ToastContent, + Title: ToastTitle, + Description: ToastDescription, + Actions: ToastActions, + CloseButton: ToastCloseButton, + ProgressTrack: ToastProgressTrack, + ProgressFill: ToastProgressFill, +}) + +export { toaster } + +export type ToastVariant = "default" | "success" | "error" | "loading" + +export interface ToastAction { + label: string + onClick: () => void +} + +export interface ToastOptions { + title?: string + description?: string + icon?: IconProps["name"] + variant?: ToastVariant + duration?: number + actions?: ToastAction[] +} + +export function showToast(options: ToastOptions | string) { + const opts = typeof options === "string" ? { description: options } : options + return toaster.show((props) => ( + + + + + + + {opts.title} + + + {opts.description} + + + + {opts.actions!.map((action) => ( + + ))} + + + + + + )) +} + +export interface ToastPromiseOptions { + loading?: JSX.Element + success?: (data: T) => JSX.Element + error?: (error: U) => JSX.Element +} + +export function showPromiseToast( + promise: Promise | (() => Promise), + options: ToastPromiseOptions, +) { + return toaster.promise(promise, (props) => ( + + + + {props.state === "pending" && options.loading} + {props.state === "fulfilled" && options.success?.(props.data!)} + {props.state === "rejected" && options.error?.(props.error)} + + + + + )) +} diff --git a/packages/ui/src/components/tooltip.css b/packages/ui/src/components/tooltip.css index 72ee269b28..6379862492 100644 --- a/packages/ui/src/components/tooltip.css +++ b/packages/ui/src/components/tooltip.css @@ -7,6 +7,7 @@ max-width: 320px; border-radius: var(--radius-md); background-color: var(--surface-float-base); + color: var(--text-inverted-base); color: rgba(253, 252, 252, 0.94); padding: 2px 8px; border: 0.5px solid rgba(253, 252, 252, 0.2); diff --git a/packages/ui/src/demo.tsx b/packages/ui/src/demo.tsx deleted file mode 100644 index 6081f08949..0000000000 --- a/packages/ui/src/demo.tsx +++ /dev/null @@ -1,291 +0,0 @@ -import type { Component } from "solid-js" -import { createSignal } from "solid-js" -import "./index.css" -import { Button } from "./components/button" -import { Select } from "./components/select" -import { Font } from "./components/font" -import { Accordion } from "./components/accordion" -import { Tabs } from "./components/tabs" -import { Tooltip } from "./components/tooltip" -import { Input } from "./components/input" -import { Checkbox } from "./components/checkbox" -import { Icon } from "./components/icon" -import { IconButton } from "./components/icon-button" -import { Dialog } from "./components/dialog" -import { SelectDialog } from "./components/select-dialog" -import { Collapsible } from "./components/collapsible" - -const Demo: Component = () => { - const [dialogOpen, setDialogOpen] = createSignal(false) - const [selectDialogOpen, setSelectDialogOpen] = createSignal(false) - const [inputValue, setInputValue] = createSignal("") - const [checked, setChecked] = createSignal(false) - const [termsAccepted, setTermsAccepted] = createSignal(false) - - const Content = (props: { dark?: boolean }) => ( -
    -

    Buttons

    -
    - - - - - - - - -
    -

    Select

    -
    - - setInputValue(e.currentTarget.value)} - /> - - -
    -

    Checkbox

    -
    - - - - - - - - -
    -

    Icons

    -
    - - - - - - - - -
    -

    Icon Buttons

    -
    - console.log("Close clicked")} /> - console.log("Check clicked")} /> - console.log("Search clicked")} disabled /> -
    -

    Dialog

    -
    - - - Example Dialog - This is an example dialog with a title and description. -
    - - -
    -
    -
    -

    Select Dialog

    -
    - - x} - onSelect={(option) => { - console.log("Selected:", option) - setSelectDialogOpen(false) - }} - placeholder="Search options..." - > - {(item) =>
    {item}
    } -
    -
    -

    Collapsible

    -
    - - - - - -
    -

    This is collapsible content that can be toggled open and closed.

    -

    It animates smoothly using CSS animations.

    -
    -
    -
    -
    -

    Accordion

    -
    - - - - What is Kobalte? - - -
    -

    Kobalte is a UI toolkit for building accessible web apps and design systems with SolidJS.

    -
    -
    -
    - - - Is it accessible? - - -
    -

    Yes. It adheres to the WAI-ARIA design patterns.

    -
    -
    -
    - - - Can it be animated? - - -
    -

    Yes! You can animate the content height using CSS animations.

    -
    -
    -
    -
    -
    -
    - ) - - return ( - <> - -
    - - -
    - - ) -} - -export default Demo diff --git a/packages/ui/src/hooks/use-filtered-list.tsx b/packages/ui/src/hooks/use-filtered-list.tsx index e0cb6e7aaf..e3b373d4d9 100644 --- a/packages/ui/src/hooks/use-filtered-list.tsx +++ b/packages/ui/src/hooks/use-filtered-list.tsx @@ -5,14 +5,14 @@ import { createStore } from "solid-js/store" import { createList } from "solid-list" export interface FilteredListProps { - items: T[] | ((filter: string) => Promise) + items: (filter: string) => T[] | Promise key: (item: T) => string filterKeys?: string[] current?: T groupBy?: (x: T) => string sortBy?: (a: T, b: T) => number sortGroupsBy?: (a: { category: string; items: T[] }, b: { category: string; items: T[] }) => number - onSelect?: (value: T | undefined) => void + onSelect?: (value: T | undefined, index: number) => void } export function useFilteredList(props: FilteredListProps) { @@ -22,7 +22,7 @@ export function useFilteredList(props: FilteredListProps) { () => store.filter, async (filter) => { const needle = filter?.toLowerCase() - const all = (typeof props.items === "function" ? await props.items(needle) : props.items) || [] + const all = (await props.items(needle)) || [] const result = pipe( all, (x) => { @@ -63,8 +63,9 @@ export function useFilteredList(props: FilteredListProps) { const onKeyDown = (event: KeyboardEvent) => { if (event.key === "Enter") { event.preventDefault() - const selected = flat().find((x) => props.key(x) === list.active()) - if (selected) props.onSelect?.(selected) + const selectedIndex = flat().findIndex((x) => props.key(x) === list.active()) + const selected = flat()[selectedIndex] + if (selected) props.onSelect?.(selected, selectedIndex) } else { list.onKeyDown(event) } diff --git a/packages/ui/src/index.css b/packages/ui/src/index.css deleted file mode 100644 index 27bcac4dac..0000000000 --- a/packages/ui/src/index.css +++ /dev/null @@ -1,40 +0,0 @@ -@import "./styles/index.css"; - -:root { - body { - margin: 0; - background-color: var(--background-base); - color: var(--text-base); - } - main { - display: flex; - flex-direction: row; - overflow-x: hidden; - } - main > div { - flex: 1; - padding: 2rem; - min-width: 0; - overflow-x: hidden; - display: flex; - flex-direction: column; - gap: 2rem; - } - h3 { - font-size: 1.25rem; - font-weight: 600; - margin: 0 0 1rem 0; - margin-bottom: -1rem; - } - section { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - align-items: flex-start; - } -} - -.dark { - background-color: var(--background-base); - color: var(--text-base); -} diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx deleted file mode 100644 index fa76ba9af0..0000000000 --- a/packages/ui/src/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* @refresh reload */ -import { render } from "solid-js/web" -import { MetaProvider } from "@solidjs/meta" - -import Demo from "./demo" - -const root = document.getElementById("root") - -if (import.meta.env.DEV && !(root instanceof HTMLElement)) { - throw new Error( - "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?", - ) -} - -render( - () => ( - - - - ), - root!, -) diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css index ab45a3a25f..d60082d931 100644 --- a/packages/ui/src/styles/index.css +++ b/packages/ui/src/styles/index.css @@ -21,7 +21,8 @@ @import "../components/provider-icon.css" layer(components); @import "../components/icon.css" layer(components); @import "../components/icon-button.css" layer(components); -@import "../components/input.css" layer(components); +@import "../components/text-field.css" layer(components); +@import "../components/list.css" layer(components); @import "../components/logo.css" layer(components); @import "../components/markdown.css" layer(components); @import "../components/message-part.css" layer(components); @@ -36,6 +37,8 @@ @import "../components/session-turn.css" layer(components); @import "../components/sticky-accordion-header.css" layer(components); @import "../components/tabs.css" layer(components); +@import "../components/tag.css" layer(components); +@import "../components/toast.css" layer(components); @import "../components/tooltip.css" layer(components); @import "../components/typewriter.css" layer(components); diff --git a/packages/ui/src/styles/tailwind/index.css b/packages/ui/src/styles/tailwind/index.css index bc6bb6f6da..d0a414fee7 100644 --- a/packages/ui/src/styles/tailwind/index.css +++ b/packages/ui/src/styles/tailwind/index.css @@ -57,6 +57,7 @@ --radius-sm: 0.25rem; --radius-md: 0.375rem; --radius-lg: 0.5rem; + --radius-xl: 0.625rem; --shadow-xs: var(--shadow-xs); --shadow-md: var(--shadow-md); diff --git a/packages/ui/src/styles/theme.css b/packages/ui/src/styles/theme.css index 4450358f83..2da926673a 100644 --- a/packages/ui/src/styles/theme.css +++ b/packages/ui/src/styles/theme.css @@ -40,9 +40,11 @@ --container-6xl: 72rem; --container-7xl: 80rem; + --radius-xs: 0.125rem; --radius-sm: 0.25rem; --radius-md: 0.375rem; --radius-lg: 0.5rem; + --radius-xl: 0.625rem; --shadow-xs: 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); @@ -130,16 +132,20 @@ --surface-diff-delete-weaker: var(--ember-light-1); --surface-diff-delete-strong: var(--ember-light-6); --surface-diff-delete-stronger: var(--ember-light-9); - --text-base: var(--smoke-light-11); --input-base: var(--smoke-light-1); --input-hover: var(--smoke-light-2); --input-active: var(--cobalt-light-1); --input-selected: var(--cobalt-light-4); --input-focus: var(--cobalt-light-1); --input-disabled: var(--smoke-light-4); + --text-base: var(--smoke-light-11); --text-weak: var(--smoke-light-9); --text-weaker: var(--smoke-light-8); --text-strong: var(--smoke-light-12); + --text-invert-base: var(--smoke-dark-alpha-11); + --text-invert-weak: var(--smoke-dark-alpha-9); + --text-invert-weaker: var(--smoke-dark-alpha-8); + --text-invert-strong: var(--smoke-dark-alpha-12); --text-interactive-base: var(--cobalt-light-9); --text-on-brand-base: var(--smoke-light-alpha-11); --text-on-interactive-base: var(--smoke-light-1); @@ -301,6 +307,18 @@ --border-weaker-focus: var(--smoke-light-alpha-6); --button-ghost-hover: var(--smoke-light-alpha-2); --button-ghost-hover2: var(--smoke-light-alpha-3); + --avatar-background-pink: #feeef8; + --avatar-background-mint: #e1fbf4; + --avatar-background-orange: #fff1e7; + --avatar-background-purple: #f9f1fe; + --avatar-background-cyan: #e7f9fb; + --avatar-background-lime: #eefadc; + --avatar-text-pink: #cd1d8d; + --avatar-text-mint: #147d6f; + --avatar-text-orange: #ed5f00; + --avatar-text-purple: #8445bc; + --avatar-text-cyan: #0894b3; + --avatar-text-lime: #5d770d; @media (prefers-color-scheme: dark) { color-scheme: dark; @@ -370,16 +388,20 @@ --surface-diff-delete-weaker: var(--ember-dark-3); --surface-diff-delete-strong: var(--ember-dark-5); --surface-diff-delete-stronger: var(--ember-dark-11); - --text-base: var(--smoke-dark-alpha-11); --input-base: var(--smoke-dark-2); --input-hover: var(--smoke-dark-2); --input-active: var(--cobalt-dark-1); --input-selected: var(--cobalt-dark-2); --input-focus: var(--cobalt-dark-1); --input-disabled: var(--smoke-dark-4); + --text-base: var(--smoke-dark-alpha-11); --text-weak: var(--smoke-dark-alpha-9); --text-weaker: var(--smoke-dark-alpha-8); --text-strong: var(--smoke-dark-alpha-12); + --text-invert-base: var(--smoke-dark-alpha-11); + --text-invert-weak: var(--smoke-dark-alpha-9); + --text-invert-weaker: var(--smoke-dark-alpha-8); + --text-invert-strong: var(--smoke-dark-alpha-12); --text-interactive-base: var(--cobalt-dark-11); --text-on-brand-base: var(--smoke-dark-alpha-11); --text-on-interactive-base: var(--smoke-dark-12); @@ -541,6 +563,18 @@ --border-weaker-focus: var(--smoke-dark-alpha-6); --button-ghost-hover: var(--smoke-dark-alpha-2); --button-ghost-hover2: var(--smoke-dark-alpha-3); + --avatar-background-pink: #501b3f; + --avatar-background-mint: #033a34; + --avatar-background-orange: #5f2a06; + --avatar-background-purple: #432155; + --avatar-background-cyan: #0f3058; + --avatar-background-lime: #2b3711; + --avatar-text-pink: #e34ba9; + --avatar-text-mint: #95f3d9; + --avatar-text-orange: #ff802b; + --avatar-text-purple: #9d5bd2; + --avatar-text-cyan: #369eff; + --avatar-text-lime: #c4f042; } } diff --git a/packages/util/package.json b/packages/util/package.json index 089e101954..496987ebb3 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.0.141", + "version": "1.0.150", "private": true, "type": "module", "exports": { diff --git a/packages/web/astro.config.mjs b/packages/web/astro.config.mjs index 3c6367c6a6..1e112b1705 100644 --- a/packages/web/astro.config.mjs +++ b/packages/web/astro.config.mjs @@ -31,7 +31,7 @@ export default defineConfig({ configSchema(), solidJs(), starlight({ - title: "opencode", + title: "OpenCode", lastUpdated: true, expressiveCode: { themes: ["github-light", "github-dark"] }, social: [ diff --git a/packages/web/package.json b/packages/web/package.json index 0222408f02..5b82ae78bb 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/web", "type": "module", - "version": "1.0.141", + "version": "1.0.150", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/packages/web/src/components/Lander.astro b/packages/web/src/components/Lander.astro index 2335ce3cbf..f015fd0a67 100644 --- a/packages/web/src/components/Lander.astro +++ b/packages/web/src/components/Lander.astro @@ -133,9 +133,9 @@ if (image) {

    Mise

    -