diff --git a/.github/workflows/publish-vscode.yml b/.github/workflows/publish-vscode.yml index 6014d430a..48357813f 100644 --- a/.github/workflows/publish-vscode.yml +++ b/.github/workflows/publish-vscode.yml @@ -24,6 +24,10 @@ jobs: - run: git fetch --force --tags - run: bun install -g @vscode/vsce + - name: Install extension dependencies + run: bun install + working-directory: ./sdks/vscode + - name: Publish run: | ./script/publish diff --git a/STATS.md b/STATS.md index 933d46053..901c3fd3f 100644 --- a/STATS.md +++ b/STATS.md @@ -115,3 +115,4 @@ | 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) | diff --git a/bun.lock b/bun.lock index 434a31563..dce7c3a3b 100644 --- a/bun.lock +++ b/bun.lock @@ -117,6 +117,7 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", "@shikijs/transformers": "3.9.2", + "@solid-primitives/active-element": "2.1.3", "@solid-primitives/event-bus": "1.1.2", "@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/scroll": "2.1.3", @@ -286,7 +287,9 @@ "version": "0.15.13", "dependencies": { "@kobalte/core": "catalog:", + "@pierre/precision-diffs": "0.0.2-alpha.1-1", "@solidjs/meta": "catalog:", + "fuzzysort": "catalog:", "luxon": "catalog:", "remeda": "catalog:", "solid-js": "catalog:", @@ -1019,6 +1022,8 @@ "@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="], + "@pierre/precision-diffs": ["@pierre/precision-diffs@0.0.2-alpha.1-1", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/transformers": "3.13.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.13.0" } }, "sha512-T43cwB7gMnbM+tp9p73NptUm4uUOfmrP5ihMOAHWQPpzBa/oeTjqZlmEmSQLpT8WKKnWG0lbKZPtlw7l0gW0Vw=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], "@planetscale/database": ["@planetscale/database@1.19.0", "", {}, "sha512-Tv4jcFUFAFjOWrGSio49H6R2ijALv0ZzVBfJKIdm+kl9X046Fh4LLawrF9OMsglVbK6ukqMJsUCeucGAFTBcMA=="], @@ -1189,9 +1194,9 @@ "@smithy/abort-controller": ["@smithy/abort-controller@4.2.3", "", { "dependencies": { "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-xWL9Mf8b7tIFuAlpjKtRPnHrR8XVrwTj5NPYO/QwZPtc0SDLsPxb56V5tzi5yspSMytISHybifez+4jlrx0vkQ=="], - "@smithy/config-resolver": ["@smithy/config-resolver@4.3.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.3", "@smithy/types": "^4.8.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.3", "tslib": "^2.6.2" } }, "sha512-xSql8A1Bl41O9JvGU/CtgiLBlwkvpHTSKRlvz9zOBvBCPjXghZ6ZkcVzmV2f7FLAA+80+aqKmIOmy8pEDrtCaw=="], + "@smithy/config-resolver": ["@smithy/config-resolver@4.4.0", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.3", "@smithy/types": "^4.8.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.3", "@smithy/util-middleware": "^4.2.3", "tslib": "^2.6.2" } }, "sha512-Kkmz3Mup2PGp/HNJxhCWkLNdlajJORLSjwkcfrj0E7nu6STAEdcMR1ir5P9/xOmncx8xXfru0fbUYLlZog/cFg=="], - "@smithy/core": ["@smithy/core@3.17.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.3", "@smithy/protocol-http": "^5.3.3", "@smithy/types": "^4.8.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.3", "@smithy/util-stream": "^4.5.3", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-Tir3DbfoTO97fEGUZjzGeoXgcQAUBRDTmuH9A8lxuP8ATrgezrAJ6cLuRvwdKN4ZbYNlHgKlBX69Hyu3THYhtg=="], + "@smithy/core": ["@smithy/core@3.17.1", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.3", "@smithy/protocol-http": "^5.3.3", "@smithy/types": "^4.8.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.3", "@smithy/util-stream": "^4.5.4", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-V4Qc2CIb5McABYfaGiIYLTmo/vwNIK7WXI5aGveBd9UcdhbOMwcvIMxIw/DJj1S9QgOMa/7FBkarMdIC0EOTEQ=="], "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.3", "@smithy/property-provider": "^4.2.3", "@smithy/types": "^4.8.0", "@smithy/url-parser": "^4.2.3", "tslib": "^2.6.2" } }, "sha512-hA1MQ/WAHly4SYltJKitEsIDVsNmXcQfYBRv2e+q04fnqtAX5qXaybxy/fhUeAMCnQIdAjaGDb04fMHQefWRhw=="], @@ -1207,9 +1212,9 @@ "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.3", "", { "dependencies": { "@smithy/protocol-http": "^5.3.3", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-/atXLsT88GwKtfp5Jr0Ks1CSa4+lB+IgRnkNrrYP0h1wL4swHNb0YONEvTceNKNdZGJsye+W2HH8W7olbcPUeA=="], - "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.4", "", { "dependencies": { "@smithy/core": "^3.17.0", "@smithy/middleware-serde": "^4.2.3", "@smithy/node-config-provider": "^4.3.3", "@smithy/shared-ini-file-loader": "^4.3.3", "@smithy/types": "^4.8.0", "@smithy/url-parser": "^4.2.3", "@smithy/util-middleware": "^4.2.3", "tslib": "^2.6.2" } }, "sha512-/RJhpYkMOaUZoJEkddamGPPIYeKICKXOu/ojhn85dKDM0n5iDIhjvYAQLP3K5FPhgB203O3GpWzoK2OehEoIUw=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.5", "", { "dependencies": { "@smithy/core": "^3.17.1", "@smithy/middleware-serde": "^4.2.3", "@smithy/node-config-provider": "^4.3.3", "@smithy/shared-ini-file-loader": "^4.3.3", "@smithy/types": "^4.8.0", "@smithy/url-parser": "^4.2.3", "@smithy/util-middleware": "^4.2.3", "tslib": "^2.6.2" } }, "sha512-SIzKVTvEudFWJbxAaq7f2GvP3jh2FHDpIFI6/VAf4FOWGFZy0vnYMPSRj8PGYI8Hjt29mvmwSRgKuO3bK4ixDw=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.3", "@smithy/protocol-http": "^5.3.3", "@smithy/service-error-classification": "^4.2.3", "@smithy/smithy-client": "^4.9.0", "@smithy/types": "^4.8.0", "@smithy/util-middleware": "^4.2.3", "@smithy/util-retry": "^4.2.3", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-vSgABQAkuUHRO03AhR2rWxVQ1un284lkBn+NFawzdahmzksAoOeVMnXXsuPViL4GlhRHXqFaMlc8Mj04OfQk1w=="], + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.3", "@smithy/protocol-http": "^5.3.3", "@smithy/service-error-classification": "^4.2.3", "@smithy/smithy-client": "^4.9.1", "@smithy/types": "^4.8.0", "@smithy/util-middleware": "^4.2.3", "@smithy/util-retry": "^4.2.3", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-DCaXbQqcZ4tONMvvdz+zccDE21sLcbwWoNqzPLFlZaxt1lDtOE2tlVpRSwcTOJrjJSUThdgEYn7HrX5oLGlK9A=="], "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.3", "", { "dependencies": { "@smithy/protocol-http": "^5.3.3", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-8g4NuUINpYccxiCXM5s1/V+uLtts8NcX4+sPEbvYQDZk4XoJfDpq5y2FQxfmUL89syoldpzNzA0R9nhzdtdKnQ=="], @@ -1217,7 +1222,7 @@ "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.3", "", { "dependencies": { "@smithy/property-provider": "^4.2.3", "@smithy/shared-ini-file-loader": "^4.3.3", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-NzI1eBpBSViOav8NVy1fqOlSfkLgkUjUTlohUSgAEhHaFWA3XJiLditvavIP7OpvTjDp5u2LhtlBhkBlEisMwA=="], - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.2", "", { "dependencies": { "@smithy/abort-controller": "^4.2.3", "@smithy/protocol-http": "^5.3.3", "@smithy/querystring-builder": "^4.2.3", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-MHFvTjts24cjGo1byXqhXrbqm7uznFD/ESFx8npHMWTFQVdBZjrT1hKottmp69LBTRm/JQzP/sn1vPt0/r6AYQ=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.3", "", { "dependencies": { "@smithy/abort-controller": "^4.2.3", "@smithy/protocol-http": "^5.3.3", "@smithy/querystring-builder": "^4.2.3", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-MAwltrDB0lZB/H6/2M5PIsISSwdI5yIh6DaBB9r0Flo9nx3y0dzl/qTMJPd7tJvPdsx6Ks/cwVzheGNYzXyNbQ=="], "@smithy/property-provider": ["@smithy/property-provider@4.2.3", "", { "dependencies": { "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-+1EZ+Y+njiefCohjlhyOcy1UNYjT+1PwGFHCxA/gYctjg3DQWAU19WigOXAco/Ql8hZokNehpzLd0/+3uCreqQ=="], @@ -1233,7 +1238,7 @@ "@smithy/signature-v4": ["@smithy/signature-v4@5.3.3", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.3", "@smithy/types": "^4.8.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.3", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-CmSlUy+eEYbIEYN5N3vvQTRfqt0lJlQkaQUIf+oizu7BbDut0pozfDjBGecfcfWf7c62Yis4JIEgqQ/TCfodaA=="], - "@smithy/smithy-client": ["@smithy/smithy-client@4.9.0", "", { "dependencies": { "@smithy/core": "^3.17.0", "@smithy/middleware-endpoint": "^4.3.4", "@smithy/middleware-stack": "^4.2.3", "@smithy/protocol-http": "^5.3.3", "@smithy/types": "^4.8.0", "@smithy/util-stream": "^4.5.3", "tslib": "^2.6.2" } }, "sha512-qz7RTd15GGdwJ3ZCeBKLDQuUQ88m+skh2hJwcpPm1VqLeKzgZvXf6SrNbxvx7uOqvvkjCMXqx3YB5PDJyk00ww=="], + "@smithy/smithy-client": ["@smithy/smithy-client@4.9.1", "", { "dependencies": { "@smithy/core": "^3.17.1", "@smithy/middleware-endpoint": "^4.3.5", "@smithy/middleware-stack": "^4.2.3", "@smithy/protocol-http": "^5.3.3", "@smithy/types": "^4.8.0", "@smithy/util-stream": "^4.5.4", "tslib": "^2.6.2" } }, "sha512-Ngb95ryR5A9xqvQFT5mAmYkCwbXvoLavLFwmi7zVg/IowFPCfiqRfkOKnbc/ZRL8ZKJ4f+Tp6kSu6wjDQb8L/g=="], "@smithy/types": ["@smithy/types@4.8.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QpELEHLO8SsQVtqP+MkEgCYTFW0pleGozfs3cZ183ZBj9z3VC1CX1/wtFMK64p+5bhtZo41SeLK1rBRtd25nHQ=="], @@ -1249,9 +1254,9 @@ "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="], - "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.3", "", { "dependencies": { "@smithy/property-provider": "^4.2.3", "@smithy/smithy-client": "^4.9.0", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-vqHoybAuZXbFXZqgzquiUXtdY+UT/aU33sxa4GBPkiYklmR20LlCn+d3Wc3yA5ZM13gQ92SZe/D8xh6hkjx+IQ=="], + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.4", "", { "dependencies": { "@smithy/property-provider": "^4.2.3", "@smithy/smithy-client": "^4.9.1", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-qI5PJSW52rnutos8Bln8nwQZRpyoSRN6k2ajyoUHNMUzmWqHnOJCnDELJuV6m5PML0VkHI+XcXzdB+6awiqYUw=="], - "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.4", "", { "dependencies": { "@smithy/config-resolver": "^4.3.3", "@smithy/credential-provider-imds": "^4.2.3", "@smithy/node-config-provider": "^4.3.3", "@smithy/property-provider": "^4.2.3", "@smithy/smithy-client": "^4.9.0", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-X5/xrPHedifo7hJUUWKlpxVb2oDOiqPUXlvsZv1EZSjILoutLiJyWva3coBpn00e/gPSpH8Rn2eIbgdwHQdW7Q=="], + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.6", "", { "dependencies": { "@smithy/config-resolver": "^4.4.0", "@smithy/credential-provider-imds": "^4.2.3", "@smithy/node-config-provider": "^4.3.3", "@smithy/property-provider": "^4.2.3", "@smithy/smithy-client": "^4.9.1", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-c6M/ceBTm31YdcFpgfgQAJaw3KbaLuRKnAz91iMWFLSrgxRpYm03c3bu5cpYojNMfkV9arCUelelKA7XQT36SQ=="], "@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.3", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-aCfxUOVv0CzBIkU10TubdgKSx5uRvzH064kaiPEWfNIvKOtNpu642P4FP1hgOFkjQIkDObrfIDnKMKkeyrejvQ=="], @@ -1261,7 +1266,7 @@ "@smithy/util-retry": ["@smithy/util-retry@4.2.3", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.3", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-lLPWnakjC0q9z+OtiXk+9RPQiYPNAovt2IXD3CP4LkOnd9NpUsxOjMx1SnoUVB7Orb7fZp67cQMtTBKMFDvOGg=="], - "@smithy/util-stream": ["@smithy/util-stream@4.5.3", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.4", "@smithy/node-http-handler": "^4.4.2", "@smithy/types": "^4.8.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-oZvn8a5bwwQBNYHT2eNo0EU8Kkby3jeIg1P2Lu9EQtqDxki1LIjGRJM6dJ5CZUig8QmLxWxqOKWvg3mVoOBs5A=="], + "@smithy/util-stream": ["@smithy/util-stream@4.5.4", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.4", "@smithy/node-http-handler": "^4.4.3", "@smithy/types": "^4.8.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-+qDxSkiErejw1BAIXUFBSfM5xh3arbz1MmxlbMCKanDDZtVEQ7PSKW9FQS0Vud1eI/kYn0oCTVKyNzRlq+9MUw=="], "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], @@ -1269,6 +1274,8 @@ "@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@solid-primitives/active-element": ["@solid-primitives/active-element@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9t5K4aR2naVDj950XU8OjnLgOg94a8k5wr6JNOPK+N5ESLsJDq42c1ZP8UKpewi1R+wplMMxiM6OPKRzbxJY7A=="], + "@solid-primitives/event-bus": ["@solid-primitives/event-bus@1.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-l+n10/51neGcMaP3ypYt21bXfoeWh8IaC8k7fYuY3ww2a8S1Zv2N2a7FF5Qn+waTu86l0V8/nRHjkyqVIZBYwA=="], "@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="], @@ -1589,9 +1596,9 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "bare-events": ["bare-events@2.8.0", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA=="], + "bare-events": ["bare-events@2.8.1", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-oxSAxTS1hRfnyit2CL5QpAOS5ixfBjj6ex3yTNvXyY/kE719jQ/IjuESJBK2w5v4wwQRAHGseVJXx9QBYOtFGQ=="], - "bare-fs": ["bare-fs@4.4.11", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-Bejmm9zRMvMTRoHS+2adgmXw1ANZnCNx+B5dgZpGwlP1E3x6Yuxea8RToddHUbWtVV0iUMWqsgZr8+jcgUI2SA=="], + "bare-fs": ["bare-fs@4.5.0", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-GljgCjeupKZJNetTqxKaQArLK10vpmK28or0+RwWjEl5Rk+/xG3wkpmkv+WrcBm3q1BwHKlnhXzR8O37kcvkXQ=="], "bare-os": ["bare-os@3.6.2", "", {}, "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A=="], @@ -1641,7 +1648,7 @@ "brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="], - "browserslist": ["browserslist@4.26.3", "", { "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w=="], + "browserslist": ["browserslist@4.27.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", "electron-to-chromium": "^1.5.238", "node-releases": "^2.0.26", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw=="], "buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="], @@ -1851,7 +1858,7 @@ "deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="], - "devalue": ["devalue@5.4.1", "", {}, "sha512-YtoaOfsqjbZQKGIMRYDWKjUmSB4VJ/RElB+bXZawQAQYAo4xu08GKTMVlsZDTF6R2MbAgjcAQRPI5eIyRAT2OQ=="], + "devalue": ["devalue@5.4.2", "", {}, "sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], @@ -1899,7 +1906,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.237", "", {}, "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg=="], + "electron-to-chromium": ["electron-to-chromium@1.5.239", "", {}, "sha512-1y5w0Zsq39MSPmEjHjbizvhYoTaulVtivpxkp5q5kaPmQtsK6/2nvAzGRxNMS9DoYySp9PkW0MAQDwU1m764mg=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -2101,7 +2108,7 @@ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], - "get-tsconfig": ["get-tsconfig@4.12.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw=="], + "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], @@ -3415,7 +3422,7 @@ "unwasm": ["unwasm@0.3.11", "", { "dependencies": { "knitwork": "^1.2.0", "magic-string": "^0.30.17", "mlly": "^1.7.4", "pathe": "^2.0.3", "pkg-types": "^2.2.0", "unplugin": "^2.3.6" } }, "sha512-Vhp5gb1tusSQw5of/g3Q697srYgMXvwMgXMjcG4ZNga02fDX9coxJ9fAb0Ci38hM2Hv/U1FXRPGgjP2BYqhNoQ=="], - "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], + "update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "uqr": ["uqr@0.1.2", "", {}, "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA=="], @@ -3577,6 +3584,8 @@ "@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="], + "@astrojs/markdown-remark/shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="], + "@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.8", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.4", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.13.0", "smol-toml": "^1.4.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-uFNyFWadnULWK2cOw4n0hLKeu+xaVWeuECdP10cQ3K2fkybtTlhb7J7TcScdjmS8Yps7oje9S/ehYMfZrhrgCg=="], "@astrojs/sitemap/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -3707,6 +3716,12 @@ "@parcel/watcher-wasm/napi-wasm": ["napi-wasm@1.1.3", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="], + "@pierre/precision-diffs/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="], + + "@pierre/precision-diffs/@shikijs/transformers": ["@shikijs/transformers@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/types": "3.13.0" } }, "sha512-833lcuVzcRiG+fXvgslWsM2f4gHpjEgui1ipIknSizRuTgMkNZupiXE5/TVJ6eSYfhNBFhBZKkReKWO2GgYmqA=="], + + "@pierre/precision-diffs/shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="], + "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], "@rollup/plugin-commonjs/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], @@ -3777,6 +3792,8 @@ "astro/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], + "astro/shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="], + "astro/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], "astro/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -4093,6 +4110,18 @@ "@actions/github/@octokit/request-error/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], + "@astrojs/markdown-remark/shiki/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="], + + "@astrojs/markdown-remark/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="], + + "@astrojs/markdown-remark/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="], + + "@astrojs/markdown-remark/shiki/@shikijs/langs": ["@shikijs/langs@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ=="], + + "@astrojs/markdown-remark/shiki/@shikijs/themes": ["@shikijs/themes@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg=="], + + "@astrojs/markdown-remark/shiki/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="], + "@astrojs/mdx/@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.4", "", {}, "sha512-lDA9MqE8WGi7T/t2BMi+EAXhs4Vcvr94Gqx3q15cFEz8oFZMO4/SFBqYr/UcmNlvW+35alowkVj+w9VhLvs5Cw=="], "@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="], @@ -4287,6 +4316,20 @@ "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@pierre/precision-diffs/@shikijs/core/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="], + + "@pierre/precision-diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="], + + "@pierre/precision-diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="], + + "@pierre/precision-diffs/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="], + + "@pierre/precision-diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ=="], + + "@pierre/precision-diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg=="], + + "@pierre/precision-diffs/shiki/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="], + "@slack/web-api/p-queue/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], "@slack/web-api/p-queue/p-timeout": ["p-timeout@3.2.0", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="], @@ -4323,6 +4366,18 @@ "archiver-utils/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "astro/shiki/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="], + + "astro/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="], + + "astro/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="], + + "astro/shiki/@shikijs/langs": ["@shikijs/langs@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ=="], + + "astro/shiki/@shikijs/themes": ["@shikijs/themes@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg=="], + + "astro/shiki/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="], + "axios/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="], diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 6e3cc3acf..aacd2da34 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -25,7 +25,9 @@ "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/ui": "workspace:*", "@shikijs/transformers": "3.9.2", + "@solid-primitives/active-element": "2.1.3", "@solid-primitives/event-bus": "1.1.2", "@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/scroll": "2.1.3", @@ -33,14 +35,13 @@ "@solidjs/router": "0.15.3", "@thisbeyond/solid-dnd": "0.7.5", "diff": "catalog:", - "@opencode-ai/ui": "workspace:*", "fuzzysort": "catalog:", "luxon": "catalog:", "marked": "16.2.0", "marked-shiki": "1.2.1", "remeda": "catalog:", - "solid-js": "catalog:", "shiki": "3.9.2", + "solid-js": "catalog:", "solid-list": "catalog:", "tailwindcss": "catalog:", "virtua": "catalog:" diff --git a/packages/desktop/src/components/code.tsx b/packages/desktop/src/components/code.tsx index 40a40aa9a..b4dd216e9 100644 --- a/packages/desktop/src/components/code.tsx +++ b/packages/desktop/src/components/code.tsx @@ -394,7 +394,7 @@ export function Code(props: Props) { [&_.diff-blank_.diff-oldln]:bg-background-element [&_.diff-blank_.diff-newln]:bg-background-element [&_.diff-collapsed]:block! [&_.diff-collapsed]:w-full [&_.diff-collapsed]:relative - [&_.diff-collapsed]:cursor-pointer [&_.diff-collapsed]:select-none + [&_.diff-collapsed]:select-none [&_.diff-collapsed]:bg-info/20 [&_.diff-collapsed]:hover:bg-info/40! [&_.diff-collapsed]:text-info/80 [&_.diff-collapsed]:hover:text-info [&_.diff-collapsed]:text-xs diff --git a/packages/desktop/src/components/editor-pane.tsx b/packages/desktop/src/components/editor-pane.tsx index 2741a6208..a97a0ef7f 100644 --- a/packages/desktop/src/components/editor-pane.tsx +++ b/packages/desktop/src/components/editor-pane.tsx @@ -1,7 +1,6 @@ import { For, Match, Show, Switch, createSignal, splitProps } from "solid-js" -import { Tabs, Tooltip } from "@opencode-ai/ui" -import { Icon } from "@opencode-ai/ui" -import { FileIcon, IconButton } from "@/ui" +import { IconButton, Tabs, Tooltip } from "@opencode-ai/ui" +import { FileIcon } from "@/ui" import { DragDropProvider, DragDropSensors, @@ -92,20 +91,16 @@ export default function EditorPane(props: EditorPaneProps): JSX.Element {
- navigateChange(-1)}> - - + navigateChange(-1)} /> - navigateChange(1)}> - - + navigateChange(1)} />
local.file.setView(activeFile.path, "raw")} - > - - + /> local.file.setView(activeFile.path, "diff-unified")} - > - - + /> local.file.setView(activeFile.path, "diff-split")} - > - - + /> ) @@ -221,13 +210,11 @@ function SortableTab(props: { props.onTabClose(props.file)} - > - - + /> diff --git a/packages/desktop/src/components/file-tree.tsx b/packages/desktop/src/components/file-tree.tsx index 348e25ad7..7e4b1abcc 100644 --- a/packages/desktop/src/components/file-tree.tsx +++ b/packages/desktop/src/components/file-tree.tsx @@ -19,7 +19,7 @@ export default function FileTree(props: { - + , - candidate: AttachmentCandidate, - tokens: (string | undefined)[], -) { - for (const token of tokens) { - if (!token) continue - const normalized = token.toLowerCase() - if (map.has(normalized)) continue - map.set(normalized, candidate) - } -} - -export function parsePrompt(value: string, lookup: Map) { - const segments: PromptSegment[] = [] - if (!value) return { parts: [] as PromptContentPart[], segments } - - const pushTextRange = (rangeStart: number, rangeEnd: number) => { - if (rangeEnd <= rangeStart) return - const text = value.slice(rangeStart, rangeEnd) - let cursor = 0 - for (const match of text.matchAll(mentionPattern)) { - const localIndex = match.index ?? 0 - if (localIndex > cursor) { - addTextSegment(segments, rangeStart + cursor, text.slice(cursor, localIndex)) - } - const token = match[1] - const candidate = lookup.get(token.toLowerCase()) - if (candidate) { - const start = rangeStart + localIndex - const end = start + match[0].length - segments.push({ - kind: "attachment", - token, - display: candidate.display, - path: candidate.path, - selection: candidate.selection, - origin: candidate.origin, - start, - end, - }) - } else { - addTextSegment(segments, rangeStart + localIndex, match[0]) - } - cursor = localIndex + match[0].length - } - if (cursor < text.length) { - addTextSegment(segments, rangeStart + cursor, text.slice(cursor)) - } - } - - pushTextRange(0, value.length) - - const parts: PromptContentPart[] = [] - for (const segment of segments) { - if (segment.kind === "text") { - pushTextPart(parts, segment.value) - } else { - const { start, end, ...attachment } = segment - parts.push(attachment as PromptAttachmentPart) - } - } - return { parts, segments } -} - -export function composeDisplaySegments( - segments: PromptSegment[], - inputValue: string, - interim: string, -): PromptDisplaySegment[] { - if (segments.length === 0 && !interim) return [] - - const display: PromptDisplaySegment[] = segments.map((segment) => { - if (segment.kind === "text") { - return { kind: "text", value: segment.value } - } - const { start, end, ...part } = segment - const placeholder = inputValue.slice(start, end) - return { kind: "attachment", part: part as PromptAttachmentPart, source: placeholder } - }) - - if (interim) { - const leadingSpace = !!(inputValue && !inputValue.endsWith(" ") && !interim.startsWith(" ")) - display.push({ kind: "interim", value: interim, leadingSpace }) - } - - return display -} diff --git a/packages/desktop/src/components/prompt-form-hooks.ts b/packages/desktop/src/components/prompt-form-hooks.ts deleted file mode 100644 index 026b84359..000000000 --- a/packages/desktop/src/components/prompt-form-hooks.ts +++ /dev/null @@ -1,396 +0,0 @@ -import { createEffect, createMemo, createResource, type Accessor } from "solid-js" -import type { SetStoreFunction } from "solid-js/store" -import { getDirectory, getFilename } from "@/utils" -import { createSpeechRecognition } from "@/utils/speech" -import { - createAttachmentDisplay, - mentionPattern, - mentionTriggerPattern, - type PromptAttachmentPart, - type PromptAttachmentSegment, -} from "./prompt-form-helpers" -import type { LocalFile, TextSelection } from "@/context/local" - -export type MentionRange = { - start: number - end: number -} - -export interface PromptFormState { - promptInput: string - isDragOver: boolean - mentionOpen: boolean - mentionQuery: string - mentionRange: MentionRange | undefined - mentionIndex: number - mentionAnchorOffset: { x: number; y: number } - inlineAliases: Map -} - -interface MentionControllerOptions { - state: PromptFormState - setState: SetStoreFunction - attachmentSegments: Accessor - getInputRef: () => HTMLTextAreaElement | undefined - getOverlayRef: () => HTMLDivElement | undefined - getMeasureRef: () => HTMLDivElement | undefined - searchFiles: (query: string) => Promise - resolveFile: (path: string) => LocalFile | undefined - addContextFile: (path: string, selection?: TextSelection) => void - getActiveContext: () => { path: string; selection?: TextSelection } | undefined -} - -interface MentionKeyDownOptions { - event: KeyboardEvent & { currentTarget: HTMLTextAreaElement } - mentionItems: () => string[] - insertMention: (path: string) => void -} - -interface ScrollSyncOptions { - state: PromptFormState - getInputRef: () => HTMLTextAreaElement | undefined - getOverlayRef: () => HTMLDivElement | undefined - interim: Accessor - updateMentionPosition: (element: HTMLTextAreaElement, range?: MentionRange) => void -} - -export function usePromptSpeech(updatePromptInput: (updater: (prev: string) => string) => void) { - return createSpeechRecognition({ - onFinal: (text) => updatePromptInput((prev) => (prev && !prev.endsWith(" ") ? `${prev} ` : prev) + text), - }) -} - -export function useMentionController(options: MentionControllerOptions) { - const mentionSource = createMemo(() => (options.state.mentionOpen ? options.state.mentionQuery : undefined)) - const [mentionResults, { mutate: mutateMentionResults }] = createResource(mentionSource, (query) => { - if (!options.state.mentionOpen) return [] - return options.searchFiles(query ?? "") - }) - const mentionItems = createMemo(() => mentionResults() ?? []) - - createEffect(() => { - if (!options.state.mentionOpen) return - options.state.mentionQuery - options.setState("mentionIndex", 0) - }) - - createEffect(() => { - if (!options.state.mentionOpen) return - queueMicrotask(() => { - const input = options.getInputRef() - if (!input) return - if (document.activeElement === input) return - input.focus() - }) - }) - - createEffect(() => { - const used = new Set() - for (const match of options.state.promptInput.matchAll(mentionPattern)) { - const token = match[1] - if (token) used.add(token.toLowerCase()) - } - options.setState("inlineAliases", (prev) => { - if (prev.size === 0) return prev - const next = new Map(prev) - let changed = false - for (const key of prev.keys()) { - if (!used.has(key.toLowerCase())) { - next.delete(key) - changed = true - } - } - return changed ? next : prev - }) - }) - - createEffect(() => { - if (!options.state.mentionOpen) return - const items = mentionItems() - if (items.length === 0) { - options.setState("mentionIndex", 0) - return - } - if (options.state.mentionIndex < items.length) return - options.setState("mentionIndex", items.length - 1) - }) - - createEffect(() => { - if (!options.state.mentionOpen) return - const rangeValue = options.state.mentionRange - if (!rangeValue) return - options.state.promptInput - queueMicrotask(() => { - const input = options.getInputRef() - if (!input) return - updateMentionPosition(input, rangeValue) - }) - }) - - function closeMention() { - if (options.state.mentionOpen) options.setState("mentionOpen", false) - options.setState("mentionQuery", "") - options.setState("mentionRange", undefined) - options.setState("mentionIndex", 0) - mutateMentionResults(() => undefined) - options.setState("mentionAnchorOffset", { x: 0, y: 0 }) - } - - function updateMentionPosition(element: HTMLTextAreaElement, rangeValue = options.state.mentionRange) { - const measure = options.getMeasureRef() - if (!measure) return - if (!rangeValue) return - measure.style.width = `${element.clientWidth}px` - const measurement = element.value.slice(0, rangeValue.end) - measure.textContent = measurement - const caretSpan = document.createElement("span") - caretSpan.textContent = "\u200b" - measure.append(caretSpan) - const caretRect = caretSpan.getBoundingClientRect() - const containerRect = measure.getBoundingClientRect() - measure.removeChild(caretSpan) - const left = caretRect.left - containerRect.left - const top = caretRect.top - containerRect.top - element.scrollTop - options.setState("mentionAnchorOffset", { x: left, y: top < 0 ? 0 : top }) - } - - function isValidMentionQuery(value: string) { - return /^[A-Za-z0-9_\-./]*$/.test(value) - } - - function syncMentionFromCaret(element: HTMLTextAreaElement) { - if (!options.state.mentionOpen) return - const rangeValue = options.state.mentionRange - if (!rangeValue) { - closeMention() - return - } - const caret = element.selectionEnd ?? element.selectionStart ?? element.value.length - if (rangeValue.start < 0 || rangeValue.start >= element.value.length) { - closeMention() - return - } - if (element.value[rangeValue.start] !== "@") { - closeMention() - return - } - if (caret <= rangeValue.start) { - closeMention() - return - } - const mentionValue = element.value.slice(rangeValue.start + 1, caret) - if (!isValidMentionQuery(mentionValue)) { - closeMention() - return - } - options.setState("mentionRange", { start: rangeValue.start, end: caret }) - options.setState("mentionQuery", mentionValue) - updateMentionPosition(element, { start: rangeValue.start, end: caret }) - } - - function tryOpenMentionFromCaret(element: HTMLTextAreaElement) { - const selectionStart = element.selectionStart ?? element.value.length - const selectionEnd = element.selectionEnd ?? selectionStart - if (selectionStart !== selectionEnd) return false - const caret = selectionEnd - if (options.attachmentSegments().some((segment) => caret >= segment.start && caret <= segment.end)) { - return false - } - const before = element.value.slice(0, caret) - const match = before.match(mentionTriggerPattern) - if (!match) return false - const token = match[2] ?? "" - const start = caret - token.length - 1 - if (start < 0) return false - options.setState("mentionOpen", true) - options.setState("mentionRange", { start, end: caret }) - options.setState("mentionQuery", token) - options.setState("mentionIndex", 0) - queueMicrotask(() => { - updateMentionPosition(element, { start, end: caret }) - }) - return true - } - - function handlePromptInput(event: InputEvent & { currentTarget: HTMLTextAreaElement }) { - const element = event.currentTarget - options.setState("promptInput", element.value) - if (options.state.mentionOpen) { - syncMentionFromCaret(element) - if (options.state.mentionOpen) return - } - const isDeletion = event.inputType ? event.inputType.startsWith("delete") : false - if (!isDeletion && tryOpenMentionFromCaret(element)) return - closeMention() - } - - function handleMentionKeyDown({ event, mentionItems: items, insertMention }: MentionKeyDownOptions) { - if (!options.state.mentionOpen) return false - const list = items() - if (event.key === "ArrowDown") { - event.preventDefault() - if (list.length === 0) return true - const next = options.state.mentionIndex + 1 >= list.length ? 0 : options.state.mentionIndex + 1 - options.setState("mentionIndex", next) - return true - } - if (event.key === "ArrowUp") { - event.preventDefault() - if (list.length === 0) return true - const previous = options.state.mentionIndex - 1 < 0 ? list.length - 1 : options.state.mentionIndex - 1 - options.setState("mentionIndex", previous) - return true - } - if (event.key === "Enter") { - event.preventDefault() - const targetItem = list[options.state.mentionIndex] ?? list[0] - if (targetItem) insertMention(targetItem) - return true - } - if (event.key === "Escape") { - event.preventDefault() - closeMention() - return true - } - return false - } - - function generateMentionAlias(path: string) { - const existing = new Set() - for (const key of options.state.inlineAliases.keys()) { - existing.add(key.toLowerCase()) - } - for (const match of options.state.promptInput.matchAll(mentionPattern)) { - const token = match[1] - if (token) existing.add(token.toLowerCase()) - } - - const base = getFilename(path) - if (base) { - if (!existing.has(base.toLowerCase())) return base - } - - const directory = getDirectory(path) - if (base && directory) { - const segments = directory.split("/").filter(Boolean) - for (let i = segments.length - 1; i >= 0; i -= 1) { - const candidate = `${segments.slice(i).join("/")}/${base}` - if (!existing.has(candidate.toLowerCase())) return candidate - } - } - - if (!existing.has(path.toLowerCase())) return path - - const fallback = base || path || "file" - let index = 2 - let candidate = `${fallback}-${index}` - while (existing.has(candidate.toLowerCase())) { - index += 1 - candidate = `${fallback}-${index}` - } - return candidate - } - - function insertMention(path: string) { - const input = options.getInputRef() - if (!input) return - const rangeValue = options.state.mentionRange - if (!rangeValue) return - const node = options.resolveFile(path) - const alias = generateMentionAlias(path) - const mentionText = `@${alias}` - const value = options.state.promptInput - const before = value.slice(0, rangeValue.start) - const after = value.slice(rangeValue.end) - const needsLeadingSpace = before.length > 0 && !/\s$/.test(before) - const needsTrailingSpace = after.length > 0 && !/^\s/.test(after) - const leading = needsLeadingSpace ? `${before} ` : before - const trailingSpacer = needsTrailingSpace ? " " : "" - const nextValue = `${leading}${mentionText}${trailingSpacer}${after}` - const origin = options.getActiveContext()?.path === path ? "active" : "context" - const part: PromptAttachmentPart = { - kind: "attachment", - token: alias, - display: createAttachmentDisplay(path, node?.selection), - path, - selection: node?.selection, - origin, - } - options.setState("promptInput", nextValue) - if (input.value !== nextValue) { - input.value = nextValue - } - options.setState("inlineAliases", (prev) => { - const next = new Map(prev) - next.set(alias, part) - return next - }) - options.addContextFile(path, node?.selection) - closeMention() - queueMicrotask(() => { - const caret = leading.length + mentionText.length + trailingSpacer.length - input.setSelectionRange(caret, caret) - syncMentionFromCaret(input) - }) - } - - return { - mentionResults, - mentionItems, - closeMention, - syncMentionFromCaret, - tryOpenMentionFromCaret, - updateMentionPosition, - handlePromptInput, - handleMentionKeyDown, - insertMention, - } -} - -export function usePromptScrollSync(options: ScrollSyncOptions) { - let shouldAutoScroll = true - - createEffect(() => { - options.state.promptInput - options.interim() - queueMicrotask(() => { - const input = options.getInputRef() - const overlay = options.getOverlayRef() - if (!input || !overlay) return - if (!shouldAutoScroll) { - overlay.scrollTop = input.scrollTop - if (options.state.mentionOpen) options.updateMentionPosition(input) - return - } - const maxInputScroll = input.scrollHeight - input.clientHeight - const next = maxInputScroll > 0 ? maxInputScroll : 0 - input.scrollTop = next - overlay.scrollTop = next - if (options.state.mentionOpen) options.updateMentionPosition(input) - }) - }) - - function handlePromptScroll(event: Event & { currentTarget: HTMLTextAreaElement }) { - const target = event.currentTarget - shouldAutoScroll = target.scrollTop + target.clientHeight >= target.scrollHeight - 4 - const overlay = options.getOverlayRef() - if (overlay) overlay.scrollTop = target.scrollTop - if (options.state.mentionOpen) options.updateMentionPosition(target) - } - - function resetScrollPosition() { - shouldAutoScroll = true - const input = options.getInputRef() - const overlay = options.getOverlayRef() - if (input) input.scrollTop = 0 - if (overlay) overlay.scrollTop = 0 - } - - return { - handlePromptScroll, - resetScrollPosition, - setAutoScroll: (value: boolean) => { - shouldAutoScroll = value - }, - } -} diff --git a/packages/desktop/src/components/prompt-form.tsx b/packages/desktop/src/components/prompt-form.tsx deleted file mode 100644 index 06fbfbb03..000000000 --- a/packages/desktop/src/components/prompt-form.tsx +++ /dev/null @@ -1,581 +0,0 @@ -import { For, Show, createMemo, onCleanup, type JSX } from "solid-js" -import { createStore } from "solid-js/store" -import { Popover } from "@kobalte/core/popover" -import { Tooltip, Button, Icon, Select } from "@opencode-ai/ui" -import { FileIcon, IconButton } from "@/ui" -import { useLocal } from "@/context" -import type { FileContext, LocalFile } from "@/context/local" -import { getDirectory, getFilename } from "@/utils" -import { composeDisplaySegments, createAttachmentDisplay, parsePrompt, registerCandidate } from "./prompt-form-helpers" -import type { - AttachmentCandidate, - PromptAttachmentPart, - PromptAttachmentSegment, - PromptDisplaySegment, - PromptSubmitValue, -} from "./prompt-form-helpers" -import { useMentionController, usePromptScrollSync, usePromptSpeech, type PromptFormState } from "./prompt-form-hooks" - -interface PromptFormProps { - class?: string - classList?: Record - onSubmit: (prompt: PromptSubmitValue) => Promise | void - onOpenModelSelect: () => void - onInputRefChange?: (element: HTMLTextAreaElement | undefined) => void -} - -export default function PromptForm(props: PromptFormProps) { - const local = useLocal() - - const [state, setState] = createStore({ - promptInput: "", - isDragOver: false, - mentionOpen: false, - mentionQuery: "", - mentionRange: undefined, - mentionIndex: 0, - mentionAnchorOffset: { x: 0, y: 0 }, - inlineAliases: new Map(), - }) - - const placeholderText = "Start typing or speaking..." - - const { - isSupported, - isRecording, - interim: interimTranscript, - start: startSpeech, - stop: stopSpeech, - } = usePromptSpeech((updater) => setState("promptInput", updater)) - - let inputRef: HTMLTextAreaElement | undefined = undefined - let overlayContainerRef: HTMLDivElement | undefined = undefined - let mentionMeasureRef: HTMLDivElement | undefined = undefined - - const attachmentLookup = createMemo(() => { - const map = new Map() - const activeFile = local.context.active() - if (activeFile) { - registerCandidate( - map, - { - origin: "active", - path: activeFile.path, - selection: activeFile.selection, - display: createAttachmentDisplay(activeFile.path, activeFile.selection), - }, - [activeFile.path, getFilename(activeFile.path)], - ) - } - for (const item of local.context.all()) { - registerCandidate( - map, - { - origin: "context", - path: item.path, - selection: item.selection, - display: createAttachmentDisplay(item.path, item.selection), - }, - [item.path, getFilename(item.path)], - ) - } - for (const [alias, part] of state.inlineAliases) { - registerCandidate( - map, - { - origin: part.origin, - path: part.path, - selection: part.selection, - display: part.display ?? createAttachmentDisplay(part.path, part.selection), - }, - [alias], - ) - } - return map - }) - - const parsedPrompt = createMemo(() => parsePrompt(state.promptInput, attachmentLookup())) - const baseParts = createMemo(() => parsedPrompt().parts) - const attachmentSegments = createMemo(() => - parsedPrompt().segments.filter((segment): segment is PromptAttachmentSegment => segment.kind === "attachment"), - ) - - const { - mentionResults, - mentionItems, - closeMention, - syncMentionFromCaret, - updateMentionPosition, - handlePromptInput, - handleMentionKeyDown, - insertMention, - } = useMentionController({ - state, - setState, - attachmentSegments, - getInputRef: () => inputRef, - getOverlayRef: () => overlayContainerRef, - getMeasureRef: () => mentionMeasureRef, - searchFiles: (query) => local.file.search(query), - resolveFile: (path) => local.file.node(path) ?? undefined, - addContextFile: (path, selection) => - local.context.add({ - type: "file", - path, - selection, - }), - getActiveContext: () => local.context.active() ?? undefined, - }) - - const { handlePromptScroll, resetScrollPosition } = usePromptScrollSync({ - state, - getInputRef: () => inputRef, - getOverlayRef: () => overlayContainerRef, - interim: () => (isRecording() ? interimTranscript() : ""), - updateMentionPosition, - }) - - const displaySegments = createMemo(() => { - const value = state.promptInput - const segments = parsedPrompt().segments - const interim = isRecording() ? interimTranscript() : "" - return composeDisplaySegments(segments, value, interim) - }) - - const hasDisplaySegments = createMemo(() => displaySegments().length > 0) - - function handleAttachmentNavigation( - event: KeyboardEvent & { currentTarget: HTMLTextAreaElement }, - direction: "left" | "right", - ) { - const element = event.currentTarget - const caret = element.selectionStart ?? 0 - const segments = attachmentSegments() - if (direction === "left") { - let match = segments.find((segment) => caret > segment.start && caret <= segment.end) - if (!match && element.selectionStart !== element.selectionEnd) { - match = segments.find( - (segment) => element.selectionStart === segment.start && element.selectionEnd === segment.end, - ) - } - if (!match) return false - event.preventDefault() - if (element.selectionStart === match.start && element.selectionEnd === match.end) { - const next = Math.max(0, match.start) - element.setSelectionRange(next, next) - syncMentionFromCaret(element) - return true - } - element.setSelectionRange(match.start, match.end) - syncMentionFromCaret(element) - return true - } - if (direction === "right") { - let match = segments.find((segment) => caret >= segment.start && caret < segment.end) - if (!match && element.selectionStart !== element.selectionEnd) { - match = segments.find( - (segment) => element.selectionStart === segment.start && element.selectionEnd === segment.end, - ) - } - if (!match) return false - event.preventDefault() - if (element.selectionStart === match.start && element.selectionEnd === match.end) { - const next = match.end - element.setSelectionRange(next, next) - syncMentionFromCaret(element) - return true - } - element.setSelectionRange(match.start, match.end) - syncMentionFromCaret(element) - return true - } - return false - } - - function renderAttachmentChip(part: PromptAttachmentPart, _placeholder: string) { - const display = part.display ?? createAttachmentDisplay(part.path, part.selection) - return @{display} - } - - function renderTextSegment(value: string) { - if (!value) return undefined - return {value} - } - - function handlePromptKeyDown(event: KeyboardEvent & { currentTarget: HTMLTextAreaElement }) { - if (event.isComposing) return - const target = event.currentTarget - const key = event.key - - const handled = handleMentionKeyDown({ - event, - mentionItems, - insertMention, - }) - if (handled) return - - if (!state.mentionOpen) { - if (key === "ArrowLeft") { - if (handleAttachmentNavigation(event, "left")) return - } - if (key === "ArrowRight") { - if (handleAttachmentNavigation(event, "right")) return - } - } - - if (key === "ArrowLeft" || key === "ArrowRight" || key === "Home" || key === "End") { - queueMicrotask(() => { - syncMentionFromCaret(target) - }) - } - - if (key === "Enter" && !event.shiftKey) { - event.preventDefault() - target.form?.requestSubmit() - } - } - - const handleSubmit = async (event: SubmitEvent) => { - event.preventDefault() - const parts = baseParts() - const text = parts - .map((part) => { - if (part.kind === "text") return part.value - return `@${part.path}` - }) - .join("") - - const currentPrompt: PromptSubmitValue = { - text, - parts, - } - setState("promptInput", "") - resetScrollPosition() - if (inputRef) { - inputRef.blur() - } - - await props.onSubmit(currentPrompt) - } - - onCleanup(() => { - props.onInputRefChange?.(undefined) - }) - - return ( -
-
{ - const evt = event as unknown as globalThis.DragEvent - if (evt.dataTransfer?.types.includes("text/plain")) { - evt.preventDefault() - setState("isDragOver", true) - } - }} - onDragLeave={(event) => { - if (event.currentTarget === event.target) { - setState("isDragOver", false) - } - }} - onDragOver={(event) => { - const evt = event as unknown as globalThis.DragEvent - if (evt.dataTransfer?.types.includes("text/plain")) { - evt.preventDefault() - evt.dataTransfer.dropEffect = "copy" - } - }} - onDrop={(event) => { - const evt = event as unknown as globalThis.DragEvent - evt.preventDefault() - setState("isDragOver", false) - - const data = evt.dataTransfer?.getData("text/plain") - if (data && data.startsWith("file:")) { - const filePath = data.slice(5) - const fileNode = local.file.node(filePath) - if (fileNode) { - local.context.add({ - type: "file", - path: filePath, - }) - } - } - }} - > - 0 || local.context.active()}> -
- - local.context.removeActive()} /> - - - {(file) => local.context.remove(file.key)} />} - -
-
-
- -
{ - overlayContainerRef = element ?? undefined - }} - class="pointer-events-none absolute inset-0 overflow-hidden" - > - -
-
{ - mentionMeasureRef = element ?? undefined - }} - class="pointer-events-none invisible absolute inset-0 whitespace-pre-wrap text-base font-light leading-relaxed px-0.5" - aria-hidden="true" - >
- setState("mentionIndex", index)} - onSelect={insertMention} - /> -
-
-
- agent.name)} + current={local.agent.current().name} + onSelect={local.agent.set} + class="capitalize" + /> + `${x.provider.id}:${x.id}`} + items={local.model.list()} + current={local.model.current()} + filterKeys={["provider.name", "name", "id"]} + groupBy={(x) => x.provider.name} + sortGroupsBy={(a, b) => { + const order = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"] + 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)} + trigger={ + + } + > + {(i) => ( +
+
+ +
+ {i.name} + + {DateTime.fromFormat(i.release_date, "yyyy-MM-dd").toFormat("LLL yyyy")} + +
+
+ +
Free
+
+
+ )} +
- -
-
+ +
+
) } @@ -223,7 +367,7 @@ function isEqual(arrA: ContentPart[], arrB: ContentPart[]): boolean { if (partA.type === "text" && partA.content !== (partB as TextPart).content) { return false } - if (partA.type === "attachment" && partA.fileId !== (partB as AttachmentPart).fileId) { + if (partA.type === "file" && partA.path !== (partB as FileAttachmentPart).path) { return false } } @@ -241,24 +385,48 @@ function getCursorPosition(parent: HTMLElement): number { } function setCursorPosition(parent: HTMLElement, position: number) { - let child = parent.firstChild - let offset = position - while (child) { - if (offset > child.textContent!.length) { - offset -= child.textContent!.length - child = child.nextSibling - } else { - try { - const range = document.createRange() - const sel = window.getSelection() - range.setStart(child, offset) - range.collapse(true) - sel?.removeAllRanges() - sel?.addRange(range) - } catch (e) { - console.error("Failed to set cursor position.", e) - } + let remaining = position + let node = parent.firstChild + while (node) { + const length = node.textContent ? node.textContent.length : 0 + const isText = node.nodeType === Node.TEXT_NODE + const isFile = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type === "file" + + if (isText && remaining <= length) { + const range = document.createRange() + const selection = window.getSelection() + range.setStart(node, remaining) + range.collapse(true) + selection?.removeAllRanges() + selection?.addRange(range) return } + + if (isFile && remaining <= length) { + const range = document.createRange() + const selection = window.getSelection() + range.setStartAfter(node) + range.collapse(true) + selection?.removeAllRanges() + selection?.addRange(range) + return + } + + remaining -= length + node = node.nextSibling } + + const fallbackRange = document.createRange() + const fallbackSelection = window.getSelection() + const last = parent.lastChild + if (last && last.nodeType === Node.TEXT_NODE) { + const len = last.textContent ? last.textContent.length : 0 + fallbackRange.setStart(last, len) + } + if (!last || last.nodeType !== Node.TEXT_NODE) { + fallbackRange.selectNodeContents(parent) + } + fallbackRange.collapse(false) + fallbackSelection?.removeAllRanges() + fallbackSelection?.addRange(fallbackRange) } diff --git a/packages/desktop/src/components/select-dialog.tsx b/packages/desktop/src/components/select-dialog.tsx deleted file mode 100644 index bf9aa0dbd..000000000 --- a/packages/desktop/src/components/select-dialog.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import { createEffect, Show, For, createMemo, type JSX, createResource } from "solid-js" -import { Dialog } from "@kobalte/core/dialog" -import { Icon } from "@opencode-ai/ui" -import { IconButton } from "@/ui" -import { createStore } from "solid-js/store" -import { entries, flatMap, groupBy, map, pipe } from "remeda" -import { createList } from "solid-list" -import fuzzysort from "fuzzysort" - -interface SelectDialogProps { - items: T[] | ((filter: string) => Promise) - key: (item: T) => string - render: (item: T) => JSX.Element - filter?: string[] - current?: T - placeholder?: string - groupBy?: (x: T) => string - onSelect?: (value: T | undefined) => void - onClose?: () => void -} - -export function SelectDialog(props: SelectDialogProps) { - let scrollRef: HTMLDivElement | undefined - const [store, setStore] = createStore({ - filter: "", - mouseActive: false, - }) - - const [grouped] = createResource( - () => store.filter, - async (filter) => { - const needle = filter.toLowerCase() - const all = (typeof props.items === "function" ? await props.items(needle) : props.items) || [] - const result = pipe( - all, - (x) => { - if (!needle) return x - if (!props.filter && Array.isArray(x) && x.every((e) => typeof e === "string")) { - return fuzzysort.go(needle, x).map((x) => x.target) as T[] - } - return fuzzysort.go(needle, x, { keys: props.filter! }).map((x) => x.obj) - }, - groupBy((x) => (props.groupBy ? props.groupBy(x) : "")), - // mapValues((x) => x.sort((a, b) => props.key(a).localeCompare(props.key(b)))), - entries(), - map(([k, v]) => ({ category: k, items: v })), - ) - return result - }, - ) - const flat = createMemo(() => { - return pipe( - grouped() || [], - flatMap((x) => x.items), - ) - }) - const list = createList({ - items: () => flat().map(props.key), - initialActive: props.current ? props.key(props.current) : undefined, - loop: true, - }) - const resetSelection = () => { - const all = flat() - if (all.length === 0) return - list.setActive(props.key(all[0])) - } - - createEffect(() => { - store.filter - scrollRef?.scrollTo(0, 0) - resetSelection() - }) - - createEffect(() => { - const all = flat() - if (store.mouseActive || all.length === 0) return - if (list.active() === props.key(all[0])) { - scrollRef?.scrollTo(0, 0) - return - } - const element = scrollRef?.querySelector(`[data-key="${list.active()}"]`) - element?.scrollIntoView({ block: "nearest", behavior: "smooth" }) - }) - - const handleInput = (value: string) => { - setStore("filter", value) - resetSelection() - } - - const handleSelect = (item: T) => { - props.onSelect?.(item) - props.onClose?.() - } - - const handleKey = (e: KeyboardEvent) => { - setStore("mouseActive", false) - - if (e.key === "Enter") { - e.preventDefault() - const selected = flat().find((x) => props.key(x) === list.active()) - if (selected) handleSelect(selected) - } else if (e.key === "Escape") { - e.preventDefault() - props.onClose?.() - } else { - list.onKeyDown(e) - } - } - - return ( - open || props.onClose?.()}> - - - -
-
- - handleInput(e.currentTarget.value)} - onKeyDown={handleKey} - placeholder={props.placeholder} - class="w-full pl-10 pr-4 py-2 rounded-t-md - text-sm text-text placeholder-text-muted/70 - focus:outline-none" - autofocus - spellcheck={false} - autocorrect="off" - autocomplete="off" - autocapitalize="off" - /> -
- {/* -
- -
-
*/} - - { - setStore("filter", "") - resetSelection() - }} - > - - - -
-
-
-
(scrollRef = el)} class="relative flex-1 overflow-y-auto"> - 0} - fallback={
No results
} - > - - {(group) => ( - <> - -
- {group.category} -
-
-
- - {(item) => ( - - )} - -
- - )} -
-
-
-
-
- - - ↑↓ - - Navigate - - - - ↵ - - Select - - - - ESC - - Close - -
- {`${flat().length} results`} -
-
-
-
- ) -} diff --git a/packages/desktop/src/components/session-timeline.tsx b/packages/desktop/src/components/session-timeline.tsx index 2474b3101..0d8a7cd3c 100644 --- a/packages/desktop/src/components/session-timeline.tsx +++ b/packages/desktop/src/components/session-timeline.tsx @@ -1,11 +1,10 @@ import { useLocal, useSync } from "@/context" import { Icon, Tooltip } from "@opencode-ai/ui" import { Collapsible } from "@/ui" -import type { AssistantMessage, Part, ToolPart } from "@opencode-ai/sdk" +import type { AssistantMessage, Message, Part, ToolPart } from "@opencode-ai/sdk" import { DateTime } from "luxon" import { createSignal, - onMount, For, Match, splitProps, @@ -67,7 +66,7 @@ function ReadToolPart(props: { part: ToolPart }) { {(state) => { const path = state().input["filePath"] as string return ( - local.file.open(path)}> + local.file.open(path)}> Read {getFilename(path)} ) @@ -253,7 +252,7 @@ export default function SessionTimeline(props: { session: string; class?: string case "patch": return false case "text": - return !part.synthetic + return !part.synthetic && part.text.trim() case "reasoning": return part.text.trim() case "tool": @@ -270,8 +269,17 @@ export default function SessionTimeline(props: { session: string; class?: string } } + const hasValidParts = (message: Message) => { + return sync.data.part[message.id]?.filter(valid).length > 0 + } + + const hasTextPart = (message: Message) => { + return !!sync.data.part[message.id]?.filter(valid).find((p) => p.type === "text") + } + const session = createMemo(() => sync.session.get(props.session)) const messages = createMemo(() => sync.data.message[props.session] ?? []) + const messagesWithValidParts = createMemo(() => sync.data.message[props.session]?.filter(hasValidParts) ?? []) const working = createMemo(() => { const last = messages()[messages().length - 1] if (!last) return false @@ -386,7 +394,7 @@ export default function SessionTimeline(props: { session: string; class?: string [props.class ?? ""]: !!props.class, }} > -
+
@@ -397,11 +405,16 @@ export default function SessionTimeline(props: { session: string; class?: string
{cost()}
-
    - +
      + {(message) => ( -
      - +
      + {(part) => (
    • {part.type}
    • }> @@ -449,9 +462,9 @@ export default function SessionTimeline(props: { session: string; class?: string
      - + Raw Session Data - +
      @@ -460,9 +473,9 @@ export default function SessionTimeline(props: { session: string; class?: string
      - + session - +
      @@ -477,9 +490,9 @@ export default function SessionTimeline(props: { session: string; class?: string
      - + {message.role === "user" ? "user" : "assistant"} - +
      @@ -493,9 +506,9 @@ export default function SessionTimeline(props: { session: string; class?: string
      - + {part.type} - +
      diff --git a/packages/desktop/src/pages/index.tsx b/packages/desktop/src/pages/index.tsx index 80473d84a..58d479111 100644 --- a/packages/desktop/src/pages/index.tsx +++ b/packages/desktop/src/pages/index.tsx @@ -1,16 +1,14 @@ -import { Button, Icon, List, Tooltip } from "@opencode-ai/ui" -import { FileIcon, IconButton } from "@/ui" +import { Button, Icon, List, SelectDialog, Tooltip } from "@opencode-ai/ui" +import { FileIcon } from "@/ui" import FileTree from "@/components/file-tree" import EditorPane from "@/components/editor-pane" -import { For, Match, onCleanup, onMount, Show, Switch } from "solid-js" -import { SelectDialog } from "@/components/select-dialog" +import { For, onCleanup, onMount, Show } from "solid-js" import { useSync, useSDK, useLocal } from "@/context" import type { LocalFile, TextSelection } from "@/context/local" import SessionTimeline from "@/components/session-timeline" -import { type PromptContentPart, type PromptSubmitValue } from "@/components/prompt-form" import { createStore } from "solid-js/store" import { getDirectory, getFilename } from "@/utils" -import { PromptInput } from "@/components/prompt-input" +import { ContentPart, PromptInput } from "@/components/prompt-input" import { DateTime } from "luxon" export default function Page() { @@ -22,8 +20,7 @@ export default function Page() { modelSelectOpen: false, fileSelectOpen: false, }) - - let inputRef: HTMLTextAreaElement | undefined = undefined + let inputRef!: HTMLDivElement const MOD = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control" @@ -50,7 +47,7 @@ export default function Page() { const focused = document.activeElement === inputRef if (focused) { if (event.key === "Escape") { - // inputRef?.blur() + inputRef?.blur() } return } @@ -77,7 +74,7 @@ export default function Page() { } if (event.key.length === 1 && event.key !== "Unidentified") { - // inputRef?.focus() + inputRef?.focus() } } @@ -104,9 +101,7 @@ export default function Page() { } } - const handlePromptSubmit2 = () => {} - - const handlePromptSubmit = async (prompt: PromptSubmitValue) => { + const handlePromptSubmit = async (parts: ContentPart[]) => { const existingSession = local.session.active() let session = existingSession if (!session) { @@ -134,6 +129,7 @@ export default function Page() { const toAbsolutePath = (path: string) => (path.startsWith("/") ? path : sync.absolute(path)) + const text = parts.map((part) => part.content).join("") const attachments = new Map() const registerAttachment = (path: string, selection: TextSelection | undefined, label?: string) => { @@ -147,30 +143,27 @@ export default function Page() { }) } - const promptAttachments = prompt.parts.filter( - (part): part is Extract => part.kind === "attachment", - ) - + const promptAttachments = parts.filter((part) => part.type === "file") for (const part of promptAttachments) { - registerAttachment(part.path, part.selection, part.display) + registerAttachment(part.path, part.selection, part.content) } - const activeFile = local.context.active() - if (activeFile) { - registerAttachment( - activeFile.path, - activeFile.selection, - activeFile.name ?? formatAttachmentLabel(activeFile.path, activeFile.selection), - ) - } + // const activeFile = local.context.active() + // if (activeFile) { + // registerAttachment( + // activeFile.path, + // activeFile.selection, + // activeFile.name ?? formatAttachmentLabel(activeFile.path, activeFile.selection), + // ) + // } - for (const contextFile of local.context.all()) { - registerAttachment( - contextFile.path, - contextFile.selection, - formatAttachmentLabel(contextFile.path, contextFile.selection), - ) - } + // for (const contextFile of local.context.all()) { + // registerAttachment( + // contextFile.path, + // contextFile.selection, + // formatAttachmentLabel(contextFile.path, contextFile.selection), + // ) + // } const attachmentParts = Array.from(attachments.values()).map((attachment) => { const absolute = toAbsolutePath(attachment.path) @@ -205,7 +198,7 @@ export default function Page() { parts: [ { type: "text", - text: prompt.text, + text, }, ...attachmentParts, ], @@ -213,16 +206,10 @@ export default function Page() { }) } - const plus = ( - setStore("fileSelectOpen", true)} - > - - - ) + const handleNewSession = () => { + local.session.setActive(undefined) + inputRef?.focus() + } return (
      @@ -234,7 +221,8 @@ export default function Page() {
      -
      @@ -268,25 +256,30 @@ export default function Page() {
      -
      +
      {(activeSession) => }
      - + + +
      -
      - - {/* setStore("modelSelectOpen", true)} */} - {/* onInputRefChange={(element: HTMLTextAreaElement | undefined) => { */} - {/* inputRef = element ?? undefined */} - {/* }} */} - {/* /> */} +
      + { + inputRef = el + }} + onSubmit={handlePromptSubmit} + />
      - - `${x.provider.id}:${x.id}`} - items={local.model.list()} - current={local.model.current()} - render={(i) => ( -
      -
      - - {i.name} - - {i.id} - -
      -
      - - - - - - - - - -
      - {new Intl.NumberFormat("en-US", { - notation: "compact", - compactDisplay: "short", - }).format(i.limit.context)} -
      - -
      - - 10}>$$$ - 1}>$$ - 0.1}>$ - -
      -
      -
      -
      - )} - filter={["provider.name", "name", "id"]} - groupBy={(x) => x.provider.name} - onClose={() => setStore("modelSelectOpen", false)} - onSelect={(x) => local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined)} - /> -
      x} - render={(i) => ( + onOpenChange={(open) => setStore("fileSelectOpen", open)} + onSelect={(x) => (x ? local.file.open(x, { pinned: true }) : undefined)} + > + {(i) => (
      @@ -382,9 +332,7 @@ export default function Page() {
      )} - onClose={() => setStore("fileSelectOpen", false)} - onSelect={(x) => (x ? local.file.open(x, { pinned: true }) : undefined)} - /> +
      ) diff --git a/packages/desktop/src/ui/collapsible.tsx b/packages/desktop/src/ui/collapsible.tsx index d17b3e623..fbc6fcbfe 100644 --- a/packages/desktop/src/ui/collapsible.tsx +++ b/packages/desktop/src/ui/collapsible.tsx @@ -16,7 +16,7 @@ function CollapsibleTrigger(props: CollapsibleTriggerProps) { return ( { - variant?: "primary" | "secondary" | "outline" | "ghost" - size?: "xs" | "sm" | "md" | "lg" - children: JSX.Element -} - -export function IconButton(props: IconButtonProps) { - const [local, others] = splitProps(props, ["variant", "size", "class", "classList"]) - return ( - - ) -} diff --git a/packages/desktop/src/ui/index.ts b/packages/desktop/src/ui/index.ts index a6ade6ff3..e273e8efe 100644 --- a/packages/desktop/src/ui/index.ts +++ b/packages/desktop/src/ui/index.ts @@ -5,4 +5,3 @@ export { type CollapsibleContentProps, } from "./collapsible" export { FileIcon, type FileIconProps } from "./file-icon" -export { IconButton, type IconButtonProps } from "./icon-button" diff --git a/packages/opencode/src/cli/cmd/debug/file.ts b/packages/opencode/src/cli/cmd/debug/file.ts index 7be304737..fabef32b8 100644 --- a/packages/opencode/src/cli/cmd/debug/file.ts +++ b/packages/opencode/src/cli/cmd/debug/file.ts @@ -1,3 +1,4 @@ +import { EOL } from "os" import { File } from "../../../file" import { bootstrap } from "../../bootstrap" import { cmd } from "../cmd" @@ -13,7 +14,7 @@ const FileSearchCommand = cmd({ async handler(args) { await bootstrap(process.cwd(), async () => { const results = await File.search({ query: args.query }) - console.log(results.join("\n")) + console.log(results.join(EOL)) }) }, }) diff --git a/packages/opencode/src/cli/cmd/debug/ripgrep.ts b/packages/opencode/src/cli/cmd/debug/ripgrep.ts index 884b291b5..1c9f89973 100644 --- a/packages/opencode/src/cli/cmd/debug/ripgrep.ts +++ b/packages/opencode/src/cli/cmd/debug/ripgrep.ts @@ -1,3 +1,4 @@ +import { EOL } from "os" import { Ripgrep } from "../../../file/ripgrep" import { Instance } from "../../../project/instance" import { bootstrap } from "../../bootstrap" @@ -48,7 +49,7 @@ const FilesCommand = cmd({ files.push(file) if (args.limit && files.length >= args.limit) break } - console.log(files.join("\n")) + console.log(files.join(EOL)) }) }, }) diff --git a/packages/opencode/src/cli/cmd/export.ts b/packages/opencode/src/cli/cmd/export.ts index c8825c83d..82952f1f6 100644 --- a/packages/opencode/src/cli/cmd/export.ts +++ b/packages/opencode/src/cli/cmd/export.ts @@ -4,6 +4,7 @@ import { cmd } from "./cmd" import { bootstrap } from "../bootstrap" import { UI } from "../ui" import * as prompts from "@clack/prompts" +import { EOL } from "os" export const ExportCommand = cmd({ command: "export [sessionID]", @@ -67,7 +68,7 @@ export const ExportCommand = cmd({ } process.stdout.write(JSON.stringify(exportData, null, 2)) - process.stdout.write("\n") + process.stdout.write(EOL) } catch (error) { UI.error(`Session not found: ${sessionID!}`) process.exit(1) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 9a6445a22..60788c7d8 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -13,6 +13,7 @@ import { Identifier } from "../../id/id" import { Agent } from "../../agent/agent" import { Command } from "../../command" import { SessionPrompt } from "../../session/prompt" +import { EOL } from "os" const TOOL: Record = { todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD], @@ -194,13 +195,12 @@ export const RunCommand = cmd({ sessionID: session?.id, ...data, } - process.stdout.write(JSON.stringify(jsonEvent) + "\n") + process.stdout.write(JSON.stringify(jsonEvent) + EOL) return true } return false } - let text = "" const messageID = Identifier.ascending("message") Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => { @@ -232,15 +232,14 @@ export const RunCommand = cmd({ } if (part.type === "text") { - text = part.text + const text = part.text + const isPiped = !process.stdout.isTTY if (part.time?.end) { if (outputJsonEvent("text", { part })) return - UI.empty() - UI.println(UI.markdown(text)) - UI.empty() - text = "" - return + if (!isPiped) UI.println() + process.stdout.write((isPiped ? text : UI.markdown(text)) + EOL) + if (!isPiped) UI.println() } } }) @@ -254,13 +253,13 @@ export const RunCommand = cmd({ if ("data" in error && error.data && "message" in error.data) { err = error.data.message } - errorMsg = errorMsg ? errorMsg + "\n" + err : err + errorMsg = errorMsg ? errorMsg + EOL + err : err if (outputJsonEvent("error", { error })) return UI.error(err) }) - const result = await (async () => { + await (async () => { if (args.command) { return await SessionPrompt.command({ messageID, @@ -289,15 +288,6 @@ export const RunCommand = cmd({ ], }) })() - - const isPiped = !process.stdout.isTTY - if (isPiped) { - const match = result.parts.findLast((x: any) => x.type === "text") as any - if (outputJsonEvent("text", { text: match })) return - if (match) process.stdout.write(UI.markdown(match.text)) - if (errorMsg) process.stdout.write(errorMsg) - } - UI.empty() if (errorMsg) process.exit(1) }) }, diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 0437c4c69..fff271cd2 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -12,6 +12,7 @@ export namespace Flag { // Experimental export const OPENCODE_EXPERIMENTAL_WATCHER = truthy("OPENCODE_EXPERIMENTAL_WATCHER") + export const OPENCODE_EXPERIMENTAL_TURN_SUMMARY = truthy("OPENCODE_EXPERIMENTAL_TURN_SUMMARY") export const OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP = truthy("OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP") function truthy(key: string) { diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index df8fdd460..de29ca85f 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -21,6 +21,7 @@ import { AttachCommand } from "./cli/cmd/tui/attach" import { TuiThreadCommand } from "./cli/cmd/tui/thread" import { TuiSpawnCommand } from "./cli/cmd/tui/spawn" import { AcpCommand } from "./cli/cmd/acp" +import { EOL } from "os" const cancel = new AbortController() @@ -132,7 +133,7 @@ try { const formatted = FormatError(e) if (formatted) UI.error(formatted) if (formatted === undefined) { - UI.error("Unexpected error, check log file at " + Log.file() + " for more details\n") + UI.error("Unexpected error, check log file at " + Log.file() + " for more details" + EOL) console.error(e) } process.exitCode = 1 diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index c895c9316..7c40955dc 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -95,7 +95,14 @@ export namespace Provider { switch (regionPrefix) { case "us": { - const modelRequiresPrefix = ["claude", "deepseek"].some((m) => modelID.includes(m)) + const modelRequiresPrefix = [ + "nova-micro", + "nova-lite", + "nova-pro", + "nova-premier", + "claude", + "deepseek" + ].some((m) => modelID.includes(m)) const isGovCloud = region.startsWith("us-gov") if (modelRequiresPrefix && !isGovCloud) { modelID = `${regionPrefix}.${modelID}` @@ -121,9 +128,10 @@ export namespace Provider { } case "ap": { const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region) - if (isAustraliaRegion && ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => - modelID.includes(m), - )) { + if ( + isAustraliaRegion && + ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => modelID.includes(m)) + ) { regionPrefix = "au" modelID = `${regionPrefix}.${modelID}` } else { @@ -273,31 +281,31 @@ export namespace Provider { cost: !model.cost && !existing?.cost ? { - input: 0, - output: 0, - cache_read: 0, - cache_write: 0, - } + input: 0, + output: 0, + cache_read: 0, + cache_write: 0, + } : { - cache_read: 0, - cache_write: 0, - ...existing?.cost, - ...model.cost, - }, + cache_read: 0, + cache_write: 0, + ...existing?.cost, + ...model.cost, + }, options: { ...existing?.options, ...model.options, }, limit: model.limit ?? existing?.limit ?? { - context: 0, - output: 0, - }, + context: 0, + output: 0, + }, modalities: model.modalities ?? existing?.modalities ?? { - input: ["text"], - output: ["text"], - }, + input: ["text"], + output: ["text"], + }, provider: model.provider ?? existing?.provider, } if (model.id && model.id !== modelID) { @@ -509,7 +517,14 @@ export namespace Provider { const provider = await state().then((state) => state.providers[providerID]) if (!provider) return - const priority = ["3-5-haiku", "3.5-haiku", "gemini-2.5-flash", "gpt-5-nano"] + const priority = [ + "claude-haiku-4-5", + "claude-haiku-4.5", + "3-5-haiku", + "3.5-haiku", + "gemini-2.5-flash", + "gpt-5-nano", + ] for (const item of priority) { for (const model of Object.keys(provider.info.models)) { if (model.includes(item)) return getModel(providerID, model) diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 73735faf8..03722aa6a 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -98,7 +98,7 @@ export namespace SessionCompaction { draft.time.compacting = undefined }) }) - const toSummarize = await Session.messages(input.sessionID).then(MessageV2.filterSummarized) + const toSummarize = await Session.messages(input.sessionID).then(MessageV2.filterCompacted) const model = await Provider.getModel(input.providerID, input.modelID) const system = [ ...SystemPrompt.summarize(model.providerID), @@ -109,6 +109,7 @@ export namespace SessionCompaction { const msg = (await Session.updateMessage({ id: Identifier.ascending("message"), role: "assistant", + parentID: toSummarize.findLast((m) => m.info.role === "user")?.info.id!, sessionID: input.sessionID, system, mode: "build", diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 313cb41ab..cf0faa802 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -5,6 +5,7 @@ import { Message } from "./message" import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai" import { Identifier } from "../id/id" import { LSP } from "../lsp" +import { Snapshot } from "@/snapshot" export namespace MessageV2 { export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({})) @@ -243,6 +244,12 @@ export namespace MessageV2 { time: z.object({ created: z.number(), }), + summary: z + .object({ + diffs: Snapshot.FileDiff.array(), + text: z.string(), + }) + .optional(), }).meta({ ref: "UserMessage", }) @@ -281,6 +288,7 @@ export namespace MessageV2 { .optional(), system: z.string().array(), finish: z.string().optional(), + parentID: z.string(), modelID: z.string(), providerID: z.string(), mode: z.string(), @@ -349,6 +357,7 @@ export namespace MessageV2 { if (v1.role === "assistant") { const info: Assistant = { id: v1.id, + parentID: "", sessionID: v1.metadata.sessionID, role: "assistant", time: { @@ -600,7 +609,7 @@ export namespace MessageV2 { return convertToModelMessages(result) } - export function filterSummarized(msgs: { info: MessageV2.Info; parts: MessageV2.Part[] }[]) { + export function filterCompacted(msgs: { info: MessageV2.Info; parts: MessageV2.Part[] }[]) { const i = msgs.findLastIndex((m) => m.info.role === "assistant" && !!m.info.summary) if (i === -1) return msgs.slice() return msgs.slice(i) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 6e57d1db3..c01ada1ad 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -50,6 +50,7 @@ import { spawn } from "child_process" import { Command } from "../command" import { $, fileURLToPath } from "bun" import { ConfigMarkdown } from "../config/markdown" +import { MessageSummary } from "./summary" export namespace SessionPrompt { const log = Log.create({ service: "session.prompt" }) @@ -235,7 +236,7 @@ export namespace SessionPrompt { modelID: model.info.id, }) step++ - await processor.next() + await processor.next(msgs.findLast((m) => m.info.role === "user")?.info.id!) await using _ = defer(async () => { await processor.end() }) @@ -345,6 +346,11 @@ export namespace SessionPrompt { } state().queued.delete(input.sessionID) SessionCompaction.prune(input) + MessageSummary.summarize({ + sessionID: input.sessionID, + messageID: result.info.parentID, + providerID: model.providerID, + }) return result } } @@ -355,7 +361,7 @@ export namespace SessionPrompt { providerID: string signal: AbortSignal }) { - let msgs = await Session.messages(input.sessionID).then(MessageV2.filterSummarized) + let msgs = await Session.messages(input.sessionID).then(MessageV2.filterCompacted) const lastAssistant = msgs.findLast((msg) => msg.info.role === "assistant") if ( lastAssistant?.info.role === "assistant" && @@ -900,9 +906,10 @@ export namespace SessionPrompt { let snapshot: string | undefined let blocked = false - async function createMessage() { + async function createMessage(parentID: string) { const msg: MessageV2.Info = { id: Identifier.ascending("message"), + parentID, role: "assistant", system: input.system, mode: input.agent, @@ -938,11 +945,11 @@ export namespace SessionPrompt { assistantMsg = undefined } }, - async next() { + async next(parentID: string) { if (assistantMsg) { throw new Error("end previous assistant message first") } - assistantMsg = await createMessage() + assistantMsg = await createMessage(parentID) return assistantMsg }, get message() { @@ -1429,6 +1436,7 @@ export namespace SessionPrompt { const msg: MessageV2.Assistant = { id: Identifier.ascending("message"), sessionID: input.sessionID, + parentID: userMsg.id, system: [], mode: input.agent, cost: 0, @@ -1701,6 +1709,7 @@ export namespace SessionPrompt { const assistantMsg: MessageV2.Assistant = { id: Identifier.ascending("message"), sessionID: input.sessionID, + parentID: userMsg.id, system: [], mode: agentName, cost: 0, diff --git a/packages/opencode/src/session/prompt/summarize-turn.txt b/packages/opencode/src/session/prompt/summarize-turn.txt new file mode 100644 index 000000000..718d0f637 --- /dev/null +++ b/packages/opencode/src/session/prompt/summarize-turn.txt @@ -0,0 +1,5 @@ +Your job is to generate a summary of what happened in this conversation and why. + +Keep the results to 2-3 sentences. + +Output the message summary now: diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts new file mode 100644 index 000000000..c5fd8c123 --- /dev/null +++ b/packages/opencode/src/session/summary.ts @@ -0,0 +1,46 @@ +import { Provider } from "@/provider/provider" +import { fn } from "@/util/fn" +import z from "zod" +import { Session } from "." +import { generateText } from "ai" +import { MessageV2 } from "./message-v2" +import SUMMARIZE_TURN from "./prompt/summarize-turn.txt" +import { Flag } from "@/flag/flag" + +export namespace MessageSummary { + export const summarize = fn( + z.object({ + sessionID: z.string(), + messageID: z.string(), + providerID: z.string(), + }), + async (input) => { + if (!Flag.OPENCODE_EXPERIMENTAL_TURN_SUMMARY) return + const messages = await Session.messages(input.sessionID).then((msgs) => + msgs.filter( + (m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID), + ), + ) + const small = await Provider.getSmallModel(input.providerID) + if (!small) return + + const result = await generateText({ + model: small.language, + messages: [ + { + role: "system", + content: SUMMARIZE_TURN, + }, + ...MessageV2.toModelMessage(messages), + ], + }) + + const userMsg = messages.find((m) => m.info.id === input.messageID)! + userMsg.info.summary = { + text: result.text, + diffs: [], + } + await Session.updateMessage(userMsg.info) + }, + ) +} diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index da77b42f2..69fa7bdb8 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -1158,9 +1158,9 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) { // status.Warn("Agent is working, please wait...") return a, nil } - editor := os.Getenv("EDITOR") + editor := util.GetEditor() if editor == "" { - return a, toast.NewErrorToast("No EDITOR set, can't open editor") + return a, toast.NewErrorToast("No editor found. Set EDITOR environment variable (e.g., export EDITOR=vim)") } value := a.editor.Value() @@ -1404,10 +1404,9 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) { // Format to Markdown markdownContent := formatConversationToMarkdown(messages) - // Check if EDITOR is set - editor := os.Getenv("EDITOR") + editor := util.GetEditor() if editor == "" { - return a, toast.NewErrorToast("No EDITOR set, can't open editor") + return a, toast.NewErrorToast("No editor found. Set EDITOR environment variable (e.g., export EDITOR=vim)") } // Create and write to temp file diff --git a/packages/tui/internal/util/util.go b/packages/tui/internal/util/util.go index fdefb2901..b49d2e292 100644 --- a/packages/tui/internal/util/util.go +++ b/packages/tui/internal/util/util.go @@ -3,6 +3,8 @@ package util import ( "log/slog" "os" + "os/exec" + "runtime" "strings" "time" @@ -45,3 +47,25 @@ func Measure(tag string) func(...any) { slog.Debug(tag, args...) } } + +func GetEditor() string { + if editor := os.Getenv("VISUAL"); editor != "" { + return editor + } + if editor := os.Getenv("EDITOR"); editor != "" { + return editor + } + + commonEditors := []string{"vim", "nvim", "zed", "code", "cursor", "vi", "nano"} + if runtime.GOOS == "windows" { + commonEditors = []string{"vim", "nvim", "zed", "code.cmd", "cursor.cmd", "notepad.exe", "vi", "nano"} + } + + for _, editor := range commonEditors { + if _, err := exec.LookPath(editor); err == nil { + return editor + } + } + + return "" +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 4419b1f20..c32bfb7ee 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -5,6 +5,7 @@ "exports": { ".": "./src/components/index.ts", "./*": "./src/components/*.tsx", + "./hooks": "./src/hooks/index.ts", "./styles": "./src/styles/index.css", "./styles/tailwind": "./src/styles/tailwind/index.css", "./fonts/*": "./src/assets/fonts/*" @@ -23,11 +24,13 @@ }, "dependencies": { "@kobalte/core": "catalog:", + "@pierre/precision-diffs": "0.0.2-alpha.1-1", "@solidjs/meta": "catalog:", - "remeda": "catalog:", + "fuzzysort": "catalog:", "luxon": "catalog:", - "virtua": "catalog:", + "remeda": "catalog:", "solid-js": "catalog:", - "solid-list": "catalog:" + "solid-list": "catalog:", + "virtua": "catalog:" } } diff --git a/packages/ui/src/components/button.css b/packages/ui/src/components/button.css index c9ccf4ecb..7bf096853 100644 --- a/packages/ui/src/components/button.css +++ b/packages/ui/src/components/button.css @@ -1,5 +1,4 @@ [data-component="button"] { - cursor: pointer; display: inline-flex; align-items: center; justify-content: center; @@ -32,12 +31,7 @@ border-color: var(--border-weak-base); background-color: var(--button-secondary-base); color: var(--text-strong); - - /* shadow-xs */ - box-shadow: - 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); + box-shadow: var(--shadow-xs); &:hover:not(:disabled) { border-color: var(--border-hover); @@ -84,12 +78,11 @@ padding: 0 8px 0 6px; gap: 8px; - /* text-12-medium */ font-family: var(--font-family-sans); - font-size: var(--font-size-small); + font-size: var(--font-size-base); font-style: normal; font-weight: var(--font-weight-medium); - line-height: var(--line-height-large); /* 166.667% */ + line-height: var(--line-height-large); /* 171.429% */ letter-spacing: var(--letter-spacing-normal); } diff --git a/packages/ui/src/components/button.tsx b/packages/ui/src/components/button.tsx index db1da2fb9..cae658439 100644 --- a/packages/ui/src/components/button.tsx +++ b/packages/ui/src/components/button.tsx @@ -1,12 +1,14 @@ import { Button as Kobalte } from "@kobalte/core/button" import { type ComponentProps, splitProps } from "solid-js" -export interface ButtonProps { +export interface ButtonProps + extends ComponentProps, + Pick, "class" | "classList" | "children"> { size?: "normal" | "large" variant?: "primary" | "secondary" | "ghost" } -export function Button(props: ComponentProps<"button"> & ButtonProps) { +export function Button(props: ButtonProps) { const [split, rest] = splitProps(props, ["variant", "size", "class", "classList"]) return ( ["class"] + classList?: ComponentProps<"div">["classList"] +} + +export function DialogRoot(props: DialogProps) { + let trigger!: HTMLElement + const [local, others] = splitProps(props, ["trigger", "class", "classList", "children"]) + + const resetTabIndex = () => { + trigger.tabIndex = 0 + } + + const handleTriggerFocus = (e: FocusEvent & { currentTarget: HTMLElement | null }) => { + const firstChild = e.currentTarget?.firstElementChild as HTMLElement + if (!firstChild) return + + firstChild.focus() + trigger.tabIndex = -1 + + firstChild.addEventListener("focusout", resetTabIndex) + onCleanup(() => { + firstChild.removeEventListener("focusout", resetTabIndex) + }) + } + + return ( + + + + {props.trigger} + + + + +
      +
      + + {local.children} + +
      +
      +
      +
      + ) +} + +function DialogHeader(props: ComponentProps<"div">) { + return
      +} + +function DialogBody(props: ComponentProps<"div">) { + return
      +} + +function DialogTitle(props: DialogTitleProps & ComponentProps<"h2">) { + return +} + +function DialogDescription(props: DialogDescriptionProps & ComponentProps<"p">) { + return +} + +function DialogCloseButton(props: DialogCloseButtonProps & ComponentProps<"button">) { + return +} + +export const Dialog = Object.assign(DialogRoot, { + Header: DialogHeader, + Title: DialogTitle, + Description: DialogDescription, + CloseButton: DialogCloseButton, + Body: DialogBody, +}) diff --git a/packages/ui/src/components/icon-button.css b/packages/ui/src/components/icon-button.css new file mode 100644 index 000000000..6fe95fccf --- /dev/null +++ b/packages/ui/src/components/icon-button.css @@ -0,0 +1,117 @@ +[data-component="icon-button"] { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 100%; + text-decoration: none; + user-select: none; + aspect-ratio: 1; + + &:disabled { + background-color: var(--icon-strong-disabled); + color: var(--icon-invert-base); + cursor: not-allowed; + } + + &:focus { + outline: none; + } + + &[data-variant="primary"] { + background-color: var(--icon-strong-base); + + [data-slot="icon"] { + /* color: var(--icon-weak-base); */ + color: var(--icon-invert-base); + + /* &:hover:not(:disabled) { */ + /* color: var(--icon-weak-hover); */ + /* } */ + /* &:active:not(:disabled) { */ + /* color: var(--icon-string-active); */ + /* } */ + } + + &:hover:not(:disabled) { + background-color: var(--icon-strong-hover); + } + &:active:not(:disabled) { + background-color: var(--icon-string-active); + } + &:focus:not(:disabled) { + background-color: var(--icon-strong-focus); + } + &:disabled { + background-color: var(--icon-strong-disabled); + + [data-slot="icon"] { + color: var(--icon-invert-base); + } + } + } + + &[data-variant="secondary"] { + background-color: var(--button-secondary-base); + color: var(--text-strong); + + &:hover:not(:disabled) { + background-color: var(--surface-hover); + } + &:active:not(:disabled) { + background-color: var(--surface-active); + } + &:focus:not(:disabled) { + background-color: var(--surface-focus); + } + } + + &[data-variant="ghost"] { + background-color: transparent; + + [data-slot="icon"] { + color: var(--icon-weak-base); + + &:hover:not(:disabled) { + color: var(--icon-weak-hover); + } + &:active:not(:disabled) { + color: var(--icon-string-active); + } + } + + /* color: var(--text-strong); */ + /**/ + /* &:hover:not(:disabled) { */ + /* background-color: var(--surface-hover); */ + /* } */ + /* &:active:not(:disabled) { */ + /* background-color: var(--surface-active); */ + /* } */ + /* &:focus:not(:disabled) { */ + /* background-color: var(--surface-focus); */ + /* } */ + } + + &[data-size="normal"] { + width: 24px; + height: 24px; + + font-size: var(--font-size-small); + line-height: var(--line-height-large); + gap: calc(var(--spacing) * 0.5); + } + + &[data-size="large"] { + height: 32px; + padding: 0 8px 0 6px; + gap: 8px; + + /* 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); + } +} diff --git a/packages/ui/src/components/icon-button.tsx b/packages/ui/src/components/icon-button.tsx new file mode 100644 index 000000000..f483f92a7 --- /dev/null +++ b/packages/ui/src/components/icon-button.tsx @@ -0,0 +1,27 @@ +import { Button as Kobalte } from "@kobalte/core/button" +import { type ComponentProps, splitProps } from "solid-js" +import { Icon, IconProps } from "./icon" + +export interface IconButtonProps { + icon: IconProps["name"] + size?: "normal" | "large" + variant?: "primary" | "secondary" | "ghost" +} + +export function IconButton(props: ComponentProps<"button"> & IconButtonProps) { + const [split, rest] = splitProps(props, ["variant", "size", "class", "classList"]) + return ( + + + + ) +} diff --git a/packages/ui/src/components/icon.css b/packages/ui/src/components/icon.css index abc193220..59c644b70 100644 --- a/packages/ui/src/components/icon.css +++ b/packages/ui/src/components/icon.css @@ -3,4 +3,27 @@ align-items: center; justify-content: center; flex-shrink: 0; + /* resize: both; */ + aspect-ratio: 1/1; + color: var(--icon-base); + + &[data-size="small"] { + width: 16px; + height: 16px; + } + + &[data-size="normal"] { + width: 20px; + height: 20px; + } + + &[data-size="large"] { + width: 32px; + height: 32px; + } + + [data-slot="svg"] { + width: 100%; + height: auto; + } } diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index 05dda6ea6..8d63bf0f8 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -128,28 +128,55 @@ const icons = { mic: '', } as const +const newIcons = { + "circle-x": ``, + "magnifying-glass": ``, + "plus-small": ``, + "chevron-down": ``, + "arrow-up": ``, +} + export interface IconProps extends ComponentProps<"svg"> { - name: keyof typeof icons - size?: number + name: keyof typeof icons | keyof typeof newIcons + size?: "small" | "normal" | "large" } export function Icon(props: IconProps) { const [local, others] = splitProps(props, ["name", "size", "class", "classList"]) - const size = local.size ?? 24 + + if (local.name in newIcons) { + return ( +
      + +
      + ) + } + return ( - +
      + +
      ) } diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index d6ddc3ec0..71cfd3a89 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -1,7 +1,11 @@ export * from "./button" +export * from "./dialog" export * from "./icon" +export * from "./icon-button" +export * from "./input" export * from "./fonts" export * from "./list" export * from "./select" +export * from "./select-dialog" export * from "./tabs" export * from "./tooltip" diff --git a/packages/ui/src/components/input.css b/packages/ui/src/components/input.css new file mode 100644 index 000000000..24cec19c5 --- /dev/null +++ b/packages/ui/src/components/input.css @@ -0,0 +1,23 @@ +[data-component="input"] { + /* [data-slot="label"] {} */ + + [data-slot="input"] { + 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); + } + } +} diff --git a/packages/ui/src/components/input.tsx b/packages/ui/src/components/input.tsx new file mode 100644 index 000000000..509e242c9 --- /dev/null +++ b/packages/ui/src/components/input.tsx @@ -0,0 +1,27 @@ +import { TextField as Kobalte } from "@kobalte/core/text-field" +import { Show, splitProps } from "solid-js" +import type { ComponentProps } from "solid-js" + +export interface InputProps extends ComponentProps { + label?: string + hideLabel?: boolean + description?: string +} + +export function Input(props: InputProps) { + const [local, others] = splitProps(props, ["class", "label", "hideLabel", "description", "placeholder"]) + return ( + + + + {local.label} + + + + + {local.description} + + + + ) +} diff --git a/packages/ui/src/components/list.css b/packages/ui/src/components/list.css index b98cae07c..d60b55aeb 100644 --- a/packages/ui/src/components/list.css +++ b/packages/ui/src/components/list.css @@ -12,7 +12,6 @@ scrollbar-width: none; [data-slot="item"] { - cursor: pointer; width: 100%; padding: 4px 12px; text-align: left; @@ -23,6 +22,9 @@ &[data-active="true"] { background-color: var(--surface-raised-base-hover); } + &:hover { + background-color: var(--surface-raised-base-hover); + } &:focus { outline: none; } diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index 8bfbbdc98..cb212d1a8 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -29,6 +29,7 @@ export function List(props: ListProps) { // } const handleSelect = (item: T) => { props.onSelect?.(item) + list.setActive(props.key(item)) } const handleKey = (e: KeyboardEvent) => { @@ -64,10 +65,10 @@ export function List(props: ListProps) { data-key={props.key(item)} data-active={props.key(item) === list.active()} onClick={() => handleSelect(item)} - onMouseMove={(e) => { - e.currentTarget.focus() + onMouseMove={() => { + // e.currentTarget.focus() setStore("mouseActive", true) - list.setActive(props.key(item)) + // list.setActive(props.key(item)) }} > {props.children(item)} diff --git a/packages/ui/src/components/select-dialog.css b/packages/ui/src/components/select-dialog.css new file mode 100644 index 000000000..41d8f3921 --- /dev/null +++ b/packages/ui/src/components/select-dialog.css @@ -0,0 +1,109 @@ +[data-component="select-dialog-input"] { + display: flex; + height: 40px; + flex-shrink: 0; + padding: 4px 10px 4px 6px; + align-items: center; + gap: 12px; + align-self: stretch; + + border-radius: 8px; + background: var(--surface-base); + + [data-slot="input-container"] { + display: flex; + align-items: center; + gap: 12px; + flex: 1 0 0; + + /* [data-slot="icon"] {} */ + + [data-slot="input"] { + width: 100%; + } + } + + /* [data-slot="clear-button"] {} */ +} + +[data-component="select-dialog"] { + display: flex; + flex-direction: column; + gap: 8px; + + [data-slot="empty-state"] { + display: flex; + padding: 32px 160px; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 8px; + align-self: stretch; + + [data-slot="message"] { + display: flex; + justify-content: center; + align-items: center; + gap: 2px; + color: var(--text-weak); + text-align: center; + + /* 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); + } + + [data-slot="filter"] { + color: var(--text-strong); + } + } + + [data-slot="group"] { + display: flex; + flex-direction: column; + gap: 4px; + + [data-slot="header"] { + display: flex; + padding: 4px 8px; + justify-content: space-between; + align-items: center; + align-self: stretch; + + 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: var(--line-height-large); /* 166.667% */ + letter-spacing: var(--letter-spacing-normal); + } + + [data-slot="list"] { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + align-self: stretch; + + [data-slot="item"] { + display: flex; + width: 100%; + height: 32px; + padding: 4px 8px 4px 4px; + align-items: center; + + &[data-active="true"] { + border-radius: 8px; + background: var(--surface-raised-base-hover); + } + } + } + } +} diff --git a/packages/ui/src/components/select-dialog.tsx b/packages/ui/src/components/select-dialog.tsx new file mode 100644 index 000000000..63fad13ec --- /dev/null +++ b/packages/ui/src/components/select-dialog.tsx @@ -0,0 +1,156 @@ +import { createEffect, Show, For, type JSX, splitProps } from "solid-js" +import { Dialog, DialogProps, Icon, IconButton, Input } from "@opencode-ai/ui" +import { createStore } from "solid-js/store" +import { FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks" + +interface SelectDialogProps + extends FilteredListProps, + Pick { + title: string + placeholder?: string + emptyMessage?: string + children: (item: T) => JSX.Element + onSelect?: (value: T | undefined) => void +} + +export function SelectDialog(props: SelectDialogProps) { + const [dialog, others] = splitProps(props, ["trigger", "onOpenChange", "defaultOpen"]) + let closeButton!: HTMLButtonElement + let scrollRef: HTMLDivElement | undefined + const [store, setStore] = createStore({ + mouseActive: false, + }) + + const { filter, grouped, flat, reset, clear, active, setActive, onKeyDown, onInput } = useFilteredList({ + items: others.items, + key: others.key, + filterKeys: others.filterKeys, + current: others.current, + groupBy: others.groupBy, + sortBy: others.sortBy, + sortGroupsBy: others.sortGroupsBy, + }) + + createEffect(() => { + filter() + scrollRef?.scrollTo(0, 0) + reset() + }) + + createEffect(() => { + const all = flat() + if (store.mouseActive || all.length === 0) return + if (active() === others.key(all[0])) { + scrollRef?.scrollTo(0, 0) + return + } + const element = scrollRef?.querySelector(`[data-key="${active()}"]`) + element?.scrollIntoView({ block: "nearest", behavior: "smooth" }) + }) + + const handleInput = (value: string) => { + onInput(value) + reset() + } + + const handleSelect = (item: T | undefined) => { + others.onSelect?.(item) + closeButton.click() + } + + const handleKey = (e: KeyboardEvent) => { + setStore("mouseActive", false) + if (e.key === "Escape") return + + if (e.key === "Enter") { + e.preventDefault() + const selected = flat().find((x) => others.key(x) === active()) + if (selected) handleSelect(selected) + } else { + onKeyDown(e) + } + } + + const handleOpenChange = (open: boolean) => { + if (!open) clear() + props.onOpenChange?.(open) + } + + return ( + + + {others.title} + + +
      +
      + + handleInput(value)} + onKeyDown={handleKey} + placeholder={others.placeholder} + autofocus + spellcheck={false} + autocorrect="off" + autocomplete="off" + autocapitalize="off" + /> +
      + + { + onInput("") + reset() + }} + /> + +
      + + 0} + fallback={ +
      +
      + {props.emptyMessage ?? "No search results"} for "{filter()}" +
      +
      + } + > + + {(group) => ( +
      + +
      {group.category}
      +
      +
      + + {(item) => ( + + )} + +
      +
      + )} +
      +
      +
      +
      + ) +} diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css index b6b884a1f..0eb7cea15 100644 --- a/packages/ui/src/components/select.css +++ b/packages/ui/src/components/select.css @@ -1,6 +1,7 @@ [data-component="select"] { [data-slot="trigger"] { padding: 0 4px 0 8px; + box-shadow: none; [data-slot="value"] { overflow: hidden; @@ -8,8 +9,8 @@ white-space: nowrap; } [data-slot="icon"] { - width: fit-content; - height: fit-content; + width: 16px; + height: 16px; flex-shrink: 0; color: var(--text-weak); transition: transform 0.1s ease-in-out; @@ -18,15 +19,15 @@ } [data-component="select-content"] { - min-width: 8rem; + min-width: 4rem; overflow: hidden; - border-radius: var(--radius-md); + border-radius: 8px; border-width: 1px; border-style: solid; border-color: var(--border-weak-base); - background-color: var(--surface-raised-base); - padding: calc(var(--spacing) * 1); - box-shadow: var(--shadow-md); + background-color: var(--surface-raised-stronger-non-alpha); + padding: 2px; + box-shadow: var(--shadow-xs); z-index: 50; &[data-closed] { @@ -42,36 +43,35 @@ max-height: 12rem; white-space: nowrap; overflow-x: hidden; + display: flex; + flex-direction: column; + gap: 2px; &:focus { outline: none; } } - [data-slot="section"] { - font-size: var(--text-xs); - line-height: var(--text-xs--line-height); - font-weight: var(--font-weight-light); - text-transform: uppercase; - color: var(--text-weak); - opacity: 0.6; - margin-top: calc(var(--spacing) * 3); - margin-left: calc(var(--spacing) * 2); - &:first-child { - margin-top: 0; - } - } + /* [data-slot="section"] { */ + /* } */ [data-slot="item"] { position: relative; display: flex; align-items: center; - padding: calc(var(--spacing) * 2) calc(var(--spacing) * 2); - border-radius: var(--radius-sm); - font-size: var(--text-xs); - line-height: var(--text-xs--line-height); - color: var(--text-base); - cursor: pointer; + padding: 0 6px 0 6px; + border-radius: 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); + + color: var(--text-strong); + transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; @@ -79,24 +79,20 @@ user-select: none; &[data-highlighted] { - background-color: var(--surface-base); + background: var(--surface-raised-base-hover); } - &[data-disabled] { - background-color: var(--surface-disabled); + background-color: var(--surface-raised-base); pointer-events: none; } - [data-slot="item-indicator"] { margin-left: auto; } - &:focus { outline: none; } - &:hover { - background-color: var(--surface-hover); + background: var(--surface-raised-base-hover); } } } diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx index ecf05d5e2..111608e28 100644 --- a/packages/ui/src/components/select.tsx +++ b/packages/ui/src/components/select.tsx @@ -52,7 +52,7 @@ export function Select(props: SelectProps & ButtonProps) { {props.label ? props.label(itemProps.item.rawValue) : (itemProps.item.rawValue as string)} - + )} @@ -79,7 +79,7 @@ export function Select(props: SelectProps & ButtonProps) { }} - + diff --git a/packages/ui/src/components/tabs.css b/packages/ui/src/components/tabs.css index c6d09c656..70d7b03e1 100644 --- a/packages/ui/src/components/tabs.css +++ b/packages/ui/src/components/tabs.css @@ -10,7 +10,7 @@ background-color: var(--background-stronger); overflow: clip; - & [data-slot="list"] { + [data-slot="list"] { width: 100%; position: relative; display: flex; @@ -40,7 +40,7 @@ } } - & [data-slot="trigger"] { + [data-slot="trigger"] { position: relative; height: 36px; padding: 8px 12px; @@ -49,7 +49,7 @@ font-size: var(--text-sm); font-weight: var(--font-weight-medium); color: var(--text-weak); - cursor: pointer; + white-space: nowrap; flex-shrink: 0; border-bottom: 1px solid var(--border-weak-base); @@ -77,7 +77,7 @@ } } - & [data-slot="content"] { + [data-slot="content"] { overflow-y: auto; flex: 1; diff --git a/packages/ui/src/components/tooltip.tsx b/packages/ui/src/components/tooltip.tsx index 14e433e21..ff13c8d61 100644 --- a/packages/ui/src/components/tooltip.tsx +++ b/packages/ui/src/components/tooltip.tsx @@ -36,7 +36,7 @@ export function Tooltip(props: TooltipProps) { {typeof others.value === "function" ? others.value() : others.value} - {/* */} + {/* */} diff --git a/packages/ui/src/hooks/index.ts b/packages/ui/src/hooks/index.ts new file mode 100644 index 000000000..7eef78091 --- /dev/null +++ b/packages/ui/src/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-filtered-list" diff --git a/packages/ui/src/hooks/use-filtered-list.tsx b/packages/ui/src/hooks/use-filtered-list.tsx new file mode 100644 index 000000000..b3ddf69ed --- /dev/null +++ b/packages/ui/src/hooks/use-filtered-list.tsx @@ -0,0 +1,89 @@ +import fuzzysort from "fuzzysort" +import { entries, flatMap, groupBy, map, pipe } from "remeda" +import { createMemo, createResource } from "solid-js" +import { createStore } from "solid-js/store" +import { createList } from "solid-list" + +export interface FilteredListProps { + items: T[] | ((filter: string) => 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 +} + +export function useFilteredList(props: FilteredListProps) { + const [store, setStore] = createStore<{ filter: string }>({ filter: "" }) + + const [grouped] = createResource( + () => store.filter, + async (filter) => { + const needle = filter?.toLowerCase() + const all = (typeof props.items === "function" ? await props.items(needle) : props.items) || [] + const result = pipe( + all, + (x) => { + if (!needle) return x + if (!props.filterKeys && Array.isArray(x) && x.every((e) => typeof e === "string")) { + return fuzzysort.go(needle, x).map((x) => x.target) as T[] + } + return fuzzysort.go(needle, x, { keys: props.filterKeys! }).map((x) => x.obj) + }, + groupBy((x) => (props.groupBy ? props.groupBy(x) : "")), + entries(), + map(([k, v]) => ({ category: k, items: props.sortBy ? v.sort(props.sortBy) : v })), + (groups) => (props.sortGroupsBy ? groups.sort(props.sortGroupsBy) : groups), + ) + return result + }, + ) + + const flat = createMemo(() => { + return pipe( + grouped() || [], + flatMap((x) => x.items), + ) + }) + + const list = createList({ + items: () => flat().map(props.key), + initialActive: props.current ? props.key(props.current) : props.key(flat()[0]), + loop: true, + }) + + const reset = () => { + const all = flat() + if (all.length === 0) return + list.setActive(props.key(all[0])) + } + + 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) + } else { + list.onKeyDown(event) + } + } + + const onInput = (value: string) => { + setStore("filter", value) + reset() + } + + return { + filter: () => store.filter, + grouped, + flat, + reset, + clear: () => setStore("filter", ""), + onKeyDown, + onInput, + active: list.active, + setActive: list.setActive, + } +} diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css index 0a89a4a0d..dc5335c43 100644 --- a/packages/ui/src/styles/index.css +++ b/packages/ui/src/styles/index.css @@ -6,9 +6,13 @@ @import "./base.css" layer(base); @import "../components/button.css" layer(components); +@import "../components/dialog.css" layer(components); @import "../components/icon.css" layer(components); +@import "../components/icon-button.css" layer(components); +@import "../components/input.css" layer(components); @import "../components/list.css" layer(components); @import "../components/select.css" layer(components); +@import "../components/select-dialog.css" layer(components); @import "../components/tabs.css" layer(components); @import "../components/tooltip.css" layer(components); diff --git a/packages/ui/src/styles/utilities.css b/packages/ui/src/styles/utilities.css index f01a6b2ee..7d14b6539 100644 --- a/packages/ui/src/styles/utilities.css +++ b/packages/ui/src/styles/utilities.css @@ -5,11 +5,11 @@ pointer-events: none; } - ::selection { - background-color: color-mix(in srgb, var(--color-primary) 33%, transparent); - /* background-color: var(--color-primary); */ - /* color: var(--color-background); */ - } + /* ::selection { */ + /* background-color: color-mix(in srgb, var(--color-primary) 33%, transparent); */ + /* background-color: var(--color-primary); */ + /* color: var(--color-background); */ + /* } */ ::-webkit-scrollbar-track { background: var(--theme-background-panel); @@ -36,6 +36,18 @@ } } +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + .text-12-regular { font-family: var(--font-family-sans); font-size: var(--font-size-small); diff --git a/packages/web/src/content/docs/sdk.mdx b/packages/web/src/content/docs/sdk.mdx index 2b90bd9bc..6d66cebfd 100644 --- a/packages/web/src/content/docs/sdk.mdx +++ b/packages/web/src/content/docs/sdk.mdx @@ -69,7 +69,7 @@ opencode.server.close() ## Client only -If you aready have a running instance of opencode, you can create a client instance to connect to it: +If you already have a running instance of opencode, you can create a client instance to connect to it: ```javascript import { createOpencodeClient } from "@opencode-ai/sdk" diff --git a/sdks/vscode/bun.lock b/sdks/vscode/bun.lock index a5d26f355..085f0661a 100644 --- a/sdks/vscode/bun.lock +++ b/sdks/vscode/bun.lock @@ -6,7 +6,7 @@ "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "20.x", - "@types/vscode": "^1.102.0", + "@types/vscode": "^1.94.0", "@typescript-eslint/eslint-plugin": "^8.31.1", "@typescript-eslint/parser": "^8.31.1", "@vscode/test-cli": "^0.0.11", diff --git a/sdks/vscode/script/publish b/sdks/vscode/script/publish index f8eb6d1f3..bc48f574c 100755 --- a/sdks/vscode/script/publish +++ b/sdks/vscode/script/publish @@ -1,4 +1,5 @@ #!/usr/bin/env bash +set -euo pipefail # Get the latest Git tag latest_tag=$(git tag --sort=committerdate | grep -E '^vscode-v[0-9]+\.[0-9]+\.[0-9]+$' | tail -1) @@ -7,14 +8,14 @@ if [ -z "$latest_tag" ]; then exit 1 fi echo "Latest tag: $latest_tag" -version=$(echo $latest_tag | sed 's/^vscode-v//') +version=$(echo "$latest_tag" | sed 's/^vscode-v//') echo "Latest version: $version" # package-marketplace -vsce package --no-git-tag-version --no-update-package-json --no-dependencies --skip-license -o dist/opencode.vsix $version +vsce package --no-git-tag-version --no-update-package-json --no-dependencies --skip-license -o dist/opencode.vsix "$version" # publish-marketplace vsce publish --packagePath dist/opencode.vsix # publish-openvsx -npx ovsx publish dist/opencode.vsix -p $OPENVSX_TOKEN \ No newline at end of file +npx ovsx publish dist/opencode.vsix -p "$OPENVSX_TOKEN" diff --git a/sdks/vscode/src/extension.ts b/sdks/vscode/src/extension.ts index 4d302fa4d..77c70513f 100644 --- a/sdks/vscode/src/extension.ts +++ b/sdks/vscode/src/extension.ts @@ -1,46 +1,46 @@ // This method is called when your extension is deactivated export function deactivate() {} -import * as vscode from "vscode" +import * as vscode from "vscode"; -const TERMINAL_NAME = "opencode" +const TERMINAL_NAME = "opencode"; export function activate(context: vscode.ExtensionContext) { let openNewTerminalDisposable = vscode.commands.registerCommand("opencode.openNewTerminal", async () => { - await openTerminal() - }) + await openTerminal(); + }); let openTerminalDisposable = vscode.commands.registerCommand("opencode.openTerminal", async () => { // An opencode terminal already exists => focus it - const existingTerminal = vscode.window.terminals.find((t) => t.name === TERMINAL_NAME) + const existingTerminal = vscode.window.terminals.find((t) => t.name === TERMINAL_NAME); if (existingTerminal) { - existingTerminal.show() - return + existingTerminal.show(); + return; } - await openTerminal() - }) + await openTerminal(); + }); let addFilepathDisposable = vscode.commands.registerCommand("opencode.addFilepathToTerminal", async () => { - const fileRef = getActiveFile() - if (!fileRef) return + const fileRef = getActiveFile(); + if (!fileRef) {return;} - const terminal = vscode.window.activeTerminal - if (!terminal) return + const terminal = vscode.window.activeTerminal; + if (!terminal) {return;} if (terminal.name === TERMINAL_NAME) { // @ts-ignore - const port = terminal.creationOptions.env?.["_EXTENSION_OPENCODE_PORT"] - port ? await appendPrompt(parseInt(port), fileRef) : terminal.sendText(fileRef) - terminal.show() + const port = terminal.creationOptions.env?.["_EXTENSION_OPENCODE_PORT"]; + port ? await appendPrompt(parseInt(port), fileRef) : terminal.sendText(fileRef); + terminal.show(); } - }) + }); - context.subscriptions.push(openTerminalDisposable, addFilepathDisposable) + context.subscriptions.push(openTerminalDisposable, addFilepathDisposable); async function openTerminal() { // Create a new terminal in split screen - const port = Math.floor(Math.random() * (65535 - 16384 + 1)) + 16384 + const port = Math.floor(Math.random() * (65535 - 16384 + 1)) + 16384; const terminal = vscode.window.createTerminal({ name: TERMINAL_NAME, iconPath: { @@ -55,32 +55,32 @@ export function activate(context: vscode.ExtensionContext) { _EXTENSION_OPENCODE_PORT: port.toString(), OPENCODE_CALLER: "vscode", }, - }) + }); - terminal.show() - terminal.sendText(`opencode --port ${port}`) + terminal.show(); + terminal.sendText(`opencode --port ${port}`); - const fileRef = getActiveFile() - if (!fileRef) return + const fileRef = getActiveFile(); + if (!fileRef) {return;} // Wait for the terminal to be ready - let tries = 10 - let connected = false + let tries = 10; + let connected = false; do { - await new Promise((resolve) => setTimeout(resolve, 200)) + await new Promise((resolve) => setTimeout(resolve, 200)); try { - await fetch(`http://localhost:${port}/app`) - connected = true - break + await fetch(`http://localhost:${port}/app`); + connected = true; + break; } catch (e) {} - tries-- - } while (tries > 0) + tries--; + } while (tries > 0); // If connected, append the prompt to the terminal if (connected) { - await appendPrompt(port, `In ${fileRef}`) - terminal.show() + await appendPrompt(port, `In ${fileRef}`); + terminal.show(); } } @@ -91,37 +91,37 @@ export function activate(context: vscode.ExtensionContext) { "Content-Type": "application/json", }, body: JSON.stringify({ text }), - }) + }); } function getActiveFile() { - const activeEditor = vscode.window.activeTextEditor - if (!activeEditor) return + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) {return;} - const document = activeEditor.document - const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri) - if (!workspaceFolder) return + const document = activeEditor.document; + const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); + if (!workspaceFolder) {return;} // Get the relative path from workspace root - const relativePath = vscode.workspace.asRelativePath(document.uri) - let filepathWithAt = `@${relativePath}` + const relativePath = vscode.workspace.asRelativePath(document.uri); + let filepathWithAt = `@${relativePath}`; // Check if there's a selection and add line numbers - const selection = activeEditor.selection + const selection = activeEditor.selection; if (!selection.isEmpty) { // Convert to 1-based line numbers - const startLine = selection.start.line + 1 - const endLine = selection.end.line + 1 + const startLine = selection.start.line + 1; + const endLine = selection.end.line + 1; if (startLine === endLine) { // Single line selection - filepathWithAt += `#L${startLine}` + filepathWithAt += `#L${startLine}`; } else { // Multi-line selection - filepathWithAt += `#L${startLine}-${endLine}` + filepathWithAt += `#L${startLine}-${endLine}`; } } - return filepathWithAt + return filepathWithAt; } } diff --git a/sdks/vscode/tsconfig.json b/sdks/vscode/tsconfig.json index 83733a8fa..710f9ede4 100644 --- a/sdks/vscode/tsconfig.json +++ b/sdks/vscode/tsconfig.json @@ -6,7 +6,8 @@ "sourceMap": true, "rootDir": "src", "typeRoots": ["./node_modules/@types"], - "strict": true /* enable all strict type-checking options */ + "strict": true /* enable all strict type-checking options */, + "skipLibCheck": true /* Additional Checks */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */