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

feat(desktop): mobile responsiveness

Adam 3 месяцев назад
Родитель
Сommit
653c206688

+ 34 - 22
packages/desktop/src/components/header.tsx

@@ -20,6 +20,7 @@ import { iife } from "@opencode-ai/util/iife"
 export function Header(props: {
 export function Header(props: {
   navigateToProject: (directory: string) => void
   navigateToProject: (directory: string) => void
   navigateToSession: (session: Session | undefined) => void
   navigateToSession: (session: Session | undefined) => void
+  onMobileMenuToggle?: () => void
 }) {
 }) {
   const globalSync = useGlobalSync()
   const globalSync = useGlobalSync()
   const globalSDK = useGlobalSDK()
   const globalSDK = useGlobalSDK()
@@ -29,11 +30,19 @@ export function Header(props: {
 
 
   return (
   return (
     <header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
     <header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
+      <button
+        type="button"
+        class="xl:hidden w-12 shrink-0 flex items-center justify-center border-r border-border-weak-base hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors"
+        onClick={props.onMobileMenuToggle}
+      >
+        <Icon name="menu" size="small" />
+      </button>
       <A
       <A
         href="/"
         href="/"
         classList={{
         classList={{
+          "hidden xl:flex": true,
           "w-12 shrink-0 px-4 py-3.5": true,
           "w-12 shrink-0 px-4 py-3.5": true,
-          "flex items-center justify-start self-stretch": true,
+          "items-center justify-start self-stretch": true,
           "border-r border-border-weak-base": true,
           "border-r border-border-weak-base": true,
         }}
         }}
         style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
         style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
@@ -51,25 +60,27 @@ export function Header(props: {
             const shareEnabled = createMemo(() => store().config.share !== "disabled")
             const shareEnabled = createMemo(() => store().config.share !== "disabled")
             return (
             return (
               <>
               <>
-                <div class="flex items-center gap-3">
-                  <div class="flex items-center gap-2">
-                    <Select
-                      options={layout.projects.list().map((project) => project.worktree)}
-                      current={currentDirectory()}
-                      label={(x) => getFilename(x)}
-                      onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
-                      class="text-14-regular text-text-base"
-                      variant="ghost"
-                    >
-                      {/* @ts-ignore */}
-                      {(i) => (
-                        <div class="flex items-center gap-2">
-                          <Icon name="folder" size="small" />
-                          <div class="text-text-strong">{getFilename(i)}</div>
-                        </div>
-                      )}
-                    </Select>
-                    <div class="text-text-weaker">/</div>
+                <div class="flex items-center gap-3 min-w-0">
+                  <div class="flex items-center gap-2 min-w-0">
+                    <div class="hidden xl:flex items-center gap-2">
+                      <Select
+                        options={layout.projects.list().map((project) => project.worktree)}
+                        current={currentDirectory()}
+                        label={(x) => getFilename(x)}
+                        onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
+                        class="text-14-regular text-text-base"
+                        variant="ghost"
+                      >
+                        {/* @ts-ignore */}
+                        {(i) => (
+                          <div class="flex items-center gap-2">
+                            <Icon name="folder" size="small" />
+                            <div class="text-text-strong">{getFilename(i)}</div>
+                          </div>
+                        )}
+                      </Select>
+                      <div class="text-text-weaker">/</div>
+                    </div>
                     <Select
                     <Select
                       options={sessions()}
                       options={sessions()}
                       current={currentSession()}
                       current={currentSession()}
@@ -77,12 +88,13 @@ export function Header(props: {
                       label={(x) => x.title}
                       label={(x) => x.title}
                       value={(x) => x.id}
                       value={(x) => x.id}
                       onSelect={props.navigateToSession}
                       onSelect={props.navigateToSession}
-                      class="text-14-regular text-text-base max-w-md"
+                      class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
                       variant="ghost"
                       variant="ghost"
                     />
                     />
                   </div>
                   </div>
                   <Show when={currentSession()}>
                   <Show when={currentSession()}>
                     <Tooltip
                     <Tooltip
+                      class="hidden xl:block"
                       value={
                       value={
                         <div class="flex items-center gap-2">
                         <div class="flex items-center gap-2">
                           <span>New session</span>
                           <span>New session</span>
@@ -98,7 +110,7 @@ export function Header(props: {
                 </div>
                 </div>
                 <div class="flex items-center gap-4">
                 <div class="flex items-center gap-4">
                   <Tooltip
                   <Tooltip
-                    class="shrink-0"
+                    class="hidden md:block shrink-0"
                     value={
                     value={
                       <div class="flex items-center gap-2">
                       <div class="flex items-center gap-2">
                         <span>Toggle terminal</span>
                         <span>Toggle terminal</span>

+ 4 - 2
packages/desktop/src/components/prompt-input.tsx

@@ -972,7 +972,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
             }}
             }}
           />
           />
           <Show when={!prompt.dirty() && store.imageAttachments.length === 0}>
           <Show when={!prompt.dirty() && store.imageAttachments.length === 0}>
-            <div class="absolute top-0 left-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none">
+            <div class="absolute top-0 inset-x-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate">
               {store.mode === "shell"
               {store.mode === "shell"
                 ? "Enter shell command..."
                 ? "Enter shell command..."
                 : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
                 : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
@@ -1026,7 +1026,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                     }
                     }
                   >
                   >
                     {local.model.current()?.name ?? "Select model"}
                     {local.model.current()?.name ?? "Select model"}
-                    <span class="ml-0.5 text-text-weak text-12-regular">{local.model.current()?.provider.name}</span>
+                    <span class="hidden md:block ml-0.5 text-text-weak text-12-regular">
+                      {local.model.current()?.provider.name}
+                    </span>
                     <Icon name="chevron-down" size="small" />
                     <Icon name="chevron-down" size="small" />
                   </Button>
                   </Button>
                 </Tooltip>
                 </Tooltip>

+ 4 - 2
packages/desktop/src/context/layout.tsx

@@ -108,10 +108,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
           setStore("projects", (x) => x.filter((x) => x.worktree !== directory))
           setStore("projects", (x) => x.filter((x) => x.worktree !== directory))
         },
         },
         expand(directory: string) {
         expand(directory: string) {
-          setStore("projects", (x) => x.map((x) => (x.worktree === directory ? { ...x, expanded: true } : x)))
+          const index = store.projects.findIndex((x) => x.worktree === directory)
+          if (index !== -1) setStore("projects", index, "expanded", true)
         },
         },
         collapse(directory: string) {
         collapse(directory: string) {
-          setStore("projects", (x) => x.map((x) => (x.worktree === directory ? { ...x, expanded: false } : x)))
+          const index = store.projects.findIndex((x) => x.worktree === directory)
+          if (index !== -1) setStore("projects", index, "expanded", false)
         },
         },
         move(directory: string, toIndex: number) {
         move(directory: string, toIndex: number) {
           setStore("projects", (projects) => {
           setStore("projects", (projects) => {

+ 198 - 121
packages/desktop/src/pages/layout.tsx

@@ -1,4 +1,16 @@
-import { createEffect, createMemo, For, Match, onMount, ParentProps, Show, Switch, type JSX } from "solid-js"
+import {
+  createEffect,
+  createMemo,
+  createSignal,
+  For,
+  Match,
+  onCleanup,
+  onMount,
+  ParentProps,
+  Show,
+  Switch,
+  type JSX,
+} from "solid-js"
 import { DateTime } from "luxon"
 import { DateTime } from "luxon"
 import { A, useNavigate, useParams } from "@solidjs/router"
 import { A, useNavigate, useParams } from "@solidjs/router"
 import { useLayout, getAvatarColors, LocalProject } from "@/context/layout"
 import { useLayout, getAvatarColors, LocalProject } from "@/context/layout"
@@ -42,9 +54,29 @@ export default function Layout(props: ParentProps) {
   const [store, setStore] = createStore({
   const [store, setStore] = createStore({
     lastSession: {} as { [directory: string]: string },
     lastSession: {} as { [directory: string]: string },
     activeDraggable: undefined as string | undefined,
     activeDraggable: undefined as string | undefined,
+    mobileSidebarOpen: false,
+    mobileProjectsExpanded: {} as Record<string, boolean>,
   })
   })
 
 
+  const mobileSidebar = {
+    open: () => store.mobileSidebarOpen,
+    show: () => setStore("mobileSidebarOpen", true),
+    hide: () => setStore("mobileSidebarOpen", false),
+    toggle: () => setStore("mobileSidebarOpen", (x) => !x),
+  }
+
+  const mobileProjects = {
+    expanded: (directory: string) => store.mobileProjectsExpanded[directory] ?? true,
+    expand: (directory: string) => setStore("mobileProjectsExpanded", directory, true),
+    collapse: (directory: string) => setStore("mobileProjectsExpanded", directory, false),
+  }
+
   let scrollContainerRef: HTMLDivElement | undefined
   let scrollContainerRef: HTMLDivElement | undefined
+  const xlQuery = window.matchMedia("(min-width: 1280px)")
+  const [isLargeViewport, setIsLargeViewport] = createSignal(xlQuery.matches)
+  const handleViewportChange = (e: MediaQueryListEvent) => setIsLargeViewport(e.matches)
+  xlQuery.addEventListener("change", handleViewportChange)
+  onCleanup(() => xlQuery.removeEventListener("change", handleViewportChange))
 
 
   const params = useParams()
   const params = useParams()
   const globalSDK = useGlobalSDK()
   const globalSDK = useGlobalSDK()
@@ -259,11 +291,13 @@ export default function Layout(props: ParentProps) {
     if (!directory) return
     if (!directory) return
     const lastSession = store.lastSession[directory]
     const lastSession = store.lastSession[directory]
     navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`)
     navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`)
+    mobileSidebar.hide()
   }
   }
 
 
   function navigateToSession(session: Session | undefined) {
   function navigateToSession(session: Session | undefined) {
     if (!session) return
     if (!session) return
     navigate(`/${params.dir}/session/${session?.id}`)
     navigate(`/${params.dir}/session/${session?.id}`)
+    mobileSidebar.hide()
   }
   }
 
 
   function openProject(directory: string, navigate = true) {
   function openProject(directory: string, navigate = true) {
@@ -302,8 +336,12 @@ export default function Layout(props: ParentProps) {
   })
   })
 
 
   createEffect(() => {
   createEffect(() => {
-    const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48
-    document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`)
+    if (isLargeViewport()) {
+      const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48
+      document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`)
+    } else {
+      document.documentElement.style.setProperty("--dialog-left-margin", "0px")
+    }
   })
   })
 
 
   function getDraggableId(event: unknown): string | undefined {
   function getDraggableId(event: unknown): string | undefined {
@@ -419,6 +457,7 @@ export default function Layout(props: ParentProps) {
     project: LocalProject
     project: LocalProject
     depth?: number
     depth?: number
     childrenMap: Map<string, Session[]>
     childrenMap: Map<string, Session[]>
+    mobile?: boolean
   }): JSX.Element => {
   }): JSX.Element => {
     const notification = useNotification()
     const notification = useNotification()
     const depth = props.depth ?? 0
     const depth = props.depth ?? 0
@@ -439,7 +478,7 @@ export default function Layout(props: ParentProps) {
                  hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
                  hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
           style={{ "padding-left": `${16 + depth * 12}px` }}
           style={{ "padding-left": `${16 + depth * 12}px` }}
         >
         >
-          <Tooltip placement="right" value={props.session.title} gutter={10}>
+          <Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}>
             <A
             <A
               href={`${props.slug}/session/${props.session.id}`}
               href={`${props.slug}/session/${props.session.id}`}
               class="flex flex-col min-w-0 text-left w-full focus:outline-none"
               class="flex flex-col min-w-0 text-left w-full focus:outline-none"
@@ -486,7 +525,7 @@ export default function Layout(props: ParentProps) {
             </A>
             </A>
           </Tooltip>
           </Tooltip>
           <div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
           <div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
-            <Tooltip placement="right" value="Archive session">
+            <Tooltip placement={props.mobile ? "bottom" : "right"} value="Archive session">
               <IconButton icon="archive" variant="ghost" onClick={() => archiveSession(props.session)} />
               <IconButton icon="archive" variant="ghost" onClick={() => archiveSession(props.session)} />
             </Tooltip>
             </Tooltip>
           </div>
           </div>
@@ -499,6 +538,7 @@ export default function Layout(props: ParentProps) {
               project={props.project}
               project={props.project}
               depth={depth + 1}
               depth={depth + 1}
               childrenMap={props.childrenMap}
               childrenMap={props.childrenMap}
+              mobile={props.mobile}
             />
             />
           )}
           )}
         </For>
         </For>
@@ -506,8 +546,9 @@ export default function Layout(props: ParentProps) {
     )
     )
   }
   }
 
 
-  const SortableProject = (props: { project: LocalProject }): JSX.Element => {
+  const SortableProject = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
     const sortable = createSortable(props.project.worktree)
     const sortable = createSortable(props.project.worktree)
+    const showExpanded = createMemo(() => props.mobile || layout.sidebar.opened())
     const slug = createMemo(() => base64Encode(props.project.worktree))
     const slug = createMemo(() => base64Encode(props.project.worktree))
     const name = createMemo(() => getFilename(props.project.worktree))
     const name = createMemo(() => getFilename(props.project.worktree))
     const [store, setProjectStore] = globalSync.child(props.project.worktree)
     const [store, setProjectStore] = globalSync.child(props.project.worktree)
@@ -531,21 +572,24 @@ export default function Layout(props: ParentProps) {
       setProjectStore("limit", (limit) => limit + 5)
       setProjectStore("limit", (limit) => limit + 5)
       await globalSync.project.loadSessions(props.project.worktree)
       await globalSync.project.loadSessions(props.project.worktree)
     }
     }
+    const isExpanded = createMemo(() =>
+      props.mobile ? mobileProjects.expanded(props.project.worktree) : props.project.expanded,
+    )
     const handleOpenChange = (open: boolean) => {
     const handleOpenChange = (open: boolean) => {
-      if (open) layout.projects.expand(props.project.worktree)
-      else layout.projects.collapse(props.project.worktree)
+      if (props.mobile) {
+        if (open) mobileProjects.expand(props.project.worktree)
+        else mobileProjects.collapse(props.project.worktree)
+      } else {
+        if (open) layout.projects.expand(props.project.worktree)
+        else layout.projects.collapse(props.project.worktree)
+      }
     }
     }
     return (
     return (
       // @ts-ignore
       // @ts-ignore
       <div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
       <div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
         <Switch>
         <Switch>
-          <Match when={layout.sidebar.opened()}>
-            <Collapsible
-              variant="ghost"
-              open={props.project.expanded}
-              class="gap-2 shrink-0"
-              onOpenChange={handleOpenChange}
-            >
+          <Match when={showExpanded()}>
+            <Collapsible variant="ghost" open={isExpanded()} class="gap-2 shrink-0" onOpenChange={handleOpenChange}>
               <Button
               <Button
                 as={"div"}
                 as={"div"}
                 variant="ghost"
                 variant="ghost"
@@ -556,7 +600,7 @@ export default function Layout(props: ParentProps) {
                     project={props.project}
                     project={props.project}
                     class="group-hover/session:hidden"
                     class="group-hover/session:hidden"
                     expandable
                     expandable
-                    notify={!props.project.expanded}
+                    notify={!isExpanded()}
                   />
                   />
                   <span class="truncate text-14-medium text-text-strong">{name()}</span>
                   <span class="truncate text-14-medium text-text-strong">{name()}</span>
                 </Collapsible.Trigger>
                 </Collapsible.Trigger>
@@ -585,6 +629,7 @@ export default function Layout(props: ParentProps) {
                         slug={slug()}
                         slug={slug()}
                         project={props.project}
                         project={props.project}
                         childrenMap={childSessionsByParent()}
                         childrenMap={childSessionsByParent()}
+                        mobile={props.mobile}
                       />
                       />
                     )}
                     )}
                   </For>
                   </For>
@@ -595,7 +640,7 @@ export default function Layout(props: ParentProps) {
                     >
                     >
                       <div class="flex items-center self-stretch w-full">
                       <div class="flex items-center self-stretch w-full">
                         <div class="flex-1 min-w-0">
                         <div class="flex-1 min-w-0">
-                          <Tooltip placement="right" value="New session">
+                          <Tooltip placement={props.mobile ? "bottom" : "right"} value="New session">
                             <A
                             <A
                               href={`${slug()}/session`}
                               href={`${slug()}/session`}
                               class="flex flex-col gap-1 min-w-0 text-left w-full focus:outline-none"
                               class="flex flex-col gap-1 min-w-0 text-left w-full focus:outline-none"
@@ -650,30 +695,12 @@ export default function Layout(props: ParentProps) {
     )
     )
   }
   }
 
 
-  return (
-    <div class="relative flex-1 min-h-0 flex flex-col">
-      <Header navigateToProject={navigateToProject} navigateToSession={navigateToSession} />
-      <div class="flex-1 min-h-0 flex">
-        <div
-          classList={{
-            "relative @container w-12 pb-5 shrink-0 bg-background-base": true,
-            "flex flex-col gap-5.5 items-start self-stretch justify-between": true,
-            "border-r border-border-weak-base contain-strict": true,
-          }}
-          style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
-        >
-          <Show when={layout.sidebar.opened()}>
-            <ResizeHandle
-              direction="horizontal"
-              size={layout.sidebar.width()}
-              min={150}
-              max={window.innerWidth * 0.3}
-              collapseThreshold={80}
-              onResize={layout.sidebar.resize}
-              onCollapse={layout.sidebar.close}
-            />
-          </Show>
-          <div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
+  const SidebarContent = (sidebarProps: { mobile?: boolean }) => {
+    const expanded = () => sidebarProps.mobile || layout.sidebar.opened()
+    return (
+      <>
+        <div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
+          <Show when={!sidebarProps.mobile}>
             <Tooltip
             <Tooltip
               class="shrink-0"
               class="shrink-0"
               placement="right"
               placement="right"
@@ -683,7 +710,7 @@ export default function Layout(props: ParentProps) {
                   <span class="text-icon-base text-12-medium">{command.keybind("sidebar.toggle")}</span>
                   <span class="text-icon-base text-12-medium">{command.keybind("sidebar.toggle")}</span>
                 </div>
                 </div>
               }
               }
-              inactive={layout.sidebar.opened()}
+              inactive={expanded()}
             >
             >
               <Button
               <Button
                 variant="ghost"
                 variant="ghost"
@@ -715,110 +742,160 @@ export default function Layout(props: ParentProps) {
                 </Show>
                 </Show>
               </Button>
               </Button>
             </Tooltip>
             </Tooltip>
-            <DragDropProvider
-              onDragStart={handleDragStart}
-              onDragEnd={handleDragEnd}
-              onDragOver={handleDragOver}
-              collisionDetector={closestCenter}
+          </Show>
+          <DragDropProvider
+            onDragStart={handleDragStart}
+            onDragEnd={handleDragEnd}
+            onDragOver={handleDragOver}
+            collisionDetector={closestCenter}
+          >
+            <DragDropSensors />
+            <ConstrainDragXAxis />
+            <div
+              ref={sidebarProps.mobile ? undefined : scrollContainerRef}
+              class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar"
             >
             >
-              <DragDropSensors />
-              <ConstrainDragXAxis />
-              <div
-                ref={scrollContainerRef}
-                class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar"
-              >
-                <SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
-                  <For each={layout.projects.list()}>{(project) => <SortableProject project={project} />}</For>
-                </SortableProvider>
-              </div>
-              <DragOverlay>
-                <ProjectDragOverlay />
-              </DragOverlay>
-            </DragDropProvider>
-          </div>
-          <div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
-            <Switch>
-              <Match when={!providers.paid().length && layout.sidebar.opened()}>
-                <div class="rounded-md bg-background-stronger shadow-xs-border-base">
-                  <div class="p-3 flex flex-col gap-2">
-                    <div class="text-12-medium text-text-strong">Getting started</div>
-                    <div class="text-text-base">OpenCode includes free models so you can start immediately.</div>
-                    <div class="text-text-base">Connect any provider to use models, inc. Claude, GPT, Gemini etc.</div>
-                  </div>
-                  <Tooltip placement="right" value="Connect provider" inactive={layout.sidebar.opened()}>
-                    <Button
-                      class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-lg rounded-t-none shadow-none border-t border-border-weak-base pl-2.25 pb-px"
-                      size="large"
-                      icon="plus"
-                      onClick={connectProvider}
-                    >
-                      <Show when={layout.sidebar.opened()}>Connect provider</Show>
-                    </Button>
-                  </Tooltip>
+              <SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
+                <For each={layout.projects.list()}>
+                  {(project) => <SortableProject project={project} mobile={sidebarProps.mobile} />}
+                </For>
+              </SortableProvider>
+            </div>
+            <DragOverlay>
+              <ProjectDragOverlay />
+            </DragOverlay>
+          </DragDropProvider>
+        </div>
+        <div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
+          <Switch>
+            <Match when={!providers.paid().length && expanded()}>
+              <div class="rounded-md bg-background-stronger shadow-xs-border-base">
+                <div class="p-3 flex flex-col gap-2">
+                  <div class="text-12-medium text-text-strong">Getting started</div>
+                  <div class="text-text-base">OpenCode includes free models so you can start immediately.</div>
+                  <div class="text-text-base">Connect any provider to use models, inc. Claude, GPT, Gemini etc.</div>
                 </div>
                 </div>
-              </Match>
-              <Match when={true}>
-                <Tooltip placement="right" value="Connect provider" inactive={layout.sidebar.opened()}>
+                <Tooltip placement="right" value="Connect provider" inactive={expanded()}>
                   <Button
                   <Button
-                    class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
-                    variant="ghost"
+                    class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-lg rounded-t-none shadow-none border-t border-border-weak-base pl-2.25 pb-px"
                     size="large"
                     size="large"
                     icon="plus"
                     icon="plus"
                     onClick={connectProvider}
                     onClick={connectProvider}
                   >
                   >
-                    <Show when={layout.sidebar.opened()}>Connect provider</Show>
+                    Connect provider
                   </Button>
                   </Button>
                 </Tooltip>
                 </Tooltip>
-              </Match>
-            </Switch>
-            <Show when={platform.openDirectoryPickerDialog}>
-              <Tooltip
-                placement="right"
-                value={
-                  <div class="flex items-center gap-2">
-                    <span>Open project</span>
-                    <span class="text-icon-base text-12-medium">{command.keybind("project.open")}</span>
-                  </div>
-                }
-                inactive={layout.sidebar.opened()}
-              >
+              </div>
+            </Match>
+            <Match when={true}>
+              <Tooltip placement="right" value="Connect provider" inactive={expanded()}>
                 <Button
                 <Button
                   class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
                   class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
                   variant="ghost"
                   variant="ghost"
                   size="large"
                   size="large"
-                  icon="folder-add-left"
-                  onClick={chooseProject}
+                  icon="plus"
+                  onClick={connectProvider}
                 >
                 >
-                  <Show when={layout.sidebar.opened()}>Open project</Show>
+                  <Show when={expanded()}>Connect provider</Show>
                 </Button>
                 </Button>
               </Tooltip>
               </Tooltip>
-            </Show>
-            {/* <Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}> */}
-            {/*   <Button */}
-            {/*     disabled */}
-            {/*     class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2" */}
-            {/*     variant="ghost" */}
-            {/*     size="large" */}
-            {/*     icon="settings-gear" */}
-            {/*   > */}
-            {/*     <Show when={layout.sidebar.opened()}>Settings</Show> */}
-            {/*   </Button> */}
-            {/* </Tooltip> */}
-            <Tooltip placement="right" value="Share feedback" inactive={layout.sidebar.opened()}>
+            </Match>
+          </Switch>
+          <Show when={platform.openDirectoryPickerDialog}>
+            <Tooltip
+              placement="right"
+              value={
+                <div class="flex items-center gap-2">
+                  <span>Open project</span>
+                  <Show when={!sidebarProps.mobile}>
+                    <span class="text-icon-base text-12-medium">{command.keybind("project.open")}</span>
+                  </Show>
+                </div>
+              }
+              inactive={expanded()}
+            >
               <Button
               <Button
-                as={"a"}
-                href="https://opencode.ai/desktop-feedback"
-                target="_blank"
                 class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
                 class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
                 variant="ghost"
                 variant="ghost"
                 size="large"
                 size="large"
-                icon="bubble-5"
+                icon="folder-add-left"
+                onClick={chooseProject}
               >
               >
-                <Show when={layout.sidebar.opened()}>Share feedback</Show>
+                <Show when={expanded()}>Open project</Show>
               </Button>
               </Button>
             </Tooltip>
             </Tooltip>
+          </Show>
+          <Tooltip placement="right" value="Share feedback" inactive={expanded()}>
+            <Button
+              as={"a"}
+              href="https://opencode.ai/desktop-feedback"
+              target="_blank"
+              class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
+              variant="ghost"
+              size="large"
+              icon="bubble-5"
+            >
+              <Show when={expanded()}>Share feedback</Show>
+            </Button>
+          </Tooltip>
+        </div>
+      </>
+    )
+  }
+
+  return (
+    <div class="relative flex-1 min-h-0 flex flex-col">
+      <Header
+        navigateToProject={navigateToProject}
+        navigateToSession={navigateToSession}
+        onMobileMenuToggle={mobileSidebar.toggle}
+      />
+      <div class="flex-1 min-h-0 flex">
+        <div
+          classList={{
+            "hidden xl:flex": true,
+            "relative @container w-12 pb-5 shrink-0 bg-background-base": true,
+            "flex-col gap-5.5 items-start self-stretch justify-between": true,
+            "border-r border-border-weak-base contain-strict": true,
+          }}
+          style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
+        >
+          <Show when={layout.sidebar.opened()}>
+            <ResizeHandle
+              direction="horizontal"
+              size={layout.sidebar.width()}
+              min={150}
+              max={window.innerWidth * 0.3}
+              collapseThreshold={80}
+              onResize={layout.sidebar.resize}
+              onCollapse={layout.sidebar.close}
+            />
+          </Show>
+          <SidebarContent />
+        </div>
+        <div class="xl:hidden">
+          <div
+            classList={{
+              "fixed inset-0 bg-black/50 z-40 transition-opacity duration-200": true,
+              "opacity-100 pointer-events-auto": mobileSidebar.open(),
+              "opacity-0 pointer-events-none": !mobileSidebar.open(),
+            }}
+            onClick={(e) => {
+              if (e.target === e.currentTarget) mobileSidebar.hide()
+            }}
+          />
+          <div
+            classList={{
+              "@container fixed inset-y-0 left-0 z-50 w-72 bg-background-base border-r border-border-weak-base flex flex-col gap-5.5 items-start self-stretch justify-between pt-12 pb-5 transition-transform duration-200 ease-out": true,
+              "translate-x-0": mobileSidebar.open(),
+              "-translate-x-full": !mobileSidebar.open(),
+            }}
+            onClick={(e) => e.stopPropagation()}
+          >
+            <SidebarContent mobile />
           </div>
           </div>
         </div>
         </div>
+
         <main class="size-full overflow-x-hidden flex flex-col items-start contain-strict">{props.children}</main>
         <main class="size-full overflow-x-hidden flex flex-col items-start contain-strict">{props.children}</main>
       </div>
       </div>
       <Toast.Region />
       <Toast.Region />

+ 210 - 62
packages/desktop/src/pages/session.tsx

@@ -125,6 +125,11 @@ export default function Page() {
     activeTerminalDraggable: undefined as string | undefined,
     activeTerminalDraggable: undefined as string | undefined,
     userInteracted: false,
     userInteracted: false,
     stepsExpanded: true,
     stepsExpanded: true,
+    mobileStepsExpanded: {} as Record<string, boolean>,
+    mobileLastScrollTop: 0,
+    mobileLastScrollHeight: 0,
+    mobileAutoScrolled: false,
+    mobileUserScrolled: false,
   })
   })
   let inputRef!: HTMLDivElement
   let inputRef!: HTMLDivElement
 
 
@@ -533,72 +538,215 @@ export default function Page() {
 
 
   const showTabs = createMemo(() => diffs().length > 0 || tabs().all().length > 0)
   const showTabs = createMemo(() => diffs().length > 0 || tabs().all().length > 0)
 
 
+  let mobileScrollRef: HTMLDivElement | undefined
+
+  const mobileWorking = createMemo(() => status().type !== "idle")
+
+  function handleMobileScroll() {
+    if (!mobileScrollRef || store.mobileAutoScrolled) return
+
+    const scrollTop = mobileScrollRef.scrollTop
+    const scrollHeight = mobileScrollRef.scrollHeight
+
+    const scrolledUp = scrollTop < store.mobileLastScrollTop - 50
+    if (scrolledUp && mobileWorking()) {
+      setStore("mobileUserScrolled", true)
+      setStore("userInteracted", true)
+    }
+
+    batch(() => {
+      setStore("mobileLastScrollTop", scrollTop)
+      setStore("mobileLastScrollHeight", scrollHeight)
+    })
+  }
+
+  function handleMobileInteraction() {
+    if (mobileWorking()) {
+      setStore("mobileUserScrolled", true)
+      setStore("userInteracted", true)
+    }
+  }
+
+  function scrollMobileToBottom() {
+    if (!mobileScrollRef || store.mobileUserScrolled || !mobileWorking()) return
+    setStore("mobileAutoScrolled", true)
+    requestAnimationFrame(() => {
+      mobileScrollRef?.scrollTo({ top: mobileScrollRef.scrollHeight, behavior: "smooth" })
+      requestAnimationFrame(() => {
+        batch(() => {
+          setStore("mobileLastScrollTop", mobileScrollRef?.scrollTop ?? 0)
+          setStore("mobileLastScrollHeight", mobileScrollRef?.scrollHeight ?? 0)
+          setStore("mobileAutoScrolled", false)
+        })
+      })
+    })
+  }
+
+  // Reset mobile user scrolled when work completes
+  createEffect(() => {
+    if (!mobileWorking()) setStore("mobileUserScrolled", false)
+  })
+
+  // Auto-scroll when content changes
+  createEffect(() => {
+    // Track changes to messages/parts to trigger scroll
+    const msgs = visibleUserMessages()
+    const lastMsg = msgs.at(-1)
+    if (lastMsg && mobileWorking()) {
+      sync.data.part[lastMsg.id]
+      scrollMobileToBottom()
+    }
+  })
+
+  const MobileTurns = () => (
+    <div
+      ref={mobileScrollRef}
+      onScroll={handleMobileScroll}
+      onClick={handleMobileInteraction}
+      class="relative mt-2 min-w-0 w-full h-full overflow-y-auto no-scrollbar pb-12"
+    >
+      <div class="flex flex-col gap-45 items-start justify-start mt-4">
+        <For each={visibleUserMessages()}>
+          {(message) => (
+            <SessionTurn
+              sessionID={params.id!}
+              messageID={message.id}
+              stepsExpanded={store.mobileStepsExpanded[message.id] ?? false}
+              onStepsExpandedToggle={() => setStore("mobileStepsExpanded", message.id, (x) => !x)}
+              onUserInteracted={() => setStore("userInteracted", true)}
+              classes={{
+                root: "min-w-0 w-full relative",
+                content:
+                  "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
+                container: "px-4",
+              }}
+            />
+          )}
+        </For>
+      </div>
+    </div>
+  )
+
+  const NewSessionView = () => (
+    <div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6">
+      <div class="text-20-medium text-text-weaker">New session</div>
+      <div class="flex justify-center items-center gap-3">
+        <Icon name="folder" size="small" />
+        <div class="text-12-medium text-text-weak">
+          {getDirectory(sync.data.path.directory)}
+          <span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
+        </div>
+      </div>
+      <Show when={sync.project}>
+        {(project) => (
+          <div class="flex justify-center items-center gap-3">
+            <Icon name="pencil-line" size="small" />
+            <div class="text-12-medium text-text-weak">
+              Last modified&nbsp;
+              <span class="text-text-strong">
+                {DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
+              </span>
+            </div>
+          </div>
+        )}
+      </Show>
+    </div>
+  )
+
+  const DesktopSessionContent = () => (
+    <Switch>
+      <Match when={params.id}>
+        <div class="flex items-start justify-start h-full min-h-0">
+          <SessionMessageRail
+            messages={visibleUserMessages()}
+            current={activeMessage()}
+            onMessageSelect={setActiveMessage}
+            wide={!showTabs()}
+          />
+          <Show when={activeMessage()}>
+            <SessionTurn
+              sessionID={params.id!}
+              messageID={activeMessage()!.id}
+              stepsExpanded={store.stepsExpanded}
+              onStepsExpandedToggle={() => setStore("stepsExpanded", (x) => !x)}
+              onUserInteracted={() => setStore("userInteracted", true)}
+              classes={{
+                root: "pb-20 flex-1 min-w-0",
+                content: "pb-20",
+                container:
+                  "w-full " +
+                  (!showTabs() ? "max-w-200 mx-auto px-6" : visibleUserMessages().length > 1 ? "pr-6 pl-18" : "px-6"),
+              }}
+            />
+          </Show>
+        </div>
+      </Match>
+      <Match when={true}>
+        <NewSessionView />
+      </Match>
+    </Switch>
+  )
+
   return (
   return (
     <div class="relative bg-background-base size-full overflow-hidden flex flex-col">
     <div class="relative bg-background-base size-full overflow-hidden flex flex-col">
-      <div class="min-h-0 grow w-full flex">
-        {/* Session pane - always visible */}
+      <div class="md:hidden flex-1 min-h-0 flex flex-col bg-background-stronger">
+        <Switch>
+          <Match when={!params.id}>
+            <div class="flex-1 min-h-0 overflow-hidden">
+              <NewSessionView />
+            </div>
+          </Match>
+          <Match when={diffs().length > 0}>
+            <Tabs class="flex-1 min-h-0 flex flex-col pb-28">
+              <Tabs.List>
+                <Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
+                  Session
+                </Tabs.Trigger>
+                <Tabs.Trigger value="review" class="w-1/2 !border-r-0" classes={{ button: "w-full" }}>
+                  {diffs().length} Files Changed
+                </Tabs.Trigger>
+              </Tabs.List>
+              <Tabs.Content value="session" class="flex-1 !overflow-hidden">
+                <MobileTurns />
+              </Tabs.Content>
+              <Tabs.Content forceMount value="review" class="flex-1 !overflow-hidden hidden data-[selected]:block">
+                <div class="relative h-full mt-6 overflow-y-auto no-scrollbar">
+                  <SessionReview
+                    diffs={diffs()}
+                    classes={{
+                      root: "pb-32",
+                      header: "px-4",
+                      container: "px-4",
+                    }}
+                  />
+                </div>
+              </Tabs.Content>
+            </Tabs>
+          </Match>
+          <Match when={true}>
+            <div class="flex-1 min-h-0 overflow-hidden">
+              <MobileTurns />
+            </div>
+          </Match>
+        </Switch>
+        <div class="absolute inset-x-0 bottom-4 flex flex-col justify-center items-center z-50 px-4">
+          <div class="w-full">
+            <PromptInput
+              ref={(el) => {
+                inputRef = el
+              }}
+            />
+          </div>
+        </div>
+      </div>
+
+      <div class="hidden md:flex min-h-0 grow w-full">
         <div
         <div
           class="@container relative shrink-0 py-3 flex flex-col gap-6 min-h-0 h-full bg-background-stronger"
           class="@container relative shrink-0 py-3 flex flex-col gap-6 min-h-0 h-full bg-background-stronger"
           style={{ width: showTabs() ? `${layout.session.width()}px` : "100%" }}
           style={{ width: showTabs() ? `${layout.session.width()}px` : "100%" }}
         >
         >
           <div class="flex-1 min-h-0 overflow-hidden">
           <div class="flex-1 min-h-0 overflow-hidden">
-            <Switch>
-              <Match when={params.id}>
-                <div class="flex items-start justify-start h-full min-h-0">
-                  <SessionMessageRail
-                    messages={visibleUserMessages()}
-                    current={activeMessage()}
-                    onMessageSelect={setActiveMessage}
-                    wide={!showTabs()}
-                  />
-                  <Show when={activeMessage()}>
-                    <SessionTurn
-                      sessionID={params.id!}
-                      messageID={activeMessage()!.id}
-                      stepsExpanded={store.stepsExpanded}
-                      onStepsExpandedToggle={() => setStore("stepsExpanded", (x) => !x)}
-                      onUserInteracted={() => setStore("userInteracted", true)}
-                      classes={{
-                        root: "pb-20 flex-1 min-w-0",
-                        content: "pb-20",
-                        container:
-                          "w-full " +
-                          (!showTabs()
-                            ? "max-w-200 mx-auto px-6"
-                            : visibleUserMessages().length > 1
-                              ? "pr-6 pl-18"
-                              : "px-6"),
-                      }}
-                    />
-                  </Show>
-                </div>
-              </Match>
-              <Match when={true}>
-                <div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6">
-                  <div class="text-20-medium text-text-weaker">New session</div>
-                  <div class="flex justify-center items-center gap-3">
-                    <Icon name="folder" size="small" />
-                    <div class="text-12-medium text-text-weak">
-                      {getDirectory(sync.data.path.directory)}
-                      <span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
-                    </div>
-                  </div>
-                  <Show when={sync.project}>
-                    {(project) => (
-                      <div class="flex justify-center items-center gap-3">
-                        <Icon name="pencil-line" size="small" />
-                        <div class="text-12-medium text-text-weak">
-                          Last modified&nbsp;
-                          <span class="text-text-strong">
-                            {DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
-                          </span>
-                        </div>
-                      </div>
-                    )}
-                  </Show>
-                </div>
-              </Match>
-            </Switch>
+            <DesktopSessionContent />
           </div>
           </div>
           <div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
           <div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
             <div
             <div
@@ -625,7 +773,6 @@ export default function Page() {
           </Show>
           </Show>
         </div>
         </div>
 
 
-        {/* Tabs pane - visible when there are diffs or file tabs */}
         <Show when={showTabs()}>
         <Show when={showTabs()}>
           <div class="relative flex-1 min-w-0 h-full border-l border-border-weak-base">
           <div class="relative flex-1 min-w-0 h-full border-l border-border-weak-base">
             <DragDropProvider
             <DragDropProvider
@@ -683,7 +830,7 @@ export default function Page() {
                 </div>
                 </div>
                 <Show when={diffs().length}>
                 <Show when={diffs().length}>
                   <Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden contain-strict">
                   <Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden contain-strict">
-                    <div class="relative pt-3 flex-1 min-h-0 overflow-hidden">
+                    <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
                       <SessionReview
                       <SessionReview
                         classes={{
                         classes={{
                           root: "pb-40",
                           root: "pb-40",
@@ -754,9 +901,10 @@ export default function Page() {
           </div>
           </div>
         </Show>
         </Show>
       </div>
       </div>
+
       <Show when={layout.terminal.opened()}>
       <Show when={layout.terminal.opened()}>
         <div
         <div
-          class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base"
+          class="hidden md:flex relative w-full flex-col shrink-0 border-t border-border-weak-base"
           style={{ height: `${layout.terminal.height()}px` }}
           style={{ height: `${layout.terminal.height()}px` }}
         >
         >
           <ResizeHandle
           <ResizeHandle

+ 1 - 0
packages/ui/src/components/icon.tsx

@@ -54,6 +54,7 @@ const icons = {
   photo: `<path d="M16.6665 16.6666L11.6665 11.6666L9.99984 13.3333L6.6665 9.99996L3.08317 13.5833M2.9165 2.91663H17.0832V17.0833H2.9165V2.91663ZM13.3332 7.49996C13.3332 8.30537 12.6803 8.95829 11.8748 8.95829C11.0694 8.95829 10.4165 8.30537 10.4165 7.49996C10.4165 6.69454 11.0694 6.04163 11.8748 6.04163C12.6803 6.04163 13.3332 6.69454 13.3332 7.49996Z" stroke="currentColor" stroke-linecap="square"/>`,
   photo: `<path d="M16.6665 16.6666L11.6665 11.6666L9.99984 13.3333L6.6665 9.99996L3.08317 13.5833M2.9165 2.91663H17.0832V17.0833H2.9165V2.91663ZM13.3332 7.49996C13.3332 8.30537 12.6803 8.95829 11.8748 8.95829C11.0694 8.95829 10.4165 8.30537 10.4165 7.49996C10.4165 6.69454 11.0694 6.04163 11.8748 6.04163C12.6803 6.04163 13.3332 6.69454 13.3332 7.49996Z" stroke="currentColor" stroke-linecap="square"/>`,
   share: `<path d="M10.0013 12.0846L10.0013 3.33464M13.7513 6.66797L10.0013 2.91797L6.2513 6.66797M17.0846 10.418V17.0846H2.91797V10.418" stroke="currentColor" stroke-linecap="square"/>`,
   share: `<path d="M10.0013 12.0846L10.0013 3.33464M13.7513 6.66797L10.0013 2.91797L6.2513 6.66797M17.0846 10.418V17.0846H2.91797V10.418" stroke="currentColor" stroke-linecap="square"/>`,
   download: `<path d="M13.9583 10.6257L10 14.584L6.04167 10.6257M10 2.08398V13.959M16.25 17.9173H3.75" stroke="currentColor" stroke-linecap="square"/>`,
   download: `<path d="M13.9583 10.6257L10 14.584L6.04167 10.6257M10 2.08398V13.959M16.25 17.9173H3.75" stroke="currentColor" stroke-linecap="square"/>`,
+  menu: `<path d="M2.5 5H17.5M2.5 10H17.5M2.5 15H17.5" stroke="currentColor" stroke-linecap="square"/>`,
 }
 }
 
 
 export interface IconProps extends ComponentProps<"svg"> {
 export interface IconProps extends ComponentProps<"svg"> {

+ 7 - 6
packages/ui/src/components/message-part.tsx

@@ -5,9 +5,11 @@ import {
   FilePart,
   FilePart,
   Message as MessageType,
   Message as MessageType,
   Part as PartType,
   Part as PartType,
+  ReasoningPart,
   TextPart,
   TextPart,
   ToolPart,
   ToolPart,
   UserMessage,
   UserMessage,
+  Todo,
 } from "@opencode-ai/sdk/v2"
 } from "@opencode-ai/sdk/v2"
 import { useData } from "../context"
 import { useData } from "../context"
 import { useDiffComponent } from "../context/diff"
 import { useDiffComponent } from "../context/diff"
@@ -111,7 +113,7 @@ export type ToolInfo = {
   subtitle?: string
   subtitle?: string
 }
 }
 
 
-export function getToolInfo(tool: string, input: Record<string, any> = {}): ToolInfo {
+export function getToolInfo(tool: string, input: any = {}): ToolInfo {
   switch (tool) {
   switch (tool) {
     case "read":
     case "read":
       return {
       return {
@@ -186,8 +188,7 @@ export function getToolInfo(tool: string, input: Record<string, any> = {}): Tool
 }
 }
 
 
 function getToolPartInfo(part: ToolPart): ToolInfo {
 function getToolPartInfo(part: ToolPart): ToolInfo {
-  const state = part.state as any
-  const input = state.input || {}
+  const input = part.state.input || {}
   return getToolInfo(part.tool, input)
   return getToolInfo(part.tool, input)
 }
 }
 
 
@@ -424,7 +425,7 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
 }
 }
 
 
 PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) {
 PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) {
-  const part = props.part as any
+  const part = props.part as ReasoningPart
   return (
   return (
     <Show when={part.text.trim()}>
     <Show when={part.text.trim()}>
       <div data-component="reasoning-part">
       <div data-component="reasoning-part">
@@ -722,14 +723,14 @@ ToolRegistry.register({
         trigger={{
         trigger={{
           title: "To-dos",
           title: "To-dos",
           subtitle: props.input.todos
           subtitle: props.input.todos
-            ? `${props.input.todos.filter((t: any) => t.status === "completed").length}/${props.input.todos.length}`
+            ? `${props.input.todos.filter((t: Todo) => t.status === "completed").length}/${props.input.todos.length}`
             : "",
             : "",
         }}
         }}
       >
       >
         <Show when={props.input.todos?.length}>
         <Show when={props.input.todos?.length}>
           <div data-component="todos">
           <div data-component="todos">
             <For each={props.input.todos}>
             <For each={props.input.todos}>
-              {(todo: any) => (
+              {(todo: Todo) => (
                 <Checkbox readOnly checked={todo.status === "completed"}>
                 <Checkbox readOnly checked={todo.status === "completed"}>
                   <div data-slot="message-part-todo-content" data-completed={todo.status === "completed"}>
                   <div data-slot="message-part-todo-content" data-completed={todo.status === "completed"}>
                     {todo.content}
                     {todo.content}