Parcourir la source

ignore: fix share page

Dax Raad il y a 6 mois
Parent
commit
0ce7d92a8b

+ 17 - 11
bun.lock

@@ -13,7 +13,7 @@
     },
     "cloud/core": {
       "name": "@opencode/cloud-core",
-      "version": "0.4.18",
+      "version": "0.4.19",
       "dependencies": {
         "@aws-sdk/client-sts": "3.782.0",
         "drizzle-orm": "0.41.0",
@@ -27,7 +27,7 @@
     },
     "cloud/function": {
       "name": "@opencode/cloud-function",
-      "version": "0.4.18",
+      "version": "0.4.19",
       "dependencies": {
         "@ai-sdk/anthropic": "2.0.0",
         "@ai-sdk/openai": "2.0.2",
@@ -47,7 +47,7 @@
     },
     "cloud/web": {
       "name": "@opencode/cloud-web",
-      "version": "0.4.18",
+      "version": "0.4.19",
       "dependencies": {
         "@kobalte/core": "0.13.9",
         "@openauthjs/solid": "0.0.0-20250322224806",
@@ -66,7 +66,7 @@
     },
     "packages/function": {
       "name": "@opencode/function",
-      "version": "0.4.18",
+      "version": "0.4.19",
       "dependencies": {
         "@octokit/auth-app": "8.0.1",
         "@octokit/rest": "22.0.0",
@@ -81,7 +81,7 @@
     },
     "packages/opencode": {
       "name": "opencode",
-      "version": "0.4.18",
+      "version": "0.4.19",
       "bin": {
         "opencode": "./bin/opencode",
       },
@@ -133,7 +133,7 @@
     },
     "packages/plugin": {
       "name": "@opencode-ai/plugin",
-      "version": "0.4.18",
+      "version": "0.4.19",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
       },
@@ -145,7 +145,7 @@
     },
     "packages/sdk/js": {
       "name": "@opencode-ai/sdk",
-      "version": "0.4.18",
+      "version": "0.4.19",
       "devDependencies": {
         "@hey-api/openapi-ts": "0.80.1",
         "@tsconfig/node22": "catalog:",
@@ -154,9 +154,9 @@
     },
     "packages/web": {
       "name": "@opencode/web",
-      "version": "0.4.18",
+      "version": "0.4.19",
       "dependencies": {
-        "@astrojs/cloudflare": "^12.5.4",
+        "@astrojs/cloudflare": "12.6.3",
         "@astrojs/markdown-remark": "6.3.1",
         "@astrojs/solid-js": "5.1.0",
         "@astrojs/starlight": "0.34.3",
@@ -229,11 +229,11 @@
 
     "@apidevtools/json-schema-ref-parser": ["@apidevtools/[email protected]", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" } }, "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ=="],
 
-    "@astrojs/cloudflare": ["@astrojs/[email protected].0", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-pQ8bokC59GEiXvyXpC4swBNoL7C/EknP+82KFzQwgR/Aeo5N1oPiAoPHgJbpPya/YF4E26WODdCQfBQDvLRfuw=="],
+    "@astrojs/cloudflare": ["@astrojs/[email protected].3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-xhJptF5tU2k5eo70nIMyL1Udma0CqmUEnGSlGyFflLqSY82CRQI6nWZ/xZt0ZvmXuErUjIx0YYQNfZsz5CNjLQ=="],
 
     "@astrojs/compiler": ["@astrojs/[email protected]", "", {}, "sha512-w2zfvhjNCkNMmMMOn5b0J8+OmUaBL1o40ipMvqcG6NRpdC+lKxmTi48DT8Xw0SzJ3AfmeFLB45zXZXtmbsjcgw=="],
 
-    "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
+    "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.1", "", {}, "sha512-7dwEVigz9vUWDw3nRwLQ/yH/xYovlUA0ZD86xoeKEBmkz9O6iELG1yri67PgAPW6VLL/xInA4t7H0CK6VmtkKQ=="],
 
     "@astrojs/markdown-remark": ["@astrojs/[email protected]", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.2.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "remark-smartypants": "^3.0.2", "shiki": "^3.0.0", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-c5F5gGrkczUaTVgmMW9g1YMJGzOtRvjjhw6IfGuxarM6ct09MpwysP10US729dy07gg8y+ofVifezvP3BNsWZg=="],
 
@@ -2569,6 +2569,8 @@
 
     "@astrojs/cloudflare/vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
 
+    "@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/[email protected]", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
+
     "@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/[email protected]", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.2.1", "smol-toml": "^1.3.4", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-DDRtD1sPvAuA7ms2btc9A7/7DApKqgLMNrE6kh5tmkfy8utD0Z738gqd3p5aViYYdUtHIyEJ1X4mCMxfCfu15w=="],
 
     "@astrojs/mdx/source-map": ["[email protected]", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
@@ -2757,6 +2759,8 @@
 
     "anymatch/picomatch": ["[email protected]", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
 
+    "astro/@astrojs/internal-helpers": ["@astrojs/[email protected]", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
+
     "astro/diff": ["[email protected]", "", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="],
 
     "astro/esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.8", "@esbuild/android-arm": "0.25.8", "@esbuild/android-arm64": "0.25.8", "@esbuild/android-x64": "0.25.8", "@esbuild/darwin-arm64": "0.25.8", "@esbuild/darwin-x64": "0.25.8", "@esbuild/freebsd-arm64": "0.25.8", "@esbuild/freebsd-x64": "0.25.8", "@esbuild/linux-arm": "0.25.8", "@esbuild/linux-arm64": "0.25.8", "@esbuild/linux-ia32": "0.25.8", "@esbuild/linux-loong64": "0.25.8", "@esbuild/linux-mips64el": "0.25.8", "@esbuild/linux-ppc64": "0.25.8", "@esbuild/linux-riscv64": "0.25.8", "@esbuild/linux-s390x": "0.25.8", "@esbuild/linux-x64": "0.25.8", "@esbuild/netbsd-arm64": "0.25.8", "@esbuild/netbsd-x64": "0.25.8", "@esbuild/openbsd-arm64": "0.25.8", "@esbuild/openbsd-x64": "0.25.8", "@esbuild/openharmony-arm64": "0.25.8", "@esbuild/sunos-x64": "0.25.8", "@esbuild/win32-arm64": "0.25.8", "@esbuild/win32-ia32": "0.25.8", "@esbuild/win32-x64": "0.25.8" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q=="],
@@ -2933,6 +2937,8 @@
 
     "@astrojs/cloudflare/vite/esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.8", "@esbuild/android-arm": "0.25.8", "@esbuild/android-arm64": "0.25.8", "@esbuild/android-x64": "0.25.8", "@esbuild/darwin-arm64": "0.25.8", "@esbuild/darwin-x64": "0.25.8", "@esbuild/freebsd-arm64": "0.25.8", "@esbuild/freebsd-x64": "0.25.8", "@esbuild/linux-arm": "0.25.8", "@esbuild/linux-arm64": "0.25.8", "@esbuild/linux-ia32": "0.25.8", "@esbuild/linux-loong64": "0.25.8", "@esbuild/linux-mips64el": "0.25.8", "@esbuild/linux-ppc64": "0.25.8", "@esbuild/linux-riscv64": "0.25.8", "@esbuild/linux-s390x": "0.25.8", "@esbuild/linux-x64": "0.25.8", "@esbuild/netbsd-arm64": "0.25.8", "@esbuild/netbsd-x64": "0.25.8", "@esbuild/openbsd-arm64": "0.25.8", "@esbuild/openbsd-x64": "0.25.8", "@esbuild/openharmony-arm64": "0.25.8", "@esbuild/sunos-x64": "0.25.8", "@esbuild/win32-arm64": "0.25.8", "@esbuild/win32-ia32": "0.25.8", "@esbuild/win32-x64": "0.25.8" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q=="],
 
+    "@astrojs/mdx/@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/[email protected]", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
+
     "@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/[email protected]", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
 
     "@astrojs/solid-js/vite/esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.8", "@esbuild/android-arm": "0.25.8", "@esbuild/android-arm64": "0.25.8", "@esbuild/android-x64": "0.25.8", "@esbuild/darwin-arm64": "0.25.8", "@esbuild/darwin-x64": "0.25.8", "@esbuild/freebsd-arm64": "0.25.8", "@esbuild/freebsd-x64": "0.25.8", "@esbuild/linux-arm": "0.25.8", "@esbuild/linux-arm64": "0.25.8", "@esbuild/linux-ia32": "0.25.8", "@esbuild/linux-loong64": "0.25.8", "@esbuild/linux-mips64el": "0.25.8", "@esbuild/linux-ppc64": "0.25.8", "@esbuild/linux-riscv64": "0.25.8", "@esbuild/linux-s390x": "0.25.8", "@esbuild/linux-x64": "0.25.8", "@esbuild/netbsd-arm64": "0.25.8", "@esbuild/netbsd-x64": "0.25.8", "@esbuild/openbsd-arm64": "0.25.8", "@esbuild/openbsd-x64": "0.25.8", "@esbuild/openharmony-arm64": "0.25.8", "@esbuild/sunos-x64": "0.25.8", "@esbuild/win32-arm64": "0.25.8", "@esbuild/win32-ia32": "0.25.8", "@esbuild/win32-x64": "0.25.8" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q=="],

+ 1 - 1
packages/opencode/src/provider/provider.ts

@@ -448,7 +448,7 @@ export namespace Provider {
     }
   }
 
-  const priority = ["gemini-2.5-pro-preview", "codex-mini", "claude-sonnet-4"]
+  const priority = ["gemini-2.5-pro-preview", "gpt-5", "claude-sonnet-4"]
   export function sort(models: ModelsDev.Model[]) {
     return sortBy(
       models,

+ 1 - 1
packages/web/package.json

@@ -11,7 +11,7 @@
     "astro": "astro"
   },
   "dependencies": {
-    "@astrojs/cloudflare": "^12.5.4",
+    "@astrojs/cloudflare": "12.6.3",
     "@astrojs/markdown-remark": "6.3.1",
     "@astrojs/solid-js": "5.1.0",
     "@astrojs/starlight": "0.34.3",

+ 201 - 190
packages/web/src/components/Share.tsx

@@ -1,7 +1,6 @@
 import { For, Show, onMount, Suspense, onCleanup, createMemo, createSignal, SuspenseList, createEffect } from "solid-js"
 import { DateTime } from "luxon"
 import { createStore, reconcile, unwrap } from "solid-js/store"
-import { mapValues } from "remeda"
 import { IconArrowDown } from "./icons"
 import { IconOpencode } from "./icons/custom"
 import styles from "./share.module.css"
@@ -42,7 +41,6 @@ export default function Share(props: {
   id: string
   api: string
   info: Session.Info
-  messages: Record<string, MessageWithParts>
 }) {
   let lastScrollY = 0
   let hasScrolledToAnchor = false
@@ -50,7 +48,6 @@ export default function Share(props: {
   let scrollSentinel: HTMLElement | undefined
   let scrollObserver: IntersectionObserver | undefined
 
-  const id = props.id
   const params = new URLSearchParams(window.location.search)
   const debug = params.get("debug") === "true"
 
@@ -61,17 +58,27 @@ export default function Share(props: {
   const [store, setStore] = createStore<{
     info?: Session.Info
     messages: Record<string, MessageWithParts>
-  }>({ info: props.info, messages: {} })
+  }>({
+    info: {
+      id: props.id,
+      title: props.info.title,
+      version: props.info.version,
+      time: {
+        created: props.info.time.created,
+        updated: props.info.time.updated,
+      },
+    }, 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))
-  // })
+  createEffect(() => {
+    console.log(unwrap(store))
+  })
 
   onMount(() => {
     const apiUrl = props.api
 
-    if (!id) {
+    if (!props.id) {
       setConnectionStatus(["error", "id not found"])
       return
     }
@@ -96,7 +103,7 @@ export default function Share(props: {
 
       // Always use secure WebSocket protocol (wss)
       const wsBaseUrl = apiUrl.replace(/^https?:\/\//, "wss://")
-      const wsUrl = `${wsBaseUrl}/share_poll?id=${id}`
+      const wsUrl = `${wsBaseUrl}/share_poll?id=${props.id}`
       console.log("Connecting to WebSocket URL:", wsUrl)
 
       // Create WebSocket connection
@@ -261,7 +268,9 @@ export default function Share(props: {
       },
     }
 
-    result.created = props.info.time.created
+    if (!store.info) return result
+
+    result.created = store.info.time.created
 
     const msgs = messages()
     for (let i = 0; i < msgs.length; i++) {
@@ -290,197 +299,199 @@ export default function Share(props: {
   })
 
   return (
-    <main classList={{ [styles.root]: true, "not-content": true }}>
-      <div data-component="header">
-        <h1 data-component="header-title">{store.info?.title}</h1>
-        <div data-component="header-details">
-          <ul data-component="header-stats">
-            <li title="opencode version" data-slot="item">
-              <div data-slot="icon" title="opencode">
-                <IconOpencode width={16} height={16} />
-              </div>
-              <Show when={store.info?.version} fallback="v0.0.1">
-                <span>v{store.info?.version}</span>
-              </Show>
-            </li>
-            {Object.values(data().models).length > 0 ? (
-              <For each={Object.values(data().models)}>
-                {([provider, model]) => (
-                  <li data-slot="item">
-                    <div data-slot="icon" title={provider}>
-                      <ProviderIcon model={model} />
-                    </div>
-                    <span data-slot="model">{model}</span>
-                  </li>
-                )}
-              </For>
-            ) : (
-              <li>
-                <span data-element-label>Models</span>
-                <span data-placeholder>&mdash;</span>
+    <Show when={store.info}>
+      <main classList={{ [styles.root]: true, "not-content": true }}>
+        <div data-component="header">
+          <h1 data-component="header-title">{store.info?.title}</h1>
+          <div data-component="header-details">
+            <ul data-component="header-stats">
+              <li title="opencode version" data-slot="item">
+                <div data-slot="icon" title="opencode">
+                  <IconOpencode width={16} height={16} />
+                </div>
+                <Show when={store.info?.version} fallback="v0.0.1">
+                  <span>v{store.info?.version}</span>
+                </Show>
               </li>
-            )}
-          </ul>
-          <div
-            data-component="header-time"
-            title={DateTime.fromMillis(data().created || 0).toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}
-          >
-            {DateTime.fromMillis(data().created || 0).toLocaleString(DateTime.DATETIME_MED)}
+              {Object.values(data().models).length > 0 ? (
+                <For each={Object.values(data().models)}>
+                  {([provider, model]) => (
+                    <li data-slot="item">
+                      <div data-slot="icon" title={provider}>
+                        <ProviderIcon model={model} />
+                      </div>
+                      <span data-slot="model">{model}</span>
+                    </li>
+                  )}
+                </For>
+              ) : (
+                <li>
+                  <span data-element-label>Models</span>
+                  <span data-placeholder>&mdash;</span>
+                </li>
+              )}
+            </ul>
+            <div
+              data-component="header-time"
+              title={DateTime.fromMillis(data().created || 0).toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}
+            >
+              {DateTime.fromMillis(data().created || 0).toLocaleString(DateTime.DATETIME_MED)}
+            </div>
           </div>
         </div>
-      </div>
-
-      <div>
-        <Show when={data().messages.length > 0} fallback={<p>Waiting for messages...</p>}>
-          <div class={styles.parts}>
-            <SuspenseList revealOrder="forwards">
-              <For each={data().messages}>
-                {(msg, msgIndex) => {
-                  const filteredParts = createMemo(() =>
-                    msg.parts.filter((x, index) => {
-                      if (x.type === "step-start" && index > 0) return false
-                      if (x.type === "snapshot") return false
-                      if (x.type === "patch") return false
-                      if (x.type === "step-finish") return false
-                      if (x.type === "text" && x.synthetic === true) return false
-                      if (x.type === "tool" && x.tool === "todoread") return false
-                      if (x.type === "text" && !x.text) return false
-                      if (x.type === "tool" && (x.state.status === "pending" || x.state.status === "running"))
-                        return false
-                      return true
-                    }),
-                  )
-
-                  return (
-                    <Suspense>
-                      <For each={filteredParts()}>
-                        {(part, partIndex) => {
-                          const last = createMemo(
-                            () =>
-                              data().messages.length === msgIndex() + 1 && filteredParts().length === partIndex() + 1,
-                          )
-
-                          onMount(() => {
-                            const hash = window.location.hash.slice(1)
-                            // Wait till all parts are loaded
-                            if (
-                              hash !== "" &&
-                              !hasScrolledToAnchor &&
-                              filteredParts().length === partIndex() + 1 &&
-                              data().messages.length === msgIndex() + 1
-                            ) {
-                              hasScrolledToAnchor = true
-                              scrollToAnchor(hash)
-                            }
-                          })
-
-                          return <Part last={last()} part={part} index={partIndex()} message={msg} />
-                        }}
-                      </For>
-                    </Suspense>
-                  )
-                }}
-              </For>
-            </SuspenseList>
-            <div data-section="part" data-part-type="summary">
-              <div data-section="decoration">
-                <span data-status={connectionStatus()[0]}></span>
+
+        <div>
+          <Show when={data().messages.length > 0} fallback={<p>Waiting for messages...</p>}>
+            <div class={styles.parts}>
+              <SuspenseList revealOrder="forwards">
+                <For each={data().messages}>
+                  {(msg, msgIndex) => {
+                    const filteredParts = createMemo(() =>
+                      msg.parts.filter((x, index) => {
+                        if (x.type === "step-start" && index > 0) return false
+                        if (x.type === "snapshot") return false
+                        if (x.type === "patch") return false
+                        if (x.type === "step-finish") return false
+                        if (x.type === "text" && x.synthetic === true) return false
+                        if (x.type === "tool" && x.tool === "todoread") return false
+                        if (x.type === "text" && !x.text) return false
+                        if (x.type === "tool" && (x.state.status === "pending" || x.state.status === "running"))
+                          return false
+                        return true
+                      }),
+                    )
+
+                    return (
+                      <Suspense>
+                        <For each={filteredParts()}>
+                          {(part, partIndex) => {
+                            const last = createMemo(
+                              () =>
+                                data().messages.length === msgIndex() + 1 && filteredParts().length === partIndex() + 1,
+                            )
+
+                            onMount(() => {
+                              const hash = window.location.hash.slice(1)
+                              // Wait till all parts are loaded
+                              if (
+                                hash !== "" &&
+                                !hasScrolledToAnchor &&
+                                filteredParts().length === partIndex() + 1 &&
+                                data().messages.length === msgIndex() + 1
+                              ) {
+                                hasScrolledToAnchor = true
+                                scrollToAnchor(hash)
+                              }
+                            })
+
+                            return <Part last={last()} part={part} index={partIndex()} message={msg} />
+                          }}
+                        </For>
+                      </Suspense>
+                    )
+                  }}
+                </For>
+              </SuspenseList>
+              <div data-section="part" data-part-type="summary">
+                <div data-section="decoration">
+                  <span data-status={connectionStatus()[0]}></span>
+                </div>
+                <div data-section="content">
+                  <p data-section="copy">{getStatusText(connectionStatus())}</p>
+                  <ul data-section="stats">
+                    <li>
+                      <span data-element-label>Cost</span>
+                      {data().cost !== undefined ? (
+                        <span>${data().cost.toFixed(2)}</span>
+                      ) : (
+                        <span data-placeholder>&mdash;</span>
+                      )}
+                    </li>
+                    <li>
+                      <span data-element-label>Input Tokens</span>
+                      {data().tokens.input ? <span>{data().tokens.input}</span> : <span data-placeholder>&mdash;</span>}
+                    </li>
+                    <li>
+                      <span data-element-label>Output Tokens</span>
+                      {data().tokens.output ? <span>{data().tokens.output}</span> : <span data-placeholder>&mdash;</span>}
+                    </li>
+                    <li>
+                      <span data-element-label>Reasoning Tokens</span>
+                      {data().tokens.reasoning ? (
+                        <span>{data().tokens.reasoning}</span>
+                      ) : (
+                        <span data-placeholder>&mdash;</span>
+                      )}
+                    </li>
+                  </ul>
+                </div>
               </div>
-              <div data-section="content">
-                <p data-section="copy">{getStatusText(connectionStatus())}</p>
-                <ul data-section="stats">
-                  <li>
-                    <span data-element-label>Cost</span>
-                    {data().cost !== undefined ? (
-                      <span>${data().cost.toFixed(2)}</span>
-                    ) : (
-                      <span data-placeholder>&mdash;</span>
-                    )}
-                  </li>
-                  <li>
-                    <span data-element-label>Input Tokens</span>
-                    {data().tokens.input ? <span>{data().tokens.input}</span> : <span data-placeholder>&mdash;</span>}
-                  </li>
-                  <li>
-                    <span data-element-label>Output Tokens</span>
-                    {data().tokens.output ? <span>{data().tokens.output}</span> : <span data-placeholder>&mdash;</span>}
-                  </li>
-                  <li>
-                    <span data-element-label>Reasoning Tokens</span>
-                    {data().tokens.reasoning ? (
-                      <span>{data().tokens.reasoning}</span>
-                    ) : (
-                      <span data-placeholder>&mdash;</span>
+            </div>
+          </Show>
+        </div>
+
+        <Show when={debug}>
+          <div style={{ margin: "2rem 0" }}>
+            <div
+              style={{
+                border: "1px solid #ccc",
+                padding: "1rem",
+                "overflow-y": "auto",
+              }}
+            >
+              <Show when={data().messages.length > 0} fallback={<p>Waiting for messages...</p>}>
+                <ul style={{ "list-style-type": "none", padding: 0 }}>
+                  <For each={data().messages}>
+                    {(msg) => (
+                      <li
+                        style={{
+                          padding: "0.75rem",
+                          margin: "0.75rem 0",
+                          "box-shadow": "0 1px 3px rgba(0,0,0,0.1)",
+                        }}
+                      >
+                        <div>
+                          <strong>Key:</strong> {msg.id}
+                        </div>
+                        <pre>{JSON.stringify(msg, null, 2)}</pre>
+                      </li>
                     )}
-                  </li>
+                  </For>
                 </ul>
-              </div>
+              </Show>
             </div>
           </div>
         </Show>
-      </div>
-
-      <Show when={debug}>
-        <div style={{ margin: "2rem 0" }}>
-          <div
-            style={{
-              border: "1px solid #ccc",
-              padding: "1rem",
-              "overflow-y": "auto",
+
+        <Show when={showScrollButton()}>
+          <button
+            type="button"
+            class={styles["scroll-button"]}
+            onClick={() => document.body.scrollIntoView({ behavior: "smooth", block: "end" })}
+            onMouseEnter={() => {
+              setIsButtonHovered(true)
+              if (scrollTimeout) {
+                clearTimeout(scrollTimeout)
+              }
             }}
+            onMouseLeave={() => {
+              setIsButtonHovered(false)
+              if (showScrollButton()) {
+                scrollTimeout = window.setTimeout(() => {
+                  if (!isButtonHovered()) {
+                    setShowScrollButton(false)
+                  }
+                }, 3000)
+              }
+            }}
+            title="Scroll to bottom"
+            aria-label="Scroll to bottom"
           >
-            <Show when={data().messages.length > 0} fallback={<p>Waiting for messages...</p>}>
-              <ul style={{ "list-style-type": "none", padding: 0 }}>
-                <For each={data().messages}>
-                  {(msg) => (
-                    <li
-                      style={{
-                        padding: "0.75rem",
-                        margin: "0.75rem 0",
-                        "box-shadow": "0 1px 3px rgba(0,0,0,0.1)",
-                      }}
-                    >
-                      <div>
-                        <strong>Key:</strong> {msg.id}
-                      </div>
-                      <pre>{JSON.stringify(msg, null, 2)}</pre>
-                    </li>
-                  )}
-                </For>
-              </ul>
-            </Show>
-          </div>
-        </div>
-      </Show>
-
-      <Show when={showScrollButton()}>
-        <button
-          type="button"
-          class={styles["scroll-button"]}
-          onClick={() => document.body.scrollIntoView({ behavior: "smooth", block: "end" })}
-          onMouseEnter={() => {
-            setIsButtonHovered(true)
-            if (scrollTimeout) {
-              clearTimeout(scrollTimeout)
-            }
-          }}
-          onMouseLeave={() => {
-            setIsButtonHovered(false)
-            if (showScrollButton()) {
-              scrollTimeout = window.setTimeout(() => {
-                if (!isButtonHovered()) {
-                  setShowScrollButton(false)
-                }
-              }, 3000)
-            }
-          }}
-          title="Scroll to bottom"
-          aria-label="Scroll to bottom"
-        >
-          <IconArrowDown width={20} height={20} />
-        </button>
-      </Show>
-    </main>
+            <IconArrowDown width={20} height={20} />
+          </button>
+        </Show>
+      </main>
+    </Show>
   )
 }