Просмотр исходного кода

v2 message format and upgrade to ai sdk v5 (#743)

Co-authored-by: GitHub Action <[email protected]>
Co-authored-by: Liang-Shih Lin <[email protected]>
Co-authored-by: Dominik Engelhardt <[email protected]>
Co-authored-by: Jay V <[email protected]>
Co-authored-by: adamdottv <[email protected]>
Dax 9 месяцев назад
Родитель
Сommit
f884766445
100 измененных файлов с 2294 добавлено и 4789 удалено
  1. 1 1
      .github/workflows/stats.yml
  2. 28 30
      bun.lock
  3. 4 6
      package.json
  4. 3 8
      packages/function/src/api.ts
  5. 10 31
      packages/opencode/script/publish.ts
  6. 3 12
      packages/opencode/src/app/app.ts
  7. 12 21
      packages/opencode/src/auth/anthropic.ts
  8. 1 3
      packages/opencode/src/auth/copilot.ts
  9. 1 4
      packages/opencode/src/auth/github-copilot.ts
  10. 13 8
      packages/opencode/src/bun/index.ts
  11. 2 8
      packages/opencode/src/bus/index.ts
  12. 1 4
      packages/opencode/src/cli/bootstrap.ts
  13. 6 22
      packages/opencode/src/cli/cmd/auth.ts
  14. 1 2
      packages/opencode/src/cli/cmd/debug/file.ts
  15. 1 3
      packages/opencode/src/cli/cmd/debug/index.ts
  16. 3 6
      packages/opencode/src/cli/cmd/debug/lsp.ts
  17. 1 6
      packages/opencode/src/cli/cmd/debug/ripgrep.ts
  18. 1 5
      packages/opencode/src/cli/cmd/debug/snapshot.ts
  19. 1 4
      packages/opencode/src/cli/cmd/generate.ts
  20. 8 28
      packages/opencode/src/cli/cmd/run.ts
  21. 1 3
      packages/opencode/src/cli/cmd/serve.ts
  22. 1 3
      packages/opencode/src/cli/cmd/tui.ts
  23. 3 8
      packages/opencode/src/cli/cmd/upgrade.ts
  24. 2 5
      packages/opencode/src/cli/error.ts
  25. 24 98
      packages/opencode/src/config/config.ts
  26. 1 3
      packages/opencode/src/config/hooks.ts
  27. 9 18
      packages/opencode/src/file/fzf.ts
  28. 5 23
      packages/opencode/src/file/index.ts
  29. 12 36
      packages/opencode/src/file/ripgrep.ts
  30. 1 4
      packages/opencode/src/file/time.ts
  31. 12 15
      packages/opencode/src/file/watch.ts
  32. 1 15
      packages/opencode/src/format/formatter.ts
  33. 4 17
      packages/opencode/src/id/id.ts
  34. 2 8
      packages/opencode/src/index.ts
  35. 1 2
      packages/opencode/src/installation/index.ts
  36. 4 15
      packages/opencode/src/lsp/client.ts
  37. 4 14
      packages/opencode/src/lsp/index.ts
  38. 15 36
      packages/opencode/src/lsp/server.ts
  39. 24 60
      packages/opencode/src/provider/provider.ts
  40. 6 10
      packages/opencode/src/provider/transform.ts
  41. 9 17
      packages/opencode/src/server/server.ts
  42. 277 421
      packages/opencode/src/session/index.ts
  43. 398 0
      packages/opencode/src/session/message-v2.ts
  44. 5 42
      packages/opencode/src/session/message.ts
  45. 1 3
      packages/opencode/src/share/share.ts
  46. 1 3
      packages/opencode/src/snapshot/index.ts
  47. 46 12
      packages/opencode/src/storage/storage.ts
  48. 3 15
      packages/opencode/src/tool/bash.ts
  49. 14 53
      packages/opencode/src/tool/edit.ts
  50. 3 7
      packages/opencode/src/tool/glob.ts
  51. 9 21
      packages/opencode/src/tool/grep.ts
  52. 4 13
      packages/opencode/src/tool/ls.ts
  53. 3 7
      packages/opencode/src/tool/lsp-diagnostics.ts
  54. 2 9
      packages/opencode/src/tool/lsp-hover.ts
  55. 2 4
      packages/opencode/src/tool/multiedit.ts
  56. 10 37
      packages/opencode/src/tool/patch.ts
  57. 9 27
      packages/opencode/src/tool/read.ts
  58. 14 19
      packages/opencode/src/tool/task.ts
  59. 4 8
      packages/opencode/src/tool/todo.ts
  60. 6 10
      packages/opencode/src/tool/tool.ts
  61. 17 41
      packages/opencode/src/tool/webfetch.ts
  62. 4 12
      packages/opencode/src/tool/write.ts
  63. 1 4
      packages/opencode/src/util/error.ts
  64. 3 12
      packages/opencode/src/util/log.ts
  65. 20 91
      packages/opencode/test/tool/edit.test.ts
  66. 1 4
      packages/opencode/test/tool/tool.test.ts
  67. 1 1
      packages/tui/go.mod
  68. 26 25
      packages/tui/internal/app/app.go
  69. 4 4
      packages/tui/internal/components/chat/editor.go
  70. 152 172
      packages/tui/internal/components/chat/message.go
  71. 33 33
      packages/tui/internal/components/chat/messages.go
  72. 14 11
      packages/tui/internal/components/status/status.go
  73. 26 13
      packages/tui/internal/tui/tui.go
  74. 7 7
      packages/tui/sdk/.github/workflows/ci.yml
  75. 1 1
      packages/tui/sdk/.release-please-manifest.json
  76. 3 3
      packages/tui/sdk/.stats.yml
  77. 12 13
      packages/tui/sdk/CHANGELOG.md
  78. 11 16
      packages/tui/sdk/api.md
  79. 60 60
      packages/tui/sdk/event.go
  80. 2 5
      packages/tui/sdk/release-please-config.json
  81. 293 454
      packages/tui/sdk/session.go
  82. 1 1
      packages/tui/sdk/session_test.go
  83. 2 2
      packages/tui/sdk/shared/shared.go
  84. 5 5
      packages/web/astro.config.mjs
  85. 1 0
      packages/web/package.json
  86. 1 8
      packages/web/public/theme.json
  87. 2 6
      packages/web/src/components/CodeBlock.tsx
  88. 0 39
      packages/web/src/components/MarkdownView.tsx
  89. 58 1590
      packages/web/src/components/Share.tsx
  90. 0 1
      packages/web/src/components/codeblock.module.css
  91. 0 121
      packages/web/src/components/diffview.module.css
  92. 6 1
      packages/web/src/components/icons/custom.tsx
  93. 88 453
      packages/web/src/components/icons/index.tsx
  94. 0 106
      packages/web/src/components/markdownview.module.css
  95. 101 83
      packages/web/src/components/share.module.css
  96. 60 0
      packages/web/src/components/share/common.tsx
  97. 25 0
      packages/web/src/components/share/content-code.module.css
  98. 32 0
      packages/web/src/components/share/content-code.tsx
  99. 125 0
      packages/web/src/components/share/content-diff.module.css
  100. 43 58
      packages/web/src/components/share/content-diff.tsx

+ 1 - 1
.github/workflows/stats.yml

@@ -28,5 +28,5 @@ jobs:
           git config --local user.email "[email protected]"
           git config --local user.email "[email protected]"
           git config --local user.name "GitHub Action"
           git config --local user.name "GitHub Action"
           git add STATS.md
           git add STATS.md
-          git diff --staged --quiet || git commit -m "Update download stats $(date -I)"
+          git diff --staged --quiet || git commit -m "ignore: update download stats $(date -I)"
           git push
           git push

+ 28 - 30
bun.lock

@@ -96,30 +96,22 @@
     "sharp",
     "sharp",
     "esbuild",
     "esbuild",
   ],
   ],
-  "patchedDependencies": {
-    "[email protected]": "patches/[email protected]",
-  },
-  "overrides": {
-    "zod": "3.24.2",
-  },
   "catalog": {
   "catalog": {
     "@types/node": "22.13.9",
     "@types/node": "22.13.9",
-    "ai": "4.3.16",
+    "ai": "5.0.0-beta.7",
     "typescript": "5.8.2",
     "typescript": "5.8.2",
-    "zod": "3.24.2",
+    "zod": "3.25.49",
   },
   },
   "packages": {
   "packages": {
     "@ai-sdk/amazon-bedrock": ["@ai-sdk/[email protected]", "", { "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/amazon-bedrock": ["@ai-sdk/[email protected]", "", { "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/[email protected]", "", { "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/anthropic": ["@ai-sdk/[email protected]", "", { "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/provider": ["@ai-sdk/[email protected]", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
-
-    "@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "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/gateway": ["@ai-sdk/[email protected]", "", { "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/react": ["@ai-sdk/[email protected]", "", { "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/provider": ["@ai-sdk/[email protected]", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Z8SPncMtS3RsoXITmT7NVwrAq6M44dmw0DoUOYJqNNtCu8iMWuxB8Nxsoqpa0uEEy9R1V1ZThJAXTYgjTUxl3w=="],
 
 
-    "@ai-sdk/ui-utils": ["@ai-sdk/[email protected]", "", { "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=="],
+    "@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "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=="],
 
 
     "@ampproject/remapping": ["@ampproject/[email protected]", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
     "@ampproject/remapping": ["@ampproject/[email protected]", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
 
 
@@ -467,8 +459,6 @@
 
 
     "@types/debug": ["@types/[email protected]", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
     "@types/debug": ["@types/[email protected]", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
 
 
-    "@types/diff-match-patch": ["@types/[email protected]", "", {}, "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="],
-
     "@types/estree": ["@types/[email protected]", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
     "@types/estree": ["@types/[email protected]", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
 
 
     "@types/estree-jsx": ["@types/[email protected]", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
     "@types/estree-jsx": ["@types/[email protected]", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
@@ -515,7 +505,7 @@
 
 
     "acorn-walk": ["[email protected]", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="],
     "acorn-walk": ["[email protected]", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="],
 
 
-    "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=="],
+    "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=="],
 
 
     "ansi-align": ["[email protected]", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
     "ansi-align": ["[email protected]", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
 
 
@@ -733,8 +723,6 @@
 
 
     "diff": ["[email protected]", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
     "diff": ["[email protected]", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
 
 
-    "diff-match-patch": ["[email protected]", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="],
-
     "diff3": ["[email protected]", "", {}, "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g=="],
     "diff3": ["[email protected]", "", {}, "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g=="],
 
 
     "direction": ["[email protected]", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
     "direction": ["[email protected]", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
@@ -803,7 +791,7 @@
 
 
     "eventsource": ["[email protected]", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
     "eventsource": ["[email protected]", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
 
 
-    "eventsource-parser": ["[email protected].2", "", {}, "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA=="],
+    "eventsource-parser": ["[email protected].3", "", {}, "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA=="],
 
 
     "exit-hook": ["[email protected]", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
     "exit-hook": ["[email protected]", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
 
 
@@ -1025,8 +1013,6 @@
 
 
     "json5": ["[email protected]", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
     "json5": ["[email protected]", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
 
 
-    "jsondiffpatch": ["[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
     "kleur": ["[email protected]", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
 
 
     "klona": ["[email protected]", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
     "klona": ["[email protected]", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
@@ -1343,8 +1329,6 @@
 
 
     "rc": ["[email protected]", "", { "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=="],
     "rc": ["[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
-
     "readable-stream": ["[email protected]", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
     "readable-stream": ["[email protected]", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
 
 
     "readdirp": ["[email protected]", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
     "readdirp": ["[email protected]", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
@@ -1529,8 +1513,6 @@
 
 
     "supports-color": ["[email protected]", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
     "supports-color": ["[email protected]", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
 
 
-    "swr": ["[email protected]", "", { "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": ["[email protected]", "", { "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-fs": ["[email protected]", "", { "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": ["[email protected]", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
     "tar-stream": ["[email protected]", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
@@ -1539,8 +1521,6 @@
 
 
     "thread-stream": ["[email protected]", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="],
     "thread-stream": ["[email protected]", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="],
 
 
-    "throttleit": ["[email protected]", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="],
-
     "tiny-inflate": ["[email protected]", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
     "tiny-inflate": ["[email protected]", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
 
 
     "tinyexec": ["[email protected]", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
     "tinyexec": ["[email protected]", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
@@ -1625,8 +1605,6 @@
 
 
     "url": ["[email protected]", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="],
     "url": ["[email protected]", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="],
 
 
-    "use-sync-external-store": ["[email protected]", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
-
     "util": ["[email protected]", "", { "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": ["[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
     "util-deprecate": ["[email protected]", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
@@ -1703,7 +1681,7 @@
 
 
     "youch": ["[email protected]", "", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="],
     "youch": ["[email protected]", "", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="],
 
 
-    "zod": ["[email protected]4.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
+    "zod": ["[email protected]5.49", "", {}, "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q=="],
 
 
     "zod-openapi": ["[email protected]", "", { "peerDependencies": { "zod": "^3.21.4" } }, "sha512-tsrQpbpqFCXqVXUzi3TPwFhuMtLN3oNZobOtYnK6/5VkXsNdnIgyNr4r8no4wmYluaxzN3F7iS+8xCW8BmMQ8g=="],
     "zod-openapi": ["[email protected]", "", { "peerDependencies": { "zod": "^3.21.4" } }, "sha512-tsrQpbpqFCXqVXUzi3TPwFhuMtLN3oNZobOtYnK6/5VkXsNdnIgyNr4r8no4wmYluaxzN3F7iS+8xCW8BmMQ8g=="],
 
 
@@ -1715,12 +1693,22 @@
 
 
     "zwitch": ["[email protected]", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
     "zwitch": ["[email protected]", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
 
 
+    "@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/[email protected]", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
+
+    "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
     "@ai-sdk/amazon-bedrock/aws4fetch": ["[email protected]", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
 
 
+    "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/[email protected]", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
+
+    "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "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/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
     "@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
 
 
     "@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/[email protected]", "", { "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/mdx/@astrojs/markdown-remark": ["@astrojs/[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
+
     "@aws-crypto/crc32/tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
     "@aws-crypto/crc32/tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 
 
     "@aws-crypto/util/@smithy/util-utf8": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
     "@aws-crypto/util/@smithy/util-utf8": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
@@ -1739,6 +1727,8 @@
 
 
     "@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
     "@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
 
 
+    "@modelcontextprotocol/sdk/zod": ["[email protected]", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
+
     "@openauthjs/openauth/@standard-schema/spec": ["@standard-schema/[email protected]", "", {}, "sha512-0ifF3BjA1E8SY9C+nUew8RefNOIq0cDlYALPty4rhUm8Rrl6tCM8hBT4bhGhx7I7iXD0uAgt50lgo8dD73ACMw=="],
     "@openauthjs/openauth/@standard-schema/spec": ["@standard-schema/[email protected]", "", {}, "sha512-0ifF3BjA1E8SY9C+nUew8RefNOIq0cDlYALPty4rhUm8Rrl6tCM8hBT4bhGhx7I7iXD0uAgt50lgo8dD73ACMw=="],
 
 
     "@openauthjs/openauth/aws4fetch": ["[email protected]", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
     "@openauthjs/openauth/aws4fetch": ["[email protected]", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
@@ -1775,10 +1765,14 @@
 
 
     "astro/sharp": ["[email protected]", "", { "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/sharp": ["[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
+
     "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
     "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
 
 
     "bl/buffer": ["[email protected]", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
     "bl/buffer": ["[email protected]", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
 
 
+    "eventsource/eventsource-parser": ["[email protected]", "", {}, "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA=="],
+
     "express/cookie": ["[email protected]", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
     "express/cookie": ["[email protected]", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
 
 
     "get-source/source-map": ["[email protected]", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
     "get-source/source-map": ["[email protected]", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
@@ -1791,8 +1785,12 @@
 
 
     "miniflare/sharp": ["[email protected]", "", { "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/sharp": ["[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
+
     "opencontrol/hono": ["[email protected]", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="],
     "opencontrol/hono": ["[email protected]", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="],
 
 
+    "opencontrol/zod": ["[email protected]", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
+
     "opencontrol/zod-to-json-schema": ["[email protected]", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="],
     "opencontrol/zod-to-json-schema": ["[email protected]", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="],
 
 
     "openid-client/jose": ["[email protected]", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
     "openid-client/jose": ["[email protected]", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],

+ 4 - 6
package.json

@@ -17,8 +17,8 @@
     "catalog": {
     "catalog": {
       "typescript": "5.8.2",
       "typescript": "5.8.2",
       "@types/node": "22.13.9",
       "@types/node": "22.13.9",
-      "zod": "3.24.2",
-      "ai": "4.3.16"
+      "zod": "3.25.49",
+      "ai": "5.0.0-beta.7"
     }
     }
   },
   },
   "devDependencies": {
   "devDependencies": {
@@ -31,10 +31,8 @@
   },
   },
   "license": "MIT",
   "license": "MIT",
   "prettier": {
   "prettier": {
-    "semi": false
-  },
-  "overrides": {
-    "zod": "3.24.2"
+    "semi": false,
+    "printWidth": 120
   },
   },
   "trustedDependencies": [
   "trustedDependencies": [
     "esbuild",
     "esbuild",

+ 3 - 8
packages/function/src/api.ts

@@ -38,10 +38,7 @@ export class SyncServer extends DurableObject<Env> {
 
 
   async publish(key: string, content: any) {
   async publish(key: string, content: any) {
     const sessionID = await this.getSessionID()
     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 })
       return new Response("Error: Invalid key", { status: 400 })
 
 
     // store message
     // store message
@@ -184,8 +181,7 @@ export default {
       }
       }
       const id = url.searchParams.get("id")
       const id = url.searchParams.get("id")
       console.log("share_poll", 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))
       const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
       return stub.fetch(request)
       return stub.fetch(request)
     }
     }
@@ -193,8 +189,7 @@ export default {
     if (request.method === "GET" && method === "share_data") {
     if (request.method === "GET" && method === "share_data") {
       const id = url.searchParams.get("id")
       const id = url.searchParams.get("id")
       console.log("share_data", 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 stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
       const data = await stub.getData()
       const data = await stub.getData()
 
 

+ 10 - 31
packages/opencode/script/publish.ts

@@ -57,8 +57,7 @@ for (const [os, arch] of targets) {
       2,
       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
   optionalDependencies[name] = version
 }
 }
 
 
@@ -82,8 +81,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
     2,
     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) {
 if (!snapshot) {
   // Github Release
   // Github Release
@@ -91,15 +89,11 @@ if (!snapshot) {
     await $`cd dist/${key}/bin && zip -r ../../${key}.zip *`
     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((res) => res.json())
     .then((data) => data.tag_name)
     .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((res) => res.json())
     .then((data) => data.commits || [])
     .then((data) => data.commits || [])
 
 
@@ -117,26 +111,13 @@ if (!snapshot) {
     })
     })
     .join("\n")
     .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
   // 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
   // AUR package
   const pkgbuild = [
   const pkgbuild = [
@@ -170,9 +151,7 @@ if (!snapshot) {
   for (const pkg of ["opencode", "opencode-bin"]) {
   for (const pkg of ["opencode", "opencode-bin"]) {
     await $`rm -rf ./dist/aur-${pkg}`
     await $`rm -rf ./dist/aur-${pkg}`
     await $`git clone ssh://[email protected]/${pkg}.git ./dist/aur-${pkg}`
     await $`git clone ssh://[email protected]/${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} && makepkg --printsrcinfo > .SRCINFO`
     await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
     await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
     await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`
     await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`

+ 3 - 12
packages/opencode/src/app/app.ts

@@ -45,23 +45,14 @@ export namespace App {
   }
   }
 
 
   export const provideExisting = ctx.provide
   export const provideExisting = ctx.provide
-  export async function provide<T>(
-    input: Input,
-    cb: (app: App.Info) => Promise<T>,
-  ) {
+  export async function provide<T>(input: Input, cb: (app: App.Info) => Promise<T>) {
     log.info("creating", {
     log.info("creating", {
       cwd: input.cwd,
       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 })
     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 stateFile = Bun.file(path.join(data, APP_JSON))
     const state = (await stateFile.json().catch(() => ({}))) as {
     const state = (await stateFile.json().catch(() => ({}))) as {
       initialized: number
       initialized: number

+ 12 - 21
packages/opencode/src/auth/anthropic.ts

@@ -10,14 +10,8 @@ export namespace AuthAnthropic {
     url.searchParams.set("code", "true")
     url.searchParams.set("code", "true")
     url.searchParams.set("client_id", CLIENT_ID)
     url.searchParams.set("client_id", CLIENT_ID)
     url.searchParams.set("response_type", "code")
     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", pkce.challenge)
     url.searchParams.set("code_challenge_method", "S256")
     url.searchParams.set("code_challenge_method", "S256")
     url.searchParams.set("state", pkce.verifier)
     url.searchParams.set("state", pkce.verifier)
@@ -57,20 +51,17 @@ export namespace AuthAnthropic {
     const info = await Auth.get("anthropic")
     const info = await Auth.get("anthropic")
     if (!info || info.type !== "oauth") return
     if (!info || info.type !== "oauth") return
     if (info.access && info.expires > Date.now()) return info.access
     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",
-        },
-        body: JSON.stringify({
-          grant_type: "refresh_token",
-          refresh_token: info.refresh,
-          client_id: CLIENT_ID,
-        }),
+    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,
+      }),
+    })
     if (!response.ok) return
     if (!response.ok) return
     const json = await response.json()
     const json = await response.json()
     await Auth.set("anthropic", {
     await Auth.set("anthropic", {

+ 1 - 3
packages/opencode/src/auth/copilot.ts

@@ -4,9 +4,7 @@ import path from "path"
 
 
 export const AuthCopilot = lazy(async () => {
 export const AuthCopilot = lazy(async () => {
   const file = Bun.file(path.join(Global.Path.state, "plugin", "copilot.ts"))
   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))
     .then((x) => Bun.write(file, x))
     .catch(() => {})
     .catch(() => {})
 
 

+ 1 - 4
packages/opencode/src/auth/github-copilot.ts

@@ -122,10 +122,7 @@ export namespace AuthGithubCopilot {
     return tokenData.token
     return tokenData.token
   }
   }
 
 
-  export const DeviceCodeError = NamedError.create(
-    "DeviceCodeError",
-    z.object({}),
-  )
+  export const DeviceCodeError = NamedError.create("DeviceCodeError", z.object({}))
 
 
   export const TokenExchangeError = NamedError.create(
   export const TokenExchangeError = NamedError.create(
     "TokenExchangeError",
     "TokenExchangeError",

+ 13 - 8
packages/opencode/src/bun/index.ts

@@ -8,10 +8,7 @@ import { readableStreamToText } from "bun"
 export namespace BunProc {
 export namespace BunProc {
   const log = Log.create({ service: "bun" })
   const log = Log.create({ service: "bun" })
 
 
-  export async function run(
-    cmd: string[],
-    options?: Bun.SpawnOptions.OptionsObject<any, any, any>,
-  ) {
+  export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
     log.info("running", {
     log.info("running", {
       cmd: [which(), ...cmd],
       cmd: [which(), ...cmd],
       ...options,
       ...options,
@@ -26,9 +23,17 @@ export namespace BunProc {
         BUN_BE_BUN: "1",
         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", {
     log.info("done", {
       code,
       code,
       stdout,
       stdout,
@@ -61,7 +66,7 @@ export namespace BunProc {
     if (parsed.dependencies[pkg] === version) return mod
     if (parsed.dependencies[pkg] === version) return mod
     parsed.dependencies[pkg] = version
     parsed.dependencies[pkg] = version
     await Bun.write(pkgjson, JSON.stringify(parsed, null, 2))
     await Bun.write(pkgjson, JSON.stringify(parsed, null, 2))
-    await BunProc.run(["install", "--registry=https://registry.npmjs.org"], {
+    await BunProc.run(["install", "--cwd", Global.Path.cache, "--registry=https://registry.npmjs.org"], {
       cwd: Global.Path.cache,
       cwd: Global.Path.cache,
     }).catch((e) => {
     }).catch((e) => {
       throw new InstallFailedError(
       throw new InstallFailedError(

+ 2 - 8
packages/opencode/src/bus/index.ts

@@ -18,10 +18,7 @@ export namespace Bus {
 
 
   const registry = new Map<string, EventDefinition>()
   const registry = new Map<string, EventDefinition>()
 
 
-  export function event<Type extends string, Properties extends ZodType>(
-    type: Type,
-    properties: Properties,
-  ) {
+  export function event<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
     const result = {
     const result = {
       type,
       type,
       properties,
       properties,
@@ -72,10 +69,7 @@ export namespace Bus {
 
 
   export function subscribe<Definition extends EventDefinition>(
   export function subscribe<Definition extends EventDefinition>(
     def: Definition,
     def: Definition,
-    callback: (event: {
-      type: Definition["type"]
-      properties: z.infer<Definition["properties"]>
-    }) => void,
+    callback: (event: { type: Definition["type"]; properties: z.infer<Definition["properties"]> }) => void,
   ) {
   ) {
     return raw(def.type, callback)
     return raw(def.type, callback)
   }
   }

+ 1 - 4
packages/opencode/src/cli/bootstrap.ts

@@ -5,10 +5,7 @@ import { Format } from "../format"
 import { LSP } from "../lsp"
 import { LSP } from "../lsp"
 import { Share } from "../share/share"
 import { Share } from "../share/share"
 
 
-export async function bootstrap<T>(
-  input: App.Input,
-  cb: (app: App.Info) => Promise<T>,
-) {
+export async function bootstrap<T>(input: App.Input, cb: (app: App.Info) => Promise<T>) {
   return App.provide(input, async (app) => {
   return App.provide(input, async (app) => {
     Share.init()
     Share.init()
     Format.init()
     Format.init()

+ 6 - 22
packages/opencode/src/cli/cmd/auth.ts

@@ -15,11 +15,7 @@ export const AuthCommand = cmd({
   command: "auth",
   command: "auth",
   describe: "manage credentials",
   describe: "manage credentials",
   builder: (yargs) =>
   builder: (yargs) =>
-    yargs
-      .command(AuthLoginCommand)
-      .command(AuthLogoutCommand)
-      .command(AuthListCommand)
-      .demandCommand(),
+    yargs.command(AuthLoginCommand).command(AuthLogoutCommand).command(AuthListCommand).demandCommand(),
   async handler() {},
   async handler() {},
 })
 })
 
 
@@ -31,9 +27,7 @@ export const AuthListCommand = cmd({
     UI.empty()
     UI.empty()
     const authPath = path.join(Global.Path.data, "auth.json")
     const authPath = path.join(Global.Path.data, "auth.json")
     const homedir = os.homedir()
     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}`)
     prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
     const results = await Auth.all().then((x) => Object.entries(x))
     const results = await Auth.all().then((x) => Object.entries(x))
     const database = await ModelsDev.get()
     const database = await ModelsDev.get()
@@ -114,8 +108,7 @@ export const AuthLoginCommand = cmd({
     if (provider === "other") {
     if (provider === "other") {
       provider = await prompts.text({
       provider = await prompts.text({
         message: "Enter provider id",
         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()
       if (prompts.isCancel(provider)) throw new UI.CancelledError()
       provider = provider.replace(/^@ai-sdk\//, "")
       provider = provider.replace(/^@ai-sdk\//, "")
@@ -186,17 +179,13 @@ export const AuthLoginCommand = cmd({
       await new Promise((resolve) => setTimeout(resolve, 10))
       await new Promise((resolve) => setTimeout(resolve, 10))
       const deviceInfo = await copilot.authorize()
       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()
       const spinner = prompts.spinner()
       spinner.start("Waiting for authorization...")
       spinner.start("Waiting for authorization...")
 
 
       while (true) {
       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)
         const response = await copilot.poll(deviceInfo.device)
         if (response.status === "pending") continue
         if (response.status === "pending") continue
         if (response.status === "success") {
         if (response.status === "success") {
@@ -248,12 +237,7 @@ export const AuthLogoutCommand = cmd({
     const providerID = await prompts.select({
     const providerID = await prompts.select({
       message: "Select provider",
       message: "Select provider",
       options: credentials.map(([key, value]) => ({
       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,
         value: key,
       })),
       })),
     })
     })

+ 1 - 2
packages/opencode/src/cli/cmd/debug/file.ts

@@ -31,7 +31,6 @@ const FileStatusCommand = cmd({
 
 
 export const FileCommand = cmd({
 export const FileCommand = cmd({
   command: "file",
   command: "file",
-  builder: (yargs) =>
-    yargs.command(FileReadCommand).command(FileStatusCommand).demandCommand(),
+  builder: (yargs) => yargs.command(FileReadCommand).command(FileStatusCommand).demandCommand(),
   async handler() {},
   async handler() {},
 })
 })

+ 1 - 3
packages/opencode/src/cli/cmd/debug/index.ts

@@ -17,9 +17,7 @@ export const DebugCommand = cmd({
         command: "wait",
         command: "wait",
         async handler() {
         async handler() {
           await bootstrap({ cwd: process.cwd() }, async () => {
           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))
           })
           })
         },
         },
       })
       })

+ 3 - 6
packages/opencode/src/cli/cmd/debug/lsp.ts

@@ -5,15 +5,13 @@ import { Log } from "../../../util/log"
 
 
 export const LSPCommand = cmd({
 export const LSPCommand = cmd({
   command: "lsp",
   command: "lsp",
-  builder: (yargs) =>
-    yargs.command(DiagnosticsCommand).command(SymbolsCommand).demandCommand(),
+  builder: (yargs) => yargs.command(DiagnosticsCommand).command(SymbolsCommand).demandCommand(),
   async handler() {},
   async handler() {},
 })
 })
 
 
 const DiagnosticsCommand = cmd({
 const DiagnosticsCommand = cmd({
   command: "diagnostics <file>",
   command: "diagnostics <file>",
-  builder: (yargs) =>
-    yargs.positional("file", { type: "string", demandOption: true }),
+  builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
   async handler(args) {
   async handler(args) {
     await bootstrap({ cwd: process.cwd() }, async () => {
     await bootstrap({ cwd: process.cwd() }, async () => {
       await LSP.touchFile(args.file, true)
       await LSP.touchFile(args.file, true)
@@ -24,8 +22,7 @@ const DiagnosticsCommand = cmd({
 
 
 export const SymbolsCommand = cmd({
 export const SymbolsCommand = cmd({
   command: "symbols <query>",
   command: "symbols <query>",
-  builder: (yargs) =>
-    yargs.positional("query", { type: "string", demandOption: true }),
+  builder: (yargs) => yargs.positional("query", { type: "string", demandOption: true }),
   async handler(args) {
   async handler(args) {
     await bootstrap({ cwd: process.cwd() }, async () => {
     await bootstrap({ cwd: process.cwd() }, async () => {
       await LSP.touchFile("./src/index.ts", true)
       await LSP.touchFile("./src/index.ts", true)

+ 1 - 6
packages/opencode/src/cli/cmd/debug/ripgrep.ts

@@ -5,12 +5,7 @@ import { cmd } from "../cmd"
 
 
 export const RipgrepCommand = cmd({
 export const RipgrepCommand = cmd({
   command: "rg",
   command: "rg",
-  builder: (yargs) =>
-    yargs
-      .command(TreeCommand)
-      .command(FilesCommand)
-      .command(SearchCommand)
-      .demandCommand(),
+  builder: (yargs) => yargs.command(TreeCommand).command(FilesCommand).command(SearchCommand).demandCommand(),
   async handler() {},
   async handler() {},
 })
 })
 
 

+ 1 - 5
packages/opencode/src/cli/cmd/debug/snapshot.ts

@@ -4,11 +4,7 @@ import { cmd } from "../cmd"
 
 
 export const SnapshotCommand = cmd({
 export const SnapshotCommand = cmd({
   command: "snapshot",
   command: "snapshot",
-  builder: (yargs) =>
-    yargs
-      .command(SnapshotCreateCommand)
-      .command(SnapshotRestoreCommand)
-      .demandCommand(),
+  builder: (yargs) => yargs.command(SnapshotCreateCommand).command(SnapshotRestoreCommand).demandCommand(),
   async handler() {},
   async handler() {},
 })
 })
 
 

+ 1 - 4
packages/opencode/src/cli/cmd/generate.ts

@@ -10,9 +10,6 @@ export const GenerateCommand = {
     const dir = "gen"
     const dir = "gen"
     await fs.rmdir(dir, { recursive: true }).catch(() => {})
     await fs.rmdir(dir, { recursive: true }).catch(() => {})
     await fs.mkdir(dir, { recursive: true })
     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
 } satisfies CommandModule

+ 8 - 28
packages/opencode/src/cli/cmd/run.ts

@@ -2,12 +2,12 @@ import type { Argv } from "yargs"
 import { Bus } from "../../bus"
 import { Bus } from "../../bus"
 import { Provider } from "../../provider/provider"
 import { Provider } from "../../provider/provider"
 import { Session } from "../../session"
 import { Session } from "../../session"
-import { Message } from "../../session/message"
 import { UI } from "../ui"
 import { UI } from "../ui"
 import { cmd } from "./cmd"
 import { cmd } from "./cmd"
 import { Flag } from "../../flag/flag"
 import { Flag } from "../../flag/flag"
 import { Config } from "../../config/config"
 import { Config } from "../../config/config"
 import { bootstrap } from "../bootstrap"
 import { bootstrap } from "../bootstrap"
+import { MessageV2 } from "../../session/message-v2"
 
 
 const TOOL: Record<string, [string, string]> = {
 const TOOL: Record<string, [string, string]> = {
   todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
   todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
@@ -84,21 +84,12 @@ export const RunCommand = cmd({
       const cfg = await Config.get()
       const cfg = await Config.get()
       if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) {
       if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) {
         await Session.share(session.id)
         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()
       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()
       UI.empty()
 
 
       function printEvent(color: string, type: string, title: string) {
       function printEvent(color: string, type: string, title: string) {
@@ -110,24 +101,13 @@ export const RunCommand = cmd({
         )
         )
       }
       }
 
 
-      Bus.subscribe(Message.Event.PartUpdated, async (evt) => {
+      Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
         if (evt.properties.sessionID !== session.id) return
         if (evt.properties.sessionID !== session.id) return
         const part = evt.properties.part
         const part = evt.properties.part
-        const message = await Session.getMessage(
-          evt.properties.sessionID,
-          evt.properties.messageID,
-        )
 
 
-        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 === "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 === "text") {
         if (part.type === "text") {

+ 1 - 3
packages/opencode/src/cli/cmd/serve.ts

@@ -38,9 +38,7 @@ export const ServeCommand = cmd({
         hostname,
         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(() => {})
       await new Promise(() => {})
 
 

+ 1 - 3
packages/opencode/src/cli/cmd/tui.ts

@@ -40,9 +40,7 @@ export const TuiCommand = cmd({
         })
         })
 
 
         let cmd = ["go", "run", "./main.go"]
         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) {
         if (Bun.embeddedFiles.length > 0) {
           const blob = Bun.embeddedFiles[0] as File
           const blob = Bun.embeddedFiles[0] as File
           let binaryName = blob.name
           let binaryName = blob.name

+ 3 - 8
packages/opencode/src/cli/cmd/upgrade.ts

@@ -27,9 +27,7 @@ export const UpgradeCommand = {
     const detectedMethod = await Installation.method()
     const detectedMethod = await Installation.method()
     const method = (args.method as Installation.Method) ?? detectedMethod
     const method = (args.method as Installation.Method) ?? detectedMethod
     if (method === "unknown") {
     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")
       prompts.outro("Done")
       return
       return
     }
     }
@@ -37,9 +35,7 @@ export const UpgradeCommand = {
     const target = args.target ?? (await Installation.latest())
     const target = args.target ?? (await Installation.latest())
 
 
     if (Installation.VERSION === target) {
     if (Installation.VERSION === target) {
-      prompts.log.warn(
-        `opencode upgrade skipped: ${target} is already installed`,
-      )
+      prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`)
       prompts.outro("Done")
       prompts.outro("Done")
       return
       return
     }
     }
@@ -50,8 +46,7 @@ export const UpgradeCommand = {
     const err = await Installation.upgrade(method, target).catch((err) => err)
     const err = await Installation.upgrade(method, target).catch((err) => err)
     if (err) {
     if (err) {
       spinner.stop("Upgrade failed")
       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)
       else if (err instanceof Error) prompts.log.error(err.message)
       prompts.outro("Done")
       prompts.outro("Done")
       return
       return

+ 2 - 5
packages/opencode/src/cli/error.ts

@@ -5,14 +5,11 @@ import { UI } from "./ui"
 export function FormatError(input: unknown) {
 export function FormatError(input: unknown) {
   if (MCP.Failed.isInstance(input))
   if (MCP.Failed.isInstance(input))
     return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`
     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))
   if (Config.InvalidError.isInstance(input))
     return [
     return [
       `Config file at ${input.data.path} is invalid`,
       `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")
     ].join("\n")
 
 
   if (UI.CancelledError.isInstance(input)) return ""
   if (UI.CancelledError.isInstance(input)) return ""

+ 24 - 98
packages/opencode/src/config/config.ts

@@ -29,18 +29,12 @@ export namespace Config {
   export const McpLocal = z
   export const McpLocal = z
     .object({
     .object({
       type: z.literal("local").describe("Type of MCP server connection"),
       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
       environment: z
         .record(z.string(), z.string())
         .record(z.string(), z.string())
         .optional()
         .optional()
         .describe("Environment variables to set when running the MCP server"),
         .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()
     .strict()
     .openapi({
     .openapi({
@@ -51,10 +45,7 @@ export namespace Config {
     .object({
     .object({
       type: z.literal("remote").describe("Type of MCP server connection"),
       type: z.literal("remote").describe("Type of MCP server connection"),
       url: z.string().describe("URL of the remote MCP server"),
       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()
     .strict()
     .openapi({
     .openapi({
@@ -66,67 +57,31 @@ export namespace Config {
 
 
   export const Keybinds = z
   export const Keybinds = z
     .object({
     .object({
-      leader: z
-        .string()
-        .optional()
-        .describe("Leader key for keybind combinations"),
+      leader: z.string().optional().describe("Leader key for keybind combinations"),
       help: z.string().optional().describe("Show help dialog"),
       help: z.string().optional().describe("Show help dialog"),
       editor_open: z.string().optional().describe("Open external editor"),
       editor_open: z.string().optional().describe("Open external editor"),
       session_new: z.string().optional().describe("Create a new session"),
       session_new: z.string().optional().describe("Create a new session"),
       session_list: z.string().optional().describe("List all sessions"),
       session_list: z.string().optional().describe("List all sessions"),
       session_share: z.string().optional().describe("Share current session"),
       session_share: z.string().optional().describe("Share 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"),
       tool_details: z.string().optional().describe("Show tool details"),
       model_list: z.string().optional().describe("List available models"),
       model_list: z.string().optional().describe("List available models"),
       theme_list: z.string().optional().describe("List available themes"),
       theme_list: z.string().optional().describe("List available themes"),
-      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_clear: z.string().optional().describe("Clear input field"),
       input_paste: z.string().optional().describe("Paste from clipboard"),
       input_paste: z.string().optional().describe("Paste from clipboard"),
       input_submit: z.string().optional().describe("Submit input"),
       input_submit: z.string().optional().describe("Submit input"),
       input_newline: z.string().optional().describe("Insert newline in input"),
       input_newline: z.string().optional().describe("Insert newline in input"),
-      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"),
+      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_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_last: z.string().optional().describe("Navigate to last message"),
       app_exit: z.string().optional().describe("Exit the application"),
       app_exit: z.string().optional().describe("Exit the application"),
     })
     })
@@ -136,33 +91,13 @@ export namespace Config {
     })
     })
   export const Info = z
   export const Info = z
     .object({
     .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"),
       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
       provider: z
         .record(
         .record(
           ModelsDev.Provider.partial().extend({
           ModelsDev.Provider.partial().extend({
@@ -172,14 +107,8 @@ export namespace Config {
         )
         )
         .optional()
         .optional()
         .describe("Custom provider configurations and model overrides"),
         .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
       experimental: z
         .object({
         .object({
           hook: z
           hook: z
@@ -227,10 +156,7 @@ export namespace Config {
         if (provider && model) result.model = `${provider}/${model}`
         if (provider && model) result.model = `${provider}/${model}`
         result["$schema"] = "https://opencode.ai/config.json"
         result["$schema"] = "https://opencode.ai/config.json"
         result = mergeDeep(result, rest)
         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"))
         await fs.unlink(path.join(Global.Path.config, "config"))
       })
       })
       .catch(() => {})
       .catch(() => {})

+ 1 - 3
packages/opencode/src/config/hooks.ts

@@ -22,9 +22,7 @@ export namespace ConfigHooks {
           command: item.command,
           command: item.command,
         })
         })
         Bun.spawn({
         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,
           env: item.environment,
           cwd: app.path.cwd,
           cwd: app.path.cwd,
           stdout: "ignore",
           stdout: "ignore",

+ 9 - 18
packages/opencode/src/file/fzf.ts

@@ -45,10 +45,7 @@ export namespace Fzf {
       log.info("found", { filepath })
       log.info("found", { filepath })
       return { 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)
     const file = Bun.file(filepath)
     if (!(await file.exists())) {
     if (!(await file.exists())) {
@@ -56,18 +53,15 @@ export namespace Fzf {
       const arch = archMap[process.arch as keyof typeof archMap] ?? "amd64"
       const arch = archMap[process.arch as keyof typeof archMap] ?? "amd64"
 
 
       const config = PLATFORM[process.platform as keyof typeof PLATFORM]
       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 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 filename = `fzf-${version}-${platformName}_${arch}.${config.extension}`
       const url = `https://github.com/junegunn/fzf/releases/download/v${version}/${filename}`
       const url = `https://github.com/junegunn/fzf/releases/download/v${version}/${filename}`
 
 
       const response = await fetch(url)
       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 buffer = await response.arrayBuffer()
       const archivePath = path.join(Global.Path.bin, filename)
       const archivePath = path.join(Global.Path.bin, filename)
@@ -86,14 +80,11 @@ export namespace Fzf {
           })
           })
       }
       }
       if (config.extension === "zip") {
       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
         await proc.exited
         if (proc.exitCode !== 0)
         if (proc.exitCode !== 0)
           throw new ExtractionFailedError({
           throw new ExtractionFailedError({

+ 5 - 23
packages/opencode/src/file/index.ts

@@ -24,11 +24,7 @@ export namespace File {
     const app = App.info()
     const app = App.info()
     if (!app.git) return []
     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 = []
     const changedFiles = []
 
 
@@ -45,19 +41,13 @@ 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()) {
     if (untrackedOutput.trim()) {
       const untrackedFiles = untrackedOutput.trim().split("\n")
       const untrackedFiles = untrackedOutput.trim().split("\n")
       for (const filepath of untrackedFiles) {
       for (const filepath of untrackedFiles) {
         try {
         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
           const lines = content.split("\n").length
           changedFiles.push({
           changedFiles.push({
             file: filepath,
             file: filepath,
@@ -72,11 +62,7 @@ export namespace File {
     }
     }
 
 
     // Get deleted files
     // 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()) {
     if (deletedOutput.trim()) {
       const deletedFiles = deletedOutput.trim().split("\n")
       const deletedFiles = deletedOutput.trim().split("\n")
@@ -112,11 +98,7 @@ export namespace File {
         filepath: rel,
         filepath: rel,
       })
       })
       if (diff !== "unmodified") {
       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", {
         const patch = createPatch(file, original, content, "old", "new", {
           context: Infinity,
           context: Infinity,
         })
         })

+ 12 - 36
packages/opencode/src/file/ripgrep.ts

@@ -122,15 +122,11 @@ export namespace Ripgrep {
   const state = lazy(async () => {
   const state = lazy(async () => {
     let filepath = Bun.which("rg")
     let filepath = Bun.which("rg")
     if (filepath) return { filepath }
     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)
     const file = Bun.file(filepath)
     if (!(await file.exists())) {
     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]
       const config = PLATFORM[platformKey]
       if (!config) throw new UnsupportedPlatformError({ platform: platformKey })
       if (!config) throw new UnsupportedPlatformError({ platform: platformKey })
 
 
@@ -139,8 +135,7 @@ export namespace Ripgrep {
       const url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}`
       const url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}`
 
 
       const response = await fetch(url)
       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 buffer = await response.arrayBuffer()
       const archivePath = path.join(Global.Path.bin, filename)
       const archivePath = path.join(Global.Path.bin, filename)
@@ -164,14 +159,11 @@ export namespace Ripgrep {
           })
           })
       }
       }
       if (config.extension === "zip") {
       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
         await proc.exited
         if (proc.exitCode !== 0)
         if (proc.exitCode !== 0)
           throw new ExtractionFailedError({
           throw new ExtractionFailedError({
@@ -193,17 +185,11 @@ export namespace Ripgrep {
     return filepath
     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 = [
     const commands = [
       `${await filepath()} --files --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}`)
     if (input.limit) commands.push(`head -n ${input.limit}`)
     const joined = commands.join(" | ")
     const joined = commands.join(" | ")
     const result = await $`${{ raw: joined }}`.cwd(input.cwd).nothrow().text()
     const result = await $`${{ raw: joined }}`.cwd(input.cwd).nothrow().text()
@@ -310,18 +296,8 @@ export namespace Ripgrep {
     return lines.join("\n")
     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) {
     if (input.glob) {
       for (const g of input.glob) {
       for (const g of input.glob) {

+ 1 - 4
packages/opencode/src/file/time.ts

@@ -27,10 +27,7 @@ export namespace FileTime {
 
 
   export async function assert(sessionID: string, filepath: string) {
   export async function assert(sessionID: string, filepath: string) {
     const time = get(sessionID, filepath)
     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()
     const stats = await Bun.file(filepath).stat()
     if (stats.mtime.getTime() > time.getTime()) {
     if (stats.mtime.getTime() > time.getTime()) {
       throw new Error(
       throw new Error(

+ 12 - 15
packages/opencode/src/file/watch.ts

@@ -21,23 +21,20 @@ export namespace FileWatcher {
     "file.watcher",
     "file.watcher",
     () => {
     () => {
       const app = App.use()
       const app = App.use()
+      if (!app.info.git) return {}
       try {
       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 }
         return { watcher }
       } catch {
       } catch {
         return {}
         return {}

+ 1 - 15
packages/opencode/src/format/formatter.ts

@@ -94,21 +94,7 @@ export const zig: Info = {
 export const clang: Info = {
 export const clang: Info = {
   name: "clang-format",
   name: "clang-format",
   command: ["clang-format", "-i", "$FILE"],
   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() {
   async enabled() {
     return Bun.which("clang-format") !== null
     return Bun.which("clang-format") !== null
   },
   },

+ 4 - 17
packages/opencode/src/id/id.ts

@@ -26,11 +26,7 @@ export namespace Identifier {
     return generateID(prefix, true, given)
     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) {
     if (!given) {
       return generateNewID(prefix, descending)
       return generateNewID(prefix, descending)
     }
     }
@@ -42,8 +38,7 @@ export namespace Identifier {
   }
   }
 
 
   function randomBase62(length: number): string {
   function randomBase62(length: number): string {
-    const chars =
-      "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+    const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
     let result = ""
     let result = ""
     const bytes = randomBytes(length)
     const bytes = randomBytes(length)
     for (let i = 0; i < length; i++) {
     for (let i = 0; i < length; i++) {
@@ -52,10 +47,7 @@ export namespace Identifier {
     return result
     return result
   }
   }
 
 
-  function generateNewID(
-    prefix: keyof typeof prefixes,
-    descending: boolean,
-  ): string {
+  function generateNewID(prefix: keyof typeof prefixes, descending: boolean): string {
     const currentTimestamp = Date.now()
     const currentTimestamp = Date.now()
 
 
     if (currentTimestamp !== lastTimestamp) {
     if (currentTimestamp !== lastTimestamp) {
@@ -73,11 +65,6 @@ export namespace Identifier {
       timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
       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)
   }
   }
 }
 }

+ 2 - 8
packages/opencode/src/index.ts

@@ -55,10 +55,7 @@ const cli = yargs(hideBin(process.argv))
   .command(ServeCommand)
   .command(ServeCommand)
   .command(ModelsCommand)
   .command(ModelsCommand)
   .fail((msg) => {
   .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")
       cli.showHelp("log")
     }
     }
   })
   })
@@ -97,10 +94,7 @@ try {
   Log.Default.error("fatal", data)
   Log.Default.error("fatal", data)
   const formatted = FormatError(e)
   const formatted = FormatError(e)
   if (formatted) UI.error(formatted)
   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
   process.exitCode = 1
 }
 }
 
 

+ 1 - 2
packages/opencode/src/installation/index.ts

@@ -135,8 +135,7 @@ 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() {
   export async function latest() {
     return fetch("https://api.github.com/repos/sst/opencode/releases/latest")
     return fetch("https://api.github.com/repos/sst/opencode/releases/latest")

+ 4 - 15
packages/opencode/src/lsp/client.ts

@@ -1,9 +1,5 @@
 import path from "path"
 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 type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
 import { App } from "../app/app"
 import { App } from "../app/app"
 import { Log } from "../util/log"
 import { Log } from "../util/log"
@@ -121,9 +117,7 @@ export namespace LSPClient {
       },
       },
       notify: {
       notify: {
         async open(input: { path: string }) {
         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 file = Bun.file(input.path)
           const text = await file.text()
           const text = await file.text()
           const version = files[input.path]
           const version = files[input.path]
@@ -155,18 +149,13 @@ export namespace LSPClient {
         return diagnostics
         return diagnostics
       },
       },
       async waitForDiagnostics(input: { path: string }) {
       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)
         log.info("waiting for diagnostics", input)
         let unsub: () => void
         let unsub: () => void
         return await withTimeout(
         return await withTimeout(
           new Promise<void>((resolve) => {
           new Promise<void>((resolve) => {
             unsub = Bus.subscribe(Event.Diagnostics, (event) => {
             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)
                 log.info("got diagnostics", input)
                 unsub?.()
                 unsub?.()
                 resolve()
                 resolve()

+ 4 - 14
packages/opencode/src/lsp/index.ts

@@ -46,9 +46,7 @@ export namespace LSP {
           if (!file) continue
           if (!file) continue
           const handle = await server.spawn(App.info())
           const handle = await server.spawn(App.info())
           if (!handle) break
           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
           if (!client) break
           clients.set(server.id, client)
           clients.set(server.id, client)
           break
           break
@@ -77,9 +75,7 @@ export namespace LSP {
       .map((x) => x.id)
       .map((x) => x.id)
     await run(async (client) => {
     await run(async (client) => {
       if (!matches.includes(client.serverID)) return
       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 })
       await client.notify.open({ path: input })
       return wait
       return wait
     })
     })
@@ -97,11 +93,7 @@ export namespace LSP {
     return results
     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 run((client) => {
       return client.connection.sendRequest("textDocument/hover", {
       return client.connection.sendRequest("textDocument/hover", {
         textDocument: {
         textDocument: {
@@ -123,9 +115,7 @@ export namespace LSP {
     ).then((result) => result.flat() as LSP.Symbol[])
     ).then((result) => result.flat() as LSP.Symbol[])
   }
   }
 
 
-  async function run<T>(
-    input: (client: LSPClient.Info) => Promise<T>,
-  ): Promise<T[]> {
+  async function run<T>(input: (client: LSPClient.Info) => Promise<T>): Promise<T[]> {
     const clients = await state().then((x) => [...x.clients.values()])
     const clients = await state().then((x) => [...x.clients.values()])
     const tasks = clients.map((x) => input(x))
     const tasks = clients.map((x) => input(x))
     return Promise.all(tasks)
     return Promise.all(tasks)

+ 15 - 36
packages/opencode/src/lsp/server.ts

@@ -25,21 +25,14 @@ export namespace LSPServer {
     id: "typescript",
     id: "typescript",
     extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
     extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
     async spawn(app) {
     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
       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 {
       return {
         process: proc,
         process: proc,
         initialization: {
         initialization: {
@@ -73,10 +66,7 @@ export namespace LSPServer {
           log.error("Failed to install gopls")
           log.error("Failed to install gopls")
           return
           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`, {
         log.info(`installed gopls`, {
           bin,
           bin,
         })
         })
@@ -113,10 +103,7 @@ export namespace LSPServer {
           log.error("Failed to install ruby-lsp")
           log.error("Failed to install ruby-lsp")
           return
           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`, {
         log.info(`installed ruby-lsp`, {
           bin,
           bin,
         })
         })
@@ -131,16 +118,12 @@ export namespace LSPServer {
     id: "pyright",
     id: "pyright",
     extensions: [".py", ".pyi"],
     extensions: [".py", ".pyi"],
     async spawn() {
     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 {
       return {
         process: proc,
         process: proc,
       }
       }
@@ -158,9 +141,7 @@ export namespace LSPServer {
           Global.Path.bin,
           Global.Path.bin,
           "elixir-ls-master",
           "elixir-ls-master",
           "release",
           "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())) {
         if (!(await Bun.file(binary).exists())) {
@@ -172,9 +153,7 @@ export namespace LSPServer {
 
 
           log.info("downloading elixir-ls from GitHub releases")
           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
           if (!response.ok) return
           const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
           const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
           await Bun.file(zipPath).write(response)
           await Bun.file(zipPath).write(response)

+ 24 - 60
packages/opencode/src/provider/provider.ts

@@ -91,8 +91,7 @@ export namespace Provider {
             if (!info || info.type !== "oauth") return
             if (!info || info.type !== "oauth") return
             if (!info.access || info.expires < Date.now()) {
             if (!info.access || info.expires < Date.now()) {
               const tokens = await copilot.access(info.refresh)
               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", {
               await Auth.set("github-copilot", {
                 type: "oauth",
                 type: "oauth",
                 ...tokens,
                 ...tokens,
@@ -101,15 +100,9 @@ export namespace Provider {
             }
             }
             let isAgentCall = false
             let isAgentCall = false
             try {
             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) {
               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 {}
             } catch {}
             const headers = {
             const headers = {
@@ -138,14 +131,11 @@ export namespace Provider {
       }
       }
     },
     },
     "amazon-bedrock": async () => {
     "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 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 {
       return {
         autoload: true,
         autoload: true,
         options: {
         options: {
@@ -157,9 +147,7 @@ export namespace Provider {
 
 
           switch (regionPrefix) {
           switch (regionPrefix) {
             case "us": {
             case "us": {
-              const modelRequiresPrefix = ["claude", "deepseek"].some((m) =>
-                modelID.includes(m),
-              )
+              const modelRequiresPrefix = ["claude", "deepseek"].some((m) => modelID.includes(m))
               if (modelRequiresPrefix) {
               if (modelRequiresPrefix) {
                 modelID = `${regionPrefix}.${modelID}`
                 modelID = `${regionPrefix}.${modelID}`
               }
               }
@@ -174,25 +162,18 @@ export namespace Provider {
                 "eu-south-1",
                 "eu-south-1",
                 "eu-south-2",
                 "eu-south-2",
               ].some((r) => region.includes(r))
               ].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) {
               if (regionRequiresPrefix && modelRequiresPrefix) {
                 modelID = `${regionPrefix}.${modelID}`
                 modelID = `${regionPrefix}.${modelID}`
               }
               }
               break
               break
             }
             }
             case "ap": {
             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) {
               if (modelRequiresPrefix) {
                 regionPrefix = "apac"
                 regionPrefix = "apac"
                 modelID = `${regionPrefix}.${modelID}`
                 modelID = `${regionPrefix}.${modelID}`
@@ -230,10 +211,7 @@ export namespace Provider {
         options: Record<string, any>
         options: Record<string, any>
       }
       }
     } = {}
     } = {}
-    const models = new Map<
-      string,
-      { info: ModelsDev.Model; language: LanguageModel }
-    >()
+    const models = new Map<string, { info: ModelsDev.Model; language: LanguageModel }>()
     const sdk = new Map<string, SDK>()
     const sdk = new Map<string, SDK>()
 
 
     log.info("init")
     log.info("init")
@@ -308,9 +286,7 @@ export namespace Provider {
       database[providerID] = parsed
       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
     // load env
     for (const [providerID, provider] of Object.entries(database)) {
     for (const [providerID, provider] of Object.entries(database)) {
       if (disabled.has(providerID)) continue
       if (disabled.has(providerID)) continue
@@ -337,12 +313,7 @@ export namespace Provider {
       if (disabled.has(providerID)) continue
       if (disabled.has(providerID)) continue
       const result = await fn(database[providerID])
       const result = await fn(database[providerID])
       if (result && (result.autoload || providers[providerID])) {
       if (result && (result.autoload || providers[providerID])) {
-        mergeProvider(
-          providerID,
-          result.options ?? {},
-          "custom",
-          result.getModel,
-        )
+        mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
       }
       }
     }
     }
 
 
@@ -379,7 +350,7 @@ export namespace Provider {
       const existing = s.sdk.get(provider.id)
       const existing = s.sdk.get(provider.id)
       if (existing) return existing
       if (existing) return existing
       const pkg = provider.npm ?? provider.id
       const pkg = provider.npm ?? provider.id
-      const mod = await import(await BunProc.install(pkg, "latest"))
+      const mod = await import(await BunProc.install(pkg, "beta"))
       const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
       const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
       const loaded = fn(s.providers[provider.id]?.options)
       const loaded = fn(s.providers[provider.id]?.options)
       s.sdk.set(provider.id, loaded)
       s.sdk.set(provider.id, loaded)
@@ -406,9 +377,7 @@ export namespace Provider {
     const sdk = await getSDK(provider.info)
     const sdk = await getSDK(provider.info)
 
 
     try {
     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 })
       log.info("found", { providerID, modelID })
       s.models.set(key, {
       s.models.set(key, {
         info,
         info,
@@ -435,10 +404,7 @@ export namespace Provider {
   export function sort(models: ModelsDev.Model[]) {
   export function sort(models: ModelsDev.Model[]) {
     return sortBy(
     return sortBy(
       models,
       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.includes("latest") ? 0 : 1), "asc"],
       [(model) => model.id, "desc"],
       [(model) => model.id, "desc"],
     )
     )
@@ -449,11 +415,7 @@ export namespace Provider {
     if (cfg.model) return parseModel(cfg.model)
     if (cfg.model) return parseModel(cfg.model)
     const provider = await list()
     const provider = await list()
       .then((val) => Object.values(val))
       .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")
     if (!provider) throw new Error("no providers found")
     const [model] = sort(Object.values(provider.info.models))
     const [model] = sort(Object.values(provider.info.models))
     if (!model) throw new Error("no models found")
     if (!model) throw new Error("no models found")
@@ -536,9 +498,11 @@ export namespace Provider {
 
 
     if (schema instanceof z.ZodUnion) {
     if (schema instanceof z.ZodUnion) {
       return z.union(
       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[],
+        ],
       )
       )
     }
     }
 
 

+ 6 - 10
packages/opencode/src/provider/transform.ts

@@ -1,19 +1,15 @@
-import type { LanguageModelV1Prompt } from "ai"
+import type { ModelMessage } from "ai"
 import { unique } from "remeda"
 import { unique } from "remeda"
 
 
 export namespace ProviderTransform {
 export namespace ProviderTransform {
-  export function message(
-    msgs: LanguageModelV1Prompt,
-    providerID: string,
-    modelID: string,
-  ) {
+  export function message(msgs: ModelMessage[], providerID: string, modelID: string) {
     if (providerID === "anthropic" || modelID.includes("anthropic")) {
     if (providerID === "anthropic" || modelID.includes("anthropic")) {
       const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
       const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
       const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
       const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
 
 
       for (const msg of unique([...system, ...final])) {
       for (const msg of unique([...system, ...final])) {
-        msg.providerMetadata = {
-          ...msg.providerMetadata,
+        msg.providerOptions = {
+          ...msg.providerOptions,
           anthropic: {
           anthropic: {
             cacheControl: { type: "ephemeral" },
             cacheControl: { type: "ephemeral" },
           },
           },
@@ -28,8 +24,8 @@ export namespace ProviderTransform {
       const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
       const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
 
 
       for (const msg of unique([...system, ...final])) {
       for (const msg of unique([...system, ...final])) {
-        msg.providerMetadata = {
-          ...msg.providerMetadata,
+        msg.providerOptions = {
+          ...msg.providerOptions,
           bedrock: {
           bedrock: {
             cachePoint: { type: "ephemeral" },
             cachePoint: { type: "ephemeral" },
           },
           },

+ 9 - 17
packages/opencode/src/server/server.ts

@@ -6,7 +6,6 @@ import { streamSSE } from "hono/streaming"
 import { Session } from "../session"
 import { Session } from "../session"
 import { resolver, validator as zValidator } from "hono-openapi/zod"
 import { resolver, validator as zValidator } from "hono-openapi/zod"
 import { z } from "zod"
 import { z } from "zod"
-import { Message } from "../session/message"
 import { Provider } from "../provider/provider"
 import { Provider } from "../provider/provider"
 import { App } from "../app/app"
 import { App } from "../app/app"
 import { mapValues } from "remeda"
 import { mapValues } from "remeda"
@@ -16,6 +15,7 @@ import { Ripgrep } from "../file/ripgrep"
 import { Config } from "../config/config"
 import { Config } from "../config/config"
 import { File } from "../file"
 import { File } from "../file"
 import { LSP } from "../lsp"
 import { LSP } from "../lsp"
+import { MessageV2 } from "../session/message-v2"
 
 
 const ERRORS = {
 const ERRORS = {
   400: {
   400: {
@@ -51,12 +51,9 @@ export namespace Server {
             status: 400,
             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) => {
       .use(async (c, next) => {
         log.info("request", {
         log.info("request", {
@@ -407,7 +404,7 @@ export namespace Server {
               description: "List of messages",
               description: "List of messages",
               content: {
               content: {
                 "application/json": {
                 "application/json": {
-                  schema: resolver(Message.Info.array()),
+                  schema: resolver(MessageV2.Info.array()),
                 },
                 },
               },
               },
             },
             },
@@ -433,7 +430,7 @@ export namespace Server {
               description: "Created message",
               description: "Created message",
               content: {
               content: {
                 "application/json": {
                 "application/json": {
-                  schema: resolver(Message.Info),
+                  schema: resolver(MessageV2.Assistant),
                 },
                 },
               },
               },
             },
             },
@@ -450,7 +447,7 @@ export namespace Server {
           z.object({
           z.object({
             providerID: z.string(),
             providerID: z.string(),
             modelID: z.string(),
             modelID: z.string(),
-            parts: Message.MessagePart.array(),
+            parts: MessageV2.UserPart.array(),
           }),
           }),
         ),
         ),
         async (c) => {
         async (c) => {
@@ -481,15 +478,10 @@ export namespace Server {
           },
           },
         }),
         }),
         async (c) => {
         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({
           return c.json({
             providers: Object.values(providers),
             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),
           })
           })
         },
         },
       )
       )

Разница между файлами не показана из-за своего большого размера
+ 277 - 421
packages/opencode/src/session/index.ts


+ 398 - 0
packages/opencode/src/session/message-v2.ts

@@ -0,0 +1,398 @@
+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 ToolStatePending = z
+    .object({
+      status: z.literal("pending"),
+    })
+    .openapi({
+      ref: "ToolStatePending",
+    })
+
+  export type ToolStatePending = z.infer<typeof ToolStatePending>
+
+  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<typeof ToolStateRunning>
+
+  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<typeof ToolStateCompleted>
+
+  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<typeof ToolStateError>
+
+  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<typeof TextPart>
+
+  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<typeof ToolPart>
+
+  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<typeof FilePart>
+
+  export const StepStartPart = z
+    .object({
+      type: z.literal("step-start"),
+    })
+    .openapi({
+      ref: "StepStartPart",
+    })
+  export type StepStartPart = z.infer<typeof StepStartPart>
+
+  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<typeof UserPart>
+
+  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<typeof User>
+
+  export const AssistantPart = z.discriminatedUnion("type", [TextPart, ToolPart, StepStartPart]).openapi({
+    ref: "AssistantMessagePart",
+  })
+  export type AssistantPart = z.infer<typeof AssistantPart>
+
+  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])
+      .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<typeof Assistant>
+
+  export const Info = z.discriminatedUnion("role", [User, Assistant]).openapi({
+    ref: "Message",
+  })
+  export type Info = z.infer<typeof Info>
+
+  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)
+  }
+}

+ 5 - 42
packages/opencode/src/session/message.ts

@@ -1,13 +1,9 @@
 import z from "zod"
 import z from "zod"
-import { Bus } from "../bus"
 import { Provider } from "../provider/provider"
 import { Provider } from "../provider/provider"
 import { NamedError } from "../util/error"
 import { NamedError } from "../util/error"
 
 
 export namespace Message {
 export namespace Message {
-  export const OutputLengthError = NamedError.create(
-    "MessageOutputLengthError",
-    z.object({}),
-  )
+  export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
 
 
   export const ToolCall = z
   export const ToolCall = z
     .object({
     .object({
@@ -49,11 +45,9 @@ export namespace Message {
     })
     })
   export type ToolResult = z.infer<typeof ToolResult>
   export type ToolResult = z.infer<typeof ToolResult>
 
 
-  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<typeof ToolInvocation>
   export type ToolInvocation = z.infer<typeof ToolInvocation>
 
 
   export const TextPart = z
   export const TextPart = z
@@ -122,14 +116,7 @@ export namespace Message {
   export type StepStartPart = z.infer<typeof StepStartPart>
   export type StepStartPart = z.infer<typeof StepStartPart>
 
 
   export const MessagePart = z
   export const MessagePart = z
-    .discriminatedUnion("type", [
-      TextPart,
-      ReasoningPart,
-      ToolInvocationPart,
-      SourceUrlPart,
-      FilePart,
-      StepStartPart,
-    ])
+    .discriminatedUnion("type", [TextPart, ReasoningPart, ToolInvocationPart, SourceUrlPart, FilePart, StepStartPart])
     .openapi({
     .openapi({
       ref: "MessagePart",
       ref: "MessagePart",
     })
     })
@@ -197,28 +184,4 @@ export namespace Message {
       ref: "Message",
       ref: "Message",
     })
     })
   export type Info = z.infer<typeof Info>
   export type Info = z.infer<typeof Info>
-
-  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(),
-      }),
-    ),
-  }
 }
 }

+ 1 - 3
packages/opencode/src/share/share.ts

@@ -53,9 +53,7 @@ export namespace Share {
 
 
   export const URL =
   export const URL =
     process.env["OPENCODE_API"] ??
     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) {
   export async function create(sessionID: string) {
     return fetch(`${URL}/share_create`, {
     return fetch(`${URL}/share_create`, {

+ 1 - 3
packages/opencode/src/snapshot/index.ts

@@ -55,9 +55,7 @@ export namespace Snapshot {
     log.info("restore", { commit })
     log.info("restore", { commit })
     const app = App.info()
     const app = App.info()
     const git = gitdir(sessionID)
     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) {
   function gitdir(sessionID: string) {

+ 46 - 12
packages/opencode/src/storage/storage.ts

@@ -4,44 +4,77 @@ import { Bus } from "../bus"
 import path from "path"
 import path from "path"
 import z from "zod"
 import z from "zod"
 import fs from "fs/promises"
 import fs from "fs/promises"
+import { MessageV2 } from "../session/message-v2"
 
 
 export namespace Storage {
 export namespace Storage {
   const log = Log.create({ service: "storage" })
   const log = Log.create({ service: "storage" })
 
 
   export const Event = {
   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() })),
   }
   }
 
 
-  const state = App.state("storage", () => {
+  type Migration = (dir: string) => Promise<void>
+
+  const MIGRATIONS: Migration[] = [
+    async (dir: string) => {
+      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"))
+        }
+      }
+    },
+  ]
+
+  const state = App.state("storage", async () => {
     const app = App.info()
     const app = App.info()
     const dir = path.join(app.path.data, "storage")
     const dir = path.join(app.path.data, "storage")
-    log.info("init", { path: dir })
+    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())
+    }
     return {
     return {
       dir,
       dir,
     }
     }
   })
   })
 
 
   export async function remove(key: string) {
   export async function remove(key: string) {
-    const target = path.join(state().dir, key + ".json")
+    const dir = await state().then((x) => x.dir)
+    const target = path.join(dir, key + ".json")
     await fs.unlink(target).catch(() => {})
     await fs.unlink(target).catch(() => {})
   }
   }
 
 
   export async function removeDir(key: string) {
   export async function removeDir(key: string) {
-    const target = path.join(state().dir, key)
+    const dir = await state().then((x) => x.dir)
+    const target = path.join(dir, key)
     await fs.rm(target, { recursive: true, force: true }).catch(() => {})
     await fs.rm(target, { recursive: true, force: true }).catch(() => {})
   }
   }
 
 
   export async function readJSON<T>(key: string) {
   export async function readJSON<T>(key: string) {
-    return Bun.file(path.join(state().dir, key + ".json")).json() as Promise<T>
+    const dir = await state().then((x) => x.dir)
+    return Bun.file(path.join(dir, key + ".json")).json() as Promise<T>
   }
   }
 
 
   export async function writeJSON<T>(key: string, content: T) {
   export async function writeJSON<T>(key: string, content: T) {
-    const target = path.join(state().dir, key + ".json")
+    const dir = await state().then((x) => x.dir)
+    const target = path.join(dir, key + ".json")
     const tmp = target + Date.now() + ".tmp"
     const tmp = target + Date.now() + ".tmp"
-    await Bun.write(tmp, JSON.stringify(content))
+    await Bun.write(tmp, JSON.stringify(content, null, 2))
     await fs.rename(tmp, target).catch(() => {})
     await fs.rename(tmp, target).catch(() => {})
     await fs.unlink(tmp).catch(() => {})
     await fs.unlink(tmp).catch(() => {})
     Bus.publish(Event.Write, { key, content })
     Bus.publish(Event.Write, { key, content })
@@ -49,9 +82,10 @@ export namespace Storage {
 
 
   const glob = new Bun.Glob("**/*")
   const glob = new Bun.Glob("**/*")
   export async function* list(prefix: string) {
   export async function* list(prefix: string) {
+    const dir = await state().then((x) => x.dir)
     try {
     try {
       for await (const item of glob.scan({
       for await (const item of glob.scan({
-        cwd: path.join(state().dir, prefix),
+        cwd: path.join(dir, prefix),
         onlyFiles: true,
         onlyFiles: true,
       })) {
       })) {
         const result = path.join(prefix, item.slice(0, -5))
         const result = path.join(prefix, item.slice(0, -5))

+ 3 - 15
packages/opencode/src/tool/bash.ts

@@ -12,12 +12,7 @@ export const BashTool = Tool.define({
   description: DESCRIPTION,
   description: DESCRIPTION,
   parameters: z.object({
   parameters: z.object({
     command: z.string().describe("The command to execute"),
     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
     description: z
       .string()
       .string()
       .describe(
       .describe(
@@ -41,21 +36,14 @@ export const BashTool = Tool.define({
     const stderr = await new Response(process.stderr).text()
     const stderr = await new Response(process.stderr).text()
 
 
     return {
     return {
+      title: params.command,
       metadata: {
       metadata: {
         stderr,
         stderr,
         stdout,
         stdout,
         exit: process.exitCode,
         exit: process.exitCode,
         description: params.description,
         description: params.description,
-        title: params.command,
       },
       },
-      output: [
-        `<stdout>`,
-        stdout ?? "",
-        `</stdout>`,
-        `<stderr>`,
-        stderr ?? "",
-        `</stderr>`,
-      ].join("\n"),
+      output: [`<stdout>`, stdout ?? "", `</stdout>`, `<stderr>`, stderr ?? "", `</stderr>`].join("\n"),
     }
     }
   },
   },
 })
 })

+ 14 - 53
packages/opencode/src/tool/edit.ts

@@ -20,15 +20,8 @@ export const EditTool = Tool.define({
   parameters: z.object({
   parameters: z.object({
     filePath: z.string().describe("The absolute path to the file to modify"),
     filePath: z.string().describe("The absolute path to the file to modify"),
     oldString: z.string().describe("The text to replace"),
     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) {
   async execute(params, ctx) {
     if (!params.filePath) {
     if (!params.filePath) {
@@ -40,9 +33,7 @@ export const EditTool = Tool.define({
     }
     }
 
 
     const app = App.info()
     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({
     await Permission.ask({
       id: "edit",
       id: "edit",
@@ -70,17 +61,11 @@ export const EditTool = Tool.define({
       const file = Bun.file(filepath)
       const file = Bun.file(filepath)
       const stats = await file.stat().catch(() => {})
       const stats = await file.stat().catch(() => {})
       if (!stats) throw new Error(`File ${filepath} not found`)
       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)
       await FileTime.assert(ctx.sessionID, filepath)
       contentOld = await file.text()
       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 file.write(contentNew)
       await Bus.publish(File.Event.Edited, {
       await Bus.publish(File.Event.Edited, {
         file: filepath,
         file: filepath,
@@ -88,9 +73,7 @@ export const EditTool = Tool.define({
       contentNew = await file.text()
       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)
     FileTime.read(ctx.sessionID, filepath)
 
 
@@ -110,17 +93,14 @@ export const EditTool = Tool.define({
       metadata: {
       metadata: {
         diagnostics,
         diagnostics,
         diff,
         diff,
-        title: `${path.relative(app.path.root, filepath)}`,
       },
       },
+      title: `${path.relative(app.path.root, filepath)}`,
       output,
       output,
     }
     }
   },
   },
 })
 })
 
 
-export type Replacer = (
-  content: string,
-  find: string,
-) => Generator<string, void, unknown>
+export type Replacer = (content: string, find: string) => Generator<string, void, unknown>
 
 
 export const SimpleReplacer: Replacer = function* (_content, find) {
 export const SimpleReplacer: Replacer = function* (_content, find) {
   yield find
   yield find
@@ -208,10 +188,7 @@ 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 normalizeWhitespace = (text: string) => text.replace(/\s+/g, " ").trim()
   const normalizedFind = normalizeWhitespace(find)
   const normalizedFind = normalizeWhitespace(find)
 
 
@@ -229,9 +206,7 @@ export const WhitespaceNormalizedReplacer: Replacer = function* (
       // Find the actual substring in the original line that matches
       // Find the actual substring in the original line that matches
       const words = find.trim().split(/\s+/)
       const words = find.trim().split(/\s+/)
       if (words.length > 0) {
       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 {
         try {
           const regex = new RegExp(pattern)
           const regex = new RegExp(pattern)
           const match = line.match(regex)
           const match = line.match(regex)
@@ -270,9 +245,7 @@ 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)
   const normalizedFind = removeIndentation(find)
@@ -423,10 +396,7 @@ export const ContextAwareReplacer: Replacer = function* (content, find) {
             }
             }
           }
           }
 
 
-          if (
-            totalNonEmptyLines === 0 ||
-            matchingLines / totalNonEmptyLines >= 0.5
-          ) {
+          if (totalNonEmptyLines === 0 || matchingLines / totalNonEmptyLines >= 0.5) {
             yield block
             yield block
             break // Only match the first occurrence
             break // Only match the first occurrence
           }
           }
@@ -473,12 +443,7 @@ function trimDiff(diff: string): string {
   return trimmedLines.join("\n")
   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) {
   if (oldString === newString) {
     throw new Error("oldString and newString must be different")
     throw new Error("oldString and newString must be different")
   }
   }
@@ -502,11 +467,7 @@ export function replace(
       }
       }
       const lastIndex = content.lastIndexOf(search)
       const lastIndex = content.lastIndexOf(search)
       if (index !== lastIndex) continue
       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")
   throw new Error("oldString not found in content or was found multiple times")

+ 3 - 7
packages/opencode/src/tool/glob.ts

@@ -20,9 +20,7 @@ export const GlobTool = Tool.define({
   async execute(params) {
   async execute(params) {
     const app = App.info()
     const app = App.info()
     let search = params.path ?? app.path.cwd
     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 limit = 100
     const files = []
     const files = []
@@ -53,17 +51,15 @@ export const GlobTool = Tool.define({
       output.push(...files.map((f) => f.path))
       output.push(...files.map((f) => f.path))
       if (truncated) {
       if (truncated) {
         output.push("")
         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 {
     return {
+      title: path.relative(app.path.root, search),
       metadata: {
       metadata: {
         count: files.length,
         count: files.length,
         truncated,
         truncated,
-        title: path.relative(app.path.root, search),
       },
       },
       output: output.join("\n"),
       output: output.join("\n"),
     }
     }

+ 9 - 21
packages/opencode/src/tool/grep.ts

@@ -9,21 +9,9 @@ export const GrepTool = Tool.define({
   id: "grep",
   id: "grep",
   description: DESCRIPTION,
   description: DESCRIPTION,
   parameters: z.object({
   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) {
   async execute(params) {
     if (!params.pattern) {
     if (!params.pattern) {
@@ -51,7 +39,8 @@ export const GrepTool = Tool.define({
 
 
     if (exitCode === 1) {
     if (exitCode === 1) {
       return {
       return {
-        metadata: { matches: 0, truncated: false, title: params.pattern },
+        title: params.pattern,
+        metadata: { matches: 0, truncated: false },
         output: "No files found",
         output: "No files found",
       }
       }
     }
     }
@@ -93,7 +82,8 @@ export const GrepTool = Tool.define({
 
 
     if (finalMatches.length === 0) {
     if (finalMatches.length === 0) {
       return {
       return {
-        metadata: { matches: 0, truncated: false, title: params.pattern },
+        title: params.pattern,
+        metadata: { matches: 0, truncated: false },
         output: "No files found",
         output: "No files found",
       }
       }
     }
     }
@@ -114,16 +104,14 @@ export const GrepTool = Tool.define({
 
 
     if (truncated) {
     if (truncated) {
       outputLines.push("")
       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 {
     return {
+      title: params.pattern,
       metadata: {
       metadata: {
         matches: finalMatches.length,
         matches: finalMatches.length,
         truncated,
         truncated,
-        title: params.pattern,
       },
       },
       output: outputLines.join("\n"),
       output: outputLines.join("\n"),
     }
     }

+ 4 - 13
packages/opencode/src/tool/ls.ts

@@ -24,16 +24,8 @@ export const ListTool = Tool.define({
   id: "list",
   id: "list",
   description: DESCRIPTION,
   description: DESCRIPTION,
   parameters: z.object({
   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) {
   async execute(params) {
     const app = App.info()
     const app = App.info()
@@ -44,8 +36,7 @@ export const ListTool = Tool.define({
 
 
     for await (const file of glob.scan({ cwd: searchPath, dot: true })) {
     for await (const file of glob.scan({ cwd: searchPath, dot: true })) {
       if (IGNORE_PATTERNS.some((p) => file.includes(p))) continue
       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)
       files.push(file)
       if (files.length >= LIMIT) break
       if (files.length >= LIMIT) break
     }
     }
@@ -99,10 +90,10 @@ export const ListTool = Tool.define({
     const output = `${searchPath}/\n` + renderDir(".", 0)
     const output = `${searchPath}/\n` + renderDir(".", 0)
 
 
     return {
     return {
+      title: path.relative(app.path.root, searchPath),
       metadata: {
       metadata: {
         count: files.length,
         count: files.length,
         truncated: files.length >= LIMIT,
         truncated: files.length >= LIMIT,
-        title: path.relative(app.path.root, searchPath),
       },
       },
       output,
       output,
     }
     }

+ 3 - 7
packages/opencode/src/tool/lsp-diagnostics.ts

@@ -13,20 +13,16 @@ export const LspDiagnosticTool = Tool.define({
   }),
   }),
   execute: async (args) => {
   execute: async (args) => {
     const app = App.info()
     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)
     await LSP.touchFile(normalized, true)
     const diagnostics = await LSP.diagnostics()
     const diagnostics = await LSP.diagnostics()
     const file = diagnostics[normalized]
     const file = diagnostics[normalized]
     return {
     return {
+      title: path.relative(app.path.root, normalized),
       metadata: {
       metadata: {
         diagnostics,
         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",
     }
     }
   },
   },
 })
 })

+ 2 - 9
packages/opencode/src/tool/lsp-hover.ts

@@ -15,9 +15,7 @@ export const LspHoverTool = Tool.define({
   }),
   }),
   execute: async (args) => {
   execute: async (args) => {
     const app = App.info()
     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)
     await LSP.touchFile(file, true)
     const result = await LSP.hover({
     const result = await LSP.hover({
       ...args,
       ...args,
@@ -25,14 +23,9 @@ export const LspHoverTool = Tool.define({
     })
     })
 
 
     return {
     return {
+      title: path.relative(app.path.root, file) + ":" + args.line + ":" + args.character,
       metadata: {
       metadata: {
         result,
         result,
-        title:
-          path.relative(app.path.root, file) +
-          ":" +
-          args.line +
-          ":" +
-          args.character,
       },
       },
       output: JSON.stringify(result, null, 2),
       output: JSON.stringify(result, null, 2),
     }
     }

+ 2 - 4
packages/opencode/src/tool/multiedit.ts

@@ -10,9 +10,7 @@ export const MultiEditTool = Tool.define({
   description: DESCRIPTION,
   description: DESCRIPTION,
   parameters: z.object({
   parameters: z.object({
     filePath: z.string().describe("The absolute path to the file to modify"),
     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) {
   async execute(params, ctx) {
     const results = []
     const results = []
@@ -30,9 +28,9 @@ export const MultiEditTool = Tool.define({
     }
     }
     const app = App.info()
     const app = App.info()
     return {
     return {
+      title: path.relative(app.path.root, params.filePath),
       metadata: {
       metadata: {
         results: results.map((r) => r.metadata),
         results: results.map((r) => r.metadata),
-        title: path.relative(app.path.root, params.filePath),
       },
       },
       output: results.at(-1)!.output,
       output: results.at(-1)!.output,
     }
     }

+ 10 - 37
packages/opencode/src/tool/patch.ts

@@ -6,9 +6,7 @@ import { FileTime } from "../file/time"
 import DESCRIPTION from "./patch.txt"
 import DESCRIPTION from "./patch.txt"
 
 
 const PatchParams = z.object({
 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 {
 interface Change {
@@ -42,10 +40,7 @@ function identifyFilesNeeded(patchText: string): string[] {
   const files: string[] = []
   const files: string[] = []
   const lines = patchText.split("\n")
   const lines = patchText.split("\n")
   for (const line of lines) {
   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()
       const filePath = line.split(":", 2)[1]?.trim()
       if (filePath) files.push(filePath)
       if (filePath) files.push(filePath)
     }
     }
@@ -65,10 +60,7 @@ function identifyFilesAdded(patchText: string): string[] {
   return files
   return files
 }
 }
 
 
-function textToPatch(
-  patchText: string,
-  _currentFiles: Record<string, string>,
-): [PatchOperation[], number] {
+function textToPatch(patchText: string, _currentFiles: Record<string, string>): [PatchOperation[], number] {
   const operations: PatchOperation[] = []
   const operations: PatchOperation[] = []
   const lines = patchText.split("\n")
   const lines = patchText.split("\n")
   let i = 0
   let i = 0
@@ -93,11 +85,7 @@ function textToPatch(
           const changes: PatchChange[] = []
           const changes: PatchChange[] = []
           i++
           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]
             const changeLine = lines[i]
             if (changeLine.startsWith(" ")) {
             if (changeLine.startsWith(" ")) {
               changes.push({ type: "keep", content: changeLine.substring(1) })
               changes.push({ type: "keep", content: changeLine.substring(1) })
@@ -151,10 +139,7 @@ function textToPatch(
   return [operations, fuzz]
   return [operations, fuzz]
 }
 }
 
 
-function patchToCommit(
-  operations: PatchOperation[],
-  currentFiles: Record<string, string>,
-): Commit {
+function patchToCommit(operations: PatchOperation[], currentFiles: Record<string, string>): Commit {
   const changes: Record<string, Change> = {}
   const changes: Record<string, Change> = {}
 
 
   for (const op of operations) {
   for (const op of operations) {
@@ -173,9 +158,7 @@ function patchToCommit(
       const lines = originalContent.split("\n")
       const lines = originalContent.split("\n")
 
 
       for (const hunk of op.hunks) {
       for (const hunk of op.hunks) {
-        const contextIndex = lines.findIndex((line) =>
-          line.includes(hunk.contextLine),
-        )
+        const contextIndex = lines.findIndex((line) => line.includes(hunk.contextLine))
         if (contextIndex === -1) {
         if (contextIndex === -1) {
           throw new Error(`Context line not found: ${hunk.contextLine}`)
           throw new Error(`Context line not found: ${hunk.contextLine}`)
         }
         }
@@ -204,11 +187,7 @@ function patchToCommit(
   return { changes }
   return { changes }
 }
 }
 
 
-function generateDiff(
-  oldContent: string,
-  newContent: string,
-  filePath: string,
-): [string, number, number] {
+function generateDiff(oldContent: string, newContent: string, filePath: string): [string, number, number] {
   // Mock implementation - would need actual diff generation
   // Mock implementation - would need actual diff generation
   const lines1 = oldContent.split("\n")
   const lines1 = oldContent.split("\n")
   const lines2 = newContent.split("\n")
   const lines2 = newContent.split("\n")
@@ -296,9 +275,7 @@ export const PatchTool = Tool.define({
     // Process the patch
     // Process the patch
     const [patch, fuzz] = textToPatch(params.patchText, currentFiles)
     const [patch, fuzz] = textToPatch(params.patchText, currentFiles)
     if (fuzz > 3) {
     if (fuzz > 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
     // Convert patch to commit
@@ -343,11 +320,7 @@ export const PatchTool = Tool.define({
       const newContent = change.new_content || ""
       const newContent = change.new_content || ""
 
 
       // Calculate diff statistics
       // Calculate diff statistics
-      const [, additions, removals] = generateDiff(
-        oldContent,
-        newContent,
-        filePath,
-      )
+      const [, additions, removals] = generateDiff(oldContent, newContent, filePath)
       totalAdditions += additions
       totalAdditions += additions
       totalRemovals += removals
       totalRemovals += removals
 
 
@@ -358,11 +331,11 @@ export const PatchTool = Tool.define({
     const output = result
     const output = result
 
 
     return {
     return {
+      title: `${filesToRead.length} files`,
       metadata: {
       metadata: {
         changed: changedFiles,
         changed: changedFiles,
         additions: totalAdditions,
         additions: totalAdditions,
         removals: totalRemovals,
         removals: totalRemovals,
-        title: `${filesToRead.length} files`,
       },
       },
       output,
       output,
     }
     }

+ 9 - 27
packages/opencode/src/tool/read.ts

@@ -16,14 +16,8 @@ export const ReadTool = Tool.define({
   description: DESCRIPTION,
   description: DESCRIPTION,
   parameters: z.object({
   parameters: z.object({
     filePath: z.string().describe("The path to the file to read"),
     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) {
   async execute(params, ctx) {
     let filePath = params.filePath
     let filePath = params.filePath
@@ -40,16 +34,13 @@ export const ReadTool = Tool.define({
       const suggestions = dirEntries
       const suggestions = dirEntries
         .filter(
         .filter(
           (entry) =>
           (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))
         .map((entry) => path.join(dir, entry))
         .slice(0, 3)
         .slice(0, 3)
 
 
       if (suggestions.length > 0) {
       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}`)
       throw new Error(`File not found: ${filePath}`)
@@ -57,21 +48,14 @@ export const ReadTool = Tool.define({
     const stats = await file.stat()
     const stats = await file.stat()
 
 
     if (stats.size > MAX_READ_SIZE)
     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 limit = params.limit ?? DEFAULT_READ_LIMIT
     const offset = params.offset || 0
     const offset = params.offset || 0
     const isImage = isImageFile(filePath)
     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 lines = await file.text().then((text) => text.split("\n"))
     const raw = lines.slice(offset, offset + limit).map((line) => {
     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) => {
     const content = raw.map((line, index) => {
       return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`
       return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`
@@ -82,9 +66,7 @@ export const ReadTool = Tool.define({
     output += content.join("\n")
     output += content.join("\n")
 
 
     if (lines.length > offset + content.length) {
     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</file>"
     output += "\n</file>"
 
 
@@ -93,10 +75,10 @@ export const ReadTool = Tool.define({
     FileTime.read(ctx.sessionID, filePath)
     FileTime.read(ctx.sessionID, filePath)
 
 
     return {
     return {
+      title: path.relative(App.info().path.root, filePath),
       output,
       output,
       metadata: {
       metadata: {
         preview,
         preview,
-        title: path.relative(App.info().path.root, filePath),
       },
       },
     }
     }
   },
   },

+ 14 - 19
packages/opencode/src/tool/task.ts

@@ -3,41 +3,36 @@ import DESCRIPTION from "./task.txt"
 import { z } from "zod"
 import { z } from "zod"
 import { Session } from "../session"
 import { Session } from "../session"
 import { Bus } from "../bus"
 import { Bus } from "../bus"
-import { Message } from "../session/message"
+import { MessageV2 } from "../session/message-v2"
 
 
 export const TaskTool = Tool.define({
 export const TaskTool = Tool.define({
   id: "task",
   id: "task",
   description: DESCRIPTION,
   description: DESCRIPTION,
   parameters: z.object({
   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"),
     prompt: z.string().describe("The task for the agent to perform"),
   }),
   }),
   async execute(params, ctx) {
   async execute(params, ctx) {
     const session = await Session.create(ctx.sessionID)
     const session = await Session.create(ctx.sessionID)
-    const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
-    const metadata = msg.metadata.assistant!
+    const msg = (await Session.getMessage(ctx.sessionID, ctx.messageID)) as MessageV2.Assistant
 
 
-    function summary(input: Message.Info) {
+    function summary(input: MessageV2.Info) {
       const result = []
       const result = []
-
       for (const part of input.parts) {
       for (const part of input.parts) {
-        if (part.type === "tool-invocation") {
-          result.push({
-            toolInvocation: part.toolInvocation,
-            metadata: input.metadata.tool[part.toolInvocation.toolCallId],
-          })
+        if (part.type === "tool" && part.state.status === "completed") {
+          result.push(part)
         }
         }
       }
       }
       return result
       return result
     }
     }
 
 
-    const unsub = Bus.subscribe(Message.Event.Updated, async (evt) => {
-      if (evt.properties.info.metadata.sessionID !== session.id) return
+    const unsub = Bus.subscribe(MessageV2.Event.Updated, async (evt) => {
+      if (evt.properties.info.sessionID !== session.id) return
       ctx.metadata({
       ctx.metadata({
         title: params.description,
         title: params.description,
-        summary: summary(evt.properties.info),
+        metadata: {
+          summary: summary(evt.properties.info),
+        },
       })
       })
     })
     })
 
 
@@ -46,8 +41,8 @@ export const TaskTool = Tool.define({
     })
     })
     const result = await Session.chat({
     const result = await Session.chat({
       sessionID: session.id,
       sessionID: session.id,
-      modelID: metadata.modelID,
-      providerID: metadata.providerID,
+      modelID: msg.modelID,
+      providerID: msg.providerID,
       parts: [
       parts: [
         {
         {
           type: "text",
           type: "text",
@@ -57,8 +52,8 @@ export const TaskTool = Tool.define({
     })
     })
     unsub()
     unsub()
     return {
     return {
+      title: params.description,
       metadata: {
       metadata: {
-        title: params.description,
         summary: summary(result),
         summary: summary(result),
       },
       },
       output: result.parts.findLast((x) => x.type === "text")!.text,
       output: result.parts.findLast((x) => x.type === "text")!.text,

+ 4 - 8
packages/opencode/src/tool/todo.ts

@@ -5,12 +5,8 @@ import { App } from "../app/app"
 
 
 const TodoInfo = z.object({
 const TodoInfo = z.object({
   content: z.string().min(1).describe("Brief description of the task"),
   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"),
   id: z.string().describe("Unique identifier for the todo item"),
 })
 })
 type TodoInfo = z.infer<typeof TodoInfo>
 type TodoInfo = z.infer<typeof TodoInfo>
@@ -32,9 +28,9 @@ export const TodoWriteTool = Tool.define({
     const todos = state()
     const todos = state()
     todos[opts.sessionID] = params.todos
     todos[opts.sessionID] = params.todos
     return {
     return {
+      title: `${params.todos.filter((x) => x.status !== "completed").length} todos`,
       output: JSON.stringify(params.todos, null, 2),
       output: JSON.stringify(params.todos, null, 2),
       metadata: {
       metadata: {
-        title: `${params.todos.filter((x) => x.status !== "completed").length} todos`,
         todos: params.todos,
         todos: params.todos,
       },
       },
     }
     }
@@ -48,9 +44,9 @@ export const TodoReadTool = Tool.define({
   async execute(_params, opts) {
   async execute(_params, opts) {
     const todos = state()[opts.sessionID] ?? []
     const todos = state()[opts.sessionID] ?? []
     return {
     return {
+      title: `${todos.filter((x) => x.status !== "completed").length} todos`,
       metadata: {
       metadata: {
         todos,
         todos,
-        title: `${todos.filter((x) => x.status !== "completed").length} todos`,
       },
       },
       output: JSON.stringify(todos, null, 2),
       output: JSON.stringify(todos, null, 2),
     }
     }

+ 6 - 10
packages/opencode/src/tool/tool.ts

@@ -2,19 +2,15 @@ import type { StandardSchemaV1 } from "@standard-schema/spec"
 
 
 export namespace Tool {
 export namespace Tool {
   interface Metadata {
   interface Metadata {
-    title: string
     [key: string]: any
     [key: string]: any
   }
   }
   export type Context<M extends Metadata = Metadata> = {
   export type Context<M extends Metadata = Metadata> = {
     sessionID: string
     sessionID: string
     messageID: string
     messageID: string
     abort: AbortSignal
     abort: AbortSignal
-    metadata(meta: M): void
+    metadata(input: { title?: string; metadata?: M }): void
   }
   }
-  export interface Info<
-    Parameters extends StandardSchemaV1 = StandardSchemaV1,
-    M extends Metadata = Metadata,
-  > {
+  export interface Info<Parameters extends StandardSchemaV1 = StandardSchemaV1, M extends Metadata = Metadata> {
     id: string
     id: string
     description: string
     description: string
     parameters: Parameters
     parameters: Parameters
@@ -22,15 +18,15 @@ export namespace Tool {
       args: StandardSchemaV1.InferOutput<Parameters>,
       args: StandardSchemaV1.InferOutput<Parameters>,
       ctx: Context,
       ctx: Context,
     ): Promise<{
     ): Promise<{
+      title: string
       metadata: M
       metadata: M
       output: string
       output: string
     }>
     }>
   }
   }
 
 
-  export function define<
-    Parameters extends StandardSchemaV1,
-    Result extends Metadata,
-  >(input: Info<Parameters, Result>): Info<Parameters, Result> {
+  export function define<Parameters extends StandardSchemaV1, Result extends Metadata>(
+    input: Info<Parameters, Result>,
+  ): Info<Parameters, Result> {
     return input
     return input
   }
   }
 }
 }

+ 17 - 41
packages/opencode/src/tool/webfetch.ts

@@ -14,9 +14,7 @@ export const WebFetchTool = Tool.define({
     url: z.string().describe("The URL to fetch content from"),
     url: z.string().describe("The URL to fetch content from"),
     format: z
     format: z
       .enum(["text", "markdown", "html"])
       .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
     timeout: z
       .number()
       .number()
       .min(0)
       .min(0)
@@ -26,17 +24,11 @@ export const WebFetchTool = Tool.define({
   }),
   }),
   async execute(params, ctx) {
   async execute(params, ctx) {
     // Validate URL
     // 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://")
       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 controller = new AbortController()
     const timeoutId = setTimeout(() => controller.abort(), timeout)
     const timeoutId = setTimeout(() => controller.abort(), timeout)
@@ -46,8 +38,7 @@ export const WebFetchTool = Tool.define({
       headers: {
       headers: {
         "User-Agent":
         "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",
           "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",
         "Accept-Language": "en-US,en;q=0.9",
       },
       },
     })
     })
@@ -79,16 +70,14 @@ export const WebFetchTool = Tool.define({
           const text = await extractTextFromHTML(content)
           const text = await extractTextFromHTML(content)
           return {
           return {
             output: text,
             output: text,
-            metadata: {
-              title,
-            },
+            title,
+            metadata: {},
           }
           }
         }
         }
         return {
         return {
           output: content,
           output: content,
-          metadata: {
-            title,
-          },
+          title,
+          metadata: {},
         }
         }
 
 
       case "markdown":
       case "markdown":
@@ -96,32 +85,28 @@ export const WebFetchTool = Tool.define({
           const markdown = convertHTMLToMarkdown(content)
           const markdown = convertHTMLToMarkdown(content)
           return {
           return {
             output: markdown,
             output: markdown,
-            metadata: {
-              title,
-            },
+            title,
+            metadata: {},
           }
           }
         }
         }
         return {
         return {
           output: "```\n" + content + "\n```",
           output: "```\n" + content + "\n```",
-          metadata: {
-            title,
-          },
+          title,
+          metadata: {},
         }
         }
 
 
       case "html":
       case "html":
         return {
         return {
           output: content,
           output: content,
-          metadata: {
-            title,
-          },
+          title,
+          metadata: {},
         }
         }
 
 
       default:
       default:
         return {
         return {
           output: content,
           output: content,
-          metadata: {
-            title,
-          },
+          title,
+          metadata: {},
         }
         }
     }
     }
   },
   },
@@ -143,16 +128,7 @@ async function extractTextFromHTML(html: string) {
     .on("*", {
     .on("*", {
       element(element) {
       element(element) {
         // Reset skip flag when entering other elements
         // 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
           skipContent = false
         }
         }
       },
       },

+ 4 - 12
packages/opencode/src/tool/write.ts

@@ -13,18 +13,12 @@ export const WriteTool = Tool.define({
   id: "write",
   id: "write",
   description: DESCRIPTION,
   description: DESCRIPTION,
   parameters: z.object({
   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"),
     content: z.string().describe("The content to write to the file"),
   }),
   }),
   async execute(params, ctx) {
   async execute(params, ctx) {
     const app = App.info()
     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 file = Bun.file(filepath)
     const exists = await file.exists()
     const exists = await file.exists()
@@ -33,9 +27,7 @@ export const WriteTool = Tool.define({
     await Permission.ask({
     await Permission.ask({
       id: "write",
       id: "write",
       sessionID: ctx.sessionID,
       sessionID: ctx.sessionID,
-      title: exists
-        ? "Overwrite this file: " + filepath
-        : "Create new file: " + filepath,
+      title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath,
       metadata: {
       metadata: {
         filePath: filepath,
         filePath: filepath,
         content: params.content,
         content: params.content,
@@ -62,11 +54,11 @@ export const WriteTool = Tool.define({
     }
     }
 
 
     return {
     return {
+      title: path.relative(app.path.root, filepath),
       metadata: {
       metadata: {
         diagnostics,
         diagnostics,
         filepath,
         filepath,
         exists: exists,
         exists: exists,
-        title: path.relative(app.path.root, filepath),
       },
       },
       output,
       output,
     }
     }

+ 1 - 4
packages/opencode/src/util/error.ts

@@ -7,10 +7,7 @@ export abstract class NamedError extends Error {
   abstract schema(): ZodSchema
   abstract schema(): ZodSchema
   abstract toObject(): { name: string; data: any }
   abstract toObject(): { name: string; data: any }
 
 
-  static create<Name extends string, Data extends ZodSchema>(
-    name: Name,
-    data: Data,
-  ) {
+  static create<Name extends string, Data extends ZodSchema>(name: Name, data: Data) {
     const schema = z
     const schema = z
       .object({
       .object({
         name: z.literal(name),
         name: z.literal(name),

+ 3 - 12
packages/opencode/src/util/log.ts

@@ -19,10 +19,7 @@ export namespace Log {
     await fs.mkdir(dir, { recursive: true })
     await fs.mkdir(dir, { recursive: true })
     cleanup(dir)
     cleanup(dir)
     if (options.print) return
     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)
     const logfile = Bun.file(logpath)
     await fs.truncate(logpath).catch(() => {})
     await fs.truncate(logpath).catch(() => {})
     const writer = logfile.writer()
     const writer = logfile.writer()
@@ -43,9 +40,7 @@ export namespace Log {
 
 
     const filesToDelete = files.slice(0, -10)
     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()
   let last = Date.now()
@@ -63,11 +58,7 @@ export namespace Log {
       const next = new Date()
       const next = new Date()
       const diff = next.getTime() - last
       const diff = next.getTime() - last
       last = next.getTime()
       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 = {
     const result = {
       info(message?: any, extra?: Record<string, any>) {
       info(message?: any, extra?: Record<string, any>) {

+ 20 - 91
packages/opencode/test/tool/edit.test.ts

@@ -17,12 +17,7 @@ const testCases: TestCase[] = [
     replace: 'console.log("universe");',
     replace: 'console.log("universe");',
   },
   },
   {
   {
-    content: [
-      "if (condition) {",
-      "  doSomething();",
-      "  doSomethingElse();",
-      "}",
-    ].join("\n"),
+    content: ["if (condition) {", "  doSomething();", "  doSomethingElse();", "}"].join("\n"),
     find: ["  doSomething();", "  doSomethingElse();"].join("\n"),
     find: ["  doSomething();", "  doSomethingElse();"].join("\n"),
     replace: ["  doNewThing();", "  doAnotherThing();"].join("\n"),
     replace: ["  doNewThing();", "  doAnotherThing();"].join("\n"),
   },
   },
@@ -53,15 +48,8 @@ const testCases: TestCase[] = [
       "  return result;",
       "  return result;",
       "}",
       "}",
     ].join("\n"),
     ].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: [
     content: [
@@ -76,13 +64,7 @@ const testCases: TestCase[] = [
       "}",
       "}",
     ].join("\n"),
     ].join("\n"),
     find: ["class MyClass {", "  // different implementation", "}"].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
   // WhitespaceNormalizedReplacer cases
@@ -104,48 +86,21 @@ const testCases: TestCase[] = [
 
 
   // IndentationFlexibleReplacer cases
   // 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"),
     replace: ["if (true) {", 'console.log("updated");', "}"].join("\n"),
   },
   },
 
 
   // replaceAll option cases
   // 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");',
     find: 'console.log("test");',
     replace: 'console.log("updated");',
     replace: 'console.log("updated");',
     all: true,
     all: true,
@@ -213,9 +168,7 @@ const testCases: TestCase[] = [
 
 
   // MultiOccurrenceReplacer cases (with replaceAll)
   // MultiOccurrenceReplacer cases (with replaceAll)
   {
   {
-    content: ["debug('start');", "debug('middle');", "debug('end');"].join(
-      "\n",
-    ),
+    content: ["debug('start');", "debug('middle');", "debug('end');"].join("\n"),
     find: "debug",
     find: "debug",
     replace: "log",
     replace: "log",
     all: true,
     all: true,
@@ -239,9 +192,7 @@ const testCases: TestCase[] = [
     replace: "const value = 24;",
     replace: "const value = 24;",
   },
   },
   {
   {
-    content: ["", "  if (condition) {", "    doSomething();", "  }", ""].join(
-      "\n",
-    ),
+    content: ["", "  if (condition) {", "    doSomething();", "  }", ""].join("\n"),
     find: ["if (condition) {", "  doSomething();", "}"].join("\n"),
     find: ["if (condition) {", "  doSomething();", "}"].join("\n"),
     replace: ["if (condition) {", "  doNothing();", "}"].join("\n"),
     replace: ["if (condition) {", "  doNothing();", "}"].join("\n"),
   },
   },
@@ -262,9 +213,7 @@ const testCases: TestCase[] = [
       "  return result;",
       "  return result;",
       "}",
       "}",
     ].join("\n"),
     ].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: [
     content: [
@@ -278,15 +227,8 @@ const testCases: TestCase[] = [
       "  }",
       "  }",
       "}",
       "}",
     ].join("\n"),
     ].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
   // Combined edge cases for new replacers
@@ -296,9 +238,7 @@ const testCases: TestCase[] = [
     replace: 'console.log("updated");',
     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"),
     find: ["function test() {", "return 'value';", "}"].join("\n"),
     replace: ["function test() {", "return 'new value';", "}"].join("\n"),
     replace: ["function test() {", "return 'new value';", "}"].join("\n"),
   },
   },
@@ -346,13 +286,7 @@ const testCases: TestCase[] = [
 
 
   // ContextAwareReplacer - test with trailing newline in find string
   // 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: [
     find: [
       "class Test {",
       "class Test {",
       "  // different content",
       "  // different content",
@@ -401,12 +335,7 @@ describe("EditTool Replacers", () => {
         replace(testCase.content, testCase.find, testCase.replace, testCase.all)
         replace(testCase.content, testCase.find, testCase.replace, testCase.all)
       }).toThrow()
       }).toThrow()
     } else {
     } 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)
       expect(result).toContain(testCase.replace)
     }
     }
   })
   })

+ 1 - 4
packages/opencode/test/tool/tool.test.ts

@@ -42,10 +42,7 @@ describe("tool.glob", () => {
 describe("tool.ls", () => {
 describe("tool.ls", () => {
   test("basic", async () => {
   test("basic", async () => {
     const result = await App.provide({ cwd: process.cwd() }, 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()
     expect(result.output).toMatchSnapshot()
   })
   })

+ 1 - 1
packages/tui/go.mod

@@ -10,6 +10,7 @@ require (
 	github.com/charmbracelet/glamour v0.10.0
 	github.com/charmbracelet/glamour v0.10.0
 	github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1
 	github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1
 	github.com/charmbracelet/x/ansi v0.8.0
 	github.com/charmbracelet/x/ansi v0.8.0
+	github.com/google/uuid v1.6.0
 	github.com/lithammer/fuzzysearch v1.1.8
 	github.com/lithammer/fuzzysearch v1.1.8
 	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
 	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
 	github.com/muesli/reflow v0.3.0
 	github.com/muesli/reflow v0.3.0
@@ -37,7 +38,6 @@ require (
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
 	github.com/go-openapi/swag v0.23.0 // indirect
 	github.com/go-openapi/swag v0.23.0 // indirect
 	github.com/goccy/go-yaml v1.17.1 // indirect
 	github.com/goccy/go-yaml v1.17.1 // indirect
-	github.com/google/uuid v1.6.0 // indirect
 	github.com/invopop/yaml v0.3.1 // indirect
 	github.com/invopop/yaml v0.3.1 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect

+ 26 - 25
packages/tui/internal/app/app.go

@@ -30,7 +30,7 @@ type App struct {
 	Provider  *opencode.Provider
 	Provider  *opencode.Provider
 	Model     *opencode.Model
 	Model     *opencode.Model
 	Session   *opencode.Session
 	Session   *opencode.Session
-	Messages  []opencode.Message
+	Messages  []opencode.MessageUnion
 	Commands  commands.CommandRegistry
 	Commands  commands.CommandRegistry
 }
 }
 
 
@@ -47,7 +47,7 @@ type SendMsg struct {
 	Attachments []opencode.FilePartParam
 	Attachments []opencode.FilePartParam
 }
 }
 type OptimisticMessageAddedMsg struct {
 type OptimisticMessageAddedMsg struct {
-	Message opencode.Message
+	Message opencode.MessageUnion
 }
 }
 type FileRenderedMsg struct {
 type FileRenderedMsg struct {
 	FilePath string
 	FilePath string
@@ -116,7 +116,7 @@ func New(
 		State:     appState,
 		State:     appState,
 		Client:    httpClient,
 		Client:    httpClient,
 		Session:   &opencode.Session{},
 		Session:   &opencode.Session{},
-		Messages:  []opencode.Message{},
+		Messages:  []opencode.MessageUnion{},
 		Commands:  commands.LoadFromConfig(configInfo),
 		Commands:  commands.LoadFromConfig(configInfo),
 	}
 	}
 
 
@@ -223,7 +223,10 @@ func (a *App) IsBusy() bool {
 	}
 	}
 
 
 	lastMessage := a.Messages[len(a.Messages)-1]
 	lastMessage := a.Messages[len(a.Messages)-1]
-	return lastMessage.Metadata.Time.Completed == 0
+	if casted, ok := lastMessage.(opencode.AssistantMessage); ok {
+		return casted.Time.Completed == 0
+	}
+	return false
 }
 }
 
 
 func (a *App) SaveState() {
 func (a *App) SaveState() {
@@ -304,30 +307,28 @@ func (a *App) SendChatMessage(
 		cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session)))
 		cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session)))
 	}
 	}
 
 
-	optimisticParts := []opencode.MessagePart{{
-		Type: opencode.MessagePartTypeText,
+	optimisticParts := []opencode.UserMessagePart{{
+		Type: opencode.UserMessagePartTypeText,
 		Text: text,
 		Text: text,
 	}}
 	}}
 	if len(attachments) > 0 {
 	if len(attachments) > 0 {
 		for _, attachment := range attachments {
 		for _, attachment := range attachments {
-			optimisticParts = append(optimisticParts, opencode.MessagePart{
-				Type:      opencode.MessagePartTypeFile,
-				Filename:  attachment.Filename.Value,
-				MediaType: attachment.MediaType.Value,
-				URL:       attachment.URL.Value,
+			optimisticParts = append(optimisticParts, opencode.UserMessagePart{
+				Type:     opencode.UserMessagePartTypeFile,
+				Filename: attachment.Filename.Value,
+				Mime:     attachment.Mime.Value,
+				URL:      attachment.URL.Value,
 			})
 			})
 		}
 		}
 	}
 	}
 
 
-	optimisticMessage := opencode.Message{
-		ID:    fmt.Sprintf("optimistic-%d", time.Now().UnixNano()),
-		Role:  opencode.MessageRoleUser,
-		Parts: optimisticParts,
-		Metadata: opencode.MessageMetadata{
-			SessionID: a.Session.ID,
-			Time: opencode.MessageMetadataTime{
-				Created: float64(time.Now().Unix()),
-			},
+	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()),
 		},
 		},
 	}
 	}
 
 
@@ -335,7 +336,7 @@ func (a *App) SendChatMessage(
 	cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: optimisticMessage}))
 	cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: optimisticMessage}))
 
 
 	cmds = append(cmds, func() tea.Msg {
 	cmds = append(cmds, func() tea.Msg {
-		parts := []opencode.MessagePartUnionParam{
+		parts := []opencode.UserMessagePartUnionParam{
 			opencode.TextPartParam{
 			opencode.TextPartParam{
 				Type: opencode.F(opencode.TextPartTypeText),
 				Type: opencode.F(opencode.TextPartTypeText),
 				Text: opencode.F(text),
 				Text: opencode.F(text),
@@ -344,10 +345,10 @@ func (a *App) SendChatMessage(
 		if len(attachments) > 0 {
 		if len(attachments) > 0 {
 			for _, attachment := range attachments {
 			for _, attachment := range attachments {
 				parts = append(parts, opencode.FilePartParam{
 				parts = append(parts, opencode.FilePartParam{
-					MediaType: attachment.MediaType,
-					Type:      attachment.Type,
-					URL:       attachment.URL,
-					Filename:  attachment.Filename,
+					Mime:     attachment.Mime,
+					Type:     attachment.Type,
+					URL:      attachment.URL,
+					Filename: attachment.Filename,
 				})
 				})
 			}
 			}
 		}
 		}

+ 4 - 4
packages/tui/internal/components/chat/editor.go

@@ -248,10 +248,10 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
 	fileParts := make([]opencode.FilePartParam, 0)
 	fileParts := make([]opencode.FilePartParam, 0)
 	for _, attachment := range attachments {
 	for _, attachment := range attachments {
 		fileParts = append(fileParts, opencode.FilePartParam{
 		fileParts = append(fileParts, opencode.FilePartParam{
-			Type:      opencode.F(opencode.FilePartTypeFile),
-			MediaType: opencode.F(attachment.MediaType),
-			URL:       opencode.F(attachment.URL),
-			Filename:  opencode.F(attachment.Filename),
+			Type:     opencode.F(opencode.FilePartTypeFile),
+			Mime:     opencode.F(attachment.MediaType),
+			URL:      opencode.F(attachment.URL),
+			Filename: opencode.F(attachment.Filename),
 		})
 		})
 	}
 	}
 
 

+ 152 - 172
packages/tui/internal/components/chat/message.go

@@ -17,7 +17,6 @@ import (
 	"github.com/sst/opencode/internal/styles"
 	"github.com/sst/opencode/internal/styles"
 	"github.com/sst/opencode/internal/theme"
 	"github.com/sst/opencode/internal/theme"
 	"github.com/sst/opencode/internal/util"
 	"github.com/sst/opencode/internal/util"
-	"github.com/tidwall/gjson"
 	"golang.org/x/text/cases"
 	"golang.org/x/text/cases"
 	"golang.org/x/text/language"
 	"golang.org/x/text/language"
 )
 )
@@ -217,51 +216,50 @@ func renderContentBlock(
 
 
 func renderText(
 func renderText(
 	app *app.App,
 	app *app.App,
-	message opencode.Message,
+	message opencode.MessageUnion,
 	text string,
 	text string,
 	author string,
 	author string,
 	showToolDetails bool,
 	showToolDetails bool,
 	highlight bool,
 	highlight bool,
 	width int,
 	width int,
 	extra string,
 	extra string,
-	toolCalls ...opencode.ToolInvocationPart,
+	toolCalls ...opencode.ToolPart,
 ) string {
 ) string {
 	t := theme.CurrentTheme()
 	t := theme.CurrentTheme()
 
 
-	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] {
-		// don't show the date if it's today
-		timestamp = timestamp[12:]
-	}
-	info := fmt.Sprintf("%s (%s)", author, timestamp)
-	info = styles.NewStyle().Foreground(t.TextMuted()).Render(info)
-
+	var ts time.Time
 	backgroundColor := t.BackgroundPanel()
 	backgroundColor := t.BackgroundPanel()
 	if highlight {
 	if highlight {
 		backgroundColor = t.BackgroundElement()
 		backgroundColor = t.BackgroundElement()
 	}
 	}
 	messageStyle := styles.NewStyle().Background(backgroundColor)
 	messageStyle := styles.NewStyle().Background(backgroundColor)
-	if message.Role == opencode.MessageRoleUser {
+	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)
 		messageStyle = messageStyle.Width(width - 6)
 	}
 	}
 
 
-	content := messageStyle.Render(text)
-	if message.Role == opencode.MessageRoleAssistant {
-		content = util.ToMarkdown(text, width, backgroundColor)
+	timestamp := ts.
+		Local().
+		Format("02 Jan 2006 03:04 PM")
+	if time.Now().Format("02 Jan 2006") == timestamp[:11] {
+		// don't show the date if it's today
+		timestamp = timestamp[12:]
 	}
 	}
+	info := fmt.Sprintf("%s (%s)", author, timestamp)
+	info = styles.NewStyle().Foreground(t.TextMuted()).Render(info)
 
 
 	if !showToolDetails && toolCalls != nil && len(toolCalls) > 0 {
 	if !showToolDetails && toolCalls != nil && len(toolCalls) > 0 {
 		content = content + "\n\n"
 		content = content + "\n\n"
 		for _, toolCall := range toolCalls {
 		for _, toolCall := range toolCalls {
-			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]
-			}
+			title := renderToolTitle(toolCall, width)
 			style := styles.NewStyle()
 			style := styles.NewStyle()
-			if _, ok := metadata.ExtraFields["error"]; ok {
+			if toolCall.State.Status == opencode.ToolPartStateStatusError {
 				style = style.Foreground(t.Error())
 				style = style.Foreground(t.Error())
 			}
 			}
 			title = style.Render(title)
 			title = style.Render(title)
@@ -276,8 +274,8 @@ func renderText(
 	}
 	}
 	content = strings.Join(sections, "\n")
 	content = strings.Join(sections, "\n")
 
 
-	switch message.Role {
-	case opencode.MessageRoleUser:
+	switch message.(type) {
+	case opencode.UserMessage:
 		return renderContentBlock(
 		return renderContentBlock(
 			app,
 			app,
 			content,
 			content,
@@ -286,7 +284,7 @@ func renderText(
 			WithTextColor(t.Text()),
 			WithTextColor(t.Text()),
 			WithBorderColorRight(t.Secondary()),
 			WithBorderColorRight(t.Secondary()),
 		)
 		)
-	case opencode.MessageRoleAssistant:
+	case opencode.AssistantMessage:
 		return renderContentBlock(
 		return renderContentBlock(
 			app,
 			app,
 			content,
 			content,
@@ -300,39 +298,32 @@ func renderText(
 
 
 func renderToolDetails(
 func renderToolDetails(
 	app *app.App,
 	app *app.App,
-	toolCall opencode.ToolInvocationPart,
-	messageMetadata opencode.MessageMetadata,
+	toolCall opencode.ToolPart,
 	highlight bool,
 	highlight bool,
 	width int,
 	width int,
 ) string {
 ) string {
 	ignoredTools := []string{"todoread"}
 	ignoredTools := []string{"todoread"}
-	if slices.Contains(ignoredTools, toolCall.ToolInvocation.ToolName) {
+	if slices.Contains(ignoredTools, toolCall.Tool) {
 		return ""
 		return ""
 	}
 	}
 
 
-	toolCallID := toolCall.ToolInvocation.ToolCallID
-	metadata := opencode.MessageMetadataTool{}
-	if _, ok := messageMetadata.Tool[toolCallID]; ok {
-		metadata = messageMetadata.Tool[toolCallID]
+	if toolCall.State.Status == opencode.ToolPartStateStatusPending || toolCall.State.Status == opencode.ToolPartStateStatusRunning {
+		title := renderToolTitle(toolCall, width)
+		return renderContentBlock(app, title, highlight, width)
 	}
 	}
 
 
 	var result *string
 	var result *string
-	if toolCall.ToolInvocation.Result != "" {
-		result = &toolCall.ToolInvocation.Result
-	}
-
-	if toolCall.ToolInvocation.State == "partial-call" {
-		title := renderToolTitle(toolCall, messageMetadata, width)
-		return renderContentBlock(app, title, highlight, width)
+	if toolCall.State.Output != "" {
+		result = &toolCall.State.Output
 	}
 	}
 
 
-	toolArgsMap := make(map[string]any)
-	if toolCall.ToolInvocation.Args != nil {
-		value := toolCall.ToolInvocation.Args
+	toolInputMap := make(map[string]any)
+	if toolCall.State.Input != nil {
+		value := toolCall.State.Input
 		if m, ok := value.(map[string]any); ok {
 		if m, ok := value.(map[string]any); ok {
-			toolArgsMap = m
-			keys := make([]string, 0, len(toolArgsMap))
-			for key := range toolArgsMap {
+			toolInputMap = m
+			keys := make([]string, 0, len(toolInputMap))
+			for key := range toolInputMap {
 				keys = append(keys, key)
 				keys = append(keys, key)
 			}
 			}
 			slices.Sort(keys)
 			slices.Sort(keys)
@@ -340,7 +331,6 @@ func renderToolDetails(
 	}
 	}
 
 
 	body := ""
 	body := ""
-	finished := result != nil && *result != ""
 	t := theme.CurrentTheme()
 	t := theme.CurrentTheme()
 	backgroundColor := t.BackgroundPanel()
 	backgroundColor := t.BackgroundPanel()
 	borderColor := t.BackgroundPanel()
 	borderColor := t.BackgroundPanel()
@@ -349,137 +339,128 @@ func renderToolDetails(
 		borderColor = t.BorderActive()
 		borderColor = t.BorderActive()
 	}
 	}
 
 
-	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 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))
+			}
+		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
-				}
+					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
+					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.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" {
+		case "bash":
+			stdout := metadata["stdout"]
+			if stdout != nil {
+				command := toolInputMap["command"].(string)
+				body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout)
 				body = util.ToMarkdown(body, width, backgroundColor)
 				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 "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)
 				}
 				}
 			}
 			}
-			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)
+		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)
 						step = "∟ " + step
 						step = "∟ " + step
 						steps = append(steps, step)
 						steps = append(steps, step)
 					}
 					}
 				}
 				}
+				body = strings.Join(steps, "\n")
 			}
 			}
-			body = strings.Join(steps, "\n")
-		}
-	default:
-		if result == nil {
-			empty := ""
-			result = &empty
+		default:
+			if result == nil {
+				empty := ""
+				result = &empty
+			}
+			body = *result
+			body = util.TruncateHeight(body, 10)
 		}
 		}
-		body = *result
-		body = util.TruncateHeight(body, 10)
 	}
 	}
 
 
 	error := ""
 	error := ""
-	if err, ok := metadata.ExtraFields["error"].(bool); ok && err {
-		if message, ok := metadata.ExtraFields["message"].(string); ok {
-			error = message
-		}
+	if toolCall.State.Status == opencode.ToolPartStateStatusError {
+		error = toolCall.State.Error
 	}
 	}
 
 
 	if error != "" {
 	if error != "" {
@@ -494,7 +475,7 @@ func renderToolDetails(
 		body = util.TruncateHeight(body, 10)
 		body = util.TruncateHeight(body, 10)
 	}
 	}
 
 
-	title := renderToolTitle(toolCall, messageMetadata, width)
+	title := renderToolTitle(toolCall, width)
 	content := title + "\n\n" + body
 	content := title + "\n\n" + body
 	return renderContentBlock(app, content, highlight, width, WithBorderColor(borderColor))
 	return renderContentBlock(app, content, highlight, width, WithBorderColor(borderColor))
 }
 }
@@ -515,20 +496,19 @@ func renderToolName(name string) string {
 }
 }
 
 
 func renderToolTitle(
 func renderToolTitle(
-	toolCall opencode.ToolInvocationPart,
-	messageMetadata opencode.MessageMetadata,
+	toolCall opencode.ToolPart,
 	width int,
 	width int,
 ) string {
 ) string {
 	// TODO: handle truncate to width
 	// TODO: handle truncate to width
 
 
-	if toolCall.ToolInvocation.State == "partial-call" {
-		return renderToolAction(toolCall.ToolInvocation.ToolName)
+	if toolCall.State.Status == opencode.ToolPartStateStatusPending {
+		return renderToolAction(toolCall.Tool)
 	}
 	}
 
 
 	toolArgs := ""
 	toolArgs := ""
 	toolArgsMap := make(map[string]any)
 	toolArgsMap := make(map[string]any)
-	if toolCall.ToolInvocation.Args != nil {
-		value := toolCall.ToolInvocation.Args
+	if toolCall.State.Input != nil {
+		value := toolCall.State.Input
 		if m, ok := value.(map[string]any); ok {
 		if m, ok := value.(map[string]any); ok {
 			toolArgsMap = m
 			toolArgsMap = m
 
 
@@ -546,8 +526,8 @@ func renderToolTitle(
 		}
 		}
 	}
 	}
 
 
-	title := renderToolName(toolCall.ToolInvocation.ToolName)
-	switch toolCall.ToolInvocation.ToolName {
+	title := renderToolName(toolCall.Tool)
+	switch toolCall.Tool {
 	case "read":
 	case "read":
 		toolArgs = renderArgs(&toolArgsMap, "filePath")
 		toolArgs = renderArgs(&toolArgsMap, "filePath")
 		title = fmt.Sprintf("%s %s", title, toolArgs)
 		title = fmt.Sprintf("%s %s", title, toolArgs)
@@ -565,7 +545,7 @@ func renderToolTitle(
 	case "todowrite", "todoread":
 	case "todowrite", "todoread":
 		// title is just the tool name
 		// title is just the tool name
 	default:
 	default:
-		toolName := renderToolName(toolCall.ToolInvocation.ToolName)
+		toolName := renderToolName(toolCall.Tool)
 		title = fmt.Sprintf("%s %s", toolName, toolArgs)
 		title = fmt.Sprintf("%s %s", toolName, toolArgs)
 	}
 	}
 	return title
 	return title
@@ -645,8 +625,8 @@ type Diagnostic struct {
 }
 }
 
 
 // renderDiagnostics formats LSP diagnostics for display in the TUI
 // renderDiagnostics formats LSP diagnostics for display in the TUI
-func renderDiagnostics(metadata opencode.MessageMetadataTool, filePath string) string {
-	if diagnosticsData, ok := metadata.ExtraFields["diagnostics"].(map[string]any); ok {
+func renderDiagnostics(metadata map[string]any, filePath string) string {
+	if diagnosticsData, ok := metadata["diagnostics"].(map[string]any); ok {
 		if fileDiagnostics, ok := diagnosticsData[filePath].([]any); ok {
 		if fileDiagnostics, ok := diagnosticsData[filePath].([]any); ok {
 			var errorDiagnostics []string
 			var errorDiagnostics []string
 			for _, diagInterface := range fileDiagnostics {
 			for _, diagInterface := range fileDiagnostics {

+ 33 - 33
packages/tui/internal/components/chat/messages.go

@@ -99,7 +99,7 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			}
 			}
 		}
 		}
 	case opencode.EventListResponseEventMessageUpdated:
 	case opencode.EventListResponseEventMessageUpdated:
-		if msg.Properties.Info.Metadata.SessionID == m.app.Session.ID {
+		if msg.Properties.Info.SessionID == m.app.Session.ID {
 			m.renderView(m.width)
 			m.renderView(m.width)
 			if m.tail {
 			if m.tail {
 				m.viewport.GotoBottom()
 				m.viewport.GotoBottom()
@@ -124,19 +124,19 @@ func (m *messagesComponent) renderView(width int) {
 	m.partCount = 0
 	m.partCount = 0
 	m.lineCount = 0
 	m.lineCount = 0
 
 
-	orphanedToolCalls := make([]opencode.ToolInvocationPart, 0)
+	orphanedToolCalls := make([]opencode.ToolPart, 0)
 
 
 	for _, message := range m.app.Messages {
 	for _, message := range m.app.Messages {
 		var content string
 		var content string
 		var cached bool
 		var cached bool
 
 
-		switch message.Role {
-		case opencode.MessageRoleUser:
+		switch casted := message.(type) {
+		case opencode.UserMessage:
 		userLoop:
 		userLoop:
-			for partIndex, part := range message.Parts {
+			for partIndex, part := range casted.Parts {
 				switch part := part.AsUnion().(type) {
 				switch part := part.AsUnion().(type) {
 				case opencode.TextPart:
 				case opencode.TextPart:
-					remainingParts := message.Parts[partIndex+1:]
+					remainingParts := casted.Parts[partIndex+1:]
 					fileParts := make([]opencode.FilePart, 0)
 					fileParts := make([]opencode.FilePart, 0)
 					for _, part := range remainingParts {
 					for _, part := range remainingParts {
 						switch part := part.AsUnion().(type) {
 						switch part := part.AsUnion().(type) {
@@ -150,7 +150,7 @@ func (m *messagesComponent) renderView(width int) {
 						mediaTypeStyle := styles.NewStyle().Background(t.Secondary()).Foreground(t.BackgroundPanel()).Padding(0, 1)
 						mediaTypeStyle := styles.NewStyle().Background(t.Secondary()).Foreground(t.BackgroundPanel()).Padding(0, 1)
 						for _, filePart := range fileParts {
 						for _, filePart := range fileParts {
 							mediaType := ""
 							mediaType := ""
-							switch filePart.MediaType {
+							switch filePart.Mime {
 							case "text/plain":
 							case "text/plain":
 								mediaType = "txt"
 								mediaType = "txt"
 							case "image/png", "image/jpeg", "image/gif", "image/webp":
 							case "image/png", "image/jpeg", "image/gif", "image/webp":
@@ -175,7 +175,7 @@ func (m *messagesComponent) renderView(width int) {
 						flexItems...,
 						flexItems...,
 					)
 					)
 
 
-					key := m.cache.GenerateKey(message.ID, part.Text, width, m.selectedPart == m.partCount, files)
+					key := m.cache.GenerateKey(casted.ID, part.Text, width, m.selectedPart == m.partCount, files)
 					content, cached = m.cache.Get(key)
 					content, cached = m.cache.Get(key)
 					if !cached {
 					if !cached {
 						content = renderText(
 						content = renderText(
@@ -199,21 +199,21 @@ func (m *messagesComponent) renderView(width int) {
 				}
 				}
 			}
 			}
 
 
-		case opencode.MessageRoleAssistant:
+		case opencode.AssistantMessage:
 			hasTextPart := false
 			hasTextPart := false
-			for partIndex, p := range message.Parts {
+			for partIndex, p := range casted.Parts {
 				switch part := p.AsUnion().(type) {
 				switch part := p.AsUnion().(type) {
 				case opencode.TextPart:
 				case opencode.TextPart:
 					hasTextPart = true
 					hasTextPart = true
-					finished := message.Metadata.Time.Completed > 0
-					remainingParts := message.Parts[partIndex+1:]
-					toolCallParts := make([]opencode.ToolInvocationPart, 0)
+					finished := casted.Time.Completed > 0
+					remainingParts := casted.Parts[partIndex+1:]
+					toolCallParts := make([]opencode.ToolPart, 0)
 
 
 					// sometimes tool calls happen without an assistant message
 					// sometimes tool calls happen without an assistant message
 					// these should be included in this assistant message as well
 					// these should be included in this assistant message as well
 					if len(orphanedToolCalls) > 0 {
 					if len(orphanedToolCalls) > 0 {
 						toolCallParts = append(toolCallParts, orphanedToolCalls...)
 						toolCallParts = append(toolCallParts, orphanedToolCalls...)
-						orphanedToolCalls = make([]opencode.ToolInvocationPart, 0)
+						orphanedToolCalls = make([]opencode.ToolPart, 0)
 					}
 					}
 
 
 					remaining := true
 					remaining := true
@@ -226,9 +226,9 @@ func (m *messagesComponent) renderView(width int) {
 							// we only want tool calls associated with the current text part.
 							// we only want tool calls associated with the current text part.
 							// if we hit another text part, we're done.
 							// if we hit another text part, we're done.
 							remaining = false
 							remaining = false
-						case opencode.ToolInvocationPart:
+						case opencode.ToolPart:
 							toolCallParts = append(toolCallParts, part)
 							toolCallParts = append(toolCallParts, part)
-							if part.ToolInvocation.State != "result" {
+							if part.State.Status != opencode.ToolPartStateStatusCompleted || part.State.Status != opencode.ToolPartStateStatusError {
 								// i don't think there's a case where a tool call isn't in result state
 								// 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
 								// and the message time is 0, but just in case
 								finished = false
 								finished = false
@@ -237,14 +237,14 @@ func (m *messagesComponent) renderView(width int) {
 					}
 					}
 
 
 					if finished {
 					if finished {
-						key := m.cache.GenerateKey(message.ID, p.Text, width, m.showToolDetails, m.selectedPart == m.partCount)
+						key := m.cache.GenerateKey(casted.ID, p.Text, width, m.showToolDetails, m.selectedPart == m.partCount)
 						content, cached = m.cache.Get(key)
 						content, cached = m.cache.Get(key)
 						if !cached {
 						if !cached {
 							content = renderText(
 							content = renderText(
 								m.app,
 								m.app,
 								message,
 								message,
 								p.Text,
 								p.Text,
-								message.Metadata.Assistant.ModelID,
+								casted.ModelID,
 								m.showToolDetails,
 								m.showToolDetails,
 								m.partCount == m.selectedPart,
 								m.partCount == m.selectedPart,
 								width,
 								width,
@@ -258,7 +258,7 @@ func (m *messagesComponent) renderView(width int) {
 							m.app,
 							m.app,
 							message,
 							message,
 							p.Text,
 							p.Text,
-							message.Metadata.Assistant.ModelID,
+							casted.ModelID,
 							m.showToolDetails,
 							m.showToolDetails,
 							m.partCount == m.selectedPart,
 							m.partCount == m.selectedPart,
 							width,
 							width,
@@ -270,7 +270,7 @@ func (m *messagesComponent) renderView(width int) {
 						m = m.updateSelected(content, p.Text)
 						m = m.updateSelected(content, p.Text)
 						blocks = append(blocks, content)
 						blocks = append(blocks, content)
 					}
 					}
-				case opencode.ToolInvocationPart:
+				case opencode.ToolPart:
 					if !m.showToolDetails {
 					if !m.showToolDetails {
 						if !hasTextPart {
 						if !hasTextPart {
 							orphanedToolCalls = append(orphanedToolCalls, part)
 							orphanedToolCalls = append(orphanedToolCalls, part)
@@ -278,9 +278,9 @@ func (m *messagesComponent) renderView(width int) {
 						continue
 						continue
 					}
 					}
 
 
-					if part.ToolInvocation.State == "result" {
-						key := m.cache.GenerateKey(message.ID,
-							part.ToolInvocation.ToolCallID,
+					if part.State.Status == opencode.ToolPartStateStatusCompleted || part.State.Status == opencode.ToolPartStateStatusError {
+						key := m.cache.GenerateKey(casted.ID,
+							part.ID,
 							m.showToolDetails,
 							m.showToolDetails,
 							width,
 							width,
 							m.partCount == m.selectedPart,
 							m.partCount == m.selectedPart,
@@ -290,7 +290,6 @@ func (m *messagesComponent) renderView(width int) {
 							content = renderToolDetails(
 							content = renderToolDetails(
 								m.app,
 								m.app,
 								part,
 								part,
-								message.Metadata,
 								m.partCount == m.selectedPart,
 								m.partCount == m.selectedPart,
 								width,
 								width,
 							)
 							)
@@ -301,7 +300,6 @@ func (m *messagesComponent) renderView(width int) {
 						content = renderToolDetails(
 						content = renderToolDetails(
 							m.app,
 							m.app,
 							part,
 							part,
-							message.Metadata,
 							m.partCount == m.selectedPart,
 							m.partCount == m.selectedPart,
 							width,
 							width,
 						)
 						)
@@ -315,14 +313,16 @@ func (m *messagesComponent) renderView(width int) {
 		}
 		}
 
 
 		error := ""
 		error := ""
-		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 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.UnknownError:
+				error = err.Data.Message
+			}
 		}
 		}
 
 
 		if error != "" {
 		if error != "" {

+ 14 - 11
packages/tui/internal/components/status/status.go

@@ -6,6 +6,7 @@ import (
 
 
 	tea "github.com/charmbracelet/bubbletea/v2"
 	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/lipgloss/v2"
+	"github.com/sst/opencode-sdk-go"
 	"github.com/sst/opencode/internal/app"
 	"github.com/sst/opencode/internal/app"
 	"github.com/sst/opencode/internal/styles"
 	"github.com/sst/opencode/internal/styles"
 	"github.com/sst/opencode/internal/theme"
 	"github.com/sst/opencode/internal/theme"
@@ -101,18 +102,20 @@ func (m statusComponent) View() string {
 		contextWindow := m.app.Model.Limit.Context
 		contextWindow := m.app.Model.Limit.Context
 
 
 		for _, message := range m.app.Messages {
 		for _, message := range m.app.Messages {
-			cost += message.Metadata.Assistant.Cost
-			usage := message.Metadata.Assistant.Tokens
-			if usage.Output > 0 {
-				if message.Metadata.Assistant.Summary {
-					tokens = usage.Output
-					continue
+			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)
 				}
 				}
-				tokens = (usage.Input +
-					usage.Cache.Write +
-					usage.Cache.Read +
-					usage.Output +
-					usage.Reasoning)
 			}
 			}
 		}
 		}
 
 

+ 26 - 13
packages/tui/internal/tui/tui.go

@@ -363,7 +363,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case opencode.EventListResponseEventSessionDeleted:
 	case opencode.EventListResponseEventSessionDeleted:
 		if a.app.Session != nil && msg.Properties.Info.ID == a.app.Session.ID {
 		if a.app.Session != nil && msg.Properties.Info.ID == a.app.Session.ID {
 			a.app.Session = &opencode.Session{}
 			a.app.Session = &opencode.Session{}
-			a.app.Messages = []opencode.Message{}
+			a.app.Messages = []opencode.MessageUnion{}
 		}
 		}
 		return a, toast.NewSuccessToast("Session deleted successfully")
 		return a, toast.NewSuccessToast("Session deleted successfully")
 	case opencode.EventListResponseEventSessionUpdated:
 	case opencode.EventListResponseEventSessionUpdated:
@@ -371,7 +371,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			a.app.Session = &msg.Properties.Info
 			a.app.Session = &msg.Properties.Info
 		}
 		}
 	case opencode.EventListResponseEventMessageUpdated:
 	case opencode.EventListResponseEventMessageUpdated:
-		if msg.Properties.Info.Metadata.SessionID == a.app.Session.ID {
+		if msg.Properties.Info.SessionID == a.app.Session.ID {
 			exists := false
 			exists := false
 			optimisticReplaced := false
 			optimisticReplaced := false
 
 
@@ -379,12 +379,15 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			if msg.Properties.Info.Role == opencode.MessageRoleUser {
 			if msg.Properties.Info.Role == opencode.MessageRoleUser {
 				// Look for optimistic messages to replace
 				// Look for optimistic messages to replace
 				for i, m := range a.app.Messages {
 				for i, m := range a.app.Messages {
-					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
+					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
+						}
 					}
 					}
 				}
 				}
 			}
 			}
@@ -392,8 +395,15 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			// If not replacing optimistic, check for existing message with same ID
 			// If not replacing optimistic, check for existing message with same ID
 			if !optimisticReplaced {
 			if !optimisticReplaced {
 				for i, m := range a.app.Messages {
 				for i, m := range a.app.Messages {
-					if m.ID == msg.Properties.Info.ID {
-						a.app.Messages[i] = msg.Properties.Info
+					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()
 						exists = true
 						exists = true
 						break
 						break
 					}
 					}
@@ -401,7 +411,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			}
 			}
 
 
 			if !exists {
 			if !exists {
-				a.app.Messages = append(a.app.Messages, msg.Properties.Info)
+				a.app.Messages = append(a.app.Messages, msg.Properties.Info.AsUnion())
 			}
 			}
 		}
 		}
 	case opencode.EventListResponseEventSessionError:
 	case opencode.EventListResponseEventSessionError:
@@ -462,7 +472,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			return a, toast.NewErrorToast("Failed to open session")
 			return a, toast.NewErrorToast("Failed to open session")
 		}
 		}
 		a.app.Session = msg
 		a.app.Session = msg
-		a.app.Messages = messages
+		a.app.Messages = make([]opencode.MessageUnion, 0)
+		for _, message := range messages {
+			a.app.Messages = append(a.app.Messages, message.AsUnion())
+		}
 		return a, util.CmdHandler(app.SessionLoadedMsg{})
 		return a, util.CmdHandler(app.SessionLoadedMsg{})
 	case app.ModelSelectedMsg:
 	case app.ModelSelectedMsg:
 		a.app.Provider = &msg.Provider
 		a.app.Provider = &msg.Provider
@@ -813,7 +826,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
 			return a, nil
 			return a, nil
 		}
 		}
 		a.app.Session = &opencode.Session{}
 		a.app.Session = &opencode.Session{}
-		a.app.Messages = []opencode.Message{}
+		a.app.Messages = []opencode.MessageUnion{}
 		cmds = append(cmds, util.CmdHandler(app.SessionClearedMsg{}))
 		cmds = append(cmds, util.CmdHandler(app.SessionClearedMsg{}))
 	case commands.SessionListCommand:
 	case commands.SessionListCommand:
 		sessionDialog := dialog.NewSessionDialog(a.app)
 		sessionDialog := dialog.NewSessionDialog(a.app)

+ 7 - 7
packages/tui/sdk/.github/workflows/ci.yml

@@ -2,15 +2,15 @@ name: CI
 on:
 on:
   push:
   push:
     branches-ignore:
     branches-ignore:
-      - 'generated'
-      - 'codegen/**'
-      - 'integrated/**'
-      - 'stl-preview-head/**'
-      - 'stl-preview-base/**'
+      - "generated"
+      - "codegen/**"
+      - "integrated/**"
+      - "stl-preview-head/**"
+      - "stl-preview-base/**"
   pull_request:
   pull_request:
     branches-ignore:
     branches-ignore:
-      - 'stl-preview-head/**'
-      - 'stl-preview-base/**'
+      - "stl-preview-head/**"
+      - "stl-preview-base/**"
 
 
 jobs:
 jobs:
   lint:
   lint:

+ 1 - 1
packages/tui/sdk/.release-please-manifest.json

@@ -1,3 +1,3 @@
 {
 {
   ".": "0.1.0-alpha.8"
   ".": "0.1.0-alpha.8"
-}
+}

+ 3 - 3
packages/tui/sdk/.stats.yml

@@ -1,4 +1,4 @@
 configured_endpoints: 20
 configured_endpoints: 20
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-945f9da9e9a4c4008834deef63e4346c0076e020eed3d3c98c249095033c1ac5.yml
-openapi_spec_hash: 522a44f6cb0677435fe2ac7693848ad7
-config_hash: 6c8822d278ba83456e5eed6d774ca230
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-15eeb028f79b9a065b4e54a6ea6a58631e9bd5004f97820f0c79d18e3f8bac84.yml
+openapi_spec_hash: 38c8bacb6c8e4c46852a3e81e3fb9fda
+config_hash: e03e9d1aad76081fa1163086e89f201b

+ 12 - 13
packages/tui/sdk/CHANGELOG.md

@@ -6,7 +6,7 @@ Full Changelog: [v0.1.0-alpha.7...v0.1.0-alpha.8](https://github.com/sst/opencod
 
 
 ### Features
 ### Features
 
 
-* **api:** update via SDK Studio ([651e937](https://github.com/sst/opencode-sdk-go/commit/651e937c334e1caba3b968e6cac865c219879519))
+- **api:** update via SDK Studio ([651e937](https://github.com/sst/opencode-sdk-go/commit/651e937c334e1caba3b968e6cac865c219879519))
 
 
 ## 0.1.0-alpha.7 (2025-06-30)
 ## 0.1.0-alpha.7 (2025-06-30)
 
 
@@ -14,13 +14,12 @@ Full Changelog: [v0.1.0-alpha.6...v0.1.0-alpha.7](https://github.com/sst/opencod
 
 
 ### Features
 ### Features
 
 
-* **api:** update via SDK Studio ([13550a5](https://github.com/sst/opencode-sdk-go/commit/13550a5c65d77325e945ed99fe0799cd1107b775))
-* **api:** update via SDK Studio ([7b73730](https://github.com/sst/opencode-sdk-go/commit/7b73730c7fa62ba966dda3541c3e97b49be8d2bf))
-
+- **api:** update via SDK Studio ([13550a5](https://github.com/sst/opencode-sdk-go/commit/13550a5c65d77325e945ed99fe0799cd1107b775))
+- **api:** update via SDK Studio ([7b73730](https://github.com/sst/opencode-sdk-go/commit/7b73730c7fa62ba966dda3541c3e97b49be8d2bf))
 
 
 ### Chores
 ### Chores
 
 
-* **ci:** only run for pushes and fork pull requests ([bea59b8](https://github.com/sst/opencode-sdk-go/commit/bea59b886800ef555f89c47a9256d6392ed2e53d))
+- **ci:** only run for pushes and fork pull requests ([bea59b8](https://github.com/sst/opencode-sdk-go/commit/bea59b886800ef555f89c47a9256d6392ed2e53d))
 
 
 ## 0.1.0-alpha.6 (2025-06-28)
 ## 0.1.0-alpha.6 (2025-06-28)
 
 
@@ -28,7 +27,7 @@ Full Changelog: [v0.1.0-alpha.5...v0.1.0-alpha.6](https://github.com/sst/opencod
 
 
 ### Bug Fixes
 ### Bug Fixes
 
 
-* don't try to deserialize as json when ResponseBodyInto is []byte ([5988d04](https://github.com/sst/opencode-sdk-go/commit/5988d04839cb78b6613057280b91b72a60fef33d))
+- don't try to deserialize as json when ResponseBodyInto is []byte ([5988d04](https://github.com/sst/opencode-sdk-go/commit/5988d04839cb78b6613057280b91b72a60fef33d))
 
 
 ## 0.1.0-alpha.5 (2025-06-27)
 ## 0.1.0-alpha.5 (2025-06-27)
 
 
@@ -36,7 +35,7 @@ Full Changelog: [v0.1.0-alpha.4...v0.1.0-alpha.5](https://github.com/sst/opencod
 
 
 ### Features
 ### Features
 
 
-* **api:** update via SDK Studio ([9e39a59](https://github.com/sst/opencode-sdk-go/commit/9e39a59b3d5d1bd5e64633732521fb28362cc70e))
+- **api:** update via SDK Studio ([9e39a59](https://github.com/sst/opencode-sdk-go/commit/9e39a59b3d5d1bd5e64633732521fb28362cc70e))
 
 
 ## 0.1.0-alpha.4 (2025-06-27)
 ## 0.1.0-alpha.4 (2025-06-27)
 
 
@@ -44,7 +43,7 @@ Full Changelog: [v0.1.0-alpha.3...v0.1.0-alpha.4](https://github.com/sst/opencod
 
 
 ### Features
 ### Features
 
 
-* **api:** update via SDK Studio ([9609d1b](https://github.com/sst/opencode-sdk-go/commit/9609d1b1db7806d00cb846c9914cb4935cdedf52))
+- **api:** update via SDK Studio ([9609d1b](https://github.com/sst/opencode-sdk-go/commit/9609d1b1db7806d00cb846c9914cb4935cdedf52))
 
 
 ## 0.1.0-alpha.3 (2025-06-27)
 ## 0.1.0-alpha.3 (2025-06-27)
 
 
@@ -52,7 +51,7 @@ Full Changelog: [v0.1.0-alpha.2...v0.1.0-alpha.3](https://github.com/sst/opencod
 
 
 ### Features
 ### Features
 
 
-* **api:** update via SDK Studio ([57f3230](https://github.com/sst/opencode-sdk-go/commit/57f32309023cc1f0f20c20d02a3907e390a71f61))
+- **api:** update via SDK Studio ([57f3230](https://github.com/sst/opencode-sdk-go/commit/57f32309023cc1f0f20c20d02a3907e390a71f61))
 
 
 ## 0.1.0-alpha.2 (2025-06-27)
 ## 0.1.0-alpha.2 (2025-06-27)
 
 
@@ -60,7 +59,7 @@ Full Changelog: [v0.1.0-alpha.1...v0.1.0-alpha.2](https://github.com/sst/opencod
 
 
 ### Features
 ### Features
 
 
-* **api:** update via SDK Studio ([a766f1c](https://github.com/sst/opencode-sdk-go/commit/a766f1c54f02bbc1380151b0e22d97cc2c5892e6))
+- **api:** update via SDK Studio ([a766f1c](https://github.com/sst/opencode-sdk-go/commit/a766f1c54f02bbc1380151b0e22d97cc2c5892e6))
 
 
 ## 0.1.0-alpha.1 (2025-06-27)
 ## 0.1.0-alpha.1 (2025-06-27)
 
 
@@ -68,6 +67,6 @@ Full Changelog: [v0.0.1-alpha.0...v0.1.0-alpha.1](https://github.com/sst/opencod
 
 
 ### Features
 ### Features
 
 
-* **api:** update via SDK Studio ([27b7376](https://github.com/sst/opencode-sdk-go/commit/27b7376310466ee17a63f2104f546b53a2b8361a))
-* **api:** update via SDK Studio ([0a73e04](https://github.com/sst/opencode-sdk-go/commit/0a73e04c23c90b2061611edaa8fd6282dc0ce397))
-* **api:** update via SDK Studio ([9b7883a](https://github.com/sst/opencode-sdk-go/commit/9b7883a144eeac526d9d04538e0876a9d18bb844))
+- **api:** update via SDK Studio ([27b7376](https://github.com/sst/opencode-sdk-go/commit/27b7376310466ee17a63f2104f546b53a2b8361a))
+- **api:** update via SDK Studio ([0a73e04](https://github.com/sst/opencode-sdk-go/commit/0a73e04c23c90b2061611edaa8fd6282dc0ce397))
+- **api:** update via SDK Studio ([9b7883a](https://github.com/sst/opencode-sdk-go/commit/9b7883a144eeac526d9d04538e0876a9d18bb844))

+ 11 - 16
packages/tui/sdk/api.md

@@ -71,30 +71,25 @@ Methods:
 Params Types:
 Params Types:
 
 
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartParam">FilePartParam</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartParam">FilePartParam</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#MessagePartUnionParam">MessagePartUnionParam</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ReasoningPartParam">ReasoningPartParam</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SourceURLPartParam">SourceURLPartParam</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#StepStartPartParam">StepStartPartParam</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TextPartParam">TextPartParam</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TextPartParam">TextPartParam</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolCallParam">ToolCallParam</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolInvocationPartParam">ToolInvocationPartParam</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolPartialCallParam">ToolPartialCallParam</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolResultParam">ToolResultParam</a>
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#UserMessagePartUnionParam">UserMessagePartUnionParam</a>
 
 
 Response Types:
 Response Types:
 
 
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessage">AssistantMessage</a>
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessagePart">AssistantMessagePart</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePart">FilePart</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePart">FilePart</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Message">Message</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Message">Message</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#MessagePart">MessagePart</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ReasoningPart">ReasoningPart</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SourceURLPart">SourceURLPart</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#StepStartPart">StepStartPart</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#StepStartPart">StepStartPart</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TextPart">TextPart</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TextPart">TextPart</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolCall">ToolCall</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolInvocationPart">ToolInvocationPart</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolPartialCall">ToolPartialCall</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolResult">ToolResult</a>
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolPart">ToolPart</a>
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateCompleted">ToolStateCompleted</a>
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateError">ToolStateError</a>
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStatePending">ToolStatePending</a>
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateRunning">ToolStateRunning</a>
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#UserMessage">UserMessage</a>
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#UserMessagePart">UserMessagePart</a>
 
 
 Methods:
 Methods:
 
 
@@ -102,7 +97,7 @@ Methods:
 - <code title="get /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="get /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="delete /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Delete">Delete</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="delete /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Delete">Delete</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="post /session/{id}/abort">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Abort">Abort</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="post /session/{id}/abort">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Abort">Abort</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
-- <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatParams">SessionChatParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Message">Message</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+- <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatParams">SessionChatParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessage">AssistantMessage</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="post /session/{id}/init">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionInitParams">SessionInitParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="post /session/{id}/init">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionInitParams">SessionInitParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="get /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Messages">Messages</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Message">Message</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="get /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Messages">Messages</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Message">Message</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="post /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Share">Share</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="post /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Share">Share</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>

+ 60 - 60
packages/tui/sdk/event.go

@@ -51,8 +51,8 @@ type EventListResponse struct {
 	// [EventListResponseEventLspClientDiagnosticsProperties],
 	// [EventListResponseEventLspClientDiagnosticsProperties],
 	// [EventListResponseEventPermissionUpdatedProperties],
 	// [EventListResponseEventPermissionUpdatedProperties],
 	// [EventListResponseEventFileEditedProperties],
 	// [EventListResponseEventFileEditedProperties],
-	// [EventListResponseEventStorageWriteProperties],
 	// [EventListResponseEventInstallationUpdatedProperties],
 	// [EventListResponseEventInstallationUpdatedProperties],
+	// [EventListResponseEventStorageWriteProperties],
 	// [EventListResponseEventMessageUpdatedProperties],
 	// [EventListResponseEventMessageUpdatedProperties],
 	// [EventListResponseEventMessageRemovedProperties],
 	// [EventListResponseEventMessageRemovedProperties],
 	// [EventListResponseEventMessagePartUpdatedProperties],
 	// [EventListResponseEventMessagePartUpdatedProperties],
@@ -95,9 +95,9 @@ func (r *EventListResponse) UnmarshalJSON(data []byte) (err error) {
 // Possible runtime types of the union are
 // Possible runtime types of the union are
 // [EventListResponseEventLspClientDiagnostics],
 // [EventListResponseEventLspClientDiagnostics],
 // [EventListResponseEventPermissionUpdated], [EventListResponseEventFileEdited],
 // [EventListResponseEventPermissionUpdated], [EventListResponseEventFileEdited],
-// [EventListResponseEventStorageWrite],
 // [EventListResponseEventInstallationUpdated],
 // [EventListResponseEventInstallationUpdated],
-// [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved],
+// [EventListResponseEventStorageWrite], [EventListResponseEventMessageUpdated],
+// [EventListResponseEventMessageRemoved],
 // [EventListResponseEventMessagePartUpdated],
 // [EventListResponseEventMessagePartUpdated],
 // [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
 // [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
 // [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
 // [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
@@ -108,9 +108,9 @@ func (r EventListResponse) AsUnion() EventListResponseUnion {
 
 
 // Union satisfied by [EventListResponseEventLspClientDiagnostics],
 // Union satisfied by [EventListResponseEventLspClientDiagnostics],
 // [EventListResponseEventPermissionUpdated], [EventListResponseEventFileEdited],
 // [EventListResponseEventPermissionUpdated], [EventListResponseEventFileEdited],
-// [EventListResponseEventStorageWrite],
 // [EventListResponseEventInstallationUpdated],
 // [EventListResponseEventInstallationUpdated],
-// [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved],
+// [EventListResponseEventStorageWrite], [EventListResponseEventMessageUpdated],
+// [EventListResponseEventMessageRemoved],
 // [EventListResponseEventMessagePartUpdated],
 // [EventListResponseEventMessagePartUpdated],
 // [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
 // [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
 // [EventListResponseEventSessionIdle], [EventListResponseEventSessionError] or
 // [EventListResponseEventSessionIdle], [EventListResponseEventSessionError] or
@@ -140,13 +140,13 @@ func init() {
 		},
 		},
 		apijson.UnionVariant{
 		apijson.UnionVariant{
 			TypeFilter:         gjson.JSON,
 			TypeFilter:         gjson.JSON,
-			Type:               reflect.TypeOf(EventListResponseEventStorageWrite{}),
-			DiscriminatorValue: "storage.write",
+			Type:               reflect.TypeOf(EventListResponseEventInstallationUpdated{}),
+			DiscriminatorValue: "installation.updated",
 		},
 		},
 		apijson.UnionVariant{
 		apijson.UnionVariant{
 			TypeFilter:         gjson.JSON,
 			TypeFilter:         gjson.JSON,
-			Type:               reflect.TypeOf(EventListResponseEventInstallationUpdated{}),
-			DiscriminatorValue: "installation.updated",
+			Type:               reflect.TypeOf(EventListResponseEventStorageWrite{}),
+			DiscriminatorValue: "storage.write",
 		},
 		},
 		apijson.UnionVariant{
 		apijson.UnionVariant{
 			TypeFilter:         gjson.JSON,
 			TypeFilter:         gjson.JSON,
@@ -402,123 +402,123 @@ func (r EventListResponseEventFileEditedType) IsKnown() bool {
 	return false
 	return false
 }
 }
 
 
-type EventListResponseEventStorageWrite struct {
-	Properties EventListResponseEventStorageWriteProperties `json:"properties,required"`
-	Type       EventListResponseEventStorageWriteType       `json:"type,required"`
-	JSON       eventListResponseEventStorageWriteJSON       `json:"-"`
+type EventListResponseEventInstallationUpdated struct {
+	Properties EventListResponseEventInstallationUpdatedProperties `json:"properties,required"`
+	Type       EventListResponseEventInstallationUpdatedType       `json:"type,required"`
+	JSON       eventListResponseEventInstallationUpdatedJSON       `json:"-"`
 }
 }
 
 
-// eventListResponseEventStorageWriteJSON contains the JSON metadata for the struct
-// [EventListResponseEventStorageWrite]
-type eventListResponseEventStorageWriteJSON struct {
+// eventListResponseEventInstallationUpdatedJSON contains the JSON metadata for the
+// struct [EventListResponseEventInstallationUpdated]
+type eventListResponseEventInstallationUpdatedJSON struct {
 	Properties  apijson.Field
 	Properties  apijson.Field
 	Type        apijson.Field
 	Type        apijson.Field
 	raw         string
 	raw         string
 	ExtraFields map[string]apijson.Field
 	ExtraFields map[string]apijson.Field
 }
 }
 
 
-func (r *EventListResponseEventStorageWrite) UnmarshalJSON(data []byte) (err error) {
+func (r *EventListResponseEventInstallationUpdated) UnmarshalJSON(data []byte) (err error) {
 	return apijson.UnmarshalRoot(data, r)
 	return apijson.UnmarshalRoot(data, r)
 }
 }
 
 
-func (r eventListResponseEventStorageWriteJSON) RawJSON() string {
+func (r eventListResponseEventInstallationUpdatedJSON) RawJSON() string {
 	return r.raw
 	return r.raw
 }
 }
 
 
-func (r EventListResponseEventStorageWrite) implementsEventListResponse() {}
+func (r EventListResponseEventInstallationUpdated) implementsEventListResponse() {}
 
 
-type EventListResponseEventStorageWriteProperties struct {
-	Key     string                                           `json:"key,required"`
-	Content interface{}                                      `json:"content"`
-	JSON    eventListResponseEventStorageWritePropertiesJSON `json:"-"`
+type EventListResponseEventInstallationUpdatedProperties struct {
+	Version string                                                  `json:"version,required"`
+	JSON    eventListResponseEventInstallationUpdatedPropertiesJSON `json:"-"`
 }
 }
 
 
-// eventListResponseEventStorageWritePropertiesJSON contains the JSON metadata for
-// the struct [EventListResponseEventStorageWriteProperties]
-type eventListResponseEventStorageWritePropertiesJSON struct {
-	Key         apijson.Field
-	Content     apijson.Field
+// eventListResponseEventInstallationUpdatedPropertiesJSON contains the JSON
+// metadata for the struct [EventListResponseEventInstallationUpdatedProperties]
+type eventListResponseEventInstallationUpdatedPropertiesJSON struct {
+	Version     apijson.Field
 	raw         string
 	raw         string
 	ExtraFields map[string]apijson.Field
 	ExtraFields map[string]apijson.Field
 }
 }
 
 
-func (r *EventListResponseEventStorageWriteProperties) UnmarshalJSON(data []byte) (err error) {
+func (r *EventListResponseEventInstallationUpdatedProperties) UnmarshalJSON(data []byte) (err error) {
 	return apijson.UnmarshalRoot(data, r)
 	return apijson.UnmarshalRoot(data, r)
 }
 }
 
 
-func (r eventListResponseEventStorageWritePropertiesJSON) RawJSON() string {
+func (r eventListResponseEventInstallationUpdatedPropertiesJSON) RawJSON() string {
 	return r.raw
 	return r.raw
 }
 }
 
 
-type EventListResponseEventStorageWriteType string
+type EventListResponseEventInstallationUpdatedType string
 
 
 const (
 const (
-	EventListResponseEventStorageWriteTypeStorageWrite EventListResponseEventStorageWriteType = "storage.write"
+	EventListResponseEventInstallationUpdatedTypeInstallationUpdated EventListResponseEventInstallationUpdatedType = "installation.updated"
 )
 )
 
 
-func (r EventListResponseEventStorageWriteType) IsKnown() bool {
+func (r EventListResponseEventInstallationUpdatedType) IsKnown() bool {
 	switch r {
 	switch r {
-	case EventListResponseEventStorageWriteTypeStorageWrite:
+	case EventListResponseEventInstallationUpdatedTypeInstallationUpdated:
 		return true
 		return true
 	}
 	}
 	return false
 	return false
 }
 }
 
 
-type EventListResponseEventInstallationUpdated struct {
-	Properties EventListResponseEventInstallationUpdatedProperties `json:"properties,required"`
-	Type       EventListResponseEventInstallationUpdatedType       `json:"type,required"`
-	JSON       eventListResponseEventInstallationUpdatedJSON       `json:"-"`
+type EventListResponseEventStorageWrite struct {
+	Properties EventListResponseEventStorageWriteProperties `json:"properties,required"`
+	Type       EventListResponseEventStorageWriteType       `json:"type,required"`
+	JSON       eventListResponseEventStorageWriteJSON       `json:"-"`
 }
 }
 
 
-// eventListResponseEventInstallationUpdatedJSON contains the JSON metadata for the
-// struct [EventListResponseEventInstallationUpdated]
-type eventListResponseEventInstallationUpdatedJSON struct {
+// eventListResponseEventStorageWriteJSON contains the JSON metadata for the struct
+// [EventListResponseEventStorageWrite]
+type eventListResponseEventStorageWriteJSON struct {
 	Properties  apijson.Field
 	Properties  apijson.Field
 	Type        apijson.Field
 	Type        apijson.Field
 	raw         string
 	raw         string
 	ExtraFields map[string]apijson.Field
 	ExtraFields map[string]apijson.Field
 }
 }
 
 
-func (r *EventListResponseEventInstallationUpdated) UnmarshalJSON(data []byte) (err error) {
+func (r *EventListResponseEventStorageWrite) UnmarshalJSON(data []byte) (err error) {
 	return apijson.UnmarshalRoot(data, r)
 	return apijson.UnmarshalRoot(data, r)
 }
 }
 
 
-func (r eventListResponseEventInstallationUpdatedJSON) RawJSON() string {
+func (r eventListResponseEventStorageWriteJSON) RawJSON() string {
 	return r.raw
 	return r.raw
 }
 }
 
 
-func (r EventListResponseEventInstallationUpdated) implementsEventListResponse() {}
+func (r EventListResponseEventStorageWrite) implementsEventListResponse() {}
 
 
-type EventListResponseEventInstallationUpdatedProperties struct {
-	Version string                                                  `json:"version,required"`
-	JSON    eventListResponseEventInstallationUpdatedPropertiesJSON `json:"-"`
+type EventListResponseEventStorageWriteProperties struct {
+	Key     string                                           `json:"key,required"`
+	Content interface{}                                      `json:"content"`
+	JSON    eventListResponseEventStorageWritePropertiesJSON `json:"-"`
 }
 }
 
 
-// eventListResponseEventInstallationUpdatedPropertiesJSON contains the JSON
-// metadata for the struct [EventListResponseEventInstallationUpdatedProperties]
-type eventListResponseEventInstallationUpdatedPropertiesJSON struct {
-	Version     apijson.Field
+// eventListResponseEventStorageWritePropertiesJSON contains the JSON metadata for
+// the struct [EventListResponseEventStorageWriteProperties]
+type eventListResponseEventStorageWritePropertiesJSON struct {
+	Key         apijson.Field
+	Content     apijson.Field
 	raw         string
 	raw         string
 	ExtraFields map[string]apijson.Field
 	ExtraFields map[string]apijson.Field
 }
 }
 
 
-func (r *EventListResponseEventInstallationUpdatedProperties) UnmarshalJSON(data []byte) (err error) {
+func (r *EventListResponseEventStorageWriteProperties) UnmarshalJSON(data []byte) (err error) {
 	return apijson.UnmarshalRoot(data, r)
 	return apijson.UnmarshalRoot(data, r)
 }
 }
 
 
-func (r eventListResponseEventInstallationUpdatedPropertiesJSON) RawJSON() string {
+func (r eventListResponseEventStorageWritePropertiesJSON) RawJSON() string {
 	return r.raw
 	return r.raw
 }
 }
 
 
-type EventListResponseEventInstallationUpdatedType string
+type EventListResponseEventStorageWriteType string
 
 
 const (
 const (
-	EventListResponseEventInstallationUpdatedTypeInstallationUpdated EventListResponseEventInstallationUpdatedType = "installation.updated"
+	EventListResponseEventStorageWriteTypeStorageWrite EventListResponseEventStorageWriteType = "storage.write"
 )
 )
 
 
-func (r EventListResponseEventInstallationUpdatedType) IsKnown() bool {
+func (r EventListResponseEventStorageWriteType) IsKnown() bool {
 	switch r {
 	switch r {
-	case EventListResponseEventInstallationUpdatedTypeInstallationUpdated:
+	case EventListResponseEventStorageWriteTypeStorageWrite:
 		return true
 		return true
 	}
 	}
 	return false
 	return false
@@ -673,7 +673,7 @@ func (r EventListResponseEventMessagePartUpdated) implementsEventListResponse()
 
 
 type EventListResponseEventMessagePartUpdatedProperties struct {
 type EventListResponseEventMessagePartUpdatedProperties struct {
 	MessageID string                                                 `json:"messageID,required"`
 	MessageID string                                                 `json:"messageID,required"`
-	Part      MessagePart                                            `json:"part,required"`
+	Part      AssistantMessagePart                                   `json:"part,required"`
 	SessionID string                                                 `json:"sessionID,required"`
 	SessionID string                                                 `json:"sessionID,required"`
 	JSON      eventListResponseEventMessagePartUpdatedPropertiesJSON `json:"-"`
 	JSON      eventListResponseEventMessagePartUpdatedPropertiesJSON `json:"-"`
 }
 }
@@ -1159,8 +1159,8 @@ const (
 	EventListResponseTypeLspClientDiagnostics EventListResponseType = "lsp.client.diagnostics"
 	EventListResponseTypeLspClientDiagnostics EventListResponseType = "lsp.client.diagnostics"
 	EventListResponseTypePermissionUpdated    EventListResponseType = "permission.updated"
 	EventListResponseTypePermissionUpdated    EventListResponseType = "permission.updated"
 	EventListResponseTypeFileEdited           EventListResponseType = "file.edited"
 	EventListResponseTypeFileEdited           EventListResponseType = "file.edited"
-	EventListResponseTypeStorageWrite         EventListResponseType = "storage.write"
 	EventListResponseTypeInstallationUpdated  EventListResponseType = "installation.updated"
 	EventListResponseTypeInstallationUpdated  EventListResponseType = "installation.updated"
+	EventListResponseTypeStorageWrite         EventListResponseType = "storage.write"
 	EventListResponseTypeMessageUpdated       EventListResponseType = "message.updated"
 	EventListResponseTypeMessageUpdated       EventListResponseType = "message.updated"
 	EventListResponseTypeMessageRemoved       EventListResponseType = "message.removed"
 	EventListResponseTypeMessageRemoved       EventListResponseType = "message.removed"
 	EventListResponseTypeMessagePartUpdated   EventListResponseType = "message.part.updated"
 	EventListResponseTypeMessagePartUpdated   EventListResponseType = "message.part.updated"
@@ -1173,7 +1173,7 @@ const (
 
 
 func (r EventListResponseType) IsKnown() bool {
 func (r EventListResponseType) IsKnown() bool {
 	switch r {
 	switch r {
-	case EventListResponseTypeLspClientDiagnostics, EventListResponseTypePermissionUpdated, EventListResponseTypeFileEdited, EventListResponseTypeStorageWrite, EventListResponseTypeInstallationUpdated, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeFileWatcherUpdated:
+	case EventListResponseTypeLspClientDiagnostics, EventListResponseTypePermissionUpdated, EventListResponseTypeFileEdited, EventListResponseTypeInstallationUpdated, EventListResponseTypeStorageWrite, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeFileWatcherUpdated:
 		return true
 		return true
 	}
 	}
 	return false
 	return false

+ 2 - 5
packages/tui/sdk/release-please-config.json

@@ -60,8 +60,5 @@
     }
     }
   ],
   ],
   "release-type": "go",
   "release-type": "go",
-  "extra-files": [
-    "internal/version.go",
-    "README.md"
-  ]
-}
+  "extra-files": ["internal/version.go", "README.md"]
+}

Разница между файлами не показана из-за своего большого размера
+ 293 - 454
packages/tui/sdk/session.go


+ 1 - 1
packages/tui/sdk/session_test.go

@@ -118,7 +118,7 @@ func TestSessionChat(t *testing.T) {
 		"id",
 		"id",
 		opencode.SessionChatParams{
 		opencode.SessionChatParams{
 			ModelID: opencode.F("modelID"),
 			ModelID: opencode.F("modelID"),
-			Parts: opencode.F([]opencode.MessagePartUnionParam{opencode.TextPartParam{
+			Parts: opencode.F([]opencode.UserMessagePartUnionParam{opencode.TextPartParam{
 				Text: opencode.F("text"),
 				Text: opencode.F("text"),
 				Type: opencode.F(opencode.TextPartTypeText),
 				Type: opencode.F(opencode.TextPartTypeText),
 			}}),
 			}}),

+ 2 - 2
packages/tui/sdk/shared/shared.go

@@ -31,7 +31,7 @@ func (r providerAuthErrorJSON) RawJSON() string {
 
 
 func (r ProviderAuthError) ImplementsEventListResponseEventSessionErrorPropertiesError() {}
 func (r ProviderAuthError) ImplementsEventListResponseEventSessionErrorPropertiesError() {}
 
 
-func (r ProviderAuthError) ImplementsMessageMetadataError() {}
+func (r ProviderAuthError) ImplementsAssistantMessageError() {}
 
 
 type ProviderAuthErrorData struct {
 type ProviderAuthErrorData struct {
 	Message    string                    `json:"message,required"`
 	Message    string                    `json:"message,required"`
@@ -94,7 +94,7 @@ func (r unknownErrorJSON) RawJSON() string {
 
 
 func (r UnknownError) ImplementsEventListResponseEventSessionErrorPropertiesError() {}
 func (r UnknownError) ImplementsEventListResponseEventSessionErrorPropertiesError() {}
 
 
-func (r UnknownError) ImplementsMessageMetadataError() {}
+func (r UnknownError) ImplementsAssistantMessageError() {}
 
 
 type UnknownErrorData struct {
 type UnknownErrorData struct {
 	Message string               `json:"message,required"`
 	Message string               `json:"message,required"`

+ 5 - 5
packages/web/astro.config.mjs

@@ -20,11 +20,11 @@ export default defineConfig({
   devToolbar: {
   devToolbar: {
     enabled: false,
     enabled: false,
   },
   },
+  server: {
+    host: "0.0.0.0",
+  },
   markdown: {
   markdown: {
-    rehypePlugins: [
-      rehypeHeadingIds,
-      [rehypeAutolinkHeadings, { behavior: "wrap" }],
-    ],
+    rehypePlugins: [rehypeHeadingIds, [rehypeAutolinkHeadings, { behavior: "wrap" }]],
   },
   },
   integrations: [
   integrations: [
     solidJs(),
     solidJs(),
@@ -33,7 +33,7 @@ export default defineConfig({
       expressiveCode: { themes: ["github-light", "github-dark"] },
       expressiveCode: { themes: ["github-light", "github-dark"] },
       social: [
       social: [
         { icon: "github", label: "GitHub", href: config.github },
         { icon: "github", label: "GitHub", href: config.github },
-        { icon: "discord", label: "Dscord", href: config.discord }
+        { icon: "discord", label: "Dscord", href: config.discord },
       ],
       ],
       head: [
       head: [
         {
         {

+ 1 - 0
packages/web/package.json

@@ -4,6 +4,7 @@
   "version": "0.0.1",
   "version": "0.0.1",
   "scripts": {
   "scripts": {
     "dev": "astro dev",
     "dev": "astro dev",
+    "dev:remote": "sst shell --stage=dev --target=Web astro dev",
     "start": "astro dev",
     "start": "astro dev",
     "build": "astro build",
     "build": "astro build",
     "preview": "astro preview",
     "preview": "astro preview",

+ 1 - 8
packages/web/public/theme.json

@@ -88,14 +88,7 @@
         "syntaxOperator": { "$ref": "#/definitions/colorValue" },
         "syntaxOperator": { "$ref": "#/definitions/colorValue" },
         "syntaxPunctuation": { "$ref": "#/definitions/colorValue" }
         "syntaxPunctuation": { "$ref": "#/definitions/colorValue" }
       },
       },
-      "required": [
-        "primary",
-        "secondary",
-        "accent",
-        "text",
-        "textMuted",
-        "background"
-      ],
+      "required": ["primary", "secondary", "accent", "text", "textMuted", "background"],
       "additionalProperties": false
       "additionalProperties": false
     }
     }
   },
   },

+ 2 - 6
packages/web/src/components/CodeBlock.tsx

@@ -1,8 +1,4 @@
-import {
-  type JSX,
-  splitProps,
-  createResource,
-} from "solid-js"
+import { type JSX, splitProps, createResource } from "solid-js"
 import { codeToHtml } from "shiki"
 import { codeToHtml } from "shiki"
 import styles from "./codeblock.module.css"
 import styles from "./codeblock.module.css"
 import { transformerNotationDiff } from "@shikijs/transformers"
 import { transformerNotationDiff } from "@shikijs/transformers"
@@ -30,7 +26,7 @@ function CodeBlock(props: CodeBlockProps) {
     },
     },
   )
   )
 
 
-  return <div innerHTML={html()} class={styles.codeblock} {...rest}></div >
+  return <div innerHTML={html()} class={styles.codeblock} {...rest}></div>
 }
 }
 
 
 export default CodeBlock
 export default CodeBlock

+ 0 - 39
packages/web/src/components/MarkdownView.tsx

@@ -1,39 +0,0 @@
-import { type JSX, splitProps, createResource } from "solid-js"
-import { marked } from "marked"
-import markedShiki from "marked-shiki"
-import { codeToHtml } from "shiki"
-import { transformerNotationDiff } from "@shikijs/transformers"
-import styles from "./markdownview.module.css"
-
-interface MarkdownViewProps extends JSX.HTMLAttributes<HTMLDivElement> {
-  markdown: string
-}
-
-const markedWithShiki = marked.use(
-  markedShiki({
-    highlight(code, lang) {
-      return codeToHtml(code, {
-        lang: lang || "text",
-        themes: {
-          light: "github-light",
-          dark: "github-dark",
-        },
-        transformers: [transformerNotationDiff()],
-      })
-    },
-  }),
-)
-
-function MarkdownView(props: MarkdownViewProps) {
-  const [local, rest] = splitProps(props, ["markdown"])
-  const [html] = createResource(
-    () => local.markdown,
-    async (markdown) => {
-      return markedWithShiki.parse(markdown)
-    },
-  )
-
-  return <div innerHTML={html()} class={styles["markdown-body"]} {...rest} />
-}
-
-export default MarkdownView

Разница между файлами не показана из-за своего большого размера
+ 58 - 1590
packages/web/src/components/Share.tsx


+ 0 - 1
packages/web/src/components/codeblock.module.css

@@ -8,4 +8,3 @@
     }
     }
   }
   }
 }
 }
-

+ 0 - 121
packages/web/src/components/diffview.module.css

@@ -1,121 +0,0 @@
-.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;
-  }
-}

+ 6 - 1
packages/web/src/components/icons/custom.tsx

@@ -39,7 +39,12 @@ export function IconGemini(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
 export function IconOpencode(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
 export function IconOpencode(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
   return (
   return (
     <svg {...props} viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
     <svg {...props} viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
-      <path fill-rule="evenodd" clip-rule="evenodd" d="M0 13H35V58H0V13ZM26.25 22.1957H8.75V48.701H26.25V22.1957Z" fill="currentColor" />
+      <path
+        fill-rule="evenodd"
+        clip-rule="evenodd"
+        d="M0 13H35V58H0V13ZM26.25 22.1957H8.75V48.701H26.25V22.1957Z"
+        fill="currentColor"
+      />
       <path d="M43.75 13H70V22.1957H52.5V48.701H70V57.8967H43.75V13Z" fill="currentColor" />
       <path d="M43.75 13H70V22.1957H52.5V48.701H70V57.8967H43.75V13Z" fill="currentColor" />
     </svg>
     </svg>
   )
   )

Разница между файлами не показана из-за своего большого размера
+ 88 - 453
packages/web/src/components/icons/index.tsx


+ 0 - 106
packages/web/src/components/markdownview.module.css

@@ -1,106 +0,0 @@
-.markdown-body {
-  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;
-  }
-}

+ 101 - 83
packages/web/src/components/share.module.css

@@ -15,76 +15,42 @@
   --lg-tool-width: 56rem;
   --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");
   --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-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] {
+  [data-component="header"] {
     display: flex;
     display: flex;
-    align-items: center;
-    gap: 0.125rem;
+    flex-direction: column;
+    gap: 1rem;
 
 
-    span[data-button-icon] {
-      line-height: 1;
-      opacity: 0.85;
-      svg {
-        display: block;
-      }
+    @media (max-width: 30rem) {
+      gap: 1rem;
     }
     }
   }
   }
-}
-
-[data-element-label] {
-  text-transform: uppercase;
-  letter-spacing: -0.5px;
-  color: var(--sl-color-text-dimmed);
-}
 
 
-.header {
-  display: flex;
-  flex-direction: column;
-  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) {
-    gap: 1rem;
-  }
-
-  [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;
+    @media (max-width: 30rem) {
+      font-size: 1.75rem;
+      line-height: 1.25;
       -webkit-line-clamp: 3;
       -webkit-line-clamp: 3;
-      overflow: hidden;
-
-      @media (max-width: 30rem) {
-        font-size: 1.75rem;
-        line-height: 1.25;
-        -webkit-line-clamp: 3;
-      }
     }
     }
   }
   }
 
 
-  [data-section="row"] {
+  [data-component="header-details"] {
     display: flex;
     display: flex;
     flex-direction: column;
     flex-direction: column;
     gap: 0.5rem;
     gap: 0.5rem;
   }
   }
 
 
-  [data-section="stats"] {
+  [data-component="header-stats"] {
     list-style-type: none;
     list-style-type: none;
     padding: 0;
     padding: 0;
     margin: 0;
     margin: 0;
@@ -92,41 +58,62 @@
     gap: 0.5rem 0.875rem;
     gap: 0.5rem 0.875rem;
     flex-wrap: wrap;
     flex-wrap: wrap;
 
 
-    li {
+    [data-slot="item"] {
       display: flex;
       display: flex;
       align-items: center;
       align-items: center;
-      gap: 0.5rem;
+      gap: 0.3125rem;
       font-size: 0.875rem;
       font-size: 0.875rem;
 
 
       span[data-placeholder] {
       span[data-placeholder] {
         color: var(--sl-color-text-dimmed);
         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-section="stats"] {
-    li {
-      gap: 0.3125rem;
+  [data-component="header-time"] {
+    color: var(--sl-color-text-dimmed);
+    font-size: 0.875rem;
+  }
 
 
-      [data-stat-icon] {
-        flex: 0 0 auto;
-        color: var(--sl-color-text-dimmed);
+  [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;
         opacity: 0.85;
         opacity: 0.85;
+
         svg {
         svg {
           display: block;
           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;
     }
     }
   }
   }
 }
 }
@@ -170,10 +157,12 @@
           svg:nth-child(3) {
           svg:nth-child(3) {
             display: none;
             display: none;
           }
           }
+
           &:hover {
           &:hover {
             svg:nth-child(1) {
             svg:nth-child(1) {
               display: none;
               display: none;
             }
             }
+
             svg:nth-child(2) {
             svg:nth-child(2) {
               display: block;
               display: block;
             }
             }
@@ -213,12 +202,14 @@
             opacity: 1;
             opacity: 1;
             visibility: visible;
             visibility: visible;
           }
           }
+
           a,
           a,
           a:hover {
           a:hover {
             svg:nth-child(1),
             svg:nth-child(1),
             svg:nth-child(2) {
             svg:nth-child(2) {
               display: none;
               display: none;
             }
             }
+
             svg:nth-child(3) {
             svg:nth-child(3) {
               display: block;
               display: block;
             }
             }
@@ -264,7 +255,7 @@
         }
         }
 
 
         b {
         b {
-        color: var(--sl-color-text);
+          color: var(--sl-color-text);
           word-break: break-all;
           word-break: break-all;
           font-weight: 500;
           font-weight: 500;
         }
         }
@@ -348,8 +339,7 @@
   }
   }
 
 
   [data-part-type="tool-grep"] {
   [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;
       gap: 0.5rem;
     }
     }
   }
   }
@@ -374,6 +364,7 @@
       }
       }
     }
     }
   }
   }
+
   [data-part-type="summary"] {
   [data-part-type="summary"] {
     & > [data-section="decoration"] {
     & > [data-section="decoration"] {
       span:first-child {
       span:first-child {
@@ -388,15 +379,19 @@
         &[data-status="connected"] {
         &[data-status="connected"] {
           background-color: var(--sl-color-green);
           background-color: var(--sl-color-green);
         }
         }
+
         &[data-status="connecting"] {
         &[data-status="connecting"] {
           background-color: var(--sl-color-orange);
           background-color: var(--sl-color-orange);
         }
         }
+
         &[data-status="disconnected"] {
         &[data-status="disconnected"] {
           background-color: var(--sl-color-divider);
           background-color: var(--sl-color-divider);
         }
         }
+
         &[data-status="reconnecting"] {
         &[data-status="reconnecting"] {
           background-color: var(--sl-color-orange);
           background-color: var(--sl-color-orange);
         }
         }
+
         &[data-status="error"] {
         &[data-status="error"] {
           background-color: var(--sl-color-red);
           background-color: var(--sl-color-red);
         }
         }
@@ -493,14 +488,20 @@
     }
     }
   }
   }
 
 
-  &[data-background="none"] { background-color: transparent; }
-  &[data-background="blue"] { background-color: var(--sl-color-blue-low); }
+  &[data-background="none"] {
+    background-color: transparent;
+  }
+
+  &[data-background="blue"] {
+    background-color: var(--sl-color-blue-low);
+  }
 
 
   &[data-expanded="true"] {
   &[data-expanded="true"] {
     pre {
     pre {
       display: block;
       display: block;
     }
     }
   }
   }
+
   &[data-expanded="false"] {
   &[data-expanded="false"] {
     pre {
     pre {
       display: -webkit-box;
       display: -webkit-box;
@@ -536,20 +537,25 @@
 
 
       span {
       span {
         margin-right: 0.25rem;
         margin-right: 0.25rem;
+
         &:last-child {
         &:last-child {
           margin-right: 0;
           margin-right: 0;
         }
         }
       }
       }
+
       span[data-color="red"] {
       span[data-color="red"] {
         color: var(--sl-color-red);
         color: var(--sl-color-red);
       }
       }
+
       span[data-color="dimmed"] {
       span[data-color="dimmed"] {
         color: var(--sl-color-text-dimmed);
         color: var(--sl-color-text-dimmed);
       }
       }
+
       span[data-marker="label"] {
       span[data-marker="label"] {
         text-transform: uppercase;
         text-transform: uppercase;
         letter-spacing: -0.5px;
         letter-spacing: -0.5px;
       }
       }
+
       span[data-separator] {
       span[data-separator] {
         margin-right: 0.375rem;
         margin-right: 0.375rem;
       }
       }
@@ -561,6 +567,7 @@
       display: block;
       display: block;
     }
     }
   }
   }
+
   &[data-expanded="false"] {
   &[data-expanded="false"] {
     [data-section="content"] {
     [data-section="content"] {
       display: -webkit-box;
       display: -webkit-box;
@@ -575,7 +582,6 @@
     padding: 2px 0;
     padding: 2px 0;
     font-size: 0.75rem;
     font-size: 0.75rem;
   }
   }
-
 }
 }
 
 
 .message-terminal {
 .message-terminal {
@@ -611,7 +617,7 @@
       }
       }
 
 
       &::before {
       &::before {
-        content: '';
+        content: "";
         position: absolute;
         position: absolute;
         pointer-events: none;
         pointer-events: none;
         top: 8px;
         top: 8px;
@@ -651,6 +657,7 @@
       display: block;
       display: block;
     }
     }
   }
   }
+
   &[data-expanded="false"] {
   &[data-expanded="false"] {
     pre {
     pre {
       display: -webkit-box;
       display: -webkit-box;
@@ -693,6 +700,7 @@
       display: block;
       display: block;
     }
     }
   }
   }
+
   &[data-expanded="false"] {
   &[data-expanded="false"] {
     [data-element-markdown] {
     [data-element-markdown] {
       display: -webkit-box;
       display: -webkit-box;
@@ -750,10 +758,14 @@
     &[data-status="pending"] {
     &[data-status="pending"] {
       color: var(--sl-color-text);
       color: var(--sl-color-text);
     }
     }
+
     &[data-status="in_progress"] {
     &[data-status="in_progress"] {
       color: var(--sl-color-text);
       color: var(--sl-color-text);
 
 
-      & > span { border-color: var(--sl-color-orange); }
+      & > span {
+        border-color: var(--sl-color-orange);
+      }
+
       & > span::before {
       & > span::before {
         content: "";
         content: "";
         position: absolute;
         position: absolute;
@@ -764,10 +776,14 @@
         box-shadow: inset 1rem 1rem var(--sl-color-orange-low);
         box-shadow: inset 1rem 1rem var(--sl-color-orange-low);
       }
       }
     }
     }
+
     &[data-status="completed"] {
     &[data-status="completed"] {
       color: var(--sl-color-text-secondary);
       color: var(--sl-color-text-secondary);
 
 
-      & > span { border-color: var(--sl-color-green-low); }
+      & > span {
+        border-color: var(--sl-color-green-low);
+      }
+
       & > span::before {
       & > span::before {
         content: "";
         content: "";
         position: absolute;
         position: absolute;
@@ -798,7 +814,9 @@
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: 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;
   z-index: 100;
   appearance: none;
   appearance: none;
   opacity: 1;
   opacity: 1;

+ 60 - 0
packages/web/src/components/share/common.tsx

@@ -0,0 +1,60 @@
+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<HTMLDivElement> {
+  id: string
+}
+export function AnchorIcon(props: AnchorProps) {
+  const [local, rest] = splitProps(props, ["id", "children"])
+  const [copied, setCopied] = createSignal(false)
+
+  return (
+    <div {...rest} data-element-anchor title="Link to this message" data-status={copied() ? "copied" : ""}>
+      <a
+        href={`#${local.id}`}
+        onClick={(e) => {
+          e.preventDefault()
+
+          const anchor = e.currentTarget
+          const hash = anchor.getAttribute("href") || ""
+          const { origin, pathname, search } = window.location
+
+          navigator.clipboard
+            .writeText(`${origin}${pathname}${search}${hash}`)
+            .catch((err) => console.error("Copy failed", err))
+
+          setCopied(true)
+          setTimeout(() => setCopied(false), 3000)
+        }}
+      >
+        {local.children}
+        <IconHashtag width={18} height={18} />
+        <IconCheckCircle width={18} height={18} />
+      </a>
+      <span data-element-tooltip>Copied!</span>
+    </div>
+  )
+}
+
+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()
+      })
+    },
+  }
+}

+ 25 - 0
packages/web/src/components/share/content-code.module.css

@@ -0,0 +1,25 @@
+.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;
+    }
+  }
+}

+ 32 - 0
packages/web/src/components/share/content-code.tsx

@@ -0,0 +1,32 @@
+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 (
+    <Suspense>
+      <div innerHTML={html()} class={style.root} data-flush={props.flush === true ? true : undefined} />
+    </Suspense>
+  )
+}

+ 125 - 0
packages/web/src/components/share/content-diff.module.css

@@ -0,0 +1,125 @@
+.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;
+    }
+  }
+}

+ 43 - 58
packages/web/src/components/DiffView.tsx → packages/web/src/components/share/content-diff.tsx

@@ -1,7 +1,7 @@
 import { type Component, createMemo } from "solid-js"
 import { type Component, createMemo } from "solid-js"
 import { parsePatch } from "diff"
 import { parsePatch } from "diff"
-import CodeBlock from "./CodeBlock"
-import styles from "./diffview.module.css"
+import { ContentCode } from "./content-code"
+import styles from "./content-diff.module.css"
 
 
 type DiffRow = {
 type DiffRow = {
   left: string
   left: string
@@ -9,14 +9,12 @@ type DiffRow = {
   type: "added" | "removed" | "unchanged" | "modified"
   type: "added" | "removed" | "unchanged" | "modified"
 }
 }
 
 
-interface DiffViewProps {
+interface Props {
   diff: string
   diff: string
   lang?: string
   lang?: string
-  class?: string
 }
 }
 
 
-const DiffView: Component<DiffViewProps> = (props) => {
-
+export function ContentDiff(props: Props) {
   const rows = createMemo(() => {
   const rows = createMemo(() => {
     const diffRows: DiffRow[] = []
     const diffRows: DiffRow[] = []
 
 
@@ -33,20 +31,20 @@ const DiffView: Component<DiffViewProps> = (props) => {
             const content = line.slice(1)
             const content = line.slice(1)
             const prefix = line[0]
             const prefix = line[0]
 
 
-            if (prefix === '-') {
+            if (prefix === "-") {
               // Look ahead for consecutive additions to pair with removals
               // Look ahead for consecutive additions to pair with removals
               const removals: string[] = [content]
               const removals: string[] = [content]
               let j = i + 1
               let j = i + 1
 
 
               // Collect all consecutive removals
               // Collect all consecutive removals
-              while (j < lines.length && lines[j][0] === '-') {
+              while (j < lines.length && lines[j][0] === "-") {
                 removals.push(lines[j].slice(1))
                 removals.push(lines[j].slice(1))
                 j++
                 j++
               }
               }
 
 
               // Collect all consecutive additions that follow
               // Collect all consecutive additions that follow
               const additions: string[] = []
               const additions: string[] = []
-              while (j < lines.length && lines[j][0] === '+') {
+              while (j < lines.length && lines[j][0] === "+") {
                 additions.push(lines[j].slice(1))
                 additions.push(lines[j].slice(1))
                 j++
                 j++
               }
               }
@@ -62,39 +60,39 @@ const DiffView: Component<DiffViewProps> = (props) => {
                   diffRows.push({
                   diffRows.push({
                     left: removals[k],
                     left: removals[k],
                     right: additions[k],
                     right: additions[k],
-                    type: "modified"
+                    type: "modified",
                   })
                   })
                 } else if (hasLeft) {
                 } else if (hasLeft) {
                   // Pure removal
                   // Pure removal
                   diffRows.push({
                   diffRows.push({
                     left: removals[k],
                     left: removals[k],
                     right: "",
                     right: "",
-                    type: "removed"
+                    type: "removed",
                   })
                   })
                 } else if (hasRight) {
                 } else if (hasRight) {
                   // Pure addition - only create if we actually have content
                   // Pure addition - only create if we actually have content
                   diffRows.push({
                   diffRows.push({
                     left: "",
                     left: "",
                     right: additions[k],
                     right: additions[k],
-                    type: "added"
+                    type: "added",
                   })
                   })
                 }
                 }
               }
               }
 
 
               i = j
               i = j
-            } else if (prefix === '+') {
+            } else if (prefix === "+") {
               // Standalone addition (not paired with removal)
               // Standalone addition (not paired with removal)
               diffRows.push({
               diffRows.push({
                 left: "",
                 left: "",
                 right: content,
                 right: content,
-                type: "added"
+                type: "added",
               })
               })
               i++
               i++
-            } else if (prefix === ' ') {
+            } else if (prefix === " ") {
               diffRows.push({
               diffRows.push({
                 left: content,
                 left: content,
                 right: content,
                 right: content,
-                type: "unchanged"
+                type: "unchanged",
               })
               })
               i++
               i++
             } else {
             } else {
@@ -112,7 +110,7 @@ const DiffView: Component<DiffViewProps> = (props) => {
   })
   })
 
 
   const mobileRows = createMemo(() => {
   const mobileRows = createMemo(() => {
-    const mobileBlocks: { type: 'removed' | 'added' | 'unchanged', lines: string[] }[] = []
+    const mobileBlocks: { type: "removed" | "added" | "unchanged"; lines: string[] }[] = []
     const currentRows = rows()
     const currentRows = rows()
 
 
     let i = 0
     let i = 0
@@ -121,15 +119,15 @@ const DiffView: Component<DiffViewProps> = (props) => {
       const addedLines: string[] = []
       const addedLines: string[] = []
 
 
       // Collect consecutive modified/removed/added rows
       // 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]
         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)
           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)
           addedLines.push(row.right)
         }
         }
         i++
         i++
@@ -137,17 +135,17 @@ const DiffView: Component<DiffViewProps> = (props) => {
 
 
       // Add grouped blocks
       // Add grouped blocks
       if (removedLines.length > 0) {
       if (removedLines.length > 0) {
-        mobileBlocks.push({ type: 'removed', lines: removedLines })
+        mobileBlocks.push({ type: "removed", lines: removedLines })
       }
       }
       if (addedLines.length > 0) {
       if (addedLines.length > 0) {
-        mobileBlocks.push({ type: 'added', lines: addedLines })
+        mobileBlocks.push({ type: "added", lines: addedLines })
       }
       }
 
 
       // Add unchanged rows as-is
       // Add unchanged rows as-is
-      if (i < currentRows.length && currentRows[i].type === 'unchanged') {
+      if (i < currentRows.length && currentRows[i].type === "unchanged") {
         mobileBlocks.push({
         mobileBlocks.push({
-          type: 'unchanged',
-          lines: [currentRows[i].left]
+          type: "unchanged",
+          lines: [currentRows[i].left],
         })
         })
         i++
         i++
       }
       }
@@ -157,40 +155,29 @@ const DiffView: Component<DiffViewProps> = (props) => {
   })
   })
 
 
   return (
   return (
-    <div class={`${styles.diff} ${props.class ?? ""}`}>
-      <div class={styles.desktopView}>
+    <div class={styles.root}>
+      <div data-component="desktop">
         {rows().map((r) => (
         {rows().map((r) => (
-          <div class={styles.row}>
-            <div class={styles.beforeColumn}>
-              <CodeBlock
-                code={r.left}
-                lang={props.lang}
-                data-section="cell"
-                data-diff-type={r.type === "removed" || r.type === "modified" ? "removed" : ""}
-              />
+          <div data-component="diff-row" data-type={r.type}>
+            <div data-slot="before" data-diff-type={r.type === "removed" || r.type === "modified" ? "removed" : ""}>
+              <ContentCode code={r.left} flush lang={props.lang} />
             </div>
             </div>
-            <div class={styles.afterColumn}>
-              <CodeBlock
-                code={r.right}
-                lang={props.lang}
-                data-section="cell"
-                data-diff-type={r.type === "added" || r.type === "modified" ? "added" : ""}
-              />
+            <div data-slot="after" data-diff-type={r.type === "added" || r.type === "modified" ? "added" : ""}>
+              <ContentCode code={r.right} lang={props.lang} flush />
             </div>
             </div>
           </div>
           </div>
         ))}
         ))}
       </div>
       </div>
 
 
-      <div class={styles.mobileView}>
+      <div data-component="mobile">
         {mobileRows().map((block) => (
         {mobileRows().map((block) => (
-          <div class={styles.mobileBlock}>
+          <div data-component="diff-block" data-type={block.type}>
             {block.lines.map((line) => (
             {block.lines.map((line) => (
-              <CodeBlock
+              <ContentCode
                 code={line}
                 code={line}
                 lang={props.lang}
                 lang={props.lang}
                 data-section="cell"
                 data-section="cell"
-                data-diff-type={block.type === 'removed' ? 'removed' :
-                  block.type === 'added' ? 'added' : ''}
+                data-diff-type={block.type === "removed" ? "removed" : block.type === "added" ? "added" : ""}
               />
               />
             ))}
             ))}
           </div>
           </div>
@@ -200,8 +187,6 @@ const DiffView: Component<DiffViewProps> = (props) => {
   )
   )
 }
 }
 
 
-export default DiffView
-
 // const testDiff = `--- combined_before.txt	2025-06-24 16:38:08
 // const testDiff = `--- combined_before.txt	2025-06-24 16:38:08
 // +++ combined_after.txt	2025-06-24 16:38:12
 // +++ combined_after.txt	2025-06-24 16:38:12
 // @@ -1,21 +1,25 @@
 // @@ -1,21 +1,25 @@
@@ -210,12 +195,12 @@ export default DiffView
 // -old content
 // -old content
 // +added line
 // +added line
 // +new content
 // +new content
-//  
+//
 // -removed empty line below
 // -removed empty line below
 // +added empty line above
 // +added empty line above
-//  
+//
 // -	tab indented
 // -	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
 // -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: 🚀 ✨ 中文
 // -unicode content: 🚀 ✨ 中文
 // -mixed	content with	tabs and spaces
 // -mixed	content with	tabs and spaces
@@ -226,14 +211,14 @@ export default DiffView
 // +different unicode: 🎉 💻 日本語
 // +different unicode: 🎉 💻 日本語
 // +normalized content with consistent spacing
 // +normalized content with consistent spacing
 // +newline to content
 // +newline to content
-//  
+//
 // -content to remove
 // -content to remove
-// -whitespace only:    	  
+// -whitespace only:
 // -multiple
 // -multiple
 // -consecutive
 // -consecutive
 // -deletions
 // -deletions
 // -single deletion
 // -single deletion
-// +    	  
+// +
 // +single addition
 // +single addition
 // +first addition
 // +first addition
 // +second addition
 // +second addition

Некоторые файлы не были показаны из-за большого количества измененных файлов