diff --git a/.github/workflows/stats.yml b/.github/workflows/stats.yml index 188996aa..fed03b68 100644 --- a/.github/workflows/stats.yml +++ b/.github/workflows/stats.yml @@ -28,5 +28,5 @@ jobs: git config --local user.email "action@github.com" git config --local user.name "GitHub Action" git add STATS.md - git diff --staged --quiet || git commit -m "ignore: update download stats $(date -I)" + git diff --staged --quiet || git commit -m "Update download stats $(date -I)" git push diff --git a/README.md b/README.md index cf2aa6fe..24841799 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

AI coding agent, built for the terminal.

- Discord + View docs npm Build status

@@ -76,4 +76,4 @@ The other confusingly named repo has no relation to this one. You can [read the --- -**Join our community** [Discord](https://discord.gg/opencode) | [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev) +**Join our community** [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev) diff --git a/STATS.md b/STATS.md index e1e0d5e5..256ffb5d 100644 --- a/STATS.md +++ b/STATS.md @@ -1,13 +1,10 @@ # Download Stats -| Date | GitHub Downloads | npm Downloads | Total | -| ---------- | ---------------- | --------------- | ---------------- | -| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) | -| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) | -| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) | -| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) | -| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) | -| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) | -| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) | -| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) | -| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) | +| Date | GitHub Downloads | npm Downloads | Total | +| ---------- | ---------------- | --------------- | --------------- | +| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) | +| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) | +| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) | +| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) | +| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) | +| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) | diff --git a/bun.lock b/bun.lock index 8066b54a..a723e36b 100644 --- a/bun.lock +++ b/bun.lock @@ -31,6 +31,7 @@ "@openauthjs/openauth": "0.4.3", "@standard-schema/spec": "1.0.0", "ai": "catalog:", + "air": "0.4.14", "decimal.js": "10.5.0", "diff": "8.0.2", "env-paths": "3.0.0", @@ -78,7 +79,6 @@ "lang-map": "0.4.0", "luxon": "3.6.1", "marked": "15.0.12", - "marked-shiki": "1.2.0", "rehype-autolink-headings": "7.1.0", "sharp": "0.32.5", "shiki": "3.4.2", @@ -96,22 +96,30 @@ "sharp", "esbuild", ], + "patchedDependencies": { + "ai@4.3.16": "patches/ai@4.3.16.patch", + }, + "overrides": { + "zod": "3.24.2", + }, "catalog": { "@types/node": "22.13.9", - "ai": "5.0.0-beta.7", + "ai": "4.3.16", "typescript": "5.8.2", - "zod": "3.25.49", + "zod": "3.24.2", }, "packages": { "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@2.2.10", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-icLGO7Q0NinnHIPgT+y1QjHVwH4HwV+brWbvM+FfCG2Afpa89PyKa3Ret91kGjZpBgM/xnj1B7K5eM+rRlsXQA=="], "@ai-sdk/anthropic": ["@ai-sdk/anthropic@1.2.12", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.0-beta.3", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.2" }, "peerDependencies": { "zod": "^3.25.49" } }, "sha512-g49gMSkXy94lYvl5LRh438OR/0JCG6ol0jV+iLot7cy5HLltZlGocEuauETBu4b10mDXOd7XIjTEZoQpYFMYLQ=="], + "@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], - "@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-beta.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Z8SPncMtS3RsoXITmT7NVwrAq6M44dmw0DoUOYJqNNtCu8iMWuxB8Nxsoqpa0uEEy9R1V1ZThJAXTYgjTUxl3w=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-beta.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.49" } }, "sha512-H4K+4weOVgWqrDDeAbQWoA4U5mN4WrQPHQFdH7ynQYcnhj/pzctU9Q6mGlR5ESMWxaXxazxlOblSITlXo9bahA=="], + "@ai-sdk/react": ["@ai-sdk/react@1.2.12", "", { "dependencies": { "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/ui-utils": "1.2.11", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["zod"] }, "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g=="], + + "@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="], "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], @@ -459,6 +467,8 @@ "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + "@types/diff-match-patch": ["@types/diff-match-patch@1.0.36", "", {}, "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="], + "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], @@ -505,7 +515,9 @@ "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], - "ai": ["ai@5.0.0-beta.7", "", { "dependencies": { "@ai-sdk/gateway": "1.0.0-beta.3", "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.2", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.49" } }, "sha512-oC4KzUJCQPMB7v9rCqL/rVk2ogZvI6lYiXfKjzPYHwa1zIgy329qqRLmAd3mKEDTTG6By1r0zasQu7FKmG+4gw=="], + "ai": ["ai@4.3.16", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/react": "1.2.12", "@ai-sdk/ui-utils": "1.2.11", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g=="], + + "air": ["air@0.4.14", "", { "dependencies": { "zephyr": "~1.3.5" } }, "sha512-E8bl9LlSGSQqjxxjeGIrpYpf8jVyJplsdK1bTobh61F7ks+3aLeXL4KbGSJIFsiaSSz5ZExLU51DGztmQSlZTQ=="], "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], @@ -723,6 +735,8 @@ "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + "diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="], + "diff3": ["diff3@0.0.3", "", {}, "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g=="], "direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="], @@ -791,7 +805,7 @@ "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - "eventsource-parser": ["eventsource-parser@3.0.3", "", {}, "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA=="], + "eventsource-parser": ["eventsource-parser@3.0.2", "", {}, "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA=="], "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], @@ -1013,6 +1027,8 @@ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="], + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], @@ -1039,8 +1055,6 @@ "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], - "marked-shiki": ["marked-shiki@1.2.0", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-N924hp8veE6Mc91g5/kCNVoTU7TkeJfB2G2XEWb+k1fVA0Bck2T0rVt93d39BlOYH6ohP4Q9BFlPk+UkblhXbg=="], - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="], @@ -1329,6 +1343,8 @@ "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], @@ -1513,6 +1529,8 @@ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="], + "tar-fs": ["tar-fs@3.0.9", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA=="], "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], @@ -1521,6 +1539,8 @@ "thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="], + "throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="], + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], @@ -1605,6 +1625,8 @@ "url": ["url@0.10.3", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="], + "use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], + "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], @@ -1681,7 +1703,9 @@ "youch": ["youch@3.3.4", "", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="], - "zod": ["zod@3.25.49", "", {}, "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q=="], + "zephyr": ["zephyr@1.3.6", "", {}, "sha512-oYH52DGZzIbXNrkijskaR8YpVKnXAe8jNgH1KirglVBnTFOn6mK9/0SVCxGn+73l0Hjhr4UYNzYkO07LXSWy6w=="], + + "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], "zod-openapi": ["zod-openapi@4.2.4", "", { "peerDependencies": { "zod": "^3.21.4" } }, "sha512-tsrQpbpqFCXqVXUzi3TPwFhuMtLN3oNZobOtYnK6/5VkXsNdnIgyNr4r8no4wmYluaxzN3F7iS+8xCW8BmMQ8g=="], @@ -1693,22 +1717,12 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], - - "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="], - "@ai-sdk/amazon-bedrock/aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], - "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], - - "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="], - "@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], "@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@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.1.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.2.1", "smol-toml": "^1.3.1", "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-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q=="], - "@astrojs/sitemap/zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], - "@aws-crypto/crc32/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], @@ -1727,8 +1741,6 @@ "@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], - "@modelcontextprotocol/sdk/zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], - "@openauthjs/openauth/@standard-schema/spec": ["@standard-schema/spec@1.0.0-beta.3", "", {}, "sha512-0ifF3BjA1E8SY9C+nUew8RefNOIq0cDlYALPty4rhUm8Rrl6tCM8hBT4bhGhx7I7iXD0uAgt50lgo8dD73ACMw=="], "@openauthjs/openauth/aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], @@ -1765,14 +1777,10 @@ "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/zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], - "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "eventsource/eventsource-parser": ["eventsource-parser@3.0.2", "", {}, "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA=="], - "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "get-source/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -1785,12 +1793,8 @@ "miniflare/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=="], - "miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], - "opencontrol/hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="], - "opencontrol/zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], - "opencontrol/zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], diff --git a/infra/app.ts b/infra/app.ts index caaea0e9..834936b7 100644 --- a/infra/app.ts +++ b/infra/app.ts @@ -39,8 +39,6 @@ new sst.cloudflare.x.Astro("Web", { domain, path: "packages/web", environment: { - // For astro config - SST_STAGE: $app.stage, VITE_API_URL: api.url, }, }) diff --git a/opencode.json b/opencode.json index ff206980..57b94008 100644 --- a/opencode.json +++ b/opencode.json @@ -1,11 +1,5 @@ { "$schema": "https://opencode.ai/config.json", - "mcp": { - "weather": { - "type": "local", - "command": ["opencode", "x", "@h1deya/mcp-server-weather"] - } - }, "experimental": { "hook": { "file_edited": { diff --git a/package.json b/package.json index 7a8c6b07..09248dcf 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "bun run packages/opencode/src/index.ts", "typecheck": "bun run --filter='*' typecheck", - "stainless": "./scripts/stainless", + "stainless": "bun run ./packages/opencode/src/index.ts serve ", "postinstall": "./scripts/hooks" }, "workspaces": { @@ -17,8 +17,8 @@ "catalog": { "typescript": "5.8.2", "@types/node": "22.13.9", - "zod": "3.25.49", - "ai": "5.0.0-beta.7" + "zod": "3.24.2", + "ai": "4.3.16" } }, "devDependencies": { @@ -31,8 +31,10 @@ }, "license": "MIT", "prettier": { - "semi": false, - "printWidth": 120 + "semi": false + }, + "overrides": { + "zod": "3.24.2" }, "trustedDependencies": [ "esbuild", diff --git a/packages/function/src/api.ts b/packages/function/src/api.ts index be6ef192..701b9854 100644 --- a/packages/function/src/api.ts +++ b/packages/function/src/api.ts @@ -38,7 +38,10 @@ export class SyncServer extends DurableObject { async publish(key: string, content: any) { const sessionID = await this.getSessionID() - if (!key.startsWith(`session/info/${sessionID}`) && !key.startsWith(`session/message/${sessionID}/`)) + if ( + !key.startsWith(`session/info/${sessionID}`) && + !key.startsWith(`session/message/${sessionID}/`) + ) return new Response("Error: Invalid key", { status: 400 }) // store message @@ -181,7 +184,8 @@ export default { } const id = url.searchParams.get("id") console.log("share_poll", id) - if (!id) return new Response("Error: Share ID is required", { status: 400 }) + if (!id) + return new Response("Error: Share ID is required", { status: 400 }) const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id)) return stub.fetch(request) } @@ -189,7 +193,8 @@ export default { if (request.method === "GET" && method === "share_data") { const id = url.searchParams.get("id") console.log("share_data", id) - if (!id) return new Response("Error: Share ID is required", { status: 400 }) + if (!id) + return new Response("Error: Share ID is required", { status: 400 }) const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id)) const data = await stub.getData() diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts index 967313ff..3f4c2005 100755 --- a/packages/opencode/script/publish.ts +++ b/packages/opencode/script/publish.ts @@ -57,7 +57,8 @@ for (const [os, arch] of targets) { 2, ), ) - if (!dry) await $`cd dist/${name} && bun publish --access public --tag ${npmTag}` + if (!dry) + await $`cd dist/${name} && bun publish --access public --tag ${npmTag}` optionalDependencies[name] = version } @@ -81,7 +82,8 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write( 2, ), ) -if (!dry) await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${npmTag}` +if (!dry) + await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${npmTag}` if (!snapshot) { // Github Release @@ -89,11 +91,15 @@ if (!snapshot) { await $`cd dist/${key}/bin && zip -r ../../${key}.zip *` } - const previous = await fetch("https://api.github.com/repos/sst/opencode/releases/latest") + const previous = await fetch( + "https://api.github.com/repos/sst/opencode/releases/latest", + ) .then((res) => res.json()) .then((data) => data.tag_name) - const commits = await fetch(`https://api.github.com/repos/sst/opencode/compare/${previous}...HEAD`) + const commits = await fetch( + `https://api.github.com/repos/sst/opencode/compare/${previous}...HEAD`, + ) .then((res) => res.json()) .then((data) => data.commits || []) @@ -111,13 +117,26 @@ if (!snapshot) { }) .join("\n") - if (!dry) await $`gh release create v${version} --title "v${version}" --notes ${notes} ./dist/*.zip` + if (!dry) + await $`gh release create v${version} --title "v${version}" --notes ${notes} ./dist/*.zip` // Calculate SHA values - const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim()) - const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim()) - const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim()) - const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim()) + const arm64Sha = + await $`sha256sum ./dist/opencode-linux-arm64.zip | cut -d' ' -f1` + .text() + .then((x) => x.trim()) + const x64Sha = + await $`sha256sum ./dist/opencode-linux-x64.zip | cut -d' ' -f1` + .text() + .then((x) => x.trim()) + const macX64Sha = + await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1` + .text() + .then((x) => x.trim()) + const macArm64Sha = + await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1` + .text() + .then((x) => x.trim()) // AUR package const pkgbuild = [ @@ -151,7 +170,9 @@ if (!snapshot) { for (const pkg of ["opencode", "opencode-bin"]) { await $`rm -rf ./dist/aur-${pkg}` await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}` - await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild.replace("${pkg}", pkg)) + await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write( + pkgbuild.replace("${pkg}", pkg), + ) await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO` await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO` await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"` diff --git a/packages/opencode/src/app/app.ts b/packages/opencode/src/app/app.ts index 69ddd2ac..6467d252 100644 --- a/packages/opencode/src/app/app.ts +++ b/packages/opencode/src/app/app.ts @@ -45,14 +45,23 @@ export namespace App { } export const provideExisting = ctx.provide - export async function provide(input: Input, cb: (app: App.Info) => Promise) { + export async function provide( + input: Input, + cb: (app: App.Info) => Promise, + ) { log.info("creating", { cwd: input.cwd, }) - const git = await Filesystem.findUp(".git", input.cwd).then(([x]) => (x ? path.dirname(x) : undefined)) + const git = await Filesystem.findUp(".git", input.cwd).then(([x]) => + x ? path.dirname(x) : undefined, + ) log.info("git", { git }) - const data = path.join(Global.Path.data, "project", git ? directory(git) : "global") + const data = path.join( + Global.Path.data, + "project", + git ? directory(git) : "global", + ) const stateFile = Bun.file(path.join(data, APP_JSON)) const state = (await stateFile.json().catch(() => ({}))) as { initialized: number diff --git a/packages/opencode/src/auth/anthropic.ts b/packages/opencode/src/auth/anthropic.ts index cb38238e..df4af692 100644 --- a/packages/opencode/src/auth/anthropic.ts +++ b/packages/opencode/src/auth/anthropic.ts @@ -10,8 +10,14 @@ export namespace AuthAnthropic { url.searchParams.set("code", "true") url.searchParams.set("client_id", CLIENT_ID) url.searchParams.set("response_type", "code") - url.searchParams.set("redirect_uri", "https://console.anthropic.com/oauth/code/callback") - url.searchParams.set("scope", "org:create_api_key user:profile user:inference") + url.searchParams.set( + "redirect_uri", + "https://console.anthropic.com/oauth/code/callback", + ) + url.searchParams.set( + "scope", + "org:create_api_key user:profile user:inference", + ) url.searchParams.set("code_challenge", pkce.challenge) url.searchParams.set("code_challenge_method", "S256") url.searchParams.set("state", pkce.verifier) @@ -51,17 +57,20 @@ export namespace AuthAnthropic { const info = await Auth.get("anthropic") if (!info || info.type !== "oauth") return if (info.access && info.expires > Date.now()) return info.access - const response = await fetch("https://console.anthropic.com/v1/oauth/token", { - method: "POST", - headers: { - "Content-Type": "application/json", + const response = await fetch( + "https://console.anthropic.com/v1/oauth/token", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + grant_type: "refresh_token", + refresh_token: info.refresh, + client_id: CLIENT_ID, + }), }, - body: JSON.stringify({ - grant_type: "refresh_token", - refresh_token: info.refresh, - client_id: CLIENT_ID, - }), - }) + ) if (!response.ok) return const json = await response.json() await Auth.set("anthropic", { diff --git a/packages/opencode/src/auth/copilot.ts b/packages/opencode/src/auth/copilot.ts index 042f7c35..4bbbaf2c 100644 --- a/packages/opencode/src/auth/copilot.ts +++ b/packages/opencode/src/auth/copilot.ts @@ -4,7 +4,9 @@ import path from "path" export const AuthCopilot = lazy(async () => { const file = Bun.file(path.join(Global.Path.state, "plugin", "copilot.ts")) - const response = fetch("https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts") + const response = fetch( + "https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts", + ) .then((x) => Bun.write(file, x)) .catch(() => {}) diff --git a/packages/opencode/src/auth/github-copilot.ts b/packages/opencode/src/auth/github-copilot.ts index ba5274e5..c03caa26 100644 --- a/packages/opencode/src/auth/github-copilot.ts +++ b/packages/opencode/src/auth/github-copilot.ts @@ -122,7 +122,10 @@ export namespace AuthGithubCopilot { return tokenData.token } - export const DeviceCodeError = NamedError.create("DeviceCodeError", z.object({})) + export const DeviceCodeError = NamedError.create( + "DeviceCodeError", + z.object({}), + ) export const TokenExchangeError = NamedError.create( "TokenExchangeError", diff --git a/packages/opencode/src/bun/index.ts b/packages/opencode/src/bun/index.ts index 23180e31..7a7d89cf 100644 --- a/packages/opencode/src/bun/index.ts +++ b/packages/opencode/src/bun/index.ts @@ -8,7 +8,10 @@ import { readableStreamToText } from "bun" export namespace BunProc { const log = Log.create({ service: "bun" }) - export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject) { + export async function run( + cmd: string[], + options?: Bun.SpawnOptions.OptionsObject, + ) { log.info("running", { cmd: [which(), ...cmd], ...options, @@ -23,17 +26,9 @@ export namespace BunProc { BUN_BE_BUN: "1", }, }) - const code = await result.exited - const stdout = result.stdout - ? typeof result.stdout === "number" - ? result.stdout - : await readableStreamToText(result.stdout) - : undefined - const stderr = result.stderr - ? typeof result.stderr === "number" - ? result.stderr - : await readableStreamToText(result.stderr) - : undefined + const code = await result.exited; + const stdout = result.stdout ? typeof result.stdout === "number" ? result.stdout : await readableStreamToText(result.stdout) : undefined + const stderr = result.stderr ? typeof result.stderr === "number" ? result.stderr : await readableStreamToText(result.stderr) : undefined log.info("done", { code, stdout, @@ -66,7 +61,7 @@ export namespace BunProc { if (parsed.dependencies[pkg] === version) return mod parsed.dependencies[pkg] = version await Bun.write(pkgjson, JSON.stringify(parsed, null, 2)) - await BunProc.run(["install", "--cwd", Global.Path.cache, "--registry=https://registry.npmjs.org"], { + await BunProc.run(["install", "--registry=https://registry.npmjs.org"], { cwd: Global.Path.cache, }).catch((e) => { throw new InstallFailedError( diff --git a/packages/opencode/src/bus/index.ts b/packages/opencode/src/bus/index.ts index 0353da90..8461269a 100644 --- a/packages/opencode/src/bus/index.ts +++ b/packages/opencode/src/bus/index.ts @@ -18,7 +18,10 @@ export namespace Bus { const registry = new Map() - export function event(type: Type, properties: Properties) { + export function event( + type: Type, + properties: Properties, + ) { const result = { type, properties, @@ -69,7 +72,10 @@ export namespace Bus { export function subscribe( def: Definition, - callback: (event: { type: Definition["type"]; properties: z.infer }) => void, + callback: (event: { + type: Definition["type"] + properties: z.infer + }) => void, ) { return raw(def.type, callback) } diff --git a/packages/opencode/src/cli/bootstrap.ts b/packages/opencode/src/cli/bootstrap.ts index 4419773b..9ae274ed 100644 --- a/packages/opencode/src/cli/bootstrap.ts +++ b/packages/opencode/src/cli/bootstrap.ts @@ -1,15 +1,20 @@ import { App } from "../app/app" import { ConfigHooks } from "../config/hooks" +import { FileWatcher } from "../file/watch" import { Format } from "../format" import { LSP } from "../lsp" import { Share } from "../share/share" -export async function bootstrap(input: App.Input, cb: (app: App.Info) => Promise) { +export async function bootstrap( + input: App.Input, + cb: (app: App.Info) => Promise, +) { return App.provide(input, async (app) => { Share.init() Format.init() ConfigHooks.init() LSP.init() + FileWatcher.init() return cb(app) }) diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index 91ec61ea..9e8da95b 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -15,7 +15,11 @@ export const AuthCommand = cmd({ command: "auth", describe: "manage credentials", builder: (yargs) => - yargs.command(AuthLoginCommand).command(AuthLogoutCommand).command(AuthListCommand).demandCommand(), + yargs + .command(AuthLoginCommand) + .command(AuthLogoutCommand) + .command(AuthListCommand) + .demandCommand(), async handler() {}, }) @@ -27,7 +31,9 @@ export const AuthListCommand = cmd({ UI.empty() const authPath = path.join(Global.Path.data, "auth.json") const homedir = os.homedir() - const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath + const displayPath = authPath.startsWith(homedir) + ? authPath.replace(homedir, "~") + : authPath prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`) const results = await Auth.all().then((x) => Object.entries(x)) const database = await ModelsDev.get() @@ -108,7 +114,8 @@ export const AuthLoginCommand = cmd({ if (provider === "other") { provider = await prompts.text({ message: "Enter provider id", - validate: (x) => (x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only"), + validate: (x) => + x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only", }) if (prompts.isCancel(provider)) throw new UI.CancelledError() provider = provider.replace(/^@ai-sdk\//, "") @@ -179,13 +186,17 @@ export const AuthLoginCommand = cmd({ await new Promise((resolve) => setTimeout(resolve, 10)) const deviceInfo = await copilot.authorize() - prompts.note(`Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`) + prompts.note( + `Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`, + ) const spinner = prompts.spinner() spinner.start("Waiting for authorization...") while (true) { - await new Promise((resolve) => setTimeout(resolve, deviceInfo.interval * 1000)) + await new Promise((resolve) => + setTimeout(resolve, deviceInfo.interval * 1000), + ) const response = await copilot.poll(deviceInfo.device) if (response.status === "pending") continue if (response.status === "success") { @@ -237,7 +248,12 @@ export const AuthLogoutCommand = cmd({ const providerID = await prompts.select({ message: "Select provider", options: credentials.map(([key, value]) => ({ - label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")", + label: + (database[key]?.name || key) + + UI.Style.TEXT_DIM + + " (" + + value.type + + ")", value: key, })), }) diff --git a/packages/opencode/src/cli/cmd/debug/file.ts b/packages/opencode/src/cli/cmd/debug/file.ts index f773dbd9..021c49db 100644 --- a/packages/opencode/src/cli/cmd/debug/file.ts +++ b/packages/opencode/src/cli/cmd/debug/file.ts @@ -31,6 +31,7 @@ const FileStatusCommand = cmd({ export const FileCommand = cmd({ command: "file", - builder: (yargs) => yargs.command(FileReadCommand).command(FileStatusCommand).demandCommand(), + builder: (yargs) => + yargs.command(FileReadCommand).command(FileStatusCommand).demandCommand(), async handler() {}, }) diff --git a/packages/opencode/src/cli/cmd/debug/index.ts b/packages/opencode/src/cli/cmd/debug/index.ts index 886131b6..e748183e 100644 --- a/packages/opencode/src/cli/cmd/debug/index.ts +++ b/packages/opencode/src/cli/cmd/debug/index.ts @@ -17,7 +17,9 @@ export const DebugCommand = cmd({ command: "wait", async handler() { await bootstrap({ cwd: process.cwd() }, async () => { - await new Promise((resolve) => setTimeout(resolve, 1_000 * 60 * 60 * 24)) + await new Promise((resolve) => + setTimeout(resolve, 1_000 * 60 * 60 * 24), + ) }) }, }) diff --git a/packages/opencode/src/cli/cmd/debug/lsp.ts b/packages/opencode/src/cli/cmd/debug/lsp.ts index 8a54b4b0..d596bf6c 100644 --- a/packages/opencode/src/cli/cmd/debug/lsp.ts +++ b/packages/opencode/src/cli/cmd/debug/lsp.ts @@ -5,13 +5,15 @@ import { Log } from "../../../util/log" export const LSPCommand = cmd({ command: "lsp", - builder: (yargs) => yargs.command(DiagnosticsCommand).command(SymbolsCommand).demandCommand(), + builder: (yargs) => + yargs.command(DiagnosticsCommand).command(SymbolsCommand).demandCommand(), async handler() {}, }) const DiagnosticsCommand = cmd({ command: "diagnostics ", - builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }), + builder: (yargs) => + yargs.positional("file", { type: "string", demandOption: true }), async handler(args) { await bootstrap({ cwd: process.cwd() }, async () => { await LSP.touchFile(args.file, true) @@ -22,7 +24,8 @@ const DiagnosticsCommand = cmd({ export const SymbolsCommand = cmd({ command: "symbols ", - builder: (yargs) => yargs.positional("query", { type: "string", demandOption: true }), + builder: (yargs) => + yargs.positional("query", { type: "string", demandOption: true }), async handler(args) { await bootstrap({ cwd: process.cwd() }, async () => { await LSP.touchFile("./src/index.ts", true) diff --git a/packages/opencode/src/cli/cmd/debug/ripgrep.ts b/packages/opencode/src/cli/cmd/debug/ripgrep.ts index 3e2b434e..c7136867 100644 --- a/packages/opencode/src/cli/cmd/debug/ripgrep.ts +++ b/packages/opencode/src/cli/cmd/debug/ripgrep.ts @@ -5,7 +5,12 @@ import { cmd } from "../cmd" export const RipgrepCommand = cmd({ command: "rg", - builder: (yargs) => yargs.command(TreeCommand).command(FilesCommand).command(SearchCommand).demandCommand(), + builder: (yargs) => + yargs + .command(TreeCommand) + .command(FilesCommand) + .command(SearchCommand) + .demandCommand(), async handler() {}, }) diff --git a/packages/opencode/src/cli/cmd/debug/snapshot.ts b/packages/opencode/src/cli/cmd/debug/snapshot.ts index a06048c6..a6d129d5 100644 --- a/packages/opencode/src/cli/cmd/debug/snapshot.ts +++ b/packages/opencode/src/cli/cmd/debug/snapshot.ts @@ -4,7 +4,11 @@ import { cmd } from "../cmd" export const SnapshotCommand = cmd({ command: "snapshot", - builder: (yargs) => yargs.command(SnapshotCreateCommand).command(SnapshotRestoreCommand).demandCommand(), + builder: (yargs) => + yargs + .command(SnapshotCreateCommand) + .command(SnapshotRestoreCommand) + .demandCommand(), async handler() {}, }) diff --git a/packages/opencode/src/cli/cmd/generate.ts b/packages/opencode/src/cli/cmd/generate.ts index d6ed0eb1..0cef1077 100644 --- a/packages/opencode/src/cli/cmd/generate.ts +++ b/packages/opencode/src/cli/cmd/generate.ts @@ -10,6 +10,9 @@ export const GenerateCommand = { const dir = "gen" await fs.rmdir(dir, { recursive: true }).catch(() => {}) await fs.mkdir(dir, { recursive: true }) - await Bun.write(path.join(dir, "openapi.json"), JSON.stringify(specs, null, 2)) + await Bun.write( + path.join(dir, "openapi.json"), + JSON.stringify(specs, null, 2), + ) }, } satisfies CommandModule diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 218e1ea8..1905aa17 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -2,12 +2,12 @@ import type { Argv } from "yargs" import { Bus } from "../../bus" import { Provider } from "../../provider/provider" import { Session } from "../../session" +import { Message } from "../../session/message" import { UI } from "../ui" import { cmd } from "./cmd" import { Flag } from "../../flag/flag" import { Config } from "../../config/config" import { bootstrap } from "../bootstrap" -import { MessageV2 } from "../../session/message-v2" const TOOL: Record = { todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD], @@ -54,10 +54,7 @@ export const RunCommand = cmd({ }) }, handler: async (args) => { - let message = args.message.join(" ") - - if (!process.stdin.isTTY) message += "\n" + (await Bun.stdin.text()) - + const message = args.message.join(" ") await bootstrap({ cwd: process.cwd() }, async () => { const session = await (async () => { if (args.continue) { @@ -81,19 +78,27 @@ export const RunCommand = cmd({ UI.empty() UI.println(UI.logo()) UI.empty() - const displayMessage = message.length > 300 ? message.slice(0, 300) + "..." : message - UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", displayMessage) + UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message) UI.empty() const cfg = await Config.get() if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) { await Session.share(session.id) - UI.println(UI.Style.TEXT_INFO_BOLD + "~ https://opencode.ai/s/" + session.id.slice(-8)) + UI.println( + UI.Style.TEXT_INFO_BOLD + + "~ https://opencode.ai/s/" + + session.id.slice(-8), + ) } UI.empty() - const { providerID, modelID } = args.model ? Provider.parseModel(args.model) : await Provider.defaultModel() - UI.println(UI.Style.TEXT_NORMAL_BOLD + "@ ", UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`) + const { providerID, modelID } = args.model + ? Provider.parseModel(args.model) + : await Provider.defaultModel() + UI.println( + UI.Style.TEXT_NORMAL_BOLD + "@ ", + UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`, + ) UI.empty() function printEvent(color: string, type: string, title: string) { @@ -105,13 +110,24 @@ export const RunCommand = cmd({ ) } - Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => { + Bus.subscribe(Message.Event.PartUpdated, async (evt) => { if (evt.properties.sessionID !== session.id) return const part = evt.properties.part + const message = await Session.getMessage( + evt.properties.sessionID, + evt.properties.messageID, + ) - if (part.type === "tool" && part.state.status === "completed") { - const [tool, color] = TOOL[part.tool] ?? [part.tool, UI.Style.TEXT_INFO_BOLD] - printEvent(color, tool, part.state.title || "Unknown") + if ( + part.type === "tool-invocation" && + part.toolInvocation.state === "result" + ) { + const metadata = message.metadata.tool[part.toolInvocation.toolCallId] + const [tool, color] = TOOL[part.toolInvocation.toolName] ?? [ + part.toolInvocation.toolName, + UI.Style.TEXT_INFO_BOLD, + ] + printEvent(color, tool, metadata?.title || "Unknown") } if (part.type === "text") { @@ -125,20 +141,6 @@ export const RunCommand = cmd({ } }) - let errorMsg: string | undefined - Bus.subscribe(Session.Event.Error, async (evt) => { - const { sessionID, error } = evt.properties - if (sessionID !== session.id || !error) return - let err = String(error.name) - - if ("data" in error && error.data && "message" in error.data) { - err = error.data.message - } - errorMsg = errorMsg ? errorMsg + "\n" + err : err - - UI.error(err) - }) - const result = await Session.chat({ sessionID: session.id, providerID, @@ -154,7 +156,6 @@ export const RunCommand = cmd({ if (isPiped) { const match = result.parts.findLast((x) => x.type === "text") if (match) process.stdout.write(match.text) - if (errorMsg) process.stdout.write(errorMsg) } UI.empty() }) diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index 6dab38f1..f3686f30 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -38,7 +38,9 @@ export const ServeCommand = cmd({ hostname, }) - console.log(`opencode server listening on http://${server.hostname}:${server.port}`) + console.log( + `opencode server listening on http://${server.hostname}:${server.port}`, + ) await new Promise(() => {}) diff --git a/packages/opencode/src/cli/cmd/tui.ts b/packages/opencode/src/cli/cmd/tui.ts index e584a96c..5cdda83c 100644 --- a/packages/opencode/src/cli/cmd/tui.ts +++ b/packages/opencode/src/cli/cmd/tui.ts @@ -10,27 +10,15 @@ import { Installation } from "../../installation" import { Config } from "../../config/config" import { Bus } from "../../bus" import { Log } from "../../util/log" -import { FileWatcher } from "../../file/watch" export const TuiCommand = cmd({ command: "$0 [project]", describe: "start opencode tui", builder: (yargs) => - yargs - .positional("project", { - type: "string", - describe: "path to start opencode in", - }) - .option("model", { - type: "string", - alias: ["m"], - describe: "model to use in the format of provider/model", - }) - .option("prompt", { - alias: ["p"], - type: "string", - describe: "prompt to use", - }), + yargs.positional("project", { + type: "string", + describe: "path to start opencode in", + }), handler: async (args) => { while (true) { const cwd = args.project ? path.resolve(args.project) : process.cwd() @@ -41,7 +29,6 @@ export const TuiCommand = cmd({ return } const result = await bootstrap({ cwd }, async (app) => { - FileWatcher.init() const providers = await Provider.list() if (Object.keys(providers).length === 0) { return "needs_provider" @@ -53,7 +40,9 @@ export const TuiCommand = cmd({ }) let cmd = ["go", "run", "./main.go"] - let cwd = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url)) + let cwd = Bun.fileURLToPath( + new URL("../../../../tui/cmd/opencode", import.meta.url), + ) if (Bun.embeddedFiles.length > 0) { const blob = Bun.embeddedFiles[0] as File let binaryName = blob.name @@ -73,11 +62,7 @@ export const TuiCommand = cmd({ cmd, }) const proc = Bun.spawn({ - cmd: [ - ...cmd, - ...(args.model ? ["--model", args.model] : []), - ...(args.prompt ? ["--prompt", args.prompt] : []), - ], + cmd: [...cmd, ...process.argv.slice(2)], cwd, stdout: "inherit", stderr: "inherit", diff --git a/packages/opencode/src/cli/cmd/upgrade.ts b/packages/opencode/src/cli/cmd/upgrade.ts index 17d18168..759ab5ae 100644 --- a/packages/opencode/src/cli/cmd/upgrade.ts +++ b/packages/opencode/src/cli/cmd/upgrade.ts @@ -27,26 +27,22 @@ export const UpgradeCommand = { const detectedMethod = await Installation.method() const method = (args.method as Installation.Method) ?? detectedMethod if (method === "unknown") { - prompts.log.error(`opencode is installed to ${process.execPath} and seems to be managed by a package manager`) + prompts.log.error( + `opencode is installed to ${process.execPath} and seems to be managed by a package manager`, + ) prompts.outro("Done") return } prompts.log.info("Using method: " + method) const target = args.target ?? (await Installation.latest()) - - if (Installation.VERSION === target) { - prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`) - prompts.outro("Done") - return - } - prompts.log.info(`From ${Installation.VERSION} → ${target}`) const spinner = prompts.spinner() spinner.start("Upgrading...") const err = await Installation.upgrade(method, target).catch((err) => err) if (err) { spinner.stop("Upgrade failed") - if (err instanceof Installation.UpgradeFailedError) prompts.log.error(err.data.stderr) + if (err instanceof Installation.UpgradeFailedError) + prompts.log.error(err.data.stderr) else if (err instanceof Error) prompts.log.error(err.message) prompts.outro("Done") return diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts index 261206a1..752ad696 100644 --- a/packages/opencode/src/cli/error.ts +++ b/packages/opencode/src/cli/error.ts @@ -5,11 +5,14 @@ import { UI } from "./ui" export function FormatError(input: unknown) { if (MCP.Failed.isInstance(input)) return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.` - if (Config.JsonError.isInstance(input)) return `Config file at ${input.data.path} is not valid JSON` + if (Config.JsonError.isInstance(input)) + return `Config file at ${input.data.path} is not valid JSON` if (Config.InvalidError.isInstance(input)) return [ `Config file at ${input.data.path} is invalid`, - ...(input.data.issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ?? []), + ...(input.data.issues?.map( + (issue) => "↳ " + issue.message + " " + issue.path.join("."), + ) ?? []), ].join("\n") if (UI.CancelledError.isInstance(input)) return "" diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index c43a382a..eb67778e 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -29,12 +29,18 @@ export namespace Config { export const McpLocal = z .object({ type: z.literal("local").describe("Type of MCP server connection"), - command: z.string().array().describe("Command and arguments to run the MCP server"), + command: z + .string() + .array() + .describe("Command and arguments to run the MCP server"), environment: z .record(z.string(), z.string()) .optional() .describe("Environment variables to set when running the MCP server"), - enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), + enabled: z + .boolean() + .optional() + .describe("Enable or disable the MCP server on startup"), }) .strict() .openapi({ @@ -45,7 +51,10 @@ export namespace Config { .object({ type: z.literal("remote").describe("Type of MCP server connection"), url: z.string().describe("URL of the remote MCP server"), - enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), + enabled: z + .boolean() + .optional() + .describe("Enable or disable the MCP server on startup"), }) .strict() .openapi({ @@ -57,38 +66,68 @@ export namespace Config { export const Keybinds = z .object({ - leader: z.string().optional().describe("Leader key for keybind combinations"), - app_help: z.string().optional().describe("Show help dialog"), + leader: z + .string() + .optional() + .describe("Leader key for keybind combinations"), + help: z.string().optional().describe("Show help dialog"), editor_open: z.string().optional().describe("Open external editor"), session_new: z.string().optional().describe("Create a new session"), session_list: z.string().optional().describe("List all sessions"), session_share: z.string().optional().describe("Share current session"), - session_unshare: z.string().optional().describe("Unshare current session"), - session_interrupt: z.string().optional().describe("Interrupt current session"), - session_compact: z.string().optional().describe("Toggle compact mode for session"), + session_interrupt: z + .string() + .optional() + .describe("Interrupt current session"), + session_compact: z + .string() + .optional() + .describe("Toggle compact mode for session"), tool_details: z.string().optional().describe("Show tool details"), model_list: z.string().optional().describe("List available models"), theme_list: z.string().optional().describe("List available themes"), - file_list: z.string().optional().describe("List files"), - file_close: z.string().optional().describe("Close file"), - file_search: z.string().optional().describe("Search file"), - file_diff_toggle: z.string().optional().describe("Toggle split/unified diff"), - project_init: z.string().optional().describe("Initialize project configuration"), + project_init: z + .string() + .optional() + .describe("Initialize project configuration"), input_clear: z.string().optional().describe("Clear input field"), input_paste: z.string().optional().describe("Paste from clipboard"), input_submit: z.string().optional().describe("Submit input"), input_newline: z.string().optional().describe("Insert newline in input"), - messages_page_up: z.string().optional().describe("Scroll messages up by one page"), - messages_page_down: z.string().optional().describe("Scroll messages down by one page"), - messages_half_page_up: z.string().optional().describe("Scroll messages up by half page"), - messages_half_page_down: z.string().optional().describe("Scroll messages down by half page"), - messages_previous: z.string().optional().describe("Navigate to previous message"), + history_previous: z + .string() + .optional() + .describe("Navigate to previous history item"), + history_next: z + .string() + .optional() + .describe("Navigate to next history item"), + messages_page_up: z + .string() + .optional() + .describe("Scroll messages up by one page"), + messages_page_down: z + .string() + .optional() + .describe("Scroll messages down by one page"), + messages_half_page_up: z + .string() + .optional() + .describe("Scroll messages up by half page"), + messages_half_page_down: z + .string() + .optional() + .describe("Scroll messages down by half page"), + messages_previous: z + .string() + .optional() + .describe("Navigate to previous message"), messages_next: z.string().optional().describe("Navigate to next message"), - messages_first: z.string().optional().describe("Navigate to first message"), + messages_first: z + .string() + .optional() + .describe("Navigate to first message"), messages_last: z.string().optional().describe("Navigate to last message"), - messages_layout_toggle: z.string().optional().describe("Toggle layout"), - messages_copy: z.string().optional().describe("Copy message"), - messages_revert: z.string().optional().describe("Revert message"), app_exit: z.string().optional().describe("Exit the application"), }) .strict() @@ -97,13 +136,33 @@ export namespace Config { }) export const Info = z .object({ - $schema: z.string().optional().describe("JSON schema reference for configuration validation"), - theme: z.string().optional().describe("Theme name to use for the interface"), + $schema: z + .string() + .optional() + .describe("JSON schema reference for configuration validation"), + theme: z + .string() + .optional() + .describe("Theme name to use for the interface"), keybinds: Keybinds.optional().describe("Custom keybind configurations"), - autoshare: z.boolean().optional().describe("Share newly created sessions automatically"), - autoupdate: z.boolean().optional().describe("Automatically update to the latest version"), - disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"), - model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(), + autoshare: z + .boolean() + .optional() + .describe("Share newly created sessions automatically"), + autoupdate: z + .boolean() + .optional() + .describe("Automatically update to the latest version"), + disabled_providers: z + .array(z.string()) + .optional() + .describe("Disable providers that are loaded automatically"), + model: z + .string() + .describe( + "Model to use in the format of provider/model, eg anthropic/claude-2", + ) + .optional(), provider: z .record( ModelsDev.Provider.partial().extend({ @@ -113,8 +172,14 @@ export namespace Config { ) .optional() .describe("Custom provider configurations and model overrides"), - mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"), - instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"), + mcp: z + .record(z.string(), Mcp) + .optional() + .describe("MCP (Model Context Protocol) server configurations"), + instructions: z + .array(z.string()) + .optional() + .describe("Additional instruction files or patterns to include"), experimental: z .object({ hook: z @@ -162,7 +227,10 @@ export namespace Config { if (provider && model) result.model = `${provider}/${model}` result["$schema"] = "https://opencode.ai/config.json" result = mergeDeep(result, rest) - await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2)) + await Bun.write( + path.join(Global.Path.config, "config.json"), + JSON.stringify(result, null, 2), + ) await fs.unlink(path.join(Global.Path.config, "config")) }) .catch(() => {}) diff --git a/packages/opencode/src/config/hooks.ts b/packages/opencode/src/config/hooks.ts index 973575b7..ffa2475f 100644 --- a/packages/opencode/src/config/hooks.ts +++ b/packages/opencode/src/config/hooks.ts @@ -22,7 +22,9 @@ export namespace ConfigHooks { command: item.command, }) Bun.spawn({ - cmd: item.command.map((x) => x.replace("$FILE", payload.properties.file)), + cmd: item.command.map((x) => + x.replace("$FILE", payload.properties.file), + ), env: item.environment, cwd: app.path.cwd, stdout: "ignore", diff --git a/packages/opencode/src/file/fzf.ts b/packages/opencode/src/file/fzf.ts index 1376af8c..6d52702f 100644 --- a/packages/opencode/src/file/fzf.ts +++ b/packages/opencode/src/file/fzf.ts @@ -45,7 +45,10 @@ export namespace Fzf { log.info("found", { filepath }) return { filepath } } - filepath = path.join(Global.Path.bin, "fzf" + (process.platform === "win32" ? ".exe" : "")) + filepath = path.join( + Global.Path.bin, + "fzf" + (process.platform === "win32" ? ".exe" : ""), + ) const file = Bun.file(filepath) if (!(await file.exists())) { @@ -53,15 +56,18 @@ export namespace Fzf { const arch = archMap[process.arch as keyof typeof archMap] ?? "amd64" const config = PLATFORM[process.platform as keyof typeof PLATFORM] - if (!config) throw new UnsupportedPlatformError({ platform: process.platform }) + if (!config) + throw new UnsupportedPlatformError({ platform: process.platform }) const version = VERSION - const platformName = process.platform === "win32" ? "windows" : process.platform + const platformName = + process.platform === "win32" ? "windows" : process.platform const filename = `fzf-${version}-${platformName}_${arch}.${config.extension}` const url = `https://github.com/junegunn/fzf/releases/download/v${version}/${filename}` const response = await fetch(url) - if (!response.ok) throw new DownloadFailedError({ url, status: response.status }) + if (!response.ok) + throw new DownloadFailedError({ url, status: response.status }) const buffer = await response.arrayBuffer() const archivePath = path.join(Global.Path.bin, filename) @@ -80,11 +86,14 @@ export namespace Fzf { }) } if (config.extension === "zip") { - const proc = Bun.spawn(["unzip", "-j", archivePath, "fzf.exe", "-d", Global.Path.bin], { - cwd: Global.Path.bin, - stderr: "pipe", - stdout: "ignore", - }) + const proc = Bun.spawn( + ["unzip", "-j", archivePath, "fzf.exe", "-d", Global.Path.bin], + { + cwd: Global.Path.bin, + stderr: "pipe", + stdout: "ignore", + }, + ) await proc.exited if (proc.exitCode !== 0) throw new ExtractionFailedError({ diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index 1b93ca26..d9e61fa5 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -24,7 +24,11 @@ export namespace File { const app = App.info() if (!app.git) return [] - const diffOutput = await $`git diff --numstat HEAD`.cwd(app.path.cwd).quiet().nothrow().text() + const diffOutput = await $`git diff --numstat HEAD` + .cwd(app.path.cwd) + .quiet() + .nothrow() + .text() const changedFiles = [] @@ -41,13 +45,19 @@ export namespace File { } } - const untrackedOutput = await $`git ls-files --others --exclude-standard`.cwd(app.path.cwd).quiet().nothrow().text() + const untrackedOutput = await $`git ls-files --others --exclude-standard` + .cwd(app.path.cwd) + .quiet() + .nothrow() + .text() if (untrackedOutput.trim()) { const untrackedFiles = untrackedOutput.trim().split("\n") for (const filepath of untrackedFiles) { try { - const content = await Bun.file(path.join(app.path.root, filepath)).text() + const content = await Bun.file( + path.join(app.path.root, filepath), + ).text() const lines = content.split("\n").length changedFiles.push({ file: filepath, @@ -62,7 +72,11 @@ export namespace File { } // Get deleted files - const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD`.cwd(app.path.cwd).quiet().nothrow().text() + const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD` + .cwd(app.path.cwd) + .quiet() + .nothrow() + .text() if (deletedOutput.trim()) { const deletedFiles = deletedOutput.trim().split("\n") @@ -98,7 +112,11 @@ export namespace File { filepath: rel, }) if (diff !== "unmodified") { - const original = await $`git show HEAD:${rel}`.cwd(app.path.root).quiet().nothrow().text() + const original = await $`git show HEAD:${rel}` + .cwd(app.path.root) + .quiet() + .nothrow() + .text() const patch = createPatch(file, original, content, "old", "new", { context: Infinity, }) diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index 379dc4ad..a975d34b 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -122,11 +122,15 @@ export namespace Ripgrep { const state = lazy(async () => { let filepath = Bun.which("rg") if (filepath) return { filepath } - filepath = path.join(Global.Path.bin, "rg" + (process.platform === "win32" ? ".exe" : "")) + filepath = path.join( + Global.Path.bin, + "rg" + (process.platform === "win32" ? ".exe" : ""), + ) const file = Bun.file(filepath) if (!(await file.exists())) { - const platformKey = `${process.arch}-${process.platform}` as keyof typeof PLATFORM + const platformKey = + `${process.arch}-${process.platform}` as keyof typeof PLATFORM const config = PLATFORM[platformKey] if (!config) throw new UnsupportedPlatformError({ platform: platformKey }) @@ -135,7 +139,8 @@ export namespace Ripgrep { const url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}` const response = await fetch(url) - if (!response.ok) throw new DownloadFailedError({ url, status: response.status }) + if (!response.ok) + throw new DownloadFailedError({ url, status: response.status }) const buffer = await response.arrayBuffer() const archivePath = path.join(Global.Path.bin, filename) @@ -159,11 +164,14 @@ export namespace Ripgrep { }) } if (config.extension === "zip") { - const proc = Bun.spawn(["unzip", "-j", archivePath, "*/rg.exe", "-d", Global.Path.bin], { - cwd: Global.Path.bin, - stderr: "pipe", - stdout: "ignore", - }) + const proc = Bun.spawn( + ["unzip", "-j", archivePath, "*/rg.exe", "-d", Global.Path.bin], + { + cwd: Global.Path.bin, + stderr: "pipe", + stdout: "ignore", + }, + ) await proc.exited if (proc.exitCode !== 0) throw new ExtractionFailedError({ @@ -185,11 +193,17 @@ export namespace Ripgrep { return filepath } - export async function files(input: { cwd: string; query?: string; glob?: string; limit?: number }) { + export async function files(input: { + cwd: string + query?: string + glob?: string + limit?: number + }) { const commands = [ - `${await filepath()} --files --follow --hidden --glob='!.git/*' ${input.glob ? `--glob='${input.glob}'` : ``}`, + `${await filepath()} --files --hidden --glob='!.git/*' ${input.glob ? `--glob='${input.glob}'` : ``}`, ] - if (input.query) commands.push(`${await Fzf.filepath()} --filter=${input.query}`) + if (input.query) + commands.push(`${await Fzf.filepath()} --filter=${input.query}`) if (input.limit) commands.push(`head -n ${input.limit}`) const joined = commands.join(" | ") const result = await $`${{ raw: joined }}`.cwd(input.cwd).nothrow().text() @@ -296,8 +310,18 @@ export namespace Ripgrep { return lines.join("\n") } - export async function search(input: { cwd: string; pattern: string; glob?: string[]; limit?: number }) { - const args = [`${await filepath()}`, "--json", "--hidden", "--glob='!.git/*'"] + export async function search(input: { + cwd: string + pattern: string + glob?: string[] + limit?: number + }) { + const args = [ + `${await filepath()}`, + "--json", + "--hidden", + "--glob='!.git/*'", + ] if (input.glob) { for (const g of input.glob) { diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts index 453259e8..53132197 100644 --- a/packages/opencode/src/file/time.ts +++ b/packages/opencode/src/file/time.ts @@ -1,8 +1,6 @@ import { App } from "../app/app" -import { Log } from "../util/log" export namespace FileTime { - const log = Log.create({ service: "file.time" }) export const state = App.state("tool.filetimes", () => { const read: { [sessionID: string]: { @@ -15,7 +13,6 @@ export namespace FileTime { }) export function read(sessionID: string, file: string) { - log.info("read", { sessionID, file }) const { read } = state() read[sessionID] = read[sessionID] || {} read[sessionID][file] = new Date() @@ -27,7 +24,10 @@ export namespace FileTime { export async function assert(sessionID: string, filepath: string) { const time = get(sessionID, filepath) - if (!time) throw new Error(`You must read the file ${filepath} before overwriting it. Use the Read tool first`) + if (!time) + throw new Error( + `You must read the file ${filepath} before overwriting it. Use the Read tool first`, + ) const stats = await Bun.file(filepath).stat() if (stats.mtime.getTime() > time.getTime()) { throw new Error( diff --git a/packages/opencode/src/file/watch.ts b/packages/opencode/src/file/watch.ts index 6546f64d..1d12168f 100644 --- a/packages/opencode/src/file/watch.ts +++ b/packages/opencode/src/file/watch.ts @@ -21,20 +21,23 @@ export namespace FileWatcher { "file.watcher", () => { const app = App.use() - if (!app.info.git) return {} try { - const watcher = fs.watch(app.info.path.cwd, { recursive: true }, (event, file) => { - log.info("change", { file, event }) - if (!file) return - // for some reason async local storage is lost here - // https://github.com/oven-sh/bun/issues/20754 - App.provideExisting(app, async () => { - Bus.publish(Event.Updated, { - file, - event, + const watcher = fs.watch( + app.info.path.cwd, + { recursive: true }, + (event, file) => { + log.info("change", { file, event }) + if (!file) return + // for some reason async local storage is lost here + // https://github.com/oven-sh/bun/issues/20754 + App.provideExisting(app, async () => { + Bus.publish(Event.Updated, { + file, + event, + }) }) - }) - }) + }, + ) return { watcher } } catch { return {} diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index 3f24debf..60ca6a32 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -94,7 +94,21 @@ export const zig: Info = { export const clang: Info = { name: "clang-format", command: ["clang-format", "-i", "$FILE"], - extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"], + extensions: [ + ".c", + ".cc", + ".cpp", + ".cxx", + ".c++", + ".h", + ".hh", + ".hpp", + ".hxx", + ".h++", + ".ino", + ".C", + ".H", + ], async enabled() { return Bun.which("clang-format") !== null }, diff --git a/packages/opencode/src/id/id.ts b/packages/opencode/src/id/id.ts index b705ff2c..cf9a3042 100644 --- a/packages/opencode/src/id/id.ts +++ b/packages/opencode/src/id/id.ts @@ -26,7 +26,11 @@ export namespace Identifier { return generateID(prefix, true, given) } - function generateID(prefix: keyof typeof prefixes, descending: boolean, given?: string): string { + function generateID( + prefix: keyof typeof prefixes, + descending: boolean, + given?: string, + ): string { if (!given) { return generateNewID(prefix, descending) } @@ -38,7 +42,8 @@ export namespace Identifier { } function randomBase62(length: number): string { - const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + const chars = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" let result = "" const bytes = randomBytes(length) for (let i = 0; i < length; i++) { @@ -47,7 +52,10 @@ export namespace Identifier { return result } - function generateNewID(prefix: keyof typeof prefixes, descending: boolean): string { + function generateNewID( + prefix: keyof typeof prefixes, + descending: boolean, + ): string { const currentTimestamp = Date.now() if (currentTimestamp !== lastTimestamp) { @@ -65,6 +73,11 @@ export namespace Identifier { timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff)) } - return prefixes[prefix] + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12) + return ( + prefixes[prefix] + + "_" + + timeBytes.toString("hex") + + randomBase62(LENGTH - 12) + ) } } diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index f0328fe3..3ca40ce7 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -55,7 +55,10 @@ const cli = yargs(hideBin(process.argv)) .command(ServeCommand) .command(ModelsCommand) .fail((msg) => { - if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) { + if ( + msg.startsWith("Unknown argument") || + msg.startsWith("Not enough non-option arguments") + ) { cli.showHelp("log") } }) @@ -94,7 +97,10 @@ try { Log.Default.error("fatal", data) 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") + if (formatted === undefined) + UI.error( + "Unexpected error, check log file at " + Log.file() + " for more details", + ) process.exitCode = 1 } diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 9dfe2243..4af5c807 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -135,7 +135,8 @@ export namespace Installation { }) } - export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev" + export const VERSION = + typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev" export async function latest() { return fetch("https://api.github.com/repos/sst/opencode/releases/latest") diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index 7fafe7a9..5aff437d 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -1,5 +1,9 @@ import path from "path" -import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node" +import { + createMessageConnection, + StreamMessageReader, + StreamMessageWriter, +} from "vscode-jsonrpc/node" import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types" import { App } from "../app/app" import { Log } from "../util/log" @@ -62,7 +66,6 @@ export namespace LSPClient { log.info("sending initialize", { id: serverID }) await withTimeout( connection.sendRequest("initialize", { - rootUri: "file://" + app.path.cwd, processId: server.process.pid, workspaceFolders: [ { @@ -117,7 +120,9 @@ export namespace LSPClient { }, notify: { async open(input: { path: string }) { - input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path) + input.path = path.isAbsolute(input.path) + ? input.path + : path.resolve(app.path.cwd, input.path) const file = Bun.file(input.path) const text = await file.text() const version = files[input.path] @@ -149,13 +154,18 @@ export namespace LSPClient { return diagnostics }, async waitForDiagnostics(input: { path: string }) { - input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path) + input.path = path.isAbsolute(input.path) + ? input.path + : path.resolve(app.path.cwd, input.path) log.info("waiting for diagnostics", input) let unsub: () => void return await withTimeout( new Promise((resolve) => { unsub = Bus.subscribe(Event.Diagnostics, (event) => { - if (event.properties.path === input.path && event.properties.serverID === result.serverID) { + if ( + event.properties.path === input.path && + event.properties.serverID === result.serverID + ) { log.info("got diagnostics", input) unsub?.() resolve() diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 3434c030..88e549bb 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -46,7 +46,9 @@ export namespace LSP { if (!file) continue const handle = await server.spawn(App.info()) if (!handle) break - const client = await LSPClient.create(server.id, handle).catch((err) => log.error("", { error: err })) + const client = await LSPClient.create(server.id, handle).catch( + (err) => log.error("", { error: err }), + ) if (!client) break clients.set(server.id, client) break @@ -75,7 +77,9 @@ export namespace LSP { .map((x) => x.id) await run(async (client) => { if (!matches.includes(client.serverID)) return - const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve() + const wait = waitForDiagnostics + ? client.waitForDiagnostics({ path: input }) + : Promise.resolve() await client.notify.open({ path: input }) return wait }) @@ -93,7 +97,11 @@ export namespace LSP { return results } - export async function hover(input: { file: string; line: number; character: number }) { + export async function hover(input: { + file: string + line: number + character: number + }) { return run((client) => { return client.connection.sendRequest("textDocument/hover", { textDocument: { @@ -115,7 +123,9 @@ export namespace LSP { ).then((result) => result.flat() as LSP.Symbol[]) } - async function run(input: (client: LSPClient.Info) => Promise): Promise { + async function run( + input: (client: LSPClient.Info) => Promise, + ): Promise { const clients = await state().then((x) => [...x.clients.values()]) const tasks = clients.map((x) => input(x)) return Promise.all(tasks) diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 3d965acb..39a23f0f 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -25,14 +25,21 @@ export namespace LSPServer { id: "typescript", extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"], async spawn(app) { - const tsserver = await Bun.resolve("typescript/lib/tsserver.js", app.path.cwd).catch(() => {}) + const tsserver = await Bun.resolve( + "typescript/lib/tsserver.js", + app.path.cwd, + ).catch(() => {}) if (!tsserver) return - const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], { - env: { - ...process.env, - BUN_BE_BUN: "1", + const proc = spawn( + BunProc.which(), + ["x", "typescript-language-server", "--stdio"], + { + env: { + ...process.env, + BUN_BE_BUN: "1", + }, }, - }) + ) return { process: proc, initialization: { @@ -66,7 +73,10 @@ export namespace LSPServer { log.error("Failed to install gopls") return } - bin = path.join(Global.Path.bin, "gopls" + (process.platform === "win32" ? ".exe" : "")) + bin = path.join( + Global.Path.bin, + "gopls" + (process.platform === "win32" ? ".exe" : ""), + ) log.info(`installed gopls`, { bin, }) @@ -103,7 +113,10 @@ export namespace LSPServer { log.error("Failed to install ruby-lsp") return } - bin = path.join(Global.Path.bin, "ruby-lsp" + (process.platform === "win32" ? ".exe" : "")) + bin = path.join( + Global.Path.bin, + "ruby-lsp" + (process.platform === "win32" ? ".exe" : ""), + ) log.info(`installed ruby-lsp`, { bin, }) @@ -118,12 +131,16 @@ export namespace LSPServer { id: "pyright", extensions: [".py", ".pyi"], async spawn() { - const proc = spawn(BunProc.which(), ["x", "pyright-langserver", "--stdio"], { - env: { - ...process.env, - BUN_BE_BUN: "1", + const proc = spawn( + BunProc.which(), + ["x", "pyright-langserver", "--stdio"], + { + env: { + ...process.env, + BUN_BE_BUN: "1", + }, }, - }) + ) return { process: proc, } @@ -141,7 +158,9 @@ export namespace LSPServer { Global.Path.bin, "elixir-ls-master", "release", - process.platform === "win32" ? "language_server.bar" : "language_server.sh", + process.platform === "win32" + ? "language_server.bar" + : "language_server.sh", ) if (!(await Bun.file(binary).exists())) { @@ -153,7 +172,9 @@ export namespace LSPServer { log.info("downloading elixir-ls from GitHub releases") - const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip") + const response = await fetch( + "https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip", + ) if (!response.ok) return const zipPath = path.join(Global.Path.bin, "elixir-ls.zip") await Bun.file(zipPath).write(response) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 730d920b..f05d15ce 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -91,7 +91,8 @@ export namespace Provider { if (!info || info.type !== "oauth") return if (!info.access || info.expires < Date.now()) { const tokens = await copilot.access(info.refresh) - if (!tokens) throw new Error("GitHub Copilot authentication expired") + if (!tokens) + throw new Error("GitHub Copilot authentication expired") await Auth.set("github-copilot", { type: "oauth", ...tokens, @@ -100,9 +101,15 @@ export namespace Provider { } let isAgentCall = false try { - const body = typeof init.body === "string" ? JSON.parse(init.body) : init.body + const body = + typeof init.body === "string" + ? JSON.parse(init.body) + : init.body if (body?.messages) { - isAgentCall = body.messages.some((msg: any) => msg.role && ["tool", "assistant"].includes(msg.role)) + isAgentCall = body.messages.some( + (msg: any) => + msg.role && ["tool", "assistant"].includes(msg.role), + ) } } catch {} const headers = { @@ -131,11 +138,14 @@ export namespace Provider { } }, "amazon-bedrock": async () => { - if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"]) return { autoload: false } + if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"]) + return { autoload: false } const region = process.env["AWS_REGION"] ?? "us-east-1" - const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers")) + const { fromNodeProviderChain } = await import( + await BunProc.install("@aws-sdk/credential-providers") + ) return { autoload: true, options: { @@ -147,7 +157,9 @@ export namespace Provider { switch (regionPrefix) { case "us": { - const modelRequiresPrefix = ["claude", "deepseek"].some((m) => modelID.includes(m)) + const modelRequiresPrefix = ["claude", "deepseek"].some((m) => + modelID.includes(m), + ) if (modelRequiresPrefix) { modelID = `${regionPrefix}.${modelID}` } @@ -162,18 +174,25 @@ export namespace Provider { "eu-south-1", "eu-south-2", ].some((r) => region.includes(r)) - const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) => - modelID.includes(m), - ) + const modelRequiresPrefix = [ + "claude", + "nova-lite", + "nova-micro", + "llama3", + "pixtral", + ].some((m) => modelID.includes(m)) if (regionRequiresPrefix && modelRequiresPrefix) { modelID = `${regionPrefix}.${modelID}` } break } case "ap": { - const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) => - modelID.includes(m), - ) + const modelRequiresPrefix = [ + "claude", + "nova-lite", + "nova-micro", + "nova-pro", + ].some((m) => modelID.includes(m)) if (modelRequiresPrefix) { regionPrefix = "apac" modelID = `${regionPrefix}.${modelID}` @@ -211,7 +230,10 @@ export namespace Provider { options: Record } } = {} - const models = new Map() + const models = new Map< + string, + { info: ModelsDev.Model; language: LanguageModel } + >() const sdk = new Map() log.info("init") @@ -286,7 +308,9 @@ export namespace Provider { database[providerID] = parsed } - const disabled = await Config.get().then((cfg) => new Set(cfg.disabled_providers ?? [])) + const disabled = await Config.get().then( + (cfg) => new Set(cfg.disabled_providers ?? []), + ) // load env for (const [providerID, provider] of Object.entries(database)) { if (disabled.has(providerID)) continue @@ -313,7 +337,12 @@ export namespace Provider { if (disabled.has(providerID)) continue const result = await fn(database[providerID]) if (result && (result.autoload || providers[providerID])) { - mergeProvider(providerID, result.options ?? {}, "custom", result.getModel) + mergeProvider( + providerID, + result.options ?? {}, + "custom", + result.getModel, + ) } } @@ -350,7 +379,7 @@ export namespace Provider { const existing = s.sdk.get(provider.id) if (existing) return existing const pkg = provider.npm ?? provider.id - const mod = await import(await BunProc.install(pkg, "beta")) + const mod = await import(await BunProc.install(pkg, "latest")) const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!] const loaded = fn(s.providers[provider.id]?.options) s.sdk.set(provider.id, loaded) @@ -377,7 +406,9 @@ export namespace Provider { const sdk = await getSDK(provider.info) try { - const language = provider.getModel ? await provider.getModel(sdk, modelID) : sdk.languageModel(modelID) + const language = provider.getModel + ? await provider.getModel(sdk, modelID) + : sdk.languageModel(modelID) log.info("found", { providerID, modelID }) s.models.set(key, { info, @@ -404,7 +435,10 @@ export namespace Provider { export function sort(models: ModelsDev.Model[]) { return sortBy( models, - [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"], + [ + (model) => priority.findIndex((filter) => model.id.includes(filter)), + "desc", + ], [(model) => (model.id.includes("latest") ? 0 : 1), "asc"], [(model) => model.id, "desc"], ) @@ -415,7 +449,11 @@ export namespace Provider { if (cfg.model) return parseModel(cfg.model) const provider = await list() .then((val) => Object.values(val)) - .then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id))) + .then((x) => + x.find( + (p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id), + ), + ) if (!provider) throw new Error("no providers found") const [model] = sort(Object.values(provider.info.models)) if (!model) throw new Error("no models found") @@ -498,11 +536,9 @@ export namespace Provider { if (schema instanceof z.ZodUnion) { return z.union( - schema.options.map((option: z.ZodTypeAny) => optionalToNullable(option)) as [ - z.ZodTypeAny, - z.ZodTypeAny, - ...z.ZodTypeAny[], - ], + schema.options.map((option: z.ZodTypeAny) => + optionalToNullable(option), + ) as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]], ) } diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 433e9f41..d5e140b3 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1,21 +1,22 @@ -import type { ModelMessage } from "ai" +import type { LanguageModelV1Prompt } from "ai" import { unique } from "remeda" export namespace ProviderTransform { - export function message(msgs: ModelMessage[], providerID: string, modelID: string) { + export function message( + msgs: LanguageModelV1Prompt, + providerID: string, + modelID: string, + ) { if (providerID === "anthropic" || modelID.includes("anthropic")) { const system = msgs.filter((msg) => msg.role === "system").slice(0, 2) const final = msgs.filter((msg) => msg.role !== "system").slice(-2) for (const msg of unique([...system, ...final])) { - msg.providerOptions = { - ...msg.providerOptions, + msg.providerMetadata = { + ...msg.providerMetadata, anthropic: { cacheControl: { type: "ephemeral" }, }, - openaiCompatible: { - cache_control: { type: "ephemeral" }, - }, } } } @@ -24,8 +25,8 @@ export namespace ProviderTransform { const final = msgs.filter((msg) => msg.role !== "system").slice(-2) for (const msg of unique([...system, ...final])) { - msg.providerOptions = { - ...msg.providerOptions, + msg.providerMetadata = { + ...msg.providerMetadata, bedrock: { cachePoint: { type: "ephemeral" }, }, diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 39f8c261..df645cd8 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -6,6 +6,7 @@ import { streamSSE } from "hono/streaming" import { Session } from "../session" import { resolver, validator as zValidator } from "hono-openapi/zod" import { z } from "zod" +import { Message } from "../session/message" import { Provider } from "../provider/provider" import { App } from "../app/app" import { mapValues } from "remeda" @@ -15,7 +16,6 @@ import { Ripgrep } from "../file/ripgrep" import { Config } from "../config/config" import { File } from "../file" import { LSP } from "../lsp" -import { MessageV2 } from "../session/message-v2" const ERRORS = { 400: { @@ -51,9 +51,12 @@ export namespace Server { status: 400, }) } - return c.json(new NamedError.Unknown({ message: err.toString() }).toObject(), { - status: 400, - }) + return c.json( + new NamedError.Unknown({ message: err.toString() }).toObject(), + { + status: 400, + }, + ) }) .use(async (c, next) => { log.info("request", { @@ -404,7 +407,7 @@ export namespace Server { description: "List of messages", content: { "application/json": { - schema: resolver(MessageV2.Info.array()), + schema: resolver(Message.Info.array()), }, }, }, @@ -430,7 +433,7 @@ export namespace Server { description: "Created message", content: { "application/json": { - schema: resolver(MessageV2.Assistant), + schema: resolver(Message.Info), }, }, }, @@ -447,7 +450,7 @@ export namespace Server { z.object({ providerID: z.string(), modelID: z.string(), - parts: MessageV2.UserPart.array(), + parts: Message.MessagePart.array(), }), ), async (c) => { @@ -478,10 +481,15 @@ export namespace Server { }, }), async (c) => { - const providers = await Provider.list().then((x) => mapValues(x, (item) => item.info)) + const providers = await Provider.list().then((x) => + mapValues(x, (item) => item.info), + ) return c.json({ providers: Object.values(providers), - default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id), + default: mapValues( + providers, + (item) => Provider.sort(Object.values(item.models))[0].id, + ), }) }, ) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 41454307..5a2c1b5e 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -1,40 +1,40 @@ import path from "path" -import { Decimal } from "decimal.js" -import { z, ZodSchema } from "zod" +import { App } from "../app/app" +import { Identifier } from "../id/id" +import { Storage } from "../storage/storage" +import { Log } from "../util/log" import { generateText, LoadAPIKeyError, + convertToCoreMessages, streamText, tool, - wrapLanguageModel, type Tool as AITool, type LanguageModelUsage, + type CoreMessage, + type UIMessage, type ProviderMetadata, - type ModelMessage, - stepCountIs, + wrapLanguageModel, } from "ai" +import { z, ZodSchema } from "zod" +import { Decimal } from "decimal.js" import PROMPT_INITIALIZE from "../session/prompt/initialize.txt" -import { App } from "../app/app" -import { Bus } from "../bus" -import { Config } from "../config/config" -import { Flag } from "../flag/flag" -import { Identifier } from "../id/id" -import { Installation } from "../installation" -import { MCP } from "../mcp" -import { Provider } from "../provider/provider" -import { ProviderTransform } from "../provider/transform" -import type { ModelsDev } from "../provider/models" import { Share } from "../share/share" -import { Snapshot } from "../snapshot" -import { Storage } from "../storage/storage" -import type { Tool } from "../tool/tool" -import { Log } from "../util/log" +import { Message } from "./message" +import { Bus } from "../bus" +import { Provider } from "../provider/provider" +import { MCP } from "../mcp" import { NamedError } from "../util/error" +import type { Tool } from "../tool/tool" import { SystemPrompt } from "./system" -import { FileTime } from "../file/time" -import { MessageV2 } from "./message-v2" +import { Flag } from "../flag/flag" +import type { ModelsDev } from "../provider/models" +import { Installation } from "../installation" +import { Config } from "../config/config" +import { ProviderTransform } from "../provider/transform" +import { Snapshot } from "../snapshot" export namespace Session { const log = Log.create({ service: "session" }) @@ -99,8 +99,7 @@ export namespace Session { Error: Bus.event( "session.error", z.object({ - sessionID: z.string().optional(), - error: MessageV2.Assistant.shape.error, + error: Message.Info.shape.metadata.shape.error, }), ), } @@ -109,7 +108,7 @@ export namespace Session { "session", () => { const sessions = new Map() - const messages = new Map() + const messages = new Map() const pending = new Map() return { @@ -130,7 +129,9 @@ export namespace Session { id: Identifier.descending("session"), version: Installation.VERSION, parentID, - title: (parentID ? "Child session - " : "New Session - ") + new Date().toISOString(), + title: + (parentID ? "Child session - " : "New Session - ") + + new Date().toISOString(), time: { created: Date.now(), updated: Date.now(), @@ -186,6 +187,7 @@ export namespace Session { export async function unshare(id: string) { const share = await getShare(id) if (!share) return + console.log("share", share) await Storage.remove("session/share/" + id) await update(id, (draft) => { draft.share = undefined @@ -208,10 +210,10 @@ export namespace Session { } export async function messages(sessionID: string) { - const result = [] as MessageV2.Info[] + const result = [] as Message.Info[] const list = Storage.list("session/message/" + sessionID) for await (const p of list) { - const read = await Storage.readJSON(p) + const read = await Storage.readJSON(p) result.push(read) } result.sort((a, b) => (a.id > b.id ? 1 : -1)) @@ -219,7 +221,9 @@ export namespace Session { } export async function getMessage(sessionID: string, messageID: string) { - return Storage.readJSON("session/message/" + sessionID + "/" + messageID) + return Storage.readJSON( + "session/message/" + sessionID + "/" + messageID, + ) } export async function* list() { @@ -270,9 +274,12 @@ export namespace Session { } } - async function updateMessage(msg: MessageV2.Info) { - await Storage.writeJSON("session/message/" + msg.sessionID + "/" + msg.id, msg) - Bus.publish(MessageV2.Event.Updated, { + async function updateMessage(msg: Message.Info) { + await Storage.writeJSON( + "session/message/" + msg.metadata.sessionID + "/" + msg.id, + msg, + ) + Bus.publish(Message.Event.Updated, { info: msg, }) } @@ -281,11 +288,10 @@ export namespace Session { sessionID: string providerID: string modelID: string - parts: MessageV2.UserPart[] + parts: Message.MessagePart[] system?: string[] tools?: Tool.Info[] }) { - using abort = lock(input.sessionID) const l = log.clone().tag("session", input.sessionID) l.info("chatting") const model = await Provider.getModel(input.providerID, input.modelID) @@ -295,9 +301,14 @@ export namespace Session { if (session.revert) { const trimmed = [] for (const msg of msgs) { - if (msg.id > session.revert.messageID || (msg.id === session.revert.messageID && session.revert.part === 0)) { - await Storage.remove("session/message/" + input.sessionID + "/" + msg.id) - await Bus.publish(MessageV2.Event.Removed, { + if ( + msg.id > session.revert.messageID || + (msg.id === session.revert.messageID && session.revert.part === 0) + ) { + await Storage.remove( + "session/message/" + input.sessionID + "/" + msg.id, + ) + await Bus.publish(Message.Event.Removed, { sessionID: input.sessionID, messageID: msg.id, }) @@ -316,15 +327,22 @@ export namespace Session { }) } - const previous = msgs.at(-1) as MessageV2.Assistant + const previous = msgs.at(-1) // auto summarize if too long - if (previous) { + if (previous?.metadata.assistant) { const tokens = - previous.tokens.input + previous.tokens.cache.read + previous.tokens.cache.write + previous.tokens.output + previous.metadata.assistant.tokens.input + + previous.metadata.assistant.tokens.cache.read + + previous.metadata.assistant.tokens.cache.write + + previous.metadata.assistant.tokens.output if ( model.info.limit.context && - tokens > Math.max((model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9, 0) + tokens > + Math.max( + (model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9, + 0, + ) ) { await summarize({ sessionID: input.sessionID, @@ -335,77 +353,30 @@ export namespace Session { } } - const lastSummary = msgs.findLast((msg) => msg.role === "assistant" && msg.summary === true) + using abort = lock(input.sessionID) + + const lastSummary = msgs.findLast( + (msg) => msg.metadata.assistant?.summary === true, + ) if (lastSummary) msgs = msgs.filter((msg) => msg.id >= lastSummary.id) const app = App.info() - input.parts = await Promise.all( - input.parts.map(async (part): Promise => { - if (part.type === "file") { - const url = new URL(part.url) - switch (url.protocol) { - case "file:": - const filepath = path.join(app.path.cwd, url.pathname) - let file = Bun.file(filepath) - - if (part.mime === "text/plain") { - let text = await file.text() - const range = { - start: url.searchParams.get("start"), - end: url.searchParams.get("end"), - } - if (range.start != null && part.mime === "text/plain") { - const lines = text.split("\n") - const start = parseInt(range.start) - const end = range.end ? parseInt(range.end) : lines.length - text = lines.slice(start, end).join("\n") - } - FileTime.read(input.sessionID, filepath) - return [ - { - type: "text", - text: ["Called the Read tool on " + url.pathname, "", text, ""].join("\n"), - }, - ] - } - - return [ - { - type: "text", - text: ["Called the Read tool on " + url.pathname].join("\n"), - }, - { - type: "file", - url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64url"), - mime: part.mime, - filename: part.filename!, - }, - ] - } - } - return [part] - }), - ).then((x) => x.flat()) if (msgs.length === 0 && !session.parentID) { generateText({ - maxOutputTokens: input.providerID === "google" ? 1024 : 20, + maxTokens: input.providerID === "google" ? 1024 : 20, providerOptions: model.info.options, messages: [ ...SystemPrompt.title(input.providerID).map( - (x): ModelMessage => ({ + (x): CoreMessage => ({ role: "system", content: x, }), ), - ...MessageV2.toModelMessage([ + ...convertToCoreMessages([ { - id: Identifier.ascending("message"), role: "user", - sessionID: input.sessionID, - parts: input.parts, - time: { - created: Date.now(), - }, + content: "", + parts: toParts(input.parts), }, ]), ], @@ -419,13 +390,18 @@ export namespace Session { }) .catch(() => {}) } - const msg: MessageV2.Info = { - id: Identifier.ascending("message"), + const snapshot = await Snapshot.create(input.sessionID) + const msg: Message.Info = { role: "user", - sessionID: input.sessionID, + id: Identifier.ascending("message"), parts: input.parts, - time: { - created: Date.now(), + metadata: { + time: { + created: Date.now(), + }, + sessionID: input.sessionID, + tool: {}, + snapshot, }, } await updateMessage(msg) @@ -435,28 +411,34 @@ export namespace Session { system.push(...(await SystemPrompt.environment())) system.push(...(await SystemPrompt.custom())) - const next: MessageV2.Info = { + const next: Message.Info = { id: Identifier.ascending("message"), role: "assistant", parts: [], - system, - path: { - cwd: app.path.cwd, - root: app.path.root, + metadata: { + snapshot, + assistant: { + system, + path: { + cwd: app.path.cwd, + root: app.path.root, + }, + cost: 0, + tokens: { + input: 0, + output: 0, + reasoning: 0, + cache: { read: 0, write: 0 }, + }, + modelID: input.modelID, + providerID: input.providerID, + }, + time: { + created: Date.now(), + }, + sessionID: input.sessionID, + tool: {}, }, - cost: 0, - tokens: { - input: 0, - output: 0, - reasoning: 0, - cache: { read: 0, write: 0 }, - }, - modelID: input.modelID, - providerID: input.providerID, - time: { - created: Date.now(), - }, - sessionID: input.sessionID, } await updateMessage(next) const tools: Record = {} @@ -465,29 +447,48 @@ export namespace Session { tools[item.id.replaceAll(".", "_")] = tool({ id: item.id as any, description: item.description, - inputSchema: item.parameters as ZodSchema, + parameters: item.parameters as ZodSchema, async execute(args, opts) { - const result = await item.execute(args, { - sessionID: input.sessionID, - abort: abort.signal, - messageID: next.id, - metadata: async (val) => { - const match = next.parts.find( - (p): p is MessageV2.ToolPart => p.type === "tool" && p.id === opts.toolCallId, - ) - if (match && match.state.status === "running") { - match.state.title = val.title - match.state.metadata = val.metadata - } - await updateMessage(next) - }, - }) - return result - }, - toModelOutput(result) { - return { - type: "text", - value: result.output, + const start = Date.now() + try { + const result = await item.execute(args, { + sessionID: input.sessionID, + abort: abort.signal, + messageID: next.id, + metadata: async (val) => { + next.metadata.tool[opts.toolCallId] = { + ...val, + time: { + start: 0, + end: 0, + }, + } + await updateMessage(next) + }, + }) + next.metadata!.tool![opts.toolCallId] = { + ...result.metadata, + snapshot: await Snapshot.create(input.sessionID), + time: { + start, + end: Date.now(), + }, + } + await updateMessage(next) + return result.output + } catch (e: any) { + next.metadata!.tool![opts.toolCallId] = { + error: true, + message: e.toString(), + title: e.toString(), + snapshot: await Snapshot.create(input.sessionID), + time: { + start, + end: Date.now(), + }, + } + await updateMessage(next) + return e.toString() } }, }) @@ -497,44 +498,109 @@ export namespace Session { const execute = item.execute if (!execute) continue item.execute = async (args, opts) => { - const result = await execute(args, opts) - const output = result.content - .filter((x: any) => x.type === "text") - .map((x: any) => x.text) - .join("\n\n") - - return { - output, - } - } - item.toModelOutput = (result) => { - return { - type: "text", - value: result.output, + const start = Date.now() + try { + const result = await execute(args, opts) + next.metadata!.tool![opts.toolCallId] = { + ...result.metadata, + snapshot: await Snapshot.create(input.sessionID), + time: { + start, + end: Date.now(), + }, + } + await updateMessage(next) + return result.content + .filter((x: any) => x.type === "text") + .map((x: any) => x.text) + .join("\n\n") + } catch (e: any) { + next.metadata!.tool![opts.toolCallId] = { + error: true, + message: e.toString(), + snapshot: await Snapshot.create(input.sessionID), + title: "mcp", + time: { + start, + end: Date.now(), + }, + } + await updateMessage(next) + return e.toString() } } tools[key] = item } - let text: MessageV2.TextPart = { - type: "text", - text: "", - } + let text: Message.TextPart | undefined const result = streamText({ - onError() {}, + onStepFinish: async (step) => { + log.info("step finish", { finishReason: step.finishReason }) + const assistant = next.metadata!.assistant! + const usage = getUsage(model.info, step.usage, step.providerMetadata) + assistant.cost += usage.cost + assistant.tokens = usage.tokens + await updateMessage(next) + if (text) { + Bus.publish(Message.Event.PartUpdated, { + part: text, + messageID: next.id, + sessionID: next.metadata.sessionID, + }) + } + text = undefined + }, + onError(err) { + log.error("callback error", err) + switch (true) { + case LoadAPIKeyError.isInstance(err.error): + next.metadata.error = new Provider.AuthError( + { + providerID: input.providerID, + message: err.error.message, + }, + { cause: err.error }, + ).toObject() + break + case err.error instanceof Error: + next.metadata.error = new NamedError.Unknown( + { message: err.error.toString() }, + { cause: err.error }, + ).toObject() + break + default: + next.metadata.error = new NamedError.Unknown( + { message: JSON.stringify(err.error) }, + { cause: err.error }, + ) + } + Bus.publish(Event.Error, { + error: next.metadata.error, + }) + }, + // async prepareStep(step) { + // next.parts.push({ + // type: "step-start", + // }) + // await updateMessage(next) + // return step + // }, + toolCallStreaming: true, maxRetries: 10, - maxOutputTokens: Math.max(0, model.info.limit.output) || undefined, + maxTokens: Math.max(0, model.info.limit.output) || undefined, abortSignal: abort.signal, - stopWhen: stepCountIs(1000), + maxSteps: 1000, providerOptions: model.info.options, messages: [ ...system.map( - (x): ModelMessage => ({ + (x): CoreMessage => ({ role: "system", content: x, }), ), - ...MessageV2.toModelMessage(msgs), + ...convertToCoreMessages( + msgs.map(toUIMessage).filter((x) => x.parts.length > 0), + ), ], temperature: model.info.temperature ? 0 : undefined, tools: model.info.tool_call === false ? undefined : tools, @@ -544,8 +610,11 @@ export namespace Session { { async transformParams(args) { if (args.type === "stream") { - // @ts-expect-error - args.params.prompt = ProviderTransform.message(args.params.prompt, input.providerID, input.modelID) + args.params.prompt = ProviderTransform.message( + args.params.prompt, + input.providerID, + input.modelID, + ) } return args.params }, @@ -559,161 +628,122 @@ export namespace Session { type: value.type, }) switch (value.type) { - case "start": - break - - case "tool-input-start": - next.parts.push({ - type: "tool", - tool: value.toolName, - id: value.id, - state: { - status: "pending", - }, - }) - Bus.publish(MessageV2.Event.PartUpdated, { - part: next.parts[next.parts.length - 1], - sessionID: next.sessionID, - messageID: next.id, - }) - break - - case "tool-input-delta": - break - - case "tool-call": { - const match = next.parts.find( - (p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId, - ) - if (match) { - match.state = { - status: "running", - input: value.input, - time: { - start: Date.now(), - }, - } - Bus.publish(MessageV2.Event.PartUpdated, { - part: match, - sessionID: next.sessionID, - messageID: next.id, - }) - } - break - } - case "tool-result": { - const match = next.parts.find( - (p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId, - ) - if (match && match.state.status === "running") { - match.state = { - status: "completed", - input: value.input, - output: value.output.output, - metadata: value.output.metadata, - title: value.output.title, - time: { - start: match.state.time.start, - end: Date.now(), - }, - } - Bus.publish(MessageV2.Event.PartUpdated, { - part: match, - sessionID: next.sessionID, - messageID: next.id, - }) - } - break - } - - case "tool-error": { - const match = next.parts.find( - (p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId, - ) - if (match && match.state.status === "running") { - match.state = { - status: "error", - input: value.input, - error: (value.error as any).toString(), - time: { - start: match.state.time.start, - end: Date.now(), - }, - } - Bus.publish(MessageV2.Event.PartUpdated, { - part: match, - sessionID: next.sessionID, - messageID: next.id, - }) - } - break - } - - case "error": - throw value.error - - case "start-step": + case "step-start": next.parts.push({ type: "step-start", }) break - - case "finish-step": - const usage = getUsage(model.info, value.usage, value.providerMetadata) - next.cost += usage.cost - next.tokens = usage.tokens + case "text-delta": + if (!text) { + text = { + type: "text", + text: value.textDelta, + } + next.parts.push(text) + break + } else text.text += value.textDelta break - case "text-start": - text = { - type: "text", - text: "", - } - break - - case "text": - if (text.text === "") next.parts.push(text) - text.text += value.text - break - - case "text-end": - Bus.publish(MessageV2.Event.PartUpdated, { - part: text, - sessionID: next.sessionID, + case "tool-call": { + const [match] = next.parts.flatMap((p) => + p.type === "tool-invocation" && + p.toolInvocation.toolCallId === value.toolCallId + ? [p] + : [], + ) + if (!match) break + match.toolInvocation.args = value.args + match.toolInvocation.state = "call" + Bus.publish(Message.Event.PartUpdated, { + part: match, messageID: next.id, + sessionID: next.metadata.sessionID, + }) + break + } + + case "tool-call-streaming-start": + next.parts.push({ + type: "tool-invocation", + toolInvocation: { + state: "partial-call", + toolName: value.toolName, + toolCallId: value.toolCallId, + args: {}, + }, + }) + Bus.publish(Message.Event.PartUpdated, { + part: next.parts[next.parts.length - 1], + messageID: next.id, + sessionID: next.metadata.sessionID, }) break - case "finish": - next.time.completed = Date.now() + case "tool-call-delta": + continue + + // for some reason ai sdk claims to not send this part but it does + // @ts-expect-error + case "tool-result": + const match = next.parts.find( + (p) => + p.type === "tool-invocation" && + // @ts-expect-error + p.toolInvocation.toolCallId === value.toolCallId, + ) + if (match && match.type === "tool-invocation") { + match.toolInvocation = { + // @ts-expect-error + args: value.args, + // @ts-expect-error + toolCallId: value.toolCallId, + // @ts-expect-error + toolName: value.toolName, + state: "result", + // @ts-expect-error + result: value.result as string, + } + Bus.publish(Message.Event.PartUpdated, { + part: match, + messageID: next.id, + sessionID: next.metadata.sessionID, + }) + } break + case "finish": + log.info("message finish", { + reason: value.finishReason, + }) + const assistant = next.metadata!.assistant! + const usage = getUsage( + model.info, + value.usage, + value.providerMetadata, + ) + assistant.cost += usage.cost + await updateMessage(next) + if (value.finishReason === "length") + throw new Message.OutputLengthError({}) + break default: l.info("unhandled", { - ...value, + type: value.type, }) continue } await updateMessage(next) } - } catch (e) { - log.error("", { + } catch (e: any) { + log.error("stream error", { error: e, }) switch (true) { - case e instanceof DOMException && e.name === "AbortError": - next.error = new MessageV2.AbortedError( - { message: e.message }, - { - cause: e, - }, - ).toObject() - break - case MessageV2.OutputLengthError.isInstance(e): - next.error = e + case Message.OutputLengthError.isInstance(e): + next.metadata.error = e break case LoadAPIKeyError.isInstance(e): - next.error = new Provider.AuthError( + next.metadata.error = new Provider.AuthError( { providerID: input.providerID, message: e.message, @@ -722,37 +752,43 @@ export namespace Session { ).toObject() break case e instanceof Error: - next.error = new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject() + next.metadata.error = new NamedError.Unknown( + { message: e.toString() }, + { cause: e }, + ).toObject() break default: - next.error = new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }) + next.metadata.error = new NamedError.Unknown( + { message: JSON.stringify(e) }, + { cause: e }, + ) } Bus.publish(Event.Error, { - sessionID: next.sessionID, - error: next.error, + error: next.metadata.error, }) } + next.metadata!.time.completed = Date.now() for (const part of next.parts) { - if (part.type === "tool" && part.state.status !== "completed") { - part.state = { - status: "error", - error: "Tool execution aborted", - time: { - start: Date.now(), - end: Date.now(), - }, - input: {}, + if ( + part.type === "tool-invocation" && + part.toolInvocation.state !== "result" + ) { + part.toolInvocation = { + ...part.toolInvocation, + state: "result", + result: "request was aborted", } } } - next.time.completed = Date.now() await updateMessage(next) return next } - export async function revert(_input: { sessionID: string; messageID: string; part: number }) { - // TODO - /* + export async function revert(input: { + sessionID: string + messageID: string + part: number + }) { const message = await getMessage(input.sessionID, input.messageID) if (!message) return const part = message.parts[input.part] @@ -780,66 +816,77 @@ export namespace Session { snapshot, } }) - */ } export async function unrevert(sessionID: string) { const session = await get(sessionID) if (!session) return if (!session.revert) return - if (session.revert.snapshot) await Snapshot.restore(sessionID, session.revert.snapshot) + if (session.revert.snapshot) + await Snapshot.restore(sessionID, session.revert.snapshot) update(sessionID, (draft) => { draft.revert = undefined }) } - export async function summarize(input: { sessionID: string; providerID: string; modelID: string }) { + export async function summarize(input: { + sessionID: string + providerID: string + modelID: string + }) { using abort = lock(input.sessionID) const msgs = await messages(input.sessionID) - const lastSummary = msgs.findLast((msg) => msg.role === "assistant" && msg.summary === true)?.id + const lastSummary = msgs.findLast( + (msg) => msg.metadata.assistant?.summary === true, + )?.id const filtered = msgs.filter((msg) => !lastSummary || msg.id >= lastSummary) const model = await Provider.getModel(input.providerID, input.modelID) const app = App.info() const system = SystemPrompt.summarize(input.providerID) - const next: MessageV2.Info = { + const next: Message.Info = { id: Identifier.ascending("message"), role: "assistant", parts: [], - sessionID: input.sessionID, - system, - path: { - cwd: app.path.cwd, - root: app.path.root, - }, - summary: true, - cost: 0, - modelID: input.modelID, - providerID: input.providerID, - tokens: { - input: 0, - output: 0, - reasoning: 0, - cache: { read: 0, write: 0 }, - }, - time: { - created: Date.now(), + metadata: { + tool: {}, + sessionID: input.sessionID, + assistant: { + system, + path: { + cwd: app.path.cwd, + root: app.path.root, + }, + summary: true, + cost: 0, + modelID: input.modelID, + providerID: input.providerID, + tokens: { + input: 0, + output: 0, + reasoning: 0, + cache: { read: 0, write: 0 }, + }, + }, + time: { + created: Date.now(), + }, }, } await updateMessage(next) - let text: MessageV2.TextPart | undefined + let text: Message.TextPart | undefined const result = streamText({ abortSignal: abort.signal, model: model.language, messages: [ ...system.map( - (x): ModelMessage => ({ + (x): CoreMessage => ({ role: "system", content: x, }), ), - ...MessageV2.toModelMessage(filtered), + ...convertToCoreMessages(filtered.map(toUIMessage)), { role: "user", content: [ @@ -851,38 +898,41 @@ export namespace Session { }, ], onStepFinish: async (step) => { + const assistant = next.metadata!.assistant! const usage = getUsage(model.info, step.usage, step.providerMetadata) - next.cost += usage.cost - next.tokens = usage.tokens + assistant.cost += usage.cost + assistant.tokens = usage.tokens await updateMessage(next) if (text) { - Bus.publish(MessageV2.Event.PartUpdated, { + Bus.publish(Message.Event.PartUpdated, { part: text, messageID: next.id, - sessionID: next.sessionID, + sessionID: next.metadata.sessionID, }) } text = undefined }, async onFinish(input) { + const assistant = next.metadata!.assistant! const usage = getUsage(model.info, input.usage, input.providerMetadata) - next.cost += usage.cost - next.tokens = usage.tokens - next.time.completed = Date.now() + assistant.cost += usage.cost + assistant.tokens = usage.tokens + next.metadata!.time.completed = Date.now() await updateMessage(next) }, }) for await (const value of result.fullStream) { switch (value.type) { - case "text": + case "text-delta": if (!text) { text = { type: "text", - text: value.text, + text: value.textDelta, } next.parts.push(text) - } else text.text += value.text + } else text.text += value.textDelta + await updateMessage(next) break } @@ -906,25 +956,40 @@ export namespace Session { } } - function getUsage(model: ModelsDev.Model, usage: LanguageModelUsage, metadata?: ProviderMetadata) { + function getUsage( + model: ModelsDev.Model, + usage: LanguageModelUsage, + metadata?: ProviderMetadata, + ) { const tokens = { - input: usage.inputTokens ?? 0, - output: usage.outputTokens ?? 0, + input: usage.promptTokens ?? 0, + output: usage.completionTokens ?? 0, reasoning: 0, cache: { write: (metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? // @ts-expect-error metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ?? 0) as number, - read: usage.cachedInputTokens ?? 0, + read: (metadata?.["anthropic"]?.["cacheReadInputTokens"] ?? + // @ts-expect-error + metadata?.["bedrock"]?.["usage"]?.["cacheReadInputTokens"] ?? + 0) as number, }, } return { cost: new Decimal(0) .add(new Decimal(tokens.input).mul(model.cost.input).div(1_000_000)) .add(new Decimal(tokens.output).mul(model.cost.output).div(1_000_000)) - .add(new Decimal(tokens.cache.read).mul(model.cost.cache_read ?? 0).div(1_000_000)) - .add(new Decimal(tokens.cache.write).mul(model.cost.cache_write ?? 0).div(1_000_000)) + .add( + new Decimal(tokens.cache.read) + .mul(model.cost.cache_read ?? 0) + .div(1_000_000), + ) + .add( + new Decimal(tokens.cache.write) + .mul(model.cost.cache_write ?? 0) + .div(1_000_000), + ) .toNumber(), tokens, } @@ -936,7 +1001,11 @@ export namespace Session { } } - export async function initialize(input: { sessionID: string; modelID: string; providerID: string }) { + export async function initialize(input: { + sessionID: string + modelID: string + providerID: string + }) { const app = App.info() await Session.chat({ sessionID: input.sessionID, @@ -952,3 +1021,57 @@ export namespace Session { await App.initialize() } } + +function toUIMessage(msg: Message.Info): UIMessage { + if (msg.role === "assistant") { + return { + id: msg.id, + role: "assistant", + content: "", + parts: toParts(msg.parts), + } + } + + if (msg.role === "user") { + return { + id: msg.id, + role: "user", + content: "", + parts: toParts(msg.parts), + } + } + + throw new Error("not implemented") +} + +function toParts(parts: Message.MessagePart[]): UIMessage["parts"] { + const result: UIMessage["parts"] = [] + for (const part of parts) { + switch (part.type) { + case "text": + result.push({ type: "text", text: part.text }) + break + case "file": + result.push({ + type: "file", + data: part.url, + mimeType: part.mediaType, + }) + break + case "tool-invocation": + result.push({ + type: "tool-invocation", + toolInvocation: part.toolInvocation, + }) + break + case "step-start": + result.push({ + type: "step-start", + }) + break + default: + break + } + } + return result +} diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts deleted file mode 100644 index 8b09e68e..00000000 --- a/packages/opencode/src/session/message-v2.ts +++ /dev/null @@ -1,404 +0,0 @@ -import z from "zod" -import { Bus } from "../bus" -import { Provider } from "../provider/provider" -import { NamedError } from "../util/error" -import { Message } from "./message" -import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai" - -export namespace MessageV2 { - export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({})) - export const AbortedError = NamedError.create("MessageAbortedError", z.object({})) - - export const ToolStatePending = z - .object({ - status: z.literal("pending"), - }) - .openapi({ - ref: "ToolStatePending", - }) - - export type ToolStatePending = z.infer - - export const ToolStateRunning = z - .object({ - status: z.literal("running"), - input: z.any(), - title: z.string().optional(), - metadata: z.record(z.any()).optional(), - time: z.object({ - start: z.number(), - }), - }) - .openapi({ - ref: "ToolStateRunning", - }) - export type ToolStateRunning = z.infer - - export const ToolStateCompleted = z - .object({ - status: z.literal("completed"), - input: z.record(z.any()), - output: z.string(), - title: z.string(), - metadata: z.record(z.any()), - time: z.object({ - start: z.number(), - end: z.number(), - }), - }) - .openapi({ - ref: "ToolStateCompleted", - }) - export type ToolStateCompleted = z.infer - - export const ToolStateError = z - .object({ - status: z.literal("error"), - input: z.record(z.any()), - error: z.string(), - time: z.object({ - start: z.number(), - end: z.number(), - }), - }) - .openapi({ - ref: "ToolStateError", - }) - export type ToolStateError = z.infer - - export const ToolState = z - .discriminatedUnion("status", [ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]) - .openapi({ - ref: "ToolState", - }) - - export const TextPart = z - .object({ - type: z.literal("text"), - text: z.string(), - }) - .openapi({ - ref: "TextPart", - }) - export type TextPart = z.infer - - export const ToolPart = z - .object({ - type: z.literal("tool"), - id: z.string(), - tool: z.string(), - state: ToolState, - }) - .openapi({ - ref: "ToolPart", - }) - export type ToolPart = z.infer - - export const FilePart = z - .object({ - type: z.literal("file"), - mime: z.string(), - filename: z.string().optional(), - url: z.string(), - }) - .openapi({ - ref: "FilePart", - }) - export type FilePart = z.infer - - export const StepStartPart = z - .object({ - type: z.literal("step-start"), - }) - .openapi({ - ref: "StepStartPart", - }) - export type StepStartPart = z.infer - - const Base = z.object({ - id: z.string(), - sessionID: z.string(), - }) - - export const UserPart = z.discriminatedUnion("type", [TextPart, FilePart]).openapi({ - ref: "UserMessagePart", - }) - export type UserPart = z.infer - - export const User = Base.extend({ - role: z.literal("user"), - parts: z.array(UserPart), - time: z.object({ - created: z.number(), - }), - }).openapi({ - ref: "UserMessage", - }) - export type User = z.infer - - export const AssistantPart = z.discriminatedUnion("type", [TextPart, ToolPart, StepStartPart]).openapi({ - ref: "AssistantMessagePart", - }) - export type AssistantPart = z.infer - - export const Assistant = Base.extend({ - role: z.literal("assistant"), - parts: z.array(AssistantPart), - time: z.object({ - created: z.number(), - completed: z.number().optional(), - }), - error: z - .discriminatedUnion("name", [ - Provider.AuthError.Schema, - NamedError.Unknown.Schema, - OutputLengthError.Schema, - AbortedError.Schema, - ]) - .optional(), - system: z.string().array(), - modelID: z.string(), - providerID: z.string(), - path: z.object({ - cwd: z.string(), - root: z.string(), - }), - cost: z.number(), - summary: z.boolean().optional(), - tokens: z.object({ - input: z.number(), - output: z.number(), - reasoning: z.number(), - cache: z.object({ - read: z.number(), - write: z.number(), - }), - }), - }).openapi({ - ref: "AssistantMessage", - }) - export type Assistant = z.infer - - export const Info = z.discriminatedUnion("role", [User, Assistant]).openapi({ - ref: "Message", - }) - export type Info = z.infer - - export const Event = { - Updated: Bus.event( - "message.updated", - z.object({ - info: Info, - }), - ), - Removed: Bus.event( - "message.removed", - z.object({ - sessionID: z.string(), - messageID: z.string(), - }), - ), - PartUpdated: Bus.event( - "message.part.updated", - z.object({ - part: AssistantPart, - sessionID: z.string(), - messageID: z.string(), - }), - ), - } - - export function fromV1(v1: Message.Info) { - if (v1.role === "assistant") { - const result: Assistant = { - id: v1.id, - sessionID: v1.metadata.sessionID, - role: "assistant", - time: { - created: v1.metadata.time.created, - completed: v1.metadata.time.completed, - }, - cost: v1.metadata.assistant!.cost, - path: v1.metadata.assistant!.path, - summary: v1.metadata.assistant!.summary, - tokens: v1.metadata.assistant!.tokens, - modelID: v1.metadata.assistant!.modelID, - providerID: v1.metadata.assistant!.providerID, - system: v1.metadata.assistant!.system, - error: v1.metadata.error, - parts: v1.parts.flatMap((part): AssistantPart[] => { - if (part.type === "text") { - return [ - { - type: "text", - text: part.text, - }, - ] - } - if (part.type === "step-start") { - return [ - { - type: "step-start", - }, - ] - } - if (part.type === "tool-invocation") { - return [ - { - type: "tool", - id: part.toolInvocation.toolCallId, - tool: part.toolInvocation.toolName, - state: (() => { - if (part.toolInvocation.state === "partial-call") { - return { - status: "pending", - } - } - - const { title, time, ...metadata } = v1.metadata.tool[part.toolInvocation.toolCallId] ?? {} - if (part.toolInvocation.state === "call") { - return { - status: "running", - input: part.toolInvocation.args, - time: { - start: time?.start, - }, - } - } - - if (part.toolInvocation.state === "result") { - return { - status: "completed", - input: part.toolInvocation.args, - output: part.toolInvocation.result, - title, - time, - metadata, - } - } - throw new Error("unknown tool invocation state") - })(), - }, - ] - } - return [] - }), - } - return result - } - - if (v1.role === "user") { - const result: User = { - id: v1.id, - sessionID: v1.metadata.sessionID, - role: "user", - time: { - created: v1.metadata.time.created, - }, - parts: v1.parts.flatMap((part): UserPart[] => { - if (part.type === "text") { - return [ - { - type: "text", - text: part.text, - }, - ] - } - if (part.type === "file") { - return [ - { - type: "file", - mime: part.mediaType, - filename: part.filename, - url: part.url, - }, - ] - } - return [] - }), - } - return result - } - } - - export function toModelMessage(input: Info[]): ModelMessage[] { - const result: UIMessage[] = [] - - for (const msg of input) { - if (msg.parts.length === 0) continue - if (msg.role === "user") { - result.push({ - id: msg.id, - role: "user", - parts: msg.parts.flatMap((part): UIMessage["parts"] => { - if (part.type === "text") - return [ - { - type: "text", - text: part.text, - }, - ] - if (part.type === "file") - return [ - { - type: "file", - url: part.url, - mediaType: part.mime, - filename: part.filename, - }, - ] - return [] - }), - }) - } - - if (msg.role === "assistant") { - result.push({ - id: msg.id, - role: "assistant", - parts: msg.parts.flatMap((part): UIMessage["parts"] => { - if (part.type === "text") - return [ - { - type: "text", - text: part.text, - }, - ] - if (part.type === "step-start") - return [ - { - type: "step-start", - }, - ] - if (part.type === "tool") { - if (part.state.status === "completed") - return [ - { - type: ("tool-" + part.tool) as `tool-${string}`, - state: "output-available", - toolCallId: part.id, - input: part.state.input, - output: part.state.output, - }, - ] - if (part.state.status === "error") - return [ - { - type: ("tool-" + part.tool) as `tool-${string}`, - state: "output-error", - toolCallId: part.id, - input: part.state.input, - errorText: part.state.error, - }, - ] - } - - return [] - }), - }) - } - } - - return convertToModelMessages(result) - } -} diff --git a/packages/opencode/src/session/message.ts b/packages/opencode/src/session/message.ts index 34b522fc..2d319e87 100644 --- a/packages/opencode/src/session/message.ts +++ b/packages/opencode/src/session/message.ts @@ -1,9 +1,13 @@ import z from "zod" +import { Bus } from "../bus" import { Provider } from "../provider/provider" import { NamedError } from "../util/error" export namespace Message { - export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({})) + export const OutputLengthError = NamedError.create( + "MessageOutputLengthError", + z.object({}), + ) export const ToolCall = z .object({ @@ -45,9 +49,11 @@ export namespace Message { }) export type ToolResult = z.infer - export const ToolInvocation = z.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult]).openapi({ - ref: "ToolInvocation", - }) + export const ToolInvocation = z + .discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult]) + .openapi({ + ref: "ToolInvocation", + }) export type ToolInvocation = z.infer export const TextPart = z @@ -116,7 +122,14 @@ export namespace Message { export type StepStartPart = z.infer export const MessagePart = z - .discriminatedUnion("type", [TextPart, ReasoningPart, ToolInvocationPart, SourceUrlPart, FilePart, StepStartPart]) + .discriminatedUnion("type", [ + TextPart, + ReasoningPart, + ToolInvocationPart, + SourceUrlPart, + FilePart, + StepStartPart, + ]) .openapi({ ref: "MessagePart", }) @@ -184,4 +197,28 @@ export namespace Message { ref: "Message", }) export type Info = z.infer + + export const Event = { + Updated: Bus.event( + "message.updated", + z.object({ + info: Info, + }), + ), + Removed: Bus.event( + "message.removed", + z.object({ + sessionID: z.string(), + messageID: z.string(), + }), + ), + PartUpdated: Bus.event( + "message.part.updated", + z.object({ + part: MessagePart, + sessionID: z.string(), + messageID: z.string(), + }), + ), + } } diff --git a/packages/opencode/src/share/share.ts b/packages/opencode/src/share/share.ts index 2996e4d9..a5080969 100644 --- a/packages/opencode/src/share/share.ts +++ b/packages/opencode/src/share/share.ts @@ -53,7 +53,9 @@ export namespace Share { export const URL = process.env["OPENCODE_API"] ?? - (Installation.isSnapshot() || Installation.isDev() ? "https://api.dev.opencode.ai" : "https://api.opencode.ai") + (Installation.isSnapshot() || Installation.isDev() + ? "https://api.dev.opencode.ai" + : "https://api.opencode.ai") export async function create(sessionID: string) { return fetch(`${URL}/share_create`, { diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 608eb89c..1bbb870f 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -55,7 +55,9 @@ export namespace Snapshot { log.info("restore", { commit }) const app = App.info() const git = gitdir(sessionID) - await $`git --git-dir=${git} checkout ${commit} --force`.quiet().cwd(app.path.root) + await $`git --git-dir=${git} checkout ${commit} --force` + .quiet() + .cwd(app.path.root) } function gitdir(sessionID: string) { diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index 7093fb25..eee33a09 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -4,79 +4,44 @@ import { Bus } from "../bus" import path from "path" import z from "zod" import fs from "fs/promises" -import { MessageV2 } from "../session/message-v2" export namespace Storage { const log = Log.create({ service: "storage" }) export const Event = { - Write: Bus.event("storage.write", z.object({ key: z.string(), content: z.any() })), + Write: Bus.event( + "storage.write", + z.object({ key: z.string(), content: z.any() }), + ), } - type Migration = (dir: string) => Promise - - const MIGRATIONS: Migration[] = [ - async (dir: string) => { - try { - const files = new Bun.Glob("session/message/*/*.json").scanSync({ - cwd: dir, - absolute: true, - }) - for (const file of files) { - const content = await Bun.file(file).json() - if (!content.metadata) continue - log.info("migrating to v2 message", { file }) - try { - const result = MessageV2.fromV1(content) - await Bun.write(file, JSON.stringify(result, null, 2)) - } catch (e) { - await fs.rename(file, file.replace("storage", "broken")) - } - } - } catch {} - }, - ] - - const state = App.state("storage", async () => { + const state = App.state("storage", () => { const app = App.info() const dir = path.join(app.path.data, "storage") - const migration = await Bun.file(path.join(dir, "migration")) - .json() - .then((x) => parseInt(x)) - .catch(() => 0) - for (let index = migration; index < MIGRATIONS.length; index++) { - log.info("running migration", { index }) - const migration = MIGRATIONS[index] - await migration(dir) - await Bun.write(path.join(dir, "migration"), (index + 1).toString()) - } + log.info("init", { path: dir }) return { dir, } }) export async function remove(key: string) { - const dir = await state().then((x) => x.dir) - const target = path.join(dir, key + ".json") + const target = path.join(state().dir, key + ".json") await fs.unlink(target).catch(() => {}) } export async function removeDir(key: string) { - const dir = await state().then((x) => x.dir) - const target = path.join(dir, key) + const target = path.join(state().dir, key) await fs.rm(target, { recursive: true, force: true }).catch(() => {}) } export async function readJSON(key: string) { - const dir = await state().then((x) => x.dir) - return Bun.file(path.join(dir, key + ".json")).json() as Promise + return Bun.file(path.join(state().dir, key + ".json")).json() as Promise } export async function writeJSON(key: string, content: T) { - const dir = await state().then((x) => x.dir) - const target = path.join(dir, key + ".json") + const target = path.join(state().dir, key + ".json") const tmp = target + Date.now() + ".tmp" - await Bun.write(tmp, JSON.stringify(content, null, 2)) + await Bun.write(tmp, JSON.stringify(content)) await fs.rename(tmp, target).catch(() => {}) await fs.unlink(tmp).catch(() => {}) Bus.publish(Event.Write, { key, content }) @@ -84,10 +49,9 @@ export namespace Storage { const glob = new Bun.Glob("**/*") export async function* list(prefix: string) { - const dir = await state().then((x) => x.dir) try { for await (const item of glob.scan({ - cwd: path.join(dir, prefix), + cwd: path.join(state().dir, prefix), onlyFiles: true, })) { const result = path.join(prefix, item.slice(0, -5)) diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 050a5a97..3ef44bd5 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -4,6 +4,25 @@ import DESCRIPTION from "./bash.txt" import { App } from "../app/app" const MAX_OUTPUT_LENGTH = 30000 +const BANNED_COMMANDS = [ + "alias", + "curl", + "curlie", + "wget", + "axel", + "aria2c", + "nc", + "telnet", + "lynx", + "w3m", + "links", + "httpie", + "xh", + "http-prompt", + "chrome", + "firefox", + "safari", +] const DEFAULT_TIMEOUT = 1 * 60 * 1000 const MAX_TIMEOUT = 10 * 60 * 1000 @@ -12,7 +31,12 @@ export const BashTool = Tool.define({ description: DESCRIPTION, parameters: z.object({ command: z.string().describe("The command to execute"), - timeout: z.number().min(0).max(MAX_TIMEOUT).describe("Optional timeout in milliseconds").optional(), + timeout: z + .number() + .min(0) + .max(MAX_TIMEOUT) + .describe("Optional timeout in milliseconds") + .optional(), description: z .string() .describe( @@ -21,6 +45,8 @@ export const BashTool = Tool.define({ }), async execute(params, ctx) { const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT) + if (BANNED_COMMANDS.some((item) => params.command.startsWith(item))) + throw new Error(`Command '${params.command}' is not allowed`) const process = Bun.spawn({ cmd: ["bash", "-c", params.command], @@ -36,14 +62,21 @@ export const BashTool = Tool.define({ const stderr = await new Response(process.stderr).text() return { - title: params.command, metadata: { stderr, stdout, exit: process.exitCode, description: params.description, + title: params.command, }, - output: [``, stdout ?? "", ``, ``, stderr ?? "", ``].join("\n"), + output: [ + ``, + stdout ?? "", + ``, + ``, + stderr ?? "", + ``, + ].join("\n"), } }, }) diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 7e8d733d..8c9043e6 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -20,8 +20,15 @@ export const EditTool = Tool.define({ parameters: z.object({ filePath: z.string().describe("The absolute path to the file to modify"), oldString: z.string().describe("The text to replace"), - newString: z.string().describe("The text to replace it with (must be different from old_string)"), - replaceAll: z.boolean().optional().describe("Replace all occurrences of old_string (default false)"), + newString: z + .string() + .describe( + "The text to replace it with (must be different from old_string)", + ), + replaceAll: z + .boolean() + .optional() + .describe("Replace all occurrences of old_string (default false)"), }), async execute(params, ctx) { if (!params.filePath) { @@ -33,7 +40,9 @@ export const EditTool = Tool.define({ } const app = App.info() - const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath) + const filepath = path.isAbsolute(params.filePath) + ? params.filePath + : path.join(app.path.cwd, params.filePath) await Permission.ask({ id: "edit", @@ -61,11 +70,17 @@ export const EditTool = Tool.define({ const file = Bun.file(filepath) const stats = await file.stat().catch(() => {}) if (!stats) throw new Error(`File ${filepath} not found`) - if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filepath}`) + if (stats.isDirectory()) + throw new Error(`Path is a directory, not a file: ${filepath}`) await FileTime.assert(ctx.sessionID, filepath) contentOld = await file.text() - contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll) + contentNew = replace( + contentOld, + params.oldString, + params.newString, + params.replaceAll, + ) await file.write(contentNew) await Bus.publish(File.Event.Edited, { file: filepath, @@ -73,7 +88,9 @@ export const EditTool = Tool.define({ contentNew = await file.text() })() - const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, contentNew)) + const diff = trimDiff( + createTwoFilesPatch(filepath, filepath, contentOld, contentNew), + ) FileTime.read(ctx.sessionID, filepath) @@ -93,14 +110,17 @@ export const EditTool = Tool.define({ metadata: { diagnostics, diff, + title: `${path.relative(app.path.root, filepath)}`, }, - title: `${path.relative(app.path.root, filepath)}`, output, } }, }) -export type Replacer = (content: string, find: string) => Generator +export type Replacer = ( + content: string, + find: string, +) => Generator export const SimpleReplacer: Replacer = function* (_content, find) { yield find @@ -188,7 +208,10 @@ export const BlockAnchorReplacer: Replacer = function* (content, find) { } } -export const WhitespaceNormalizedReplacer: Replacer = function* (content, find) { +export const WhitespaceNormalizedReplacer: Replacer = function* ( + content, + find, +) { const normalizeWhitespace = (text: string) => text.replace(/\s+/g, " ").trim() const normalizedFind = normalizeWhitespace(find) @@ -206,7 +229,9 @@ export const WhitespaceNormalizedReplacer: Replacer = function* (content, find) // Find the actual substring in the original line that matches const words = find.trim().split(/\s+/) if (words.length > 0) { - const pattern = words.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+") + const pattern = words + .map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) + .join("\\s+") try { const regex = new RegExp(pattern) const match = line.match(regex) @@ -245,7 +270,9 @@ export const IndentationFlexibleReplacer: Replacer = function* (content, find) { }), ) - return lines.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent))).join("\n") + return lines + .map((line) => (line.trim().length === 0 ? line : line.slice(minIndent))) + .join("\n") } const normalizedFind = removeIndentation(find) @@ -396,7 +423,10 @@ export const ContextAwareReplacer: Replacer = function* (content, find) { } } - if (totalNonEmptyLines === 0 || matchingLines / totalNonEmptyLines >= 0.5) { + if ( + totalNonEmptyLines === 0 || + matchingLines / totalNonEmptyLines >= 0.5 + ) { yield block break // Only match the first occurrence } @@ -443,7 +473,12 @@ function trimDiff(diff: string): string { return trimmedLines.join("\n") } -export function replace(content: string, oldString: string, newString: string, replaceAll = false): string { +export function replace( + content: string, + oldString: string, + newString: string, + replaceAll = false, +): string { if (oldString === newString) { throw new Error("oldString and newString must be different") } @@ -467,7 +502,11 @@ export function replace(content: string, oldString: string, newString: string, r } const lastIndex = content.lastIndexOf(search) if (index !== lastIndex) continue - return content.substring(0, index) + newString + content.substring(index + search.length) + return ( + content.substring(0, index) + + newString + + content.substring(index + search.length) + ) } } throw new Error("oldString not found in content or was found multiple times") diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts index 44f7ad8e..70d5df47 100644 --- a/packages/opencode/src/tool/glob.ts +++ b/packages/opencode/src/tool/glob.ts @@ -20,7 +20,9 @@ export const GlobTool = Tool.define({ async execute(params) { const app = App.info() let search = params.path ?? app.path.cwd - search = path.isAbsolute(search) ? search : path.resolve(app.path.cwd, search) + search = path.isAbsolute(search) + ? search + : path.resolve(app.path.cwd, search) const limit = 100 const files = [] @@ -51,15 +53,17 @@ export const GlobTool = Tool.define({ output.push(...files.map((f) => f.path)) if (truncated) { output.push("") - output.push("(Results are truncated. Consider using a more specific path or pattern.)") + output.push( + "(Results are truncated. Consider using a more specific path or pattern.)", + ) } } return { - title: path.relative(app.path.root, search), metadata: { count: files.length, truncated, + title: path.relative(app.path.root, search), }, output: output.join("\n"), } diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index cd28fb48..e4ceec5e 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -9,9 +9,21 @@ export const GrepTool = Tool.define({ id: "grep", description: DESCRIPTION, parameters: z.object({ - pattern: z.string().describe("The regex pattern to search for in file contents"), - path: z.string().optional().describe("The directory to search in. Defaults to the current working directory."), - include: z.string().optional().describe('File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")'), + pattern: z + .string() + .describe("The regex pattern to search for in file contents"), + path: z + .string() + .optional() + .describe( + "The directory to search in. Defaults to the current working directory.", + ), + include: z + .string() + .optional() + .describe( + 'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")', + ), }), async execute(params) { if (!params.pattern) { @@ -39,8 +51,7 @@ export const GrepTool = Tool.define({ if (exitCode === 1) { return { - title: params.pattern, - metadata: { matches: 0, truncated: false }, + metadata: { matches: 0, truncated: false, title: params.pattern }, output: "No files found", } } @@ -82,8 +93,7 @@ export const GrepTool = Tool.define({ if (finalMatches.length === 0) { return { - title: params.pattern, - metadata: { matches: 0, truncated: false }, + metadata: { matches: 0, truncated: false, title: params.pattern }, output: "No files found", } } @@ -104,14 +114,16 @@ export const GrepTool = Tool.define({ if (truncated) { outputLines.push("") - outputLines.push("(Results are truncated. Consider using a more specific path or pattern.)") + outputLines.push( + "(Results are truncated. Consider using a more specific path or pattern.)", + ) } return { - title: params.pattern, metadata: { matches: finalMatches.length, truncated, + title: params.pattern, }, output: outputLines.join("\n"), } diff --git a/packages/opencode/src/tool/ls.ts b/packages/opencode/src/tool/ls.ts index ea83920c..bfceba85 100644 --- a/packages/opencode/src/tool/ls.ts +++ b/packages/opencode/src/tool/ls.ts @@ -24,8 +24,16 @@ export const ListTool = Tool.define({ id: "list", description: DESCRIPTION, parameters: z.object({ - path: z.string().describe("The absolute path to the directory to list (must be absolute, not relative)").optional(), - ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(), + path: z + .string() + .describe( + "The absolute path to the directory to list (must be absolute, not relative)", + ) + .optional(), + ignore: z + .array(z.string()) + .describe("List of glob patterns to ignore") + .optional(), }), async execute(params) { const app = App.info() @@ -36,7 +44,8 @@ export const ListTool = Tool.define({ for await (const file of glob.scan({ cwd: searchPath, dot: true })) { if (IGNORE_PATTERNS.some((p) => file.includes(p))) continue - if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file))) continue + if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file))) + continue files.push(file) if (files.length >= LIMIT) break } @@ -90,10 +99,10 @@ export const ListTool = Tool.define({ const output = `${searchPath}/\n` + renderDir(".", 0) return { - title: path.relative(app.path.root, searchPath), metadata: { count: files.length, truncated: files.length >= LIMIT, + title: path.relative(app.path.root, searchPath), }, output, } diff --git a/packages/opencode/src/tool/lsp-diagnostics.ts b/packages/opencode/src/tool/lsp-diagnostics.ts index fc9699bf..97ae7a26 100644 --- a/packages/opencode/src/tool/lsp-diagnostics.ts +++ b/packages/opencode/src/tool/lsp-diagnostics.ts @@ -13,16 +13,20 @@ export const LspDiagnosticTool = Tool.define({ }), execute: async (args) => { const app = App.info() - const normalized = path.isAbsolute(args.path) ? args.path : path.join(app.path.cwd, args.path) + const normalized = path.isAbsolute(args.path) + ? args.path + : path.join(app.path.cwd, args.path) await LSP.touchFile(normalized, true) const diagnostics = await LSP.diagnostics() const file = diagnostics[normalized] return { - title: path.relative(app.path.root, normalized), metadata: { diagnostics, + title: path.relative(app.path.root, normalized), }, - output: file?.length ? file.map(LSP.Diagnostic.pretty).join("\n") : "No errors found", + output: file?.length + ? file.map(LSP.Diagnostic.pretty).join("\n") + : "No errors found", } }, }) diff --git a/packages/opencode/src/tool/lsp-hover.ts b/packages/opencode/src/tool/lsp-hover.ts index bb94ddb3..f5031f85 100644 --- a/packages/opencode/src/tool/lsp-hover.ts +++ b/packages/opencode/src/tool/lsp-hover.ts @@ -15,7 +15,9 @@ export const LspHoverTool = Tool.define({ }), execute: async (args) => { const app = App.info() - const file = path.isAbsolute(args.file) ? args.file : path.join(app.path.cwd, args.file) + const file = path.isAbsolute(args.file) + ? args.file + : path.join(app.path.cwd, args.file) await LSP.touchFile(file, true) const result = await LSP.hover({ ...args, @@ -23,9 +25,14 @@ export const LspHoverTool = Tool.define({ }) return { - title: path.relative(app.path.root, file) + ":" + args.line + ":" + args.character, metadata: { result, + title: + path.relative(app.path.root, file) + + ":" + + args.line + + ":" + + args.character, }, output: JSON.stringify(result, null, 2), } diff --git a/packages/opencode/src/tool/multiedit.ts b/packages/opencode/src/tool/multiedit.ts index 041893b9..f8d0f16c 100644 --- a/packages/opencode/src/tool/multiedit.ts +++ b/packages/opencode/src/tool/multiedit.ts @@ -10,7 +10,9 @@ export const MultiEditTool = Tool.define({ description: DESCRIPTION, parameters: z.object({ filePath: z.string().describe("The absolute path to the file to modify"), - edits: z.array(EditTool.parameters).describe("Array of edit operations to perform sequentially on the file"), + edits: z + .array(EditTool.parameters) + .describe("Array of edit operations to perform sequentially on the file"), }), async execute(params, ctx) { const results = [] @@ -28,9 +30,9 @@ export const MultiEditTool = Tool.define({ } const app = App.info() return { - title: path.relative(app.path.root, params.filePath), metadata: { results: results.map((r) => r.metadata), + title: path.relative(app.path.root, params.filePath), }, output: results.at(-1)!.output, } diff --git a/packages/opencode/src/tool/patch.ts b/packages/opencode/src/tool/patch.ts index 11cc56c9..6266d163 100644 --- a/packages/opencode/src/tool/patch.ts +++ b/packages/opencode/src/tool/patch.ts @@ -6,7 +6,9 @@ import { FileTime } from "../file/time" import DESCRIPTION from "./patch.txt" const PatchParams = z.object({ - patchText: z.string().describe("The full patch text that describes all changes to be made"), + patchText: z + .string() + .describe("The full patch text that describes all changes to be made"), }) interface Change { @@ -40,7 +42,10 @@ function identifyFilesNeeded(patchText: string): string[] { const files: string[] = [] const lines = patchText.split("\n") for (const line of lines) { - if (line.startsWith("*** Update File:") || line.startsWith("*** Delete File:")) { + if ( + line.startsWith("*** Update File:") || + line.startsWith("*** Delete File:") + ) { const filePath = line.split(":", 2)[1]?.trim() if (filePath) files.push(filePath) } @@ -60,7 +65,10 @@ function identifyFilesAdded(patchText: string): string[] { return files } -function textToPatch(patchText: string, _currentFiles: Record): [PatchOperation[], number] { +function textToPatch( + patchText: string, + _currentFiles: Record, +): [PatchOperation[], number] { const operations: PatchOperation[] = [] const lines = patchText.split("\n") let i = 0 @@ -85,7 +93,11 @@ function textToPatch(patchText: string, _currentFiles: Record): const changes: PatchChange[] = [] i++ - while (i < lines.length && !lines[i].startsWith("@@") && !lines[i].startsWith("***")) { + while ( + i < lines.length && + !lines[i].startsWith("@@") && + !lines[i].startsWith("***") + ) { const changeLine = lines[i] if (changeLine.startsWith(" ")) { changes.push({ type: "keep", content: changeLine.substring(1) }) @@ -139,7 +151,10 @@ function textToPatch(patchText: string, _currentFiles: Record): return [operations, fuzz] } -function patchToCommit(operations: PatchOperation[], currentFiles: Record): Commit { +function patchToCommit( + operations: PatchOperation[], + currentFiles: Record, +): Commit { const changes: Record = {} for (const op of operations) { @@ -158,7 +173,9 @@ function patchToCommit(operations: PatchOperation[], currentFiles: Record line.includes(hunk.contextLine)) + const contextIndex = lines.findIndex((line) => + line.includes(hunk.contextLine), + ) if (contextIndex === -1) { throw new Error(`Context line not found: ${hunk.contextLine}`) } @@ -187,7 +204,11 @@ function patchToCommit(operations: PatchOperation[], currentFiles: Record 3) { - throw new Error(`patch contains fuzzy matches (fuzz level: ${fuzz}). Please make your context lines more precise`) + throw new Error( + `patch contains fuzzy matches (fuzz level: ${fuzz}). Please make your context lines more precise`, + ) } // Convert patch to commit @@ -320,7 +343,11 @@ export const PatchTool = Tool.define({ const newContent = change.new_content || "" // Calculate diff statistics - const [, additions, removals] = generateDiff(oldContent, newContent, filePath) + const [, additions, removals] = generateDiff( + oldContent, + newContent, + filePath, + ) totalAdditions += additions totalRemovals += removals @@ -331,11 +358,11 @@ export const PatchTool = Tool.define({ const output = result return { - title: `${filesToRead.length} files`, metadata: { changed: changedFiles, additions: totalAdditions, removals: totalRemovals, + title: `${filesToRead.length} files`, }, output, } diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 62d67aa1..f524389e 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -16,8 +16,14 @@ export const ReadTool = Tool.define({ description: DESCRIPTION, parameters: z.object({ filePath: z.string().describe("The path to the file to read"), - offset: z.number().describe("The line number to start reading from (0-based)").optional(), - limit: z.number().describe("The number of lines to read (defaults to 2000)").optional(), + offset: z + .number() + .describe("The line number to start reading from (0-based)") + .optional(), + limit: z + .number() + .describe("The number of lines to read (defaults to 2000)") + .optional(), }), async execute(params, ctx) { let filePath = params.filePath @@ -34,13 +40,16 @@ export const ReadTool = Tool.define({ const suggestions = dirEntries .filter( (entry) => - entry.toLowerCase().includes(base.toLowerCase()) || base.toLowerCase().includes(entry.toLowerCase()), + entry.toLowerCase().includes(base.toLowerCase()) || + base.toLowerCase().includes(entry.toLowerCase()), ) .map((entry) => path.join(dir, entry)) .slice(0, 3) if (suggestions.length > 0) { - throw new Error(`File not found: ${filePath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`) + throw new Error( + `File not found: ${filePath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`, + ) } throw new Error(`File not found: ${filePath}`) @@ -48,14 +57,21 @@ export const ReadTool = Tool.define({ const stats = await file.stat() if (stats.size > MAX_READ_SIZE) - throw new Error(`File is too large (${stats.size} bytes). Maximum size is ${MAX_READ_SIZE} bytes`) + throw new Error( + `File is too large (${stats.size} bytes). Maximum size is ${MAX_READ_SIZE} bytes`, + ) const limit = params.limit ?? DEFAULT_READ_LIMIT const offset = params.offset || 0 const isImage = isImageFile(filePath) - if (isImage) throw new Error(`This is an image file of type: ${isImage}\nUse a different tool to process images`) + if (isImage) + throw new Error( + `This is an image file of type: ${isImage}\nUse a different tool to process images`, + ) const lines = await file.text().then((text) => text.split("\n")) const raw = lines.slice(offset, offset + limit).map((line) => { - return line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + "..." : line + return line.length > MAX_LINE_LENGTH + ? line.substring(0, MAX_LINE_LENGTH) + "..." + : line }) const content = raw.map((line, index) => { return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}` @@ -66,7 +82,9 @@ export const ReadTool = Tool.define({ output += content.join("\n") if (lines.length > offset + content.length) { - output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${offset + content.length})` + output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${ + offset + content.length + })` } output += "\n" @@ -75,10 +93,10 @@ export const ReadTool = Tool.define({ FileTime.read(ctx.sessionID, filePath) return { - title: path.relative(App.info().path.root, filePath), output, metadata: { preview, + title: path.relative(App.info().path.root, filePath), }, } }, diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 75739833..2796d0fe 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -3,36 +3,41 @@ import DESCRIPTION from "./task.txt" import { z } from "zod" import { Session } from "../session" import { Bus } from "../bus" -import { MessageV2 } from "../session/message-v2" +import { Message } from "../session/message" export const TaskTool = Tool.define({ id: "task", description: DESCRIPTION, parameters: z.object({ - description: z.string().describe("A short (3-5 words) description of the task"), + description: z + .string() + .describe("A short (3-5 words) description of the task"), prompt: z.string().describe("The task for the agent to perform"), }), async execute(params, ctx) { const session = await Session.create(ctx.sessionID) - const msg = (await Session.getMessage(ctx.sessionID, ctx.messageID)) as MessageV2.Assistant + const msg = await Session.getMessage(ctx.sessionID, ctx.messageID) + const metadata = msg.metadata.assistant! - function summary(input: MessageV2.Info) { + function summary(input: Message.Info) { const result = [] + for (const part of input.parts) { - if (part.type === "tool" && part.state.status === "completed") { - result.push(part) + if (part.type === "tool-invocation") { + result.push({ + toolInvocation: part.toolInvocation, + metadata: input.metadata.tool[part.toolInvocation.toolCallId], + }) } } return result } - const unsub = Bus.subscribe(MessageV2.Event.Updated, async (evt) => { - if (evt.properties.info.sessionID !== session.id) return + const unsub = Bus.subscribe(Message.Event.Updated, async (evt) => { + if (evt.properties.info.metadata.sessionID !== session.id) return ctx.metadata({ title: params.description, - metadata: { - summary: summary(evt.properties.info), - }, + summary: summary(evt.properties.info), }) }) @@ -41,8 +46,8 @@ export const TaskTool = Tool.define({ }) const result = await Session.chat({ sessionID: session.id, - modelID: msg.modelID, - providerID: msg.providerID, + modelID: metadata.modelID, + providerID: metadata.providerID, parts: [ { type: "text", @@ -52,8 +57,8 @@ export const TaskTool = Tool.define({ }) unsub() return { - title: params.description, metadata: { + title: params.description, summary: summary(result), }, output: result.parts.findLast((x) => x.type === "text")!.text, diff --git a/packages/opencode/src/tool/todo.ts b/packages/opencode/src/tool/todo.ts index 94156460..33ac3d12 100644 --- a/packages/opencode/src/tool/todo.ts +++ b/packages/opencode/src/tool/todo.ts @@ -5,8 +5,12 @@ import { App } from "../app/app" const TodoInfo = z.object({ content: z.string().min(1).describe("Brief description of the task"), - status: z.enum(["pending", "in_progress", "completed"]).describe("Current status of the task"), - priority: z.enum(["high", "medium", "low"]).describe("Priority level of the task"), + status: z + .enum(["pending", "in_progress", "completed"]) + .describe("Current status of the task"), + priority: z + .enum(["high", "medium", "low"]) + .describe("Priority level of the task"), id: z.string().describe("Unique identifier for the todo item"), }) type TodoInfo = z.infer @@ -28,9 +32,9 @@ export const TodoWriteTool = Tool.define({ const todos = state() todos[opts.sessionID] = params.todos return { - title: `${params.todos.filter((x) => x.status !== "completed").length} todos`, output: JSON.stringify(params.todos, null, 2), metadata: { + title: `${params.todos.filter((x) => x.status !== "completed").length} todos`, todos: params.todos, }, } @@ -44,9 +48,9 @@ export const TodoReadTool = Tool.define({ async execute(_params, opts) { const todos = state()[opts.sessionID] ?? [] return { - title: `${todos.filter((x) => x.status !== "completed").length} todos`, metadata: { todos, + title: `${todos.filter((x) => x.status !== "completed").length} todos`, }, output: JSON.stringify(todos, null, 2), } diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index f44322ed..8c1cbf48 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -2,15 +2,19 @@ import type { StandardSchemaV1 } from "@standard-schema/spec" export namespace Tool { interface Metadata { + title: string [key: string]: any } export type Context = { sessionID: string messageID: string abort: AbortSignal - metadata(input: { title?: string; metadata?: M }): void + metadata(meta: M): void } - export interface Info { + export interface Info< + Parameters extends StandardSchemaV1 = StandardSchemaV1, + M extends Metadata = Metadata, + > { id: string description: string parameters: Parameters @@ -18,15 +22,15 @@ export namespace Tool { args: StandardSchemaV1.InferOutput, ctx: Context, ): Promise<{ - title: string metadata: M output: string }> } - export function define( - input: Info, - ): Info { + export function define< + Parameters extends StandardSchemaV1, + Result extends Metadata, + >(input: Info): Info { return input } } diff --git a/packages/opencode/src/tool/webfetch.ts b/packages/opencode/src/tool/webfetch.ts index 235d2113..5b7b9f9d 100644 --- a/packages/opencode/src/tool/webfetch.ts +++ b/packages/opencode/src/tool/webfetch.ts @@ -14,7 +14,9 @@ export const WebFetchTool = Tool.define({ url: z.string().describe("The URL to fetch content from"), format: z .enum(["text", "markdown", "html"]) - .describe("The format to return the content in (text, markdown, or html)"), + .describe( + "The format to return the content in (text, markdown, or html)", + ), timeout: z .number() .min(0) @@ -24,11 +26,17 @@ export const WebFetchTool = Tool.define({ }), async execute(params, ctx) { // Validate URL - if (!params.url.startsWith("http://") && !params.url.startsWith("https://")) { + if ( + !params.url.startsWith("http://") && + !params.url.startsWith("https://") + ) { throw new Error("URL must start with http:// or https://") } - const timeout = Math.min((params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000, MAX_TIMEOUT) + const timeout = Math.min( + (params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000, + MAX_TIMEOUT, + ) const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), timeout) @@ -38,7 +46,8 @@ export const WebFetchTool = Tool.define({ headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", + Accept: + "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9", }, }) @@ -70,14 +79,16 @@ export const WebFetchTool = Tool.define({ const text = await extractTextFromHTML(content) return { output: text, - title, - metadata: {}, + metadata: { + title, + }, } } return { output: content, - title, - metadata: {}, + metadata: { + title, + }, } case "markdown": @@ -85,28 +96,32 @@ export const WebFetchTool = Tool.define({ const markdown = convertHTMLToMarkdown(content) return { output: markdown, - title, - metadata: {}, + metadata: { + title, + }, } } return { output: "```\n" + content + "\n```", - title, - metadata: {}, + metadata: { + title, + }, } case "html": return { output: content, - title, - metadata: {}, + metadata: { + title, + }, } default: return { output: content, - title, - metadata: {}, + metadata: { + title, + }, } } }, @@ -128,7 +143,16 @@ async function extractTextFromHTML(html: string) { .on("*", { element(element) { // Reset skip flag when entering other elements - if (!["script", "style", "noscript", "iframe", "object", "embed"].includes(element.tagName)) { + if ( + ![ + "script", + "style", + "noscript", + "iframe", + "object", + "embed", + ].includes(element.tagName) + ) { skipContent = false } }, diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index be92d626..b0515805 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -13,12 +13,18 @@ export const WriteTool = Tool.define({ id: "write", description: DESCRIPTION, parameters: z.object({ - filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"), + filePath: z + .string() + .describe( + "The absolute path to the file to write (must be absolute, not relative)", + ), content: z.string().describe("The content to write to the file"), }), async execute(params, ctx) { const app = App.info() - const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath) + const filepath = path.isAbsolute(params.filePath) + ? params.filePath + : path.join(app.path.cwd, params.filePath) const file = Bun.file(filepath) const exists = await file.exists() @@ -27,7 +33,9 @@ export const WriteTool = Tool.define({ await Permission.ask({ id: "write", sessionID: ctx.sessionID, - title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath, + title: exists + ? "Overwrite this file: " + filepath + : "Create new file: " + filepath, metadata: { filePath: filepath, content: params.content, @@ -54,11 +62,11 @@ export const WriteTool = Tool.define({ } return { - title: path.relative(app.path.root, filepath), metadata: { diagnostics, filepath, exists: exists, + title: path.relative(app.path.root, filepath), }, output, } diff --git a/packages/opencode/src/util/error.ts b/packages/opencode/src/util/error.ts index 53b434c6..be8764ee 100644 --- a/packages/opencode/src/util/error.ts +++ b/packages/opencode/src/util/error.ts @@ -7,7 +7,10 @@ export abstract class NamedError extends Error { abstract schema(): ZodSchema abstract toObject(): { name: string; data: any } - static create(name: Name, data: Data) { + static create( + name: Name, + data: Data, + ) { const schema = z .object({ name: z.literal(name), diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index 54027c26..b73e2dee 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -19,7 +19,10 @@ export namespace Log { await fs.mkdir(dir, { recursive: true }) cleanup(dir) if (options.print) return - logpath = path.join(dir, new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log") + logpath = path.join( + dir, + new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log", + ) const logfile = Bun.file(logpath) await fs.truncate(logpath).catch(() => {}) const writer = logfile.writer() @@ -40,7 +43,9 @@ export namespace Log { const filesToDelete = files.slice(0, -10) - await Promise.all(filesToDelete.map((file) => fs.unlink(file).catch(() => {}))) + await Promise.all( + filesToDelete.map((file) => fs.unlink(file).catch(() => {})), + ) } let last = Date.now() @@ -58,7 +63,11 @@ export namespace Log { const next = new Date() const diff = next.getTime() - last last = next.getTime() - return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n" + return ( + [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message] + .filter(Boolean) + .join(" ") + "\n" + ) } const result = { info(message?: any, extra?: Record) { diff --git a/packages/opencode/test/tool/edit.test.ts b/packages/opencode/test/tool/edit.test.ts index 6906062d..6de4f2a7 100644 --- a/packages/opencode/test/tool/edit.test.ts +++ b/packages/opencode/test/tool/edit.test.ts @@ -17,7 +17,12 @@ const testCases: TestCase[] = [ replace: 'console.log("universe");', }, { - content: ["if (condition) {", " doSomething();", " doSomethingElse();", "}"].join("\n"), + content: [ + "if (condition) {", + " doSomething();", + " doSomethingElse();", + "}", + ].join("\n"), find: [" doSomething();", " doSomethingElse();"].join("\n"), replace: [" doNewThing();", " doAnotherThing();"].join("\n"), }, @@ -48,8 +53,15 @@ const testCases: TestCase[] = [ " return result;", "}", ].join("\n"), - find: ["function calculate(a, b) {", " // different middle content", " return result;", "}"].join("\n"), - replace: ["function calculate(a, b) {", " return a * b * 2;", "}"].join("\n"), + find: [ + "function calculate(a, b) {", + " // different middle content", + " return result;", + "}", + ].join("\n"), + replace: ["function calculate(a, b) {", " return a * b * 2;", "}"].join( + "\n", + ), }, { content: [ @@ -64,7 +76,13 @@ const testCases: TestCase[] = [ "}", ].join("\n"), find: ["class MyClass {", " // different implementation", "}"].join("\n"), - replace: ["class MyClass {", " constructor() {", " this.value = 42;", " }", "}"].join("\n"), + replace: [ + "class MyClass {", + " constructor() {", + " this.value = 42;", + " }", + "}", + ].join("\n"), }, // WhitespaceNormalizedReplacer cases @@ -86,21 +104,48 @@ const testCases: TestCase[] = [ // IndentationFlexibleReplacer cases { - content: [" function nested() {", ' console.log("deeply nested");', " return true;", " }"].join( - "\n", - ), - find: ["function nested() {", ' console.log("deeply nested");', " return true;", "}"].join("\n"), - replace: ["function nested() {", ' console.log("updated");', " return false;", "}"].join("\n"), + content: [ + " function nested() {", + ' console.log("deeply nested");', + " return true;", + " }", + ].join("\n"), + find: [ + "function nested() {", + ' console.log("deeply nested");', + " return true;", + "}", + ].join("\n"), + replace: [ + "function nested() {", + ' console.log("updated");', + " return false;", + "}", + ].join("\n"), }, { - content: [" if (true) {", ' console.log("level 1");', ' console.log("level 2");', " }"].join("\n"), - find: ["if (true) {", 'console.log("level 1");', ' console.log("level 2");', "}"].join("\n"), + content: [ + " if (true) {", + ' console.log("level 1");', + ' console.log("level 2");', + " }", + ].join("\n"), + find: [ + "if (true) {", + 'console.log("level 1");', + ' console.log("level 2");', + "}", + ].join("\n"), replace: ["if (true) {", 'console.log("updated");', "}"].join("\n"), }, // replaceAll option cases { - content: ['console.log("test");', 'console.log("test");', 'console.log("test");'].join("\n"), + content: [ + 'console.log("test");', + 'console.log("test");', + 'console.log("test");', + ].join("\n"), find: 'console.log("test");', replace: 'console.log("updated");', all: true, @@ -168,7 +213,9 @@ const testCases: TestCase[] = [ // MultiOccurrenceReplacer cases (with replaceAll) { - content: ["debug('start');", "debug('middle');", "debug('end');"].join("\n"), + content: ["debug('start');", "debug('middle');", "debug('end');"].join( + "\n", + ), find: "debug", replace: "log", all: true, @@ -192,7 +239,9 @@ const testCases: TestCase[] = [ replace: "const value = 24;", }, { - content: ["", " if (condition) {", " doSomething();", " }", ""].join("\n"), + content: ["", " if (condition) {", " doSomething();", " }", ""].join( + "\n", + ), find: ["if (condition) {", " doSomething();", "}"].join("\n"), replace: ["if (condition) {", " doNothing();", "}"].join("\n"), }, @@ -213,7 +262,9 @@ const testCases: TestCase[] = [ " return result;", "}", ].join("\n"), - replace: ["function calculate(a, b) {", " return (a + b) * 2;", "}"].join("\n"), + replace: ["function calculate(a, b) {", " return (a + b) * 2;", "}"].join( + "\n", + ), }, { content: [ @@ -227,8 +278,15 @@ const testCases: TestCase[] = [ " }", "}", ].join("\n"), - find: ["class TestClass {", " // different implementation", " // with multiple lines", "}"].join("\n"), - replace: ["class TestClass {", " getValue() { return 42; }", "}"].join("\n"), + find: [ + "class TestClass {", + " // different implementation", + " // with multiple lines", + "}", + ].join("\n"), + replace: ["class TestClass {", " getValue() { return 42; }", "}"].join( + "\n", + ), }, // Combined edge cases for new replacers @@ -238,7 +296,9 @@ const testCases: TestCase[] = [ replace: 'console.log("updated");', }, { - content: [" ", "function test() {", " return 'value';", "}", " "].join("\n"), + content: [" ", "function test() {", " return 'value';", "}", " "].join( + "\n", + ), find: ["function test() {", "return 'value';", "}"].join("\n"), replace: ["function test() {", "return 'new value';", "}"].join("\n"), }, @@ -286,7 +346,13 @@ const testCases: TestCase[] = [ // ContextAwareReplacer - test with trailing newline in find string { - content: ["class Test {", " method1() {", " return 1;", " }", "}"].join("\n"), + content: [ + "class Test {", + " method1() {", + " return 1;", + " }", + "}", + ].join("\n"), find: [ "class Test {", " // different content", @@ -335,7 +401,12 @@ describe("EditTool Replacers", () => { replace(testCase.content, testCase.find, testCase.replace, testCase.all) }).toThrow() } else { - const result = replace(testCase.content, testCase.find, testCase.replace, testCase.all) + const result = replace( + testCase.content, + testCase.find, + testCase.replace, + testCase.all, + ) expect(result).toContain(testCase.replace) } }) diff --git a/packages/opencode/test/tool/tool.test.ts b/packages/opencode/test/tool/tool.test.ts index 88325029..4723a61d 100644 --- a/packages/opencode/test/tool/tool.test.ts +++ b/packages/opencode/test/tool/tool.test.ts @@ -42,7 +42,10 @@ describe("tool.glob", () => { describe("tool.ls", () => { test("basic", async () => { const result = await App.provide({ cwd: process.cwd() }, async () => { - return await ListTool.execute({ path: "./example", ignore: [".git"] }, ctx) + return await ListTool.execute( + { path: "./example", ignore: [".git"] }, + ctx, + ) }) expect(result.output).toMatchSnapshot() }) diff --git a/packages/tui/cmd/opencode/main.go b/packages/tui/cmd/opencode/main.go index ada1880d..a9283525 100644 --- a/packages/tui/cmd/opencode/main.go +++ b/packages/tui/cmd/opencode/main.go @@ -9,12 +9,10 @@ import ( "strings" tea "github.com/charmbracelet/bubbletea/v2" - flag "github.com/spf13/pflag" "github.com/sst/opencode-sdk-go" "github.com/sst/opencode-sdk-go/option" "github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/tui" - "golang.design/x/clipboard" ) var Version = "dev" @@ -25,10 +23,6 @@ func main() { version = "v" + Version } - var model *string = flag.String("model", "", "model to begin with") - var prompt *string = flag.String("prompt", "", "prompt to begin with") - flag.Parse() - url := os.Getenv("OPENCODE_SERVER") appInfoStr := os.Getenv("OPENCODE_APP_INFO") @@ -67,18 +61,11 @@ func main() { os.Exit(1) } - go func() { - err = clipboard.Init() - if err != nil { - slog.Error("Failed to initialize clipboard", "error", err) - } - }() - // Create main context for the application ctx, cancel := context.WithCancel(context.Background()) defer cancel() - app_, err := app.New(ctx, version, appInfo, httpClient, model, prompt) + app_, err := app.New(ctx, version, appInfo, httpClient) if err != nil { panic(err) } diff --git a/packages/tui/go.mod b/packages/tui/go.mod index 10f6e7e4..043d9fcd 100644 --- a/packages/tui/go.mod +++ b/packages/tui/go.mod @@ -10,14 +10,13 @@ require ( github.com/charmbracelet/glamour v0.10.0 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/charmbracelet/x/ansi v0.8.0 - github.com/google/uuid v1.6.0 github.com/lithammer/fuzzysearch v1.1.8 github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.16.0 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 github.com/sst/opencode-sdk-go v0.1.0-alpha.8 - golang.design/x/clipboard v0.7.1 + github.com/tidwall/gjson v1.14.4 rsc.io/qr v0.2.0 ) @@ -50,15 +49,12 @@ require ( github.com/sosodev/duration v1.3.1 // indirect github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect github.com/spf13/cobra v1.9.1 // indirect - github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect - golang.org/x/exp/shiny v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f // indirect - golang.org/x/mod v0.25.0 // indirect - golang.org/x/tools v0.34.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/tools v0.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) @@ -69,6 +65,7 @@ require ( github.com/charmbracelet/colorprofile v0.3.1 // indirect github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/disintegration/imaging v1.6.2 github.com/dlclark/regexp2 v1.11.5 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/gorilla/css v1.0.1 // indirect @@ -80,16 +77,16 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/rivo/uniseg v0.4.7 github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/spf13/pflag v1.0.6 + github.com/spf13/pflag v1.0.6 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/goldmark v1.7.8 // indirect github.com/yuin/goldmark-emoji v1.0.5 // indirect - golang.org/x/image v0.28.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.26.0 + golang.org/x/image v0.26.0 + golang.org/x/net v0.39.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/packages/tui/go.sum b/packages/tui/go.sum index f3541711..29548273 100644 --- a/packages/tui/go.sum +++ b/packages/tui/go.sum @@ -54,6 +54,8 @@ github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= @@ -90,8 +92,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= @@ -210,25 +210,20 @@ github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= -golang.design/x/clipboard v0.7.1 h1:OEG3CmcYRBNnRwpDp7+uWLiZi3hrMRJpE9JkkkYtz2c= -golang.design/x/clipboard v0.7.1/go.mod h1:i5SiIqj0wLFw9P/1D7vfILFK0KHMk7ydE72HRrUIgkg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= -golang.org/x/exp/shiny v0.0.0-20250620022241-b7579e27df2b h1:zELBzk+7ERc6m8BxhzU2VYjp03wlEvi+cIgYQR5H3CI= -golang.org/x/exp/shiny v0.0.0-20250620022241-b7579e27df2b/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8= -golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE= -golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY= -golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f h1:/n+PL2HlfqeSiDCuhdBbRNlGS/g2fM4OHufalHaTVG8= -golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f/go.mod h1:ESkJ836Z6LpG6mTVAhA48LpfW/8fNR0ifStlH2axyfg= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY= +golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -239,15 +234,15 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -266,28 +261,28 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go index 237ee0fe..6b59acae 100644 --- a/packages/tui/internal/app/app.go +++ b/packages/tui/internal/app/app.go @@ -18,23 +18,20 @@ import ( "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/util" - "golang.design/x/clipboard" ) type App struct { - Info opencode.App - Version string - StatePath string - Config *opencode.Config - Client *opencode.Client - State *config.State - Provider *opencode.Provider - Model *opencode.Model - Session *opencode.Session - Messages []opencode.MessageUnion - Commands commands.CommandRegistry - InitialModel *string - InitialPrompt *string + Info opencode.App + Version string + StatePath string + Config *opencode.Config + Client *opencode.Client + State *config.State + Provider *opencode.Provider + Model *opencode.Model + Session *opencode.Session + Messages []opencode.Message + Commands commands.CommandRegistry } type SessionSelectedMsg = *opencode.Session @@ -47,10 +44,10 @@ type SessionClearedMsg struct{} type CompactSessionMsg struct{} type SendMsg struct { Text string - Attachments []opencode.FilePartParam + Attachments []Attachment } type OptimisticMessageAddedMsg struct { - Message opencode.MessageUnion + Message opencode.Message } type FileRenderedMsg struct { FilePath string @@ -61,8 +58,6 @@ func New( version string, appInfo opencode.App, httpClient *opencode.Client, - model *string, - prompt *string, ) (*App, error) { util.RootPath = appInfo.Path.Root util.CwdPath = appInfo.Path.Cwd @@ -114,17 +109,15 @@ func New( slog.Debug("Loaded config", "config", configInfo) app := &App{ - Info: appInfo, - Version: version, - StatePath: appStatePath, - Config: configInfo, - State: appState, - Client: httpClient, - Session: &opencode.Session{}, - Messages: []opencode.MessageUnion{}, - Commands: commands.LoadFromConfig(configInfo), - InitialModel: model, - InitialPrompt: prompt, + Info: appInfo, + Version: version, + StatePath: appStatePath, + Config: configInfo, + State: appState, + Client: httpClient, + Session: &opencode.Session{}, + Messages: []opencode.Message{}, + Commands: commands.LoadFromConfig(configInfo), } return app, nil @@ -147,101 +140,66 @@ func (a *App) Key(commandName commands.CommandName) string { return base(key) + muted(" "+command.Description) } -func (a *App) SetClipboard(text string) tea.Cmd { - var cmds []tea.Cmd - cmds = append(cmds, func() tea.Msg { - clipboard.Write(clipboard.FmtText, []byte(text)) - return nil - }) - // try to set the clipboard using OSC52 for terminals that support it - cmds = append(cmds, tea.SetClipboard(text)) - return tea.Sequence(cmds...) -} - func (a *App) InitializeProvider() tea.Cmd { - providersResponse, err := a.Client.Config.Providers(context.Background()) - if err != nil { - slog.Error("Failed to list providers", "error", err) - // TODO: notify user - return nil - } - providers := providersResponse.Providers - var defaultProvider *opencode.Provider - var defaultModel *opencode.Model - - var anthropic *opencode.Provider - for _, provider := range providers { - if provider.ID == "anthropic" { - anthropic = &provider + return func() tea.Msg { + providersResponse, err := a.Client.Config.Providers(context.Background()) + if err != nil { + slog.Error("Failed to list providers", "error", err) + // TODO: notify user + return nil } - } + providers := providersResponse.Providers + var defaultProvider *opencode.Provider + var defaultModel *opencode.Model - // default to anthropic if available - if anthropic != nil { - defaultProvider = anthropic - defaultModel = getDefaultModel(providersResponse, *anthropic) - } - - for _, provider := range providers { - if defaultProvider == nil || defaultModel == nil { - defaultProvider = &provider - defaultModel = getDefaultModel(providersResponse, provider) - } - providers = append(providers, provider) - } - if len(providers) == 0 { - slog.Error("No providers configured") - return nil - } - - var currentProvider *opencode.Provider - var currentModel *opencode.Model - for _, provider := range providers { - if provider.ID == a.State.Provider { - currentProvider = &provider - - for _, model := range provider.Models { - if model.ID == a.State.Model { - currentModel = &model - } + var anthropic *opencode.Provider + for _, provider := range providers { + if provider.ID == "anthropic" { + anthropic = &provider } } - } - if currentProvider == nil || currentModel == nil { - currentProvider = defaultProvider - currentModel = defaultModel - } - var initialProvider *opencode.Provider - var initialModel *opencode.Model - if a.InitialModel != nil && *a.InitialModel != "" { - splits := strings.Split(*a.InitialModel, "/") + // default to anthropic if available + if anthropic != nil { + defaultProvider = anthropic + defaultModel = getDefaultModel(providersResponse, *anthropic) + } + for _, provider := range providers { - if provider.ID == splits[0] { - initialProvider = &provider + if defaultProvider == nil || defaultModel == nil { + defaultProvider = &provider + defaultModel = getDefaultModel(providersResponse, provider) + } + providers = append(providers, provider) + } + if len(providers) == 0 { + slog.Error("No providers configured") + return nil + } + + var currentProvider *opencode.Provider + var currentModel *opencode.Model + for _, provider := range providers { + if provider.ID == a.State.Provider { + currentProvider = &provider + for _, model := range provider.Models { - if model.ID == splits[1] { - initialModel = &model + if model.ID == a.State.Model { + currentModel = &model } } } } - } + if currentProvider == nil || currentModel == nil { + currentProvider = defaultProvider + currentModel = defaultModel + } - if initialProvider != nil && initialModel != nil { - currentProvider = initialProvider - currentModel = initialModel + return ModelSelectedMsg{ + Provider: *currentProvider, + Model: *currentModel, + } } - - var cmds []tea.Cmd - cmds = append(cmds, util.CmdHandler(ModelSelectedMsg{ - Provider: *currentProvider, - Model: *currentModel, - })) - if a.InitialPrompt != nil && *a.InitialPrompt != "" { - cmds = append(cmds, util.CmdHandler(SendMsg{Text: *a.InitialPrompt})) - } - return tea.Sequence(cmds...) } func getDefaultModel( @@ -259,16 +217,20 @@ func getDefaultModel( return nil } +type Attachment struct { + FilePath string + FileName string + MimeType string + Content []byte +} + func (a *App) IsBusy() bool { if len(a.Messages) == 0 { return false } lastMessage := a.Messages[len(a.Messages)-1] - if casted, ok := lastMessage.(opencode.AssistantMessage); ok { - return casted.Time.Completed == 0 - } - return false + return lastMessage.Metadata.Time.Completed == 0 } func (a *App) SaveState() { @@ -334,43 +296,29 @@ func (a *App) CreateSession(ctx context.Context) (*opencode.Session, error) { return session, nil } -func (a *App) SendChatMessage( - ctx context.Context, - text string, - attachments []opencode.FilePartParam, -) (*App, tea.Cmd) { +func (a *App) SendChatMessage(ctx context.Context, text string, attachments []Attachment) tea.Cmd { var cmds []tea.Cmd if a.Session.ID == "" { session, err := a.CreateSession(ctx) if err != nil { - return a, toast.NewErrorToast(err.Error()) + return toast.NewErrorToast(err.Error()) } a.Session = session cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session))) } - optimisticParts := []opencode.UserMessagePart{{ - Type: opencode.UserMessagePartTypeText, - Text: text, - }} - if len(attachments) > 0 { - for _, attachment := range attachments { - optimisticParts = append(optimisticParts, opencode.UserMessagePart{ - Type: opencode.UserMessagePartTypeFile, - Filename: attachment.Filename.Value, - Mime: attachment.Mime.Value, - URL: attachment.URL.Value, - }) - } - } - - optimisticMessage := opencode.UserMessage{ - ID: fmt.Sprintf("optimistic-%d", time.Now().UnixNano()), - Role: opencode.UserMessageRoleUser, - Parts: optimisticParts, - SessionID: a.Session.ID, - Time: opencode.UserMessageTime{ - Created: float64(time.Now().Unix()), + optimisticMessage := opencode.Message{ + ID: fmt.Sprintf("optimistic-%d", time.Now().UnixNano()), + Role: opencode.MessageRoleUser, + Parts: []opencode.MessagePart{{ + Type: opencode.MessagePartTypeText, + Text: text, + }}, + Metadata: opencode.MessageMetadata{ + SessionID: a.Session.ID, + Time: opencode.MessageMetadataTime{ + Created: float64(time.Now().Unix()), + }, }, } @@ -378,25 +326,13 @@ func (a *App) SendChatMessage( cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: optimisticMessage})) cmds = append(cmds, func() tea.Msg { - parts := []opencode.UserMessagePartUnionParam{ - opencode.TextPartParam{ - Type: opencode.F(opencode.TextPartTypeText), - Text: opencode.F(text), - }, - } - if len(attachments) > 0 { - for _, attachment := range attachments { - parts = append(parts, opencode.FilePartParam{ - Mime: attachment.Mime, - Type: attachment.Type, - URL: attachment.URL, - Filename: attachment.Filename, - }) - } - } - _, err := a.Client.Session.Chat(ctx, a.Session.ID, opencode.SessionChatParams{ - Parts: opencode.F(parts), + Parts: opencode.F([]opencode.MessagePartUnionParam{ + opencode.TextPartParam{ + Type: opencode.F(opencode.TextPartTypeText), + Text: opencode.F(text), + }, + }), ProviderID: opencode.F(a.Provider.ID), ModelID: opencode.F(a.Model.ID), }) @@ -410,7 +346,7 @@ func (a *App) SendChatMessage( // The actual response will come through SSE // For now, just return success - return a, tea.Batch(cmds...) + return tea.Batch(cmds...) } func (a *App) Cancel(ctx context.Context, sessionID string) error { diff --git a/packages/tui/internal/commands/command.go b/packages/tui/internal/commands/command.go index 791f7475..9c4da12e 100644 --- a/packages/tui/internal/commands/command.go +++ b/packages/tui/internal/commands/command.go @@ -29,7 +29,7 @@ type Command struct { Name CommandName Description string Keybindings []Keybinding - Trigger []string + Trigger string } func (c Command) Keys() []string { @@ -40,21 +40,6 @@ func (c Command) Keys() []string { return keys } -func (c Command) HasTrigger() bool { - return len(c.Trigger) > 0 -} - -func (c Command) PrimaryTrigger() string { - if len(c.Trigger) > 0 { - return c.Trigger[0] - } - return "" -} - -func (c Command) MatchesTrigger(trigger string) bool { - return slices.Contains(c.Trigger, trigger) -} - type CommandRegistry map[CommandName]Command func (r CommandRegistry) Sorted() []Command { @@ -150,37 +135,37 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry { Name: AppHelpCommand, Description: "show help", Keybindings: parseBindings("h"), - Trigger: []string{"help"}, + Trigger: "help", }, { Name: EditorOpenCommand, Description: "open editor", Keybindings: parseBindings("e"), - Trigger: []string{"editor"}, + Trigger: "editor", }, { Name: SessionNewCommand, Description: "new session", Keybindings: parseBindings("n"), - Trigger: []string{"new", "clear"}, + Trigger: "new", }, { Name: SessionListCommand, Description: "list sessions", Keybindings: parseBindings("l"), - Trigger: []string{"sessions", "resume", "continue"}, + Trigger: "sessions", }, { Name: SessionShareCommand, Description: "share session", Keybindings: parseBindings("s"), - Trigger: []string{"share"}, + Trigger: "share", }, { Name: SessionUnshareCommand, Description: "unshare session", Keybindings: parseBindings("u"), - Trigger: []string{"unshare"}, + Trigger: "unshare", }, { Name: SessionInterruptCommand, @@ -191,31 +176,31 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry { Name: SessionCompactCommand, Description: "compact the session", Keybindings: parseBindings("c"), - Trigger: []string{"compact", "summarize"}, + Trigger: "compact", }, { Name: ToolDetailsCommand, Description: "toggle tool details", Keybindings: parseBindings("d"), - Trigger: []string{"details"}, + Trigger: "details", }, { Name: ModelListCommand, Description: "list models", Keybindings: parseBindings("m"), - Trigger: []string{"models"}, + Trigger: "models", }, { Name: ThemeListCommand, Description: "list themes", Keybindings: parseBindings("t"), - Trigger: []string{"themes"}, + Trigger: "themes", }, { Name: FileListCommand, Description: "list files", Keybindings: parseBindings("f"), - Trigger: []string{"files"}, + Trigger: "files", }, { Name: FileCloseCommand, @@ -236,7 +221,7 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry { Name: ProjectInitCommand, Description: "create/update AGENTS.md", Keybindings: parseBindings("i"), - Trigger: []string{"init"}, + Trigger: "init", }, { Name: InputClearCommand, @@ -246,7 +231,7 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry { { Name: InputPasteCommand, Description: "paste content", - Keybindings: parseBindings("ctrl+v", "super+v"), + Keybindings: parseBindings("ctrl+v"), }, { Name: InputSubmitCommand, @@ -317,7 +302,7 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry { Name: AppExitCommand, Description: "exit the app", Keybindings: parseBindings("ctrl+c", "q"), - Trigger: []string{"exit", "quit"}, + Trigger: "exit", }, } registry := make(CommandRegistry) diff --git a/packages/tui/internal/completions/commands.go b/packages/tui/internal/completions/commands.go index 3a5dc3bb..c73923e8 100644 --- a/packages/tui/internal/completions/commands.go +++ b/packages/tui/internal/completions/commands.go @@ -31,7 +31,7 @@ func (c *CommandCompletionProvider) GetEmptyMessage() string { func getCommandCompletionItem(cmd commands.Command, space int, t theme.Theme) dialog.CompletionItemI { spacer := strings.Repeat(" ", space) - title := " /" + cmd.PrimaryTrigger() + styles.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description) + title := " /" + cmd.Trigger + styles.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description) value := string(cmd.Name) return dialog.NewCompletionItem(dialog.CompletionItem{ Title: title, @@ -45,8 +45,8 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp space := 1 for _, cmd := range c.app.Commands { - if cmd.HasTrigger() && lipgloss.Width(cmd.PrimaryTrigger()) > space { - space = lipgloss.Width(cmd.PrimaryTrigger()) + if lipgloss.Width(cmd.Trigger) > space { + space = lipgloss.Width(cmd.Trigger) } } space += 2 @@ -56,10 +56,10 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp // If no query, return all commands items := []dialog.CompletionItemI{} for _, cmd := range sorted { - if !cmd.HasTrigger() { + if cmd.Trigger == "" { continue } - space := space - lipgloss.Width(cmd.PrimaryTrigger()) + space := space - lipgloss.Width(cmd.Trigger) items = append(items, getCommandCompletionItem(cmd, space, t)) } return items, nil @@ -70,15 +70,12 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp commandMap := make(map[string]dialog.CompletionItemI) for _, cmd := range sorted { - if !cmd.HasTrigger() { + if cmd.Trigger == "" { continue } - space := space - lipgloss.Width(cmd.PrimaryTrigger()) - // Add all triggers as searchable options - for _, trigger := range cmd.Trigger { - commandNames = append(commandNames, trigger) - commandMap[trigger] = getCommandCompletionItem(cmd, space, t) - } + space := space - lipgloss.Width(cmd.Trigger) + commandNames = append(commandNames, cmd.Trigger) + commandMap[cmd.Trigger] = getCommandCompletionItem(cmd, space, t) } // Find fuzzy matches diff --git a/packages/tui/internal/completions/files-folders.go b/packages/tui/internal/completions/files-folders.go index ec298af9..8d6b9958 100644 --- a/packages/tui/internal/completions/files-folders.go +++ b/packages/tui/internal/completions/files-folders.go @@ -16,11 +16,12 @@ import ( type filesAndFoldersContextGroup struct { app *app.App + prefix string gitFiles []dialog.CompletionItemI } func (cg *filesAndFoldersContextGroup) GetId() string { - return "files" + return cg.prefix } func (cg *filesAndFoldersContextGroup) GetEmptyMessage() string { @@ -106,10 +107,9 @@ func (cg *filesAndFoldersContextGroup) GetChildEntries( func NewFileAndFolderContextGroup(app *app.App) dialog.CompletionProvider { cg := &filesAndFoldersContextGroup{ - app: app, + app: app, + prefix: "file", } - go func() { - cg.gitFiles = cg.getGitFiles() - }() + cg.gitFiles = cg.getGitFiles() return cg } diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go index cc31fbef..669ef47d 100644 --- a/packages/tui/internal/components/chat/editor.go +++ b/packages/tui/internal/components/chat/editor.go @@ -1,27 +1,21 @@ package chat import ( - "encoding/base64" "fmt" "log/slog" - "os" - "path/filepath" - "strconv" "strings" "github.com/charmbracelet/bubbles/v2/spinner" tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" - "github.com/google/uuid" - "github.com/sst/opencode-sdk-go" "github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/commands" "github.com/sst/opencode/internal/components/dialog" "github.com/sst/opencode/internal/components/textarea" + "github.com/sst/opencode/internal/image" "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/util" - "golang.design/x/clipboard" ) type EditorComponent interface { @@ -43,6 +37,7 @@ type EditorComponent interface { type editorComponent struct { app *app.App textarea textarea.Model + attachments []app.Attachment spinner spinner.Model interruptKeyInDebounce bool } @@ -66,110 +61,22 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } - case tea.PasteMsg: - text := string(msg) - text = strings.ReplaceAll(text, "\\", "") - text, err := strconv.Unquote(`"` + text + `"`) - if err != nil { - slog.Error("Failed to unquote text", "error", err) - m.textarea.InsertRunesFromUserInput([]rune(msg)) - return m, nil - } - if _, err := os.Stat(text); err != nil { - slog.Error("Failed to paste file", "error", err) - m.textarea.InsertRunesFromUserInput([]rune(msg)) - return m, nil - } - - filePath := text - ext := strings.ToLower(filepath.Ext(filePath)) - - mediaType := "" - switch ext { - case ".jpg": - mediaType = "image/jpeg" - case ".png", ".jpeg", ".gif", ".webp": - mediaType = "image/" + ext[1:] - case ".pdf": - mediaType = "application/pdf" - default: - mediaType = "text/plain" - } - - fileBytes, err := os.ReadFile(filePath) - if err != nil { - slog.Error("Failed to read file", "error", err) - m.textarea.InsertRunesFromUserInput([]rune(msg)) - return m, nil - } - base64EncodedFile := base64.StdEncoding.EncodeToString(fileBytes) - url := fmt.Sprintf("data:%s;base64,%s", mediaType, base64EncodedFile) - - attachment := &textarea.Attachment{ - ID: uuid.NewString(), - Display: fmt.Sprintf("<%s>", filePath), - URL: url, - Filename: filePath, - MediaType: mediaType, - } - m.textarea.InsertAttachment(attachment) - m.textarea.InsertString(" ") - case tea.ClipboardMsg: - text := string(msg) - m.textarea.InsertRunesFromUserInput([]rune(text)) case dialog.ThemeSelectedMsg: - m.textarea = m.resetTextareaStyles() + m.textarea = createTextArea(&m.textarea) m.spinner = createSpinner() return m, tea.Batch(m.spinner.Tick, m.textarea.Focus()) case dialog.CompletionSelectedMsg: - switch msg.ProviderID { - case "commands": + if msg.IsCommand { commandName := strings.TrimPrefix(msg.CompletionValue, "/") updated, cmd := m.Clear() m = updated.(*editorComponent) cmds = append(cmds, cmd) cmds = append(cmds, util.CmdHandler(commands.ExecuteCommandMsg(m.app.Commands[commands.CommandName(commandName)]))) return m, tea.Batch(cmds...) - case "files": - atIndex := m.textarea.LastRuneIndex('@') - if atIndex == -1 { - // Should not happen, but as a fallback, just insert. - m.textarea.InsertString(msg.CompletionValue + " ") - return m, nil - } - - // The range to replace is from the '@' up to the current cursor position. - // Replace the search term (e.g., "@search") with an empty string first. - cursorCol := m.textarea.CursorColumn() - m.textarea.ReplaceRange(atIndex, cursorCol, "") - - // Now, insert the attachment at the position where the '@' was. - // The cursor is now at `atIndex` after the replacement. - filePath := msg.CompletionValue - extension := filepath.Ext(filePath) - mediaType := "" - switch extension { - case ".jpg": - mediaType = "image/jpeg" - case ".png", ".jpeg", ".gif", ".webp": - mediaType = "image/" + extension[1:] - case ".pdf": - mediaType = "application/pdf" - default: - mediaType = "text/plain" - } - attachment := &textarea.Attachment{ - ID: uuid.NewString(), - Display: "@" + filePath, - URL: fmt.Sprintf("file://./%s", filePath), - Filename: filePath, - MediaType: mediaType, - } - m.textarea.InsertAttachment(attachment) - m.textarea.InsertString(" ") - return m, nil - default: + } else { existingValue := m.textarea.Value() + + // Replace the current token (after last space) lastSpaceIndex := strings.LastIndex(existingValue, " ") if lastSpaceIndex == -1 { m.textarea.SetValue(msg.CompletionValue + " ") @@ -221,15 +128,7 @@ func (m *editorComponent) Content(width int) string { if m.app.IsBusy() { keyText := m.getInterruptKeyText() if m.interruptKeyInDebounce { - hint = muted( - "working", - ) + m.spinner.View() + muted( - " ", - ) + base( - keyText+" again", - ) + muted( - " interrupt", - ) + hint = muted("working") + m.spinner.View() + muted(" ") + base(keyText+" again") + muted(" interrupt") } else { hint = muted("working") + m.spinner.View() + muted(" ") + base(keyText) + muted(" interrupt") } @@ -291,29 +190,19 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) { } if len(value) > 0 && value[len(value)-1] == '\\' { // If the last character is a backslash, remove it and add a newline - m.textarea.ReplaceRange(len(value)-1, len(value), "") - m.textarea.InsertString("\n") + m.textarea.SetValue(value[:len(value)-1] + "\n") return m, nil } var cmds []tea.Cmd - - attachments := m.textarea.GetAttachments() - fileParts := make([]opencode.FilePartParam, 0) - for _, attachment := range attachments { - fileParts = append(fileParts, opencode.FilePartParam{ - Type: opencode.F(opencode.FilePartTypeFile), - Mime: opencode.F(attachment.MediaType), - URL: opencode.F(attachment.URL), - Filename: opencode.F(attachment.Filename), - }) - } - updated, cmd := m.Clear() m = updated.(*editorComponent) cmds = append(cmds, cmd) - cmds = append(cmds, util.CmdHandler(app.SendMsg{Text: value, Attachments: fileParts})) + attachments := m.attachments + m.attachments = nil + + cmds = append(cmds, util.CmdHandler(app.SendMsg{Text: value, Attachments: attachments})) return m, tea.Batch(cmds...) } @@ -323,29 +212,19 @@ func (m *editorComponent) Clear() (tea.Model, tea.Cmd) { } func (m *editorComponent) Paste() (tea.Model, tea.Cmd) { - imageBytes := clipboard.Read(clipboard.FmtImage) - if imageBytes != nil { - base64EncodedFile := base64.StdEncoding.EncodeToString(imageBytes) - attachment := &textarea.Attachment{ - ID: uuid.NewString(), - Display: "", - Filename: "clipboard-image", - MediaType: "image/png", - URL: fmt.Sprintf("data:image/png;base64,%s", base64EncodedFile), - } - m.textarea.InsertAttachment(attachment) - m.textarea.InsertString(" ") + imageBytes, text, err := image.GetImageFromClipboard() + if err != nil { + slog.Error(err.Error()) return m, nil } - - textBytes := clipboard.Read(clipboard.FmtText) - if textBytes != nil { - m.textarea.InsertRunesFromUserInput([]rune(string(textBytes))) - return m, nil + if len(imageBytes) != 0 { + attachmentName := fmt.Sprintf("clipboard-image-%d", len(m.attachments)) + attachment := app.Attachment{FilePath: attachmentName, FileName: attachmentName, Content: imageBytes, MimeType: "image/png"} + m.attachments = append(m.attachments, attachment) + } else { + m.textarea.SetValue(m.textarea.Value() + text) } - - // fallback to reading the clipboard using OSC52 - return m, tea.ReadClipboard + return m, nil } func (m *editorComponent) Newline() (tea.Model, tea.Cmd) { @@ -365,37 +244,34 @@ func (m *editorComponent) getSubmitKeyText() string { return m.app.Commands[commands.InputSubmitCommand].Keys()[0] } -func (m *editorComponent) resetTextareaStyles() textarea.Model { +func createTextArea(existing *textarea.Model) textarea.Model { t := theme.CurrentTheme() bgColor := t.BackgroundElement() textColor := t.Text() textMutedColor := t.TextMuted() - ta := m.textarea + ta := textarea.New() ta.Styles.Blurred.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss() ta.Styles.Blurred.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss() - ta.Styles.Blurred.Placeholder = styles.NewStyle(). - Foreground(textMutedColor). - Background(bgColor). - Lipgloss() + ta.Styles.Blurred.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss() ta.Styles.Blurred.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss() ta.Styles.Focused.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss() ta.Styles.Focused.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss() - ta.Styles.Focused.Placeholder = styles.NewStyle(). - Foreground(textMutedColor). - Background(bgColor). - Lipgloss() + ta.Styles.Focused.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss() ta.Styles.Focused.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss() - ta.Styles.Attachment = styles.NewStyle(). - Foreground(t.Secondary()). - Background(bgColor). - Lipgloss() - ta.Styles.SelectedAttachment = styles.NewStyle(). - Foreground(t.Text()). - Background(t.Secondary()). - Lipgloss() ta.Styles.Cursor.Color = t.Primary() + + ta.Prompt = " " + ta.ShowLineNumbers = false + ta.CharLimit = -1 + + if existing != nil { + ta.SetValue(existing.Value()) + // ta.SetWidth(existing.Width()) + ta.SetHeight(existing.Height()) + } + return ta } @@ -415,19 +291,12 @@ func createSpinner() spinner.Model { func NewEditorComponent(app *app.App) EditorComponent { s := createSpinner() + ta := createTextArea(nil) - ta := textarea.New() - ta.Prompt = " " - ta.ShowLineNumbers = false - ta.CharLimit = -1 - - m := &editorComponent{ + return &editorComponent{ app: app, textarea: ta, spinner: s, interruptKeyInDebounce: false, } - m.resetTextareaStyles() - - return m } diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go index 3414a77d..4dde09ea 100644 --- a/packages/tui/internal/components/chat/message.go +++ b/packages/tui/internal/components/chat/message.go @@ -17,6 +17,7 @@ import ( "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/util" + "github.com/tidwall/gjson" "golang.org/x/text/cases" "golang.org/x/text/language" ) @@ -216,35 +217,17 @@ func renderContentBlock( func renderText( app *app.App, - message opencode.MessageUnion, + message opencode.Message, text string, author string, showToolDetails bool, highlight bool, width int, - extra string, - toolCalls ...opencode.ToolPart, + toolCalls ...opencode.ToolInvocationPart, ) string { t := theme.CurrentTheme() - var ts time.Time - backgroundColor := t.BackgroundPanel() - if highlight { - backgroundColor = t.BackgroundElement() - } - messageStyle := styles.NewStyle().Background(backgroundColor) - content := messageStyle.Render(text) - - switch casted := message.(type) { - case opencode.AssistantMessage: - ts = time.UnixMilli(int64(casted.Time.Created)) - content = util.ToMarkdown(text, width, backgroundColor) - case opencode.UserMessage: - ts = time.UnixMilli(int64(casted.Time.Created)) - messageStyle = messageStyle.Width(width - 6) - } - - timestamp := ts. + timestamp := time.UnixMilli(int64(message.Metadata.Time.Created)). Local(). Format("02 Jan 2006 03:04 PM") if time.Now().Format("02 Jan 2006") == timestamp[:11] { @@ -254,12 +237,30 @@ func renderText( info := fmt.Sprintf("%s (%s)", author, timestamp) info = styles.NewStyle().Foreground(t.TextMuted()).Render(info) + backgroundColor := t.BackgroundPanel() + if highlight { + backgroundColor = t.BackgroundElement() + } + messageStyle := styles.NewStyle().Background(backgroundColor) + if message.Role == opencode.MessageRoleUser { + messageStyle = messageStyle.Width(width - 6) + } + + content := messageStyle.Render(text) + if message.Role == opencode.MessageRoleAssistant { + content = util.ToMarkdown(text, width, backgroundColor) + } + if !showToolDetails && toolCalls != nil && len(toolCalls) > 0 { content = content + "\n\n" for _, toolCall := range toolCalls { - title := renderToolTitle(toolCall, width) + title := renderToolTitle(toolCall, message.Metadata, width) + metadata := opencode.MessageMetadataTool{} + if _, ok := message.Metadata.Tool[toolCall.ToolInvocation.ToolCallID]; ok { + metadata = message.Metadata.Tool[toolCall.ToolInvocation.ToolCallID] + } style := styles.NewStyle() - if toolCall.State.Status == opencode.ToolPartStateStatusError { + if _, ok := metadata.ExtraFields["error"]; ok { style = style.Foreground(t.Error()) } title = style.Render(title) @@ -268,14 +269,10 @@ func renderText( } } - sections := []string{content, info} - if extra != "" { - sections = append(sections, "\n"+extra) - } - content = strings.Join(sections, "\n") + content = strings.Join([]string{content, info}, "\n") - switch message.(type) { - case opencode.UserMessage: + switch message.Role { + case opencode.MessageRoleUser: return renderContentBlock( app, content, @@ -284,7 +281,7 @@ func renderText( WithTextColor(t.Text()), WithBorderColorRight(t.Secondary()), ) - case opencode.AssistantMessage: + case opencode.MessageRoleAssistant: return renderContentBlock( app, content, @@ -298,32 +295,39 @@ func renderText( func renderToolDetails( app *app.App, - toolCall opencode.ToolPart, + toolCall opencode.ToolInvocationPart, + messageMetadata opencode.MessageMetadata, highlight bool, width int, ) string { ignoredTools := []string{"todoread"} - if slices.Contains(ignoredTools, toolCall.Tool) { + if slices.Contains(ignoredTools, toolCall.ToolInvocation.ToolName) { return "" } - if toolCall.State.Status == opencode.ToolPartStateStatusPending || toolCall.State.Status == opencode.ToolPartStateStatusRunning { - title := renderToolTitle(toolCall, width) - return renderContentBlock(app, title, highlight, width) + toolCallID := toolCall.ToolInvocation.ToolCallID + metadata := opencode.MessageMetadataTool{} + if _, ok := messageMetadata.Tool[toolCallID]; ok { + metadata = messageMetadata.Tool[toolCallID] } var result *string - if toolCall.State.Output != "" { - result = &toolCall.State.Output + if toolCall.ToolInvocation.Result != "" { + result = &toolCall.ToolInvocation.Result } - toolInputMap := make(map[string]any) - if toolCall.State.Input != nil { - value := toolCall.State.Input + if toolCall.ToolInvocation.State == "partial-call" { + title := renderToolTitle(toolCall, messageMetadata, width) + return renderContentBlock(app, title, highlight, width) + } + + toolArgsMap := make(map[string]any) + if toolCall.ToolInvocation.Args != nil { + value := toolCall.ToolInvocation.Args if m, ok := value.(map[string]any); ok { - toolInputMap = m - keys := make([]string, 0, len(toolInputMap)) - for key := range toolInputMap { + toolArgsMap = m + keys := make([]string, 0, len(toolArgsMap)) + for key := range toolArgsMap { keys = append(keys, key) } slices.Sort(keys) @@ -331,6 +335,7 @@ func renderToolDetails( } body := "" + finished := result != nil && *result != "" t := theme.CurrentTheme() backgroundColor := t.BackgroundPanel() borderColor := t.BackgroundPanel() @@ -339,128 +344,137 @@ func renderToolDetails( borderColor = t.BorderActive() } - if toolCall.State.Status == opencode.ToolPartStateStatusCompleted { - metadata := toolCall.State.Metadata.(map[string]any) - switch toolCall.Tool { - case "read": - preview := metadata["preview"] - if preview != nil && toolInputMap["filePath"] != nil { - filename := toolInputMap["filePath"].(string) - body = preview.(string) - body = util.RenderFile(filename, body, width, util.WithTruncate(6)) + switch toolCall.ToolInvocation.ToolName { + case "read": + preview := metadata.ExtraFields["preview"] + if preview != nil && toolArgsMap["filePath"] != nil { + filename := toolArgsMap["filePath"].(string) + body = preview.(string) + body = util.RenderFile(filename, body, width, util.WithTruncate(6)) + } + case "edit": + if filename, ok := toolArgsMap["filePath"].(string); ok { + diffField := metadata.ExtraFields["diff"] + if diffField != nil { + patch := diffField.(string) + var formattedDiff string + formattedDiff, _ = diff.FormatUnifiedDiff( + filename, + patch, + diff.WithWidth(width-2), + ) + body = strings.TrimSpace(formattedDiff) + style := styles.NewStyle(). + Background(backgroundColor). + Foreground(t.TextMuted()). + Padding(1, 2). + Width(width - 4) + if highlight { + style = style.Foreground(t.Text()).Bold(true) + } + + if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" { + diagnostics = style.Render(diagnostics) + body += "\n" + diagnostics + } + + title := renderToolTitle(toolCall, messageMetadata, width) + title = style.Render(title) + content := title + "\n" + body + content = renderContentBlock( + app, + content, + highlight, + width, + WithPadding(0), + WithBorderColor(borderColor), + ) + return content } - case "edit": - if filename, ok := toolInputMap["filePath"].(string); ok { - diffField := metadata["diff"] - if diffField != nil { - patch := diffField.(string) - var formattedDiff string - formattedDiff, _ = diff.FormatUnifiedDiff( - filename, - patch, - diff.WithWidth(width-2), - ) - body = strings.TrimSpace(formattedDiff) - style := styles.NewStyle(). - Background(backgroundColor). - Foreground(t.TextMuted()). - Padding(1, 2). - Width(width - 4) - if highlight { - style = style.Foreground(t.Text()).Bold(true) - } - - if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" { - diagnostics = style.Render(diagnostics) - body += "\n" + diagnostics - } - - title := renderToolTitle(toolCall, width) - title = style.Render(title) - content := title + "\n" + body - content = renderContentBlock( - app, - content, - highlight, - width, - WithPadding(0), - WithBorderColor(borderColor), - ) - return content + } + case "write": + if filename, ok := toolArgsMap["filePath"].(string); ok { + if content, ok := toolArgsMap["content"].(string); ok { + body = util.RenderFile(filename, content, width) + if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" { + body += "\n\n" + diagnostics } } - case "write": - if filename, ok := toolInputMap["filePath"].(string); ok { - if content, ok := toolInputMap["content"].(string); ok { - body = util.RenderFile(filename, content, width) - if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" { - body += "\n\n" + diagnostics - } - } - } - case "bash": - stdout := metadata["stdout"] - if stdout != nil { - command := toolInputMap["command"].(string) - body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout) + } + case "bash": + stdout := metadata.ExtraFields["stdout"] + if stdout != nil { + command := toolArgsMap["command"].(string) + body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout) + body = util.ToMarkdown(body, width, backgroundColor) + } + case "webfetch": + if format, ok := toolArgsMap["format"].(string); ok && result != nil { + body = *result + body = util.TruncateHeight(body, 10) + if format == "html" || format == "markdown" { body = util.ToMarkdown(body, width, backgroundColor) } - case "webfetch": - if format, ok := toolInputMap["format"].(string); ok && result != nil { - body = *result - body = util.TruncateHeight(body, 10) - if format == "html" || format == "markdown" { - body = util.ToMarkdown(body, width, backgroundColor) + } + case "todowrite": + todos := metadata.JSON.ExtraFields["todos"] + if !todos.IsNull() && finished { + strTodos := todos.Raw() + todos := gjson.Parse(strTodos) + for _, todo := range todos.Array() { + content := todo.Get("content").String() + switch todo.Get("status").String() { + case "completed": + body += fmt.Sprintf("- [x] %s\n", content) + // case "in-progress": + // body += fmt.Sprintf("- [ ] %s\n", content) + default: + body += fmt.Sprintf("- [ ] %s\n", content) } } - case "todowrite": - todos := metadata["todos"] - if todos != nil { - for _, item := range todos.([]any) { - todo := item.(map[string]any) - content := todo["content"].(string) - switch todo["status"] { - case "completed": - body += fmt.Sprintf("- [x] %s\n", content) - // case "in-progress": - // body += fmt.Sprintf("- [ ] %s\n", content) - default: - body += fmt.Sprintf("- [ ] %s\n", content) - } - } - body = util.ToMarkdown(body, width, backgroundColor) - } - case "task": - summary := metadata["summary"] - if summary != nil { - toolcalls := summary.([]any) - steps := []string{} - for _, toolcall := range toolcalls { - call := toolcall.(map[string]any) - if toolInvocation, ok := call["toolInvocation"].(map[string]any); ok { - data, _ := json.Marshal(toolInvocation) - var toolCall opencode.ToolPart - _ = json.Unmarshal(data, &toolCall) - step := renderToolTitle(toolCall, width) + body = util.ToMarkdown(body, width, backgroundColor) + } + case "task": + summary := metadata.JSON.ExtraFields["summary"] + if !summary.IsNull() { + strValue := summary.Raw() + toolcalls := gjson.Parse(strValue).Array() + + steps := []string{} + for _, toolcall := range toolcalls { + call := toolcall.Value().(map[string]any) + if toolInvocation, ok := call["toolInvocation"].(map[string]any); ok { + data, _ := json.Marshal(toolInvocation) + var toolCall opencode.ToolInvocationPart + _ = json.Unmarshal(data, &toolCall) + + if metadata, ok := call["metadata"].(map[string]any); ok { + data, _ = json.Marshal(metadata) + var toolMetadata opencode.MessageMetadataTool + _ = json.Unmarshal(data, &toolMetadata) + + step := renderToolTitle(toolCall, messageMetadata, width) step = "∟ " + step steps = append(steps, step) } } - body = strings.Join(steps, "\n") } - default: - if result == nil { - empty := "" - result = &empty - } - body = *result - body = util.TruncateHeight(body, 10) + body = strings.Join(steps, "\n") } + default: + if result == nil { + empty := "" + result = &empty + } + body = *result + body = util.TruncateHeight(body, 10) } error := "" - if toolCall.State.Status == opencode.ToolPartStateStatusError { - error = toolCall.State.Error + if err, ok := metadata.ExtraFields["error"].(bool); ok && err { + if message, ok := metadata.ExtraFields["message"].(string); ok { + error = message + } } if error != "" { @@ -475,7 +489,7 @@ func renderToolDetails( body = util.TruncateHeight(body, 10) } - title := renderToolTitle(toolCall, width) + title := renderToolTitle(toolCall, messageMetadata, width) content := title + "\n\n" + body return renderContentBlock(app, content, highlight, width, WithBorderColor(borderColor)) } @@ -496,19 +510,20 @@ func renderToolName(name string) string { } func renderToolTitle( - toolCall opencode.ToolPart, + toolCall opencode.ToolInvocationPart, + messageMetadata opencode.MessageMetadata, width int, ) string { // TODO: handle truncate to width - if toolCall.State.Status == opencode.ToolPartStateStatusPending { - return renderToolAction(toolCall.Tool) + if toolCall.ToolInvocation.State == "partial-call" { + return renderToolAction(toolCall.ToolInvocation.ToolName) } toolArgs := "" toolArgsMap := make(map[string]any) - if toolCall.State.Input != nil { - value := toolCall.State.Input + if toolCall.ToolInvocation.Args != nil { + value := toolCall.ToolInvocation.Args if m, ok := value.(map[string]any); ok { toolArgsMap = m @@ -526,8 +541,8 @@ func renderToolTitle( } } - title := renderToolName(toolCall.Tool) - switch toolCall.Tool { + title := renderToolName(toolCall.ToolInvocation.ToolName) + switch toolCall.ToolInvocation.ToolName { case "read": toolArgs = renderArgs(&toolArgsMap, "filePath") title = fmt.Sprintf("%s %s", title, toolArgs) @@ -545,7 +560,7 @@ func renderToolTitle( case "todowrite", "todoread": // title is just the tool name default: - toolName := renderToolName(toolCall.Tool) + toolName := renderToolName(toolCall.ToolInvocation.ToolName) title = fmt.Sprintf("%s %s", toolName, toolArgs) } return title @@ -625,8 +640,8 @@ type Diagnostic struct { } // renderDiagnostics formats LSP diagnostics for display in the TUI -func renderDiagnostics(metadata map[string]any, filePath string) string { - if diagnosticsData, ok := metadata["diagnostics"].(map[string]any); ok { +func renderDiagnostics(metadata opencode.MessageMetadataTool, filePath string) string { + if diagnosticsData, ok := metadata.ExtraFields["diagnostics"].(map[string]any); ok { if fileDiagnostics, ok := diagnosticsData[filePath].([]any); ok { var errorDiagnostics []string for _, diagInterface := range fileDiagnostics { diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go index 6e9c26df..52288078 100644 --- a/packages/tui/internal/components/chat/messages.go +++ b/packages/tui/internal/components/chat/messages.go @@ -9,7 +9,6 @@ import ( "github.com/sst/opencode-sdk-go" "github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/components/dialog" - "github.com/sst/opencode/internal/layout" "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/util" @@ -68,9 +67,11 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.selectedPart = -1 return m, nil case app.OptimisticMessageAddedMsg: - m.tail = true - m.rendering = true - return m, m.Reload() + m.renderView(m.width) + if m.tail { + m.viewport.GotoBottom() + } + return m, nil case dialog.ThemeSelectedMsg: m.cache.Clear() m.rendering = true @@ -99,7 +100,7 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } case opencode.EventListResponseEventMessageUpdated: - if msg.Properties.Info.SessionID == m.app.Session.ID { + if msg.Properties.Info.Metadata.SessionID == m.app.Session.ID { m.renderView(m.width) if m.tail { m.viewport.GotoBottom() @@ -124,58 +125,18 @@ func (m *messagesComponent) renderView(width int) { m.partCount = 0 m.lineCount = 0 - orphanedToolCalls := make([]opencode.ToolPart, 0) + orphanedToolCalls := make([]opencode.ToolInvocationPart, 0) for _, message := range m.app.Messages { var content string var cached bool - switch casted := message.(type) { - case opencode.UserMessage: - userLoop: - for partIndex, part := range casted.Parts { + switch message.Role { + case opencode.MessageRoleUser: + for _, part := range message.Parts { switch part := part.AsUnion().(type) { case opencode.TextPart: - remainingParts := casted.Parts[partIndex+1:] - fileParts := make([]opencode.FilePart, 0) - for _, part := range remainingParts { - switch part := part.AsUnion().(type) { - case opencode.FilePart: - fileParts = append(fileParts, part) - } - } - flexItems := []layout.FlexItem{} - if len(fileParts) > 0 { - fileStyle := styles.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Padding(0, 1) - mediaTypeStyle := styles.NewStyle().Background(t.Secondary()).Foreground(t.BackgroundPanel()).Padding(0, 1) - for _, filePart := range fileParts { - mediaType := "" - switch filePart.Mime { - case "text/plain": - mediaType = "txt" - case "image/png", "image/jpeg", "image/gif", "image/webp": - mediaType = "img" - mediaTypeStyle = mediaTypeStyle.Background(t.Accent()) - case "application/pdf": - mediaType = "pdf" - mediaTypeStyle = mediaTypeStyle.Background(t.Primary()) - } - flexItems = append(flexItems, layout.FlexItem{ - View: mediaTypeStyle.Render(mediaType) + fileStyle.Render(filePart.Filename), - }) - } - } - bgColor := t.BackgroundPanel() - files := layout.Render( - layout.FlexOptions{ - Background: &bgColor, - Width: width - 6, - Direction: layout.Column, - }, - flexItems..., - ) - - key := m.cache.GenerateKey(casted.ID, part.Text, width, m.selectedPart == m.partCount, files) + key := m.cache.GenerateKey(message.ID, part.Text, width, m.selectedPart == m.partCount) content, cached = m.cache.Get(key) if !cached { content = renderText( @@ -186,7 +147,6 @@ func (m *messagesComponent) renderView(width int) { m.showToolDetails, m.partCount == m.selectedPart, width, - files, ) m.cache.Set(key, content) } @@ -194,26 +154,24 @@ func (m *messagesComponent) renderView(width int) { m = m.updateSelected(content, part.Text) blocks = append(blocks, content) } - // Only render the first text part - break userLoop } } - case opencode.AssistantMessage: + case opencode.MessageRoleAssistant: hasTextPart := false - for partIndex, p := range casted.Parts { + for partIndex, p := range message.Parts { switch part := p.AsUnion().(type) { case opencode.TextPart: hasTextPart = true - finished := casted.Time.Completed > 0 - remainingParts := casted.Parts[partIndex+1:] - toolCallParts := make([]opencode.ToolPart, 0) + finished := message.Metadata.Time.Completed > 0 + remainingParts := message.Parts[partIndex+1:] + toolCallParts := make([]opencode.ToolInvocationPart, 0) // sometimes tool calls happen without an assistant message // these should be included in this assistant message as well if len(orphanedToolCalls) > 0 { toolCallParts = append(toolCallParts, orphanedToolCalls...) - orphanedToolCalls = make([]opencode.ToolPart, 0) + orphanedToolCalls = make([]opencode.ToolInvocationPart, 0) } remaining := true @@ -226,9 +184,9 @@ func (m *messagesComponent) renderView(width int) { // we only want tool calls associated with the current text part. // if we hit another text part, we're done. remaining = false - case opencode.ToolPart: + case opencode.ToolInvocationPart: toolCallParts = append(toolCallParts, part) - if part.State.Status != opencode.ToolPartStateStatusCompleted || part.State.Status != opencode.ToolPartStateStatusError { + if part.ToolInvocation.State != "result" { // i don't think there's a case where a tool call isn't in result state // and the message time is 0, but just in case finished = false @@ -237,18 +195,17 @@ func (m *messagesComponent) renderView(width int) { } if finished { - key := m.cache.GenerateKey(casted.ID, p.Text, width, m.showToolDetails, m.selectedPart == m.partCount) + key := m.cache.GenerateKey(message.ID, p.Text, width, m.showToolDetails, m.selectedPart == m.partCount) content, cached = m.cache.Get(key) if !cached { content = renderText( m.app, message, p.Text, - casted.ModelID, + message.Metadata.Assistant.ModelID, m.showToolDetails, m.partCount == m.selectedPart, width, - "", toolCallParts..., ) m.cache.Set(key, content) @@ -258,11 +215,10 @@ func (m *messagesComponent) renderView(width int) { m.app, message, p.Text, - casted.ModelID, + message.Metadata.Assistant.ModelID, m.showToolDetails, m.partCount == m.selectedPart, width, - "", toolCallParts..., ) } @@ -270,7 +226,7 @@ func (m *messagesComponent) renderView(width int) { m = m.updateSelected(content, p.Text) blocks = append(blocks, content) } - case opencode.ToolPart: + case opencode.ToolInvocationPart: if !m.showToolDetails { if !hasTextPart { orphanedToolCalls = append(orphanedToolCalls, part) @@ -278,9 +234,9 @@ func (m *messagesComponent) renderView(width int) { continue } - if part.State.Status == opencode.ToolPartStateStatusCompleted || part.State.Status == opencode.ToolPartStateStatusError { - key := m.cache.GenerateKey(casted.ID, - part.ID, + if part.ToolInvocation.State == "result" { + key := m.cache.GenerateKey(message.ID, + part.ToolInvocation.ToolCallID, m.showToolDetails, width, m.partCount == m.selectedPart, @@ -290,6 +246,7 @@ func (m *messagesComponent) renderView(width int) { content = renderToolDetails( m.app, part, + message.Metadata, m.partCount == m.selectedPart, width, ) @@ -300,6 +257,7 @@ func (m *messagesComponent) renderView(width int) { content = renderToolDetails( m.app, part, + message.Metadata, m.partCount == m.selectedPart, width, ) @@ -313,18 +271,14 @@ func (m *messagesComponent) renderView(width int) { } error := "" - if assistant, ok := message.(opencode.AssistantMessage); ok { - switch err := assistant.Error.AsUnion().(type) { - case nil: - case opencode.AssistantMessageErrorMessageOutputLengthError: - error = "Message output length exceeded" - case opencode.ProviderAuthError: - error = err.Data.Message - case opencode.MessageAbortedError: - error = "Request was aborted" - case opencode.UnknownError: - error = err.Data.Message - } + switch err := message.Metadata.Error.AsUnion().(type) { + case nil: + case opencode.MessageMetadataErrorMessageOutputLengthError: + error = "Message output length exceeded" + case opencode.ProviderAuthError: + error = err.Data.Message + case opencode.UnknownError: + error = err.Data.Message } if error != "" { diff --git a/packages/tui/internal/components/commands/commands.go b/packages/tui/internal/components/commands/commands.go index 7f293230..f3080b38 100644 --- a/packages/tui/internal/components/commands/commands.go +++ b/packages/tui/internal/components/commands/commands.go @@ -56,8 +56,8 @@ func (c *commandsComponent) View() string { var untriggeredCommands []commands.Command for _, cmd := range c.app.Commands.Sorted() { - if c.showAll || cmd.HasTrigger() { - if cmd.HasTrigger() { + if c.showAll || cmd.Trigger != "" { + if cmd.Trigger != "" { triggeredCommands = append(triggeredCommands, cmd) } else if c.showAll { untriggeredCommands = append(untriggeredCommands, cmd) @@ -97,8 +97,8 @@ func (c *commandsComponent) View() string { for _, cmd := range commandsToShow { trigger := "" - if cmd.HasTrigger() { - trigger = "/" + cmd.PrimaryTrigger() + if cmd.Trigger != "" { + trigger = "/" + cmd.Trigger } else { trigger = string(cmd.Name) } diff --git a/packages/tui/internal/components/dialog/complete.go b/packages/tui/internal/components/dialog/complete.go index 7ba91dc5..caf754c7 100644 --- a/packages/tui/internal/components/dialog/complete.go +++ b/packages/tui/internal/components/dialog/complete.go @@ -64,7 +64,7 @@ type CompletionProvider interface { type CompletionSelectedMsg struct { SearchString string CompletionValue string - ProviderID string + IsCommand bool } type CompletionDialogCompleteItemMsg struct { @@ -121,6 +121,9 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var query string query = c.pseudoSearchTextArea.Value() + if query != "" { + query = query[1:] + } if query != c.query { c.query = query @@ -180,9 +183,8 @@ func (c *completionDialogComponent) View() string { for _, cmd := range completions { title := cmd.DisplayValue() - width := lipgloss.Width(title) - if width > maxWidth-4 { - maxWidth = width + 4 + if len(title) > maxWidth-4 { + maxWidth = len(title) + 4 } } @@ -211,11 +213,14 @@ func (c *completionDialogComponent) IsEmpty() bool { func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd { value := c.pseudoSearchTextArea.Value() + // Check if this is a command completion + isCommand := c.completionProvider.GetId() == "commands" + return tea.Batch( util.CmdHandler(CompletionSelectedMsg{ SearchString: value, CompletionValue: item.GetValue(), - ProviderID: c.completionProvider.GetId(), + IsCommand: isCommand, }), c.close(), ) diff --git a/packages/tui/internal/components/dialog/find.go b/packages/tui/internal/components/dialog/find.go index 3fc6e599..489b9f29 100644 --- a/packages/tui/internal/components/dialog/find.go +++ b/packages/tui/internal/components/dialog/find.go @@ -124,7 +124,7 @@ func (f *findDialogComponent) View() string { f.list.SetMaxWidth(f.width - 4) inputView := f.textInput.View() inputView = styles.NewStyle(). - Background(t.BackgroundElement()). + Background(t.BackgroundPanel()). Height(1). Width(f.width-4). Padding(0, 0). @@ -171,7 +171,7 @@ func (f *findDialogComponent) Close() tea.Cmd { func createTextInput(existing *textinput.Model) textinput.Model { t := theme.CurrentTheme() - bgColor := t.BackgroundElement() + bgColor := t.BackgroundPanel() textColor := t.Text() textMutedColor := t.TextMuted() diff --git a/packages/tui/internal/components/dialog/models.go b/packages/tui/internal/components/dialog/models.go index f8cda82a..4ebf572e 100644 --- a/packages/tui/internal/components/dialog/models.go +++ b/packages/tui/internal/components/dialog/models.go @@ -56,24 +56,24 @@ func (m ModelItem) Render(selected bool, width int) string { displayText := fmt.Sprintf("%s (%s)", m.ModelName, m.ProviderName) return styles.NewStyle(). Background(t.Primary()). - Foreground(t.BackgroundPanel()). + Foreground(t.BackgroundElement()). Width(width). PaddingLeft(1). Render(displayText) } else { modelStyle := styles.NewStyle(). Foreground(t.Text()). - Background(t.BackgroundPanel()) + Background(t.BackgroundElement()) providerStyle := styles.NewStyle(). Foreground(t.TextMuted()). - Background(t.BackgroundPanel()) + Background(t.BackgroundElement()) modelPart := modelStyle.Render(m.ModelName) providerPart := providerStyle.Render(fmt.Sprintf(" (%s)", m.ProviderName)) combinedText := modelPart + providerPart return styles.NewStyle(). - Background(t.BackgroundPanel()). + Background(t.BackgroundElement()). PaddingLeft(1). Render(combinedText) } diff --git a/packages/tui/internal/components/list/list.go b/packages/tui/internal/components/list/list.go index 16bc73ca..a7ea3458 100644 --- a/packages/tui/internal/components/list/list.go +++ b/packages/tui/internal/components/list/list.go @@ -158,12 +158,7 @@ func (c *listComponent[T]) View() string { return strings.Join(listItems, "\n") } -func NewListComponent[T ListItem]( - items []T, - maxVisibleItems int, - fallbackMsg string, - useAlphaNumericKeys bool, -) List[T] { +func NewListComponent[T ListItem](items []T, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[T] { return &listComponent[T]{ fallbackMsg: fallbackMsg, items: items, @@ -199,12 +194,7 @@ func (s StringItem) Render(selected bool, width int) string { } // NewStringList creates a new list component with string items -func NewStringList( - items []string, - maxVisibleItems int, - fallbackMsg string, - useAlphaNumericKeys bool, -) List[StringItem] { +func NewStringList(items []string, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[StringItem] { stringItems := make([]StringItem, len(items)) for i, item := range items { stringItems[i] = StringItem(item) diff --git a/packages/tui/internal/components/modal/modal.go b/packages/tui/internal/components/modal/modal.go index 5c2fbf8b..aa81a83e 100644 --- a/packages/tui/internal/components/modal/modal.go +++ b/packages/tui/internal/components/modal/modal.go @@ -90,7 +90,7 @@ func (m *Modal) Render(contentView string, background string) string { innerWidth := outerWidth - 4 - baseStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundPanel()) + baseStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundElement()) var finalContent string if m.title != "" { @@ -140,6 +140,6 @@ func (m *Modal) Render(contentView string, background string) string { modalView, background, layout.WithOverlayBorder(), - layout.WithOverlayBorderColor(t.Primary()), + layout.WithOverlayBorderColor(t.BorderActive()), ) } diff --git a/packages/tui/internal/components/qr/qr.go b/packages/tui/internal/components/qr/qr.go index 233bcf52..ccf28200 100644 --- a/packages/tui/internal/components/qr/qr.go +++ b/packages/tui/internal/components/qr/qr.go @@ -23,7 +23,7 @@ func Generate(text string) (string, int, error) { } // Create lipgloss style for QR code with theme colors - qrStyle := styles.NewStyle().Foreground(t.Text()).Background(t.Background()) + qrStyle := styles.NewStyleWithColors(t.Text(), t.Background()) var result strings.Builder diff --git a/packages/tui/internal/components/status/status.go b/packages/tui/internal/components/status/status.go index 7d5820de..791267e0 100644 --- a/packages/tui/internal/components/status/status.go +++ b/packages/tui/internal/components/status/status.go @@ -6,7 +6,6 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" - "github.com/sst/opencode-sdk-go" "github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" @@ -102,20 +101,18 @@ func (m statusComponent) View() string { contextWindow := m.app.Model.Limit.Context for _, message := range m.app.Messages { - if assistant, ok := message.(opencode.AssistantMessage); ok { - cost += assistant.Cost - usage := assistant.Tokens - if usage.Output > 0 { - if assistant.Summary { - tokens = usage.Output - continue - } - tokens = (usage.Input + - usage.Cache.Write + - usage.Cache.Read + - usage.Output + - usage.Reasoning) + cost += message.Metadata.Assistant.Cost + usage := message.Metadata.Assistant.Tokens + if usage.Output > 0 { + if message.Metadata.Assistant.Summary { + tokens = usage.Output + continue } + tokens = (usage.Input + + usage.Cache.Write + + usage.Cache.Read + + usage.Output + + usage.Reasoning) } } diff --git a/packages/tui/internal/components/textarea/textarea.go b/packages/tui/internal/components/textarea/textarea.go index 41a4a3d9..2ca08bb8 100644 --- a/packages/tui/internal/components/textarea/textarea.go +++ b/packages/tui/internal/components/textarea/textarea.go @@ -9,8 +9,7 @@ import ( "time" "unicode" - "slices" - + "github.com/atotto/clipboard" "github.com/charmbracelet/bubbles/v2/cursor" "github.com/charmbracelet/bubbles/v2/key" tea "github.com/charmbracelet/bubbletea/v2" @@ -18,6 +17,7 @@ import ( "github.com/charmbracelet/x/ansi" rw "github.com/mattn/go-runewidth" "github.com/rivo/uniseg" + "slices" ) const ( @@ -32,145 +32,6 @@ const ( maxLines = 10000 ) -// Attachment represents a special object within the text, distinct from regular characters. -type Attachment struct { - ID string // A unique identifier for this attachment instance - Display string // e.g., "@filename.txt" - URL string - Filename string - MediaType string -} - -// Helper functions for converting between runes and any slices - -// runesToInterfaces converts a slice of runes to a slice of interfaces -func runesToInterfaces(runes []rune) []any { - result := make([]any, len(runes)) - for i, r := range runes { - result[i] = r - } - return result -} - -// interfacesToRunes converts a slice of interfaces to a slice of runes (for display purposes) -func interfacesToRunes(items []any) []rune { - var result []rune - for _, item := range items { - switch val := item.(type) { - case rune: - result = append(result, val) - case *Attachment: - result = append(result, []rune(val.Display)...) - } - } - return result -} - -// copyInterfaceSlice creates a copy of an any slice -func copyInterfaceSlice(src []any) []any { - dst := make([]any, len(src)) - copy(dst, src) - return dst -} - -// interfacesToString converts a slice of interfaces to a string for display -func interfacesToString(items []any) string { - var s strings.Builder - for _, item := range items { - switch val := item.(type) { - case rune: - s.WriteRune(val) - case *Attachment: - s.WriteString(val.Display) - } - } - return s.String() -} - -// isAttachmentAtCursor checks if the cursor is positioned on or immediately after an attachment. -// This allows for proper highlighting even when the cursor is technically at the position -// after the attachment object in the underlying slice. -func (m Model) isAttachmentAtCursor() (*Attachment, int, int) { - if m.row >= len(m.value) { - return nil, -1, -1 - } - - row := m.value[m.row] - col := m.col - - if col < 0 || col > len(row) { - return nil, -1, -1 - } - - // Check if the cursor is at the same index as an attachment. - if col < len(row) { - if att, ok := row[col].(*Attachment); ok { - return att, col, col - } - } - - // Check if the cursor is immediately after an attachment. This is a common - // state, for example, after just inserting one. - if col > 0 && col <= len(row) { - if att, ok := row[col-1].(*Attachment); ok { - return att, col - 1, col - 1 - } - } - - return nil, -1, -1 -} - -// renderLineWithAttachments renders a line with proper attachment highlighting -func (m Model) renderLineWithAttachments( - items []any, - style lipgloss.Style, -) string { - var s strings.Builder - currentAttachment, _, _ := m.isAttachmentAtCursor() - - for _, item := range items { - switch val := item.(type) { - case rune: - s.WriteString(style.Render(string(val))) - case *Attachment: - // Check if this is the attachment the cursor is currently on - if currentAttachment != nil && currentAttachment.ID == val.ID { - // Cursor is on this attachment, highlight it - s.WriteString(m.Styles.SelectedAttachment.Render(val.Display)) - } else { - s.WriteString(m.Styles.Attachment.Render(val.Display)) - } - } - } - return s.String() -} - -// getRuneAt safely gets a rune at a specific position, returns 0 if not a rune -func getRuneAt(items []any, index int) rune { - if index < 0 || index >= len(items) { - return 0 - } - if r, ok := items[index].(rune); ok { - return r - } - return 0 -} - -// isSpaceAt checks if the item at index is a space rune -func isSpaceAt(items []any, index int) bool { - r := getRuneAt(items, index) - return r != 0 && unicode.IsSpace(r) -} - -// setRuneAt safely sets a rune at a specific position if it's a rune -func setRuneAt(items []any, index int, r rune) { - if index >= 0 && index < len(items) { - if _, ok := items[index].(rune); ok { - items[index] = r - } - } -} - // Internal messages for clipboard operations. type ( pasteMsg string @@ -209,96 +70,30 @@ type KeyMap struct { // upon the textarea. func DefaultKeyMap() KeyMap { return KeyMap{ - CharacterForward: key.NewBinding( - key.WithKeys("right", "ctrl+f"), - key.WithHelp("right", "character forward"), - ), - CharacterBackward: key.NewBinding( - key.WithKeys("left", "ctrl+b"), - key.WithHelp("left", "character backward"), - ), - WordForward: key.NewBinding( - key.WithKeys("alt+right", "alt+f"), - key.WithHelp("alt+right", "word forward"), - ), - WordBackward: key.NewBinding( - key.WithKeys("alt+left", "alt+b"), - key.WithHelp("alt+left", "word backward"), - ), - LineNext: key.NewBinding( - key.WithKeys("down", "ctrl+n"), - key.WithHelp("down", "next line"), - ), - LinePrevious: key.NewBinding( - key.WithKeys("up", "ctrl+p"), - key.WithHelp("up", "previous line"), - ), - DeleteWordBackward: key.NewBinding( - key.WithKeys("alt+backspace", "ctrl+w"), - key.WithHelp("alt+backspace", "delete word backward"), - ), - DeleteWordForward: key.NewBinding( - key.WithKeys("alt+delete", "alt+d"), - key.WithHelp("alt+delete", "delete word forward"), - ), - DeleteAfterCursor: key.NewBinding( - key.WithKeys("ctrl+k"), - key.WithHelp("ctrl+k", "delete after cursor"), - ), - DeleteBeforeCursor: key.NewBinding( - key.WithKeys("ctrl+u"), - key.WithHelp("ctrl+u", "delete before cursor"), - ), - InsertNewline: key.NewBinding( - key.WithKeys("enter", "ctrl+m"), - key.WithHelp("enter", "insert newline"), - ), - DeleteCharacterBackward: key.NewBinding( - key.WithKeys("backspace", "ctrl+h"), - key.WithHelp("backspace", "delete character backward"), - ), - DeleteCharacterForward: key.NewBinding( - key.WithKeys("delete", "ctrl+d"), - key.WithHelp("delete", "delete character forward"), - ), - LineStart: key.NewBinding( - key.WithKeys("home", "ctrl+a"), - key.WithHelp("home", "line start"), - ), - LineEnd: key.NewBinding( - key.WithKeys("end", "ctrl+e"), - key.WithHelp("end", "line end"), - ), - Paste: key.NewBinding( - key.WithKeys("ctrl+v"), - key.WithHelp("ctrl+v", "paste"), - ), - InputBegin: key.NewBinding( - key.WithKeys("alt+<", "ctrl+home"), - key.WithHelp("alt+<", "input begin"), - ), - InputEnd: key.NewBinding( - key.WithKeys("alt+>", "ctrl+end"), - key.WithHelp("alt+>", "input end"), - ), + CharacterForward: key.NewBinding(key.WithKeys("right", "ctrl+f"), key.WithHelp("right", "character forward")), + CharacterBackward: key.NewBinding(key.WithKeys("left", "ctrl+b"), key.WithHelp("left", "character backward")), + WordForward: key.NewBinding(key.WithKeys("alt+right", "alt+f"), key.WithHelp("alt+right", "word forward")), + WordBackward: key.NewBinding(key.WithKeys("alt+left", "alt+b"), key.WithHelp("alt+left", "word backward")), + LineNext: key.NewBinding(key.WithKeys("down", "ctrl+n"), key.WithHelp("down", "next line")), + LinePrevious: key.NewBinding(key.WithKeys("up", "ctrl+p"), key.WithHelp("up", "previous line")), + DeleteWordBackward: key.NewBinding(key.WithKeys("alt+backspace", "ctrl+w"), key.WithHelp("alt+backspace", "delete word backward")), + DeleteWordForward: key.NewBinding(key.WithKeys("alt+delete", "alt+d"), key.WithHelp("alt+delete", "delete word forward")), + DeleteAfterCursor: key.NewBinding(key.WithKeys("ctrl+k"), key.WithHelp("ctrl+k", "delete after cursor")), + DeleteBeforeCursor: key.NewBinding(key.WithKeys("ctrl+u"), key.WithHelp("ctrl+u", "delete before cursor")), + InsertNewline: key.NewBinding(key.WithKeys("enter", "ctrl+m"), key.WithHelp("enter", "insert newline")), + DeleteCharacterBackward: key.NewBinding(key.WithKeys("backspace", "ctrl+h"), key.WithHelp("backspace", "delete character backward")), + DeleteCharacterForward: key.NewBinding(key.WithKeys("delete", "ctrl+d"), key.WithHelp("delete", "delete character forward")), + LineStart: key.NewBinding(key.WithKeys("home", "ctrl+a"), key.WithHelp("home", "line start")), + LineEnd: key.NewBinding(key.WithKeys("end", "ctrl+e"), key.WithHelp("end", "line end")), + Paste: key.NewBinding(key.WithKeys("ctrl+v"), key.WithHelp("ctrl+v", "paste")), + InputBegin: key.NewBinding(key.WithKeys("alt+<", "ctrl+home"), key.WithHelp("alt+<", "input begin")), + InputEnd: key.NewBinding(key.WithKeys("alt+>", "ctrl+end"), key.WithHelp("alt+>", "input end")), - CapitalizeWordForward: key.NewBinding( - key.WithKeys("alt+c"), - key.WithHelp("alt+c", "capitalize word forward"), - ), - LowercaseWordForward: key.NewBinding( - key.WithKeys("alt+l"), - key.WithHelp("alt+l", "lowercase word forward"), - ), - UppercaseWordForward: key.NewBinding( - key.WithKeys("alt+u"), - key.WithHelp("alt+u", "uppercase word forward"), - ), + CapitalizeWordForward: key.NewBinding(key.WithKeys("alt+c"), key.WithHelp("alt+c", "capitalize word forward")), + LowercaseWordForward: key.NewBinding(key.WithKeys("alt+l"), key.WithHelp("alt+l", "lowercase word forward")), + UppercaseWordForward: key.NewBinding(key.WithKeys("alt+u"), key.WithHelp("alt+u", "uppercase word forward")), - TransposeCharacterBackward: key.NewBinding( - key.WithKeys("ctrl+t"), - key.WithHelp("ctrl+t", "transpose character backward"), - ), + TransposeCharacterBackward: key.NewBinding(key.WithKeys("ctrl+t"), key.WithHelp("ctrl+t", "transpose character backward")), } } @@ -365,11 +160,9 @@ type CursorStyle struct { // states. The appropriate styles will be chosen based on the focus state of // the textarea. type Styles struct { - Focused StyleState - Blurred StyleState - Cursor CursorStyle - Attachment lipgloss.Style - SelectedAttachment lipgloss.Style + Focused StyleState + Blurred StyleState + Cursor CursorStyle } // StyleState that will be applied to the text area. @@ -424,22 +217,13 @@ func (s StyleState) computedText() lipgloss.Style { // line is the input to the text wrapping function. This is stored in a struct // so that it can be hashed and memoized. type line struct { - content []any // Contains runes and *Attachment - width int + runes []rune + width int } // Hash returns a hash of the line. func (w line) Hash() string { - var s strings.Builder - for _, item := range w.content { - switch v := item.(type) { - case rune: - s.WriteRune(v) - case *Attachment: - s.WriteString(v.ID) - } - } - v := fmt.Sprintf("%s:%d", s.String(), w.width) + v := fmt.Sprintf("%s:%d", string(w.runes), w.width) return fmt.Sprintf("%x", sha256.Sum256([]byte(v))) } @@ -448,7 +232,7 @@ type Model struct { Err error // General settings. - cache *MemoCache[line, [][]any] + cache *MemoCache[line, [][]rune] // Prompt is printed at the beginning of each line. // @@ -511,14 +295,14 @@ type Model struct { // if there are more lines than the permitted height. height int - // Underlying text value. Contains either rune or *Attachment types. - value [][]any + // Underlying text value. + value [][]rune // focus indicates whether user input focus should be on this input // component. When false, ignore keyboard input and hide the cursor. focus bool - // Cursor column (slice index). + // Cursor column. col int // Cursor row. @@ -544,14 +328,14 @@ func New() Model { MaxWidth: defaultMaxWidth, Prompt: lipgloss.ThickBorder().Left + " ", Styles: styles, - cache: NewMemoCache[line, [][]any](maxLines), + cache: NewMemoCache[line, [][]rune](maxLines), EndOfBufferCharacter: ' ', ShowLineNumbers: true, VirtualCursor: true, virtualCursor: cur, KeyMap: DefaultKeyMap(), - value: make([][]any, minHeight, maxLines), + value: make([][]rune, minHeight, maxLines), focus: false, col: 0, row: 0, @@ -570,40 +354,25 @@ func DefaultStyles(isDark bool) Styles { var s Styles s.Focused = StyleState{ - Base: lipgloss.NewStyle(), - CursorLine: lipgloss.NewStyle(). - Background(lightDark(lipgloss.Color("255"), lipgloss.Color("0"))), - CursorLineNumber: lipgloss.NewStyle(). - Foreground(lightDark(lipgloss.Color("240"), lipgloss.Color("240"))), - EndOfBuffer: lipgloss.NewStyle(). - Foreground(lightDark(lipgloss.Color("254"), lipgloss.Color("0"))), - LineNumber: lipgloss.NewStyle(). - Foreground(lightDark(lipgloss.Color("249"), lipgloss.Color("7"))), - Placeholder: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), - Prompt: lipgloss.NewStyle().Foreground(lipgloss.Color("7")), - Text: lipgloss.NewStyle(), + Base: lipgloss.NewStyle(), + CursorLine: lipgloss.NewStyle().Background(lightDark(lipgloss.Color("255"), lipgloss.Color("0"))), + CursorLineNumber: lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("240"), lipgloss.Color("240"))), + EndOfBuffer: lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("254"), lipgloss.Color("0"))), + LineNumber: lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("249"), lipgloss.Color("7"))), + Placeholder: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), + Prompt: lipgloss.NewStyle().Foreground(lipgloss.Color("7")), + Text: lipgloss.NewStyle(), } s.Blurred = StyleState{ - Base: lipgloss.NewStyle(), - CursorLine: lipgloss.NewStyle(). - Foreground(lightDark(lipgloss.Color("245"), lipgloss.Color("7"))), - CursorLineNumber: lipgloss.NewStyle(). - Foreground(lightDark(lipgloss.Color("249"), lipgloss.Color("7"))), - EndOfBuffer: lipgloss.NewStyle(). - Foreground(lightDark(lipgloss.Color("254"), lipgloss.Color("0"))), - LineNumber: lipgloss.NewStyle(). - Foreground(lightDark(lipgloss.Color("249"), lipgloss.Color("7"))), - Placeholder: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), - Prompt: lipgloss.NewStyle().Foreground(lipgloss.Color("7")), - Text: lipgloss.NewStyle(). - Foreground(lightDark(lipgloss.Color("245"), lipgloss.Color("7"))), + Base: lipgloss.NewStyle(), + CursorLine: lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("245"), lipgloss.Color("7"))), + CursorLineNumber: lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("249"), lipgloss.Color("7"))), + EndOfBuffer: lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("254"), lipgloss.Color("0"))), + LineNumber: lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("249"), lipgloss.Color("7"))), + Placeholder: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), + Prompt: lipgloss.NewStyle().Foreground(lipgloss.Color("7")), + Text: lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("245"), lipgloss.Color("7"))), } - s.Attachment = lipgloss.NewStyle(). - Background(lipgloss.Color("11")). - Foreground(lipgloss.Color("0")) - s.SelectedAttachment = lipgloss.NewStyle(). - Background(lipgloss.Color("11")). - Foreground(lipgloss.Color("0")) s.Cursor = CursorStyle{ Color: lipgloss.Color("7"), Shape: tea.CursorBlock, @@ -652,85 +421,16 @@ func (m *Model) SetValue(s string) { // InsertString inserts a string at the cursor position. func (m *Model) InsertString(s string) { - m.InsertRunesFromUserInput([]rune(s)) + m.insertRunesFromUserInput([]rune(s)) } // InsertRune inserts a rune at the cursor position. func (m *Model) InsertRune(r rune) { - m.InsertRunesFromUserInput([]rune{r}) + m.insertRunesFromUserInput([]rune{r}) } -// InsertAttachment inserts an attachment at the cursor position. -func (m *Model) InsertAttachment(att *Attachment) { - if m.CharLimit > 0 { - availSpace := m.CharLimit - m.Length() - // If the char limit's been reached, cancel. - if availSpace <= 0 { - return - } - } - - // Insert the attachment at the current cursor position - m.value[m.row] = append( - m.value[m.row][:m.col], - append([]any{att}, m.value[m.row][m.col:]...)...) - m.col++ - m.SetCursorColumn(m.col) -} - -// ReplaceRange replaces text from startCol to endCol on the current row with the given string. -// This preserves attachments outside the replaced range. -func (m *Model) ReplaceRange(startCol, endCol int, replacement string) { - if m.row >= len(m.value) || startCol < 0 || endCol < startCol { - return - } - - // Ensure bounds are within the current row - rowLen := len(m.value[m.row]) - startCol = max(0, min(startCol, rowLen)) - endCol = max(startCol, min(endCol, rowLen)) - - // Create new row content: before + replacement + after - before := m.value[m.row][:startCol] - after := m.value[m.row][endCol:] - replacementRunes := runesToInterfaces([]rune(replacement)) - - // Combine the parts - newRow := make([]any, 0, len(before)+len(replacementRunes)+len(after)) - newRow = append(newRow, before...) - newRow = append(newRow, replacementRunes...) - newRow = append(newRow, after...) - - m.value[m.row] = newRow - - // Position cursor at end of replacement - m.col = startCol + len(replacementRunes) - m.SetCursorColumn(m.col) -} - -// CurrentRowLength returns the length of the current row. -func (m *Model) CurrentRowLength() int { - if m.row >= len(m.value) { - return 0 - } - return len(m.value[m.row]) -} - -// GetAttachments returns all attachments in the textarea. -func (m Model) GetAttachments() []*Attachment { - var attachments []*Attachment - for _, row := range m.value { - for _, item := range row { - if att, ok := item.(*Attachment); ok { - attachments = append(attachments, att) - } - } - } - return attachments -} - -// InsertRunesFromUserInput inserts runes at the current cursor position. -func (m *Model) InsertRunesFromUserInput(runes []rune) { +// insertRunesFromUserInput inserts runes at the current cursor position. +func (m *Model) insertRunesFromUserInput(runes []rune) { // Clean up any special characters in the input provided by the // clipboard. This avoids bugs due to e.g. tab characters and // whatnot. @@ -781,22 +481,23 @@ func (m *Model) InsertRunesFromUserInput(runes []rune) { // Save the remainder of the original line at the current // cursor position. - tail := copyInterfaceSlice(m.value[m.row][m.col:]) + tail := make([]rune, len(m.value[m.row][m.col:])) + copy(tail, m.value[m.row][m.col:]) // Paste the first line at the current cursor position. - m.value[m.row] = append(m.value[m.row][:m.col], runesToInterfaces(lines[0])...) + m.value[m.row] = append(m.value[m.row][:m.col], lines[0]...) m.col += len(lines[0]) if numExtraLines := len(lines) - 1; numExtraLines > 0 { // Add the new lines. // We try to reuse the slice if there's already space. - var newGrid [][]any + var newGrid [][]rune if cap(m.value) >= len(m.value)+numExtraLines { // Can reuse the extra space. newGrid = m.value[:len(m.value)+numExtraLines] } else { // No space left; need a new slice. - newGrid = make([][]any, len(m.value)+numExtraLines) + newGrid = make([][]rune, len(m.value)+numExtraLines) copy(newGrid, m.value[:m.row+1]) } // Add all the rows that were after the cursor in the original @@ -806,7 +507,7 @@ func (m *Model) InsertRunesFromUserInput(runes []rune) { // Insert all the new lines in the middle. for _, l := range lines[1:] { m.row++ - m.value[m.row] = runesToInterfaces(l) + m.value[m.row] = l m.col = len(l) } } @@ -825,14 +526,7 @@ func (m Model) Value() string { var v strings.Builder for _, l := range m.value { - for _, item := range l { - switch val := item.(type) { - case rune: - v.WriteRune(val) - case *Attachment: - v.WriteString(val.Display) - } - } + v.WriteString(string(l)) v.WriteByte('\n') } @@ -843,14 +537,7 @@ func (m Model) Value() string { func (m *Model) Length() int { var l int for _, row := range m.value { - for _, item := range row { - switch val := item.(type) { - case rune: - l += rw.RuneWidth(val) - case *Attachment: - l += uniseg.StringWidth(val.Display) - } - } + l += uniseg.StringWidth(string(row)) } // We add len(m.value) to include the newline characters. return l + len(m.value) - 1 @@ -866,29 +553,6 @@ func (m Model) Line() int { return m.row } -// CursorColumn returns the cursor's column position (slice index). -func (m Model) CursorColumn() int { - return m.col -} - -// LastRuneIndex returns the index of the last occurrence of a rune on the current line, -// searching backwards from the current cursor position. -// Returns -1 if the rune is not found before the cursor. -func (m Model) LastRuneIndex(r rune) int { - if m.row >= len(m.value) { - return -1 - } - // Iterate backwards from just before the cursor position - for i := m.col - 1; i >= 0; i-- { - if i < len(m.value[m.row]) { - if item, ok := m.value[m.row][i].(rune); ok && item == r { - return i - } - } - } - return -1 -} - func (m *Model) Newline() { if m.MaxHeight > 0 && len(m.value) >= m.MaxHeight { return @@ -897,39 +561,6 @@ func (m *Model) Newline() { m.splitLine(m.row, m.col) } -// mapVisualOffsetToSliceIndex converts a visual column offset to a slice index. -// This is used to maintain the cursor's horizontal position when moving vertically. -func (m *Model) mapVisualOffsetToSliceIndex(row int, charOffset int) int { - if row < 0 || row >= len(m.value) { - return 0 - } - - offset := 0 - // Find the slice index that corresponds to the visual offset. - for i, item := range m.value[row] { - var itemWidth int - switch v := item.(type) { - case rune: - itemWidth = rw.RuneWidth(v) - case *Attachment: - itemWidth = uniseg.StringWidth(v.Display) - } - - // If the target offset falls within the current item, this is our index. - if offset+itemWidth > charOffset { - // Decide whether to stick with the previous index or move to the current - // one based on which is closer to the target offset. - if (charOffset - offset) > ((offset + itemWidth) - charOffset) { - return i + 1 - } - return i - } - offset += itemWidth - } - - return len(m.value[row]) -} - // CursorDown moves the cursor down by one line. // Returns whether or not the cursor blink should be reset. func (m *Model) CursorDown() { @@ -938,15 +569,31 @@ func (m *Model) CursorDown() { m.lastCharOffset = charOffset if li.RowOffset+1 >= li.Height && m.row < len(m.value)-1 { - // Move to the next model line m.row++ - m.col = m.mapVisualOffsetToSliceIndex(m.row, charOffset) - } else if li.RowOffset+1 < li.Height { - // Move to the next wrapped line within the same model line - startOfNextWrappedLine := li.StartColumn + li.Width - m.col = startOfNextWrappedLine + m.mapVisualOffsetToSliceIndex(m.row, charOffset) + m.col = 0 + } else { + // Move the cursor to the start of the next line so that we can get + // the line information. We need to add 2 columns to account for the + // trailing space wrapping. + const trailingSpace = 2 + m.col = min(li.StartColumn+li.Width+trailingSpace, len(m.value[m.row])-1) + } + + nli := m.LineInfo() + m.col = nli.StartColumn + + if nli.Width <= 0 { + return + } + + offset := 0 + for offset < charOffset { + if m.row >= len(m.value) || m.col >= len(m.value[m.row]) || offset >= nli.CharWidth-1 { + break + } + offset += rw.RuneWidth(m.value[m.row][m.col]) + m.col++ } - m.SetCursorColumn(m.col) } // CursorUp moves the cursor up by one line. @@ -956,24 +603,32 @@ func (m *Model) CursorUp() { m.lastCharOffset = charOffset if li.RowOffset <= 0 && m.row > 0 { - // Move to the previous model line m.row-- - m.col = m.mapVisualOffsetToSliceIndex(m.row, charOffset) - } else if li.RowOffset > 0 { - // Move to the previous wrapped line within the same model line - // To do this, we need to find the start of the previous wrapped line. - prevLineInfo := m.LineInfo() - // prevLineStart := 0 - if prevLineInfo.RowOffset > 0 { - // This is complex, so we'll approximate by moving to the start of the current wrapped line - // and then letting characterLeft handle it. A more precise calculation would - // require re-wrapping to find the previous line's start. - // For now, a simpler approach: - m.col = li.StartColumn - 1 - } - m.col = m.mapVisualOffsetToSliceIndex(m.row, charOffset) + m.col = len(m.value[m.row]) + } else { + // Move the cursor to the end of the previous line. + // This can be done by moving the cursor to the start of the line and + // then subtracting 2 to account for the trailing space we keep on + // soft-wrapped lines. + const trailingSpace = 2 + m.col = li.StartColumn - trailingSpace + } + + nli := m.LineInfo() + m.col = nli.StartColumn + + if nli.Width <= 0 { + return + } + + offset := 0 + for offset < charOffset { + if m.col >= len(m.value[m.row]) || offset >= nli.CharWidth-1 { + break + } + offset += rw.RuneWidth(m.value[m.row][m.col]) + m.col++ } - m.SetCursorColumn(m.col) } // SetCursorColumn moves the cursor to the given position. If the position is @@ -1025,7 +680,7 @@ func (m *Model) Blur() { // Reset sets the input to its default state with no input. func (m *Model) Reset() { - m.value = make([][]any, minHeight, maxLines) + m.value = make([][]rune, minHeight, maxLines) m.col = 0 m.row = 0 m.SetCursorColumn(0) @@ -1086,7 +741,7 @@ func (m *Model) deleteWordLeft() { oldCol := m.col //nolint:ifshort m.SetCursorColumn(m.col - 1) - for isSpaceAt(m.value[m.row], m.col) { + for unicode.IsSpace(m.value[m.row][m.col]) { if m.col <= 0 { break } @@ -1095,7 +750,7 @@ func (m *Model) deleteWordLeft() { } for m.col > 0 { - if !isSpaceAt(m.value[m.row], m.col) { + if !unicode.IsSpace(m.value[m.row][m.col]) { m.SetCursorColumn(m.col - 1) } else { if m.col > 0 { @@ -1121,13 +776,13 @@ func (m *Model) deleteWordRight() { oldCol := m.col - for m.col < len(m.value[m.row]) && isSpaceAt(m.value[m.row], m.col) { + for m.col < len(m.value[m.row]) && unicode.IsSpace(m.value[m.row][m.col]) { // ignore series of whitespace after cursor m.SetCursorColumn(m.col + 1) } for m.col < len(m.value[m.row]) { - if !isSpaceAt(m.value[m.row], m.col) { + if !unicode.IsSpace(m.value[m.row][m.col]) { m.SetCursorColumn(m.col + 1) } else { break @@ -1177,13 +832,13 @@ func (m *Model) characterLeft(insideLine bool) { func (m *Model) wordLeft() { for { m.characterLeft(true /* insideLine */) - if m.col < len(m.value[m.row]) && !isSpaceAt(m.value[m.row], m.col) { + if m.col < len(m.value[m.row]) && !unicode.IsSpace(m.value[m.row][m.col]) { break } } for m.col > 0 { - if isSpaceAt(m.value[m.row], m.col-1) { + if unicode.IsSpace(m.value[m.row][m.col-1]) { break } m.SetCursorColumn(m.col - 1) @@ -1199,7 +854,7 @@ func (m *Model) wordRight() { func (m *Model) doWordRight(fn func(charIdx int, pos int)) { // Skip spaces forward. - for m.col >= len(m.value[m.row]) || isSpaceAt(m.value[m.row], m.col) { + for m.col >= len(m.value[m.row]) || unicode.IsSpace(m.value[m.row][m.col]) { if m.row == len(m.value)-1 && m.col == len(m.value[m.row]) { // End of text. break @@ -1209,7 +864,7 @@ func (m *Model) doWordRight(fn func(charIdx int, pos int)) { charIdx := 0 for m.col < len(m.value[m.row]) { - if isSpaceAt(m.value[m.row], m.col) { + if unicode.IsSpace(m.value[m.row][m.col]) { break } fn(charIdx, m.col) @@ -1221,18 +876,14 @@ func (m *Model) doWordRight(fn func(charIdx int, pos int)) { // uppercaseRight changes the word to the right to uppercase. func (m *Model) uppercaseRight() { m.doWordRight(func(_ int, i int) { - if r, ok := m.value[m.row][i].(rune); ok { - m.value[m.row][i] = unicode.ToUpper(r) - } + m.value[m.row][i] = unicode.ToUpper(m.value[m.row][i]) }) } // lowercaseRight changes the word to the right to lowercase. func (m *Model) lowercaseRight() { m.doWordRight(func(_ int, i int) { - if r, ok := m.value[m.row][i].(rune); ok { - m.value[m.row][i] = unicode.ToLower(r) - } + m.value[m.row][i] = unicode.ToLower(m.value[m.row][i]) }) } @@ -1240,9 +891,7 @@ func (m *Model) lowercaseRight() { func (m *Model) capitalizeRight() { m.doWordRight(func(charIdx int, i int) { if charIdx == 0 { - if r, ok := m.value[m.row][i].(rune); ok { - m.value[m.row][i] = unicode.ToTitle(r) - } + m.value[m.row][i] = unicode.ToTitle(m.value[m.row][i]) } }) } @@ -1256,39 +905,34 @@ func (m Model) LineInfo() LineInfo { // m.col and counting the number of runes that we need to skip. var counter int for i, line := range grid { - start := counter - end := counter + len(line) - - if m.col >= start && m.col <= end { - // This is the wrapped line the cursor is on. - - // Special case: if the cursor is at the end of a wrapped line, - // and there's another wrapped line after it, the cursor should - // be considered at the beginning of the next line. - if m.col == end && i < len(grid)-1 { - nextLine := grid[i+1] - return LineInfo{ - CharOffset: 0, - ColumnOffset: 0, - Height: len(grid), - RowOffset: i + 1, - StartColumn: end, - Width: len(nextLine), - CharWidth: uniseg.StringWidth(interfacesToString(nextLine)), - } - } - + // We've found the line that we are on + if counter+len(line) == m.col && i+1 < len(grid) { + // We wrap around to the next line if we are at the end of the + // previous line so that we can be at the very beginning of the row return LineInfo{ - CharOffset: uniseg.StringWidth(interfacesToString(line[:max(0, m.col-start)])), - ColumnOffset: m.col - start, + CharOffset: 0, + ColumnOffset: 0, Height: len(grid), - RowOffset: i, - StartColumn: start, - Width: len(line), - CharWidth: uniseg.StringWidth(interfacesToString(line)), + RowOffset: i + 1, + StartColumn: m.col, + Width: len(grid[i+1]), + CharWidth: uniseg.StringWidth(string(line)), } } - counter = end + + if counter+len(line) >= m.col { + return LineInfo{ + CharOffset: uniseg.StringWidth(string(line[:max(0, m.col-counter)])), + ColumnOffset: m.col - counter, + Height: len(grid), + RowOffset: i, + StartColumn: counter, + Width: len(line), + CharWidth: uniseg.StringWidth(string(line)), + } + } + + counter += len(line) } return LineInfo{} } @@ -1416,18 +1060,17 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { var cmds []tea.Cmd - if m.row >= len(m.value) { - m.value = append(m.value, make([]any, 0)) - } if m.value[m.row] == nil { - m.value[m.row] = make([]any, 0) + m.value[m.row] = make([]rune, 0) } if m.MaxHeight > 0 && m.MaxHeight != m.cache.Capacity() { - m.cache = NewMemoCache[line, [][]any](m.MaxHeight) + m.cache = NewMemoCache[line, [][]rune](m.MaxHeight) } switch msg := msg.(type) { + case tea.PasteMsg: + m.insertRunesFromUserInput([]rune(msg)) case tea.KeyPressMsg: switch { case key.Matches(msg, m.KeyMap.DeleteAfterCursor): @@ -1450,9 +1093,11 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { m.mergeLineAbove(m.row) break } - if len(m.value[m.row]) > 0 && m.col > 0 { - m.value[m.row] = slices.Delete(m.value[m.row], m.col-1, m.col) - m.SetCursorColumn(m.col - 1) + if len(m.value[m.row]) > 0 { + m.value[m.row] = append(m.value[m.row][:max(0, m.col-1)], m.value[m.row][m.col:]...) + if m.col > 0 { + m.SetCursorColumn(m.col - 1) + } } case key.Matches(msg, m.KeyMap.DeleteCharacterForward): if len(m.value[m.row]) > 0 && m.col < len(m.value[m.row]) { @@ -1487,6 +1132,8 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { m.CursorDown() case key.Matches(msg, m.KeyMap.WordForward): m.wordRight() + case key.Matches(msg, m.KeyMap.Paste): + return m, Paste case key.Matches(msg, m.KeyMap.CharacterBackward): m.characterLeft(false /* insideLine */) case key.Matches(msg, m.KeyMap.LinePrevious): @@ -1507,11 +1154,11 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { m.transposeLeft() default: - m.InsertRunesFromUserInput([]rune(msg.Text)) + m.insertRunesFromUserInput([]rune(msg.Text)) } case pasteMsg: - m.InsertRunesFromUserInput([]rune(msg)) + m.insertRunesFromUserInput([]rune(msg)) case pasteErrMsg: m.Err = msg @@ -1579,8 +1226,7 @@ func (m Model) View() string { widestLineNumber = lnw } - wrappedLineStr := interfacesToString(wrappedLine) - strwidth := uniseg.StringWidth(wrappedLineStr) + strwidth := uniseg.StringWidth(string(wrappedLine)) padding := m.width - strwidth // If the trailing space causes the line to be wider than the // width, we should not draw it to the screen since it will result @@ -1590,46 +1236,22 @@ func (m Model) View() string { // The character causing the line to be wider than the width is // guaranteed to be a space since any other character would // have been wrapped. - wrappedLineStr = strings.TrimSuffix(wrappedLineStr, " ") + wrappedLine = []rune(strings.TrimSuffix(string(wrappedLine), " ")) padding -= m.width - strwidth } - if m.row == l && lineInfo.RowOffset == wl { - // Render the part of the line before the cursor - s.WriteString( - m.renderLineWithAttachments( - wrappedLine[:lineInfo.ColumnOffset], - style, - ), - ) - + s.WriteString(style.Render(string(wrappedLine[:lineInfo.ColumnOffset]))) if m.col >= len(line) && lineInfo.CharOffset >= m.width { m.virtualCursor.SetChar(" ") s.WriteString(m.virtualCursor.View()) - } else if lineInfo.ColumnOffset < len(wrappedLine) { - // Render the item under the cursor - item := wrappedLine[lineInfo.ColumnOffset] - if att, ok := item.(*Attachment); ok { - // Item at cursor is an attachment. Render it with the selection style. - // This becomes the "cursor" visually. - s.WriteString(m.Styles.SelectedAttachment.Render(att.Display)) - } else { - // Item at cursor is a rune. Render it with the virtual cursor. - m.virtualCursor.SetChar(string(item.(rune))) - s.WriteString(style.Render(m.virtualCursor.View())) - } - - // Render the part of the line after the cursor - s.WriteString(m.renderLineWithAttachments(wrappedLine[lineInfo.ColumnOffset+1:], style)) } else { - // Cursor is at the end of the line - m.virtualCursor.SetChar(" ") + m.virtualCursor.SetChar(string(wrappedLine[lineInfo.ColumnOffset])) s.WriteString(style.Render(m.virtualCursor.View())) + s.WriteString(style.Render(string(wrappedLine[lineInfo.ColumnOffset+1:]))) } } else { - s.WriteString(m.renderLineWithAttachments(wrappedLine, style)) + s.WriteString(style.Render(string(wrappedLine))) } - s.WriteString(style.Render(strings.Repeat(" ", max(0, padding)))) s.WriteRune('\n') newLines++ @@ -1821,12 +1443,12 @@ func (m Model) Cursor() *tea.Cursor { return c } -func (m Model) memoizedWrap(content []any, width int) [][]any { - input := line{content: content, width: width} +func (m Model) memoizedWrap(runes []rune, width int) [][]rune { + input := line{runes: runes, width: width} if v, ok := m.cache.Get(input); ok { return v } - v := wrapInterfaces(content, width) + v := wrap(runes, width) m.cache.Set(input, v) return v } @@ -1892,7 +1514,8 @@ func (m *Model) splitLine(row, col int) { // the cursor, take the content after the cursor and make it the content of // the line underneath, and shift the remaining lines down by one head, tailSrc := m.value[row][:col], m.value[row][col:] - tail := copyInterfaceSlice(tailSrc) + tail := make([]rune, len(tailSrc)) + copy(tail, tailSrc) m.value = append(m.value[:row+1], m.value[row:]...) @@ -1903,84 +1526,75 @@ func (m *Model) splitLine(row, col int) { m.row++ } -func wrapInterfaces(content []any, width int) [][]any { - if width <= 0 { - return [][]any{content} +// Paste is a command for pasting from the clipboard into the text input. +func Paste() tea.Msg { + str, err := clipboard.ReadAll() + if err != nil { + return pasteErrMsg{err} } + return pasteMsg(str) +} +func wrap(runes []rune, width int) [][]rune { var ( - lines = [][]any{{}} - word = []any{} - wordW int - lineW int - spaceW int - inSpaces bool + lines = [][]rune{{}} + word = []rune{} + row int + spaces int ) - for _, item := range content { - itemW := 0 - isSpace := false - - if r, ok := item.(rune); ok { - if unicode.IsSpace(r) { - isSpace = true - } - itemW = rw.RuneWidth(r) - } else if att, ok := item.(*Attachment); ok { - itemW = uniseg.StringWidth(att.Display) + // Word wrap the runes + for _, r := range runes { + if unicode.IsSpace(r) { + spaces++ + } else { + word = append(word, r) } - if isSpace { - if !inSpaces { - // End of a word - if lineW > 0 && lineW+wordW > width { - lines = append(lines, word) - lineW = wordW - } else { - lines[len(lines)-1] = append(lines[len(lines)-1], word...) - lineW += wordW - } + if spaces > 0 { //nolint:nestif + if uniseg.StringWidth(string(lines[row]))+uniseg.StringWidth(string(word))+spaces > width { + row++ + lines = append(lines, []rune{}) + lines[row] = append(lines[row], word...) + lines[row] = append(lines[row], repeatSpaces(spaces)...) + spaces = 0 + word = nil + } else { + lines[row] = append(lines[row], word...) + lines[row] = append(lines[row], repeatSpaces(spaces)...) + spaces = 0 word = nil - wordW = 0 } - inSpaces = true - spaceW += itemW } else { - if inSpaces { - // End of spaces - if lineW > 0 && lineW+spaceW > width { - lines = append(lines, []any{}) - lineW = 0 - } else { - lineW += spaceW + // If the last character is a double-width rune, then we may not be able to add it to this line + // as it might cause us to go past the width. + lastCharLen := rw.RuneWidth(word[len(word)-1]) + if uniseg.StringWidth(string(word))+lastCharLen > width { + // If the current line has any content, let's move to the next + // line because the current word fills up the entire line. + if len(lines[row]) > 0 { + row++ + lines = append(lines, []rune{}) } - // Add spaces to current line - for i := 0; i < spaceW; i++ { - lines[len(lines)-1] = append(lines[len(lines)-1], rune(' ')) - } - spaceW = 0 + lines[row] = append(lines[row], word...) + word = nil } - inSpaces = false - word = append(word, item) - wordW += itemW } } - // Handle any remaining word/spaces - if wordW > 0 { - if lineW > 0 && lineW+wordW > width { - lines = append(lines, word) - } else { - lines[len(lines)-1] = append(lines[len(lines)-1], word...) - } - } - if spaceW > 0 { - if lineW > 0 && lineW+spaceW > width { - lines = append(lines, []any{}) - } - for i := 0; i < spaceW; i++ { - lines[len(lines)-1] = append(lines[len(lines)-1], rune(' ')) - } + if uniseg.StringWidth(string(lines[row]))+uniseg.StringWidth(string(word))+spaces >= width { + lines = append(lines, []rune{}) + lines[row+1] = append(lines[row+1], word...) + // We add an extra space at the end of the line to account for the + // trailing space at the end of the previous soft-wrapped lines so that + // behaviour when navigating is consistent and so that we don't need to + // continually add edges to handle the last line of the wrapped input. + spaces++ + lines[row+1] = append(lines[row+1], repeatSpaces(spaces)...) + } else { + lines[row] = append(lines[row], word...) + spaces++ + lines[row] = append(lines[row], repeatSpaces(spaces)...) } return lines diff --git a/packages/tui/internal/image/clipboard_unix.go b/packages/tui/internal/image/clipboard_unix.go new file mode 100644 index 00000000..2653d8ca --- /dev/null +++ b/packages/tui/internal/image/clipboard_unix.go @@ -0,0 +1,46 @@ +//go:build !windows + +package image + +import ( + "bytes" + "fmt" + "github.com/atotto/clipboard" + "image" +) + +func GetImageFromClipboard() ([]byte, string, error) { + text, err := clipboard.ReadAll() + if err != nil { + return nil, "", fmt.Errorf("Error reading clipboard") + } + + if text == "" { + return nil, "", nil + } + + binaryData := []byte(text) + imageBytes, err := binaryToImage(binaryData) + if err != nil { + return nil, text, nil + } + return imageBytes, "", nil + +} + +func binaryToImage(data []byte) ([]byte, error) { + reader := bytes.NewReader(data) + img, _, err := image.Decode(reader) + if err != nil { + return nil, fmt.Errorf("Unable to covert bytes to image") + } + + return ImageToBytes(img) +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/packages/tui/internal/image/clipboard_windows.go b/packages/tui/internal/image/clipboard_windows.go new file mode 100644 index 00000000..6431ce3d --- /dev/null +++ b/packages/tui/internal/image/clipboard_windows.go @@ -0,0 +1,192 @@ +//go:build windows + +package image + +import ( + "bytes" + "fmt" + "image" + "image/color" + "log/slog" + "syscall" + "unsafe" +) + +var ( + user32 = syscall.NewLazyDLL("user32.dll") + kernel32 = syscall.NewLazyDLL("kernel32.dll") + openClipboard = user32.NewProc("OpenClipboard") + closeClipboard = user32.NewProc("CloseClipboard") + getClipboardData = user32.NewProc("GetClipboardData") + isClipboardFormatAvailable = user32.NewProc("IsClipboardFormatAvailable") + globalLock = kernel32.NewProc("GlobalLock") + globalUnlock = kernel32.NewProc("GlobalUnlock") + globalSize = kernel32.NewProc("GlobalSize") +) + +const ( + CF_TEXT = 1 + CF_UNICODETEXT = 13 + CF_DIB = 8 +) + +type BITMAPINFOHEADER struct { + BiSize uint32 + BiWidth int32 + BiHeight int32 + BiPlanes uint16 + BiBitCount uint16 + BiCompression uint32 + BiSizeImage uint32 + BiXPelsPerMeter int32 + BiYPelsPerMeter int32 + BiClrUsed uint32 + BiClrImportant uint32 +} + +func GetImageFromClipboard() ([]byte, string, error) { + ret, _, _ := openClipboard.Call(0) + if ret == 0 { + return nil, "", fmt.Errorf("failed to open clipboard") + } + defer func(closeClipboard *syscall.LazyProc, a ...uintptr) { + _, _, err := closeClipboard.Call(a...) + if err != nil { + slog.Error("close clipboard failed") + return + } + }(closeClipboard) + isTextAvailable, _, _ := isClipboardFormatAvailable.Call(uintptr(CF_TEXT)) + isUnicodeTextAvailable, _, _ := isClipboardFormatAvailable.Call(uintptr(CF_UNICODETEXT)) + + if isTextAvailable != 0 || isUnicodeTextAvailable != 0 { + // Get text from clipboard + var formatToUse uintptr = CF_TEXT + if isUnicodeTextAvailable != 0 { + formatToUse = CF_UNICODETEXT + } + + hClipboardText, _, _ := getClipboardData.Call(formatToUse) + if hClipboardText != 0 { + textPtr, _, _ := globalLock.Call(hClipboardText) + if textPtr != 0 { + defer func(globalUnlock *syscall.LazyProc, a ...uintptr) { + _, _, err := globalUnlock.Call(a...) + if err != nil { + slog.Error("Global unlock failed") + return + } + }(globalUnlock, hClipboardText) + + // Get clipboard text + var clipboardText string + if formatToUse == CF_UNICODETEXT { + // Convert wide string to Go string + clipboardText = syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(textPtr))[:]) + } else { + // Get size of ANSI text + size, _, _ := globalSize.Call(hClipboardText) + if size > 0 { + // Convert ANSI string to Go string + textBytes := make([]byte, size) + copy(textBytes, (*[1 << 20]byte)(unsafe.Pointer(textPtr))[:size:size]) + clipboardText = bytesToString(textBytes) + } + } + + // Check if the text is not empty + if clipboardText != "" { + return nil, clipboardText, nil + } + } + } + } + hClipboardData, _, _ := getClipboardData.Call(uintptr(CF_DIB)) + if hClipboardData == 0 { + return nil, "", fmt.Errorf("failed to get clipboard data") + } + + dataPtr, _, _ := globalLock.Call(hClipboardData) + if dataPtr == 0 { + return nil, "", fmt.Errorf("failed to lock clipboard data") + } + defer func(globalUnlock *syscall.LazyProc, a ...uintptr) { + _, _, err := globalUnlock.Call(a...) + if err != nil { + slog.Error("Global unlock failed") + return + } + }(globalUnlock, hClipboardData) + + bmiHeader := (*BITMAPINFOHEADER)(unsafe.Pointer(dataPtr)) + + width := int(bmiHeader.BiWidth) + height := int(bmiHeader.BiHeight) + if height < 0 { + height = -height + } + bitsPerPixel := int(bmiHeader.BiBitCount) + + img := image.NewRGBA(image.Rect(0, 0, width, height)) + + var bitsOffset uintptr + if bitsPerPixel <= 8 { + numColors := uint32(1) << bitsPerPixel + if bmiHeader.BiClrUsed > 0 { + numColors = bmiHeader.BiClrUsed + } + bitsOffset = unsafe.Sizeof(*bmiHeader) + uintptr(numColors*4) + } else { + bitsOffset = unsafe.Sizeof(*bmiHeader) + } + + for y := range height { + for x := range width { + + srcY := height - y - 1 + if bmiHeader.BiHeight < 0 { + srcY = y + } + + var pixelPointer unsafe.Pointer + var r, g, b, a uint8 + + switch bitsPerPixel { + case 24: + stride := (width*3 + 3) &^ 3 + pixelPointer = unsafe.Pointer(dataPtr + bitsOffset + uintptr(srcY*stride+x*3)) + b = *(*byte)(pixelPointer) + g = *(*byte)(unsafe.Add(pixelPointer, 1)) + r = *(*byte)(unsafe.Add(pixelPointer, 2)) + a = 255 + case 32: + pixelPointer = unsafe.Pointer(dataPtr + bitsOffset + uintptr(srcY*width*4+x*4)) + b = *(*byte)(pixelPointer) + g = *(*byte)(unsafe.Add(pixelPointer, 1)) + r = *(*byte)(unsafe.Add(pixelPointer, 2)) + a = *(*byte)(unsafe.Add(pixelPointer, 3)) + if a == 0 { + a = 255 + } + default: + return nil, "", fmt.Errorf("unsupported bit count: %d", bitsPerPixel) + } + + img.Set(x, y, color.RGBA{R: r, G: g, B: b, A: a}) + } + } + + imageBytes, err := ImageToBytes(img) + if err != nil { + return nil, "", err + } + return imageBytes, "", nil +} + +func bytesToString(b []byte) string { + i := bytes.IndexByte(b, 0) + if i == -1 { + return string(b) + } + return string(b[:i]) +} diff --git a/packages/tui/internal/image/images.go b/packages/tui/internal/image/images.go new file mode 100644 index 00000000..742eb30a --- /dev/null +++ b/packages/tui/internal/image/images.go @@ -0,0 +1,86 @@ +package image + +import ( + "bytes" + "fmt" + "image" + "image/color" + "image/png" + "os" + "strings" + + "github.com/charmbracelet/lipgloss/v2" + "github.com/disintegration/imaging" + "github.com/lucasb-eyer/go-colorful" + _ "golang.org/x/image/webp" +) + +func ValidateFileSize(filePath string, sizeLimit int64) (bool, error) { + fileInfo, err := os.Stat(filePath) + if err != nil { + return false, fmt.Errorf("error getting file info: %w", err) + } + + if fileInfo.Size() > sizeLimit { + return true, nil + } + + return false, nil +} + +func ToString(width int, img image.Image) string { + img = imaging.Resize(img, width, 0, imaging.Lanczos) + b := img.Bounds() + imageWidth := b.Max.X + h := b.Max.Y + str := strings.Builder{} + + for heightCounter := 0; heightCounter < h; heightCounter += 2 { + for x := range imageWidth { + c1, _ := colorful.MakeColor(img.At(x, heightCounter)) + color1 := lipgloss.Color(c1.Hex()) + + var color2 color.Color + if heightCounter+1 < h { + c2, _ := colorful.MakeColor(img.At(x, heightCounter+1)) + color2 = lipgloss.Color(c2.Hex()) + } else { + color2 = color1 + } + + str.WriteString(lipgloss.NewStyle().Foreground(color1). + Background(color2).Render("▀")) + } + + str.WriteString("\n") + } + + return str.String() +} + +func ImagePreview(width int, filename string) (string, error) { + imageContent, err := os.Open(filename) + if err != nil { + return "", err + } + defer imageContent.Close() + + img, _, err := image.Decode(imageContent) + if err != nil { + return "", err + } + + imageString := ToString(width, img) + + return imageString, nil +} + +func ImageToBytes(image image.Image) ([]byte, error) { + buf := new(bytes.Buffer) + err := png.Encode(buf, image) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/packages/tui/internal/layout/flex_example_test.go b/packages/tui/internal/layout/flex_example_test.go new file mode 100644 index 00000000..a03346eb --- /dev/null +++ b/packages/tui/internal/layout/flex_example_test.go @@ -0,0 +1,41 @@ +package layout_test + +import ( + "fmt" + "github.com/sst/opencode/internal/layout" +) + +func ExampleRender_withGap() { + // Create a horizontal layout with 3px gap between items + result := layout.Render( + layout.FlexOptions{ + Direction: layout.Row, + Width: 30, + Height: 1, + Gap: 3, + }, + layout.FlexItem{View: "Item1"}, + layout.FlexItem{View: "Item2"}, + layout.FlexItem{View: "Item3"}, + ) + fmt.Println(result) + // Output: Item1 Item2 Item3 +} + +func ExampleRender_withGapAndJustify() { + // Create a horizontal layout with gap and space-between justification + result := layout.Render( + layout.FlexOptions{ + Direction: layout.Row, + Width: 30, + Height: 1, + Gap: 2, + Justify: layout.JustifySpaceBetween, + }, + layout.FlexItem{View: "A"}, + layout.FlexItem{View: "B"}, + layout.FlexItem{View: "C"}, + ) + fmt.Println(result) + // Output: A B C +} diff --git a/packages/tui/internal/layout/flex_test.go b/packages/tui/internal/layout/flex_test.go new file mode 100644 index 00000000..cad38dc8 --- /dev/null +++ b/packages/tui/internal/layout/flex_test.go @@ -0,0 +1,90 @@ +package layout + +import ( + "strings" + "testing" +) + +func TestFlexGap(t *testing.T) { + tests := []struct { + name string + opts FlexOptions + items []FlexItem + expected string + }{ + { + name: "Row with gap", + opts: FlexOptions{ + Direction: Row, + Width: 20, + Height: 1, + Gap: 2, + }, + items: []FlexItem{ + {View: "A"}, + {View: "B"}, + {View: "C"}, + }, + expected: "A B C", + }, + { + name: "Column with gap", + opts: FlexOptions{ + Direction: Column, + Width: 1, + Height: 5, + Gap: 1, + Align: AlignStart, + }, + items: []FlexItem{ + {View: "A", FixedSize: 1}, + {View: "B", FixedSize: 1}, + {View: "C", FixedSize: 1}, + }, + expected: "A\n \nB\n \nC", + }, + { + name: "Row with gap and justify space between", + opts: FlexOptions{ + Direction: Row, + Width: 15, + Height: 1, + Gap: 1, + Justify: JustifySpaceBetween, + }, + items: []FlexItem{ + {View: "A"}, + {View: "B"}, + {View: "C"}, + }, + expected: "A B C", + }, + { + name: "No gap specified", + opts: FlexOptions{ + Direction: Row, + Width: 10, + Height: 1, + }, + items: []FlexItem{ + {View: "A"}, + {View: "B"}, + {View: "C"}, + }, + expected: "ABC", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Render(tt.opts, tt.items...) + // Trim any trailing spaces for comparison + result = strings.TrimRight(result, " ") + expected := strings.TrimRight(tt.expected, " ") + + if result != expected { + t.Errorf("Render() = %q, want %q", result, expected) + } + }) + } +} diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index d25fc139..29235229 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -52,9 +52,7 @@ type appModel struct { messages chat.MessagesComponent completions dialog.CompletionDialog commandProvider dialog.CompletionProvider - fileProvider dialog.CompletionProvider showCompletionDialog bool - fileCompletionActive bool leaderBinding *key.Binding isLeaderSequence bool toastManager *toast.ToastManager @@ -182,33 +180,11 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { !a.showCompletionDialog && a.editor.Value() == "" { a.showCompletionDialog = true - a.fileCompletionActive = false updated, cmd := a.editor.Update(msg) a.editor = updated.(chat.EditorComponent) cmds = append(cmds, cmd) - // Set command provider for command completion - a.completions = dialog.NewCompletionDialogComponent(a.commandProvider) - updated, cmd = a.completions.Update(msg) - a.completions = updated.(dialog.CompletionDialog) - cmds = append(cmds, cmd) - - return a, tea.Sequence(cmds...) - } - - // Handle file completions trigger - if keyString == "@" && - !a.showCompletionDialog { - a.showCompletionDialog = true - a.fileCompletionActive = true - - updated, cmd := a.editor.Update(msg) - a.editor = updated.(chat.EditorComponent) - cmds = append(cmds, cmd) - - // Set file provider for file completion - a.completions = dialog.NewCompletionDialogComponent(a.fileProvider) updated, cmd = a.completions.Update(msg) a.completions = updated.(dialog.CompletionDialog) cmds = append(cmds, cmd) @@ -218,7 +194,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if a.showCompletionDialog { switch keyString { - case "tab", "enter", "esc", "ctrl+c", "up", "down": + case "tab", "enter", "esc", "ctrl+c": updated, cmd := a.completions.Update(msg) a.completions = updated.(dialog.CompletionDialog) cmds = append(cmds, cmd) @@ -350,11 +326,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return a, toast.NewErrorToast(msg.Error()) case app.SendMsg: a.showCompletionDialog = false - a.app, cmd = a.app.SendChatMessage(context.Background(), msg.Text, msg.Attachments) + cmd := a.app.SendChatMessage(context.Background(), msg.Text, msg.Attachments) cmds = append(cmds, cmd) case dialog.CompletionDialogCloseMsg: a.showCompletionDialog = false - a.fileCompletionActive = false case opencode.EventListResponseEventInstallationUpdated: return a, toast.NewSuccessToast( "opencode updated to "+msg.Properties.Version+", restart to apply.", @@ -363,7 +338,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case opencode.EventListResponseEventSessionDeleted: if a.app.Session != nil && msg.Properties.Info.ID == a.app.Session.ID { a.app.Session = &opencode.Session{} - a.app.Messages = []opencode.MessageUnion{} + a.app.Messages = []opencode.Message{} } return a, toast.NewSuccessToast("Session deleted successfully") case opencode.EventListResponseEventSessionUpdated: @@ -371,7 +346,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.app.Session = &msg.Properties.Info } case opencode.EventListResponseEventMessageUpdated: - if msg.Properties.Info.SessionID == a.app.Session.ID { + if msg.Properties.Info.Metadata.SessionID == a.app.Session.ID { exists := false optimisticReplaced := false @@ -379,15 +354,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Properties.Info.Role == opencode.MessageRoleUser { // Look for optimistic messages to replace for i, m := range a.app.Messages { - switch m := m.(type) { - case opencode.UserMessage: - if strings.HasPrefix(m.ID, "optimistic-") && m.Role == opencode.UserMessageRoleUser { - // Replace the optimistic message with the real one - a.app.Messages[i] = msg.Properties.Info.AsUnion() - exists = true - optimisticReplaced = true - break - } + if strings.HasPrefix(m.ID, "optimistic-") && m.Role == opencode.MessageRoleUser { + // Replace the optimistic message with the real one + a.app.Messages[i] = msg.Properties.Info + exists = true + optimisticReplaced = true + break } } } @@ -395,15 +367,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // If not replacing optimistic, check for existing message with same ID if !optimisticReplaced { for i, m := range a.app.Messages { - var id string - switch m := m.(type) { - case opencode.UserMessage: - id = m.ID - case opencode.AssistantMessage: - id = m.ID - } - if id == msg.Properties.Info.ID { - a.app.Messages[i] = msg.Properties.Info.AsUnion() + if m.ID == msg.Properties.Info.ID { + a.app.Messages[i] = msg.Properties.Info exists = true break } @@ -411,7 +376,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if !exists { - a.app.Messages = append(a.app.Messages, msg.Properties.Info.AsUnion()) + a.app.Messages = append(a.app.Messages, msg.Properties.Info) } } case opencode.EventListResponseEventSessionError: @@ -472,10 +437,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return a, toast.NewErrorToast("Failed to open session") } a.app.Session = msg - a.app.Messages = make([]opencode.MessageUnion, 0) - for _, message := range messages { - a.app.Messages = append(a.app.Messages, message.AsUnion()) - } + a.app.Messages = messages return a, util.CmdHandler(app.SessionLoadedMsg{}) case app.ModelSelectedMsg: a.app.Provider = &msg.Provider @@ -816,8 +778,11 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd) return nil } os.Remove(tmpfile.Name()) + // attachments := m.attachments + // m.attachments = nil return app.SendMsg{ - Text: string(content), + Text: string(content), + Attachments: []app.Attachment{}, // attachments, } }) cmds = append(cmds, cmd) @@ -826,7 +791,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd) return a, nil } a.app.Session = &opencode.Session{} - a.app.Messages = []opencode.MessageUnion{} + a.app.Messages = []opencode.Message{} cmds = append(cmds, util.CmdHandler(app.SessionClearedMsg{})) case commands.SessionListCommand: sessionDialog := dialog.NewSessionDialog(a.app) @@ -841,7 +806,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd) return a, toast.NewErrorToast("Failed to share session") } shareUrl := response.Share.URL - cmds = append(cmds, a.app.SetClipboard(shareUrl)) + cmds = append(cmds, tea.SetClipboard(shareUrl)) cmds = append(cmds, toast.NewSuccessToast("Share URL copied to clipboard!")) case commands.SessionUnshareCommand: if a.app.Session.ID == "" { @@ -975,7 +940,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd) case commands.MessagesCopyCommand: selected := a.messages.Selected() if selected != "" { - cmd = a.app.SetClipboard(selected) + cmd = tea.SetClipboard(selected) cmds = append(cmds, cmd) cmd = toast.NewSuccessToast("Message copied to clipboard") cmds = append(cmds, cmd) @@ -989,7 +954,6 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd) func NewModel(app *app.App) tea.Model { commandProvider := completions.NewCommandCompletionProvider(app) - fileProvider := completions.NewFileAndFolderContextGroup(app) messages := chat.NewMessagesComponent(app) editor := chat.NewEditorComponent(app) @@ -1008,11 +972,9 @@ func NewModel(app *app.App) tea.Model { messages: messages, completions: completions, commandProvider: commandProvider, - fileProvider: fileProvider, leaderBinding: leaderBinding, isLeaderSequence: false, showCompletionDialog: false, - fileCompletionActive: false, toastManager: toast.NewToastManager(), interruptKeyState: InterruptKeyIdle, fileViewer: fileviewer.New(app), diff --git a/packages/tui/internal/util/file.go b/packages/tui/internal/util/file.go index b079f24c..2c0987dc 100644 --- a/packages/tui/internal/util/file.go +++ b/packages/tui/internal/util/file.go @@ -83,7 +83,7 @@ func Extension(path string) string { } func ToMarkdown(content string, width int, backgroundColor compat.AdaptiveColor) string { - r := styles.GetMarkdownRenderer(width-6, backgroundColor) + r := styles.GetMarkdownRenderer(width-7, backgroundColor) content = strings.ReplaceAll(content, RootPath+"/", "") rendered, _ := r.Render(content) lines := strings.Split(rendered, "\n") diff --git a/packages/tui/sdk/.stats.yml b/packages/tui/sdk/.stats.yml index 4b404ade..ab6c4a20 100644 --- a/packages/tui/sdk/.stats.yml +++ b/packages/tui/sdk/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 20 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-c06a9b8d8284683e8350fdd3eceff0b5756877f7b67e974acd565409b67d32a0.yml -openapi_spec_hash: 5933bca0c79177065374ac724a6bc986 -config_hash: de53ecf98e1038f2cc2fd273b582f082 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-945f9da9e9a4c4008834deef63e4346c0076e020eed3d3c98c249095033c1ac5.yml +openapi_spec_hash: 522a44f6cb0677435fe2ac7693848ad7 +config_hash: 6c8822d278ba83456e5eed6d774ca230 diff --git a/packages/tui/sdk/aliases.go b/packages/tui/sdk/aliases.go index 6ab36d04..84dd614a 100644 --- a/packages/tui/sdk/aliases.go +++ b/packages/tui/sdk/aliases.go @@ -9,15 +9,6 @@ import ( type Error = apierror.Error -// This is an alias to an internal type. -type MessageAbortedError = shared.MessageAbortedError - -// This is an alias to an internal type. -type MessageAbortedErrorName = shared.MessageAbortedErrorName - -// This is an alias to an internal value. -const MessageAbortedErrorNameMessageAbortedError = shared.MessageAbortedErrorNameMessageAbortedError - // This is an alias to an internal type. type ProviderAuthError = shared.ProviderAuthError diff --git a/packages/tui/sdk/api.md b/packages/tui/sdk/api.md index 967b8afe..4ac9edcf 100644 --- a/packages/tui/sdk/api.md +++ b/packages/tui/sdk/api.md @@ -1,6 +1,5 @@ # Shared Response Types -- shared.MessageAbortedError - shared.ProviderAuthError - shared.UnknownError @@ -72,25 +71,30 @@ Methods: Params Types: - opencode.FilePartParam +- opencode.MessagePartUnionParam +- opencode.ReasoningPartParam +- opencode.SourceURLPartParam +- opencode.StepStartPartParam - opencode.TextPartParam -- opencode.UserMessagePartUnionParam +- opencode.ToolCallParam +- opencode.ToolInvocationPartParam +- opencode.ToolPartialCallParam +- opencode.ToolResultParam Response Types: -- opencode.AssistantMessage -- opencode.AssistantMessagePart - opencode.FilePart - opencode.Message +- opencode.MessagePart +- opencode.ReasoningPart - opencode.Session +- opencode.SourceURLPart - opencode.StepStartPart - opencode.TextPart -- opencode.ToolPart -- opencode.ToolStateCompleted -- opencode.ToolStateError -- opencode.ToolStatePending -- opencode.ToolStateRunning -- opencode.UserMessage -- opencode.UserMessagePart +- opencode.ToolCall +- opencode.ToolInvocationPart +- opencode.ToolPartialCall +- opencode.ToolResult Methods: @@ -98,7 +102,7 @@ Methods: - client.Session.List(ctx context.Context) ([]opencode.Session, error) - client.Session.Delete(ctx context.Context, id string) (bool, error) - client.Session.Abort(ctx context.Context, id string) (bool, error) -- client.Session.Chat(ctx context.Context, id string, body opencode.SessionChatParams) (opencode.AssistantMessage, error) +- client.Session.Chat(ctx context.Context, id string, body opencode.SessionChatParams) (opencode.Message, error) - client.Session.Init(ctx context.Context, id string, body opencode.SessionInitParams) (bool, error) - client.Session.Messages(ctx context.Context, id string) ([]opencode.Message, error) - client.Session.Share(ctx context.Context, id string) (opencode.Session, error) diff --git a/packages/tui/sdk/config.go b/packages/tui/sdk/config.go index 503c17bd..39da2f94 100644 --- a/packages/tui/sdk/config.go +++ b/packages/tui/sdk/config.go @@ -397,18 +397,14 @@ func (r configProviderModelsLimitJSON) RawJSON() string { type Keybinds struct { // Exit the application AppExit string `json:"app_exit"` - // Show help dialog - AppHelp string `json:"app_help"` // Open external editor EditorOpen string `json:"editor_open"` - // Close file - FileClose string `json:"file_close"` - // Toggle split/unified diff - FileDiffToggle string `json:"file_diff_toggle"` - // List files - FileList string `json:"file_list"` - // Search file - FileSearch string `json:"file_search"` + // Show help dialog + Help string `json:"help"` + // Navigate to next history item + HistoryNext string `json:"history_next"` + // Navigate to previous history item + HistoryPrevious string `json:"history_previous"` // Clear input field InputClear string `json:"input_clear"` // Insert newline in input @@ -419,8 +415,6 @@ type Keybinds struct { InputSubmit string `json:"input_submit"` // Leader key for keybind combinations Leader string `json:"leader"` - // Copy message - MessagesCopy string `json:"messages_copy"` // Navigate to first message MessagesFirst string `json:"messages_first"` // Scroll messages down by half page @@ -429,8 +423,6 @@ type Keybinds struct { MessagesHalfPageUp string `json:"messages_half_page_up"` // Navigate to last message MessagesLast string `json:"messages_last"` - // Toggle layout - MessagesLayoutToggle string `json:"messages_layout_toggle"` // Navigate to next message MessagesNext string `json:"messages_next"` // Scroll messages down by one page @@ -439,8 +431,6 @@ type Keybinds struct { MessagesPageUp string `json:"messages_page_up"` // Navigate to previous message MessagesPrevious string `json:"messages_previous"` - // Revert message - MessagesRevert string `json:"messages_revert"` // List available models ModelList string `json:"model_list"` // Initialize project configuration @@ -455,8 +445,6 @@ type Keybinds struct { SessionNew string `json:"session_new"` // Share current session SessionShare string `json:"session_share"` - // Unshare current session - SessionUnshare string `json:"session_unshare"` // List available themes ThemeList string `json:"theme_list"` // Show tool details @@ -467,28 +455,23 @@ type Keybinds struct { // keybindsJSON contains the JSON metadata for the struct [Keybinds] type keybindsJSON struct { AppExit apijson.Field - AppHelp apijson.Field EditorOpen apijson.Field - FileClose apijson.Field - FileDiffToggle apijson.Field - FileList apijson.Field - FileSearch apijson.Field + Help apijson.Field + HistoryNext apijson.Field + HistoryPrevious apijson.Field InputClear apijson.Field InputNewline apijson.Field InputPaste apijson.Field InputSubmit apijson.Field Leader apijson.Field - MessagesCopy apijson.Field MessagesFirst apijson.Field MessagesHalfPageDown apijson.Field MessagesHalfPageUp apijson.Field MessagesLast apijson.Field - MessagesLayoutToggle apijson.Field MessagesNext apijson.Field MessagesPageDown apijson.Field MessagesPageUp apijson.Field MessagesPrevious apijson.Field - MessagesRevert apijson.Field ModelList apijson.Field ProjectInit apijson.Field SessionCompact apijson.Field @@ -496,7 +479,6 @@ type keybindsJSON struct { SessionList apijson.Field SessionNew apijson.Field SessionShare apijson.Field - SessionUnshare apijson.Field ThemeList apijson.Field ToolDetails apijson.Field raw string diff --git a/packages/tui/sdk/event.go b/packages/tui/sdk/event.go index 89da8a07..ed92b2ae 100644 --- a/packages/tui/sdk/event.go +++ b/packages/tui/sdk/event.go @@ -51,11 +51,11 @@ type EventListResponse struct { // [EventListResponseEventLspClientDiagnosticsProperties], // [EventListResponseEventPermissionUpdatedProperties], // [EventListResponseEventFileEditedProperties], + // [EventListResponseEventStorageWriteProperties], // [EventListResponseEventInstallationUpdatedProperties], // [EventListResponseEventMessageUpdatedProperties], // [EventListResponseEventMessageRemovedProperties], // [EventListResponseEventMessagePartUpdatedProperties], - // [EventListResponseEventStorageWriteProperties], // [EventListResponseEventSessionUpdatedProperties], // [EventListResponseEventSessionDeletedProperties], // [EventListResponseEventSessionIdleProperties], @@ -95,12 +95,12 @@ func (r *EventListResponse) UnmarshalJSON(data []byte) (err error) { // Possible runtime types of the union are // [EventListResponseEventLspClientDiagnostics], // [EventListResponseEventPermissionUpdated], [EventListResponseEventFileEdited], +// [EventListResponseEventStorageWrite], // [EventListResponseEventInstallationUpdated], // [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved], // [EventListResponseEventMessagePartUpdated], -// [EventListResponseEventStorageWrite], [EventListResponseEventSessionUpdated], -// [EventListResponseEventSessionDeleted], [EventListResponseEventSessionIdle], -// [EventListResponseEventSessionError], +// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted], +// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError], // [EventListResponseEventFileWatcherUpdated]. func (r EventListResponse) AsUnion() EventListResponseUnion { return r.union @@ -108,12 +108,12 @@ func (r EventListResponse) AsUnion() EventListResponseUnion { // Union satisfied by [EventListResponseEventLspClientDiagnostics], // [EventListResponseEventPermissionUpdated], [EventListResponseEventFileEdited], +// [EventListResponseEventStorageWrite], // [EventListResponseEventInstallationUpdated], // [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved], // [EventListResponseEventMessagePartUpdated], -// [EventListResponseEventStorageWrite], [EventListResponseEventSessionUpdated], -// [EventListResponseEventSessionDeleted], [EventListResponseEventSessionIdle], -// [EventListResponseEventSessionError] or +// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted], +// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError] or // [EventListResponseEventFileWatcherUpdated]. type EventListResponseUnion interface { implementsEventListResponse() @@ -138,6 +138,11 @@ func init() { Type: reflect.TypeOf(EventListResponseEventFileEdited{}), DiscriminatorValue: "file.edited", }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventStorageWrite{}), + DiscriminatorValue: "storage.write", + }, apijson.UnionVariant{ TypeFilter: gjson.JSON, Type: reflect.TypeOf(EventListResponseEventInstallationUpdated{}), @@ -158,11 +163,6 @@ func init() { Type: reflect.TypeOf(EventListResponseEventMessagePartUpdated{}), DiscriminatorValue: "message.part.updated", }, - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(EventListResponseEventStorageWrite{}), - DiscriminatorValue: "storage.write", - }, apijson.UnionVariant{ TypeFilter: gjson.JSON, Type: reflect.TypeOf(EventListResponseEventSessionUpdated{}), @@ -402,6 +402,68 @@ func (r EventListResponseEventFileEditedType) IsKnown() bool { return false } +type EventListResponseEventStorageWrite struct { + Properties EventListResponseEventStorageWriteProperties `json:"properties,required"` + Type EventListResponseEventStorageWriteType `json:"type,required"` + JSON eventListResponseEventStorageWriteJSON `json:"-"` +} + +// eventListResponseEventStorageWriteJSON contains the JSON metadata for the struct +// [EventListResponseEventStorageWrite] +type eventListResponseEventStorageWriteJSON struct { + Properties apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventStorageWrite) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventStorageWriteJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventStorageWrite) implementsEventListResponse() {} + +type EventListResponseEventStorageWriteProperties struct { + Key string `json:"key,required"` + Content interface{} `json:"content"` + JSON eventListResponseEventStorageWritePropertiesJSON `json:"-"` +} + +// eventListResponseEventStorageWritePropertiesJSON contains the JSON metadata for +// the struct [EventListResponseEventStorageWriteProperties] +type eventListResponseEventStorageWritePropertiesJSON struct { + Key apijson.Field + Content apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventStorageWriteProperties) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventStorageWritePropertiesJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventStorageWriteType string + +const ( + EventListResponseEventStorageWriteTypeStorageWrite EventListResponseEventStorageWriteType = "storage.write" +) + +func (r EventListResponseEventStorageWriteType) IsKnown() bool { + switch r { + case EventListResponseEventStorageWriteTypeStorageWrite: + return true + } + return false +} + type EventListResponseEventInstallationUpdated struct { Properties EventListResponseEventInstallationUpdatedProperties `json:"properties,required"` Type EventListResponseEventInstallationUpdatedType `json:"type,required"` @@ -611,7 +673,7 @@ func (r EventListResponseEventMessagePartUpdated) implementsEventListResponse() type EventListResponseEventMessagePartUpdatedProperties struct { MessageID string `json:"messageID,required"` - Part AssistantMessagePart `json:"part,required"` + Part MessagePart `json:"part,required"` SessionID string `json:"sessionID,required"` JSON eventListResponseEventMessagePartUpdatedPropertiesJSON `json:"-"` } @@ -648,68 +710,6 @@ func (r EventListResponseEventMessagePartUpdatedType) IsKnown() bool { return false } -type EventListResponseEventStorageWrite struct { - Properties EventListResponseEventStorageWriteProperties `json:"properties,required"` - Type EventListResponseEventStorageWriteType `json:"type,required"` - JSON eventListResponseEventStorageWriteJSON `json:"-"` -} - -// eventListResponseEventStorageWriteJSON contains the JSON metadata for the struct -// [EventListResponseEventStorageWrite] -type eventListResponseEventStorageWriteJSON struct { - Properties apijson.Field - Type apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *EventListResponseEventStorageWrite) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r eventListResponseEventStorageWriteJSON) RawJSON() string { - return r.raw -} - -func (r EventListResponseEventStorageWrite) implementsEventListResponse() {} - -type EventListResponseEventStorageWriteProperties struct { - Key string `json:"key,required"` - Content interface{} `json:"content"` - JSON eventListResponseEventStorageWritePropertiesJSON `json:"-"` -} - -// eventListResponseEventStorageWritePropertiesJSON contains the JSON metadata for -// the struct [EventListResponseEventStorageWriteProperties] -type eventListResponseEventStorageWritePropertiesJSON struct { - Key apijson.Field - Content apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *EventListResponseEventStorageWriteProperties) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r eventListResponseEventStorageWritePropertiesJSON) RawJSON() string { - return r.raw -} - -type EventListResponseEventStorageWriteType string - -const ( - EventListResponseEventStorageWriteTypeStorageWrite EventListResponseEventStorageWriteType = "storage.write" -) - -func (r EventListResponseEventStorageWriteType) IsKnown() bool { - switch r { - case EventListResponseEventStorageWriteTypeStorageWrite: - return true - } - return false -} - type EventListResponseEventSessionUpdated struct { Properties EventListResponseEventSessionUpdatedProperties `json:"properties,required"` Type EventListResponseEventSessionUpdatedType `json:"type,required"` @@ -972,15 +972,13 @@ func (r *EventListResponseEventSessionErrorPropertiesError) UnmarshalJSON(data [ // // Possible runtime types of the union are [shared.ProviderAuthError], // [shared.UnknownError], -// [EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError], -// [shared.MessageAbortedError]. +// [EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError]. func (r EventListResponseEventSessionErrorPropertiesError) AsUnion() EventListResponseEventSessionErrorPropertiesErrorUnion { return r.union } -// Union satisfied by [shared.ProviderAuthError], [shared.UnknownError], -// [EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError] or -// [shared.MessageAbortedError]. +// Union satisfied by [shared.ProviderAuthError], [shared.UnknownError] or +// [EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError]. type EventListResponseEventSessionErrorPropertiesErrorUnion interface { ImplementsEventListResponseEventSessionErrorPropertiesError() } @@ -1004,11 +1002,6 @@ func init() { Type: reflect.TypeOf(EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError{}), DiscriminatorValue: "MessageOutputLengthError", }, - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(shared.MessageAbortedError{}), - DiscriminatorValue: "MessageAbortedError", - }, ) } @@ -1059,12 +1052,11 @@ const ( EventListResponseEventSessionErrorPropertiesErrorNameProviderAuthError EventListResponseEventSessionErrorPropertiesErrorName = "ProviderAuthError" EventListResponseEventSessionErrorPropertiesErrorNameUnknownError EventListResponseEventSessionErrorPropertiesErrorName = "UnknownError" EventListResponseEventSessionErrorPropertiesErrorNameMessageOutputLengthError EventListResponseEventSessionErrorPropertiesErrorName = "MessageOutputLengthError" - EventListResponseEventSessionErrorPropertiesErrorNameMessageAbortedError EventListResponseEventSessionErrorPropertiesErrorName = "MessageAbortedError" ) func (r EventListResponseEventSessionErrorPropertiesErrorName) IsKnown() bool { switch r { - case EventListResponseEventSessionErrorPropertiesErrorNameProviderAuthError, EventListResponseEventSessionErrorPropertiesErrorNameUnknownError, EventListResponseEventSessionErrorPropertiesErrorNameMessageOutputLengthError, EventListResponseEventSessionErrorPropertiesErrorNameMessageAbortedError: + case EventListResponseEventSessionErrorPropertiesErrorNameProviderAuthError, EventListResponseEventSessionErrorPropertiesErrorNameUnknownError, EventListResponseEventSessionErrorPropertiesErrorNameMessageOutputLengthError: return true } return false @@ -1167,11 +1159,11 @@ const ( EventListResponseTypeLspClientDiagnostics EventListResponseType = "lsp.client.diagnostics" EventListResponseTypePermissionUpdated EventListResponseType = "permission.updated" EventListResponseTypeFileEdited EventListResponseType = "file.edited" + EventListResponseTypeStorageWrite EventListResponseType = "storage.write" EventListResponseTypeInstallationUpdated EventListResponseType = "installation.updated" EventListResponseTypeMessageUpdated EventListResponseType = "message.updated" EventListResponseTypeMessageRemoved EventListResponseType = "message.removed" EventListResponseTypeMessagePartUpdated EventListResponseType = "message.part.updated" - EventListResponseTypeStorageWrite EventListResponseType = "storage.write" EventListResponseTypeSessionUpdated EventListResponseType = "session.updated" EventListResponseTypeSessionDeleted EventListResponseType = "session.deleted" EventListResponseTypeSessionIdle EventListResponseType = "session.idle" @@ -1181,7 +1173,7 @@ const ( func (r EventListResponseType) IsKnown() bool { switch r { - case EventListResponseTypeLspClientDiagnostics, EventListResponseTypePermissionUpdated, EventListResponseTypeFileEdited, EventListResponseTypeInstallationUpdated, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeStorageWrite, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeFileWatcherUpdated: + case EventListResponseTypeLspClientDiagnostics, EventListResponseTypePermissionUpdated, EventListResponseTypeFileEdited, EventListResponseTypeStorageWrite, EventListResponseTypeInstallationUpdated, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeFileWatcherUpdated: return true } return false diff --git a/packages/tui/sdk/scripts/lint b/packages/tui/sdk/scripts/lint index c10fa02f..fa7ba1f6 100755 --- a/packages/tui/sdk/scripts/lint +++ b/packages/tui/sdk/scripts/lint @@ -5,7 +5,4 @@ set -e cd "$(dirname "$0")/.." echo "==> Running Go build" -go build . - -# Compile the tests but don't run them -go test -c . +go build ./... diff --git a/packages/tui/sdk/session.go b/packages/tui/sdk/session.go index fc55c691..e8216a1f 100644 --- a/packages/tui/sdk/session.go +++ b/packages/tui/sdk/session.go @@ -77,7 +77,7 @@ func (r *SessionService) Abort(ctx context.Context, id string, opts ...option.Re } // Create and send a new message to a session -func (r *SessionService) Chat(ctx context.Context, id string, body SessionChatParams, opts ...option.RequestOption) (res *AssistantMessage, err error) { +func (r *SessionService) Chat(ctx context.Context, id string, body SessionChatParams, opts ...option.RequestOption) (res *Message, err error) { opts = append(r.Options[:], opts...) if id == "" { err = errors.New("missing required id parameter") @@ -148,391 +148,17 @@ func (r *SessionService) Unshare(ctx context.Context, id string, opts ...option. return } -type AssistantMessage struct { - ID string `json:"id,required"` - Cost float64 `json:"cost,required"` - ModelID string `json:"modelID,required"` - Parts []AssistantMessagePart `json:"parts,required"` - Path AssistantMessagePath `json:"path,required"` - ProviderID string `json:"providerID,required"` - Role AssistantMessageRole `json:"role,required"` - SessionID string `json:"sessionID,required"` - System []string `json:"system,required"` - Time AssistantMessageTime `json:"time,required"` - Tokens AssistantMessageTokens `json:"tokens,required"` - Error AssistantMessageError `json:"error"` - Summary bool `json:"summary"` - JSON assistantMessageJSON `json:"-"` -} - -// assistantMessageJSON contains the JSON metadata for the struct -// [AssistantMessage] -type assistantMessageJSON struct { - ID apijson.Field - Cost apijson.Field - ModelID apijson.Field - Parts apijson.Field - Path apijson.Field - ProviderID apijson.Field - Role apijson.Field - SessionID apijson.Field - System apijson.Field - Time apijson.Field - Tokens apijson.Field - Error apijson.Field - Summary apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *AssistantMessage) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r assistantMessageJSON) RawJSON() string { - return r.raw -} - -func (r AssistantMessage) implementsMessage() {} - -type AssistantMessagePath struct { - Cwd string `json:"cwd,required"` - Root string `json:"root,required"` - JSON assistantMessagePathJSON `json:"-"` -} - -// assistantMessagePathJSON contains the JSON metadata for the struct -// [AssistantMessagePath] -type assistantMessagePathJSON struct { - Cwd apijson.Field - Root apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *AssistantMessagePath) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r assistantMessagePathJSON) RawJSON() string { - return r.raw -} - -type AssistantMessageRole string - -const ( - AssistantMessageRoleAssistant AssistantMessageRole = "assistant" -) - -func (r AssistantMessageRole) IsKnown() bool { - switch r { - case AssistantMessageRoleAssistant: - return true - } - return false -} - -type AssistantMessageTime struct { - Created float64 `json:"created,required"` - Completed float64 `json:"completed"` - JSON assistantMessageTimeJSON `json:"-"` -} - -// assistantMessageTimeJSON contains the JSON metadata for the struct -// [AssistantMessageTime] -type assistantMessageTimeJSON struct { - Created apijson.Field - Completed apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *AssistantMessageTime) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r assistantMessageTimeJSON) RawJSON() string { - return r.raw -} - -type AssistantMessageTokens struct { - Cache AssistantMessageTokensCache `json:"cache,required"` - Input float64 `json:"input,required"` - Output float64 `json:"output,required"` - Reasoning float64 `json:"reasoning,required"` - JSON assistantMessageTokensJSON `json:"-"` -} - -// assistantMessageTokensJSON contains the JSON metadata for the struct -// [AssistantMessageTokens] -type assistantMessageTokensJSON struct { - Cache apijson.Field - Input apijson.Field - Output apijson.Field - Reasoning apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *AssistantMessageTokens) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r assistantMessageTokensJSON) RawJSON() string { - return r.raw -} - -type AssistantMessageTokensCache struct { - Read float64 `json:"read,required"` - Write float64 `json:"write,required"` - JSON assistantMessageTokensCacheJSON `json:"-"` -} - -// assistantMessageTokensCacheJSON contains the JSON metadata for the struct -// [AssistantMessageTokensCache] -type assistantMessageTokensCacheJSON struct { - Read apijson.Field - Write apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *AssistantMessageTokensCache) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r assistantMessageTokensCacheJSON) RawJSON() string { - return r.raw -} - -type AssistantMessageError struct { - // This field can have the runtime type of [shared.ProviderAuthErrorData], - // [shared.UnknownErrorData], [interface{}]. - Data interface{} `json:"data,required"` - Name AssistantMessageErrorName `json:"name,required"` - JSON assistantMessageErrorJSON `json:"-"` - union AssistantMessageErrorUnion -} - -// assistantMessageErrorJSON contains the JSON metadata for the struct -// [AssistantMessageError] -type assistantMessageErrorJSON struct { - Data apijson.Field - Name apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r assistantMessageErrorJSON) RawJSON() string { - return r.raw -} - -func (r *AssistantMessageError) UnmarshalJSON(data []byte) (err error) { - *r = AssistantMessageError{} - err = apijson.UnmarshalRoot(data, &r.union) - if err != nil { - return err - } - return apijson.Port(r.union, &r) -} - -// AsUnion returns a [AssistantMessageErrorUnion] interface which you can cast to -// the specific types for more type safety. -// -// Possible runtime types of the union are [shared.ProviderAuthError], -// [shared.UnknownError], [AssistantMessageErrorMessageOutputLengthError], -// [shared.MessageAbortedError]. -func (r AssistantMessageError) AsUnion() AssistantMessageErrorUnion { - return r.union -} - -// Union satisfied by [shared.ProviderAuthError], [shared.UnknownError], -// [AssistantMessageErrorMessageOutputLengthError] or [shared.MessageAbortedError]. -type AssistantMessageErrorUnion interface { - ImplementsAssistantMessageError() -} - -func init() { - apijson.RegisterUnion( - reflect.TypeOf((*AssistantMessageErrorUnion)(nil)).Elem(), - "name", - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(shared.ProviderAuthError{}), - DiscriminatorValue: "ProviderAuthError", - }, - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(shared.UnknownError{}), - DiscriminatorValue: "UnknownError", - }, - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(AssistantMessageErrorMessageOutputLengthError{}), - DiscriminatorValue: "MessageOutputLengthError", - }, - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(shared.MessageAbortedError{}), - DiscriminatorValue: "MessageAbortedError", - }, - ) -} - -type AssistantMessageErrorMessageOutputLengthError struct { - Data interface{} `json:"data,required"` - Name AssistantMessageErrorMessageOutputLengthErrorName `json:"name,required"` - JSON assistantMessageErrorMessageOutputLengthErrorJSON `json:"-"` -} - -// assistantMessageErrorMessageOutputLengthErrorJSON contains the JSON metadata for -// the struct [AssistantMessageErrorMessageOutputLengthError] -type assistantMessageErrorMessageOutputLengthErrorJSON struct { - Data apijson.Field - Name apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *AssistantMessageErrorMessageOutputLengthError) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r assistantMessageErrorMessageOutputLengthErrorJSON) RawJSON() string { - return r.raw -} - -func (r AssistantMessageErrorMessageOutputLengthError) ImplementsAssistantMessageError() {} - -type AssistantMessageErrorMessageOutputLengthErrorName string - -const ( - AssistantMessageErrorMessageOutputLengthErrorNameMessageOutputLengthError AssistantMessageErrorMessageOutputLengthErrorName = "MessageOutputLengthError" -) - -func (r AssistantMessageErrorMessageOutputLengthErrorName) IsKnown() bool { - switch r { - case AssistantMessageErrorMessageOutputLengthErrorNameMessageOutputLengthError: - return true - } - return false -} - -type AssistantMessageErrorName string - -const ( - AssistantMessageErrorNameProviderAuthError AssistantMessageErrorName = "ProviderAuthError" - AssistantMessageErrorNameUnknownError AssistantMessageErrorName = "UnknownError" - AssistantMessageErrorNameMessageOutputLengthError AssistantMessageErrorName = "MessageOutputLengthError" - AssistantMessageErrorNameMessageAbortedError AssistantMessageErrorName = "MessageAbortedError" -) - -func (r AssistantMessageErrorName) IsKnown() bool { - switch r { - case AssistantMessageErrorNameProviderAuthError, AssistantMessageErrorNameUnknownError, AssistantMessageErrorNameMessageOutputLengthError, AssistantMessageErrorNameMessageAbortedError: - return true - } - return false -} - -type AssistantMessagePart struct { - Type AssistantMessagePartType `json:"type,required"` - ID string `json:"id"` - // This field can have the runtime type of [ToolPartState]. - State interface{} `json:"state"` - Text string `json:"text"` - Tool string `json:"tool"` - JSON assistantMessagePartJSON `json:"-"` - union AssistantMessagePartUnion -} - -// assistantMessagePartJSON contains the JSON metadata for the struct -// [AssistantMessagePart] -type assistantMessagePartJSON struct { - Type apijson.Field - ID apijson.Field - State apijson.Field - Text apijson.Field - Tool apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r assistantMessagePartJSON) RawJSON() string { - return r.raw -} - -func (r *AssistantMessagePart) UnmarshalJSON(data []byte) (err error) { - *r = AssistantMessagePart{} - err = apijson.UnmarshalRoot(data, &r.union) - if err != nil { - return err - } - return apijson.Port(r.union, &r) -} - -// AsUnion returns a [AssistantMessagePartUnion] interface which you can cast to -// the specific types for more type safety. -// -// Possible runtime types of the union are [TextPart], [ToolPart], [StepStartPart]. -func (r AssistantMessagePart) AsUnion() AssistantMessagePartUnion { - return r.union -} - -// Union satisfied by [TextPart], [ToolPart] or [StepStartPart]. -type AssistantMessagePartUnion interface { - implementsAssistantMessagePart() -} - -func init() { - apijson.RegisterUnion( - reflect.TypeOf((*AssistantMessagePartUnion)(nil)).Elem(), - "type", - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(TextPart{}), - DiscriminatorValue: "text", - }, - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(ToolPart{}), - DiscriminatorValue: "tool", - }, - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(StepStartPart{}), - DiscriminatorValue: "step-start", - }, - ) -} - -type AssistantMessagePartType string - -const ( - AssistantMessagePartTypeText AssistantMessagePartType = "text" - AssistantMessagePartTypeTool AssistantMessagePartType = "tool" - AssistantMessagePartTypeStepStart AssistantMessagePartType = "step-start" -) - -func (r AssistantMessagePartType) IsKnown() bool { - switch r { - case AssistantMessagePartTypeText, AssistantMessagePartTypeTool, AssistantMessagePartTypeStepStart: - return true - } - return false -} - type FilePart struct { - Mime string `json:"mime,required"` - Type FilePartType `json:"type,required"` - URL string `json:"url,required"` - Filename string `json:"filename"` - JSON filePartJSON `json:"-"` + MediaType string `json:"mediaType,required"` + Type FilePartType `json:"type,required"` + URL string `json:"url,required"` + Filename string `json:"filename"` + JSON filePartJSON `json:"-"` } // filePartJSON contains the JSON metadata for the struct [FilePart] type filePartJSON struct { - Mime apijson.Field + MediaType apijson.Field Type apijson.Field URL apijson.Field Filename apijson.Field @@ -548,7 +174,7 @@ func (r filePartJSON) RawJSON() string { return r.raw } -func (r FilePart) implementsUserMessagePart() {} +func (r FilePart) implementsMessagePart() {} type FilePartType string @@ -565,69 +191,276 @@ func (r FilePartType) IsKnown() bool { } type FilePartParam struct { - Mime param.Field[string] `json:"mime,required"` - Type param.Field[FilePartType] `json:"type,required"` - URL param.Field[string] `json:"url,required"` - Filename param.Field[string] `json:"filename"` + MediaType param.Field[string] `json:"mediaType,required"` + Type param.Field[FilePartType] `json:"type,required"` + URL param.Field[string] `json:"url,required"` + Filename param.Field[string] `json:"filename"` } func (r FilePartParam) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } -func (r FilePartParam) implementsUserMessagePartUnionParam() {} +func (r FilePartParam) implementsMessagePartUnionParam() {} type Message struct { - ID string `json:"id,required"` - // This field can have the runtime type of [[]UserMessagePart], - // [[]AssistantMessagePart]. - Parts interface{} `json:"parts,required"` - Role MessageRole `json:"role,required"` - SessionID string `json:"sessionID,required"` - // This field can have the runtime type of [UserMessageTime], - // [AssistantMessageTime]. - Time interface{} `json:"time,required"` - Cost float64 `json:"cost"` - // This field can have the runtime type of [AssistantMessageError]. - Error interface{} `json:"error"` - ModelID string `json:"modelID"` - // This field can have the runtime type of [AssistantMessagePath]. - Path interface{} `json:"path"` - ProviderID string `json:"providerID"` - Summary bool `json:"summary"` - // This field can have the runtime type of [[]string]. - System interface{} `json:"system"` - // This field can have the runtime type of [AssistantMessageTokens]. - Tokens interface{} `json:"tokens"` - JSON messageJSON `json:"-"` - union MessageUnion + ID string `json:"id,required"` + Metadata MessageMetadata `json:"metadata,required"` + Parts []MessagePart `json:"parts,required"` + Role MessageRole `json:"role,required"` + JSON messageJSON `json:"-"` } // messageJSON contains the JSON metadata for the struct [Message] type messageJSON struct { ID apijson.Field + Metadata apijson.Field Parts apijson.Field Role apijson.Field - SessionID apijson.Field - Time apijson.Field - Cost apijson.Field - Error apijson.Field - ModelID apijson.Field - Path apijson.Field - ProviderID apijson.Field - Summary apijson.Field - System apijson.Field - Tokens apijson.Field raw string ExtraFields map[string]apijson.Field } +func (r *Message) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + func (r messageJSON) RawJSON() string { return r.raw } -func (r *Message) UnmarshalJSON(data []byte) (err error) { - *r = Message{} +type MessageMetadata struct { + SessionID string `json:"sessionID,required"` + Time MessageMetadataTime `json:"time,required"` + Tool map[string]MessageMetadataTool `json:"tool,required"` + Assistant MessageMetadataAssistant `json:"assistant"` + Error MessageMetadataError `json:"error"` + Snapshot string `json:"snapshot"` + JSON messageMetadataJSON `json:"-"` +} + +// messageMetadataJSON contains the JSON metadata for the struct [MessageMetadata] +type messageMetadataJSON struct { + SessionID apijson.Field + Time apijson.Field + Tool apijson.Field + Assistant apijson.Field + Error apijson.Field + Snapshot apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessageMetadata) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageMetadataJSON) RawJSON() string { + return r.raw +} + +type MessageMetadataTime struct { + Created float64 `json:"created,required"` + Completed float64 `json:"completed"` + JSON messageMetadataTimeJSON `json:"-"` +} + +// messageMetadataTimeJSON contains the JSON metadata for the struct +// [MessageMetadataTime] +type messageMetadataTimeJSON struct { + Created apijson.Field + Completed apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessageMetadataTime) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageMetadataTimeJSON) RawJSON() string { + return r.raw +} + +type MessageMetadataTool struct { + Time MessageMetadataToolTime `json:"time,required"` + Title string `json:"title,required"` + Snapshot string `json:"snapshot"` + ExtraFields map[string]interface{} `json:"-,extras"` + JSON messageMetadataToolJSON `json:"-"` +} + +// messageMetadataToolJSON contains the JSON metadata for the struct +// [MessageMetadataTool] +type messageMetadataToolJSON struct { + Time apijson.Field + Title apijson.Field + Snapshot apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessageMetadataTool) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageMetadataToolJSON) RawJSON() string { + return r.raw +} + +type MessageMetadataToolTime struct { + End float64 `json:"end,required"` + Start float64 `json:"start,required"` + JSON messageMetadataToolTimeJSON `json:"-"` +} + +// messageMetadataToolTimeJSON contains the JSON metadata for the struct +// [MessageMetadataToolTime] +type messageMetadataToolTimeJSON struct { + End apijson.Field + Start apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessageMetadataToolTime) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageMetadataToolTimeJSON) RawJSON() string { + return r.raw +} + +type MessageMetadataAssistant struct { + Cost float64 `json:"cost,required"` + ModelID string `json:"modelID,required"` + Path MessageMetadataAssistantPath `json:"path,required"` + ProviderID string `json:"providerID,required"` + System []string `json:"system,required"` + Tokens MessageMetadataAssistantTokens `json:"tokens,required"` + Summary bool `json:"summary"` + JSON messageMetadataAssistantJSON `json:"-"` +} + +// messageMetadataAssistantJSON contains the JSON metadata for the struct +// [MessageMetadataAssistant] +type messageMetadataAssistantJSON struct { + Cost apijson.Field + ModelID apijson.Field + Path apijson.Field + ProviderID apijson.Field + System apijson.Field + Tokens apijson.Field + Summary apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessageMetadataAssistant) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageMetadataAssistantJSON) RawJSON() string { + return r.raw +} + +type MessageMetadataAssistantPath struct { + Cwd string `json:"cwd,required"` + Root string `json:"root,required"` + JSON messageMetadataAssistantPathJSON `json:"-"` +} + +// messageMetadataAssistantPathJSON contains the JSON metadata for the struct +// [MessageMetadataAssistantPath] +type messageMetadataAssistantPathJSON struct { + Cwd apijson.Field + Root apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessageMetadataAssistantPath) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageMetadataAssistantPathJSON) RawJSON() string { + return r.raw +} + +type MessageMetadataAssistantTokens struct { + Cache MessageMetadataAssistantTokensCache `json:"cache,required"` + Input float64 `json:"input,required"` + Output float64 `json:"output,required"` + Reasoning float64 `json:"reasoning,required"` + JSON messageMetadataAssistantTokensJSON `json:"-"` +} + +// messageMetadataAssistantTokensJSON contains the JSON metadata for the struct +// [MessageMetadataAssistantTokens] +type messageMetadataAssistantTokensJSON struct { + Cache apijson.Field + Input apijson.Field + Output apijson.Field + Reasoning apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessageMetadataAssistantTokens) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageMetadataAssistantTokensJSON) RawJSON() string { + return r.raw +} + +type MessageMetadataAssistantTokensCache struct { + Read float64 `json:"read,required"` + Write float64 `json:"write,required"` + JSON messageMetadataAssistantTokensCacheJSON `json:"-"` +} + +// messageMetadataAssistantTokensCacheJSON contains the JSON metadata for the +// struct [MessageMetadataAssistantTokensCache] +type messageMetadataAssistantTokensCacheJSON struct { + Read apijson.Field + Write apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessageMetadataAssistantTokensCache) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageMetadataAssistantTokensCacheJSON) RawJSON() string { + return r.raw +} + +type MessageMetadataError struct { + // This field can have the runtime type of [shared.ProviderAuthErrorData], + // [shared.UnknownErrorData], [interface{}]. + Data interface{} `json:"data,required"` + Name MessageMetadataErrorName `json:"name,required"` + JSON messageMetadataErrorJSON `json:"-"` + union MessageMetadataErrorUnion +} + +// messageMetadataErrorJSON contains the JSON metadata for the struct +// [MessageMetadataError] +type messageMetadataErrorJSON struct { + Data apijson.Field + Name apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r messageMetadataErrorJSON) RawJSON() string { + return r.raw +} + +func (r *MessageMetadataError) UnmarshalJSON(data []byte) (err error) { + *r = MessageMetadataError{} err = apijson.UnmarshalRoot(data, &r.union) if err != nil { return err @@ -635,36 +468,98 @@ func (r *Message) UnmarshalJSON(data []byte) (err error) { return apijson.Port(r.union, &r) } -// AsUnion returns a [MessageUnion] interface which you can cast to the specific -// types for more type safety. +// AsUnion returns a [MessageMetadataErrorUnion] interface which you can cast to +// the specific types for more type safety. // -// Possible runtime types of the union are [UserMessage], [AssistantMessage]. -func (r Message) AsUnion() MessageUnion { +// Possible runtime types of the union are [shared.ProviderAuthError], +// [shared.UnknownError], [MessageMetadataErrorMessageOutputLengthError]. +func (r MessageMetadataError) AsUnion() MessageMetadataErrorUnion { return r.union } -// Union satisfied by [UserMessage] or [AssistantMessage]. -type MessageUnion interface { - implementsMessage() +// Union satisfied by [shared.ProviderAuthError], [shared.UnknownError] or +// [MessageMetadataErrorMessageOutputLengthError]. +type MessageMetadataErrorUnion interface { + ImplementsMessageMetadataError() } func init() { apijson.RegisterUnion( - reflect.TypeOf((*MessageUnion)(nil)).Elem(), - "role", + reflect.TypeOf((*MessageMetadataErrorUnion)(nil)).Elem(), + "name", apijson.UnionVariant{ TypeFilter: gjson.JSON, - Type: reflect.TypeOf(UserMessage{}), - DiscriminatorValue: "user", + Type: reflect.TypeOf(shared.ProviderAuthError{}), + DiscriminatorValue: "ProviderAuthError", }, apijson.UnionVariant{ TypeFilter: gjson.JSON, - Type: reflect.TypeOf(AssistantMessage{}), - DiscriminatorValue: "assistant", + Type: reflect.TypeOf(shared.UnknownError{}), + DiscriminatorValue: "UnknownError", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(MessageMetadataErrorMessageOutputLengthError{}), + DiscriminatorValue: "MessageOutputLengthError", }, ) } +type MessageMetadataErrorMessageOutputLengthError struct { + Data interface{} `json:"data,required"` + Name MessageMetadataErrorMessageOutputLengthErrorName `json:"name,required"` + JSON messageMetadataErrorMessageOutputLengthErrorJSON `json:"-"` +} + +// messageMetadataErrorMessageOutputLengthErrorJSON contains the JSON metadata for +// the struct [MessageMetadataErrorMessageOutputLengthError] +type messageMetadataErrorMessageOutputLengthErrorJSON struct { + Data apijson.Field + Name apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessageMetadataErrorMessageOutputLengthError) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageMetadataErrorMessageOutputLengthErrorJSON) RawJSON() string { + return r.raw +} + +func (r MessageMetadataErrorMessageOutputLengthError) ImplementsMessageMetadataError() {} + +type MessageMetadataErrorMessageOutputLengthErrorName string + +const ( + MessageMetadataErrorMessageOutputLengthErrorNameMessageOutputLengthError MessageMetadataErrorMessageOutputLengthErrorName = "MessageOutputLengthError" +) + +func (r MessageMetadataErrorMessageOutputLengthErrorName) IsKnown() bool { + switch r { + case MessageMetadataErrorMessageOutputLengthErrorNameMessageOutputLengthError: + return true + } + return false +} + +type MessageMetadataErrorName string + +const ( + MessageMetadataErrorNameProviderAuthError MessageMetadataErrorName = "ProviderAuthError" + MessageMetadataErrorNameUnknownError MessageMetadataErrorName = "UnknownError" + MessageMetadataErrorNameMessageOutputLengthError MessageMetadataErrorName = "MessageOutputLengthError" +) + +func (r MessageMetadataErrorName) IsKnown() bool { + switch r { + case MessageMetadataErrorNameProviderAuthError, MessageMetadataErrorNameUnknownError, MessageMetadataErrorNameMessageOutputLengthError: + return true + } + return false +} + type MessageRole string const ( @@ -680,6 +575,197 @@ func (r MessageRole) IsKnown() bool { return false } +type MessagePart struct { + Type MessagePartType `json:"type,required"` + Filename string `json:"filename"` + MediaType string `json:"mediaType"` + // This field can have the runtime type of [map[string]interface{}]. + ProviderMetadata interface{} `json:"providerMetadata"` + SourceID string `json:"sourceId"` + Text string `json:"text"` + Title string `json:"title"` + // This field can have the runtime type of [ToolInvocationPartToolInvocation]. + ToolInvocation interface{} `json:"toolInvocation"` + URL string `json:"url"` + JSON messagePartJSON `json:"-"` + union MessagePartUnion +} + +// messagePartJSON contains the JSON metadata for the struct [MessagePart] +type messagePartJSON struct { + Type apijson.Field + Filename apijson.Field + MediaType apijson.Field + ProviderMetadata apijson.Field + SourceID apijson.Field + Text apijson.Field + Title apijson.Field + ToolInvocation apijson.Field + URL apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r messagePartJSON) RawJSON() string { + return r.raw +} + +func (r *MessagePart) UnmarshalJSON(data []byte) (err error) { + *r = MessagePart{} + err = apijson.UnmarshalRoot(data, &r.union) + if err != nil { + return err + } + return apijson.Port(r.union, &r) +} + +// AsUnion returns a [MessagePartUnion] interface which you can cast to the +// specific types for more type safety. +// +// Possible runtime types of the union are [TextPart], [ReasoningPart], +// [ToolInvocationPart], [SourceURLPart], [FilePart], [StepStartPart]. +func (r MessagePart) AsUnion() MessagePartUnion { + return r.union +} + +// Union satisfied by [TextPart], [ReasoningPart], [ToolInvocationPart], +// [SourceURLPart], [FilePart] or [StepStartPart]. +type MessagePartUnion interface { + implementsMessagePart() +} + +func init() { + apijson.RegisterUnion( + reflect.TypeOf((*MessagePartUnion)(nil)).Elem(), + "type", + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(TextPart{}), + DiscriminatorValue: "text", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ReasoningPart{}), + DiscriminatorValue: "reasoning", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ToolInvocationPart{}), + DiscriminatorValue: "tool-invocation", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(SourceURLPart{}), + DiscriminatorValue: "source-url", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(FilePart{}), + DiscriminatorValue: "file", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(StepStartPart{}), + DiscriminatorValue: "step-start", + }, + ) +} + +type MessagePartType string + +const ( + MessagePartTypeText MessagePartType = "text" + MessagePartTypeReasoning MessagePartType = "reasoning" + MessagePartTypeToolInvocation MessagePartType = "tool-invocation" + MessagePartTypeSourceURL MessagePartType = "source-url" + MessagePartTypeFile MessagePartType = "file" + MessagePartTypeStepStart MessagePartType = "step-start" +) + +func (r MessagePartType) IsKnown() bool { + switch r { + case MessagePartTypeText, MessagePartTypeReasoning, MessagePartTypeToolInvocation, MessagePartTypeSourceURL, MessagePartTypeFile, MessagePartTypeStepStart: + return true + } + return false +} + +type MessagePartParam struct { + Type param.Field[MessagePartType] `json:"type,required"` + Filename param.Field[string] `json:"filename"` + MediaType param.Field[string] `json:"mediaType"` + ProviderMetadata param.Field[interface{}] `json:"providerMetadata"` + SourceID param.Field[string] `json:"sourceId"` + Text param.Field[string] `json:"text"` + Title param.Field[string] `json:"title"` + ToolInvocation param.Field[interface{}] `json:"toolInvocation"` + URL param.Field[string] `json:"url"` +} + +func (r MessagePartParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r MessagePartParam) implementsMessagePartUnionParam() {} + +// Satisfied by [TextPartParam], [ReasoningPartParam], [ToolInvocationPartParam], +// [SourceURLPartParam], [FilePartParam], [StepStartPartParam], [MessagePartParam]. +type MessagePartUnionParam interface { + implementsMessagePartUnionParam() +} + +type ReasoningPart struct { + Text string `json:"text,required"` + Type ReasoningPartType `json:"type,required"` + ProviderMetadata map[string]interface{} `json:"providerMetadata"` + JSON reasoningPartJSON `json:"-"` +} + +// reasoningPartJSON contains the JSON metadata for the struct [ReasoningPart] +type reasoningPartJSON struct { + Text apijson.Field + Type apijson.Field + ProviderMetadata apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ReasoningPart) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r reasoningPartJSON) RawJSON() string { + return r.raw +} + +func (r ReasoningPart) implementsMessagePart() {} + +type ReasoningPartType string + +const ( + ReasoningPartTypeReasoning ReasoningPartType = "reasoning" +) + +func (r ReasoningPartType) IsKnown() bool { + switch r { + case ReasoningPartTypeReasoning: + return true + } + return false +} + +type ReasoningPartParam struct { + Text param.Field[string] `json:"text,required"` + Type param.Field[ReasoningPartType] `json:"type,required"` + ProviderMetadata param.Field[map[string]interface{}] `json:"providerMetadata"` +} + +func (r ReasoningPartParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ReasoningPartParam) implementsMessagePartUnionParam() {} + type Session struct { ID string `json:"id,required"` Time SessionTime `json:"time,required"` @@ -778,6 +864,64 @@ func (r sessionShareJSON) RawJSON() string { return r.raw } +type SourceURLPart struct { + SourceID string `json:"sourceId,required"` + Type SourceURLPartType `json:"type,required"` + URL string `json:"url,required"` + ProviderMetadata map[string]interface{} `json:"providerMetadata"` + Title string `json:"title"` + JSON sourceURLPartJSON `json:"-"` +} + +// sourceURLPartJSON contains the JSON metadata for the struct [SourceURLPart] +type sourceURLPartJSON struct { + SourceID apijson.Field + Type apijson.Field + URL apijson.Field + ProviderMetadata apijson.Field + Title apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *SourceURLPart) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r sourceURLPartJSON) RawJSON() string { + return r.raw +} + +func (r SourceURLPart) implementsMessagePart() {} + +type SourceURLPartType string + +const ( + SourceURLPartTypeSourceURL SourceURLPartType = "source-url" +) + +func (r SourceURLPartType) IsKnown() bool { + switch r { + case SourceURLPartTypeSourceURL: + return true + } + return false +} + +type SourceURLPartParam struct { + SourceID param.Field[string] `json:"sourceId,required"` + Type param.Field[SourceURLPartType] `json:"type,required"` + URL param.Field[string] `json:"url,required"` + ProviderMetadata param.Field[map[string]interface{}] `json:"providerMetadata"` + Title param.Field[string] `json:"title"` +} + +func (r SourceURLPartParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r SourceURLPartParam) implementsMessagePartUnionParam() {} + type StepStartPart struct { Type StepStartPartType `json:"type,required"` JSON stepStartPartJSON `json:"-"` @@ -798,7 +942,7 @@ func (r stepStartPartJSON) RawJSON() string { return r.raw } -func (r StepStartPart) implementsAssistantMessagePart() {} +func (r StepStartPart) implementsMessagePart() {} type StepStartPartType string @@ -814,6 +958,16 @@ func (r StepStartPartType) IsKnown() bool { return false } +type StepStartPartParam struct { + Type param.Field[StepStartPartType] `json:"type,required"` +} + +func (r StepStartPartParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r StepStartPartParam) implementsMessagePartUnionParam() {} + type TextPart struct { Text string `json:"text,required"` Type TextPartType `json:"type,required"` @@ -836,9 +990,7 @@ func (r textPartJSON) RawJSON() string { return r.raw } -func (r TextPart) implementsAssistantMessagePart() {} - -func (r TextPart) implementsUserMessagePart() {} +func (r TextPart) implementsMessagePart() {} type TextPartType string @@ -863,555 +1015,351 @@ func (r TextPartParam) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } -func (r TextPartParam) implementsUserMessagePartUnionParam() {} +func (r TextPartParam) implementsMessagePartUnionParam() {} -type ToolPart struct { - ID string `json:"id,required"` - State ToolPartState `json:"state,required"` - Tool string `json:"tool,required"` - Type ToolPartType `json:"type,required"` - JSON toolPartJSON `json:"-"` +type ToolCall struct { + State ToolCallState `json:"state,required"` + ToolCallID string `json:"toolCallId,required"` + ToolName string `json:"toolName,required"` + Args interface{} `json:"args"` + Step float64 `json:"step"` + JSON toolCallJSON `json:"-"` } -// toolPartJSON contains the JSON metadata for the struct [ToolPart] -type toolPartJSON struct { - ID apijson.Field +// toolCallJSON contains the JSON metadata for the struct [ToolCall] +type toolCallJSON struct { State apijson.Field - Tool apijson.Field - Type apijson.Field + ToolCallID apijson.Field + ToolName apijson.Field + Args apijson.Field + Step apijson.Field raw string ExtraFields map[string]apijson.Field } -func (r *ToolPart) UnmarshalJSON(data []byte) (err error) { +func (r *ToolCall) UnmarshalJSON(data []byte) (err error) { return apijson.UnmarshalRoot(data, r) } -func (r toolPartJSON) RawJSON() string { +func (r toolCallJSON) RawJSON() string { return r.raw } -func (r ToolPart) implementsAssistantMessagePart() {} +func (r ToolCall) implementsToolInvocationPartToolInvocation() {} -type ToolPartState struct { - Status ToolPartStateStatus `json:"status,required"` - Error string `json:"error"` - // This field can have the runtime type of [interface{}], [map[string]interface{}]. - Input interface{} `json:"input"` - // This field can have the runtime type of [map[string]interface{}]. - Metadata interface{} `json:"metadata"` - Output string `json:"output"` - // This field can have the runtime type of [ToolStateRunningTime], - // [ToolStateCompletedTime], [ToolStateErrorTime]. - Time interface{} `json:"time"` - Title string `json:"title"` - JSON toolPartStateJSON `json:"-"` - union ToolPartStateUnion -} - -// toolPartStateJSON contains the JSON metadata for the struct [ToolPartState] -type toolPartStateJSON struct { - Status apijson.Field - Error apijson.Field - Input apijson.Field - Metadata apijson.Field - Output apijson.Field - Time apijson.Field - Title apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r toolPartStateJSON) RawJSON() string { - return r.raw -} - -func (r *ToolPartState) UnmarshalJSON(data []byte) (err error) { - *r = ToolPartState{} - err = apijson.UnmarshalRoot(data, &r.union) - if err != nil { - return err - } - return apijson.Port(r.union, &r) -} - -// AsUnion returns a [ToolPartStateUnion] interface which you can cast to the -// specific types for more type safety. -// -// Possible runtime types of the union are [ToolStatePending], [ToolStateRunning], -// [ToolStateCompleted], [ToolStateError]. -func (r ToolPartState) AsUnion() ToolPartStateUnion { - return r.union -} - -// Union satisfied by [ToolStatePending], [ToolStateRunning], [ToolStateCompleted] -// or [ToolStateError]. -type ToolPartStateUnion interface { - implementsToolPartState() -} - -func init() { - apijson.RegisterUnion( - reflect.TypeOf((*ToolPartStateUnion)(nil)).Elem(), - "status", - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(ToolStatePending{}), - DiscriminatorValue: "pending", - }, - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(ToolStateRunning{}), - DiscriminatorValue: "running", - }, - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(ToolStateCompleted{}), - DiscriminatorValue: "completed", - }, - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(ToolStateError{}), - DiscriminatorValue: "error", - }, - ) -} - -type ToolPartStateStatus string +type ToolCallState string const ( - ToolPartStateStatusPending ToolPartStateStatus = "pending" - ToolPartStateStatusRunning ToolPartStateStatus = "running" - ToolPartStateStatusCompleted ToolPartStateStatus = "completed" - ToolPartStateStatusError ToolPartStateStatus = "error" + ToolCallStateCall ToolCallState = "call" ) -func (r ToolPartStateStatus) IsKnown() bool { +func (r ToolCallState) IsKnown() bool { switch r { - case ToolPartStateStatusPending, ToolPartStateStatusRunning, ToolPartStateStatusCompleted, ToolPartStateStatusError: + case ToolCallStateCall: return true } return false } -type ToolPartType string - -const ( - ToolPartTypeTool ToolPartType = "tool" -) - -func (r ToolPartType) IsKnown() bool { - switch r { - case ToolPartTypeTool: - return true - } - return false +type ToolCallParam struct { + State param.Field[ToolCallState] `json:"state,required"` + ToolCallID param.Field[string] `json:"toolCallId,required"` + ToolName param.Field[string] `json:"toolName,required"` + Args param.Field[interface{}] `json:"args"` + Step param.Field[float64] `json:"step"` } -type ToolStateCompleted struct { - Input map[string]interface{} `json:"input,required"` - Metadata map[string]interface{} `json:"metadata,required"` - Output string `json:"output,required"` - Status ToolStateCompletedStatus `json:"status,required"` - Time ToolStateCompletedTime `json:"time,required"` - Title string `json:"title,required"` - JSON toolStateCompletedJSON `json:"-"` -} - -// toolStateCompletedJSON contains the JSON metadata for the struct -// [ToolStateCompleted] -type toolStateCompletedJSON struct { - Input apijson.Field - Metadata apijson.Field - Output apijson.Field - Status apijson.Field - Time apijson.Field - Title apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *ToolStateCompleted) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r toolStateCompletedJSON) RawJSON() string { - return r.raw -} - -func (r ToolStateCompleted) implementsToolPartState() {} - -type ToolStateCompletedStatus string - -const ( - ToolStateCompletedStatusCompleted ToolStateCompletedStatus = "completed" -) - -func (r ToolStateCompletedStatus) IsKnown() bool { - switch r { - case ToolStateCompletedStatusCompleted: - return true - } - return false -} - -type ToolStateCompletedTime struct { - End float64 `json:"end,required"` - Start float64 `json:"start,required"` - JSON toolStateCompletedTimeJSON `json:"-"` -} - -// toolStateCompletedTimeJSON contains the JSON metadata for the struct -// [ToolStateCompletedTime] -type toolStateCompletedTimeJSON struct { - End apijson.Field - Start apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *ToolStateCompletedTime) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r toolStateCompletedTimeJSON) RawJSON() string { - return r.raw -} - -type ToolStateError struct { - Error string `json:"error,required"` - Input map[string]interface{} `json:"input,required"` - Status ToolStateErrorStatus `json:"status,required"` - Time ToolStateErrorTime `json:"time,required"` - JSON toolStateErrorJSON `json:"-"` -} - -// toolStateErrorJSON contains the JSON metadata for the struct [ToolStateError] -type toolStateErrorJSON struct { - Error apijson.Field - Input apijson.Field - Status apijson.Field - Time apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *ToolStateError) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r toolStateErrorJSON) RawJSON() string { - return r.raw -} - -func (r ToolStateError) implementsToolPartState() {} - -type ToolStateErrorStatus string - -const ( - ToolStateErrorStatusError ToolStateErrorStatus = "error" -) - -func (r ToolStateErrorStatus) IsKnown() bool { - switch r { - case ToolStateErrorStatusError: - return true - } - return false -} - -type ToolStateErrorTime struct { - End float64 `json:"end,required"` - Start float64 `json:"start,required"` - JSON toolStateErrorTimeJSON `json:"-"` -} - -// toolStateErrorTimeJSON contains the JSON metadata for the struct -// [ToolStateErrorTime] -type toolStateErrorTimeJSON struct { - End apijson.Field - Start apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *ToolStateErrorTime) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r toolStateErrorTimeJSON) RawJSON() string { - return r.raw -} - -type ToolStatePending struct { - Status ToolStatePendingStatus `json:"status,required"` - JSON toolStatePendingJSON `json:"-"` -} - -// toolStatePendingJSON contains the JSON metadata for the struct -// [ToolStatePending] -type toolStatePendingJSON struct { - Status apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *ToolStatePending) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r toolStatePendingJSON) RawJSON() string { - return r.raw -} - -func (r ToolStatePending) implementsToolPartState() {} - -type ToolStatePendingStatus string - -const ( - ToolStatePendingStatusPending ToolStatePendingStatus = "pending" -) - -func (r ToolStatePendingStatus) IsKnown() bool { - switch r { - case ToolStatePendingStatusPending: - return true - } - return false -} - -type ToolStateRunning struct { - Status ToolStateRunningStatus `json:"status,required"` - Time ToolStateRunningTime `json:"time,required"` - Input interface{} `json:"input"` - Metadata map[string]interface{} `json:"metadata"` - Title string `json:"title"` - JSON toolStateRunningJSON `json:"-"` -} - -// toolStateRunningJSON contains the JSON metadata for the struct -// [ToolStateRunning] -type toolStateRunningJSON struct { - Status apijson.Field - Time apijson.Field - Input apijson.Field - Metadata apijson.Field - Title apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *ToolStateRunning) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r toolStateRunningJSON) RawJSON() string { - return r.raw -} - -func (r ToolStateRunning) implementsToolPartState() {} - -type ToolStateRunningStatus string - -const ( - ToolStateRunningStatusRunning ToolStateRunningStatus = "running" -) - -func (r ToolStateRunningStatus) IsKnown() bool { - switch r { - case ToolStateRunningStatusRunning: - return true - } - return false -} - -type ToolStateRunningTime struct { - Start float64 `json:"start,required"` - JSON toolStateRunningTimeJSON `json:"-"` -} - -// toolStateRunningTimeJSON contains the JSON metadata for the struct -// [ToolStateRunningTime] -type toolStateRunningTimeJSON struct { - Start apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *ToolStateRunningTime) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r toolStateRunningTimeJSON) RawJSON() string { - return r.raw -} - -type UserMessage struct { - ID string `json:"id,required"` - Parts []UserMessagePart `json:"parts,required"` - Role UserMessageRole `json:"role,required"` - SessionID string `json:"sessionID,required"` - Time UserMessageTime `json:"time,required"` - JSON userMessageJSON `json:"-"` -} - -// userMessageJSON contains the JSON metadata for the struct [UserMessage] -type userMessageJSON struct { - ID apijson.Field - Parts apijson.Field - Role apijson.Field - SessionID apijson.Field - Time apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *UserMessage) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r userMessageJSON) RawJSON() string { - return r.raw -} - -func (r UserMessage) implementsMessage() {} - -type UserMessageRole string - -const ( - UserMessageRoleUser UserMessageRole = "user" -) - -func (r UserMessageRole) IsKnown() bool { - switch r { - case UserMessageRoleUser: - return true - } - return false -} - -type UserMessageTime struct { - Created float64 `json:"created,required"` - JSON userMessageTimeJSON `json:"-"` -} - -// userMessageTimeJSON contains the JSON metadata for the struct [UserMessageTime] -type userMessageTimeJSON struct { - Created apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *UserMessageTime) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r userMessageTimeJSON) RawJSON() string { - return r.raw -} - -type UserMessagePart struct { - Type UserMessagePartType `json:"type,required"` - Filename string `json:"filename"` - Mime string `json:"mime"` - Text string `json:"text"` - URL string `json:"url"` - JSON userMessagePartJSON `json:"-"` - union UserMessagePartUnion -} - -// userMessagePartJSON contains the JSON metadata for the struct [UserMessagePart] -type userMessagePartJSON struct { - Type apijson.Field - Filename apijson.Field - Mime apijson.Field - Text apijson.Field - URL apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r userMessagePartJSON) RawJSON() string { - return r.raw -} - -func (r *UserMessagePart) UnmarshalJSON(data []byte) (err error) { - *r = UserMessagePart{} - err = apijson.UnmarshalRoot(data, &r.union) - if err != nil { - return err - } - return apijson.Port(r.union, &r) -} - -// AsUnion returns a [UserMessagePartUnion] interface which you can cast to the -// specific types for more type safety. -// -// Possible runtime types of the union are [TextPart], [FilePart]. -func (r UserMessagePart) AsUnion() UserMessagePartUnion { - return r.union -} - -// Union satisfied by [TextPart] or [FilePart]. -type UserMessagePartUnion interface { - implementsUserMessagePart() -} - -func init() { - apijson.RegisterUnion( - reflect.TypeOf((*UserMessagePartUnion)(nil)).Elem(), - "type", - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(TextPart{}), - DiscriminatorValue: "text", - }, - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(FilePart{}), - DiscriminatorValue: "file", - }, - ) -} - -type UserMessagePartType string - -const ( - UserMessagePartTypeText UserMessagePartType = "text" - UserMessagePartTypeFile UserMessagePartType = "file" -) - -func (r UserMessagePartType) IsKnown() bool { - switch r { - case UserMessagePartTypeText, UserMessagePartTypeFile: - return true - } - return false -} - -type UserMessagePartParam struct { - Type param.Field[UserMessagePartType] `json:"type,required"` - Filename param.Field[string] `json:"filename"` - Mime param.Field[string] `json:"mime"` - Text param.Field[string] `json:"text"` - URL param.Field[string] `json:"url"` -} - -func (r UserMessagePartParam) MarshalJSON() (data []byte, err error) { +func (r ToolCallParam) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } -func (r UserMessagePartParam) implementsUserMessagePartUnionParam() {} +func (r ToolCallParam) implementsToolInvocationPartToolInvocationUnionParam() {} -// Satisfied by [TextPartParam], [FilePartParam], [UserMessagePartParam]. -type UserMessagePartUnionParam interface { - implementsUserMessagePartUnionParam() +type ToolInvocationPart struct { + ToolInvocation ToolInvocationPartToolInvocation `json:"toolInvocation,required"` + Type ToolInvocationPartType `json:"type,required"` + JSON toolInvocationPartJSON `json:"-"` } +// toolInvocationPartJSON contains the JSON metadata for the struct +// [ToolInvocationPart] +type toolInvocationPartJSON struct { + ToolInvocation apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ToolInvocationPart) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r toolInvocationPartJSON) RawJSON() string { + return r.raw +} + +func (r ToolInvocationPart) implementsMessagePart() {} + +type ToolInvocationPartToolInvocation struct { + State ToolInvocationPartToolInvocationState `json:"state,required"` + ToolCallID string `json:"toolCallId,required"` + ToolName string `json:"toolName,required"` + // This field can have the runtime type of [interface{}]. + Args interface{} `json:"args"` + Result string `json:"result"` + Step float64 `json:"step"` + JSON toolInvocationPartToolInvocationJSON `json:"-"` + union ToolInvocationPartToolInvocationUnion +} + +// toolInvocationPartToolInvocationJSON contains the JSON metadata for the struct +// [ToolInvocationPartToolInvocation] +type toolInvocationPartToolInvocationJSON struct { + State apijson.Field + ToolCallID apijson.Field + ToolName apijson.Field + Args apijson.Field + Result apijson.Field + Step apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r toolInvocationPartToolInvocationJSON) RawJSON() string { + return r.raw +} + +func (r *ToolInvocationPartToolInvocation) UnmarshalJSON(data []byte) (err error) { + *r = ToolInvocationPartToolInvocation{} + err = apijson.UnmarshalRoot(data, &r.union) + if err != nil { + return err + } + return apijson.Port(r.union, &r) +} + +// AsUnion returns a [ToolInvocationPartToolInvocationUnion] interface which you +// can cast to the specific types for more type safety. +// +// Possible runtime types of the union are [ToolCall], [ToolPartialCall], +// [ToolResult]. +func (r ToolInvocationPartToolInvocation) AsUnion() ToolInvocationPartToolInvocationUnion { + return r.union +} + +// Union satisfied by [ToolCall], [ToolPartialCall] or [ToolResult]. +type ToolInvocationPartToolInvocationUnion interface { + implementsToolInvocationPartToolInvocation() +} + +func init() { + apijson.RegisterUnion( + reflect.TypeOf((*ToolInvocationPartToolInvocationUnion)(nil)).Elem(), + "state", + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ToolCall{}), + DiscriminatorValue: "call", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ToolPartialCall{}), + DiscriminatorValue: "partial-call", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ToolResult{}), + DiscriminatorValue: "result", + }, + ) +} + +type ToolInvocationPartToolInvocationState string + +const ( + ToolInvocationPartToolInvocationStateCall ToolInvocationPartToolInvocationState = "call" + ToolInvocationPartToolInvocationStatePartialCall ToolInvocationPartToolInvocationState = "partial-call" + ToolInvocationPartToolInvocationStateResult ToolInvocationPartToolInvocationState = "result" +) + +func (r ToolInvocationPartToolInvocationState) IsKnown() bool { + switch r { + case ToolInvocationPartToolInvocationStateCall, ToolInvocationPartToolInvocationStatePartialCall, ToolInvocationPartToolInvocationStateResult: + return true + } + return false +} + +type ToolInvocationPartType string + +const ( + ToolInvocationPartTypeToolInvocation ToolInvocationPartType = "tool-invocation" +) + +func (r ToolInvocationPartType) IsKnown() bool { + switch r { + case ToolInvocationPartTypeToolInvocation: + return true + } + return false +} + +type ToolInvocationPartParam struct { + ToolInvocation param.Field[ToolInvocationPartToolInvocationUnionParam] `json:"toolInvocation,required"` + Type param.Field[ToolInvocationPartType] `json:"type,required"` +} + +func (r ToolInvocationPartParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolInvocationPartParam) implementsMessagePartUnionParam() {} + +type ToolInvocationPartToolInvocationParam struct { + State param.Field[ToolInvocationPartToolInvocationState] `json:"state,required"` + ToolCallID param.Field[string] `json:"toolCallId,required"` + ToolName param.Field[string] `json:"toolName,required"` + Args param.Field[interface{}] `json:"args"` + Result param.Field[string] `json:"result"` + Step param.Field[float64] `json:"step"` +} + +func (r ToolInvocationPartToolInvocationParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolInvocationPartToolInvocationParam) implementsToolInvocationPartToolInvocationUnionParam() { +} + +// Satisfied by [ToolCallParam], [ToolPartialCallParam], [ToolResultParam], +// [ToolInvocationPartToolInvocationParam]. +type ToolInvocationPartToolInvocationUnionParam interface { + implementsToolInvocationPartToolInvocationUnionParam() +} + +type ToolPartialCall struct { + State ToolPartialCallState `json:"state,required"` + ToolCallID string `json:"toolCallId,required"` + ToolName string `json:"toolName,required"` + Args interface{} `json:"args"` + Step float64 `json:"step"` + JSON toolPartialCallJSON `json:"-"` +} + +// toolPartialCallJSON contains the JSON metadata for the struct [ToolPartialCall] +type toolPartialCallJSON struct { + State apijson.Field + ToolCallID apijson.Field + ToolName apijson.Field + Args apijson.Field + Step apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ToolPartialCall) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r toolPartialCallJSON) RawJSON() string { + return r.raw +} + +func (r ToolPartialCall) implementsToolInvocationPartToolInvocation() {} + +type ToolPartialCallState string + +const ( + ToolPartialCallStatePartialCall ToolPartialCallState = "partial-call" +) + +func (r ToolPartialCallState) IsKnown() bool { + switch r { + case ToolPartialCallStatePartialCall: + return true + } + return false +} + +type ToolPartialCallParam struct { + State param.Field[ToolPartialCallState] `json:"state,required"` + ToolCallID param.Field[string] `json:"toolCallId,required"` + ToolName param.Field[string] `json:"toolName,required"` + Args param.Field[interface{}] `json:"args"` + Step param.Field[float64] `json:"step"` +} + +func (r ToolPartialCallParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolPartialCallParam) implementsToolInvocationPartToolInvocationUnionParam() {} + +type ToolResult struct { + Result string `json:"result,required"` + State ToolResultState `json:"state,required"` + ToolCallID string `json:"toolCallId,required"` + ToolName string `json:"toolName,required"` + Args interface{} `json:"args"` + Step float64 `json:"step"` + JSON toolResultJSON `json:"-"` +} + +// toolResultJSON contains the JSON metadata for the struct [ToolResult] +type toolResultJSON struct { + Result apijson.Field + State apijson.Field + ToolCallID apijson.Field + ToolName apijson.Field + Args apijson.Field + Step apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ToolResult) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r toolResultJSON) RawJSON() string { + return r.raw +} + +func (r ToolResult) implementsToolInvocationPartToolInvocation() {} + +type ToolResultState string + +const ( + ToolResultStateResult ToolResultState = "result" +) + +func (r ToolResultState) IsKnown() bool { + switch r { + case ToolResultStateResult: + return true + } + return false +} + +type ToolResultParam struct { + Result param.Field[string] `json:"result,required"` + State param.Field[ToolResultState] `json:"state,required"` + ToolCallID param.Field[string] `json:"toolCallId,required"` + ToolName param.Field[string] `json:"toolName,required"` + Args param.Field[interface{}] `json:"args"` + Step param.Field[float64] `json:"step"` +} + +func (r ToolResultParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolResultParam) implementsToolInvocationPartToolInvocationUnionParam() {} + type SessionChatParams struct { - ModelID param.Field[string] `json:"modelID,required"` - Parts param.Field[[]UserMessagePartUnionParam] `json:"parts,required"` - ProviderID param.Field[string] `json:"providerID,required"` + ModelID param.Field[string] `json:"modelID,required"` + Parts param.Field[[]MessagePartUnionParam] `json:"parts,required"` + ProviderID param.Field[string] `json:"providerID,required"` } func (r SessionChatParams) MarshalJSON() (data []byte, err error) { diff --git a/packages/tui/sdk/session_test.go b/packages/tui/sdk/session_test.go index 15137e62..da9fb825 100644 --- a/packages/tui/sdk/session_test.go +++ b/packages/tui/sdk/session_test.go @@ -118,7 +118,7 @@ func TestSessionChat(t *testing.T) { "id", opencode.SessionChatParams{ ModelID: opencode.F("modelID"), - Parts: opencode.F([]opencode.UserMessagePartUnionParam{opencode.TextPartParam{ + Parts: opencode.F([]opencode.MessagePartUnionParam{opencode.TextPartParam{ Text: opencode.F("text"), Type: opencode.F(opencode.TextPartTypeText), }}), diff --git a/packages/tui/sdk/shared/shared.go b/packages/tui/sdk/shared/shared.go index 58baf3d9..121f64a5 100644 --- a/packages/tui/sdk/shared/shared.go +++ b/packages/tui/sdk/shared/shared.go @@ -6,47 +6,6 @@ import ( "github.com/sst/opencode-sdk-go/internal/apijson" ) -type MessageAbortedError struct { - Data interface{} `json:"data,required"` - Name MessageAbortedErrorName `json:"name,required"` - JSON messageAbortedErrorJSON `json:"-"` -} - -// messageAbortedErrorJSON contains the JSON metadata for the struct -// [MessageAbortedError] -type messageAbortedErrorJSON struct { - Data apijson.Field - Name apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *MessageAbortedError) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r messageAbortedErrorJSON) RawJSON() string { - return r.raw -} - -func (r MessageAbortedError) ImplementsEventListResponseEventSessionErrorPropertiesError() {} - -func (r MessageAbortedError) ImplementsAssistantMessageError() {} - -type MessageAbortedErrorName string - -const ( - MessageAbortedErrorNameMessageAbortedError MessageAbortedErrorName = "MessageAbortedError" -) - -func (r MessageAbortedErrorName) IsKnown() bool { - switch r { - case MessageAbortedErrorNameMessageAbortedError: - return true - } - return false -} - type ProviderAuthError struct { Data ProviderAuthErrorData `json:"data,required"` Name ProviderAuthErrorName `json:"name,required"` @@ -72,7 +31,7 @@ func (r providerAuthErrorJSON) RawJSON() string { func (r ProviderAuthError) ImplementsEventListResponseEventSessionErrorPropertiesError() {} -func (r ProviderAuthError) ImplementsAssistantMessageError() {} +func (r ProviderAuthError) ImplementsMessageMetadataError() {} type ProviderAuthErrorData struct { Message string `json:"message,required"` @@ -135,7 +94,7 @@ func (r unknownErrorJSON) RawJSON() string { func (r UnknownError) ImplementsEventListResponseEventSessionErrorPropertiesError() {} -func (r UnknownError) ImplementsAssistantMessageError() {} +func (r UnknownError) ImplementsMessageMetadataError() {} type UnknownErrorData struct { Message string `json:"message,required"` diff --git a/packages/web/astro.config.mjs b/packages/web/astro.config.mjs index 63b93b9d..0c3a5ecc 100644 --- a/packages/web/astro.config.mjs +++ b/packages/web/astro.config.mjs @@ -20,21 +20,18 @@ export default defineConfig({ devToolbar: { enabled: false, }, - server: { - host: "0.0.0.0", - }, markdown: { - rehypePlugins: [rehypeHeadingIds, [rehypeAutolinkHeadings, { behavior: "wrap" }]], + rehypePlugins: [ + rehypeHeadingIds, + [rehypeAutolinkHeadings, { behavior: "wrap" }], + ], }, integrations: [ solidJs(), starlight({ title: "opencode", expressiveCode: { themes: ["github-light", "github-dark"] }, - social: [ - { icon: "github", label: "GitHub", href: config.github }, - { icon: "discord", label: "Dscord", href: config.discord }, - ], + social: [{ icon: "github", label: "GitHub", href: config.github }], head: [ { tag: "link", diff --git a/packages/web/config.mjs b/packages/web/config.mjs index 5e4c571d..b6ec26e3 100644 --- a/packages/web/config.mjs +++ b/packages/web/config.mjs @@ -1,12 +1,7 @@ -const stage = process.env.SST_STAGE || "dev" - export default { - url: stage === "production" - ? "https://opencode.ai" - : `https://${stage}.opencode.ai`, + url: "https://opencode.ai", socialCard: "https://social-cards.sst.dev", github: "https://github.com/sst/opencode", - discord: "https://discord.gg/opencode", headerLinks: [ { name: "Home", url: "/" }, { name: "Docs", url: "/docs/" }, diff --git a/packages/web/package.json b/packages/web/package.json index bdd6467c..383b979f 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -4,7 +4,6 @@ "version": "0.0.1", "scripts": { "dev": "astro dev", - "dev:remote": "sst shell --stage=dev --target=Web astro dev", "start": "astro dev", "build": "astro build", "preview": "astro preview", @@ -25,7 +24,6 @@ "lang-map": "0.4.0", "luxon": "3.6.1", "marked": "15.0.12", - "marked-shiki": "1.2.0", "rehype-autolink-headings": "7.1.0", "sharp": "0.32.5", "shiki": "3.4.2", diff --git a/packages/web/public/theme.json b/packages/web/public/theme.json index b3e97f7c..0b1b95f0 100644 --- a/packages/web/public/theme.json +++ b/packages/web/public/theme.json @@ -88,7 +88,14 @@ "syntaxOperator": { "$ref": "#/definitions/colorValue" }, "syntaxPunctuation": { "$ref": "#/definitions/colorValue" } }, - "required": ["primary", "secondary", "accent", "text", "textMuted", "background"], + "required": [ + "primary", + "secondary", + "accent", + "text", + "textMuted", + "background" + ], "additionalProperties": false } }, diff --git a/packages/web/src/components/CodeBlock.tsx b/packages/web/src/components/CodeBlock.tsx index 3702044b..6a684689 100644 --- a/packages/web/src/components/CodeBlock.tsx +++ b/packages/web/src/components/CodeBlock.tsx @@ -1,4 +1,8 @@ -import { type JSX, splitProps, createResource } from "solid-js" +import { + type JSX, + splitProps, + createResource, +} from "solid-js" import { codeToHtml } from "shiki" import styles from "./codeblock.module.css" import { transformerNotationDiff } from "@shikijs/transformers" @@ -26,7 +30,7 @@ function CodeBlock(props: CodeBlockProps) { }, ) - return
+ return
} export default CodeBlock diff --git a/packages/web/src/components/share/content-diff.tsx b/packages/web/src/components/DiffView.tsx similarity index 67% rename from packages/web/src/components/share/content-diff.tsx rename to packages/web/src/components/DiffView.tsx index 894145c3..66dd7f0f 100644 --- a/packages/web/src/components/share/content-diff.tsx +++ b/packages/web/src/components/DiffView.tsx @@ -1,7 +1,7 @@ import { type Component, createMemo } from "solid-js" import { parsePatch } from "diff" -import { ContentCode } from "./content-code" -import styles from "./content-diff.module.css" +import CodeBlock from "./CodeBlock" +import styles from "./diffview.module.css" type DiffRow = { left: string @@ -9,12 +9,14 @@ type DiffRow = { type: "added" | "removed" | "unchanged" | "modified" } -interface Props { +interface DiffViewProps { diff: string lang?: string + class?: string } -export function ContentDiff(props: Props) { +const DiffView: Component = (props) => { + const rows = createMemo(() => { const diffRows: DiffRow[] = [] @@ -31,20 +33,20 @@ export function ContentDiff(props: Props) { const content = line.slice(1) const prefix = line[0] - if (prefix === "-") { + if (prefix === '-') { // Look ahead for consecutive additions to pair with removals const removals: string[] = [content] let j = i + 1 // Collect all consecutive removals - while (j < lines.length && lines[j][0] === "-") { + while (j < lines.length && lines[j][0] === '-') { removals.push(lines[j].slice(1)) j++ } // Collect all consecutive additions that follow const additions: string[] = [] - while (j < lines.length && lines[j][0] === "+") { + while (j < lines.length && lines[j][0] === '+') { additions.push(lines[j].slice(1)) j++ } @@ -60,39 +62,39 @@ export function ContentDiff(props: Props) { diffRows.push({ left: removals[k], right: additions[k], - type: "modified", + type: "modified" }) } else if (hasLeft) { // Pure removal diffRows.push({ left: removals[k], right: "", - type: "removed", + type: "removed" }) } else if (hasRight) { // Pure addition - only create if we actually have content diffRows.push({ left: "", right: additions[k], - type: "added", + type: "added" }) } } i = j - } else if (prefix === "+") { + } else if (prefix === '+') { // Standalone addition (not paired with removal) diffRows.push({ left: "", right: content, - type: "added", + type: "added" }) i++ - } else if (prefix === " ") { + } else if (prefix === ' ') { diffRows.push({ left: content, right: content, - type: "unchanged", + type: "unchanged" }) i++ } else { @@ -110,7 +112,7 @@ export function ContentDiff(props: Props) { }) const mobileRows = createMemo(() => { - const mobileBlocks: { type: "removed" | "added" | "unchanged"; lines: string[] }[] = [] + const mobileBlocks: { type: 'removed' | 'added' | 'unchanged', lines: string[] }[] = [] const currentRows = rows() let i = 0 @@ -119,15 +121,15 @@ export function ContentDiff(props: Props) { const addedLines: string[] = [] // Collect consecutive modified/removed/added rows - while ( - i < currentRows.length && - (currentRows[i].type === "modified" || currentRows[i].type === "removed" || currentRows[i].type === "added") - ) { + while (i < currentRows.length && + (currentRows[i].type === 'modified' || + currentRows[i].type === 'removed' || + currentRows[i].type === 'added')) { const row = currentRows[i] - if (row.left && (row.type === "removed" || row.type === "modified")) { + if (row.left && (row.type === 'removed' || row.type === 'modified')) { removedLines.push(row.left) } - if (row.right && (row.type === "added" || row.type === "modified")) { + if (row.right && (row.type === 'added' || row.type === 'modified')) { addedLines.push(row.right) } i++ @@ -135,17 +137,17 @@ export function ContentDiff(props: Props) { // Add grouped blocks if (removedLines.length > 0) { - mobileBlocks.push({ type: "removed", lines: removedLines }) + mobileBlocks.push({ type: 'removed', lines: removedLines }) } if (addedLines.length > 0) { - mobileBlocks.push({ type: "added", lines: addedLines }) + mobileBlocks.push({ type: 'added', lines: addedLines }) } // Add unchanged rows as-is - if (i < currentRows.length && currentRows[i].type === "unchanged") { + if (i < currentRows.length && currentRows[i].type === 'unchanged') { mobileBlocks.push({ - type: "unchanged", - lines: [currentRows[i].left], + type: 'unchanged', + lines: [currentRows[i].left] }) i++ } @@ -155,29 +157,40 @@ export function ContentDiff(props: Props) { }) return ( -
-
+
+
{rows().map((r) => ( -
-
- +
+
+
-
- +
+
))}
-
+
{mobileRows().map((block) => ( -
+
{block.lines.map((line) => ( - ))}
@@ -187,6 +200,8 @@ export function ContentDiff(props: Props) { ) } +export default DiffView + // const testDiff = `--- combined_before.txt 2025-06-24 16:38:08 // +++ combined_after.txt 2025-06-24 16:38:12 // @@ -1,21 +1,25 @@ @@ -195,12 +210,12 @@ export function ContentDiff(props: Props) { // -old content // +added line // +new content -// +// // -removed empty line below // +added empty line above -// +// // - tab indented -// -trailing spaces +// -trailing spaces // -very long line that will definitely wrap in most editors and cause potential alignment issues when displayed in a two column diff view // -unicode content: 🚀 ✨ 中文 // -mixed content with tabs and spaces @@ -211,14 +226,14 @@ export function ContentDiff(props: Props) { // +different unicode: 🎉 💻 日本語 // +normalized content with consistent spacing // +newline to content -// +// // -content to remove -// -whitespace only: +// -whitespace only: // -multiple // -consecutive // -deletions // -single deletion -// + +// + // +single addition // +first addition // +second addition diff --git a/packages/web/src/components/MarkdownView.tsx b/packages/web/src/components/MarkdownView.tsx new file mode 100644 index 00000000..5e21c0d7 --- /dev/null +++ b/packages/web/src/components/MarkdownView.tsx @@ -0,0 +1,21 @@ +import { type JSX, splitProps, createResource } from "solid-js" +import { marked } from "marked" +import styles from "./markdownview.module.css" + +interface MarkdownViewProps extends JSX.HTMLAttributes { + markdown: string +} + +function MarkdownView(props: MarkdownViewProps) { + const [local, rest] = splitProps(props, ["markdown"]) + const [html] = createResource(() => local.markdown, async (markdown) => { + return marked.parse(markdown) + }) + + return ( +
+ ) +} + +export default MarkdownView + diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx index 40e2763d..ff838dab 100644 --- a/packages/web/src/components/Share.tsx +++ b/packages/web/src/components/Share.tsx @@ -1,3 +1,4 @@ +import { type JSX } from "solid-js" import { For, Show, @@ -6,21 +7,77 @@ import { onMount, Suspense, onCleanup, + splitProps, createMemo, + createEffect, createSignal, SuspenseList, } from "solid-js" +import map from "lang-map" import { DateTime } from "luxon" import { createStore, reconcile } from "solid-js/store" -import { IconOpenAI, IconGemini, IconOpencode, IconAnthropic } from "./icons/custom" -import { IconSparkles, IconArrowDown } from "./icons" +import type { Diagnostic } from "vscode-languageserver-types" +import { + IconOpenAI, + IconGemini, + IconOpencode, + IconAnthropic, +} from "./icons/custom" +import { + IconHashtag, + IconSparkles, + IconGlobeAlt, + IconDocument, + IconQueueList, + IconUserCircle, + IconCheckCircle, + IconChevronDown, + IconCommandLine, + IconChevronRight, + IconDocumentPlus, + IconPencilSquare, + IconRectangleStack, + IconMagnifyingGlass, + IconWrenchScrewdriver, + IconDocumentMagnifyingGlass, + IconArrowDown, +} from "./icons" +import DiffView from "./DiffView" +import CodeBlock from "./CodeBlock" +import MarkdownView from "./MarkdownView" import styles from "./share.module.css" -import type { MessageV2 } from "opencode/session/message-v2" import type { Message } from "opencode/session/message" import type { Session } from "opencode/session/index" -import { Part } from "./share/part" -type Status = "disconnected" | "connecting" | "connected" | "error" | "reconnecting" +const MIN_DURATION = 2 + +type Status = + | "disconnected" + | "connecting" + | "connected" + | "error" + | "reconnecting" + +type TodoStatus = "pending" | "in_progress" | "completed" + +interface Todo { + id: string + content: string + status: TodoStatus + priority: "low" | "medium" | "high" +} + +function sortTodosByStatus(todos: Todo[]) { + const statusPriority: Record = { + in_progress: 0, + pending: 1, + completed: 2, + } + + return todos + .slice() + .sort((a, b) => statusPriority[a.status] - statusPriority[b.status]) +} function scrollToAnchor(id: string) { const el = document.getElementById(id) @@ -29,6 +86,146 @@ function scrollToAnchor(id: string) { el.scrollIntoView({ behavior: "smooth" }) } +function stripWorkingDirectory(filePath?: string, workingDir?: string) { + if (filePath === undefined || workingDir === undefined) return filePath + + const prefix = workingDir.endsWith("/") ? workingDir : workingDir + "/" + + if (filePath === workingDir) { + return "" + } + + if (filePath.startsWith(prefix)) { + return filePath.slice(prefix.length) + } + + return filePath +} + +function getShikiLang(filename: string) { + const ext = filename.split(".").pop()?.toLowerCase() ?? "" + + // map.languages(ext) returns an array of matching Linguist language names (e.g. ['TypeScript']) + const langs = map.languages(ext) + const type = langs?.[0]?.toLowerCase() + + // Overrride any specific language mappings + const overrides: Record = { + conf: "shellscript", + } + + return type ? (overrides[type] ?? type) : "plaintext" +} + +function formatDuration(ms: number): string { + const ONE_SECOND = 1000 + const ONE_MINUTE = 60 * ONE_SECOND + + if (ms >= ONE_MINUTE) { + const minutes = Math.floor(ms / ONE_MINUTE) + return minutes === 1 ? `1min` : `${minutes}mins` + } + + if (ms >= ONE_SECOND) { + const seconds = Math.floor(ms / ONE_SECOND) + return `${seconds}s` + } + + return `${ms}ms` +} + +// Converts nested objects/arrays into [path, value] pairs. +// E.g. {a:{b:{c:1}}, d:[{e:2}, 3]} => [["a.b.c",1], ["d[0].e",2], ["d[1]",3]] +function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> { + const entries: Array<[string, any]> = [] + + for (const [key, value] of Object.entries(obj)) { + const path = prefix ? `${prefix}.${key}` : key + + if (value !== null && typeof value === "object") { + if (Array.isArray(value)) { + value.forEach((item, index) => { + const arrayPath = `${path}[${index}]` + if (item !== null && typeof item === "object") { + entries.push(...flattenToolArgs(item, arrayPath)) + } else { + entries.push([arrayPath, item]) + } + }) + } else { + entries.push(...flattenToolArgs(value, path)) + } + } else { + entries.push([path, value]) + } + } + + return entries +} + +function formatErrorString(error: string): JSX.Element { + const errorMarker = "Error: " + const startsWithError = error.startsWith(errorMarker) + + return startsWithError ? ( +
+      
+        Error
+      
+      {error.slice(errorMarker.length)}
+    
+ ) : ( +
+      {error}
+    
+ ) +} + +function getDiagnostics( + diagnosticsByFile: Record, + currentFile: string, +): JSX.Element[] { + // Return a flat array of error diagnostics, in the format: + // "Error [65:20] Property 'x' does not exist on type 'Y'" + const result: JSX.Element[] = [] + + if ( + diagnosticsByFile === undefined || + diagnosticsByFile[currentFile] === undefined + ) + return result + + for (const diags of Object.values(diagnosticsByFile)) { + for (const d of diags) { + // Only keep diagnostics explicitly marked as Error (severity === 1) + if (d.severity !== 1) continue + + const line = d.range.start.line + 1 // 1-based + const column = d.range.start.character + 1 // 1-based + + result.push( +
+          
+            Error
+          
+          
+            [{line}:{column}]
+          
+          {d.message}
+        
, + ) + } + } + + return result +} + +function stripEnclosingTag(text: string): string { + const wrappedRe = /^\s*<([A-Za-z]\w*)>\s*([\s\S]*?)\s*<\/\1>\s*$/ + const match = text.match(wrappedRe) + return match ? match[2] : text +} + function getStatusText(status: [Status, string?]): string { switch (status[0]) { case "connected": @@ -63,14 +260,353 @@ function ProviderIcon(props: { provider: string; size?: number }) { ) } +interface ResultsButtonProps extends JSX.HTMLAttributes { + showCopy?: string + hideCopy?: string + results: boolean +} +function ResultsButton(props: ResultsButtonProps) { + const [local, rest] = splitProps(props, ["results", "showCopy", "hideCopy"]) + return ( + + ) +} + +interface TextPartProps extends JSX.HTMLAttributes { + text: string + expand?: boolean + invert?: boolean + highlight?: boolean +} +function TextPart(props: TextPartProps) { + const [local, rest] = splitProps(props, [ + "text", + "expand", + "invert", + "highlight", + ]) + const [expanded, setExpanded] = createSignal(false) + const [overflowed, setOverflowed] = createSignal(false) + let preEl: HTMLPreElement | undefined + + function checkOverflow() { + if (preEl && !local.expand) { + setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1) + } + } + + onMount(() => { + checkOverflow() + window.addEventListener("resize", checkOverflow) + }) + + createEffect(() => { + local.text + local.expand + setTimeout(checkOverflow, 0) + }) + + onCleanup(() => { + window.removeEventListener("resize", checkOverflow) + }) + + return ( +
+
 (preEl = el)}>{local.text}
+ {((!local.expand && overflowed()) || expanded()) && ( + + )} +
+ ) +} + +interface ErrorPartProps extends JSX.HTMLAttributes { + expand?: boolean +} +function ErrorPart(props: ErrorPartProps) { + const [local, rest] = splitProps(props, ["expand", "children"]) + const [expanded, setExpanded] = createSignal(false) + const [overflowed, setOverflowed] = createSignal(false) + let preEl: HTMLElement | undefined + + function checkOverflow() { + if (preEl && !local.expand) { + setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1) + } + } + + onMount(() => { + checkOverflow() + window.addEventListener("resize", checkOverflow) + }) + + createEffect(() => { + local.children + local.expand + setTimeout(checkOverflow, 0) + }) + + onCleanup(() => { + window.removeEventListener("resize", checkOverflow) + }) + + return ( +
+
(preEl = el)}> + {local.children} +
+ {((!local.expand && overflowed()) || expanded()) && ( + + )} +
+ ) +} + +interface MarkdownPartProps extends JSX.HTMLAttributes { + text: string + expand?: boolean + highlight?: boolean +} +function MarkdownPart(props: MarkdownPartProps) { + const [local, rest] = splitProps(props, ["text", "expand", "highlight"]) + const [expanded, setExpanded] = createSignal(false) + const [overflowed, setOverflowed] = createSignal(false) + let divEl: HTMLDivElement | undefined + + function checkOverflow() { + if (divEl && !local.expand) { + setOverflowed(divEl.scrollHeight > divEl.clientHeight + 1) + } + } + + onMount(() => { + checkOverflow() + window.addEventListener("resize", checkOverflow) + }) + + createEffect(() => { + local.text + local.expand + setTimeout(checkOverflow, 0) + }) + + onCleanup(() => { + window.removeEventListener("resize", checkOverflow) + }) + + return ( +
+ (divEl = el)} + /> + {((!local.expand && overflowed()) || expanded()) && ( + + )} +
+ ) +} + +interface TerminalPartProps extends JSX.HTMLAttributes { + command: string + error?: string + result?: string + desc?: string + expand?: boolean +} +function TerminalPart(props: TerminalPartProps) { + const [local, rest] = splitProps(props, [ + "command", + "error", + "result", + "desc", + "expand", + ]) + const [expanded, setExpanded] = createSignal(false) + const [overflowed, setOverflowed] = createSignal(false) + let preEl: HTMLElement | undefined + + function checkOverflow() { + if (!preEl) return + + const code = preEl.getElementsByTagName("code")[0] + + if (code && !local.expand) { + setOverflowed(preEl.clientHeight < code.offsetHeight) + } + } + + createEffect(() => { + local.command + local.result + local.error + local.expand + setTimeout(checkOverflow, 0) + }) + + onMount(() => { + checkOverflow() + window.addEventListener("resize", checkOverflow) + }) + + onCleanup(() => { + window.removeEventListener("resize", checkOverflow) + }) + + return ( +
+
+
+ {local.desc} +
+
+ + + + (preEl = el)} + code={local.error || ""} + /> + + + (preEl = el)} + code={local.result || ""} + /> + + +
+
+ {((!local.expand && overflowed()) || expanded()) && ( + + )} +
+ ) +} + +function ToolFooter(props: { time: number }) { + return props.time > MIN_DURATION ? ( + + {formatDuration(props.time)} + + ) : ( +
+ ) +} + +interface AnchorProps extends JSX.HTMLAttributes { + id: string +} +function AnchorIcon(props: AnchorProps) { + const [local, rest] = splitProps(props, ["id", "children"]) + const [copied, setCopied] = createSignal(false) + + return ( + + ) +} + export default function Share(props: { id: string api: string info: Session.Info - messages: Record + messages: Record }) { let lastScrollY = 0 - let hasScrolledToAnchor = false let scrollTimeout: number | undefined let scrollSentinel: HTMLElement | undefined let scrollObserver: IntersectionObserver | undefined @@ -85,10 +621,14 @@ export default function Share(props: { const [store, setStore] = createStore<{ info?: Session.Info - messages: Record + messages: Record }>({ info: props.info, messages: props.messages }) - const messages = createMemo(() => Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id))) - const [connectionStatus, setConnectionStatus] = createSignal<[Status, string?]>(["disconnected", "Disconnected"]) + const messages = createMemo(() => + Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id)), + ) + const [connectionStatus, setConnectionStatus] = createSignal< + [Status, string?] + >(["disconnected", "Disconnected"]) onMount(() => { const apiUrl = props.api @@ -163,7 +703,10 @@ export default function Share(props: { // Try to reconnect after 2 seconds clearTimeout(reconnectTimer) - reconnectTimer = window.setTimeout(setupWebSocket, 2000) as unknown as number + reconnectTimer = window.setTimeout( + setupWebSocket, + 2000, + ) as unknown as number } } @@ -261,7 +804,7 @@ export default function Share(props: { rootDir: undefined as string | undefined, created: undefined as number | undefined, completed: undefined as number | undefined, - messages: [] as MessageV2.Info[], + messages: [] as Message.Info[], models: {} as Record, cost: 0, tokens: { @@ -273,41 +816,46 @@ export default function Share(props: { result.created = props.info.time.created - const msgs = messages() - for (let i = 0; i < msgs.length; i++) { - const msg = "metadata" in msgs[i] ? fromV1(msgs[i] as Message.Info) : (msgs[i] as MessageV2.Info) + for (let i = 0; i < messages().length; i++) { + const msg = messages()[i] + + const assistant = msg.metadata?.assistant result.messages.push(msg) - if (msg.role === "assistant") { - result.cost += msg.cost - result.tokens.input += msg.tokens.input - result.tokens.output += msg.tokens.output - result.tokens.reasoning += msg.tokens.reasoning + if (assistant) { + result.cost += assistant.cost + result.tokens.input += assistant.tokens.input + result.tokens.output += assistant.tokens.output + result.tokens.reasoning += assistant.tokens.reasoning - result.models[`${msg.providerID} ${msg.modelID}`] = [msg.providerID, msg.modelID] + result.models[`${assistant.providerID} ${assistant.modelID}`] = [ + assistant.providerID, + assistant.modelID, + ] - if (msg.path.root) { - result.rootDir = msg.path.root + if (assistant.path?.root) { + result.rootDir = assistant.path.root } - if (msg.time.completed) { - result.completed = msg.time.completed + if (msg.metadata?.time.completed) { + result.completed = msg.metadata?.time.completed } } } - console.log(result.messages) return result }) return ( -
-
-

{store.info?.title}

-
-
    -
  • -
    +
    +
    +
    +

    {store.info?.title}

    +
    +
    +
      +
    • +
      @@ -317,11 +865,11 @@ export default function Share(props: { {Object.values(data().models).length > 0 ? ( {([provider, model]) => ( -
    • -
      +
    • +
      - {model} + {model}
    • )} @@ -332,52 +880,1085 @@ export default function Share(props: { )}
    -
    - {DateTime.fromMillis(data().created || 0).toLocaleString(DateTime.DATETIME_MED)} +
    + {data().created ? ( + + {DateTime.fromMillis(data().created || 0).toLocaleString( + DateTime.DATETIME_MED, + )} + + ) : ( + + Started at — + + )}
    - 0} fallback={

    Waiting for messages...

    }> + 0} + fallback={

    Waiting for messages...

    } + >
    {(msg, msgIndex) => ( - { - if (x.type === "step-start" && index > 0) return false - if (x.type === "tool" && x.tool === "todoread") return false - if (x.type === "text" && !x.text) return false - if (x.type === "tool" && (x.state.status === "pending" || x.state.status === "running")) - return false - return true - })} - > + {(part, partIndex) => { - const last = createMemo( - () => data().messages.length === msgIndex() + 1 && msg.parts.length === partIndex() + 1, + if ( + (part.type === "step-start" && + (partIndex() > 0 || !msg.metadata?.assistant)) || + (msg.role === "assistant" && + part.type === "tool-invocation" && + part.toolInvocation.toolName === "todoread") ) + return null + + const anchor = createMemo( + () => `${msg.id}-${partIndex()}`, + ) + const [showResults, setShowResults] = + createSignal(false) + const isLastPart = createMemo( + () => + data().messages.length === msgIndex() + 1 && + msg.parts.length === partIndex() + 1, + ) + const toolData = createMemo(() => { + if ( + msg.role !== "assistant" || + part.type !== "tool-invocation" + ) + return {} + + const metadata = + msg.metadata?.tool[part.toolInvocation.toolCallId] + const args = part.toolInvocation.args + const result = + part.toolInvocation.state === "result" && + part.toolInvocation.result + const duration = DateTime.fromMillis( + metadata?.time.end || 0, + ) + .diff( + DateTime.fromMillis(metadata?.time.start || 0), + ) + .toMillis() + + return { metadata, args, result, duration } + }) onMount(() => { const hash = window.location.hash.slice(1) // Wait till all parts are loaded if ( - hash !== "" && - !hasScrolledToAnchor && - msg.parts.length === partIndex() + 1 && - data().messages.length === msgIndex() + 1 + hash !== "" + && msg.parts.length === partIndex() + 1 + && data().messages.length === msgIndex() + 1 ) { - hasScrolledToAnchor = true scrollToAnchor(hash) } }) - return + return ( + + {/* User text */} + + {(part) => ( +
    +
    + + + +
    +
    +
    + +
    +
    + )} +
    + {/* AI text */} + + {(part) => ( +
    +
    + + + +
    +
    +
    + + + + {DateTime.fromMillis( + data().completed || 0, + ).toLocaleString(DateTime.DATETIME_MED)} + + +
    +
    + )} +
    + {/* AI model */} + + {(assistant) => { + return ( +
    +
    + + + +
    +
    +
    +
    +
    + + {assistant().providerID} + +
    + + {assistant().modelID} + +
    +
    +
    + ) + }} +
    + + {/* Grep tool */} + + {(_part) => { + const matches = () => + toolData()?.metadata?.matches + const splitArgs = () => { + const { pattern, ...rest } = toolData()?.args + return { pattern, rest } + } + + return ( +
    +
    + + + +
    +
    +
    +
    +
    + Grep + + “{splitArgs().pattern}” + +
    + 0 + } + > +
    + + {([name, value]) => ( + <> +
    +
    {name}
    +
    {value}
    + + )} +
    +
    +
    + + 0}> +
    + + setShowResults((e) => !e) + } + /> + + + +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    + ) + }} +
    + {/* Glob tool */} + + {(_part) => { + const count = () => toolData()?.metadata?.count + const pattern = () => toolData()?.args.pattern + + return ( +
    +
    + + + +
    +
    +
    +
    +
    + Glob + “{pattern()}” +
    + + 0}> +
    + + setShowResults((e) => !e) + } + /> + + + +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    + ) + }} +
    + {/* LS tool */} + + {(_part) => { + const path = createMemo(() => + toolData()?.args?.path !== data().rootDir + ? stripWorkingDirectory( + toolData()?.args?.path, + data().rootDir, + ) + : toolData()?.args?.path, + ) + + return ( +
    +
    + + + +
    +
    +
    +
    +
    + LS + + {path()} + +
    + + +
    + + setShowResults((e) => !e) + } + /> + + + +
    +
    +
    +
    + +
    +
    + ) + }} +
    + {/* Read tool */} + + {(_part) => { + const filePath = createMemo(() => + stripWorkingDirectory( + toolData()?.args?.filePath, + data().rootDir, + ), + ) + const hasError = () => + toolData()?.metadata?.error + const preview = () => + toolData()?.metadata?.preview + + return ( +
    +
    + + + +
    +
    +
    +
    +
    + Read + + {filePath()} + +
    + + +
    + + {formatErrorString( + toolData()?.result, + )} + +
    +
    + {/* Always try to show CodeBlock if preview is available (even if empty string) */} + +
    + + setShowResults((e) => !e) + } + /> + +
    + +
    +
    +
    +
    + {/* Fallback to TextPart if preview is not a string (e.g. undefined) AND result exists */} + +
    + + setShowResults((e) => !e) + } + /> + + + +
    +
    +
    +
    + +
    +
    + ) + }} +
    + {/* Write tool */} + + {(_part) => { + const filePath = createMemo(() => + stripWorkingDirectory( + toolData()?.args?.filePath, + data().rootDir, + ), + ) + const hasError = () => + toolData()?.metadata?.error + const content = () => toolData()?.args?.content + const diagnostics = createMemo(() => + getDiagnostics( + toolData()?.metadata?.diagnostics, + toolData()?.args.filePath, + ), + ) + + return ( +
    +
    + + + +
    +
    +
    +
    +
    + Write + + {filePath()} + +
    + 0}> + {diagnostics()} + + + +
    + + {formatErrorString( + toolData()?.result, + )} + +
    +
    + +
    + + setShowResults((e) => !e) + } + /> + +
    + +
    +
    +
    +
    +
    +
    + +
    +
    + ) + }} +
    + {/* Edit tool */} + + {(_part) => { + const diff = () => toolData()?.metadata?.diff + const message = () => + toolData()?.metadata?.message + const hasError = () => + toolData()?.metadata?.error + const filePath = createMemo(() => + stripWorkingDirectory( + toolData()?.args.filePath, + data().rootDir, + ), + ) + const diagnostics = createMemo(() => + getDiagnostics( + toolData()?.metadata?.diagnostics, + toolData()?.args.filePath, + ), + ) + + return ( +
    +
    + + + +
    +
    +
    +
    +
    + Edit + + {filePath()} + +
    + + +
    + + {formatErrorString(message())} + +
    +
    + +
    + +
    +
    +
    + 0}> + {diagnostics()} + +
    + +
    +
    + ) + }} +
    + {/* Bash tool */} + + {(_part) => { + const command = () => + toolData()?.metadata?.title + const desc = () => + toolData()?.metadata?.description + const result = () => + toolData()?.metadata?.stdout + const error = () => toolData()?.metadata?.stderr + + return ( +
    +
    + + + +
    +
    +
    + {command() && ( +
    + +
    + )} + +
    +
    + ) + }} +
    + {/* Todo write */} + + {(_part) => { + const todos = createMemo(() => + sortTodosByStatus( + toolData()?.args?.todos ?? [], + ), + ) + const starting = () => + todos().every((t) => t.status === "pending") + const finished = () => + todos().every((t) => t.status === "completed") + + return ( +
    +
    + + + +
    +
    +
    +
    +
    + + + + Creating plan + + + Completing plan + + + +
    + 0}> +
      + + {(todo) => ( +
    • + + {todo.content} +
    • + )} +
      +
    +
    +
    + +
    +
    + ) + }} +
    + {/* Fetch tool */} + + {(_part) => { + const url = () => toolData()?.args.url + const format = () => toolData()?.args.format + const hasError = () => + toolData()?.metadata?.error + + return ( +
    +
    + + + +
    +
    +
    +
    +
    + Fetch + {url()} +
    + + +
    + + {formatErrorString( + toolData()?.result, + )} + +
    +
    + +
    + + setShowResults((e) => !e) + } + /> + +
    + +
    +
    +
    +
    +
    +
    + +
    +
    + ) + }} +
    + {/* Tool call */} + + {(part) => { + return ( +
    +
    + + + +
    +
    +
    +
    +
    + {part().toolInvocation.toolName} +
    +
    + + {(arg) => ( + <> +
    +
    {arg[0]}
    +
    {arg[1]}
    + + )} +
    +
    + + +
    + + setShowResults((e) => !e) + } + /> + + + +
    +
    + + + +
    +
    + +
    +
    + ) + }} +
    + {/* Fallback */} + +
    +
    + + + } + > + + + + + + + + + +
    +
    +
    +
    +
    + + {part.type} + +
    + +
    +
    +
    +
    +
    + ) }}
    @@ -402,11 +1983,19 @@ export default function Share(props: {
  • Input Tokens - {data().tokens.input ? {data().tokens.input} : } + {data().tokens.input ? ( + {data().tokens.input} + ) : ( + + )}
  • Output Tokens - {data().tokens.output ? {data().tokens.output} : } + {data().tokens.output ? ( + {data().tokens.output} + ) : ( + + )}
  • Reasoning Tokens @@ -432,7 +2021,10 @@ export default function Share(props: { "overflow-y": "auto", }} > - 0} fallback={

    Waiting for messages...

    }> + 0} + fallback={

    Waiting for messages...

    } + >
      {(msg) => ( @@ -460,7 +2052,9 @@ export default function Share(props: {
) } - -export function fromV1(v1: Message.Info): MessageV2.Info { - if (v1.role === "assistant") { - const result: MessageV2.Assistant = { - id: v1.id, - sessionID: v1.metadata.sessionID, - role: "assistant", - time: { - created: v1.metadata.time.created, - completed: v1.metadata.time.completed, - }, - cost: v1.metadata.assistant!.cost, - path: v1.metadata.assistant!.path, - summary: v1.metadata.assistant!.summary, - tokens: v1.metadata.assistant!.tokens, - modelID: v1.metadata.assistant!.modelID, - providerID: v1.metadata.assistant!.providerID, - system: v1.metadata.assistant!.system, - error: v1.metadata.error, - parts: v1.parts.flatMap((part): MessageV2.AssistantPart[] => { - if (part.type === "text") { - return [ - { - type: "text", - text: part.text, - }, - ] - } - if (part.type === "step-start") { - return [ - { - type: "step-start", - }, - ] - } - if (part.type === "tool-invocation") { - return [ - { - type: "tool", - id: part.toolInvocation.toolCallId, - tool: part.toolInvocation.toolName, - state: (() => { - if (part.toolInvocation.state === "partial-call") { - return { - status: "pending", - } - } - - const { title, time, ...metadata } = v1.metadata.tool[part.toolInvocation.toolCallId] - if (part.toolInvocation.state === "call") { - return { - status: "running", - input: part.toolInvocation.args, - time: { - start: time.start, - }, - } - } - - if (part.toolInvocation.state === "result") { - return { - status: "completed", - input: part.toolInvocation.args, - output: part.toolInvocation.result, - title, - time, - metadata, - } - } - throw new Error("unknown tool invocation state") - })(), - }, - ] - } - return [] - }), - } - return result - } - - if (v1.role === "user") { - const result: MessageV2.User = { - id: v1.id, - sessionID: v1.metadata.sessionID, - role: "user", - time: { - created: v1.metadata.time.created, - }, - parts: v1.parts.flatMap((part): MessageV2.UserPart[] => { - if (part.type === "text") { - return [ - { - type: "text", - text: part.text, - }, - ] - } - if (part.type === "file") { - return [ - { - type: "file", - mime: part.mediaType, - filename: part.filename, - url: part.url, - }, - ] - } - return [] - }), - } - return result - } - - throw new Error("unknown message type") -} diff --git a/packages/web/src/components/codeblock.module.css b/packages/web/src/components/codeblock.module.css index 53120120..ddd88ef1 100644 --- a/packages/web/src/components/codeblock.module.css +++ b/packages/web/src/components/codeblock.module.css @@ -8,3 +8,4 @@ } } } + diff --git a/packages/web/src/components/diffview.module.css b/packages/web/src/components/diffview.module.css new file mode 100644 index 00000000..a748c5d0 --- /dev/null +++ b/packages/web/src/components/diffview.module.css @@ -0,0 +1,121 @@ +.diff { + display: flex; + flex-direction: column; + border: 1px solid var(--sl-color-divider); + background-color: var(--sl-color-bg-surface); + border-radius: 0.25rem; +} + +.desktopView { + display: block; +} + +.mobileView { + display: none; +} + +.mobileBlock { + display: flex; + flex-direction: column; +} + +.row { + display: grid; + grid-template-columns: 1fr 1fr; + align-items: stretch; +} + +.beforeColumn, +.afterColumn { + display: flex; + flex-direction: column; + overflow-x: visible; + min-width: 0; + align-items: stretch; +} + +.beforeColumn { + border-right: 1px solid var(--sl-color-divider); +} + +.diff > .row:first-child [data-section="cell"]:first-child { + padding-top: 0.5rem; +} + +.diff > .row:last-child [data-section="cell"]:last-child { + padding-bottom: 0.5rem; +} + +[data-section="cell"] { + position: relative; + flex: 1; + display: flex; + flex-direction: column; + + width: 100%; + padding: 0.1875rem 0.5rem 0.1875rem 2.2ch; + margin: 0; + + &[data-display-mobile="true"] { + display: none; + } + + pre { + --shiki-dark-bg: var(--sl-color-bg-surface) !important; + background-color: var(--sl-color-bg-surface) !important; + + white-space: pre-wrap; + word-break: break-word; + + code > span:empty::before { + content: "\00a0"; + white-space: pre; + display: inline-block; + width: 0; + } + } +} + +[data-diff-type="removed"] { + background-color: var(--sl-color-red-low); + + pre { + --shiki-dark-bg: var(--sl-color-red-low) !important; + background-color: var(--sl-color-red-low) !important; + } + + &::before { + content: "-"; + position: absolute; + left: 0.5ch; + user-select: none; + color: var(--sl-color-red-high); + } +} + +[data-diff-type="added"] { + background-color: var(--sl-color-green-low); + + pre { + --shiki-dark-bg: var(--sl-color-green-low) !important; + background-color: var(--sl-color-green-low) !important; + } + + &::before { + content: "+"; + position: absolute; + left: 0.6ch; + user-select: none; + color: var(--sl-color-green-high); + } +} + +@media (max-width: 40rem) { + .desktopView { + display: none; + } + + .mobileView { + display: block; + } +} diff --git a/packages/web/src/components/icons/custom.tsx b/packages/web/src/components/icons/custom.tsx index be1e2b4d..b4e32d0c 100644 --- a/packages/web/src/components/icons/custom.tsx +++ b/packages/web/src/components/icons/custom.tsx @@ -39,12 +39,7 @@ export function IconGemini(props: JSX.SvgSVGAttributes) { export function IconOpencode(props: JSX.SvgSVGAttributes) { return ( - + ) diff --git a/packages/web/src/components/icons/index.tsx b/packages/web/src/components/icons/index.tsx index 62445611..a788d8f4 100644 --- a/packages/web/src/components/icons/index.tsx +++ b/packages/web/src/components/icons/index.tsx @@ -3,7 +3,12 @@ import { type JSX } from "solid-js" export function IconAcademicCap(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconAdjustmentsHorizontal(props: JSX.SvgSVGAttributes) { +export function IconAdjustmentsHorizontal( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconAdjustmentsVertical(props: JSX.SvgSVGAttributes) { +export function IconAdjustmentsVertical( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconArchiveBoxArrowDown(props: JSX.SvgSVGAttributes) { +export function IconArchiveBoxArrowDown( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconArchiveBoxXMark(props: JSX.SvgSVGAttributes) { +export function IconArchiveBoxXMark( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } export function IconArchiveBox(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconArrowDownCircle(props: JSX.SvgSVGAttributes) { +export function IconArrowDownCircle( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } export function IconArrowDownLeft(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconArrowDownOnSquareStack(props: JSX.SvgSVGAttributes) { +export function IconArrowDownOnSquareStack( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconArrowDownOnSquare(props: JSX.SvgSVGAttributes) { +export function IconArrowDownOnSquare( + props: JSX.SvgSVGAttributes, +) { return ( - + } export function IconArrowDownRight(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowDownTray(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowDown(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconArrowLeftCircle(props: JSX.SvgSVGAttributes) { +export function IconArrowLeftCircle( + props: JSX.SvgSVGAttributes, +) { return ( - + ) ) } -export function IconArrowLeftOnRectangle(props: JSX.SvgSVGAttributes) { +export function IconArrowLeftOnRectangle( + props: JSX.SvgSVGAttributes, +) { return ( - + ) { return ( - + ) { } export function IconArrowLongDown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowLongLeft(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowLongRight(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowLongUp(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconArrowPathRoundedSquare(props: JSX.SvgSVGAttributes) { +export function IconArrowPathRoundedSquare( + props: JSX.SvgSVGAttributes, +) { return ( - + ) { return ( - + ) { ) } -export function IconArrowRightCircle(props: JSX.SvgSVGAttributes) { +export function IconArrowRightCircle( + props: JSX.SvgSVGAttributes, +) { return ( - + ) ) } -export function IconArrowRightOnRectangle(props: JSX.SvgSVGAttributes) { +export function IconArrowRightOnRectangle( + props: JSX.SvgSVGAttributes, +) { return ( - + ) { return ( - + ) { } export function IconArrowSmallDown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowSmallLeft(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconArrowSmallRight(props: JSX.SvgSVGAttributes) { +export function IconArrowSmallRight( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } export function IconArrowSmallUp(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconArrowTopRightOnSquare(props: JSX.SvgSVGAttributes) { +export function IconArrowTopRightOnSquare( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconArrowTrendingDown(props: JSX.SvgSVGAttributes) { +export function IconArrowTrendingDown( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconArrowTrendingUp(props: JSX.SvgSVGAttributes) { +export function IconArrowTrendingUp( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } export function IconArrowUpCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowUpLeft(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconArrowUpOnSquareStack(props: JSX.SvgSVGAttributes) { +export function IconArrowUpOnSquareStack( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconArrowUpOnSquare(props: JSX.SvgSVGAttributes) { +export function IconArrowUpOnSquare( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } export function IconArrowUpRight(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowUpTray(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowUp(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowUturnDown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowUturnLeft(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconArrowUturnRight(props: JSX.SvgSVGAttributes) { +export function IconArrowUturnRight( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } export function IconArrowUturnUp(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconArrowsPointingIn(props: JSX.SvgSVGAttributes) { +export function IconArrowsPointingIn( + props: JSX.SvgSVGAttributes, +) { return ( - + ) ) } -export function IconArrowsPointingOut(props: JSX.SvgSVGAttributes) { +export function IconArrowsPointingOut( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconArrowsRightLeft(props: JSX.SvgSVGAttributes) { +export function IconArrowsRightLeft( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } export function IconArrowsUpDown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconAtSymbol(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBackspace(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBackward(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBanknotes(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBars2(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconBars3BottomLeft(props: JSX.SvgSVGAttributes) { +export function IconBars3BottomLeft( + props: JSX.SvgSVGAttributes, +) { return ( - + ) ) } -export function IconBars3BottomRight(props: JSX.SvgSVGAttributes) { +export function IconBars3BottomRight( + props: JSX.SvgSVGAttributes, +) { return ( - + ) ) } -export function IconBars3CenterLeft(props: JSX.SvgSVGAttributes) { +export function IconBars3CenterLeft( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } export function IconBars3(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBars4(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBarsArrowDown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBarsArrowUp(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBattery0(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBattery100(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBattery50(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBeaker(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBellAlert(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBellSlash(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBellSnooze(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBell(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBoltSlash(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBolt(props: JSX.SvgSVGAttributes) { return ( - + ) { export function IconBoltSolid(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBookOpen(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBookmarkSlash(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBookmarkSquare(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBookmark(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBriefcase(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBugAnt(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconBuildingLibrary(props: JSX.SvgSVGAttributes) { +export function IconBuildingLibrary( + props: JSX.SvgSVGAttributes, +) { return ( - + ) ) } -export function IconBuildingOffice2(props: JSX.SvgSVGAttributes) { +export function IconBuildingOffice2( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } export function IconBuildingOffice(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconBuildingStorefront(props: JSX.SvgSVGAttributes) { +export function IconBuildingStorefront( + props: JSX.SvgSVGAttributes, +) { return ( - + ) { return ( - + ) { } export function IconCalculator(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCalendarDays(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCalendar(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCamera(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconChartBarSquare(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconChartBar(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconChartPie(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconChatBubbleBottomCenterText(props: JSX.SvgSVGAttributes) { +export function IconChatBubbleBottomCenterText( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconChatBubbleBottomCenter(props: JSX.SvgSVGAttributes) { +export function IconChatBubbleBottomCenter( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconChatBubbleLeftEllipsis(props: JSX.SvgSVGAttributes) { +export function IconChatBubbleLeftEllipsis( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconChatBubbleLeftRight(props: JSX.SvgSVGAttributes) { +export function IconChatBubbleLeftRight( + props: JSX.SvgSVGAttributes, +) { return ( - + ) { return ( - + ) { ) } -export function IconChatBubbleOvalLeftEllipsis(props: JSX.SvgSVGAttributes) { +export function IconChatBubbleOvalLeftEllipsis( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconChatBubbleOvalLeft(props: JSX.SvgSVGAttributes) { +export function IconChatBubbleOvalLeft( + props: JSX.SvgSVGAttributes, +) { return ( - + ) { return ( - + ) { } export function IconCheckCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCheck(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconChevronDoubleDown(props: JSX.SvgSVGAttributes) { +export function IconChevronDoubleDown( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconChevronDoubleLeft(props: JSX.SvgSVGAttributes) { +export function IconChevronDoubleLeft( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconChevronDoubleRight(props: JSX.SvgSVGAttributes) { +export function IconChevronDoubleRight( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconChevronDoubleUp(props: JSX.SvgSVGAttributes) { +export function IconChevronDoubleUp( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } export function IconChevronDown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconChevronLeft(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconChevronRight(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconChevronUpDown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconChevronUp(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCircleStack(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconClipboardDocumentCheck(props: JSX.SvgSVGAttributes) { +export function IconClipboardDocumentCheck( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconClipboardDocumentList(props: JSX.SvgSVGAttributes) { +export function IconClipboardDocumentList( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconClipboardDocument(props: JSX.SvgSVGAttributes) { +export function IconClipboardDocument( + props: JSX.SvgSVGAttributes, +) { return ( - + } export function IconClipboard(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconClock(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCloudArrowDown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCloudArrowUp(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCloud(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconCodeBracketSquare(props: JSX.SvgSVGAttributes) { +export function IconCodeBracketSquare( + props: JSX.SvgSVGAttributes, +) { return ( - + } export function IconCodeBracket(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCog6Tooth(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCog8Tooth(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCog(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCommandLine(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconComputerDesktop(props: JSX.SvgSVGAttributes) { +export function IconComputerDesktop( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } export function IconCpuChip(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCreditCard(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconCubeTransparent(props: JSX.SvgSVGAttributes) { +export function IconCubeTransparent( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } export function IconCube(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconCurrencyBangladeshi(props: JSX.SvgSVGAttributes) { +export function IconCurrencyBangladeshi( + props: JSX.SvgSVGAttributes, +) { return ( - + ) { return ( - + ) { } export function IconCurrencyEuro(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCurrencyPound(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCurrencyRupee(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCurrencyYen(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconCursorArrowRays(props: JSX.SvgSVGAttributes) { +export function IconCursorArrowRays( + props: JSX.SvgSVGAttributes, +) { return ( - + ) ) } -export function IconCursorArrowRipple(props: JSX.SvgSVGAttributes) { +export function IconCursorArrowRipple( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconDevicePhoneMobile(props: JSX.SvgSVGAttributes) { +export function IconDevicePhoneMobile( + props: JSX.SvgSVGAttributes, +) { return ( - + } export function IconDeviceTablet(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconDocumentArrowDown(props: JSX.SvgSVGAttributes) { +export function IconDocumentArrowDown( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconDocumentArrowUp(props: JSX.SvgSVGAttributes) { +export function IconDocumentArrowUp( + props: JSX.SvgSVGAttributes, +) { return ( - + ) ) } -export function IconDocumentChartBar(props: JSX.SvgSVGAttributes) { +export function IconDocumentChartBar( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } export function IconDocumentCheck(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconDocumentDuplicate(props: JSX.SvgSVGAttributes) { +export function IconDocumentDuplicate( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconDocumentMagnifyingGlass(props: JSX.SvgSVGAttributes) { +export function IconDocumentMagnifyingGlass( + props: JSX.SvgSVGAttributes, +) { return ( - + ) { return ( - + ) { } export function IconDocumentPlus(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconDocumentText(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconDocument(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconEllipsisHorizontalCircle(props: JSX.SvgSVGAttributes) { +export function IconEllipsisHorizontalCircle( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconEllipsisHorizontal(props: JSX.SvgSVGAttributes) { +export function IconEllipsisHorizontal( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconEllipsisVertical(props: JSX.SvgSVGAttributes) { +export function IconEllipsisVertical( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } export function IconEnvelopeOpen(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconEnvelope(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconEnvelopeSolid(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconExclamationCircle(props: JSX.SvgSVGAttributes) { +export function IconExclamationCircle( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconExclamationTriangle(props: JSX.SvgSVGAttributes) { +export function IconExclamationTriangle( + props: JSX.SvgSVGAttributes, +) { return ( - + ) { return ( - + ) { } export function IconEyeSlash(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconEye(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFaceFrown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFaceSmile(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFilm(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFingerPrint(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFire(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFlag(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconFolderArrowDown(props: JSX.SvgSVGAttributes) { +export function IconFolderArrowDown( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } export function IconFolderMinus(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFolderOpen(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFolderPlus(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFolder(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconForward(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFunnel(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconGif(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconGiftTop(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconGift(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconGlobeAlt(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconGlobeAmericas(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconGlobeAsiaAustralia(props: JSX.SvgSVGAttributes) { +export function IconGlobeAsiaAustralia( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconGlobeEuropeAfrica(props: JSX.SvgSVGAttributes) { +export function IconGlobeEuropeAfrica( + props: JSX.SvgSVGAttributes, +) { return ( - + } export function IconHandRaised(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconHandThumbDown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconHandThumbUp(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconHashtag(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconHeart(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconHomeModern(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconHome(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconIdentification(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconInboxArrowDown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconInboxStack(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconInbox(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconInformationCircle(props: JSX.SvgSVGAttributes) { +export function IconInformationCircle( + props: JSX.SvgSVGAttributes, +) { return ( - + } export function IconKey(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconLanguage(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconLifebuoy(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconLightBulb(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconLink(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconListBullet(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconLockClosed(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconLockOpen(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconMagnifyingGlassCircle(props: JSX.SvgSVGAttributes) { +export function IconMagnifyingGlassCircle( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconMagnifyingGlassMinus(props: JSX.SvgSVGAttributes) { +export function IconMagnifyingGlassMinus( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconMagnifyingGlassPlus(props: JSX.SvgSVGAttributes) { +export function IconMagnifyingGlassPlus( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconMagnifyingGlass(props: JSX.SvgSVGAttributes) { +export function IconMagnifyingGlass( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } export function IconMapPin(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconMap(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconMegaphone(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconMicrophone(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconMinusCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconMinusSmall(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconMinus(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconMoon(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconMusicalNote(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconNewspaper(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconNoSymbol(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPaintBrush(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPaperAirplane(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPaperClip(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPauseCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPause(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPencilSquare(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPencil(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconPhoneArrowDownLeft(props: JSX.SvgSVGAttributes) { +export function IconPhoneArrowDownLeft( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconPhoneArrowUpRight(props: JSX.SvgSVGAttributes) { +export function IconPhoneArrowUpRight( + props: JSX.SvgSVGAttributes, +) { return ( - + } export function IconPhoneXMark(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPhone(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPhoto(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPlayCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPlayPause(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPlay(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPlusCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPlusSmall(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPlus(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPower(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconPresentationChartBar(props: JSX.SvgSVGAttributes) { +export function IconPresentationChartBar( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } -export function IconPresentationChartLine(props: JSX.SvgSVGAttributes) { +export function IconPresentationChartLine( + props: JSX.SvgSVGAttributes, +) { return ( - + ) { return ( - + ) { } export function IconPuzzlePiece(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconQrCode(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconQuestionMarkCircle(props: JSX.SvgSVGAttributes) { +export function IconQuestionMarkCircle( + props: JSX.SvgSVGAttributes, +) { return ( - + ) { return ( - + ) { } export function IconRadio(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconReceiptPercent(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconReceiptRefund(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconRectangleGroup(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconRectangleStack(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconRocketLaunch(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconRss(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconScale(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconScissors(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconServerStack(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconServer(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconShare(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconShieldCheck(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconShieldExclamation(props: JSX.SvgSVGAttributes) { +export function IconShieldExclamation( + props: JSX.SvgSVGAttributes, +) { return ( - + } export function IconShoppingBag(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconShoppingCart(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSignalSlash(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSignal(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSparkles(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSpeakerWave(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSpeakerXMark(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSquare2Stack(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSquare3Stack3d(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSquares2x2(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSquaresPlus(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconStar(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconStopCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconStop(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSun(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSwatch(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconTableCells(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconTag(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconTicket(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconTrash(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconTrophy(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconTruck(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconTv(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconUserCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconUserGroup(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconUserMinus(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconUserPlus(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconUser(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconUsers(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconVariable(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconVideoCameraSlash(props: JSX.SvgSVGAttributes) { +export function IconVideoCameraSlash( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } export function IconVideoCamera(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconViewColumns(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconViewfinderCircle(props: JSX.SvgSVGAttributes) { +export function IconViewfinderCircle( + props: JSX.SvgSVGAttributes, +) { return ( - + ) } export function IconWallet(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconWifi(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconWindow(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconWrenchScrewdriver(props: JSX.SvgSVGAttributes) { +export function IconWrenchScrewdriver( + props: JSX.SvgSVGAttributes, +) { return ( - + } export function IconWrench(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconXCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconXMark(props: JSX.SvgSVGAttributes) { return ( - + ) { // index export function IconCommand(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconLetter(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconMultiSelect(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSettings(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSingleSelect(props: JSX.SvgSVGAttributes) { return ( - + *:last-child { + margin-bottom: 0; + } + + pre { + white-space: pre-wrap; + border-radius: 0.25rem; + border: 1px solid rgba(0, 0, 0, 0.2); + padding: 0.5rem 0.75rem; + font-size: 0.75rem; + } + + code { + font-weight: 500; + + &:not(pre code) { + &::before { + content: "`"; + font-weight: 700; + } + &::after { + content: "`"; + font-weight: 700; + } + } + } +} diff --git a/packages/web/src/components/share.module.css b/packages/web/src/components/share.module.css index 14680736..dafbdd8a 100644 --- a/packages/web/src/components/share.module.css +++ b/packages/web/src/components/share.module.css @@ -15,42 +15,76 @@ --lg-tool-width: 56rem; --term-icon: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2060%2016'%20preserveAspectRatio%3D'xMidYMid%20meet'%3E%3Ccircle%20cx%3D'8'%20cy%3D'8'%20r%3D'8'%2F%3E%3Ccircle%20cx%3D'30'%20cy%3D'8'%20r%3D'8'%2F%3E%3Ccircle%20cx%3D'52'%20cy%3D'8'%20r%3D'8'%2F%3E%3C%2Fsvg%3E"); +} - [data-component="header"] { +[data-element-button-text] { + cursor: pointer; + appearance: none; + background-color: transparent; + border: none; + padding: 0; + color: var(--sl-color-text-secondary); + + &:hover { + color: var(--sl-color-text); + } + + &[data-element-button-more] { display: flex; - flex-direction: column; + align-items: center; + gap: 0.125rem; + + span[data-button-icon] { + line-height: 1; + opacity: 0.85; + svg { + display: block; + } + } + } +} + +[data-element-label] { + text-transform: uppercase; + letter-spacing: -0.5px; + color: var(--sl-color-text-dimmed); +} + +.header { + display: flex; + flex-direction: column; + gap: 1rem; + + @media (max-width: 30rem) { gap: 1rem; - - @media (max-width: 30rem) { - gap: 1rem; - } } - [data-component="header-title"] { - font-size: 2.75rem; - font-weight: 500; - line-height: 1.2; - letter-spacing: -0.05em; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 3; - line-clamp: 3; - overflow: hidden; - - @media (max-width: 30rem) { - font-size: 1.75rem; - line-height: 1.25; + [data-section="title"] { + h1 { + font-size: 2.75rem; + font-weight: 500; + line-height: 1.2; + letter-spacing: -0.05em; + display: -webkit-box; + -webkit-box-orient: vertical; -webkit-line-clamp: 3; + overflow: hidden; + + @media (max-width: 30rem) { + font-size: 1.75rem; + line-height: 1.25; + -webkit-line-clamp: 3; + } } } - [data-component="header-details"] { + [data-section="row"] { display: flex; flex-direction: column; gap: 0.5rem; } - [data-component="header-stats"] { + [data-section="stats"] { list-style-type: none; padding: 0; margin: 0; @@ -58,62 +92,41 @@ gap: 0.5rem 0.875rem; flex-wrap: wrap; - [data-slot="item"] { + li { display: flex; align-items: center; - gap: 0.3125rem; + gap: 0.5rem; font-size: 0.875rem; span[data-placeholder] { color: var(--sl-color-text-dimmed); } } - - [data-slot="icon"] { - flex: 0 0 auto; - color: var(--sl-color-text-dimmed); - opacity: 0.85; - - svg { - display: block; - } - } - - [data-slot="model"] { - color: var(--sl-color-text); - } } - [data-component="header-time"] { - color: var(--sl-color-text-dimmed); - font-size: 0.875rem; - } + [data-section="stats"] { + li { + gap: 0.3125rem; - [data-component="text-button"] { - cursor: pointer; - appearance: none; - background-color: transparent; - border: none; - padding: 0; - color: var(--sl-color-text-secondary); - - &:hover { - color: var(--sl-color-text); - } - - &[data-element-button-more] { - display: flex; - align-items: center; - gap: 0.125rem; - - span[data-button-icon] { - line-height: 1; + [data-stat-icon] { + flex: 0 0 auto; + color: var(--sl-color-text-dimmed); opacity: 0.85; - svg { display: block; } } + + span[data-stat-model] { + color: var(--sl-color-text); + } + } + } + + [data-section="time"] { + span { + color: var(--sl-color-text-dimmed); + font-size: 0.875rem; } } } @@ -157,12 +170,10 @@ svg:nth-child(3) { display: none; } - &:hover { svg:nth-child(1) { display: none; } - svg:nth-child(2) { display: block; } @@ -202,14 +213,12 @@ opacity: 1; visibility: visible; } - a, a:hover { svg:nth-child(1), svg:nth-child(2) { display: none; } - svg:nth-child(3) { display: block; } @@ -244,7 +253,7 @@ line-height: 18px; font-size: 0.875rem; color: var(--sl-color-text-secondary); - max-width: var(--md-tool-width); + max-width: var(--sm-tool-width); display: flex; align-items: flex-start; @@ -255,7 +264,7 @@ } b { - color: var(--sl-color-text); + color: var(--sl-color-text); word-break: break-all; font-weight: 500; } @@ -339,7 +348,8 @@ } [data-part-type="tool-grep"] { - &:not(:has([data-part-tool-args])) > [data-section="content"] > [data-part-tool-body] { + &:not(:has([data-part-tool-args])) + > [data-section="content"] > [data-part-tool-body] { gap: 0.5rem; } } @@ -364,7 +374,6 @@ } } } - [data-part-type="summary"] { & > [data-section="decoration"] { span:first-child { @@ -379,19 +388,15 @@ &[data-status="connected"] { background-color: var(--sl-color-green); } - &[data-status="connecting"] { background-color: var(--sl-color-orange); } - &[data-status="disconnected"] { background-color: var(--sl-color-divider); } - &[data-status="reconnecting"] { background-color: var(--sl-color-orange); } - &[data-status="error"] { background-color: var(--sl-color-red); } @@ -488,11 +493,7 @@ } } - &[data-background="none"] { - background-color: transparent; - } - - &[data-background="blue"] { + &[data-highlight="true"] { background-color: var(--sl-color-blue-low); } @@ -501,7 +502,6 @@ display: block; } } - &[data-expanded="false"] { pre { display: -webkit-box; @@ -537,25 +537,20 @@ span { margin-right: 0.25rem; - &:last-child { margin-right: 0; } } - span[data-color="red"] { color: var(--sl-color-red); } - span[data-color="dimmed"] { color: var(--sl-color-text-dimmed); } - span[data-marker="label"] { text-transform: uppercase; letter-spacing: -0.5px; } - span[data-separator] { margin-right: 0.375rem; } @@ -567,7 +562,6 @@ display: block; } } - &[data-expanded="false"] { [data-section="content"] { display: -webkit-box; @@ -582,6 +576,7 @@ padding: 2px 0; font-size: 0.75rem; } + } .message-terminal { @@ -617,7 +612,7 @@ } &::before { - content: ""; + content: ''; position: absolute; pointer-events: none; top: 8px; @@ -657,7 +652,6 @@ display: block; } } - &[data-expanded="false"] { pre { display: -webkit-box; @@ -675,7 +669,7 @@ } .message-markdown { - border: 1px solid var(--sl-color-blue-high); + background-color: var(--sl-color-bg-surface); padding: 0.5rem calc(0.5rem + 3px); border-radius: 0.25rem; display: flex; @@ -700,7 +694,6 @@ display: block; } } - &[data-expanded="false"] { [data-element-markdown] { display: -webkit-box; @@ -758,14 +751,10 @@ &[data-status="pending"] { color: var(--sl-color-text); } - &[data-status="in_progress"] { color: var(--sl-color-text); - & > span { - border-color: var(--sl-color-orange); - } - + & > span { border-color: var(--sl-color-orange); } & > span::before { content: ""; position: absolute; @@ -776,14 +765,10 @@ box-shadow: inset 1rem 1rem var(--sl-color-orange-low); } } - &[data-status="completed"] { color: var(--sl-color-text-secondary); - & > span { - border-color: var(--sl-color-green-low); - } - + & > span { border-color: var(--sl-color-green-low); } & > span::before { content: ""; position: absolute; @@ -814,9 +799,7 @@ display: flex; align-items: center; justify-content: center; - transition: - all 0.15s ease, - opacity 0.5s ease; + transition: all 0.15s ease, opacity 0.5s ease; z-index: 100; appearance: none; opacity: 1; diff --git a/packages/web/src/components/share/common.tsx b/packages/web/src/components/share/common.tsx deleted file mode 100644 index 9f5221de..00000000 --- a/packages/web/src/components/share/common.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { createSignal, onCleanup, splitProps } from "solid-js" -import type { JSX } from "solid-js/jsx-runtime" -import { IconCheckCircle, IconHashtag } from "../icons" - -interface AnchorProps extends JSX.HTMLAttributes { - id: string -} -export function AnchorIcon(props: AnchorProps) { - const [local, rest] = splitProps(props, ["id", "children"]) - const [copied, setCopied] = createSignal(false) - - return ( - - ) -} - -export function createOverflow() { - const [overflow, setOverflow] = createSignal(false) - return { - get status() { - return overflow() - }, - ref(el: HTMLElement) { - const ro = new ResizeObserver(() => { - if (el.scrollHeight > el.clientHeight + 1) { - setOverflow(true) - } - return - }) - ro.observe(el) - - onCleanup(() => { - ro.disconnect() - }) - }, - } -} diff --git a/packages/web/src/components/share/content-code.module.css b/packages/web/src/components/share/content-code.module.css deleted file mode 100644 index b95f936d..00000000 --- a/packages/web/src/components/share/content-code.module.css +++ /dev/null @@ -1,25 +0,0 @@ -.root { - max-width: var(--md-tool-width); - border: 1px solid var(--sl-color-divider); - background-color: var(--sl-color-bg-surface); - border-radius: 0.25rem; - padding: 0.5rem calc(0.5rem + 3px); - - &[data-flush="true"] { - border: none; - background-color: transparent; - padding: 0; - } - - pre { - --shiki-dark-bg: var(--sl-color-bg-surface) !important; - line-height: 1.6; - font-size: 0.75rem; - white-space: pre-wrap; - word-break: break-word; - - span { - white-space: break-spaces; - } - } -} diff --git a/packages/web/src/components/share/content-code.tsx b/packages/web/src/components/share/content-code.tsx deleted file mode 100644 index b8c4f2cc..00000000 --- a/packages/web/src/components/share/content-code.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { type JSX, splitProps, createResource, Suspense } from "solid-js" -import { codeToHtml } from "shiki" -import style from "./content-code.module.css" -import { transformerNotationDiff } from "@shikijs/transformers" - -interface Props { - code: string - lang?: string - flush?: boolean -} -export function ContentCode(props: Props) { - const [html] = createResource( - () => [props.code, props.lang], - async ([code, lang]) => { - // TODO: For testing delays - // await new Promise((resolve) => setTimeout(resolve, 3000)) - return (await codeToHtml(code || "", { - lang: lang || "text", - themes: { - light: "github-light", - dark: "github-dark", - }, - transformers: [transformerNotationDiff()], - })) as string - }, - ) - return ( - -
- - ) -} diff --git a/packages/web/src/components/share/content-diff.module.css b/packages/web/src/components/share/content-diff.module.css deleted file mode 100644 index 718ae369..00000000 --- a/packages/web/src/components/share/content-diff.module.css +++ /dev/null @@ -1,125 +0,0 @@ -.root { - display: flex; - flex-direction: column; - border: 1px solid var(--sl-color-divider); - background-color: var(--sl-color-bg-surface); - border-radius: 0.25rem; - - [data-component="desktop"] { - display: block; - } - - [data-component="mobile"] { - display: none; - } - - [data-component="diff-block"] { - display: flex; - flex-direction: column; - } - - [data-component="diff-row"] { - display: grid; - grid-template-columns: 1fr 1fr; - align-items: stretch; - - [data-slot="before"], - [data-slot="after"] { - position: relative; - display: flex; - flex-direction: column; - overflow-x: visible; - min-width: 0; - align-items: stretch; - padding: 0 1rem; - - &[data-diff-type="removed"] { - background-color: var(--sl-color-red-low); - - pre { - --shiki-dark-bg: var(--sl-color-red-low) !important; - background-color: var(--sl-color-red-low) !important; - } - - &::before { - content: "-"; - position: absolute; - left: 0.5ch; - top: 1px; - user-select: none; - color: var(--sl-color-red-high); - } - } - - &[data-diff-type="added"] { - background-color: var(--sl-color-green-low); - - pre { - --shiki-dark-bg: var(--sl-color-green-low) !important; - background-color: var(--sl-color-green-low) !important; - } - - &::before { - content: "+"; - position: absolute; - user-select: none; - color: var(--sl-color-green-high); - left: 0.5ch; - top: 1px; - } - } - } - - [data-slot="before"] { - border-right: 1px solid var(--sl-color-divider); - } - } - - .diff > .row:first-child [data-section="cell"]:first-child { - padding-top: 0.5rem; - } - - .diff > .row:last-child [data-section="cell"]:last-child { - padding-bottom: 0.5rem; - } - - [data-section="cell"] { - position: relative; - flex: 1; - display: flex; - flex-direction: column; - - width: 100%; - padding: 0.1875rem 0.5rem 0.1875rem 2.2ch; - margin: 0; - - &[data-display-mobile="true"] { - display: none; - } - - pre { - --shiki-dark-bg: var(--sl-color-bg-surface) !important; - background-color: var(--sl-color-bg-surface) !important; - - white-space: pre-wrap; - word-break: break-word; - - code > span:empty::before { - content: "\00a0"; - white-space: pre; - display: inline-block; - width: 0; - } - } - } - - @media (max-width: 40rem) { - [data-slot="desktop"] { - display: none; - } - - [data-slot="mobile"] { - display: block; - } - } -} diff --git a/packages/web/src/components/share/content-markdown.module.css b/packages/web/src/components/share/content-markdown.module.css deleted file mode 100644 index da1aa112..00000000 --- a/packages/web/src/components/share/content-markdown.module.css +++ /dev/null @@ -1,140 +0,0 @@ -.root { - border: 1px solid var(--sl-color-blue-high); - padding: 0.5rem calc(0.5rem + 3px); - border-radius: 0.25rem; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 1rem; - align-self: flex-start; - max-width: var(--md-tool-width); - - &[data-highlight="true"] { - background-color: var(--sl-color-blue-low); - } - - [data-slot="expand-button"] { - flex: 0 0 auto; - padding: 2px 0; - font-size: 0.75rem; - } - - [data-slot="markdown"] { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 3; - line-clamp: 3; - overflow: hidden; - - [data-expanded] & { - display: block; - } - - font-size: 0.875rem; - line-height: 1.5; - - p, - blockquote, - ul, - ol, - dl, - table, - pre { - margin-bottom: 1rem; - } - - strong { - font-weight: 600; - } - - ol { - list-style-position: inside; - padding-left: 0.75rem; - } - - ul { - padding-left: 1.5rem; - } - - h1, - h2, - h3, - h4, - h5, - h6 { - font-size: 0.875rem; - font-weight: 600; - margin-bottom: 0.5rem; - } - - & > *:last-child { - margin-bottom: 0; - } - - pre { - --shiki-dark-bg: var(--sl-color-bg-surface) !important; - background-color: var(--sl-color-bg-surface) !important; - padding: 0.5rem 0.75rem; - line-height: 1.6; - font-size: 0.75rem; - white-space: pre-wrap; - word-break: break-word; - - span { - white-space: break-spaces; - } - } - - code { - font-weight: 500; - - &:not(pre code) { - &::before { - content: "`"; - font-weight: 700; - } - - &::after { - content: "`"; - font-weight: 700; - } - } - } - - table { - border-collapse: collapse; - width: 100%; - } - - th, - td { - border: 1px solid var(--sl-color-border); - padding: 0.5rem 0.75rem; - text-align: left; - } - - th { - border-bottom: 1px solid var(--sl-color-border); - } - - /* Remove outer borders */ - table tr:first-child th, - table tr:first-child td { - border-top: none; - } - - table tr:last-child td { - border-bottom: none; - } - - table th:first-child, - table td:first-child { - border-left: none; - } - - table th:last-child, - table td:last-child { - border-right: none; - } - } -} diff --git a/packages/web/src/components/share/content-markdown.tsx b/packages/web/src/components/share/content-markdown.tsx deleted file mode 100644 index f7927129..00000000 --- a/packages/web/src/components/share/content-markdown.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import style from "./content-markdown.module.css" -import { createResource, createSignal } from "solid-js" -import { createOverflow } from "./common" -import { transformerNotationDiff } from "@shikijs/transformers" -import { marked } from "marked" -import markedShiki from "marked-shiki" -import { codeToHtml } from "shiki" - -const markedWithShiki = marked.use( - markedShiki({ - highlight(code, lang) { - return codeToHtml(code, { - lang: lang || "text", - themes: { - light: "github-light", - dark: "github-dark", - }, - transformers: [transformerNotationDiff()], - }) - }, - }), -) - -interface Props { - text: string - expand?: boolean - highlight?: boolean -} -export function ContentMarkdown(props: Props) { - const [html] = createResource( - () => strip(props.text), - async (markdown) => { - return markedWithShiki.parse(markdown) - }, - ) - const [expanded, setExpanded] = createSignal(false) - const overflow = createOverflow() - - return ( -
-
- - {!props.expand && overflow.status && ( - - )} -
- ) -} - -function strip(text: string): string { - const wrappedRe = /^\s*<([A-Za-z]\w*)>\s*([\s\S]*?)\s*<\/\1>\s*$/ - const match = text.match(wrappedRe) - return match ? match[2] : text -} diff --git a/packages/web/src/components/share/content-text.module.css b/packages/web/src/components/share/content-text.module.css deleted file mode 100644 index f8d0b0b9..00000000 --- a/packages/web/src/components/share/content-text.module.css +++ /dev/null @@ -1,57 +0,0 @@ -.root { - color: var(--sl-color-text); - background-color: var(--sl-color-bg-surface); - padding: 0.5rem calc(0.5rem + 3px); - border-radius: 0.25rem; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 1rem; - align-self: flex-start; - max-width: var(--md-tool-width); - font-size: 0.875rem; - - &[data-compact] { - font-size: 0.75rem; - color: var(--sl-color-text-dimmed); - } - - [data-slot="text"] { - line-height: 1.5; - white-space: pre-wrap; - overflow-wrap: anywhere; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 3; - line-clamp: 3; - overflow: hidden; - - [data-expanded] & { - display: block; - } - } - - [data-slot="expand-button"] { - flex: 0 0 auto; - padding: 2px 0; - font-size: 0.75rem; - } - - &[data-theme="invert"] { - background-color: var(--sl-color-blue-high); - color: var(--sl-color-text-invert); - - [data-slot="expand-button"] { - opacity: 0.85; - color: var(--sl-color-text-invert); - - &:hover { - opacity: 1; - } - } - } - - &[data-theme="blue"] { - background-color: var(--sl-color-blue-low); - } -} diff --git a/packages/web/src/components/share/content-text.tsx b/packages/web/src/components/share/content-text.tsx deleted file mode 100644 index c52e0dfc..00000000 --- a/packages/web/src/components/share/content-text.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import style from "./content-text.module.css" -import { createSignal } from "solid-js" -import { createOverflow } from "./common" - -interface Props { - text: string - expand?: boolean - compact?: boolean -} -export function ContentText(props: Props) { - const [expanded, setExpanded] = createSignal(false) - const overflow = createOverflow() - - return ( -
-
-        {props.text}
-      
- {((!props.expand && overflow.status) || expanded()) && ( - - )} -
- ) -} diff --git a/packages/web/src/components/share/part.module.css b/packages/web/src/components/share/part.module.css deleted file mode 100644 index 9145cddf..00000000 --- a/packages/web/src/components/share/part.module.css +++ /dev/null @@ -1,375 +0,0 @@ -.root { - display: flex; - gap: 0.625rem; - - [data-component="decoration"] { - flex: 0 0 auto; - display: flex; - flex-direction: column; - gap: 0.625rem; - align-items: center; - justify-content: flex-start; - - [data-slot="anchor"] { - position: relative; - - a:first-child { - display: block; - flex: 0 0 auto; - width: 18px; - opacity: 0.65; - - svg { - color: var(--sl-color-text-secondary); - display: block; - - &:nth-child(3) { - color: var(--sl-color-green-high); - } - } - - svg:nth-child(2), - svg:nth-child(3) { - display: none; - } - - &:hover { - svg:nth-child(1) { - display: none; - } - - svg:nth-child(2) { - display: block; - } - } - } - - [data-copied] & { - a, - a:hover { - svg:nth-child(1), - svg:nth-child(2) { - display: none; - } - - svg:nth-child(3) { - display: block; - } - } - } - } - - [data-slot="bar"] { - width: 3px; - height: 100%; - border-radius: 1px; - background-color: var(--sl-color-hairline); - } - - [data-slot="tooltip"] { - position: absolute; - top: 50%; - left: calc(100% + 12px); - transform: translate(0, -50%); - line-height: 1.1; - padding: 0.375em 0.5em calc(0.375em + 2px); - background: var(--sl-color-white); - color: var(--sl-color-text-invert); - font-size: 0.6875rem; - border-radius: 7px; - white-space: nowrap; - - z-index: 1; - opacity: 0; - visibility: hidden; - - &::after { - content: ""; - position: absolute; - top: 50%; - left: -15px; - transform: translateY(-50%); - border: 8px solid transparent; - border-right-color: var(--sl-color-white); - } - - [data-copied] & { - opacity: 1; - visibility: visible; - } - } - } - - [data-component="content"] { - display: flex; - flex-direction: column; - gap: 1rem; - flex-grow: 1; - } - - [data-component="spacer"] { - height: 0rem; - } - - [data-component="content-footer"] { - align-self: flex-start; - font-size: 0.75rem; - color: var(--sl-color-text-dimmed); - } - - [data-component="step-start"] { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 0.375rem; - padding-bottom: 1rem; - - [data-slot="provider"] { - line-height: 18px; - font-size: 0.875rem; - text-transform: uppercase; - letter-spacing: -0.5px; - color: var(--sl-color-text-secondary); - } - - [data-slot="model"] { - line-height: 1.5; - } - } - - [data-component="button-text"] { - cursor: pointer; - appearance: none; - background-color: transparent; - border: none; - padding: 0; - color: var(--sl-color-text-secondary); - font-size: 0.75rem; - - &:hover { - color: var(--sl-color-text); - } - - &[data-more] { - display: flex; - align-items: center; - gap: 0.125rem; - - span[data-slot="icon"] { - line-height: 1; - opacity: 0.85; - - svg { - display: block; - } - } - } - } - - [data-component="tool"] { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 0.375rem; - padding-bottom: 1rem; - } - - [data-component="tool-title"] { - line-height: 18px; - font-size: 0.875rem; - color: var(--sl-color-text-secondary); - max-width: var(--md-tool-width); - display: flex; - align-items: flex-start; - gap: 0.375rem; - - [data-slot="name"] { - text-transform: uppercase; - letter-spacing: -0.5px; - } - - [data-slot="target"] { - color: var(--sl-color-text); - word-break: break-all; - font-weight: 500; - } - } - - [data-component="tool-result"] { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 0.5rem; - } - - [data-component="todos"] { - list-style-type: none; - padding: 0; - margin: 0; - width: 100%; - max-width: var(--sm-tool-width); - border: 1px solid var(--sl-color-divider); - border-radius: 0.25rem; - - [data-slot="item"] { - margin: 0; - position: relative; - padding-left: 1.5rem; - font-size: 0.75rem; - padding: 0.375rem 0.625rem 0.375rem 1.75rem; - border-bottom: 1px solid var(--sl-color-divider); - line-height: 1.5; - word-break: break-word; - - &:last-child { - border-bottom: none; - } - - & > span { - position: absolute; - display: inline-block; - left: 0.5rem; - top: calc(0.5rem + 1px); - width: 0.75rem; - height: 0.75rem; - border: 1px solid var(--sl-color-divider); - border-radius: 0.15rem; - - &::before { - } - } - - &[data-status="pending"] { - color: var(--sl-color-text); - } - - &[data-status="in_progress"] { - color: var(--sl-color-text); - - & > span { - border-color: var(--sl-color-orange); - } - - & > span::before { - content: ""; - position: absolute; - top: 2px; - left: 2px; - width: calc(0.75rem - 2px - 4px); - height: calc(0.75rem - 2px - 4px); - box-shadow: inset 1rem 1rem var(--sl-color-orange-low); - } - } - - &[data-status="completed"] { - color: var(--sl-color-text-secondary); - - & > span { - border-color: var(--sl-color-green-low); - } - - & > span::before { - content: ""; - position: absolute; - top: 2px; - left: 2px; - width: calc(0.75rem - 2px - 4px); - height: calc(0.75rem - 2px - 4px); - box-shadow: inset 1rem 1rem var(--sl-color-green); - - transform-origin: bottom left; - clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); - } - } - } - } - - [data-component="terminal"] { - width: 100%; - max-width: var(--sm-tool-width); - - [data-slot="body"] { - display: flex; - flex-direction: column; - border: 1px solid var(--sl-color-divider); - border-radius: 0.25rem; - overflow: hidden; - } - - [data-slot="header"] { - position: relative; - border-bottom: 1px solid var(--sl-color-divider); - width: 100%; - height: 1.625rem; - text-align: center; - padding: 0 3.25rem; - - > span { - max-width: min(100%, 140ch); - display: inline-block; - white-space: nowrap; - overflow: hidden; - line-height: 1.625rem; - font-size: 0.75rem; - text-overflow: ellipsis; - color: var(--sl-color-text-dimmed); - } - - &::before { - content: ""; - position: absolute; - pointer-events: none; - top: 8px; - left: 10px; - width: 2rem; - height: 0.5rem; - line-height: 0; - background-color: var(--sl-color-hairline); - mask-image: var(--term-icon); - mask-repeat: no-repeat; - } - } - - [data-slot="content"] { - display: flex; - flex-direction: column; - padding: 0.5rem calc(0.5rem + 3px); - - pre { - --shiki-dark-bg: var(--sl-color-bg) !important; - background-color: var(--sl-color-bg) !important; - line-height: 1.6; - font-size: 0.75rem; - white-space: pre-wrap; - word-break: break-word; - } - } - } - - [data-component="tool-args"] { - display: inline-grid; - align-items: center; - grid-template-columns: max-content max-content minmax(0, 1fr); - max-width: var(--md-tool-width); - gap: 0.25rem 0.375rem; - - & > div:nth-child(3n + 1) { - width: 8px; - height: 2px; - border-radius: 1px; - background: var(--sl-color-divider); - } - - & > div:nth-child(3n + 2), - & > div:nth-child(3n + 3) { - font-size: 0.75rem; - line-height: 1.5; - } - - & > div:nth-child(3n + 3) { - padding-left: 0.125rem; - word-break: break-word; - color: var(--sl-color-text-secondary); - } - } -} diff --git a/packages/web/src/components/share/part.tsx b/packages/web/src/components/share/part.tsx deleted file mode 100644 index 3ee2c61a..00000000 --- a/packages/web/src/components/share/part.tsx +++ /dev/null @@ -1,664 +0,0 @@ -import { createMemo, createSignal, For, Match, Show, Switch, type JSX, type ParentProps } from "solid-js" -import { - IconCheckCircle, - IconChevronDown, - IconChevronRight, - IconHashtag, - IconSparkles, - IconGlobeAlt, - IconDocument, - IconQueueList, - IconCommandLine, - IconDocumentPlus, - IconPencilSquare, - IconRectangleStack, - IconMagnifyingGlass, - IconDocumentMagnifyingGlass, -} from "../icons" -import styles from "./part.module.css" -import type { MessageV2 } from "opencode/session/message-v2" -import { ContentText } from "./content-text" -import { ContentMarkdown } from "./content-markdown" -import { DateTime } from "luxon" -import CodeBlock from "../CodeBlock" -import map from "lang-map" -import type { Diagnostic } from "vscode-languageserver-types" - -import { ContentCode } from "./content-code" -import { ContentDiff } from "./content-diff" - -export interface PartProps { - index: number - message: MessageV2.Info - part: MessageV2.AssistantPart | MessageV2.UserPart - last: boolean -} - -export function Part(props: PartProps) { - const [copied, setCopied] = createSignal(false) - const id = createMemo(() => props.message.id + "-" + props.index) - - return ( -
- -
- {props.message.role === "user" && props.part.type === "text" && ( - <> - - - )} - {props.message.role === "assistant" && props.part.type === "text" && ( - <> - - {props.last && props.message.role === "assistant" && props.message.time.completed && ( -
- {DateTime.fromMillis(props.message.time.completed).toLocaleString(DateTime.DATETIME_MED)} -
- )} - - - )} - {props.part.type === "step-start" && props.message.role === "assistant" && ( -
-
{props.message.providerID}
-
{props.message.modelID}
-
- )} - {props.part.type === "tool" && - props.part.state.status === "completed" && - props.message.role === "assistant" && ( -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- )} -
-
- ) -} - -type ToolProps = { - id: MessageV2.ToolPart["id"] - tool: MessageV2.ToolPart["tool"] - state: MessageV2.ToolStateCompleted - message: MessageV2.Assistant - isLastPart?: boolean -} - -interface Todo { - id: string - content: string - status: "pending" | "in_progress" | "completed" - priority: "low" | "medium" | "high" -} - -function stripWorkingDirectory(filePath?: string, workingDir?: string) { - if (filePath === undefined || workingDir === undefined) return filePath - - const prefix = workingDir.endsWith("/") ? workingDir : workingDir + "/" - - if (filePath === workingDir) { - return "" - } - - if (filePath.startsWith(prefix)) { - return filePath.slice(prefix.length) - } - - return filePath -} - -function getShikiLang(filename: string) { - const ext = filename.split(".").pop()?.toLowerCase() ?? "" - const langs = map.languages(ext) - const type = langs?.[0]?.toLowerCase() - - const overrides: Record = { - conf: "shellscript", - } - - return type ? (overrides[type] ?? type) : "plaintext" -} - -function getDiagnostics(diagnosticsByFile: Record, currentFile: string): JSX.Element[] { - const result: JSX.Element[] = [] - - if (diagnosticsByFile === undefined || diagnosticsByFile[currentFile] === undefined) return result - - for (const diags of Object.values(diagnosticsByFile)) { - for (const d of diags) { - if (d.severity !== 1) continue - - const line = d.range.start.line + 1 - const column = d.range.start.character + 1 - - result.push( -
-          
-            Error
-          
-          
-            [{line}:{column}]
-          
-          {d.message}
-        
, - ) - } - } - - return result -} - -function formatErrorString(error: string): JSX.Element { - const errorMarker = "Error: " - const startsWithError = error.startsWith(errorMarker) - - return startsWithError ? ( -
-      
-        Error
-      
-      {error.slice(errorMarker.length)}
-    
- ) : ( -
-      {error}
-    
- ) -} - -export function TodoWriteTool(props: ToolProps) { - const priority: Record = { - in_progress: 0, - pending: 1, - completed: 2, - } - const todos = createMemo(() => - ((props.state.input?.todos ?? []) as Todo[]).slice().sort((a, b) => priority[a.status] - priority[b.status]), - ) - const starting = () => todos().every((t: Todo) => t.status === "pending") - const finished = () => todos().every((t: Todo) => t.status === "completed") - - return ( - <> -
- - - Creating plan - Completing plan - - -
- 0}> -
    - - {(todo) => ( -
  • - - {todo.content} -
  • - )} -
    -
-
- - ) -} - -export function GrepTool(props: ToolProps) { - return ( - <> -
- Grep - “{props.state.input.pattern}” -
-
- - 0}> - - - - - - - - -
- - ) -} - -export function ListTool(props: ToolProps) { - const path = createMemo(() => - props.state.input?.path !== props.message.path.cwd - ? stripWorkingDirectory(props.state.input?.path, props.message.path.cwd) - : props.state.input?.path, - ) - - return ( - <> -
- LS - - {path()} - -
-
- - - - - - - -
- - ) -} - -export function WebFetchTool(props: ToolProps) { - return ( - <> -
- Fetch - {props.state.input.url} -
-
- - -
{formatErrorString(props.state.output)}
-
- - - - - -
-
- - ) -} - -export function ReadTool(props: ToolProps) { - const filePath = createMemo(() => stripWorkingDirectory(props.state.input?.filePath, props.message.path.cwd)) - - return ( - <> -
- Read - - {filePath()} - -
-
- - -
{formatErrorString(props.state.output)}
-
- - - - - - - - - - -
-
- - ) -} - -export function WriteTool(props: ToolProps) { - const filePath = createMemo(() => stripWorkingDirectory(props.state.input?.filePath, props.message.path.cwd)) - const diagnostics = createMemo(() => getDiagnostics(props.state.metadata?.diagnostics, props.state.input.filePath)) - - return ( - <> -
- Write - - {filePath()} - -
- 0}> -
{diagnostics()}
-
-
- - -
{formatErrorString(props.state.output)}
-
- - - - - -
-
- - ) -} - -export function EditTool(props: ToolProps) { - const filePath = createMemo(() => stripWorkingDirectory(props.state.input.filePath, props.message.path.cwd)) - const diagnostics = createMemo(() => getDiagnostics(props.state.metadata?.diagnostics, props.state.input.filePath)) - - return ( - <> -
- Edit - - {filePath()} - -
-
- - -
{formatErrorString(props.state.metadata?.message || "")}
-
- -
- -
-
-
-
- 0}> -
{diagnostics()}
-
- - ) -} - -export function BashTool(props: ToolProps) { - return ( - <> -
-
-
- {props.state.metadata.description} -
-
- - -
-
-
- - ) -} - -export function GlobTool(props: ToolProps) { - return ( - <> -
- Glob - “{props.state.input.pattern}” -
- - 0}> -
- - - -
-
- - - -
- - ) -} - -interface ResultsButtonProps extends ParentProps { - showCopy?: string - hideCopy?: string -} -function ResultsButton(props: ResultsButtonProps) { - const [show, setShow] = createSignal(false) - - return ( - <> - - {props.children} - - ) -} - -export function Spacer() { - return
-} - -function Footer(props: ParentProps<{ title: string }>) { - return ( -
- {props.children} -
- ) -} - -export function FallbackTool(props: ToolProps) { - return ( - <> -
- {props.tool} -
-
- - {(arg) => ( - <> -
-
{arg[0]}
-
{arg[1]}
- - )} -
-
- - -
- - - -
-
-
- - ) -} - -// Converts nested objects/arrays into [path, value] pairs. -// E.g. {a:{b:{c:1}}, d:[{e:2}, 3]} => [["a.b.c",1], ["d[0].e",2], ["d[1]",3]] -function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> { - const entries: Array<[string, any]> = [] - - for (const [key, value] of Object.entries(obj)) { - const path = prefix ? `${prefix}.${key}` : key - - if (value !== null && typeof value === "object") { - if (Array.isArray(value)) { - value.forEach((item, index) => { - const arrayPath = `${path}[${index}]` - if (item !== null && typeof item === "object") { - entries.push(...flattenToolArgs(item, arrayPath)) - } else { - entries.push([arrayPath, item]) - } - }) - } else { - entries.push(...flattenToolArgs(value, path)) - } - } else { - entries.push([path, value]) - } - } - - return entries -} diff --git a/packages/web/src/content/docs/docs/cli.mdx b/packages/web/src/content/docs/docs/cli.mdx index 57e59521..49d343be 100644 --- a/packages/web/src/content/docs/docs/cli.mdx +++ b/packages/web/src/content/docs/docs/cli.mdx @@ -39,12 +39,12 @@ opencode run Explain the use of context in Go #### Flags -| Flag | Short | Description | -| ------------ | ----- | ------------------------------------------ | -| `--continue` | `-c` | Continue the last session | -| `--session` | `-s` | Session ID to continue | -| `--share` | | Share the session | -| `--model` | `-m` | Model to use in the form of provider/model | +| Flag | Short | Description | +| ----------------- | ----- | --------------------- | +| `--continue` | `-c` | Continue the last session | +| `--session` | `-s` | Session ID to continue | +| `--share` | | Share the session | +| `--model` | `-m` | Model to use in the form of provider/model | --- @@ -122,8 +122,8 @@ opencode upgrade v0.1.48 The opencode CLI takes the following flags. -| Flag | Short | Description | -| -------------- | ----- | -------------------- | -| `--help` | `-h` | Display help | -| `--version` | | Print version number | -| `--print-logs` | | Print logs to stderr | +| Flag | Short | Description | +| ----------------- | ----- | --------------------- | +| `--help` | `-h` | Display help | +| `--version` | | Print version number | +| `--print-logs` | | Print logs to stderr | diff --git a/packages/web/src/content/docs/docs/config.mdx b/packages/web/src/content/docs/docs/config.mdx index 40583ea0..d88749c6 100644 --- a/packages/web/src/content/docs/docs/config.mdx +++ b/packages/web/src/content/docs/docs/config.mdx @@ -39,7 +39,7 @@ You can configure the providers and models you want to use in your opencode conf ```json title="opencode.json" { "$schema": "https://opencode.ai/config.json", - "provider": {}, + "provider": { }, "model": "" } ``` @@ -70,7 +70,7 @@ You can customize your keybinds through the `keybinds` option. ```json title="opencode.json" { "$schema": "https://opencode.ai/config.json", - "keybinds": {} + "keybinds": { } } ``` @@ -85,7 +85,7 @@ You can configure MCP servers you want to use through the `mcp` option. ```json title="opencode.json" { "$schema": "https://opencode.ai/config.json", - "mcp": {} + "mcp": { } } ``` @@ -105,7 +105,6 @@ You can disable providers that are loaded automatically through the `disabled_pr ``` The `disabled_providers` option accepts an array of provider IDs. When a provider is disabled: - - It won't be loaded even if environment variables are set - It won't be loaded even if API keys are configured through `opencode auth login` - The provider's models won't appear in the model selection list diff --git a/packages/web/src/content/docs/docs/index.mdx b/packages/web/src/content/docs/docs/index.mdx index 0484e9b3..b39ce452 100644 --- a/packages/web/src/content/docs/docs/index.mdx +++ b/packages/web/src/content/docs/docs/index.mdx @@ -3,7 +3,7 @@ title: Intro description: Get started with opencode. --- -import { Tabs, TabItem } from "@astrojs/starlight/components" +import { Tabs, TabItem } from '@astrojs/starlight/components'; [**opencode**](/) is an AI coding agent built for the terminal. It features: @@ -22,24 +22,24 @@ import { Tabs, TabItem } from "@astrojs/starlight/components" - ```bash - npm install -g opencode-ai - ``` + ```bash + npm install -g opencode-ai + ``` - ```bash - bun install -g opencode-ai - ``` + ```bash + bun install -g opencode-ai + ``` - ```bash - pnpm install -g opencode-ai - ``` + ```bash + pnpm install -g opencode-ai + ``` - ```bash - yarn global add opencode-ai - ``` + ```bash + yarn global add opencode-ai + ``` diff --git a/packages/web/src/content/docs/docs/mcp-servers.mdx b/packages/web/src/content/docs/docs/mcp-servers.mdx index 0496e31c..72c33e8a 100644 --- a/packages/web/src/content/docs/docs/mcp-servers.mdx +++ b/packages/web/src/content/docs/docs/mcp-servers.mdx @@ -30,7 +30,6 @@ Add a local MCP servers under `mcp.localmcp`. "enabled": true, "environment": { "MY_ENV_VAR": "my_env_var_value" - } } } } diff --git a/packages/web/src/content/docs/docs/rules.mdx b/packages/web/src/content/docs/docs/rules.mdx index b1b55b02..aed08535 100644 --- a/packages/web/src/content/docs/docs/rules.mdx +++ b/packages/web/src/content/docs/docs/rules.mdx @@ -31,20 +31,17 @@ You can also just create this file manually. Here's an example of some things yo This is an SST v3 monorepo with TypeScript. The project uses bun workspaces for package management. ## Project Structure - - `packages/` - Contains all workspace packages (functions, core, web, etc.) - `infra/` - Infrastructure definitions split by service (storage.ts, api.ts, web.ts) - `sst.config.ts` - Main SST configuration with dynamic imports ## Code Standards - - Use TypeScript with strict mode enabled - Shared code goes in `packages/core/` with proper exports configuration - Functions go in `packages/functions/` - Infrastructure should be split into logical files in `infra/` ## Monorepo Conventions - - Import shared modules using workspace names: `@my-app/core/example` ``` @@ -76,29 +73,3 @@ So when opencode starts, it looks for: 2. **Global file** by checking `~/.config/opencode/AGENTS.md` If you have both global and project-specific rules, opencode will combine them together. - ---- - -## Custom Instructions - -You can also specify custom instruction files using the `instructions` configuration in your `opencode.json` or global `~/.config/opencode/config.json`: - -```json title="opencode.json" -{ - "$schema": "https://opencode.ai/config.json", - "instructions": [".cursor/rules/*.md"] -} -``` - -You can specify multiple files like `CONTRIBUTING.md` and `docs/guidelines.md`, and use glob patterns to match multiple files. - -For example, to reuse your existing Cursor rules: - -```json title="opencode.json" -{ - "$schema": "https://opencode.ai/config.json", - "instructions": [".cursor/rules/*.md"] -} -``` - -All instruction files are combined with your `AGENTS.md` files. diff --git a/packages/web/src/content/docs/docs/themes.mdx b/packages/web/src/content/docs/docs/themes.mdx index 12559153..da612284 100644 --- a/packages/web/src/content/docs/docs/themes.mdx +++ b/packages/web/src/content/docs/docs/themes.mdx @@ -13,18 +13,18 @@ By default, opencode uses our own `opencode` theme. opencode comes with several built-in themes. -| Name | Description | -| ------------ | ------------------------------------------ | -| `system` | Adapts to your terminal's background color | -| `tokyonight` | Based on the Tokyonight theme | -| `everforest` | Based on the Everforest theme | -| `ayu` | Based on the Ayu dark theme | -| `catppuccin` | Based on the Catppuccin theme | -| `gruvbox` | Based on the Gruvbox theme | -| `kanagawa` | Based on the Kanagawa theme | -| `nord` | Based on the Nord theme | -| `matrix` | Hacker-style green on black theme | -| `one-dark` | Based on the Atom One Dark theme | +| Name | Description | +| --- | --- | +| `system` | Adapts to your terminal's background color | +| `tokyonight` | Based on the Tokyonight theme | +| `everforest` | Based on the Everforest theme | +| `ayu` | Based on the Ayu dark theme | +| `catppuccin` | Based on the Catppuccin theme | +| `gruvbox` | Based on the Gruvbox theme | +| `kanagawa` | Based on the Kanagawa theme | +| `nord` | Based on the Nord theme | +| `matrix` | Hacker-style green on black theme | +| `one-dark` | Based on the Atom One Dark theme | And more, we are constantly adding new themes. @@ -61,7 +61,7 @@ You can select a theme by bringing up the theme select with the `/theme` command ## Custom themes -opencode supports a flexible JSON-based theme system that allows users to create and customize themes easily. +opencode supports a flexible JSON-based theme system that allows users to create and customize themes easily. --- diff --git a/packages/web/src/types/lang-map.d.ts b/packages/web/src/types/lang-map.d.ts index 6df26d6a..b21d2a00 100644 --- a/packages/web/src/types/lang-map.d.ts +++ b/packages/web/src/types/lang-map.d.ts @@ -2,9 +2,9 @@ declare module "lang-map" { /** Returned by calling `map()` */ export interface MapReturn { /** All extensions keyed by language name */ - extensions: Record + extensions: Record; /** All languages keyed by file-extension */ - languages: Record + languages: Record; } /** @@ -14,14 +14,14 @@ declare module "lang-map" { * const { extensions, languages } = map(); * ``` */ - function map(): MapReturn + function map(): MapReturn; /** Static method: get extensions for a given language */ namespace map { - function extensions(language: string): string[] + function extensions(language: string): string[]; /** Static method: get languages for a given extension */ - function languages(extension: string): string[] + function languages(extension: string): string[]; } - export = map + export = map; } diff --git a/packages/web/sst-env.d.ts b/packages/web/sst-env.d.ts index 0397645b..b6a7e906 100644 --- a/packages/web/sst-env.d.ts +++ b/packages/web/sst-env.d.ts @@ -6,4 +6,4 @@ /// import "sst" -export {} +export {} \ No newline at end of file diff --git a/scripts/stats.ts b/scripts/stats.ts index 2abe7e1c..b30e57d9 100755 --- a/scripts/stats.ts +++ b/scripts/stats.ts @@ -26,9 +26,13 @@ async function fetchNpmDownloads(packageName: string): Promise { // Use a range from 2020 to current year + 5 years to ensure it works forever const currentYear = new Date().getFullYear() const endYear = currentYear + 5 - const response = await fetch(`https://api.npmjs.org/downloads/range/2020-01-01:${endYear}-12-31/${packageName}`) + const response = await fetch( + `https://api.npmjs.org/downloads/range/2020-01-01:${endYear}-12-31/${packageName}`, + ) if (!response.ok) { - console.warn(`Failed to fetch npm downloads for ${packageName}: ${response.status}`) + console.warn( + `Failed to fetch npm downloads for ${packageName}: ${response.status}`, + ) return 0 } const data: NpmDownloadsRange = await response.json() @@ -49,7 +53,9 @@ async function fetchReleases(): Promise { const response = await fetch(url) if (!response.ok) { - throw new Error(`GitHub API error: ${response.status} ${response.statusText}`) + throw new Error( + `GitHub API error: ${response.status} ${response.statusText}`, + ) } const batch: Release[] = await response.json() @@ -109,7 +115,11 @@ async function save(githubTotal: number, npmDownloads: number) { for (let i = lines.length - 1; i >= 0; i--) { const line = lines[i].trim() - if (line.startsWith("|") && !line.includes("Date") && !line.includes("---")) { + if ( + line.startsWith("|") && + !line.includes("Date") && + !line.includes("---") + ) { const match = line.match( /\|\s*[\d-]+\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|/, ) @@ -137,7 +147,11 @@ async function save(githubTotal: number, npmDownloads: number) { ? ` (${githubChange.toLocaleString()})` : " (+0)" const npmChangeStr = - npmChange > 0 ? ` (+${npmChange.toLocaleString()})` : npmChange < 0 ? ` (${npmChange.toLocaleString()})` : " (+0)" + npmChange > 0 + ? ` (+${npmChange.toLocaleString()})` + : npmChange < 0 + ? ` (${npmChange.toLocaleString()})` + : " (+0)" const totalChangeStr = totalChange > 0 ? ` (+${totalChange.toLocaleString()})` @@ -168,7 +182,9 @@ const { total: githubTotal, stats } = calculate(releases) console.log("Fetching npm all-time downloads for opencode-ai...\n") const npmDownloads = await fetchNpmDownloads("opencode-ai") -console.log(`Fetched npm all-time downloads: ${npmDownloads.toLocaleString()}\n`) +console.log( + `Fetched npm all-time downloads: ${npmDownloads.toLocaleString()}\n`, +) await save(githubTotal, npmDownloads) @@ -186,18 +202,24 @@ console.log("-".repeat(60)) stats .sort((a, b) => b.downloads - a.downloads) .forEach((release) => { - console.log(`${release.tag.padEnd(15)} ${release.downloads.toLocaleString().padStart(10)} downloads`) + console.log( + `${release.tag.padEnd(15)} ${release.downloads.toLocaleString().padStart(10)} downloads`, + ) if (release.assets.length > 1) { release.assets .sort((a, b) => b.downloads - a.downloads) .forEach((asset) => { - console.log(` └─ ${asset.name.padEnd(25)} ${asset.downloads.toLocaleString().padStart(8)}`) + console.log( + ` └─ ${asset.name.padEnd(25)} ${asset.downloads.toLocaleString().padStart(8)}`, + ) }) } }) console.log("-".repeat(60)) -console.log(`GitHub Total: ${githubTotal.toLocaleString()} downloads across ${releases.length} releases`) +console.log( + `GitHub Total: ${githubTotal.toLocaleString()} downloads across ${releases.length} releases`, +) console.log(`npm Total: ${npmDownloads.toLocaleString()} downloads`) console.log(`Combined Total: ${totalDownloads.toLocaleString()} downloads`) diff --git a/sst-env.d.ts b/sst-env.d.ts index 45c07b66..627d74a5 100644 --- a/sst-env.d.ts +++ b/sst-env.d.ts @@ -5,20 +5,20 @@ declare module "sst" { export interface Resource { - Api: { - type: "sst.cloudflare.Worker" - url: string + "Api": { + "type": "sst.cloudflare.Worker" + "url": string } - Bucket: { - type: "sst.cloudflare.Bucket" + "Bucket": { + "type": "sst.cloudflare.Bucket" } - Web: { - type: "sst.cloudflare.Astro" - url: string + "Web": { + "type": "sst.cloudflare.Astro" + "url": string } } } /// import "sst" -export {} +export {} \ No newline at end of file diff --git a/stainless.yml b/stainless.yml index 2e262b74..f8d654fb 100644 --- a/stainless.yml +++ b/stainless.yml @@ -34,7 +34,6 @@ resources: models: unknownError: UnknownError providerAuthError: ProviderAuthError - messageAbortedError: MessageAbortedError event: methods: @@ -79,19 +78,16 @@ resources: models: session: Session message: Message + toolCall: ToolCall + toolPartialCall: ToolPartialCall + toolResult: ToolResult textPart: TextPart + reasoningPart: ReasoningPart + toolInvocationPart: ToolInvocationPart + sourceUrlPart: SourceUrlPart filePart: FilePart - toolPart: ToolPart stepStartPart: StepStartPart - assistantMessage: AssistantMessage - assistantMessagePart: AssistantMessagePart - userMessage: UserMessage - userMessagePart: UserMessagePart - toolStatePending: ToolStatePending - toolStateRunning: ToolStateRunning - toolStateCompleted: ToolStateCompleted - toolStateError: ToolStateError - + messagePart: MessagePart methods: list: get /session create: post /session