Explorar el Código

fix: desktop layout

Adam hace 2 meses
padre
commit
41e234c6d0
Se han modificado 3 ficheros con 323 adiciones y 276 borrados
  1. 4 0
      bun.lock
  2. 2 0
      packages/ui/package.json
  3. 317 276
      packages/ui/src/components/session-turn.tsx

+ 4 - 0
bun.lock

@@ -382,6 +382,8 @@
         "@opencode-ai/util": "workspace:*",
         "@pierre/precision-diffs": "catalog:",
         "@shikijs/transformers": "3.9.2",
+        "@solid-primitives/bounds": "0.1.3",
+        "@solid-primitives/resize-observer": "2.1.3",
         "@solidjs/meta": "catalog:",
         "@typescript/native-preview": "catalog:",
         "fuzzysort": "catalog:",
@@ -1553,6 +1555,8 @@
 
     "@solid-primitives/audio": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UMD3ORQfI5Ky8yuKPxidDiEazsjv/dsoiKK5yZxLnsgaeNR1Aym3/77h/qT1jBYeXUgj4DX6t7NMpFUSVr14OQ=="],
 
+    "@solid-primitives/bounds": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/resize-observer": "^2.1.3", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UbiyKMdSPmtijcEDnYLQL3zzaejpwWDAJJ4Gt5P0hgVs6A72piov0GyNw7V2SroH7NZFwxlYS22YmOr8A5xc1Q=="],
+
     "@solid-primitives/event-bus": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-l+n10/51neGcMaP3ypYt21bXfoeWh8IaC8k7fYuY3ww2a8S1Zv2N2a7FF5Qn+waTu86l0V8/nRHjkyqVIZBYwA=="],
 
     "@solid-primitives/event-listener": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="],

+ 2 - 0
packages/ui/package.json

@@ -37,6 +37,8 @@
     "@opencode-ai/util": "workspace:*",
     "@pierre/precision-diffs": "catalog:",
     "@shikijs/transformers": "3.9.2",
+    "@solid-primitives/bounds": "0.1.3",
+    "@solid-primitives/resize-observer": "2.1.3",
     "@solidjs/meta": "catalog:",
     "@typescript/native-preview": "catalog:",
     "fuzzysort": "catalog:",

+ 317 - 276
packages/ui/src/components/session-turn.tsx

@@ -3,7 +3,19 @@ import { useData } from "../context"
 import { useDiffComponent } from "../context/diff"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
 import { checksum } from "@opencode-ai/util/encode"
-import { createEffect, createMemo, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
+import {
+  createEffect,
+  createMemo,
+  createSignal,
+  For,
+  Match,
+  onCleanup,
+  onMount,
+  ParentProps,
+  Show,
+  Switch,
+} from "solid-js"
+import { createResizeObserver } from "@solid-primitives/resize-observer"
 import { DiffChanges } from "./diff-changes"
 import { Typewriter } from "./typewriter"
 import { Message } from "./message-part"
@@ -48,304 +60,333 @@ export function SessionTurn(
   )
   const working = createMemo(() => status()?.type !== "idle")
 
+  let scrollRef: HTMLDivElement | undefined
+  let contentRef: HTMLDivElement | undefined
+  const [userScrolled, setUserScrolled] = createSignal(false)
+
+  function handleScroll() {
+    if (!scrollRef) return
+    const { scrollTop, scrollHeight, clientHeight } = scrollRef
+    const atBottom = scrollHeight - scrollTop - clientHeight < 50
+    if (!atBottom && working()) {
+      setUserScrolled(true)
+    }
+  }
+
+  createEffect(() => {
+    if (!working()) {
+      setUserScrolled(false)
+    }
+  })
+
+  onMount(() => {
+    if (!contentRef) return
+    createResizeObserver(contentRef, () => {
+      if (!scrollRef || userScrolled() || !working()) return
+      scrollRef.scrollTop = scrollRef.scrollHeight
+    })
+  })
+
   return (
     <div data-component="session-turn" class={props.classes?.root}>
-      <div data-slot="session-turn-content" class={props.classes?.content}>
-        <Show when={message()}>
-          {(message) => {
-            const assistantMessages = createMemo(() => {
-              return messages()?.filter(
-                (m) => m.role === "assistant" && m.parentID == message().id,
-              ) as AssistantMessage[]
-            })
-            const lastAssistantMessage = createMemo(() => assistantMessages()?.at(-1))
-            const assistantMessageParts = createMemo(() => assistantMessages()?.flatMap((m) => data.store.part[m.id]))
-            const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
-            const parts = createMemo(() => data.store.part[message().id])
-            const lastTextPart = createMemo(() =>
-              assistantMessageParts()
-                .filter((p) => p?.type === "text")
-                ?.at(-1),
-            )
-            const summary = createMemo(() => message().summary?.body ?? lastTextPart()?.text)
-            const lastTextPartShown = createMemo(
-              () => !message().summary?.body && (lastTextPart()?.text?.length ?? 0) > 0,
-            )
+      <div ref={scrollRef} onScroll={handleScroll} data-slot="session-turn-content" class={props.classes?.content}>
+        <div ref={contentRef}>
+          <Show when={message()}>
+            {(message) => {
+              const assistantMessages = createMemo(() => {
+                return messages()?.filter(
+                  (m) => m.role === "assistant" && m.parentID == message().id,
+                ) as AssistantMessage[]
+              })
+              const lastAssistantMessage = createMemo(() => assistantMessages()?.at(-1))
+              const assistantMessageParts = createMemo(() => assistantMessages()?.flatMap((m) => data.store.part[m.id]))
+              const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
+              const parts = createMemo(() => data.store.part[message().id])
+              const lastTextPart = createMemo(() =>
+                assistantMessageParts()
+                  .filter((p) => p?.type === "text")
+                  ?.at(-1),
+              )
+              const summary = createMemo(() => message().summary?.body ?? lastTextPart()?.text)
+              const lastTextPartShown = createMemo(
+                () => !message().summary?.body && (lastTextPart()?.text?.length ?? 0) > 0,
+              )
 
-            const assistantParts = createMemo(() => assistantMessages().flatMap((m) => data.store.part[m.id]))
-            const currentTask = createMemo(
-              () =>
-                assistantParts().findLast(
-                  (p) =>
-                    p &&
-                    p.type === "tool" &&
-                    p.tool === "task" &&
-                    p.state &&
-                    "metadata" in p.state &&
-                    p.state.metadata &&
-                    p.state.metadata.sessionId &&
-                    p.state.status === "running",
-                ) as ToolPart,
-            )
-            const resolvedParts = createMemo(() => {
-              let resolved = assistantParts()
-              const task = currentTask()
-              if (task && task.state && "metadata" in task.state && task.state.metadata?.sessionId) {
-                const messages = data.store.message[task.state.metadata.sessionId as string]?.filter(
-                  (m) => m.role === "assistant",
-                )
-                resolved = messages?.flatMap((m) => data.store.part[m.id]) ?? assistantParts()
-              }
-              return resolved
-            })
-            const lastPart = createMemo(() => resolvedParts().slice(-1)?.at(0))
-            const rawStatus = createMemo(() => {
-              const last = lastPart()
-              if (!last) return undefined
+              const assistantParts = createMemo(() => assistantMessages().flatMap((m) => data.store.part[m.id]))
+              const currentTask = createMemo(
+                () =>
+                  assistantParts().findLast(
+                    (p) =>
+                      p &&
+                      p.type === "tool" &&
+                      p.tool === "task" &&
+                      p.state &&
+                      "metadata" in p.state &&
+                      p.state.metadata &&
+                      p.state.metadata.sessionId &&
+                      p.state.status === "running",
+                  ) as ToolPart,
+              )
+              const resolvedParts = createMemo(() => {
+                let resolved = assistantParts()
+                const task = currentTask()
+                if (task && task.state && "metadata" in task.state && task.state.metadata?.sessionId) {
+                  const messages = data.store.message[task.state.metadata.sessionId as string]?.filter(
+                    (m) => m.role === "assistant",
+                  )
+                  resolved = messages?.flatMap((m) => data.store.part[m.id]) ?? assistantParts()
+                }
+                return resolved
+              })
+              const lastPart = createMemo(() => resolvedParts().slice(-1)?.at(0))
+              const rawStatus = createMemo(() => {
+                const last = lastPart()
+                if (!last) return undefined
 
-              if (last.type === "tool") {
-                switch (last.tool) {
-                  case "task":
-                    return "Delegating work"
-                  case "todowrite":
-                  case "todoread":
-                    return "Planning next steps"
-                  case "read":
-                    return "Gathering context"
-                  case "list":
-                  case "grep":
-                  case "glob":
-                    return "Searching the codebase"
-                  case "webfetch":
-                    return "Searching the web"
-                  case "edit":
-                  case "write":
-                    return "Making edits"
-                  case "bash":
-                    return "Running commands"
-                  default:
-                    break
+                if (last.type === "tool") {
+                  switch (last.tool) {
+                    case "task":
+                      return "Delegating work"
+                    case "todowrite":
+                    case "todoread":
+                      return "Planning next steps"
+                    case "read":
+                      return "Gathering context"
+                    case "list":
+                    case "grep":
+                    case "glob":
+                      return "Searching the codebase"
+                    case "webfetch":
+                      return "Searching the web"
+                    case "edit":
+                    case "write":
+                      return "Making edits"
+                    case "bash":
+                      return "Running commands"
+                    default:
+                      break
+                  }
+                } else if (last.type === "reasoning") {
+                  return "Thinking"
+                } else if (last.type === "text") {
+                  return "Gathering thoughts"
                 }
-              } else if (last.type === "reasoning") {
-                return "Thinking"
-              } else if (last.type === "text") {
-                return "Gathering thoughts"
-              }
-              return undefined
-            })
+                return undefined
+              })
 
-            function duration() {
-              const completed = lastAssistantMessage()?.time.completed
-              const from = DateTime.fromMillis(message()!.time.created)
-              const to = completed ? DateTime.fromMillis(completed) : DateTime.now()
-              const interval = Interval.fromDateTimes(from, to)
-              const unit: DurationUnit[] = interval.length("seconds") > 60 ? ["minutes", "seconds"] : ["seconds"]
+              function duration() {
+                const completed = lastAssistantMessage()?.time.completed
+                const from = DateTime.fromMillis(message()!.time.created)
+                const to = completed ? DateTime.fromMillis(completed) : DateTime.now()
+                const interval = Interval.fromDateTimes(from, to)
+                const unit: DurationUnit[] = interval.length("seconds") > 60 ? ["minutes", "seconds"] : ["seconds"]
 
-              return interval.toDuration(unit).normalize().toHuman({
-                notation: "compact",
-                unitDisplay: "narrow",
-                compactDisplay: "short",
-                showZeros: false,
-              })
-            }
+                return interval.toDuration(unit).normalize().toHuman({
+                  notation: "compact",
+                  unitDisplay: "narrow",
+                  compactDisplay: "short",
+                  showZeros: false,
+                })
+              }
 
-            const [store, setStore] = createStore({
-              status: rawStatus(),
-              detailsExpanded: true,
-              duration: duration(),
-            })
+              const [store, setStore] = createStore({
+                status: rawStatus(),
+                detailsExpanded: true,
+                duration: duration(),
+              })
 
-            createEffect(() => {
-              const timer = setInterval(() => {
-                setStore("duration", duration())
-              }, 1000)
-              onCleanup(() => clearInterval(timer))
-            })
+              createEffect(() => {
+                const timer = setInterval(() => {
+                  setStore("duration", duration())
+                }, 1000)
+                onCleanup(() => clearInterval(timer))
+              })
 
-            let lastStatusChange = Date.now()
-            let statusTimeout: number | undefined
-            createEffect(() => {
-              const newStatus = rawStatus()
-              if (newStatus === store.status || !newStatus) return
+              let lastStatusChange = Date.now()
+              let statusTimeout: number | undefined
+              createEffect(() => {
+                const newStatus = rawStatus()
+                if (newStatus === store.status || !newStatus) return
 
-              const timeSinceLastChange = Date.now() - lastStatusChange
+                const timeSinceLastChange = Date.now() - lastStatusChange
 
-              if (timeSinceLastChange >= 2500) {
-                setStore("status", newStatus)
-                lastStatusChange = Date.now()
-                if (statusTimeout) {
-                  clearTimeout(statusTimeout)
-                  statusTimeout = undefined
-                }
-              } else {
-                if (statusTimeout) clearTimeout(statusTimeout)
-                statusTimeout = setTimeout(() => {
-                  setStore("status", rawStatus())
+                if (timeSinceLastChange >= 2500) {
+                  setStore("status", newStatus)
                   lastStatusChange = Date.now()
-                  statusTimeout = undefined
-                }, 2500 - timeSinceLastChange) as unknown as number
-              }
-            })
+                  if (statusTimeout) {
+                    clearTimeout(statusTimeout)
+                    statusTimeout = undefined
+                  }
+                } else {
+                  if (statusTimeout) clearTimeout(statusTimeout)
+                  statusTimeout = setTimeout(() => {
+                    setStore("status", rawStatus())
+                    lastStatusChange = Date.now()
+                    statusTimeout = undefined
+                  }, 2500 - timeSinceLastChange) as unknown as number
+                }
+              })
 
-            return (
-              <div
-                data-message={message().id}
-                data-slot="session-turn-message-container"
-                class={props.classes?.container}
-              >
-                {/* Title */}
-                <div data-slot="session-turn-message-header">
-                  <div data-slot="session-turn-message-title">
-                    <Switch>
-                      <Match when={working()}>
-                        <Typewriter as="h1" text={message().summary?.title} data-slot="session-turn-typewriter" />
-                      </Match>
-                      <Match when={true}>
-                        <h1>{message().summary?.title}</h1>
-                      </Match>
-                    </Switch>
+              return (
+                <div
+                  data-message={message().id}
+                  data-slot="session-turn-message-container"
+                  class={props.classes?.container}
+                >
+                  {/* Title */}
+                  <div data-slot="session-turn-message-header">
+                    <div data-slot="session-turn-message-title">
+                      <Switch>
+                        <Match when={working()}>
+                          <Typewriter as="h1" text={message().summary?.title} data-slot="session-turn-typewriter" />
+                        </Match>
+                        <Match when={true}>
+                          <h1>{message().summary?.title}</h1>
+                        </Match>
+                      </Switch>
+                    </div>
                   </div>
-                </div>
-                <div data-slot="session-turn-message-content">
-                  <Message message={message()} parts={parts()} />
-                </div>
-                {/* Response */}
-                <div data-slot="session-turn-response-section">
-                  <Collapsible
-                    variant="ghost"
-                    open={store.detailsExpanded}
-                    onOpenChange={(open) => setStore("detailsExpanded", open)}
-                    data-slot="session-turn-collapsible"
-                  >
-                    <Collapsible.Trigger
-                      as={Button}
-                      data-slot="session-turn-collapsible-trigger-content"
+                  <div data-slot="session-turn-message-content">
+                    <Message message={message()} parts={parts()} />
+                  </div>
+                  {/* Response */}
+                  <div data-slot="session-turn-response-section">
+                    <Collapsible
                       variant="ghost"
-                      size="small"
+                      open={store.detailsExpanded}
+                      onOpenChange={(open) => setStore("detailsExpanded", open)}
+                      data-slot="session-turn-collapsible"
                     >
-                      <Show when={working()}>
-                        <Spinner />
-                      </Show>
-                      <Switch>
-                        <Match when={working()}>{store.status ?? "Considering next steps..."}</Match>
-                        <Match when={store.detailsExpanded}>Hide steps</Match>
-                        <Match when={!store.detailsExpanded}>Show steps</Match>
-                      </Switch>
-                      <span>·</span>
-                      <span>{store.duration}</span>
-                      <Icon name="chevron-grabber-vertical" size="small" />
-                    </Collapsible.Trigger>
-                    <Collapsible.Content>
-                      <div data-slot="session-turn-collapsible-content-inner">
-                        <For each={assistantMessages()}>
-                          {(assistantMessage) => {
-                            const parts = createMemo(() => data.store.part[assistantMessage.id] ?? [])
-                            const last = createMemo(() =>
-                              parts()
-                                .filter((p) => p?.type === "text")
-                                .at(-1),
-                            )
-                            return (
-                              <Switch>
-                                <Match when={lastTextPartShown() && lastTextPart()?.id === last()?.id}>
-                                  <Message
-                                    message={assistantMessage}
-                                    parts={parts().filter((p) => p?.id !== last()?.id)}
-                                  />
-                                </Match>
-                                <Match when={true}>
-                                  <Message message={assistantMessage} parts={parts()} />
-                                </Match>
-                              </Switch>
-                            )
-                          }}
-                        </For>
-                        <Show when={error()}>
-                          <Card variant="error" class="error-card">
-                            {error()?.data?.message as string}
-                          </Card>
+                      <Collapsible.Trigger
+                        as={Button}
+                        data-slot="session-turn-collapsible-trigger-content"
+                        variant="ghost"
+                        size="small"
+                      >
+                        <Show when={working()}>
+                          <Spinner />
                         </Show>
-                      </div>
-                    </Collapsible.Content>
-                  </Collapsible>
-                </div>
-                {/* Summary */}
-                <Show when={!working()}>
-                  <div data-slot="session-turn-summary-section">
-                    <div data-slot="session-turn-summary-header">
-                      <h2 data-slot="session-turn-summary-title">
                         <Switch>
-                          <Match when={message().summary?.diffs?.length}>Summary</Match>
-                          <Match when={true}>Response</Match>
+                          <Match when={working()}>{store.status ?? "Considering next steps..."}</Match>
+                          <Match when={store.detailsExpanded}>Hide steps</Match>
+                          <Match when={!store.detailsExpanded}>Show steps</Match>
                         </Switch>
-                      </h2>
-                      <Show when={summary()}>
-                        {(summary) => (
-                          <Markdown
-                            data-slot="session-turn-markdown"
-                            data-diffs={!!message().summary?.diffs?.length}
-                            text={summary()}
-                          />
-                        )}
-                      </Show>
-                    </div>
-                    <Accordion data-slot="session-turn-accordion" multiple>
-                      <For each={message().summary?.diffs ?? []}>
-                        {(diff) => (
-                          <Accordion.Item value={diff.file}>
-                            <StickyAccordionHeader>
-                              <Accordion.Trigger>
-                                <div data-slot="session-turn-accordion-trigger-content">
-                                  <div data-slot="session-turn-file-info">
-                                    <FileIcon
-                                      node={{ path: diff.file, type: "file" }}
-                                      data-slot="session-turn-file-icon"
+                        <span>·</span>
+                        <span>{store.duration}</span>
+                        <Icon name="chevron-grabber-vertical" size="small" />
+                      </Collapsible.Trigger>
+                      <Collapsible.Content>
+                        <div data-slot="session-turn-collapsible-content-inner">
+                          <For each={assistantMessages()}>
+                            {(assistantMessage) => {
+                              const parts = createMemo(() => data.store.part[assistantMessage.id] ?? [])
+                              const last = createMemo(() =>
+                                parts()
+                                  .filter((p) => p?.type === "text")
+                                  .at(-1),
+                              )
+                              return (
+                                <Switch>
+                                  <Match when={lastTextPartShown() && lastTextPart()?.id === last()?.id}>
+                                    <Message
+                                      message={assistantMessage}
+                                      parts={parts().filter((p) => p?.id !== last()?.id)}
                                     />
-                                    <div data-slot="session-turn-file-path">
-                                      <Show when={diff.file.includes("/")}>
-                                        <span data-slot="session-turn-directory">{getDirectory(diff.file)}&lrm;</span>
-                                      </Show>
-                                      <span data-slot="session-turn-filename">{getFilename(diff.file)}</span>
+                                  </Match>
+                                  <Match when={true}>
+                                    <Message message={assistantMessage} parts={parts()} />
+                                  </Match>
+                                </Switch>
+                              )
+                            }}
+                          </For>
+                          <Show when={error()}>
+                            <Card variant="error" class="error-card">
+                              {error()?.data?.message as string}
+                            </Card>
+                          </Show>
+                        </div>
+                      </Collapsible.Content>
+                    </Collapsible>
+                  </div>
+                  {/* Summary */}
+                  <Show when={!working()}>
+                    <div data-slot="session-turn-summary-section">
+                      <div data-slot="session-turn-summary-header">
+                        <h2 data-slot="session-turn-summary-title">
+                          <Switch>
+                            <Match when={message().summary?.diffs?.length}>Summary</Match>
+                            <Match when={true}>Response</Match>
+                          </Switch>
+                        </h2>
+                        <Show when={summary()}>
+                          {(summary) => (
+                            <Markdown
+                              data-slot="session-turn-markdown"
+                              data-diffs={!!message().summary?.diffs?.length}
+                              text={summary()}
+                            />
+                          )}
+                        </Show>
+                      </div>
+                      <Accordion data-slot="session-turn-accordion" multiple>
+                        <For each={message().summary?.diffs ?? []}>
+                          {(diff) => (
+                            <Accordion.Item value={diff.file}>
+                              <StickyAccordionHeader>
+                                <Accordion.Trigger>
+                                  <div data-slot="session-turn-accordion-trigger-content">
+                                    <div data-slot="session-turn-file-info">
+                                      <FileIcon
+                                        node={{ path: diff.file, type: "file" }}
+                                        data-slot="session-turn-file-icon"
+                                      />
+                                      <div data-slot="session-turn-file-path">
+                                        <Show when={diff.file.includes("/")}>
+                                          <span data-slot="session-turn-directory">{getDirectory(diff.file)}&lrm;</span>
+                                        </Show>
+                                        <span data-slot="session-turn-filename">{getFilename(diff.file)}</span>
+                                      </div>
+                                    </div>
+                                    <div data-slot="session-turn-accordion-actions">
+                                      <DiffChanges changes={diff} />
+                                      <Icon name="chevron-grabber-vertical" size="small" />
                                     </div>
                                   </div>
-                                  <div data-slot="session-turn-accordion-actions">
-                                    <DiffChanges changes={diff} />
-                                    <Icon name="chevron-grabber-vertical" size="small" />
-                                  </div>
-                                </div>
-                              </Accordion.Trigger>
-                            </StickyAccordionHeader>
-                            <Accordion.Content data-slot="session-turn-accordion-content">
-                              <Dynamic
-                                component={diffComponent}
-                                before={{
-                                  name: diff.file!,
-                                  contents: diff.before!,
-                                  cacheKey: checksum(diff.before!),
-                                }}
-                                after={{
-                                  name: diff.file!,
-                                  contents: diff.after!,
-                                  cacheKey: checksum(diff.after!),
-                                }}
-                              />
-                            </Accordion.Content>
-                          </Accordion.Item>
-                        )}
-                      </For>
-                    </Accordion>
-                  </div>
-                </Show>
-                <Show when={error() && !store.detailsExpanded}>
-                  <Card variant="error" class="error-card">
-                    {error()?.data?.message as string}
-                  </Card>
-                </Show>
-              </div>
-            )
-          }}
-        </Show>
-        {props.children}
+                                </Accordion.Trigger>
+                              </StickyAccordionHeader>
+                              <Accordion.Content data-slot="session-turn-accordion-content">
+                                <Dynamic
+                                  component={diffComponent}
+                                  before={{
+                                    name: diff.file!,
+                                    contents: diff.before!,
+                                    cacheKey: checksum(diff.before!),
+                                  }}
+                                  after={{
+                                    name: diff.file!,
+                                    contents: diff.after!,
+                                    cacheKey: checksum(diff.after!),
+                                  }}
+                                />
+                              </Accordion.Content>
+                            </Accordion.Item>
+                          )}
+                        </For>
+                      </Accordion>
+                    </div>
+                  </Show>
+                  <Show when={error() && !store.detailsExpanded}>
+                    <Card variant="error" class="error-card">
+                      {error()?.data?.message as string}
+                    </Card>
+                  </Show>
+                </div>
+              )
+            }}
+          </Show>
+          {props.children}
+        </div>
       </div>
     </div>
   )