Dax 7 месяцев назад
Родитель
Сommit
90d6c4ab41

+ 182 - 3
bun.lock

@@ -37,6 +37,7 @@
         "@openauthjs/openauth": "0.4.3",
         "@standard-schema/spec": "1.0.0",
         "ai": "catalog:",
+        "cli-markdown": "3.5.1",
         "decimal.js": "10.5.0",
         "diff": "8.0.2",
         "env-paths": "3.0.0",
@@ -209,6 +210,8 @@
 
     "@cloudflare/workers-types": ["@cloudflare/[email protected]", "", {}, "sha512-9RIffHobc35JWeddzBguGgPa4wLDr5x5F94+0/qy7LiV6pTBQ/M5qGEN9VA16IDT3EUpYI0WKh6VpcmeVEtVtw=="],
 
+    "@colors/colors": ["@colors/[email protected]", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="],
+
     "@cspotcode/source-map-support": ["@cspotcode/[email protected]", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
 
     "@ctrl/tinycolor": ["@ctrl/[email protected]", "", {}, "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ=="],
@@ -337,6 +340,8 @@
 
     "@jsdevtools/ono": ["@jsdevtools/[email protected]", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="],
 
+    "@mdit/plugin-alert": ["@mdit/[email protected]", "", { "dependencies": { "@types/markdown-it": "^14.1.2" }, "peerDependencies": { "markdown-it": "^14.1.0" }, "optionalPeers": ["markdown-it"] }, "sha512-n2oVSeg3yeZBCjqfAqbnJxeu4PGq+CXwUWsiwrrARj39z23QZ62FbgL5WGNyP/WFnDAeHMedLDYtipC9OgIOgA=="],
+
     "@mdx-js/mdx": ["@mdx-js/[email protected]", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw=="],
 
     "@mixmark-io/domino": ["@mixmark-io/[email protected]", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
@@ -513,10 +518,18 @@
 
     "@types/json-schema": ["@types/[email protected]", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
 
+    "@types/linkify-it": ["@types/[email protected]", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
+
     "@types/luxon": ["@types/[email protected]", "", {}, "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw=="],
 
+    "@types/markdown-it": ["@types/[email protected]", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="],
+
+    "@types/marked": ["@types/[email protected]", "", { "dependencies": { "marked": "*" } }, "sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA=="],
+
     "@types/mdast": ["@types/[email protected]", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
 
+    "@types/mdurl": ["@types/[email protected]", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
+
     "@types/mdx": ["@types/[email protected]", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
 
     "@types/ms": ["@types/[email protected]", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
@@ -553,10 +566,14 @@
 
     "ansi-align": ["[email protected]", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
 
+    "ansi-escapes": ["[email protected]", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="],
+
     "ansi-regex": ["[email protected]", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
 
     "ansi-styles": ["[email protected]", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
 
+    "any-promise": ["[email protected]", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
+
     "anymatch": ["[email protected]", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
 
     "arctic": ["[email protected]", "", { "dependencies": { "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", "@oslojs/jwt": "0.2.0" } }, "sha512-+p30BOWsctZp+CVYCt7oAean/hWGW42sH5LAcRQX56ttEkFJWbzXBhmSpibbzwSJkRrotmsA+oAoJoVsU0f5xA=="],
@@ -569,6 +586,8 @@
 
     "aria-query": ["[email protected]", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
 
+    "arity-n": ["[email protected]", "", {}, "sha512-fExL2kFDC1Q2DUOx3whE/9KoN66IzkY4b4zUHUBFM1ojEYjZZYDcUW3bek/ufGionX9giIKDC5redH2IlGqcQQ=="],
+
     "array-iterate": ["[email protected]", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="],
 
     "as-table": ["[email protected]", "", { "dependencies": { "printable-characters": "^1.0.42" } }, "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ=="],
@@ -641,6 +660,8 @@
 
     "buffer": ["[email protected]", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="],
 
+    "buffer-from": ["[email protected]", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
+
     "bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
 
     "bundle-name": ["[email protected]", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
@@ -661,6 +682,10 @@
 
     "chalk": ["[email protected]", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
 
+    "chalk-string": ["[email protected]", "", { "dependencies": { "colors-option": "^6.0.1", "is-plain-obj": "^4.1.0" } }, "sha512-kUA4bEXNsDXRnMBRNex8Vsp9cUF9w9UZeRwBBePSUhU3/hHDMuSQKUPYtmuqIHwiDomYd/IVYu2N8aUGl/t6SQ=="],
+
+    "change-case": ["[email protected]", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="],
+
     "character-entities": ["[email protected]", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
 
     "character-entities-html4": ["[email protected]", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
@@ -673,12 +698,22 @@
 
     "chownr": ["[email protected]", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
 
+    "chroma-js": ["[email protected]", "", {}, "sha512-jTwQiT859RTFN/vIf7s+Vl/Z2LcMrvMv3WUFmd/4u76AdlFC0NTNgqEEFPcRiHmAswPsMiQEDZLM8vX8qXpZNQ=="],
+
     "ci-info": ["[email protected]", "", {}, "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg=="],
 
     "clean-git-ref": ["[email protected]", "", {}, "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw=="],
 
     "cli-boxes": ["[email protected]", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
 
+    "cli-highlight": ["[email protected]", "", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="],
+
+    "cli-html": ["[email protected]", "", { "dependencies": { "ansi-align": "^3.0.1", "ansi-escapes": "^7.0.0", "boxen": "^8.0.1", "chalk": "^5.4.1", "chalk-string": "^3.0.1", "change-case": "^5.4.4", "cli-highlight": "^2.1.11", "cli-table3": "^0.6.5", "color-namer": "^1.4.0", "compose-function": "^3.0.3", "concat-stream": "^2.0.0", "env-paths": "^3.0.0", "he": "^1.2.0", "inline-style-parser": "^0.2.4", "languages-aliases": "^3.0.0", "longest-line": "0.0.3", "normalize-html-whitespace": "^1.0.0", "number-to-alphabet": "^1.0.0", "parse5": "^7.3.0", "romanize": "^1.1.1", "supports-hyperlinks": "^4.1.0", "terminal-size": "^4.0.0", "wrap-ansi": "^9.0.0", "yaml": "^2.8.0" }, "bin": { "html": "bin/html.js" } }, "sha512-H71WH7iAgLh7RN84qzhFxlqlP66Ek4wdqhm8ziJq23zKzrIFjM7GZo9fvUx/jtjB72GCdsHRcu+hgdLlfExWUg=="],
+
+    "cli-markdown": ["[email protected]", "", { "dependencies": { "@mdit/plugin-alert": "^0.22.1", "@types/marked": "^6.0.0", "cli-html": "^4.4.0", "concat-stream": "^2.0.0", "markdown-it": "^14.1.0", "markdown-it-abbr": "^2.0.0", "markdown-it-container": "^4.0.0", "markdown-it-deflist": "^3.0.0", "markdown-it-footnote": "^4.0.0", "markdown-it-ins": "^4.0.0", "markdown-it-mark": "^4.0.0", "markdown-it-sub": "^2.0.0", "markdown-it-sup": "^2.0.0", "markdown-it-task-lists": "^2.1.1" }, "bin": { "markdown": "bin/markdown.js", "md": "bin/markdown.js" } }, "sha512-4WFct6LuFxibmSvAXJTGTxZaVqT14jQ8x58lNXKk6afPgWZEFIkUVTCcEn2afBOb7q2bpGIvTRM58e7WcOLSyQ=="],
+
+    "cli-table3": ["[email protected]", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="],
+
     "cliui": ["[email protected]", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
 
     "clone": ["[email protected]", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
@@ -693,12 +728,20 @@
 
     "color-name": ["[email protected]", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
 
+    "color-namer": ["[email protected]", "", { "dependencies": { "chroma-js": "^1.3.4", "es6-weak-map": "^2.0.3" } }, "sha512-3mQMY9MJyfdV2uhe+xjQWcKHtYnPtl5svGjt89V2WWT2MlaLAd7C02886Wq7H1MTjjIIEa/NJLYPNF/Lhxhq2A=="],
+
     "color-string": ["[email protected]", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
 
+    "colors-option": ["[email protected]", "", { "dependencies": { "chalk": "^5.4.1", "is-plain-obj": "^4.1.0" } }, "sha512-FsAlu5KTTN+W6Xc4NpxNAhl8iCKwVBzjL7Y2ZK6G9zMv50AfMDlU7Mi16lzaDK8Iwpoq/GfAXX+WrYx38gfSHA=="],
+
     "comma-separated-tokens": ["[email protected]", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
 
     "common-ancestor-path": ["[email protected]", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="],
 
+    "compose-function": ["[email protected]", "", { "dependencies": { "arity-n": "^1.0.4" } }, "sha512-xzhzTJ5eC+gmIzvZq+C3kCJHsp9os6tJkrigDRZclyGtOKINbZtE8n1Tzmeh32jW+BUDPbvZpibwvJHBLGMVwg=="],
+
+    "concat-stream": ["[email protected]", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="],
+
     "content-disposition": ["[email protected]", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
 
     "content-type": ["[email protected]", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
@@ -729,6 +772,8 @@
 
     "csstype": ["[email protected]", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
 
+    "d": ["[email protected]", "", { "dependencies": { "es5-ext": "^0.10.64", "type": "^2.7.2" } }, "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw=="],
+
     "data-uri-to-buffer": ["[email protected]", "", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="],
 
     "dateformat": ["[email protected]", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
@@ -793,10 +838,12 @@
 
     "end-of-stream": ["[email protected]", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="],
 
-    "entities": ["entities@6.0.0", "", {}, "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw=="],
+    "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
 
     "env-paths": ["[email protected]", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
 
+    "environment": ["[email protected]", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
+
     "es-define-property": ["[email protected]", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
 
     "es-errors": ["[email protected]", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
@@ -805,6 +852,14 @@
 
     "es-object-atoms": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
 
+    "es5-ext": ["[email protected]", "", { "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", "esniff": "^2.0.1", "next-tick": "^1.1.0" } }, "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg=="],
+
+    "es6-iterator": ["[email protected]", "", { "dependencies": { "d": "1", "es5-ext": "^0.10.35", "es6-symbol": "^3.1.1" } }, "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g=="],
+
+    "es6-symbol": ["[email protected]", "", { "dependencies": { "d": "^1.0.2", "ext": "^1.7.0" } }, "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg=="],
+
+    "es6-weak-map": ["[email protected]", "", { "dependencies": { "d": "1", "es5-ext": "^0.10.46", "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.1" } }, "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA=="],
+
     "esast-util-from-estree": ["[email protected]", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
 
     "esast-util-from-js": ["[email protected]", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="],
@@ -817,6 +872,8 @@
 
     "escape-string-regexp": ["[email protected]", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
 
+    "esniff": ["[email protected]", "", { "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.62", "event-emitter": "^0.3.5", "type": "^2.7.2" } }, "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg=="],
+
     "estree-util-attach-comments": ["[email protected]", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
 
     "estree-util-build-jsx": ["[email protected]", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="],
@@ -833,6 +890,8 @@
 
     "etag": ["[email protected]", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
 
+    "event-emitter": ["[email protected]", "", { "dependencies": { "d": "1", "es5-ext": "~0.10.14" } }, "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA=="],
+
     "eventemitter3": ["[email protected]", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
 
     "events": ["[email protected]", "", {}, "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw=="],
@@ -853,6 +912,8 @@
 
     "exsolve": ["[email protected]", "", {}, "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg=="],
 
+    "ext": ["[email protected]", "", { "dependencies": { "type": "^2.7.2" } }, "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw=="],
+
     "extend": ["[email protected]", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
 
     "fast-content-type-parse": ["[email protected]", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="],
@@ -917,7 +978,7 @@
 
     "h3": ["[email protected]", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.4", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.0", "radix3": "^1.1.2", "ufo": "^1.6.1", "uncrypto": "^0.1.3" } }, "sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ=="],
 
-    "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+    "has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="],
 
     "has-property-descriptors": ["[email protected]", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
 
@@ -969,6 +1030,10 @@
 
     "hastscript": ["[email protected]", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
 
+    "he": ["[email protected]", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
+
+    "highlight.js": ["[email protected]", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="],
+
     "hono": ["[email protected]", "", {}, "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ=="],
 
     "hono-openapi": ["[email protected]", "", { "dependencies": { "json-schema-walker": "^2.0.0" }, "peerDependencies": { "@hono/arktype-validator": "^2.0.0", "@hono/effect-validator": "^1.2.0", "@hono/typebox-validator": "^0.2.0 || ^0.3.0", "@hono/valibot-validator": "^0.5.1", "@hono/zod-validator": "^0.4.1", "@sinclair/typebox": "^0.34.9", "@valibot/to-json-schema": "^1.0.0-beta.3", "arktype": "^2.0.0", "effect": "^3.11.3", "hono": "^4.6.13", "openapi-types": "^12.1.3", "valibot": "^1.0.0-beta.9", "zod": "^3.23.8", "zod-openapi": "^4.0.0" }, "optionalPeers": ["@hono/arktype-validator", "@hono/effect-validator", "@hono/typebox-validator", "@hono/valibot-validator", "@hono/zod-validator", "@sinclair/typebox", "@valibot/to-json-schema", "arktype", "effect", "hono", "valibot", "zod", "zod-openapi"] }, "sha512-LYr5xdtD49M7hEAduV1PftOMzuT8ZNvkyWfh1DThkLsIr4RkvDb12UxgIiFbwrJB6FLtFXLoOZL9x4IeDk2+VA=="],
@@ -1077,8 +1142,14 @@
 
     "language-map": ["[email protected]", "", {}, "sha512-n7gFZpe+DwEAX9cXVTw43i3wiudWDDtSn28RmdnS/HCPr284dQI/SztsamWanRr75oSlKSaGbV2nmWCTzGCoVg=="],
 
+    "languages-aliases": ["[email protected]", "", {}, "sha512-0TeT8ZQXq5y59hzowc2PSHkqDDEApXUpHl35BmwxG+8Uuy30ISpMMx51kdBHD3LoVmGgNxBsKcsV4HJClL4U2g=="],
+
     "leven": ["[email protected]", "", {}, "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA=="],
 
+    "linkify-it": ["[email protected]", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
+
+    "longest-line": ["[email protected]", "", { "dependencies": { "strip-ansi": "^3.0.0" } }, "sha512-ZRdPmYYhydc50iw8abrurJaWD+MGLaMOZrNOE2BzQIsumYBIbg07ByILhyiSXIczVdBxipotSrbdgt2JjWPeCg=="],
+
     "longest-streak": ["[email protected]", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
 
     "lru-cache": ["[email protected]", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
@@ -1091,6 +1162,26 @@
 
     "markdown-extensions": ["[email protected]", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
 
+    "markdown-it": ["[email protected]", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="],
+
+    "markdown-it-abbr": ["[email protected]", "", {}, "sha512-of7C8pXSjXjDojW4neNP+jD7inUYH/DO0Ca+K/4FUEccg0oHAEX/nfscw0jfz66PJbYWOAT9U8mjO21X5p6aAw=="],
+
+    "markdown-it-container": ["[email protected]", "", {}, "sha512-HaNccxUH0l7BNGYbFbjmGpf5aLHAMTinqRZQAEQbMr2cdD3z91Q6kIo1oUn1CQndkT03jat6ckrdRYuwwqLlQw=="],
+
+    "markdown-it-deflist": ["[email protected]", "", {}, "sha512-OxPmQ/keJZwbubjiQWOvKLHwpV2wZ5I3Smc81OjhwbfJsjdRrvD5aLTQxmZzzePeO0kbGzAo3Krk4QLgA8PWLg=="],
+
+    "markdown-it-footnote": ["[email protected]", "", {}, "sha512-WYJ7urf+khJYl3DqofQpYfEYkZKbmXmwxQV8c8mO/hGIhgZ1wOe7R4HLFNwqx7TjILbnC98fuyeSsin19JdFcQ=="],
+
+    "markdown-it-ins": ["[email protected]", "", {}, "sha512-sWbjK2DprrkINE4oYDhHdCijGT+MIDhEupjSHLXe5UXeVr5qmVxs/nTUVtgi0Oh/qtF+QKV0tNWDhQBEPxiMew=="],
+
+    "markdown-it-mark": ["[email protected]", "", {}, "sha512-YLhzaOsU9THO/cal0lUjfMjrqSMPjjyjChYM7oyj4DnyaXEzA8gnW6cVJeyCrCVeyesrY2PlEdUYJSPFYL4Nkg=="],
+
+    "markdown-it-sub": ["[email protected]", "", {}, "sha512-iCBKgwCkfQBRg2vApy9vx1C1Tu6D8XYo8NvevI3OlwzBRmiMtsJ2sXupBgEA7PPxiDwNni3qIUkhZ6j5wofDUA=="],
+
+    "markdown-it-sup": ["[email protected]", "", {}, "sha512-5VgmdKlkBd8sgXuoDoxMpiU+BiEt3I49GItBzzw7Mxq9CxvnhE/k09HFli09zgfFDRixDQDfDxi0mgBCXtaTvA=="],
+
+    "markdown-it-task-lists": ["[email protected]", "", {}, "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA=="],
+
     "markdown-table": ["[email protected]", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
 
     "marked": ["[email protected]", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
@@ -1137,6 +1228,8 @@
 
     "mdn-data": ["[email protected]", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
 
+    "mdurl": ["[email protected]", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
+
     "media-typer": ["[email protected]", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
 
     "merge-anything": ["[email protected]", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="],
@@ -1241,6 +1334,8 @@
 
     "mustache": ["[email protected]", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
 
+    "mz": ["[email protected]", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
+
     "nanoid": ["[email protected]", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
 
     "napi-build-utils": ["[email protected]", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
@@ -1249,6 +1344,8 @@
 
     "neotraverse": ["[email protected]", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="],
 
+    "next-tick": ["[email protected]", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="],
+
     "nlcst-to-string": ["[email protected]", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
 
     "node-abi": ["[email protected]", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg=="],
@@ -1263,10 +1360,14 @@
 
     "node-releases": ["[email protected]", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
 
+    "normalize-html-whitespace": ["[email protected]", "", {}, "sha512-9ui7CGtOOlehQu0t/OhhlmDyc71mKVlv+4vF+me4iZLPrNtRL2xoquEdfZxasC/bdQi/Hr3iTrpyRKIG+ocabA=="],
+
     "normalize-path": ["[email protected]", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
 
     "nth-check": ["[email protected]", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
 
+    "number-to-alphabet": ["[email protected]", "", {}, "sha512-5hahJfMZmQ78ydjHB0d9xrZiUOhEKD1u3BngbhfTs/3CCVbnOlbyAbSw8nIe+dpcRtJ+zOthw2YcG6DkfzlEgA=="],
+
     "object-assign": ["[email protected]", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
 
     "object-hash": ["[email protected]", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="],
@@ -1317,6 +1418,8 @@
 
     "parse5": ["[email protected]", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
 
+    "parse5-htmlparser2-tree-adapter": ["[email protected]", "", { "dependencies": { "parse5": "^6.0.1" } }, "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA=="],
+
     "parseurl": ["[email protected]", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
 
     "path-browserify": ["[email protected]", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
@@ -1373,6 +1476,8 @@
 
     "punycode": ["[email protected]", "", {}, "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="],
 
+    "punycode.js": ["[email protected]", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
+
     "qs": ["[email protected]", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
 
     "querystring": ["[email protected]", "", {}, "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g=="],
@@ -1439,6 +1544,10 @@
 
     "remeda": ["[email protected]", "", { "dependencies": { "type-fest": "^4.40.1" } }, "sha512-Ka6965m9Zu9OLsysWxVf3jdJKmp6+PKzDv7HWHinEevf0JOJ9y02YpjiC/sKxRpCqGhVyvm1U+0YIj+E6DMgKw=="],
 
+    "repeat-string": ["[email protected]", "", {}, "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w=="],
+
+    "require-directory": ["[email protected]", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
+
     "restructure": ["[email protected]", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="],
 
     "retext": ["[email protected]", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="],
@@ -1453,6 +1562,8 @@
 
     "rollup": ["[email protected]", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.1", "@rollup/rollup-android-arm64": "4.41.1", "@rollup/rollup-darwin-arm64": "4.41.1", "@rollup/rollup-darwin-x64": "4.41.1", "@rollup/rollup-freebsd-arm64": "4.41.1", "@rollup/rollup-freebsd-x64": "4.41.1", "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", "@rollup/rollup-linux-arm-musleabihf": "4.41.1", "@rollup/rollup-linux-arm64-gnu": "4.41.1", "@rollup/rollup-linux-arm64-musl": "4.41.1", "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-musl": "4.41.1", "@rollup/rollup-linux-s390x-gnu": "4.41.1", "@rollup/rollup-linux-x64-gnu": "4.41.1", "@rollup/rollup-linux-x64-musl": "4.41.1", "@rollup/rollup-win32-arm64-msvc": "4.41.1", "@rollup/rollup-win32-ia32-msvc": "4.41.1", "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw=="],
 
+    "romanize": ["[email protected]", "", { "dependencies": { "repeat-string": "^1.6.1" } }, "sha512-DpNfq5KrpHNT60jpyOtPyyZGVGNKoBbaMSkCo3m6Hl+4dLPB4jUR3Gs/Agbdi8dB314jjyQFl2+dRS7rqXy9Lw=="],
+
     "router": ["[email protected]", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
 
     "run-applescript": ["[email protected]", "", {}, "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A=="],
@@ -1573,14 +1684,22 @@
 
     "style-to-object": ["[email protected]", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="],
 
-    "supports-color": ["[email protected]", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+    "supports-color": ["[email protected]", "", {}, "sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ=="],
+
+    "supports-hyperlinks": ["[email protected]", "", { "dependencies": { "has-flag": "^5.0.1", "supports-color": "^10.0.0" } }, "sha512-6lY0rDZ5bbZhAPrwpz/nMR6XmeaFmh2itk7YnIyph2jblPmDcKMCPkSdLFTlaX8snBvg7OJmaOL3WRLqMEqcJQ=="],
 
     "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=="],
 
+    "terminal-size": ["[email protected]", "", {}, "sha512-rcdty1xZ2/BkWa4ANjWRp4JGpda2quksXIHgn5TMjNBPZfwzJIgR68DKfSYiTL+CZWowDX/sbOo5ME/FRURvYQ=="],
+
     "text-decoder": ["[email protected]", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="],
 
+    "thenify": ["[email protected]", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
+
+    "thenify-all": ["[email protected]", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
+
     "thread-stream": ["[email protected]", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="],
 
     "tiny-inflate": ["[email protected]", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
@@ -1613,12 +1732,18 @@
 
     "turndown": ["[email protected]", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="],
 
+    "type": ["[email protected]", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="],
+
     "type-fest": ["[email protected]", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
 
     "type-is": ["[email protected]", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
 
+    "typedarray": ["[email protected]", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="],
+
     "typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
 
+    "uc.micro": ["[email protected]", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
+
     "ufo": ["[email protected]", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
 
     "uint8array-extras": ["[email protected]", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
@@ -1741,6 +1866,8 @@
 
     "yallist": ["[email protected]", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
 
+    "yaml": ["[email protected]", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="],
+
     "yargs": ["[email protected]", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
 
     "yargs-parser": ["[email protected]", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
@@ -1841,6 +1968,14 @@
 
     "bl/buffer": ["[email protected]", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
 
+    "cli-highlight/chalk": ["[email protected]", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+
+    "cli-highlight/parse5": ["[email protected]", "", {}, "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="],
+
+    "cli-highlight/yargs": ["[email protected]", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="],
+
+    "cli-table3/string-width": ["[email protected]", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
     "eventsource/eventsource-parser": ["[email protected]", "", {}, "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA=="],
 
     "express/cookie": ["[email protected]", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
@@ -1849,6 +1984,8 @@
 
     "hast-util-to-parse5/property-information": ["[email protected]", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="],
 
+    "longest-line/strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^2.0.0" } }, "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg=="],
+
     "mdast-util-find-and-replace/escape-string-regexp": ["[email protected]", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
 
     "miniflare/acorn": ["[email protected]", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
@@ -1869,6 +2006,10 @@
 
     "parse-entities/@types/unist": ["@types/[email protected]", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
 
+    "parse5/entities": ["[email protected]", "", {}, "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw=="],
+
+    "parse5-htmlparser2-tree-adapter/parse5": ["[email protected]", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="],
+
     "pino-abstract-transport/split2": ["[email protected]", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
 
     "pino-pretty/chalk": ["[email protected]", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -1921,12 +2062,30 @@
 
     "bl/buffer/ieee754": ["[email protected]", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
 
+    "cli-highlight/chalk/ansi-styles": ["[email protected]", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+    "cli-highlight/chalk/supports-color": ["[email protected]", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+
+    "cli-highlight/yargs/cliui": ["[email protected]", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="],
+
+    "cli-highlight/yargs/string-width": ["[email protected]", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+    "cli-highlight/yargs/yargs-parser": ["[email protected]", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
+
+    "cli-table3/string-width/emoji-regex": ["[email protected]", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+    "cli-table3/string-width/strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+    "longest-line/strip-ansi/ansi-regex": ["[email protected]", "", {}, "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA=="],
+
     "opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["[email protected]", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
 
     "opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["[email protected]", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
 
     "pino-pretty/chalk/ansi-styles": ["[email protected]", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
 
+    "pino-pretty/chalk/supports-color": ["[email protected]", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+
     "prebuild-install/tar-fs/tar-stream": ["[email protected]", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
 
     "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
@@ -1987,6 +2146,26 @@
 
     "args/chalk/supports-color/has-flag": ["[email protected]", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
 
+    "cli-highlight/chalk/supports-color/has-flag": ["[email protected]", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+
+    "cli-highlight/yargs/cliui/strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+    "cli-highlight/yargs/cliui/wrap-ansi": ["[email protected]", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
+    "cli-highlight/yargs/string-width/emoji-regex": ["[email protected]", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+    "cli-highlight/yargs/string-width/strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+    "cli-table3/string-width/strip-ansi/ansi-regex": ["[email protected]", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+    "pino-pretty/chalk/supports-color/has-flag": ["[email protected]", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+
     "args/chalk/ansi-styles/color-convert/color-name": ["[email protected]", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
+
+    "cli-highlight/yargs/cliui/strip-ansi/ansi-regex": ["[email protected]", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+    "cli-highlight/yargs/cliui/wrap-ansi/ansi-styles": ["[email protected]", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+    "cli-highlight/yargs/string-width/strip-ansi/ansi-regex": ["[email protected]", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
   }
 }

+ 13 - 4
packages/function/src/api.ts

@@ -42,7 +42,11 @@ export class SyncServer extends DurableObject<Env> {
 
   async publish(key: string, content: any) {
     const sessionID = await this.getSessionID()
-    if (!key.startsWith(`session/info/${sessionID}`) && !key.startsWith(`session/message/${sessionID}/`))
+    if (
+      !key.startsWith(`session/info/${sessionID}`) &&
+      !key.startsWith(`session/message/${sessionID}/`) &&
+      !key.startsWith(`session/part/${sessionID}/`)
+    )
       return new Response("Error: Invalid key", { status: 400 })
 
     // store message
@@ -71,7 +75,7 @@ export class SyncServer extends DurableObject<Env> {
   }
 
   public async getData() {
-    const data = await this.ctx.storage.list()
+    const data = (await this.ctx.storage.list()) as Map<string, any>
     return Array.from(data.entries())
       .filter(([key, _]) => key.startsWith("session/"))
       .map(([key, content]) => ({ key, content }))
@@ -207,8 +211,13 @@ export default {
           return
         }
         if (type === "message") {
-          const [, messageID] = splits
-          messages[messageID] = d.content
+          messages[d.content.id] = {
+            parts: [],
+            ...d.content,
+          }
+        }
+        if (type === "part") {
+          messages[d.content.messageID].parts.push(d.content)
         }
       })
 

+ 1 - 0
packages/opencode/package.json

@@ -33,6 +33,7 @@
     "@openauthjs/openauth": "0.4.3",
     "@standard-schema/spec": "1.0.0",
     "ai": "catalog:",
+    "cli-markdown": "3.5.1",
     "decimal.js": "10.5.0",
     "diff": "8.0.2",
     "env-paths": "3.0.0",

+ 16 - 10
packages/opencode/src/cli/cmd/run.ts

@@ -9,6 +9,7 @@ import { Config } from "../../config/config"
 import { bootstrap } from "../bootstrap"
 import { MessageV2 } from "../../session/message-v2"
 import { Mode } from "../../session/mode"
+import { Identifier } from "../../id/id"
 
 const TOOL: Record<string, [string, string]> = {
   todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
@@ -83,14 +84,9 @@ export const RunCommand = cmd({
         return
       }
 
-      const isPiped = !process.stdout.isTTY
-
       UI.empty()
       UI.println(UI.logo())
       UI.empty()
-      const displayMessage = message.length > 300 ? message.slice(0, 300) + "..." : message
-      UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", displayMessage)
-      UI.empty()
 
       const cfg = await Config.get()
       if (cfg.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share) {
@@ -120,8 +116,10 @@ export const RunCommand = cmd({
         )
       }
 
+      let text = ""
       Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
-        if (evt.properties.sessionID !== session.id) return
+        if (evt.properties.part.sessionID !== session.id) return
+        if (evt.properties.part.messageID === messageID) return
         const part = evt.properties.part
 
         if (part.type === "tool" && part.state.status === "completed") {
@@ -130,13 +128,15 @@ export const RunCommand = cmd({
         }
 
         if (part.type === "text") {
-          if (part.text.includes("\n")) {
+          text = part.text
+
+          if (part.time?.end) {
             UI.empty()
-            UI.println(part.text)
+            UI.println(UI.markdown(text))
             UI.empty()
+            text = ""
             return
           }
-          printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
         }
       })
 
@@ -156,8 +156,10 @@ export const RunCommand = cmd({
 
       const mode = args.mode ? await Mode.get(args.mode) : await Mode.list().then((x) => x[0])
 
+      const messageID = Identifier.ascending("message")
       const result = await Session.chat({
         sessionID: session.id,
+        messageID,
         ...(mode.model
           ? mode.model
           : {
@@ -167,15 +169,19 @@ export const RunCommand = cmd({
         mode: mode.name,
         parts: [
           {
+            id: Identifier.ascending("part"),
+            sessionID: session.id,
+            messageID: messageID,
             type: "text",
             text: message,
           },
         ],
       })
 
+      const isPiped = !process.stdout.isTTY
       if (isPiped) {
         const match = result.parts.findLast((x) => x.type === "text")
-        if (match) process.stdout.write(match.text)
+        if (match) process.stdout.write(UI.markdown(match.text))
         if (errorMsg) process.stdout.write(errorMsg)
       }
       UI.empty()

+ 2 - 82
packages/opencode/src/cli/cmd/stats.ts

@@ -1,7 +1,4 @@
-import { Storage } from "../../storage/storage"
-import { MessageV2 } from "../../session/message-v2"
 import { cmd } from "./cmd"
-import { bootstrap } from "../bootstrap"
 
 interface SessionStats {
   totalSessions: number
@@ -27,87 +24,10 @@ interface SessionStats {
 
 export const StatsCommand = cmd({
   command: "stats",
-  handler: async () => {
-    await bootstrap({ cwd: process.cwd() }, async () => {
-      const stats: SessionStats = {
-        totalSessions: 0,
-        totalMessages: 0,
-        totalCost: 0,
-        totalTokens: {
-          input: 0,
-          output: 0,
-          reasoning: 0,
-          cache: {
-            read: 0,
-            write: 0,
-          },
-        },
-        toolUsage: {},
-        dateRange: {
-          earliest: Date.now(),
-          latest: 0,
-        },
-        days: 0,
-        costPerDay: 0,
-      }
-
-      const sessionMap = new Map<string, number>()
-
-      try {
-        for await (const messagePath of Storage.list("session/message")) {
-          try {
-            const message = await Storage.readJSON<MessageV2.Info>(messagePath)
-            if (!message.parts.find((part) => part.type === "step-finish")) continue
-
-            stats.totalMessages++
-
-            const sessionId = message.sessionID
-            sessionMap.set(sessionId, (sessionMap.get(sessionId) || 0) + 1)
-
-            if (message.time.created < stats.dateRange.earliest) {
-              stats.dateRange.earliest = message.time.created
-            }
-            if (message.time.created > stats.dateRange.latest) {
-              stats.dateRange.latest = message.time.created
-            }
-
-            if (message.role === "assistant") {
-              stats.totalCost += message.cost
-              stats.totalTokens.input += message.tokens.input
-              stats.totalTokens.output += message.tokens.output
-              stats.totalTokens.reasoning += message.tokens.reasoning
-              stats.totalTokens.cache.read += message.tokens.cache.read
-              stats.totalTokens.cache.write += message.tokens.cache.write
-
-              for (const part of message.parts) {
-                if (part.type === "tool") {
-                  stats.toolUsage[part.tool] = (stats.toolUsage[part.tool] || 0) + 1
-                }
-              }
-            }
-          } catch (e) {
-            continue
-          }
-        }
-      } catch (e) {
-        console.error("Failed to read storage:", e)
-        return
-      }
-
-      stats.totalSessions = sessionMap.size
-
-      if (stats.dateRange.latest > 0) {
-        const daysDiff = (stats.dateRange.latest - stats.dateRange.earliest) / (1000 * 60 * 60 * 24)
-        stats.days = Math.max(1, Math.ceil(daysDiff))
-        stats.costPerDay = stats.totalCost / stats.days
-      }
-
-      displayStats(stats)
-    })
-  },
+  handler: async () => {},
 })
 
-function displayStats(stats: SessionStats) {
+export function displayStats(stats: SessionStats) {
   const width = 56
 
   function renderRow(label: string, value: string): string {

+ 16 - 0
packages/opencode/src/cli/ui.ts

@@ -1,6 +1,8 @@
 import { z } from "zod"
 import { EOL } from "os"
 import { NamedError } from "../util/error"
+// @ts-ignore
+import cliMarkdown from "cli-markdown"
 
 export namespace UI {
   const LOGO = [
@@ -76,4 +78,18 @@ export namespace UI {
   export function error(message: string) {
     println(Style.TEXT_DANGER_BOLD + "Error: " + Style.TEXT_NORMAL + message)
   }
+
+  export function markdown(text: string): string {
+    const rendered = cliMarkdown(text, {
+      width: process.stdout.columns || 80,
+      firstHeading: false,
+      tab: 0,
+    }).trim()
+
+    // Remove leading space from each line
+    return rendered
+      .split("\n")
+      .map((line: string) => line.replace(/^ /, ""))
+      .join("\n")
+  }
 }

+ 1 - 0
packages/opencode/src/id/id.ts

@@ -6,6 +6,7 @@ export namespace Identifier {
     session: "ses",
     message: "msg",
     user: "usr",
+    part: "prt",
   } as const
 
   export function schema(prefix: keyof typeof prefixes) {

+ 11 - 2
packages/opencode/src/server/server.ts

@@ -269,6 +269,7 @@ export namespace Server {
         zValidator(
           "json",
           z.object({
+            messageID: z.string(),
             providerID: z.string(),
             modelID: z.string(),
           }),
@@ -405,7 +406,14 @@ export namespace Server {
               description: "List of messages",
               content: {
                 "application/json": {
-                  schema: resolver(MessageV2.Info.array()),
+                  schema: resolver(
+                    z
+                      .object({
+                        info: MessageV2.Info,
+                        parts: MessageV2.Part.array(),
+                      })
+                      .array(),
+                  ),
                 },
               },
             },
@@ -446,10 +454,11 @@ export namespace Server {
         zValidator(
           "json",
           z.object({
+            messageID: z.string(),
             providerID: z.string(),
             modelID: z.string(),
             mode: z.string(),
-            parts: MessageV2.UserPart.array(),
+            parts: z.union([MessageV2.FilePart, MessageV2.TextPart]).array(),
           }),
         ),
         async (c) => {

+ 229 - 215
packages/opencode/src/session/index.ts

@@ -12,6 +12,7 @@ import {
   type ProviderMetadata,
   type ModelMessage,
   stepCountIs,
+  type StreamTextResult,
 } from "ai"
 
 import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
@@ -190,7 +191,10 @@ export namespace Session {
     await Storage.writeJSON<ShareInfo>("session/share/" + id, share)
     await Share.sync("session/info/" + id, session)
     for (const msg of await messages(id)) {
-      await Share.sync("session/message/" + id + "/" + msg.id, msg)
+      await Share.sync("session/message/" + id + "/" + msg.info.id, msg.info)
+      for (const part of msg.parts) {
+        await Share.sync("session/part/" + id + "/" + msg.info.id + "/" + part.id, part)
+      }
     }
     return share
   }
@@ -220,13 +224,19 @@ export namespace Session {
   }
 
   export async function messages(sessionID: string) {
-    const result = [] as MessageV2.Info[]
+    const result = [] as {
+      info: MessageV2.Info
+      parts: MessageV2.Part[]
+    }[]
     const list = Storage.list("session/message/" + sessionID)
     for await (const p of list) {
       const read = await Storage.readJSON<MessageV2.Info>(p)
-      result.push(read)
+      result.push({
+        info: read,
+        parts: await parts(sessionID, read.id),
+      })
     }
-    result.sort((a, b) => (a.id > b.id ? 1 : -1))
+    result.sort((a, b) => (a.info.id > b.info.id ? 1 : -1))
     return result
   }
 
@@ -234,6 +244,16 @@ export namespace Session {
     return Storage.readJSON<MessageV2.Info>("session/message/" + sessionID + "/" + messageID)
   }
 
+  export async function parts(sessionID: string, messageID: string) {
+    const result = [] as MessageV2.Part[]
+    for await (const item of Storage.list("session/part/" + sessionID + "/" + messageID)) {
+      const read = await Storage.readJSON<MessageV2.Part>(item)
+      result.push(read)
+    }
+    result.sort((a, b) => (a.id > b.id ? 1 : -1))
+    return result
+  }
+
   export async function* list() {
     for await (const item of Storage.list("session/info")) {
       const sessionID = path.basename(item, ".json")
@@ -289,12 +309,21 @@ export namespace Session {
     })
   }
 
+  async function updatePart(part: MessageV2.Part) {
+    await Storage.writeJSON(["session", "part", part.sessionID, part.messageID, part.id].join("/"), part)
+    Bus.publish(MessageV2.Event.PartUpdated, {
+      part,
+    })
+    return part
+  }
+
   export async function chat(input: {
     sessionID: string
+    messageID: string
     providerID: string
     modelID: string
     mode?: string
-    parts: MessageV2.UserPart[]
+    parts: (MessageV2.TextPart | MessageV2.FilePart)[]
   }) {
     const l = log.clone().tag("session", input.sessionID)
     l.info("chatting")
@@ -306,16 +335,19 @@ export namespace Session {
     if (session.revert) {
       const trimmed = []
       for (const msg of msgs) {
-        if (msg.id > session.revert.messageID || (msg.id === session.revert.messageID && session.revert.part === 0)) {
-          await Storage.remove("session/message/" + input.sessionID + "/" + msg.id)
+        if (
+          msg.info.id > session.revert.messageID ||
+          (msg.info.id === session.revert.messageID && session.revert.part === 0)
+        ) {
+          await Storage.remove("session/message/" + input.sessionID + "/" + msg.info.id)
           await Bus.publish(MessageV2.Event.Removed, {
             sessionID: input.sessionID,
-            messageID: msg.id,
+            messageID: msg.info.id,
           })
           continue
         }
 
-        if (msg.id === session.revert.messageID) {
+        if (msg.info.id === session.revert.messageID) {
           if (session.revert.part === 0) break
           msg.parts = msg.parts.slice(0, session.revert.part)
         }
@@ -327,7 +359,7 @@ export namespace Session {
       })
     }
 
-    const previous = msgs.at(-1) as MessageV2.Assistant
+    const previous = msgs.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant
     const outputLimit = Math.min(model.info.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX
 
     // auto summarize if too long
@@ -346,12 +378,21 @@ export namespace Session {
 
     using abort = lock(input.sessionID)
 
-    const lastSummary = msgs.findLast((msg) => msg.role === "assistant" && msg.summary === true)
-    if (lastSummary) msgs = msgs.filter((msg) => msg.id >= lastSummary.id)
+    const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
+    if (lastSummary) msgs = msgs.filter((msg) => msg.info.id >= lastSummary.info.id)
+
+    const userMsg: MessageV2.Info = {
+      id: input.messageID,
+      role: "user",
+      sessionID: input.sessionID,
+      time: {
+        created: Date.now(),
+      },
+    }
 
     const app = App.info()
-    input.parts = await Promise.all(
-      input.parts.map(async (part): Promise<MessageV2.UserPart[]> => {
+    const userParts = await Promise.all(
+      input.parts.map(async (part): Promise<MessageV2.Part[]> => {
         if (part.type === "file") {
           const url = new URL(part.url)
           switch (url.protocol) {
@@ -406,11 +447,17 @@ export namespace Session {
                 })
                 return [
                   {
+                    id: Identifier.ascending("part"),
+                    messageID: userMsg.id,
+                    sessionID: input.sessionID,
                     type: "text",
                     synthetic: true,
                     text: `Called the Read tool with the following input: ${JSON.stringify(args)}`,
                   },
                   {
+                    id: Identifier.ascending("part"),
+                    messageID: userMsg.id,
+                    sessionID: input.sessionID,
                     type: "text",
                     synthetic: true,
                     text: result.output,
@@ -422,11 +469,17 @@ export namespace Session {
               FileTime.read(input.sessionID, filePath)
               return [
                 {
+                  id: Identifier.ascending("part"),
+                  messageID: userMsg.id,
+                  sessionID: input.sessionID,
                   type: "text",
                   text: `Called the Read tool with the following input: {\"filePath\":\"${pathname}\"}`,
                   synthetic: true,
                 },
                 {
+                  id: Identifier.ascending("part"),
+                  messageID: userMsg.id,
+                  sessionID: input.sessionID,
                   type: "file",
                   url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64"),
                   mime: part.mime,
@@ -440,7 +493,10 @@ export namespace Session {
     ).then((x) => x.flat())
 
     if (input.mode === "plan")
-      input.parts.push({
+      userParts.push({
+        id: Identifier.ascending("part"),
+        messageID: userMsg.id,
+        sessionID: input.sessionID,
         type: "text",
         text: PROMPT_PLAN,
         synthetic: true,
@@ -459,13 +515,15 @@ export namespace Session {
           ),
           ...MessageV2.toModelMessage([
             {
-              id: Identifier.ascending("message"),
-              role: "user",
-              sessionID: input.sessionID,
-              parts: input.parts,
-              time: {
-                created: Date.now(),
+              info: {
+                id: Identifier.ascending("message"),
+                role: "user",
+                sessionID: input.sessionID,
+                time: {
+                  created: Date.now(),
+                },
               },
+              parts: userParts,
             },
           ]),
         ],
@@ -479,17 +537,11 @@ export namespace Session {
         })
         .catch(() => {})
     }
-    const msg: MessageV2.Info = {
-      id: Identifier.ascending("message"),
-      role: "user",
-      sessionID: input.sessionID,
-      parts: input.parts,
-      time: {
-        created: Date.now(),
-      },
+    await updateMessage(userMsg)
+    for (const part of userParts) {
+      await updatePart(part)
     }
-    await updateMessage(msg)
-    msgs.push(msg)
+    msgs.push({ info: userMsg, parts: userParts })
 
     const mode = await Mode.get(input.mode ?? "build")
     let system = mode.prompt ? [mode.prompt] : SystemPrompt.provider(input.providerID, input.modelID)
@@ -499,10 +551,9 @@ export namespace Session {
     const [first, ...rest] = system
     system = [first, rest.join("\n")]
 
-    const next: MessageV2.Info = {
+    const assistantMsg: MessageV2.Info = {
       id: Identifier.ascending("message"),
       role: "assistant",
-      parts: [],
       system,
       path: {
         cwd: app.path.cwd,
@@ -522,7 +573,7 @@ export namespace Session {
       },
       sessionID: input.sessionID,
     }
-    await updateMessage(next)
+    await updateMessage(assistantMsg)
     const tools: Record<string, AITool> = {}
 
     for (const item of await Provider.tools(input.providerID)) {
@@ -531,20 +582,29 @@ export namespace Session {
         id: item.id as any,
         description: item.description,
         inputSchema: item.parameters as ZodSchema,
-        async execute(args, opts) {
+        async execute(args) {
           const result = await item.execute(args, {
             sessionID: input.sessionID,
             abort: abort.signal,
-            messageID: next.id,
-            metadata: async (val) => {
-              const match = next.parts.find(
-                (p): p is MessageV2.ToolPart => p.type === "tool" && p.id === opts.toolCallId,
-              )
+            messageID: assistantMsg.id,
+            metadata: async () => {
+              /*
+              const match = toolCalls[opts.toolCallId]
               if (match && match.state.status === "running") {
-                match.state.title = val.title
-                match.state.metadata = val.metadata
+                await updatePart({
+                  ...match,
+                  state: {
+                    title: val.title,
+                    metadata: val.metadata,
+                    status: "running",
+                    input: args.input,
+                    time: {
+                      start: Date.now(),
+                    },
+                  },
+                })
               }
-              await updateMessage(next)
+              */
             },
           })
           return result
@@ -582,10 +642,6 @@ export namespace Session {
       tools[key] = item
     }
 
-    let text: MessageV2.TextPart = {
-      type: "text",
-      text: "",
-    }
     const result = streamText({
       onError() {},
       maxRetries: 10,
@@ -619,9 +675,20 @@ export namespace Session {
         ],
       }),
     })
+    return processStream(assistantMsg, model.info, result)
+  }
+
+  async function processStream(
+    assistantMsg: MessageV2.Assistant,
+    model: ModelsDev.Model,
+    stream: StreamTextResult<Record<string, AITool>, never>,
+  ) {
     try {
-      for await (const value of result.fullStream) {
-        l.info("part", {
+      let currentText: MessageV2.TextPart | undefined
+      const toolCalls: Record<string, MessageV2.ToolPart> = {}
+
+      for await (const value of stream.fullStream) {
+        log.info("part", {
           type: value.type,
         })
         switch (value.type) {
@@ -629,88 +696,78 @@ export namespace Session {
             break
 
           case "tool-input-start":
-            next.parts.push({
+            const part = await updatePart({
+              id: Identifier.ascending("part"),
+              messageID: assistantMsg.id,
+              sessionID: assistantMsg.sessionID,
               type: "tool",
               tool: value.toolName,
-              id: value.id,
+              callID: value.id,
               state: {
                 status: "pending",
               },
             })
-            Bus.publish(MessageV2.Event.PartUpdated, {
-              part: next.parts[next.parts.length - 1],
-              sessionID: next.sessionID,
-              messageID: next.id,
-            })
+            toolCalls[value.id] = part as MessageV2.ToolPart
             break
 
           case "tool-input-delta":
             break
 
           case "tool-call": {
-            const match = next.parts.find(
-              (p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId,
-            )
+            const match = toolCalls[value.toolCallId]
             if (match) {
-              match.state = {
-                status: "running",
-                input: value.input,
-                time: {
-                  start: Date.now(),
+              const part = await updatePart({
+                ...match,
+                state: {
+                  status: "running",
+                  input: value.input,
+                  time: {
+                    start: Date.now(),
+                  },
                 },
-              }
-              Bus.publish(MessageV2.Event.PartUpdated, {
-                part: match,
-                sessionID: next.sessionID,
-                messageID: next.id,
               })
+              toolCalls[value.toolCallId] = part as MessageV2.ToolPart
             }
             break
           }
           case "tool-result": {
-            const match = next.parts.find(
-              (p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId,
-            )
+            const match = toolCalls[value.toolCallId]
             if (match && match.state.status === "running") {
-              match.state = {
-                status: "completed",
-                input: value.input,
-                output: value.output.output,
-                metadata: value.output.metadata,
-                title: value.output.title,
-                time: {
-                  start: match.state.time.start,
-                  end: Date.now(),
+              await updatePart({
+                ...match,
+                state: {
+                  status: "completed",
+                  input: value.input,
+                  output: value.output.output,
+                  metadata: value.output.metadata,
+                  title: value.output.title,
+                  time: {
+                    start: match.state.time.start,
+                    end: Date.now(),
+                  },
                 },
-              }
-              Bus.publish(MessageV2.Event.PartUpdated, {
-                part: match,
-                sessionID: next.sessionID,
-                messageID: next.id,
               })
+              delete toolCalls[value.toolCallId]
             }
             break
           }
 
           case "tool-error": {
-            const match = next.parts.find(
-              (p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId,
-            )
+            const match = toolCalls[value.toolCallId]
             if (match && match.state.status === "running") {
-              match.state = {
-                status: "error",
-                input: value.input,
-                error: (value.error as any).toString(),
-                time: {
-                  start: match.state.time.start,
-                  end: Date.now(),
+              await updatePart({
+                ...match,
+                state: {
+                  status: "error",
+                  input: value.input,
+                  error: (value.error as any).toString(),
+                  time: {
+                    start: match.state.time.start,
+                    end: Date.now(),
+                  },
                 },
-              }
-              Bus.publish(MessageV2.Event.PartUpdated, {
-                part: match,
-                sessionID: next.sessionID,
-                messageID: next.id,
               })
+              delete toolCalls[value.toolCallId]
             }
             break
           }
@@ -719,53 +776,71 @@ export namespace Session {
             throw value.error
 
           case "start-step":
-            next.parts.push({
+            await updatePart({
+              id: Identifier.ascending("part"),
+              messageID: assistantMsg.id,
+              sessionID: assistantMsg.sessionID,
               type: "step-start",
             })
             break
 
           case "finish-step":
-            const usage = getUsage(model.info, value.usage, value.providerMetadata)
-            next.cost += usage.cost
-            next.tokens = usage.tokens
-            next.parts.push({
+            const usage = getUsage(model, value.usage, value.providerMetadata)
+            assistantMsg.cost += usage.cost
+            assistantMsg.tokens = usage.tokens
+            await updatePart({
+              id: Identifier.ascending("part"),
+              messageID: assistantMsg.id,
+              sessionID: assistantMsg.sessionID,
               type: "step-finish",
               tokens: usage.tokens,
               cost: usage.cost,
             })
+            await updateMessage(assistantMsg)
             break
 
           case "text-start":
-            text = {
+            currentText = {
+              id: Identifier.ascending("part"),
+              messageID: assistantMsg.id,
+              sessionID: assistantMsg.sessionID,
               type: "text",
               text: "",
+              time: {
+                start: Date.now(),
+              },
             }
             break
 
           case "text":
-            if (text.text === "") next.parts.push(text)
-            text.text += value.text
+            if (currentText) {
+              currentText.text += value.text
+              await updatePart(currentText)
+            }
             break
 
           case "text-end":
-            Bus.publish(MessageV2.Event.PartUpdated, {
-              part: text,
-              sessionID: next.sessionID,
-              messageID: next.id,
-            })
+            if (currentText && currentText.text) {
+              currentText.time = {
+                start: Date.now(),
+                end: Date.now(),
+              }
+              await updatePart(currentText)
+            }
+            currentText = undefined
             break
 
           case "finish":
-            next.time.completed = Date.now()
+            assistantMsg.time.completed = Date.now()
+            await updateMessage(assistantMsg)
             break
 
           default:
-            l.info("unhandled", {
+            log.info("unhandled", {
               ...value,
             })
             continue
         }
-        await updateMessage(next)
       }
     } catch (e) {
       log.error("", {
@@ -773,7 +848,7 @@ export namespace Session {
       })
       switch (true) {
         case e instanceof DOMException && e.name === "AbortError":
-          next.error = new MessageV2.AbortedError(
+          assistantMsg.error = new MessageV2.AbortedError(
             { message: e.message },
             {
               cause: e,
@@ -781,44 +856,48 @@ export namespace Session {
           ).toObject()
           break
         case MessageV2.OutputLengthError.isInstance(e):
-          next.error = e
+          assistantMsg.error = e
           break
         case LoadAPIKeyError.isInstance(e):
-          next.error = new Provider.AuthError(
+          assistantMsg.error = new Provider.AuthError(
             {
-              providerID: input.providerID,
+              providerID: model.id,
               message: e.message,
             },
             { cause: e },
           ).toObject()
           break
         case e instanceof Error:
-          next.error = new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
+          assistantMsg.error = new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
           break
         default:
-          next.error = new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e })
+          assistantMsg.error = new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e })
       }
       Bus.publish(Event.Error, {
-        sessionID: next.sessionID,
-        error: next.error,
+        sessionID: assistantMsg.sessionID,
+        error: assistantMsg.error,
       })
     }
-    for (const part of next.parts) {
+    const p = await parts(assistantMsg.sessionID, assistantMsg.id)
+    for (const part of p) {
       if (part.type === "tool" && part.state.status !== "completed") {
-        part.state = {
-          status: "error",
-          error: "Tool execution aborted",
-          time: {
-            start: Date.now(),
-            end: Date.now(),
+        updatePart({
+          ...part,
+          state: {
+            status: "error",
+            error: "Tool execution aborted",
+            time: {
+              start: Date.now(),
+              end: Date.now(),
+            },
+            input: {},
           },
-          input: {},
-        }
+        })
       }
     }
-    next.time.completed = Date.now()
-    await updateMessage(next)
-    return next
+    assistantMsg.time.completed = Date.now()
+    await updateMessage(assistantMsg)
+    return { info: assistantMsg, parts: p }
   }
 
   export async function revert(_input: { sessionID: string; messageID: string; part: number }) {
@@ -867,8 +946,8 @@ export namespace Session {
   export async function summarize(input: { sessionID: string; providerID: string; modelID: string }) {
     using abort = lock(input.sessionID)
     const msgs = await messages(input.sessionID)
-    const lastSummary = msgs.findLast((msg) => msg.role === "assistant" && msg.summary === true)?.id
-    const filtered = msgs.filter((msg) => !lastSummary || msg.id >= lastSummary)
+    const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
+    const filtered = msgs.filter((msg) => !lastSummary || msg.info.id >= lastSummary.info.id)
     const model = await Provider.getModel(input.providerID, input.modelID)
     const app = App.info()
     const system = SystemPrompt.summarize(input.providerID)
@@ -876,7 +955,6 @@ export namespace Session {
     const next: MessageV2.Info = {
       id: Identifier.ascending("message"),
       role: "assistant",
-      parts: [],
       sessionID: input.sessionID,
       system,
       path: {
@@ -899,7 +977,6 @@ export namespace Session {
     }
     await updateMessage(next)
 
-    let text: MessageV2.TextPart | undefined
     const result = streamText({
       abortSignal: abort.signal,
       model: model.language,
@@ -921,81 +998,9 @@ export namespace Session {
           ],
         },
       ],
-      onStepFinish: async (step) => {
-        const usage = getUsage(model.info, step.usage, step.providerMetadata)
-        next.cost += usage.cost
-        next.tokens = usage.tokens
-        await updateMessage(next)
-        if (text) {
-          Bus.publish(MessageV2.Event.PartUpdated, {
-            part: text,
-            messageID: next.id,
-            sessionID: next.sessionID,
-          })
-        }
-        text = undefined
-      },
-      async onFinish(input) {
-        const usage = getUsage(model.info, input.usage, input.providerMetadata)
-        next.cost += usage.cost
-        next.tokens = usage.tokens
-        next.time.completed = Date.now()
-        await updateMessage(next)
-      },
     })
 
-    try {
-      for await (const value of result.fullStream) {
-        switch (value.type) {
-          case "text":
-            if (!text) {
-              text = {
-                type: "text",
-                text: value.text,
-              }
-              next.parts.push(text)
-            } else text.text += value.text
-            await updateMessage(next)
-            break
-        }
-      }
-    } catch (e: any) {
-      log.error("summarize stream error", {
-        error: e,
-      })
-      switch (true) {
-        case e instanceof DOMException && e.name === "AbortError":
-          next.error = new MessageV2.AbortedError(
-            { message: e.message },
-            {
-              cause: e,
-            },
-          ).toObject()
-          break
-        case MessageV2.OutputLengthError.isInstance(e):
-          next.error = e
-          break
-        case LoadAPIKeyError.isInstance(e):
-          next.error = new Provider.AuthError(
-            {
-              providerID: input.providerID,
-              message: e.message,
-            },
-            { cause: e },
-          ).toObject()
-          break
-        case e instanceof Error:
-          next.error = new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
-          break
-        default:
-          next.error = new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }).toObject()
-      }
-      Bus.publish(Event.Error, {
-        error: next.error,
-      })
-    }
-    next.time.completed = Date.now()
-    await updateMessage(next)
+    return processStream(next, model.info, result)
   }
 
   function lock(sessionID: string) {
@@ -1045,14 +1050,23 @@ export namespace Session {
     }
   }
 
-  export async function initialize(input: { sessionID: string; modelID: string; providerID: string }) {
+  export async function initialize(input: {
+    sessionID: string
+    modelID: string
+    providerID: string
+    messageID: string
+  }) {
     const app = App.info()
     await Session.chat({
       sessionID: input.sessionID,
+      messageID: input.messageID,
       providerID: input.providerID,
       modelID: input.modelID,
       parts: [
         {
+          id: Identifier.ascending("part"),
+          sessionID: input.sessionID,
+          messageID: input.messageID,
           type: "text",
           text: PROMPT_INITIALIZE.replace("${path}", app.path.root),
         },

+ 170 - 150
packages/opencode/src/session/message-v2.ts

@@ -4,6 +4,7 @@ import { Provider } from "../provider/provider"
 import { NamedError } from "../util/error"
 import { Message } from "./message"
 import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai"
+import { Identifier } from "../id/id"
 
 export namespace MessageV2 {
   export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
@@ -72,67 +73,69 @@ export namespace MessageV2 {
       ref: "ToolState",
     })
 
-  export const TextPart = z
-    .object({
-      type: z.literal("text"),
-      text: z.string(),
-      synthetic: z.boolean().optional(),
-    })
-    .openapi({
-      ref: "TextPart",
-    })
+  const PartBase = z.object({
+    id: z.string(),
+    sessionID: z.string(),
+    messageID: z.string(),
+  })
+
+  export const TextPart = PartBase.extend({
+    type: z.literal("text"),
+    text: z.string(),
+    synthetic: z.boolean().optional(),
+    time: z
+      .object({
+        start: z.number(),
+        end: z.number().optional(),
+      })
+      .optional(),
+  }).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 const ToolPart = PartBase.extend({
+    type: z.literal("tool"),
+    callID: 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 const FilePart = PartBase.extend({
+    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 const StepStartPart = PartBase.extend({
+    type: z.literal("step-start"),
+  }).openapi({
+    ref: "StepStartPart",
+  })
   export type StepStartPart = z.infer<typeof StepStartPart>
 
-  export const StepFinishPart = z
-    .object({
-      type: z.literal("step-finish"),
-      cost: z.number(),
-      tokens: z.object({
-        input: z.number(),
-        output: z.number(),
-        reasoning: z.number(),
-        cache: z.object({
-          read: z.number(),
-          write: z.number(),
-        }),
+  export const StepFinishPart = PartBase.extend({
+    type: z.literal("step-finish"),
+    cost: z.number(),
+    tokens: z.object({
+      input: z.number(),
+      output: z.number(),
+      reasoning: z.number(),
+      cache: z.object({
+        read: z.number(),
+        write: z.number(),
       }),
-    })
-    .openapi({
-      ref: "StepFinishPart",
-    })
+    }),
+  }).openapi({
+    ref: "StepFinishPart",
+  })
   export type StepFinishPart = z.infer<typeof StepFinishPart>
 
   const Base = z.object({
@@ -140,14 +143,8 @@ export namespace MessageV2 {
     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(),
     }),
@@ -156,16 +153,15 @@ export namespace MessageV2 {
   })
   export type User = z.infer<typeof User>
 
-  export const AssistantPart = z
-    .discriminatedUnion("type", [TextPart, ToolPart, StepStartPart, StepFinishPart])
+  export const Part = z
+    .discriminatedUnion("type", [TextPart, FilePart, ToolPart, StepStartPart, StepFinishPart])
     .openapi({
-      ref: "AssistantMessagePart",
+      ref: "Part",
     })
-  export type AssistantPart = z.infer<typeof AssistantPart>
+  export type Part = z.infer<typeof Part>
 
   export const Assistant = Base.extend({
     role: z.literal("assistant"),
-    parts: z.array(AssistantPart),
     time: z.object({
       created: z.number(),
       completed: z.number().optional(),
@@ -223,16 +219,14 @@ export namespace MessageV2 {
     PartUpdated: Bus.event(
       "message.part.updated",
       z.object({
-        part: AssistantPart,
-        sessionID: z.string(),
-        messageID: z.string(),
+        part: Part,
       }),
     ),
   }
 
   export function fromV1(v1: Message.Info) {
     if (v1.role === "assistant") {
-      const result: Assistant = {
+      const info: Assistant = {
         id: v1.id,
         sessionID: v1.metadata.sessionID,
         role: "assistant",
@@ -248,109 +242,135 @@ export namespace MessageV2 {
         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 parts = v1.parts.flatMap((part): Part[] => {
+        const base = {
+          id: Identifier.ascending("part"),
+          messageID: v1.id,
+          sessionID: v1.metadata.sessionID,
+        }
+        if (part.type === "text") {
+          return [
+            {
+              ...base,
+              type: "text",
+              text: part.text,
+            },
+          ]
+        }
+        if (part.type === "step-start") {
+          return [
+            {
+              ...base,
+              type: "step-start",
+            },
+          ]
+        }
+        if (part.type === "tool-invocation") {
+          return [
+            {
+              ...base,
+              type: "tool",
+              callID: 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,
-                      },
-                    }
+                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,
-                    }
+                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 []
-        }),
+                }
+                throw new Error("unknown tool invocation state")
+              })(),
+            },
+          ]
+        }
+        return []
+      })
+      return {
+        info,
+        parts,
       }
-      return result
     }
 
     if (v1.role === "user") {
-      const result: User = {
+      const info: 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
+      const parts = v1.parts.flatMap((part): Part[] => {
+        const base = {
+          id: Identifier.ascending("part"),
+          messageID: v1.id,
+          sessionID: v1.metadata.sessionID,
+        }
+        if (part.type === "text") {
+          return [
+            {
+              ...base,
+              type: "text",
+              text: part.text,
+            },
+          ]
+        }
+        if (part.type === "file") {
+          return [
+            {
+              ...base,
+              type: "file",
+              mime: part.mediaType,
+              filename: part.filename,
+              url: part.url,
+            },
+          ]
+        }
+        return []
+      })
+      return { info, parts }
     }
+
+    throw new Error("unknown message type")
   }
 
-  export function toModelMessage(input: Info[]): ModelMessage[] {
+  export function toModelMessage(
+    input: {
+      info: Info
+      parts: Part[]
+    }[],
+  ): ModelMessage[] {
     const result: UIMessage[] = []
 
     for (const msg of input) {
       if (msg.parts.length === 0) continue
-      if (msg.role === "user") {
+
+      if (msg.info.role === "user") {
         result.push({
-          id: msg.id,
+          id: msg.info.id,
           role: "user",
           parts: msg.parts.flatMap((part): UIMessage["parts"] => {
             if (part.type === "text")
@@ -374,9 +394,9 @@ export namespace MessageV2 {
         })
       }
 
-      if (msg.role === "assistant") {
+      if (msg.info.role === "assistant") {
         result.push({
-          id: msg.id,
+          id: msg.info.id,
           role: "assistant",
           parts: msg.parts.flatMap((part): UIMessage["parts"] => {
             if (part.type === "text")
@@ -398,7 +418,7 @@ export namespace MessageV2 {
                   {
                     type: ("tool-" + part.tool) as `tool-${string}`,
                     state: "output-available",
-                    toolCallId: part.id,
+                    toolCallId: part.callID,
                     input: part.state.input,
                     output: part.state.output,
                   },
@@ -408,7 +428,7 @@ export namespace MessageV2 {
                   {
                     type: ("tool-" + part.tool) as `tool-${string}`,
                     state: "output-error",
-                    toolCallId: part.id,
+                    toolCallId: part.callID,
                     input: part.state.input,
                     errorText: part.state.error,
                   },

+ 38 - 1
packages/opencode/src/storage/storage.ts

@@ -5,6 +5,7 @@ import path from "path"
 import z from "zod"
 import fs from "fs/promises"
 import { MessageV2 } from "../session/message-v2"
+import { Identifier } from "../id/id"
 
 export namespace Storage {
   const log = Log.create({ service: "storage" })
@@ -28,13 +29,49 @@ export namespace Storage {
           log.info("migrating to v2 message", { file })
           try {
             const result = MessageV2.fromV1(content)
-            await Bun.write(file, JSON.stringify(result, null, 2))
+            await Bun.write(
+              file,
+              JSON.stringify(
+                {
+                  ...result.info,
+                  parts: result.parts,
+                },
+                null,
+                2,
+              ),
+            )
           } catch (e) {
             await fs.rename(file, file.replace("storage", "broken"))
           }
         }
       } catch {}
     },
+    async (dir: string) => {
+      const files = new Bun.Glob("session/message/*/*.json").scanSync({
+        cwd: dir,
+        absolute: true,
+      })
+      for (const file of files) {
+        try {
+          const { parts, ...info } = await Bun.file(file).json()
+          if (!parts) continue
+          for (const part of parts) {
+            const id = Identifier.ascending("part")
+            await Bun.write(
+              [dir, "session", "part", info.sessionID, info.id, id + ".json"].join("/"),
+              JSON.stringify({
+                ...part,
+                id,
+                sessionID: info.sessionID,
+                messageID: info.id,
+                ...(part.type === "tool" ? { callID: part.id } : {}),
+              }),
+            )
+          }
+          await Bun.write(file, JSON.stringify(info, null, 2))
+        } catch (e) {}
+      }
+    },
   ]
 
   const state = App.state("storage", async () => {

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

@@ -4,6 +4,7 @@ import { z } from "zod"
 import { Session } from "../session"
 import { Bus } from "../bus"
 import { MessageV2 } from "../session/message-v2"
+import { Identifier } from "../id/id"
 
 export const TaskTool = Tool.define({
   id: "task",
@@ -16,9 +17,10 @@ export const TaskTool = Tool.define({
     const session = await Session.create(ctx.sessionID)
     const msg = (await Session.getMessage(ctx.sessionID, ctx.messageID)) as MessageV2.Assistant
 
-    function summary(input: MessageV2.Info) {
+    const parts: Record<string, MessageV2.Part> = {}
+    function summary(input: MessageV2.Part[]) {
       const result = []
-      for (const part of input.parts) {
+      for (const part of input) {
         if (part.type === "tool" && part.state.status === "completed") {
           result.push(part)
         }
@@ -26,12 +28,13 @@ export const TaskTool = Tool.define({
       return result
     }
 
-    const unsub = Bus.subscribe(MessageV2.Event.Updated, async (evt) => {
-      if (evt.properties.info.sessionID !== session.id) return
+    const unsub = Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
+      if (evt.properties.part.sessionID !== session.id) return
+      parts[evt.properties.part.id] = evt.properties.part
       ctx.metadata({
         title: params.description,
         metadata: {
-          summary: summary(evt.properties.info),
+          summary: Object.values(parts).sort((a, b) => a.id?.localeCompare(b.id)),
         },
       })
     })
@@ -39,12 +42,17 @@ export const TaskTool = Tool.define({
     ctx.abort.addEventListener("abort", () => {
       Session.abort(session.id)
     })
+    const messageID = Identifier.ascending("message")
     const result = await Session.chat({
+      messageID,
       sessionID: session.id,
       modelID: msg.modelID,
       providerID: msg.providerID,
       parts: [
         {
+          id: Identifier.ascending("part"),
+          messageID,
+          sessionID: session.id,
           type: "text",
           text: params.prompt,
         },
@@ -54,7 +62,7 @@ export const TaskTool = Tool.define({
     return {
       title: params.description,
       metadata: {
-        summary: summary(result),
+        summary: summary(result.parts),
       },
       output: result.parts.findLast((x) => x.type === "text")!.text,
     }

+ 69 - 40
packages/tui/internal/app/app.go

@@ -16,11 +16,17 @@ import (
 	"github.com/sst/opencode/internal/commands"
 	"github.com/sst/opencode/internal/components/toast"
 	"github.com/sst/opencode/internal/config"
+	"github.com/sst/opencode/internal/id"
 	"github.com/sst/opencode/internal/styles"
 	"github.com/sst/opencode/internal/theme"
 	"github.com/sst/opencode/internal/util"
 )
 
+type Message struct {
+	Info  opencode.MessageUnion
+	Parts []opencode.PartUnion
+}
+
 type App struct {
 	Info             opencode.App
 	Modes            []opencode.Mode
@@ -35,7 +41,7 @@ type App struct {
 	Provider         *opencode.Provider
 	Model            *opencode.Model
 	Session          *opencode.Session
-	Messages         []opencode.MessageUnion
+	Messages         []Message
 	Commands         commands.CommandRegistry
 	InitialModel     *string
 	InitialPrompt    *string
@@ -158,7 +164,7 @@ func New(
 		ModeIndex:     modeIndex,
 		Mode:          mode,
 		Session:       &opencode.Session{},
-		Messages:      []opencode.MessageUnion{},
+		Messages:      []Message{},
 		Commands:      commands.LoadFromConfig(configInfo),
 		InitialModel:  initialModel,
 		InitialPrompt: initialPrompt,
@@ -351,7 +357,7 @@ func (a *App) IsBusy() bool {
 	}
 
 	lastMessage := a.Messages[len(a.Messages)-1]
-	if casted, ok := lastMessage.(opencode.AssistantMessage); ok {
+	if casted, ok := lastMessage.Info.(opencode.AssistantMessage); ok {
 		return casted.Time.Completed == 0
 	}
 	return false
@@ -452,54 +458,67 @@ func (a *App) SendChatMessage(
 		cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session)))
 	}
 
-	optimisticParts := []opencode.UserMessagePart{{
-		Type: opencode.UserMessagePartTypeText,
-		Text: text,
+	message := opencode.UserMessage{
+		ID:        id.Ascending(id.Message),
+		SessionID: a.Session.ID,
+		Role:      opencode.UserMessageRoleUser,
+		Time: opencode.UserMessageTime{
+			Created: float64(time.Now().UnixMilli()),
+		},
+	}
+
+	parts := []opencode.PartUnion{opencode.TextPart{
+		ID:        id.Ascending(id.Part),
+		MessageID: message.ID,
+		SessionID: a.Session.ID,
+		Type:      opencode.TextPartTypeText,
+		Text:      text,
 	}}
 	if len(attachments) > 0 {
 		for _, attachment := range attachments {
-			optimisticParts = append(optimisticParts, opencode.UserMessagePart{
-				Type:     opencode.UserMessagePartTypeFile,
-				Filename: attachment.Filename.Value,
-				Mime:     attachment.Mime.Value,
-				URL:      attachment.URL.Value,
+			parts = append(parts, opencode.FilePart{
+				ID:        id.Ascending(id.Part),
+				MessageID: message.ID,
+				SessionID: a.Session.ID,
+				Type:      opencode.FilePartTypeFile,
+				Filename:  attachment.Filename.Value,
+				Mime:      attachment.Mime.Value,
+				URL:       attachment.URL.Value,
 			})
 		}
 	}
 
-	optimisticMessage := opencode.UserMessage{
-		ID:        fmt.Sprintf("optimistic-%d", time.Now().UnixNano()),
-		Role:      opencode.UserMessageRoleUser,
-		Parts:     optimisticParts,
-		SessionID: a.Session.ID,
-		Time: opencode.UserMessageTime{
-			Created: float64(time.Now().Unix()),
-		},
-	}
-
-	a.Messages = append(a.Messages, optimisticMessage)
-	cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: optimisticMessage}))
+	a.Messages = append(a.Messages, Message{Info: message, Parts: parts})
+	cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: message}))
 
 	cmds = append(cmds, func() tea.Msg {
-		parts := []opencode.UserMessagePartUnionParam{
-			opencode.TextPartParam{
-				Type: opencode.F(opencode.TextPartTypeText),
-				Text: opencode.F(text),
-			},
-		}
-		if len(attachments) > 0 {
-			for _, attachment := range attachments {
-				parts = append(parts, opencode.FilePartParam{
-					Mime:     attachment.Mime,
-					Type:     attachment.Type,
-					URL:      attachment.URL,
-					Filename: attachment.Filename,
+		partsParam := []opencode.SessionChatParamsPartUnion{}
+		for _, part := range parts {
+			switch casted := part.(type) {
+			case opencode.TextPart:
+				partsParam = append(partsParam, opencode.TextPartParam{
+					ID:        opencode.F(casted.ID),
+					MessageID: opencode.F(casted.MessageID),
+					SessionID: opencode.F(casted.SessionID),
+					Type:      opencode.F(casted.Type),
+					Text:      opencode.F(casted.Text),
+				})
+			case opencode.FilePart:
+				partsParam = append(partsParam, opencode.FilePartParam{
+					ID:        opencode.F(casted.ID),
+					Mime:      opencode.F(casted.Mime),
+					MessageID: opencode.F(casted.MessageID),
+					SessionID: opencode.F(casted.SessionID),
+					Type:      opencode.F(casted.Type),
+					URL:       opencode.F(casted.URL),
+					Filename:  opencode.F(casted.Filename),
 				})
 			}
 		}
 
 		_, err := a.Client.Session.Chat(ctx, a.Session.ID, opencode.SessionChatParams{
-			Parts:      opencode.F(parts),
+			Parts:      opencode.F(partsParam),
+			MessageID:  opencode.F(message.ID),
 			ProviderID: opencode.F(a.Provider.ID),
 			ModelID:    opencode.F(a.Model.ID),
 			Mode:       opencode.F(a.Mode.Name),
@@ -557,15 +576,25 @@ func (a *App) DeleteSession(ctx context.Context, sessionID string) error {
 	return nil
 }
 
-func (a *App) ListMessages(ctx context.Context, sessionId string) ([]opencode.Message, error) {
+func (a *App) ListMessages(ctx context.Context, sessionId string) ([]Message, error) {
 	response, err := a.Client.Session.Messages(ctx, sessionId)
 	if err != nil {
 		return nil, err
 	}
 	if response == nil {
-		return []opencode.Message{}, nil
+		return []Message{}, nil
+	}
+	messages := []Message{}
+	for _, message := range *response {
+		msg := Message{
+			Info:  message.Info.AsUnion(),
+			Parts: []opencode.PartUnion{},
+		}
+		for _, part := range message.Parts {
+			msg.Parts = append(msg.Parts, part.AsUnion())
+		}
+		messages = append(messages, msg)
 	}
-	messages := *response
 	return messages, nil
 }
 

+ 25 - 18
packages/tui/internal/components/chat/messages.go

@@ -106,6 +106,13 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				m.viewport.GotoBottom()
 			}
 		}
+	case opencode.EventListResponseEventMessagePartUpdated:
+		if msg.Properties.Part.SessionID == m.app.Session.ID {
+			m.renderView(m.width)
+			if m.tail {
+				m.viewport.GotoBottom()
+			}
+		}
 	}
 
 	viewport, cmd := m.viewport.Update(msg)
@@ -131,16 +138,16 @@ func (m *messagesComponent) renderView(width int) {
 		var content string
 		var cached bool
 
-		switch casted := message.(type) {
+		switch casted := message.Info.(type) {
 		case opencode.UserMessage:
 		userLoop:
-			for partIndex, part := range casted.Parts {
-				switch part := part.AsUnion().(type) {
+			for partIndex, part := range message.Parts {
+				switch part := part.(type) {
 				case opencode.TextPart:
-					remainingParts := casted.Parts[partIndex+1:]
+					remainingParts := message.Parts[partIndex+1:]
 					fileParts := make([]opencode.FilePart, 0)
 					for _, part := range remainingParts {
-						switch part := part.AsUnion().(type) {
+						switch part := part.(type) {
 						case opencode.FilePart:
 							fileParts = append(fileParts, part)
 						}
@@ -181,7 +188,7 @@ func (m *messagesComponent) renderView(width int) {
 					if !cached {
 						content = renderText(
 							m.app,
-							message,
+							message.Info,
 							part.Text,
 							m.app.Info.User,
 							m.showToolDetails,
@@ -202,12 +209,12 @@ func (m *messagesComponent) renderView(width int) {
 
 		case opencode.AssistantMessage:
 			hasTextPart := false
-			for partIndex, p := range casted.Parts {
-				switch part := p.AsUnion().(type) {
+			for partIndex, p := range message.Parts {
+				switch part := p.(type) {
 				case opencode.TextPart:
 					hasTextPart = true
 					finished := casted.Time.Completed > 0
-					remainingParts := casted.Parts[partIndex+1:]
+					remainingParts := message.Parts[partIndex+1:]
 					toolCallParts := make([]opencode.ToolPart, 0)
 
 					// sometimes tool calls happen without an assistant message
@@ -222,7 +229,7 @@ func (m *messagesComponent) renderView(width int) {
 						if !remaining {
 							break
 						}
-						switch part := part.AsUnion().(type) {
+						switch part := part.(type) {
 						case opencode.TextPart:
 							// we only want tool calls associated with the current text part.
 							// if we hit another text part, we're done.
@@ -238,13 +245,13 @@ func (m *messagesComponent) renderView(width int) {
 					}
 
 					if finished {
-						key := m.cache.GenerateKey(casted.ID, p.Text, width, m.showToolDetails, m.selectedPart == m.partCount)
+						key := m.cache.GenerateKey(casted.ID, part.Text, width, m.showToolDetails, m.selectedPart == m.partCount)
 						content, cached = m.cache.Get(key)
 						if !cached {
 							content = renderText(
 								m.app,
-								message,
-								p.Text,
+								message.Info,
+								part.Text,
 								casted.ModelID,
 								m.showToolDetails,
 								m.partCount == m.selectedPart,
@@ -257,8 +264,8 @@ func (m *messagesComponent) renderView(width int) {
 					} else {
 						content = renderText(
 							m.app,
-							message,
-							p.Text,
+							message.Info,
+							part.Text,
 							casted.ModelID,
 							m.showToolDetails,
 							m.partCount == m.selectedPart,
@@ -268,7 +275,7 @@ func (m *messagesComponent) renderView(width int) {
 						)
 					}
 					if content != "" {
-						m = m.updateSelected(content, p.Text)
+						m = m.updateSelected(content, part.Text)
 						blocks = append(blocks, content)
 					}
 				case opencode.ToolPart:
@@ -314,7 +321,7 @@ func (m *messagesComponent) renderView(width int) {
 		}
 
 		error := ""
-		if assistant, ok := message.(opencode.AssistantMessage); ok {
+		if assistant, ok := message.Info.(opencode.AssistantMessage); ok {
 			switch err := assistant.Error.AsUnion().(type) {
 			case nil:
 			case opencode.AssistantMessageErrorMessageOutputLengthError:
@@ -386,7 +393,7 @@ func (m *messagesComponent) header(width int) string {
 	contextWindow := m.app.Model.Limit.Context
 
 	for _, message := range m.app.Messages {
-		if assistant, ok := message.(opencode.AssistantMessage); ok {
+		if assistant, ok := message.Info.(opencode.AssistantMessage); ok {
 			cost += assistant.Cost
 			usage := assistant.Tokens
 			if usage.Output > 0 {

+ 96 - 0
packages/tui/internal/id/id.go

@@ -0,0 +1,96 @@
+package id
+
+import (
+	"crypto/rand"
+	"encoding/hex"
+	"fmt"
+	"strings"
+	"sync"
+	"time"
+)
+
+const (
+	PrefixSession = "ses"
+	PrefixMessage = "msg"
+	PrefixUser    = "usr"
+	PrefixPart    = "prt"
+)
+
+const length = 26
+
+var (
+	lastTimestamp int64
+	counter       int64
+	mu            sync.Mutex
+)
+
+type Prefix string
+
+const (
+	Session Prefix = PrefixSession
+	Message Prefix = PrefixMessage
+	User    Prefix = PrefixUser
+	Part    Prefix = PrefixPart
+)
+
+func ValidatePrefix(id string, prefix Prefix) bool {
+	return strings.HasPrefix(id, string(prefix))
+}
+
+func Ascending(prefix Prefix, given ...string) string {
+	return generateID(prefix, false, given...)
+}
+
+func Descending(prefix Prefix, given ...string) string {
+	return generateID(prefix, true, given...)
+}
+
+func generateID(prefix Prefix, descending bool, given ...string) string {
+	if len(given) > 0 && given[0] != "" {
+		if !strings.HasPrefix(given[0], string(prefix)) {
+			panic(fmt.Sprintf("ID %s does not start with %s", given[0], string(prefix)))
+		}
+		return given[0]
+	}
+	
+	return generateNewID(prefix, descending)
+}
+
+func randomBase62(length int) string {
+	const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+	result := make([]byte, length)
+	bytes := make([]byte, length)
+	rand.Read(bytes)
+	
+	for i := 0; i < length; i++ {
+		result[i] = chars[bytes[i]%62]
+	}
+	
+	return string(result)
+}
+
+func generateNewID(prefix Prefix, descending bool) string {
+	mu.Lock()
+	defer mu.Unlock()
+	
+	currentTimestamp := time.Now().UnixMilli()
+	
+	if currentTimestamp != lastTimestamp {
+		lastTimestamp = currentTimestamp
+		counter = 0
+	}
+	counter++
+	
+	now := uint64(currentTimestamp)*0x1000 + uint64(counter)
+	
+	if descending {
+		now = ^now
+	}
+	
+	timeBytes := make([]byte, 6)
+	for i := 0; i < 6; i++ {
+		timeBytes[i] = byte((now >> (40 - 8*i)) & 0xff)
+	}
+	
+	return string(prefix) + "_" + hex.EncodeToString(timeBytes) + randomBase62(length-12)
+}

+ 61 - 42
packages/tui/internal/tui/tui.go

@@ -5,6 +5,7 @@ import (
 	"log/slog"
 	"os"
 	"os/exec"
+	"slices"
 	"strings"
 	"time"
 
@@ -364,55 +365,76 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case opencode.EventListResponseEventSessionDeleted:
 		if a.app.Session != nil && msg.Properties.Info.ID == a.app.Session.ID {
 			a.app.Session = &opencode.Session{}
-			a.app.Messages = []opencode.MessageUnion{}
+			a.app.Messages = []app.Message{}
 		}
 		return a, toast.NewSuccessToast("Session deleted successfully")
 	case opencode.EventListResponseEventSessionUpdated:
 		if msg.Properties.Info.ID == a.app.Session.ID {
 			a.app.Session = &msg.Properties.Info
 		}
-	case opencode.EventListResponseEventMessageUpdated:
-		if msg.Properties.Info.SessionID == a.app.Session.ID {
-			exists := false
-			optimisticReplaced := false
-
-			// First check if this is replacing an optimistic message
-			if msg.Properties.Info.Role == opencode.MessageRoleUser {
-				// Look for optimistic messages to replace
-				for i, m := range a.app.Messages {
-					switch m := m.(type) {
-					case opencode.UserMessage:
-						if strings.HasPrefix(m.ID, "optimistic-") && m.Role == opencode.UserMessageRoleUser {
-							// Replace the optimistic message with the real one
-							a.app.Messages[i] = msg.Properties.Info.AsUnion()
-							exists = true
-							optimisticReplaced = true
-							break
-						}
+	case opencode.EventListResponseEventMessagePartUpdated:
+		slog.Info("message part updated", "message", msg.Properties.Part.MessageID, "part", msg.Properties.Part.ID)
+		if msg.Properties.Part.SessionID == a.app.Session.ID {
+			messageIndex := slices.IndexFunc(a.app.Messages, func(m app.Message) bool {
+				switch casted := m.Info.(type) {
+				case opencode.UserMessage:
+					return casted.ID == msg.Properties.Part.MessageID
+				case opencode.AssistantMessage:
+					return casted.ID == msg.Properties.Part.MessageID
+				}
+				return false
+			})
+			if messageIndex > -1 {
+				message := a.app.Messages[messageIndex]
+				partIndex := slices.IndexFunc(message.Parts, func(p opencode.PartUnion) bool {
+					switch casted := p.(type) {
+					case opencode.TextPart:
+						return casted.ID == msg.Properties.Part.ID
+					case opencode.FilePart:
+						return casted.ID == msg.Properties.Part.ID
+					case opencode.ToolPart:
+						return casted.ID == msg.Properties.Part.ID
+					case opencode.StepStartPart:
+						return casted.ID == msg.Properties.Part.ID
+					case opencode.StepFinishPart:
+						return casted.ID == msg.Properties.Part.ID
 					}
+					return false
+				})
+				if partIndex > -1 {
+					message.Parts[partIndex] = msg.Properties.Part.AsUnion()
 				}
+				if partIndex == -1 {
+					message.Parts = append(message.Parts, msg.Properties.Part.AsUnion())
+				}
+				a.app.Messages[messageIndex] = message
 			}
-
-			// If not replacing optimistic, check for existing message with same ID
-			if !optimisticReplaced {
-				for i, m := range a.app.Messages {
-					var id string
-					switch m := m.(type) {
-					case opencode.UserMessage:
-						id = m.ID
-					case opencode.AssistantMessage:
-						id = m.ID
-					}
-					if id == msg.Properties.Info.ID {
-						a.app.Messages[i] = msg.Properties.Info.AsUnion()
-						exists = true
-						break
-					}
+		}
+	case opencode.EventListResponseEventMessageUpdated:
+		if msg.Properties.Info.SessionID == a.app.Session.ID {
+			matchIndex := slices.IndexFunc(a.app.Messages, func(m app.Message) bool {
+				switch casted := m.Info.(type) {
+				case opencode.UserMessage:
+					return casted.ID == msg.Properties.Info.ID
+				case opencode.AssistantMessage:
+					return casted.ID == msg.Properties.Info.ID
+				}
+				return false
+			})
+
+			if matchIndex > -1 {
+				match := a.app.Messages[matchIndex]
+				a.app.Messages[matchIndex] = app.Message{
+					Info:  msg.Properties.Info.AsUnion(),
+					Parts: match.Parts,
 				}
 			}
 
-			if !exists {
-				a.app.Messages = append(a.app.Messages, msg.Properties.Info.AsUnion())
+			if matchIndex == -1 {
+				a.app.Messages = append(a.app.Messages, app.Message{
+					Info:  msg.Properties.Info.AsUnion(),
+					Parts: []opencode.PartUnion{},
+				})
 			}
 		}
 	case opencode.EventListResponseEventSessionError:
@@ -473,10 +495,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			return a, toast.NewErrorToast("Failed to open session")
 		}
 		a.app.Session = msg
-		a.app.Messages = make([]opencode.MessageUnion, 0)
-		for _, message := range messages {
-			a.app.Messages = append(a.app.Messages, message.AsUnion())
-		}
+		a.app.Messages = messages
 		return a, util.CmdHandler(app.SessionLoadedMsg{})
 	case app.ModelSelectedMsg:
 		a.app.Provider = &msg.Provider
@@ -837,7 +856,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
 			return a, nil
 		}
 		a.app.Session = &opencode.Session{}
-		a.app.Messages = []opencode.MessageUnion{}
+		a.app.Messages = []app.Message{}
 		cmds = append(cmds, util.CmdHandler(app.SessionClearedMsg{}))
 	case commands.SessionListCommand:
 		sessionDialog := dialog.NewSessionDialog(a.app)

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

@@ -1,4 +1,4 @@
 configured_endpoints: 22
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-eb25bb3673f94d0e98a7036e2a2b0ed7ad63d1598665f2d5e091ec0835273798.yml
-openapi_spec_hash: 62f6a8a06aaa4f4ae13e85d56652724f
-config_hash: 589ec6a935a43a3c49a325ece86cbda2
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-352994eb17f76d9472b0f0176efacf77a200a6fab2db28d1cfcd29451b211d7a.yml
+openapi_spec_hash: f01cd3de8c7cf0c9fd513896e81986de
+config_hash: 3695cfc829cfaae14490850b4a1ed282

+ 13 - 10
packages/tui/sdk/README.md

@@ -49,11 +49,14 @@ import (
 
 func main() {
 	client := opencode.NewClient()
-	events, err := client.Event.List(context.TODO())
+	stream := client.Event.ListStreaming(context.TODO())
+	for stream.Next() {
+		fmt.Printf("%+v\n", stream.Current())
+	}
+	err := stream.Err()
 	if err != nil {
 		panic(err.Error())
 	}
-	fmt.Printf("%+v\n", events)
 }
 
 ```
@@ -171,14 +174,14 @@ When the API returns a non-success status code, we return an error with type
 To handle errors, we recommend that you use the `errors.As` pattern:
 
 ```go
-_, err := client.Event.List(context.TODO())
-if err != nil {
+stream := client.Event.ListStreaming(context.TODO())
+if stream.Err() != nil {
 	var apierr *opencode.Error
-	if errors.As(err, &apierr) {
+	if errors.As(stream.Err(), &apierr) {
 		println(string(apierr.DumpRequest(true)))  // Prints the serialized HTTP request
 		println(string(apierr.DumpResponse(true))) // Prints the serialized HTTP response
 	}
-	panic(err.Error()) // GET "/event": 400 Bad Request { ... }
+	panic(stream.Err().Error()) // GET "/event": 400 Bad Request { ... }
 }
 ```
 
@@ -196,7 +199,7 @@ To set a per-retry timeout, use `option.WithRequestTimeout()`.
 // This sets the timeout for the request, including all the retries.
 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
 defer cancel()
-client.Event.List(
+client.Event.ListStreaming(
 	ctx,
 	// This sets the per-retry timeout
 	option.WithRequestTimeout(20*time.Second),
@@ -231,7 +234,7 @@ client := opencode.NewClient(
 )
 
 // Override per-request:
-client.Event.List(context.TODO(), option.WithMaxRetries(5))
+client.Event.ListStreaming(context.TODO(), option.WithMaxRetries(5))
 ```
 
 ### Accessing raw response data (e.g. response headers)
@@ -242,8 +245,8 @@ you need to examine response headers, status codes, or other details.
 ```go
 // Create a variable to store the HTTP response
 var response *http.Response
-events, err := client.Event.List(context.TODO(), option.WithResponseInto(&response))
-if err != nil {
+stream := client.Event.ListStreaming(context.TODO(), option.WithResponseInto(&response))
+if stream.Err() != nil {
 	// handle error
 }
 fmt.Printf("%+v\n", events)

+ 4 - 4
packages/tui/sdk/api.md

@@ -77,15 +77,15 @@ 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#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#UserMessagePartUnionParam">UserMessagePartUnionParam</a>
 
 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#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#Part">Part</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#StepFinishPart">StepFinishPart</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#ToolPart">ToolPart</a>
@@ -94,7 +94,7 @@ 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#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>
+- <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#SessionMessagesResponse">SessionMessagesResponse</a>
 
 Methods:
 
@@ -104,7 +104,7 @@ Methods:
 - <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#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="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#SessionMessagesResponse">SessionMessagesResponse</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}/summarize">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Summarize">Summarize</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#SessionSummarizeParams">SessionSummarizeParams</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}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unshare">Unshare</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>

+ 36 - 8
packages/tui/sdk/client_test.go

@@ -38,7 +38,7 @@ func TestUserAgentHeader(t *testing.T) {
 			},
 		}),
 	)
-	client.Event.List(context.Background())
+	client.Event.ListStreaming(context.Background())
 	if userAgent != fmt.Sprintf("Opencode/Go %s", internal.PackageVersion) {
 		t.Errorf("Expected User-Agent to be correct, but got: %#v", userAgent)
 	}
@@ -61,7 +61,11 @@ func TestRetryAfter(t *testing.T) {
 			},
 		}),
 	)
-	_, err := client.Event.List(context.Background())
+	stream := client.Event.ListStreaming(context.Background())
+	for stream.Next() {
+		// ...
+	}
+	err := stream.Err()
 	if err == nil {
 		t.Error("Expected there to be a cancel error")
 	}
@@ -95,7 +99,11 @@ func TestDeleteRetryCountHeader(t *testing.T) {
 		}),
 		option.WithHeaderDel("X-Stainless-Retry-Count"),
 	)
-	_, err := client.Event.List(context.Background())
+	stream := client.Event.ListStreaming(context.Background())
+	for stream.Next() {
+		// ...
+	}
+	err := stream.Err()
 	if err == nil {
 		t.Error("Expected there to be a cancel error")
 	}
@@ -124,7 +132,11 @@ func TestOverwriteRetryCountHeader(t *testing.T) {
 		}),
 		option.WithHeader("X-Stainless-Retry-Count", "42"),
 	)
-	_, err := client.Event.List(context.Background())
+	stream := client.Event.ListStreaming(context.Background())
+	for stream.Next() {
+		// ...
+	}
+	err := stream.Err()
 	if err == nil {
 		t.Error("Expected there to be a cancel error")
 	}
@@ -152,7 +164,11 @@ func TestRetryAfterMs(t *testing.T) {
 			},
 		}),
 	)
-	_, err := client.Event.List(context.Background())
+	stream := client.Event.ListStreaming(context.Background())
+	for stream.Next() {
+		// ...
+	}
+	err := stream.Err()
 	if err == nil {
 		t.Error("Expected there to be a cancel error")
 	}
@@ -174,7 +190,11 @@ func TestContextCancel(t *testing.T) {
 	)
 	cancelCtx, cancel := context.WithCancel(context.Background())
 	cancel()
-	_, err := client.Event.List(cancelCtx)
+	stream := client.Event.ListStreaming(cancelCtx)
+	for stream.Next() {
+		// ...
+	}
+	err := stream.Err()
 	if err == nil {
 		t.Error("Expected there to be a cancel error")
 	}
@@ -193,7 +213,11 @@ func TestContextCancelDelay(t *testing.T) {
 	)
 	cancelCtx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
 	defer cancel()
-	_, err := client.Event.List(cancelCtx)
+	stream := client.Event.ListStreaming(cancelCtx)
+	for stream.Next() {
+		// ...
+	}
+	err := stream.Err()
 	if err == nil {
 		t.Error("expected there to be a cancel error")
 	}
@@ -218,7 +242,11 @@ func TestContextDeadline(t *testing.T) {
 				},
 			}),
 		)
-		_, err := client.Event.List(deadlineCtx)
+		stream := client.Event.ListStreaming(deadlineCtx)
+		for stream.Next() {
+			// ...
+		}
+		err := stream.Err()
 		if err == nil {
 			t.Error("expected there to be a deadline error")
 		}

+ 2 - 6
packages/tui/sdk/event.go

@@ -610,18 +610,14 @@ func (r eventListResponseEventMessagePartUpdatedJSON) RawJSON() string {
 func (r EventListResponseEventMessagePartUpdated) implementsEventListResponse() {}
 
 type EventListResponseEventMessagePartUpdatedProperties struct {
-	MessageID string                                                 `json:"messageID,required"`
-	Part      AssistantMessagePart                                   `json:"part,required"`
-	SessionID string                                                 `json:"sessionID,required"`
-	JSON      eventListResponseEventMessagePartUpdatedPropertiesJSON `json:"-"`
+	Part Part                                                   `json:"part,required"`
+	JSON eventListResponseEventMessagePartUpdatedPropertiesJSON `json:"-"`
 }
 
 // eventListResponseEventMessagePartUpdatedPropertiesJSON contains the JSON
 // metadata for the struct [EventListResponseEventMessagePartUpdatedProperties]
 type eventListResponseEventMessagePartUpdatedPropertiesJSON struct {
-	MessageID   apijson.Field
 	Part        apijson.Field
-	SessionID   apijson.Field
 	raw         string
 	ExtraFields map[string]apijson.Field
 }

+ 2 - 2
packages/tui/sdk/scripts/lint

@@ -5,7 +5,7 @@ set -e
 cd "$(dirname "$0")/.."
 
 echo "==> Running Go build"
-go build .
+go build ./...
 
 echo "==> Checking tests compile"
-go test -run=^$ .
+go test -run=^$ ./...

+ 362 - 321
packages/tui/sdk/session.go

@@ -101,7 +101,7 @@ func (r *SessionService) Init(ctx context.Context, id string, body SessionInitPa
 }
 
 // List messages for a session
-func (r *SessionService) Messages(ctx context.Context, id string, opts ...option.RequestOption) (res *[]Message, err error) {
+func (r *SessionService) Messages(ctx context.Context, id string, opts ...option.RequestOption) (res *[]SessionMessagesResponse, err error) {
 	opts = append(r.Options[:], opts...)
 	if id == "" {
 		err = errors.New("missing required id parameter")
@@ -152,7 +152,6 @@ type AssistantMessage struct {
 	ID         string                 `json:"id,required"`
 	Cost       float64                `json:"cost,required"`
 	ModelID    string                 `json:"modelID,required"`
-	Parts      []AssistantMessagePart `json:"parts,required"`
 	Path       AssistantMessagePath   `json:"path,required"`
 	ProviderID string                 `json:"providerID,required"`
 	Role       AssistantMessageRole   `json:"role,required"`
@@ -171,7 +170,6 @@ type assistantMessageJSON struct {
 	ID          apijson.Field
 	Cost        apijson.Field
 	ModelID     apijson.Field
-	Parts       apijson.Field
 	Path        apijson.Field
 	ProviderID  apijson.Field
 	Role        apijson.Field
@@ -435,211 +433,23 @@ func (r AssistantMessageErrorName) IsKnown() bool {
 	return false
 }
 
-type AssistantMessagePart struct {
-	Type AssistantMessagePartType `json:"type,required"`
-	ID   string                   `json:"id"`
-	Cost float64                  `json:"cost"`
-	// This field can have the runtime type of [ToolPartState].
-	State     interface{} `json:"state"`
-	Synthetic bool        `json:"synthetic"`
-	Text      string      `json:"text"`
-	// This field can have the runtime type of
-	// [AssistantMessagePartStepFinishPartTokens].
-	Tokens interface{}              `json:"tokens"`
-	Tool   string                   `json:"tool"`
-	JSON   assistantMessagePartJSON `json:"-"`
-	union  AssistantMessagePartUnion
-}
-
-// assistantMessagePartJSON contains the JSON metadata for the struct
-// [AssistantMessagePart]
-type assistantMessagePartJSON struct {
-	Type        apijson.Field
-	ID          apijson.Field
-	Cost        apijson.Field
-	State       apijson.Field
-	Synthetic   apijson.Field
-	Text        apijson.Field
-	Tokens      apijson.Field
-	Tool        apijson.Field
-	raw         string
-	ExtraFields map[string]apijson.Field
-}
-
-func (r assistantMessagePartJSON) RawJSON() string {
-	return r.raw
-}
-
-func (r *AssistantMessagePart) UnmarshalJSON(data []byte) (err error) {
-	*r = AssistantMessagePart{}
-	err = apijson.UnmarshalRoot(data, &r.union)
-	if err != nil {
-		return err
-	}
-	return apijson.Port(r.union, &r)
-}
-
-// AsUnion returns a [AssistantMessagePartUnion] interface which you can cast to
-// the specific types for more type safety.
-//
-// Possible runtime types of the union are [TextPart], [ToolPart], [StepStartPart],
-// [AssistantMessagePartStepFinishPart].
-func (r AssistantMessagePart) AsUnion() AssistantMessagePartUnion {
-	return r.union
-}
-
-// Union satisfied by [TextPart], [ToolPart], [StepStartPart] or
-// [AssistantMessagePartStepFinishPart].
-type AssistantMessagePartUnion interface {
-	implementsAssistantMessagePart()
-}
-
-func init() {
-	apijson.RegisterUnion(
-		reflect.TypeOf((*AssistantMessagePartUnion)(nil)).Elem(),
-		"type",
-		apijson.UnionVariant{
-			TypeFilter:         gjson.JSON,
-			Type:               reflect.TypeOf(TextPart{}),
-			DiscriminatorValue: "text",
-		},
-		apijson.UnionVariant{
-			TypeFilter:         gjson.JSON,
-			Type:               reflect.TypeOf(ToolPart{}),
-			DiscriminatorValue: "tool",
-		},
-		apijson.UnionVariant{
-			TypeFilter:         gjson.JSON,
-			Type:               reflect.TypeOf(StepStartPart{}),
-			DiscriminatorValue: "step-start",
-		},
-		apijson.UnionVariant{
-			TypeFilter:         gjson.JSON,
-			Type:               reflect.TypeOf(AssistantMessagePartStepFinishPart{}),
-			DiscriminatorValue: "step-finish",
-		},
-	)
-}
-
-type AssistantMessagePartStepFinishPart struct {
-	Cost   float64                                  `json:"cost,required"`
-	Tokens AssistantMessagePartStepFinishPartTokens `json:"tokens,required"`
-	Type   AssistantMessagePartStepFinishPartType   `json:"type,required"`
-	JSON   assistantMessagePartStepFinishPartJSON   `json:"-"`
-}
-
-// assistantMessagePartStepFinishPartJSON contains the JSON metadata for the struct
-// [AssistantMessagePartStepFinishPart]
-type assistantMessagePartStepFinishPartJSON struct {
-	Cost        apijson.Field
-	Tokens      apijson.Field
-	Type        apijson.Field
-	raw         string
-	ExtraFields map[string]apijson.Field
-}
-
-func (r *AssistantMessagePartStepFinishPart) UnmarshalJSON(data []byte) (err error) {
-	return apijson.UnmarshalRoot(data, r)
-}
-
-func (r assistantMessagePartStepFinishPartJSON) RawJSON() string {
-	return r.raw
-}
-
-func (r AssistantMessagePartStepFinishPart) implementsAssistantMessagePart() {}
-
-type AssistantMessagePartStepFinishPartTokens struct {
-	Cache     AssistantMessagePartStepFinishPartTokensCache `json:"cache,required"`
-	Input     float64                                       `json:"input,required"`
-	Output    float64                                       `json:"output,required"`
-	Reasoning float64                                       `json:"reasoning,required"`
-	JSON      assistantMessagePartStepFinishPartTokensJSON  `json:"-"`
-}
-
-// assistantMessagePartStepFinishPartTokensJSON contains the JSON metadata for the
-// struct [AssistantMessagePartStepFinishPartTokens]
-type assistantMessagePartStepFinishPartTokensJSON struct {
-	Cache       apijson.Field
-	Input       apijson.Field
-	Output      apijson.Field
-	Reasoning   apijson.Field
-	raw         string
-	ExtraFields map[string]apijson.Field
-}
-
-func (r *AssistantMessagePartStepFinishPartTokens) UnmarshalJSON(data []byte) (err error) {
-	return apijson.UnmarshalRoot(data, r)
-}
-
-func (r assistantMessagePartStepFinishPartTokensJSON) RawJSON() string {
-	return r.raw
-}
-
-type AssistantMessagePartStepFinishPartTokensCache struct {
-	Read  float64                                           `json:"read,required"`
-	Write float64                                           `json:"write,required"`
-	JSON  assistantMessagePartStepFinishPartTokensCacheJSON `json:"-"`
-}
-
-// assistantMessagePartStepFinishPartTokensCacheJSON contains the JSON metadata for
-// the struct [AssistantMessagePartStepFinishPartTokensCache]
-type assistantMessagePartStepFinishPartTokensCacheJSON struct {
-	Read        apijson.Field
-	Write       apijson.Field
-	raw         string
-	ExtraFields map[string]apijson.Field
-}
-
-func (r *AssistantMessagePartStepFinishPartTokensCache) UnmarshalJSON(data []byte) (err error) {
-	return apijson.UnmarshalRoot(data, r)
-}
-
-func (r assistantMessagePartStepFinishPartTokensCacheJSON) RawJSON() string {
-	return r.raw
-}
-
-type AssistantMessagePartStepFinishPartType string
-
-const (
-	AssistantMessagePartStepFinishPartTypeStepFinish AssistantMessagePartStepFinishPartType = "step-finish"
-)
-
-func (r AssistantMessagePartStepFinishPartType) IsKnown() bool {
-	switch r {
-	case AssistantMessagePartStepFinishPartTypeStepFinish:
-		return true
-	}
-	return false
-}
-
-type AssistantMessagePartType string
-
-const (
-	AssistantMessagePartTypeText       AssistantMessagePartType = "text"
-	AssistantMessagePartTypeTool       AssistantMessagePartType = "tool"
-	AssistantMessagePartTypeStepStart  AssistantMessagePartType = "step-start"
-	AssistantMessagePartTypeStepFinish AssistantMessagePartType = "step-finish"
-)
-
-func (r AssistantMessagePartType) IsKnown() bool {
-	switch r {
-	case AssistantMessagePartTypeText, AssistantMessagePartTypeTool, AssistantMessagePartTypeStepStart, AssistantMessagePartTypeStepFinish:
-		return true
-	}
-	return false
-}
-
 type FilePart struct {
-	Mime     string       `json:"mime,required"`
-	Type     FilePartType `json:"type,required"`
-	URL      string       `json:"url,required"`
-	Filename string       `json:"filename"`
-	JSON     filePartJSON `json:"-"`
+	ID        string       `json:"id,required"`
+	MessageID string       `json:"messageID,required"`
+	Mime      string       `json:"mime,required"`
+	SessionID string       `json:"sessionID,required"`
+	Type      FilePartType `json:"type,required"`
+	URL       string       `json:"url,required"`
+	Filename  string       `json:"filename"`
+	JSON      filePartJSON `json:"-"`
 }
 
 // filePartJSON contains the JSON metadata for the struct [FilePart]
 type filePartJSON struct {
+	ID          apijson.Field
+	MessageID   apijson.Field
 	Mime        apijson.Field
+	SessionID   apijson.Field
 	Type        apijson.Field
 	URL         apijson.Field
 	Filename    apijson.Field
@@ -655,7 +465,7 @@ func (r filePartJSON) RawJSON() string {
 	return r.raw
 }
 
-func (r FilePart) implementsUserMessagePart() {}
+func (r FilePart) implementsPart() {}
 
 type FilePartType string
 
@@ -672,23 +482,23 @@ func (r FilePartType) IsKnown() bool {
 }
 
 type FilePartParam struct {
-	Mime     param.Field[string]       `json:"mime,required"`
-	Type     param.Field[FilePartType] `json:"type,required"`
-	URL      param.Field[string]       `json:"url,required"`
-	Filename param.Field[string]       `json:"filename"`
+	ID        param.Field[string]       `json:"id,required"`
+	MessageID param.Field[string]       `json:"messageID,required"`
+	Mime      param.Field[string]       `json:"mime,required"`
+	SessionID param.Field[string]       `json:"sessionID,required"`
+	Type      param.Field[FilePartType] `json:"type,required"`
+	URL       param.Field[string]       `json:"url,required"`
+	Filename  param.Field[string]       `json:"filename"`
 }
 
 func (r FilePartParam) MarshalJSON() (data []byte, err error) {
 	return apijson.MarshalRoot(r)
 }
 
-func (r FilePartParam) implementsUserMessagePartUnionParam() {}
+func (r FilePartParam) implementsSessionChatParamsPartUnion() {}
 
 type Message struct {
-	ID string `json:"id,required"`
-	// This field can have the runtime type of [[]UserMessagePart],
-	// [[]AssistantMessagePart].
-	Parts     interface{} `json:"parts,required"`
+	ID        string      `json:"id,required"`
 	Role      MessageRole `json:"role,required"`
 	SessionID string      `json:"sessionID,required"`
 	// This field can have the runtime type of [UserMessageTime],
@@ -713,7 +523,6 @@ type Message struct {
 // messageJSON contains the JSON metadata for the struct [Message]
 type messageJSON struct {
 	ID          apijson.Field
-	Parts       apijson.Field
 	Role        apijson.Field
 	SessionID   apijson.Field
 	Time        apijson.Field
@@ -787,6 +596,128 @@ func (r MessageRole) IsKnown() bool {
 	return false
 }
 
+type Part struct {
+	ID        string   `json:"id,required"`
+	MessageID string   `json:"messageID,required"`
+	SessionID string   `json:"sessionID,required"`
+	Type      PartType `json:"type,required"`
+	CallID    string   `json:"callID"`
+	Cost      float64  `json:"cost"`
+	Filename  string   `json:"filename"`
+	Mime      string   `json:"mime"`
+	// This field can have the runtime type of [ToolPartState].
+	State     interface{} `json:"state"`
+	Synthetic bool        `json:"synthetic"`
+	Text      string      `json:"text"`
+	// This field can have the runtime type of [TextPartTime].
+	Time interface{} `json:"time"`
+	// This field can have the runtime type of [StepFinishPartTokens].
+	Tokens interface{} `json:"tokens"`
+	Tool   string      `json:"tool"`
+	URL    string      `json:"url"`
+	JSON   partJSON    `json:"-"`
+	union  PartUnion
+}
+
+// partJSON contains the JSON metadata for the struct [Part]
+type partJSON struct {
+	ID          apijson.Field
+	MessageID   apijson.Field
+	SessionID   apijson.Field
+	Type        apijson.Field
+	CallID      apijson.Field
+	Cost        apijson.Field
+	Filename    apijson.Field
+	Mime        apijson.Field
+	State       apijson.Field
+	Synthetic   apijson.Field
+	Text        apijson.Field
+	Time        apijson.Field
+	Tokens      apijson.Field
+	Tool        apijson.Field
+	URL         apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r partJSON) RawJSON() string {
+	return r.raw
+}
+
+func (r *Part) UnmarshalJSON(data []byte) (err error) {
+	*r = Part{}
+	err = apijson.UnmarshalRoot(data, &r.union)
+	if err != nil {
+		return err
+	}
+	return apijson.Port(r.union, &r)
+}
+
+// AsUnion returns a [PartUnion] interface which you can cast to the specific types
+// for more type safety.
+//
+// Possible runtime types of the union are [TextPart], [FilePart], [ToolPart],
+// [StepStartPart], [StepFinishPart].
+func (r Part) AsUnion() PartUnion {
+	return r.union
+}
+
+// Union satisfied by [TextPart], [FilePart], [ToolPart], [StepStartPart] or
+// [StepFinishPart].
+type PartUnion interface {
+	implementsPart()
+}
+
+func init() {
+	apijson.RegisterUnion(
+		reflect.TypeOf((*PartUnion)(nil)).Elem(),
+		"type",
+		apijson.UnionVariant{
+			TypeFilter:         gjson.JSON,
+			Type:               reflect.TypeOf(TextPart{}),
+			DiscriminatorValue: "text",
+		},
+		apijson.UnionVariant{
+			TypeFilter:         gjson.JSON,
+			Type:               reflect.TypeOf(FilePart{}),
+			DiscriminatorValue: "file",
+		},
+		apijson.UnionVariant{
+			TypeFilter:         gjson.JSON,
+			Type:               reflect.TypeOf(ToolPart{}),
+			DiscriminatorValue: "tool",
+		},
+		apijson.UnionVariant{
+			TypeFilter:         gjson.JSON,
+			Type:               reflect.TypeOf(StepStartPart{}),
+			DiscriminatorValue: "step-start",
+		},
+		apijson.UnionVariant{
+			TypeFilter:         gjson.JSON,
+			Type:               reflect.TypeOf(StepFinishPart{}),
+			DiscriminatorValue: "step-finish",
+		},
+	)
+}
+
+type PartType string
+
+const (
+	PartTypeText       PartType = "text"
+	PartTypeFile       PartType = "file"
+	PartTypeTool       PartType = "tool"
+	PartTypeStepStart  PartType = "step-start"
+	PartTypeStepFinish PartType = "step-finish"
+)
+
+func (r PartType) IsKnown() bool {
+	switch r {
+	case PartTypeText, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish:
+		return true
+	}
+	return false
+}
+
 type Session struct {
 	ID       string        `json:"id,required"`
 	Time     SessionTime   `json:"time,required"`
@@ -885,13 +816,115 @@ func (r sessionShareJSON) RawJSON() string {
 	return r.raw
 }
 
+type StepFinishPart struct {
+	ID        string               `json:"id,required"`
+	Cost      float64              `json:"cost,required"`
+	MessageID string               `json:"messageID,required"`
+	SessionID string               `json:"sessionID,required"`
+	Tokens    StepFinishPartTokens `json:"tokens,required"`
+	Type      StepFinishPartType   `json:"type,required"`
+	JSON      stepFinishPartJSON   `json:"-"`
+}
+
+// stepFinishPartJSON contains the JSON metadata for the struct [StepFinishPart]
+type stepFinishPartJSON struct {
+	ID          apijson.Field
+	Cost        apijson.Field
+	MessageID   apijson.Field
+	SessionID   apijson.Field
+	Tokens      apijson.Field
+	Type        apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *StepFinishPart) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r stepFinishPartJSON) RawJSON() string {
+	return r.raw
+}
+
+func (r StepFinishPart) implementsPart() {}
+
+type StepFinishPartTokens struct {
+	Cache     StepFinishPartTokensCache `json:"cache,required"`
+	Input     float64                   `json:"input,required"`
+	Output    float64                   `json:"output,required"`
+	Reasoning float64                   `json:"reasoning,required"`
+	JSON      stepFinishPartTokensJSON  `json:"-"`
+}
+
+// stepFinishPartTokensJSON contains the JSON metadata for the struct
+// [StepFinishPartTokens]
+type stepFinishPartTokensJSON struct {
+	Cache       apijson.Field
+	Input       apijson.Field
+	Output      apijson.Field
+	Reasoning   apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *StepFinishPartTokens) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r stepFinishPartTokensJSON) RawJSON() string {
+	return r.raw
+}
+
+type StepFinishPartTokensCache struct {
+	Read  float64                       `json:"read,required"`
+	Write float64                       `json:"write,required"`
+	JSON  stepFinishPartTokensCacheJSON `json:"-"`
+}
+
+// stepFinishPartTokensCacheJSON contains the JSON metadata for the struct
+// [StepFinishPartTokensCache]
+type stepFinishPartTokensCacheJSON struct {
+	Read        apijson.Field
+	Write       apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *StepFinishPartTokensCache) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r stepFinishPartTokensCacheJSON) RawJSON() string {
+	return r.raw
+}
+
+type StepFinishPartType string
+
+const (
+	StepFinishPartTypeStepFinish StepFinishPartType = "step-finish"
+)
+
+func (r StepFinishPartType) IsKnown() bool {
+	switch r {
+	case StepFinishPartTypeStepFinish:
+		return true
+	}
+	return false
+}
+
 type StepStartPart struct {
-	Type StepStartPartType `json:"type,required"`
-	JSON stepStartPartJSON `json:"-"`
+	ID        string            `json:"id,required"`
+	MessageID string            `json:"messageID,required"`
+	SessionID string            `json:"sessionID,required"`
+	Type      StepStartPartType `json:"type,required"`
+	JSON      stepStartPartJSON `json:"-"`
 }
 
 // stepStartPartJSON contains the JSON metadata for the struct [StepStartPart]
 type stepStartPartJSON struct {
+	ID          apijson.Field
+	MessageID   apijson.Field
+	SessionID   apijson.Field
 	Type        apijson.Field
 	raw         string
 	ExtraFields map[string]apijson.Field
@@ -905,7 +938,7 @@ func (r stepStartPartJSON) RawJSON() string {
 	return r.raw
 }
 
-func (r StepStartPart) implementsAssistantMessagePart() {}
+func (r StepStartPart) implementsPart() {}
 
 type StepStartPartType string
 
@@ -922,17 +955,25 @@ func (r StepStartPartType) IsKnown() bool {
 }
 
 type TextPart struct {
+	ID        string       `json:"id,required"`
+	MessageID string       `json:"messageID,required"`
+	SessionID string       `json:"sessionID,required"`
 	Text      string       `json:"text,required"`
 	Type      TextPartType `json:"type,required"`
 	Synthetic bool         `json:"synthetic"`
+	Time      TextPartTime `json:"time"`
 	JSON      textPartJSON `json:"-"`
 }
 
 // textPartJSON contains the JSON metadata for the struct [TextPart]
 type textPartJSON struct {
+	ID          apijson.Field
+	MessageID   apijson.Field
+	SessionID   apijson.Field
 	Text        apijson.Field
 	Type        apijson.Field
 	Synthetic   apijson.Field
+	Time        apijson.Field
 	raw         string
 	ExtraFields map[string]apijson.Field
 }
@@ -945,9 +986,7 @@ func (r textPartJSON) RawJSON() string {
 	return r.raw
 }
 
-func (r TextPart) implementsAssistantMessagePart() {}
-
-func (r TextPart) implementsUserMessagePart() {}
+func (r TextPart) implementsPart() {}
 
 type TextPartType string
 
@@ -963,29 +1002,70 @@ func (r TextPartType) IsKnown() bool {
 	return false
 }
 
+type TextPartTime struct {
+	Start float64          `json:"start,required"`
+	End   float64          `json:"end"`
+	JSON  textPartTimeJSON `json:"-"`
+}
+
+// textPartTimeJSON contains the JSON metadata for the struct [TextPartTime]
+type textPartTimeJSON struct {
+	Start       apijson.Field
+	End         apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *TextPartTime) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r textPartTimeJSON) RawJSON() string {
+	return r.raw
+}
+
 type TextPartParam struct {
-	Text      param.Field[string]       `json:"text,required"`
-	Type      param.Field[TextPartType] `json:"type,required"`
-	Synthetic param.Field[bool]         `json:"synthetic"`
+	ID        param.Field[string]            `json:"id,required"`
+	MessageID param.Field[string]            `json:"messageID,required"`
+	SessionID param.Field[string]            `json:"sessionID,required"`
+	Text      param.Field[string]            `json:"text,required"`
+	Type      param.Field[TextPartType]      `json:"type,required"`
+	Synthetic param.Field[bool]              `json:"synthetic"`
+	Time      param.Field[TextPartTimeParam] `json:"time"`
 }
 
 func (r TextPartParam) MarshalJSON() (data []byte, err error) {
 	return apijson.MarshalRoot(r)
 }
 
-func (r TextPartParam) implementsUserMessagePartUnionParam() {}
+func (r TextPartParam) implementsSessionChatParamsPartUnion() {}
+
+type TextPartTimeParam struct {
+	Start param.Field[float64] `json:"start,required"`
+	End   param.Field[float64] `json:"end"`
+}
+
+func (r TextPartTimeParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
 
 type ToolPart struct {
-	ID    string        `json:"id,required"`
-	State ToolPartState `json:"state,required"`
-	Tool  string        `json:"tool,required"`
-	Type  ToolPartType  `json:"type,required"`
-	JSON  toolPartJSON  `json:"-"`
+	ID        string        `json:"id,required"`
+	CallID    string        `json:"callID,required"`
+	MessageID string        `json:"messageID,required"`
+	SessionID string        `json:"sessionID,required"`
+	State     ToolPartState `json:"state,required"`
+	Tool      string        `json:"tool,required"`
+	Type      ToolPartType  `json:"type,required"`
+	JSON      toolPartJSON  `json:"-"`
 }
 
 // toolPartJSON contains the JSON metadata for the struct [ToolPart]
 type toolPartJSON struct {
 	ID          apijson.Field
+	CallID      apijson.Field
+	MessageID   apijson.Field
+	SessionID   apijson.Field
 	State       apijson.Field
 	Tool        apijson.Field
 	Type        apijson.Field
@@ -1001,7 +1081,7 @@ func (r toolPartJSON) RawJSON() string {
 	return r.raw
 }
 
-func (r ToolPart) implementsAssistantMessagePart() {}
+func (r ToolPart) implementsPart() {}
 
 type ToolPartState struct {
 	Status ToolPartStateStatus `json:"status,required"`
@@ -1357,18 +1437,16 @@ func (r toolStateRunningTimeJSON) RawJSON() string {
 }
 
 type UserMessage struct {
-	ID        string            `json:"id,required"`
-	Parts     []UserMessagePart `json:"parts,required"`
-	Role      UserMessageRole   `json:"role,required"`
-	SessionID string            `json:"sessionID,required"`
-	Time      UserMessageTime   `json:"time,required"`
-	JSON      userMessageJSON   `json:"-"`
+	ID        string          `json:"id,required"`
+	Role      UserMessageRole `json:"role,required"`
+	SessionID string          `json:"sessionID,required"`
+	Time      UserMessageTime `json:"time,required"`
+	JSON      userMessageJSON `json:"-"`
 }
 
 // userMessageJSON contains the JSON metadata for the struct [UserMessage]
 type userMessageJSON struct {
 	ID          apijson.Field
-	Parts       apijson.Field
 	Role        apijson.Field
 	SessionID   apijson.Field
 	Time        apijson.Field
@@ -1420,119 +1498,82 @@ func (r userMessageTimeJSON) RawJSON() string {
 	return r.raw
 }
 
-type UserMessagePart struct {
-	Type      UserMessagePartType `json:"type,required"`
-	Filename  string              `json:"filename"`
-	Mime      string              `json:"mime"`
-	Synthetic bool                `json:"synthetic"`
-	Text      string              `json:"text"`
-	URL       string              `json:"url"`
-	JSON      userMessagePartJSON `json:"-"`
-	union     UserMessagePartUnion
+type SessionMessagesResponse struct {
+	Info  Message                     `json:"info,required"`
+	Parts []Part                      `json:"parts,required"`
+	JSON  sessionMessagesResponseJSON `json:"-"`
 }
 
-// userMessagePartJSON contains the JSON metadata for the struct [UserMessagePart]
-type userMessagePartJSON struct {
-	Type        apijson.Field
-	Filename    apijson.Field
-	Mime        apijson.Field
-	Synthetic   apijson.Field
-	Text        apijson.Field
-	URL         apijson.Field
+// sessionMessagesResponseJSON contains the JSON metadata for the struct
+// [SessionMessagesResponse]
+type sessionMessagesResponseJSON struct {
+	Info        apijson.Field
+	Parts       apijson.Field
 	raw         string
 	ExtraFields map[string]apijson.Field
 }
 
-func (r userMessagePartJSON) RawJSON() string {
+func (r *SessionMessagesResponse) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r sessionMessagesResponseJSON) RawJSON() string {
 	return r.raw
 }
 
-func (r *UserMessagePart) UnmarshalJSON(data []byte) (err error) {
-	*r = UserMessagePart{}
-	err = apijson.UnmarshalRoot(data, &r.union)
-	if err != nil {
-		return err
-	}
-	return apijson.Port(r.union, &r)
+type SessionChatParams struct {
+	MessageID  param.Field[string]                       `json:"messageID,required"`
+	Mode       param.Field[string]                       `json:"mode,required"`
+	ModelID    param.Field[string]                       `json:"modelID,required"`
+	Parts      param.Field[[]SessionChatParamsPartUnion] `json:"parts,required"`
+	ProviderID param.Field[string]                       `json:"providerID,required"`
 }
 
-// AsUnion returns a [UserMessagePartUnion] interface which you can cast to the
-// specific types for more type safety.
-//
-// Possible runtime types of the union are [TextPart], [FilePart].
-func (r UserMessagePart) AsUnion() UserMessagePartUnion {
-	return r.union
+func (r SessionChatParams) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
 }
 
-// Union satisfied by [TextPart] or [FilePart].
-type UserMessagePartUnion interface {
-	implementsUserMessagePart()
+type SessionChatParamsPart struct {
+	ID        param.Field[string]                     `json:"id,required"`
+	MessageID param.Field[string]                     `json:"messageID,required"`
+	SessionID param.Field[string]                     `json:"sessionID,required"`
+	Type      param.Field[SessionChatParamsPartsType] `json:"type,required"`
+	Filename  param.Field[string]                     `json:"filename"`
+	Mime      param.Field[string]                     `json:"mime"`
+	Synthetic param.Field[bool]                       `json:"synthetic"`
+	Text      param.Field[string]                     `json:"text"`
+	Time      param.Field[interface{}]                `json:"time"`
+	URL       param.Field[string]                     `json:"url"`
 }
 
-func init() {
-	apijson.RegisterUnion(
-		reflect.TypeOf((*UserMessagePartUnion)(nil)).Elem(),
-		"type",
-		apijson.UnionVariant{
-			TypeFilter:         gjson.JSON,
-			Type:               reflect.TypeOf(TextPart{}),
-			DiscriminatorValue: "text",
-		},
-		apijson.UnionVariant{
-			TypeFilter:         gjson.JSON,
-			Type:               reflect.TypeOf(FilePart{}),
-			DiscriminatorValue: "file",
-		},
-	)
+func (r SessionChatParamsPart) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
 }
 
-type UserMessagePartType string
+func (r SessionChatParamsPart) implementsSessionChatParamsPartUnion() {}
+
+// Satisfied by [FilePartParam], [TextPartParam], [SessionChatParamsPart].
+type SessionChatParamsPartUnion interface {
+	implementsSessionChatParamsPartUnion()
+}
+
+type SessionChatParamsPartsType string
 
 const (
-	UserMessagePartTypeText UserMessagePartType = "text"
-	UserMessagePartTypeFile UserMessagePartType = "file"
+	SessionChatParamsPartsTypeFile SessionChatParamsPartsType = "file"
+	SessionChatParamsPartsTypeText SessionChatParamsPartsType = "text"
 )
 
-func (r UserMessagePartType) IsKnown() bool {
+func (r SessionChatParamsPartsType) IsKnown() bool {
 	switch r {
-	case UserMessagePartTypeText, UserMessagePartTypeFile:
+	case SessionChatParamsPartsTypeFile, SessionChatParamsPartsTypeText:
 		return true
 	}
 	return false
 }
 
-type UserMessagePartParam struct {
-	Type      param.Field[UserMessagePartType] `json:"type,required"`
-	Filename  param.Field[string]              `json:"filename"`
-	Mime      param.Field[string]              `json:"mime"`
-	Synthetic param.Field[bool]                `json:"synthetic"`
-	Text      param.Field[string]              `json:"text"`
-	URL       param.Field[string]              `json:"url"`
-}
-
-func (r UserMessagePartParam) MarshalJSON() (data []byte, err error) {
-	return apijson.MarshalRoot(r)
-}
-
-func (r UserMessagePartParam) implementsUserMessagePartUnionParam() {}
-
-// Satisfied by [TextPartParam], [FilePartParam], [UserMessagePartParam].
-type UserMessagePartUnionParam interface {
-	implementsUserMessagePartUnionParam()
-}
-
-type SessionChatParams struct {
-	Mode       param.Field[string]                      `json:"mode,required"`
-	ModelID    param.Field[string]                      `json:"modelID,required"`
-	Parts      param.Field[[]UserMessagePartUnionParam] `json:"parts,required"`
-	ProviderID param.Field[string]                      `json:"providerID,required"`
-}
-
-func (r SessionChatParams) MarshalJSON() (data []byte, err error) {
-	return apijson.MarshalRoot(r)
-}
-
 type SessionInitParams struct {
+	MessageID  param.Field[string] `json:"messageID,required"`
 	ModelID    param.Field[string] `json:"modelID,required"`
 	ProviderID param.Field[string] `json:"providerID,required"`
 }

+ 12 - 6
packages/tui/sdk/session_test.go

@@ -117,12 +117,17 @@ func TestSessionChat(t *testing.T) {
 		context.TODO(),
 		"id",
 		opencode.SessionChatParams{
-			Mode:    opencode.F("mode"),
-			ModelID: opencode.F("modelID"),
-			Parts: opencode.F([]opencode.UserMessagePartUnionParam{opencode.TextPartParam{
-				Text:      opencode.F("text"),
-				Type:      opencode.F(opencode.TextPartTypeText),
-				Synthetic: opencode.F(true),
+			MessageID: opencode.F("messageID"),
+			Mode:      opencode.F("mode"),
+			ModelID:   opencode.F("modelID"),
+			Parts: opencode.F([]opencode.SessionChatParamsPartUnion{opencode.FilePartParam{
+				ID:        opencode.F("id"),
+				MessageID: opencode.F("messageID"),
+				Mime:      opencode.F("mime"),
+				SessionID: opencode.F("sessionID"),
+				Type:      opencode.F(opencode.FilePartTypeFile),
+				URL:       opencode.F("url"),
+				Filename:  opencode.F("filename"),
 			}}),
 			ProviderID: opencode.F("providerID"),
 		},
@@ -152,6 +157,7 @@ func TestSessionInit(t *testing.T) {
 		context.TODO(),
 		"id",
 		opencode.SessionInitParams{
+			MessageID:  opencode.F("messageID"),
 			ModelID:    opencode.F("modelID"),
 			ProviderID: opencode.F("providerID"),
 		},

+ 5 - 2
packages/tui/sdk/usage_test.go

@@ -23,10 +23,13 @@ func TestUsage(t *testing.T) {
 	client := opencode.NewClient(
 		option.WithBaseURL(baseURL),
 	)
-	events, err := client.Event.List(context.TODO())
+	stream := client.Event.ListStreaming(context.TODO())
+	for stream.Next() {
+		t.Logf("%+v\n", stream.Current())
+	}
+	err := stream.Err()
 	if err != nil {
 		t.Error(err)
 		return
 	}
-	t.Logf("%+v\n", events)
 }

+ 46 - 14
packages/web/src/components/Share.tsx

@@ -1,6 +1,6 @@
-import { For, Show, onMount, Suspense, onCleanup, createMemo, createSignal, SuspenseList } from "solid-js"
+import { For, Show, onMount, Suspense, onCleanup, createMemo, createSignal, SuspenseList, createEffect } from "solid-js"
 import { DateTime } from "luxon"
-import { createStore, reconcile } from "solid-js/store"
+import { createStore, reconcile, unwrap } from "solid-js/store"
 import { IconArrowDown } from "./icons"
 import { IconOpencode } from "./icons/custom"
 import styles from "./share.module.css"
@@ -9,6 +9,8 @@ import type { Message } from "opencode/session/message"
 import type { Session } from "opencode/session/index"
 import { Part, ProviderIcon } from "./share/part"
 
+type MessageWithParts = MessageV2.Info & { parts: MessageV2.Part[] }
+
 type Status = "disconnected" | "connecting" | "connected" | "error" | "reconnecting"
 
 function scrollToAnchor(id: string) {
@@ -39,7 +41,7 @@ export default function Share(props: {
   id: string
   api: string
   info: Session.Info
-  messages: Record<string, MessageV2.Info>
+  messages: Record<string, MessageWithParts>
 }) {
   let lastScrollY = 0
   let hasScrolledToAnchor = false
@@ -57,10 +59,13 @@ export default function Share(props: {
 
   const [store, setStore] = createStore<{
     info?: Session.Info
-    messages: Record<string, MessageV2.Info | Message.Info>
+    messages: Record<string, MessageWithParts>
   }>({ info: props.info, messages: props.messages })
   const messages = createMemo(() => Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id)))
   const [connectionStatus, setConnectionStatus] = createSignal<[Status, string?]>(["disconnected", "Disconnected"])
+  createEffect(() => {
+    console.log(unwrap(store))
+  })
 
   onMount(() => {
     const apiUrl = props.api
@@ -115,8 +120,22 @@ export default function Share(props: {
           }
           if (type === "message") {
             const [, messageID] = splits
+            if ("metadata" in d.content) {
+              d.content = fromV1(d.content)
+            }
+            d.content.parts = d.content.parts ?? store.messages[messageID]?.parts ?? []
             setStore("messages", messageID, reconcile(d.content))
           }
+          if (type === "part") {
+            setStore("messages", d.content.messageID, "parts", arr => {
+              const index = arr.findIndex((x) => x.id === d.content.id)
+              if (index === -1)
+                arr.push(d.content)
+              if (index > -1)
+                arr[index] = d.content
+              return [...arr]
+            })
+          }
         } catch (error) {
           console.error("Error parsing WebSocket message:", error)
         }
@@ -233,7 +252,7 @@ export default function Share(props: {
       rootDir: undefined as string | undefined,
       created: undefined as number | undefined,
       completed: undefined as number | undefined,
-      messages: [] as MessageV2.Info[],
+      messages: [] as MessageWithParts[],
       models: {} as Record<string, string[]>,
       cost: 0,
       tokens: {
@@ -247,7 +266,7 @@ export default function Share(props: {
 
     const msgs = messages()
     for (let i = 0; i < msgs.length; i++) {
-      const msg = "metadata" in msgs[i] ? fromV1(msgs[i] as Message.Info) : (msgs[i] as MessageV2.Info)
+      const msg = msgs[i]
 
       result.messages.push(msg)
 
@@ -464,9 +483,9 @@ export default function Share(props: {
   )
 }
 
-export function fromV1(v1: Message.Info): MessageV2.Info {
+export function fromV1(v1: Message.Info): MessageWithParts {
   if (v1.role === "assistant") {
-    const result: MessageV2.Assistant = {
+    return {
       id: v1.id,
       sessionID: v1.metadata.sessionID,
       role: "assistant",
@@ -482,10 +501,16 @@ export function fromV1(v1: Message.Info): MessageV2.Info {
       providerID: v1.metadata.assistant!.providerID,
       system: v1.metadata.assistant!.system,
       error: v1.metadata.error,
-      parts: v1.parts.flatMap((part): MessageV2.AssistantPart[] => {
+      parts: v1.parts.flatMap((part, index): MessageV2.Part[] => {
+        const base = {
+          id: index.toString(),
+          messageID: v1.id,
+          sessionID: v1.metadata.sessionID,
+        }
         if (part.type === "text") {
           return [
             {
+              ...base,
               type: "text",
               text: part.text,
             },
@@ -494,6 +519,7 @@ export function fromV1(v1: Message.Info): MessageV2.Info {
         if (part.type === "step-start") {
           return [
             {
+              ...base,
               type: "step-start",
             },
           ]
@@ -501,8 +527,9 @@ export function fromV1(v1: Message.Info): MessageV2.Info {
         if (part.type === "tool-invocation") {
           return [
             {
+              ...base,
               type: "tool",
-              id: part.toolInvocation.toolCallId,
+              callID: part.toolInvocation.toolCallId,
               tool: part.toolInvocation.toolName,
               state: (() => {
                 if (part.toolInvocation.state === "partial-call") {
@@ -540,21 +567,26 @@ export function fromV1(v1: Message.Info): MessageV2.Info {
         return []
       }),
     }
-    return result
   }
 
   if (v1.role === "user") {
-    const result: MessageV2.User = {
+    return {
       id: v1.id,
       sessionID: v1.metadata.sessionID,
       role: "user",
       time: {
         created: v1.metadata.time.created,
       },
-      parts: v1.parts.flatMap((part): MessageV2.UserPart[] => {
+      parts: v1.parts.flatMap((part, index): MessageV2.Part[] => {
+        const base = {
+          id: index.toString(),
+          messageID: v1.id,
+          sessionID: v1.metadata.sessionID,
+        }
         if (part.type === "text") {
           return [
             {
+              ...base,
               type: "text",
               text: part.text,
             },
@@ -563,6 +595,7 @@ export function fromV1(v1: Message.Info): MessageV2.Info {
         if (part.type === "file") {
           return [
             {
+              ...base,
               type: "file",
               mime: part.mediaType,
               filename: part.filename,
@@ -573,7 +606,6 @@ export function fromV1(v1: Message.Info): MessageV2.Info {
         return []
       }),
     }
-    return result
   }
 
   throw new Error("unknown message type")

+ 2 - 0
stainless.yml

@@ -88,10 +88,12 @@ resources:
     models:
       session: Session
       message: Message
+      part: Part
       textPart: TextPart
       filePart: FilePart
       toolPart: ToolPart
       stepStartPart: StepStartPart
+      stepFinishPart: StepFinishPart
       assistantMessage: AssistantMessage
       assistantMessagePart: AssistantMessagePart
       userMessage: UserMessage