Browse Source

chore: cleanup (#14113)

Adam 1 month ago
parent
commit
00c238777a

+ 47 - 19
packages/app/src/components/session/session-context-tab.tsx

@@ -5,6 +5,7 @@ import { useSync } from "@/context/sync"
 import { useLayout } from "@/context/layout"
 import { checksum } from "@opencode-ai/util/encode"
 import { findLast } from "@opencode-ai/util/array"
+import { same } from "@/utils/same"
 import { Icon } from "@opencode-ai/ui/icon"
 import { Accordion } from "@opencode-ai/ui/accordion"
 import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
@@ -16,13 +17,6 @@ import { getSessionContextMetrics } from "./session-context-metrics"
 import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown"
 import { createSessionContextFormatter } from "./session-context-format"
 
-interface SessionContextTabProps {
-  messages: () => Message[]
-  visibleUserMessages: () => UserMessage[]
-  view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
-  info: () => ReturnType<ReturnType<typeof useSync>["session"]["get"]>
-}
-
 const BREAKDOWN_COLOR: Record<SessionContextBreakdownKey, string> = {
   system: "var(--syntax-info)",
   user: "var(--syntax-success)",
@@ -91,11 +85,45 @@ function RawMessage(props: {
   )
 }
 
-export function SessionContextTab(props: SessionContextTabProps) {
+const emptyMessages: Message[] = []
+const emptyUserMessages: UserMessage[] = []
+
+export function SessionContextTab() {
   const params = useParams()
   const sync = useSync()
+  const layout = useLayout()
   const language = useLanguage()
 
+  const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
+  const view = createMemo(() => layout.view(sessionKey))
+  const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
+
+  const messages = createMemo(
+    () => {
+      const id = params.id
+      if (!id) return emptyMessages
+      return (sync.data.message[id] ?? []) as Message[]
+    },
+    emptyMessages,
+    { equals: same },
+  )
+
+  const userMessages = createMemo(
+    () => messages().filter((m) => m.role === "user") as UserMessage[],
+    emptyUserMessages,
+    { equals: same },
+  )
+
+  const visibleUserMessages = createMemo(
+    () => {
+      const revert = info()?.revert?.messageID
+      if (!revert) return userMessages()
+      return userMessages().filter((m) => m.id < revert)
+    },
+    emptyUserMessages,
+    { equals: same },
+  )
+
   const usd = createMemo(
     () =>
       new Intl.NumberFormat(language.locale(), {
@@ -104,7 +132,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
       }),
   )
 
-  const metrics = createMemo(() => getSessionContextMetrics(props.messages(), sync.data.provider.all))
+  const metrics = createMemo(() => getSessionContextMetrics(messages(), sync.data.provider.all))
   const ctx = createMemo(() => metrics().context)
   const formatter = createMemo(() => createSessionContextFormatter(language.locale()))
 
@@ -113,7 +141,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
   })
 
   const counts = createMemo(() => {
-    const all = props.messages()
+    const all = messages()
     const user = all.reduce((count, x) => count + (x.role === "user" ? 1 : 0), 0)
     const assistant = all.reduce((count, x) => count + (x.role === "assistant" ? 1 : 0), 0)
     return {
@@ -124,7 +152,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
   })
 
   const systemPrompt = createMemo(() => {
-    const msg = findLast(props.visibleUserMessages(), (m) => !!m.system)
+    const msg = findLast(visibleUserMessages(), (m) => !!m.system)
     const system = msg?.system
     if (!system) return
     const trimmed = system.trim()
@@ -146,12 +174,12 @@ export function SessionContextTab(props: SessionContextTabProps) {
 
   const breakdown = createMemo(
     on(
-      () => [ctx()?.message.id, ctx()?.input, props.messages().length, systemPrompt()],
+      () => [ctx()?.message.id, ctx()?.input, messages().length, systemPrompt()],
       () => {
         const c = ctx()
         if (!c?.input) return []
         return estimateSessionContextBreakdown({
-          messages: props.messages(),
+          messages: messages(),
           parts: sync.data.part as Record<string, Part[] | undefined>,
           input: c.input,
           systemPrompt: systemPrompt(),
@@ -169,7 +197,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
   }
 
   const stats = [
-    { label: "context.stats.session", value: () => props.info()?.title ?? params.id ?? "—" },
+    { label: "context.stats.session", value: () => info()?.title ?? params.id ?? "—" },
     { label: "context.stats.messages", value: () => counts().all.toLocaleString(language.locale()) },
     { label: "context.stats.provider", value: providerLabel },
     { label: "context.stats.model", value: modelLabel },
@@ -186,7 +214,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
     { label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.locale()) },
     { label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.locale()) },
     { label: "context.stats.totalCost", value: cost },
-    { label: "context.stats.sessionCreated", value: () => formatter().time(props.info()?.time.created) },
+    { label: "context.stats.sessionCreated", value: () => formatter().time(info()?.time.created) },
     { label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) },
   ] satisfies { label: string; value: () => JSX.Element }[]
 
@@ -199,7 +227,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
     const el = scroll
     if (!el) return
 
-    const s = props.view()?.scroll("context")
+    const s = view().scroll("context")
     if (!s) return
 
     if (el.scrollTop !== s.y) el.scrollTop = s.y
@@ -220,13 +248,13 @@ export function SessionContextTab(props: SessionContextTabProps) {
       pending = undefined
       if (!next) return
 
-      props.view().setScroll("context", next)
+      view().setScroll("context", next)
     })
   }
 
   createEffect(
     on(
-      () => props.messages().length,
+      () => messages().length,
       () => {
         requestAnimationFrame(restoreScroll)
       },
@@ -300,7 +328,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
         <div class="flex flex-col gap-2">
           <div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div>
           <Accordion multiple>
-            <For each={props.messages()}>
+            <For each={messages()}>
               {(message) => (
                 <RawMessage message={message} getParts={getParts} onRendered={restoreScroll} time={formatter().time} />
               )}

+ 13 - 509
packages/app/src/pages/session.tsx

@@ -1,26 +1,20 @@
 import { For, onCleanup, Show, Match, Switch, createMemo, createEffect, on } from "solid-js"
 import { createMediaQuery } from "@solid-primitives/media"
 import { createResizeObserver } from "@solid-primitives/resize-observer"
-import { Dynamic } from "solid-js/web"
 import { useLocal } from "@/context/local"
 import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
 import { createStore, produce } from "solid-js/store"
 import { IconButton } from "@opencode-ai/ui/icon-button"
 import { Button } from "@opencode-ai/ui/button"
 import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
-import { Dialog } from "@opencode-ai/ui/dialog"
 import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
 import { Tabs } from "@opencode-ai/ui/tabs"
 import { Select } from "@opencode-ai/ui/select"
-import { useCodeComponent } from "@opencode-ai/ui/context/code"
 import { createAutoScroll } from "@opencode-ai/ui/hooks"
 import { Mark } from "@opencode-ai/ui/logo"
 
-import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
-import type { DragEvent } from "@thisbeyond/solid-dnd"
 import { useSync } from "@/context/sync"
-import { useGlobalSync } from "@/context/global-sync"
-import { useTerminal, type LocalPTY } from "@/context/terminal"
+import { useTerminal } from "@/context/terminal"
 import { useLayout } from "@/context/layout"
 import { checksum, base64Encode } from "@opencode-ai/util/encode"
 import { findLast } from "@opencode-ai/util/array"
@@ -34,16 +28,14 @@ import { UserMessage } from "@opencode-ai/sdk/v2"
 import { useSDK } from "@/context/sdk"
 import { usePrompt } from "@/context/prompt"
 import { useComments } from "@/context/comments"
-import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
 import { usePermission } from "@/context/permission"
 import { showToast } from "@opencode-ai/ui/toast"
-import { SessionHeader, SessionContextTab, SortableTab, FileVisual, NewSessionView } from "@/components/session"
+import { SessionHeader, NewSessionView } from "@/components/session"
 import { navMark, navParams } from "@/utils/perf"
 import { same } from "@/utils/same"
-import { createOpenReviewFile, focusTerminalById, getTabReorderIndex } from "@/pages/session/helpers"
+import { createOpenReviewFile } from "@/pages/session/helpers"
 import { createScrollSpy } from "@/pages/session/scroll-spy"
 import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
-import { FileTabContent } from "@/pages/session/file-tabs"
 import {
   SessionReviewTab,
   StickyAddButton,
@@ -51,7 +43,6 @@ import {
   type SessionReviewTabProps,
 } from "@/pages/session/review-tab"
 import { TerminalPanel } from "@/pages/session/terminal-panel"
-import { terminalTabLabel } from "@/pages/session/terminal-label"
 import { MessageTimeline } from "@/pages/session/message-timeline"
 import { useSessionCommands } from "@/pages/session/use-session-commands"
 import { SessionPromptDock } from "@/pages/session/session-prompt-dock"
@@ -59,42 +50,13 @@ import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs"
 import { SessionSidePanel } from "@/pages/session/session-side-panel"
 import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
 
-type HandoffSession = {
-  prompt: string
-  files: Record<string, SelectedLineRange | null>
-}
-
-const HANDOFF_MAX = 40
-
-const handoff = {
-  session: new Map<string, HandoffSession>(),
-  terminal: new Map<string, string[]>(),
-}
-
-const touch = <K, V>(map: Map<K, V>, key: K, value: V) => {
-  map.delete(key)
-  map.set(key, value)
-  while (map.size > HANDOFF_MAX) {
-    const first = map.keys().next().value
-    if (first === undefined) return
-    map.delete(first)
-  }
-}
-
-const setSessionHandoff = (key: string, patch: Partial<HandoffSession>) => {
-  const prev = handoff.session.get(key) ?? { prompt: "", files: {} }
-  touch(handoff.session, key, { ...prev, ...patch })
-}
-
 export default function Page() {
   const layout = useLayout()
   const local = useLocal()
   const file = useFile()
   const sync = useSync()
-  const globalSync = useGlobalSync()
   const terminal = useTerminal()
   const dialog = useDialog()
-  const codeComponent = useCodeComponent()
   const command = useCommand()
   const language = useLanguage()
   const params = useParams()
@@ -104,53 +66,21 @@ export default function Page() {
   const comments = useComments()
   const permission = usePermission()
 
-  const permRequest = createMemo(() => {
-    const sessionID = params.id
-    if (!sessionID) return
-    return sync.data.permission[sessionID]?.[0]
-  })
-
-  const questionRequest = createMemo(() => {
-    const sessionID = params.id
-    if (!sessionID) return
-    return sync.data.question[sessionID]?.[0]
-  })
-
-  const blocked = createMemo(() => !!permRequest() || !!questionRequest())
-
   const [ui, setUi] = createStore({
-    responding: false,
     pendingMessage: undefined as string | undefined,
     scrollGesture: 0,
-    autoCreated: false,
     scroll: {
       overflow: false,
       bottom: true,
     },
   })
 
-  createEffect(
-    on(
-      () => permRequest()?.id,
-      () => setUi("responding", false),
-      { defer: true },
-    ),
-  )
+  const blocked = createMemo(() => {
+    const sessionID = params.id
+    if (!sessionID) return false
+    return !!sync.data.permission[sessionID]?.[0] || !!sync.data.question[sessionID]?.[0]
+  })
 
-  const decide = (response: "once" | "always" | "reject") => {
-    const perm = permRequest()
-    if (!perm) return
-    if (ui.responding) return
-
-    setUi("responding", true)
-    sdk.client.permission
-      .respond({ sessionID: perm.sessionID, permissionID: perm.id, response })
-      .catch((err: unknown) => {
-        const message = err instanceof Error ? err.message : String(err)
-        showToast({ title: language.t("common.requestFailed"), description: message })
-      })
-      .finally(() => setUi("responding", false))
-  }
   const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
   const workspaceKey = createMemo(() => params.dir ?? "")
   const workspaceTabs = createMemo(() => layout.tabs(workspaceKey))
@@ -323,206 +253,6 @@ export default function Page() {
     return sync.session.history.loading(id)
   })
 
-  const [title, setTitle] = createStore({
-    draft: "",
-    editing: false,
-    saving: false,
-    menuOpen: false,
-    pendingRename: false,
-  })
-  let titleRef: HTMLInputElement | undefined
-
-  const errorMessage = (err: unknown) => {
-    if (err && typeof err === "object" && "data" in err) {
-      const data = (err as { data?: { message?: string } }).data
-      if (data?.message) return data.message
-    }
-    if (err instanceof Error) return err.message
-    return language.t("common.requestFailed")
-  }
-
-  createEffect(
-    on(
-      sessionKey,
-      () => setTitle({ draft: "", editing: false, saving: false, menuOpen: false, pendingRename: false }),
-      { defer: true },
-    ),
-  )
-
-  const openTitleEditor = () => {
-    if (!params.id) return
-    setTitle({ editing: true, draft: info()?.title ?? "" })
-    requestAnimationFrame(() => {
-      titleRef?.focus()
-      titleRef?.select()
-    })
-  }
-
-  const closeTitleEditor = () => {
-    if (title.saving) return
-    setTitle({ editing: false, saving: false })
-  }
-
-  const saveTitleEditor = async () => {
-    const sessionID = params.id
-    if (!sessionID) return
-    if (title.saving) return
-
-    const next = title.draft.trim()
-    if (!next || next === (info()?.title ?? "")) {
-      setTitle({ editing: false, saving: false })
-      return
-    }
-
-    setTitle("saving", true)
-    await sdk.client.session
-      .update({ sessionID, title: next })
-      .then(() => {
-        sync.set(
-          produce((draft) => {
-            const index = draft.session.findIndex((s) => s.id === sessionID)
-            if (index !== -1) draft.session[index].title = next
-          }),
-        )
-        setTitle({ editing: false, saving: false })
-      })
-      .catch((err) => {
-        setTitle("saving", false)
-        showToast({
-          title: language.t("common.requestFailed"),
-          description: errorMessage(err),
-        })
-      })
-  }
-
-  const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => {
-    if (params.id !== sessionID) return
-    if (parentID) {
-      navigate(`/${params.dir}/session/${parentID}`)
-      return
-    }
-    if (nextSessionID) {
-      navigate(`/${params.dir}/session/${nextSessionID}`)
-      return
-    }
-    navigate(`/${params.dir}/session`)
-  }
-
-  async function archiveSession(sessionID: string) {
-    const session = sync.session.get(sessionID)
-    if (!session) return
-
-    const sessions = sync.data.session ?? []
-    const index = sessions.findIndex((s) => s.id === sessionID)
-    const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
-
-    await sdk.client.session
-      .update({ sessionID, time: { archived: Date.now() } })
-      .then(() => {
-        sync.set(
-          produce((draft) => {
-            const index = draft.session.findIndex((s) => s.id === sessionID)
-            if (index !== -1) draft.session.splice(index, 1)
-          }),
-        )
-        navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
-      })
-      .catch((err) => {
-        showToast({
-          title: language.t("common.requestFailed"),
-          description: errorMessage(err),
-        })
-      })
-  }
-
-  async function deleteSession(sessionID: string) {
-    const session = sync.session.get(sessionID)
-    if (!session) return false
-
-    const sessions = (sync.data.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
-    const index = sessions.findIndex((s) => s.id === sessionID)
-    const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
-
-    const result = await sdk.client.session
-      .delete({ sessionID })
-      .then((x) => x.data)
-      .catch((err) => {
-        showToast({
-          title: language.t("session.delete.failed.title"),
-          description: errorMessage(err),
-        })
-        return false
-      })
-
-    if (!result) return false
-
-    sync.set(
-      produce((draft) => {
-        const removed = new Set<string>([sessionID])
-
-        const byParent = new Map<string, string[]>()
-        for (const item of draft.session) {
-          const parentID = item.parentID
-          if (!parentID) continue
-          const existing = byParent.get(parentID)
-          if (existing) {
-            existing.push(item.id)
-            continue
-          }
-          byParent.set(parentID, [item.id])
-        }
-
-        const stack = [sessionID]
-        while (stack.length) {
-          const parentID = stack.pop()
-          if (!parentID) continue
-
-          const children = byParent.get(parentID)
-          if (!children) continue
-
-          for (const child of children) {
-            if (removed.has(child)) continue
-            removed.add(child)
-            stack.push(child)
-          }
-        }
-
-        draft.session = draft.session.filter((s) => !removed.has(s.id))
-      }),
-    )
-
-    navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
-    return true
-  }
-
-  function DialogDeleteSession(props: { sessionID: string }) {
-    const title = createMemo(() => sync.session.get(props.sessionID)?.title ?? language.t("command.session.new"))
-    const handleDelete = async () => {
-      await deleteSession(props.sessionID)
-      dialog.close()
-    }
-
-    return (
-      <Dialog title={language.t("session.delete.title")} fit>
-        <div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
-          <div class="flex flex-col gap-1">
-            <span class="text-14-regular text-text-strong">
-              {language.t("session.delete.confirm", { name: title() })}
-            </span>
-          </div>
-          <div class="flex justify-end gap-2">
-            <Button variant="ghost" size="large" onClick={() => dialog.close()}>
-              {language.t("common.cancel")}
-            </Button>
-            <Button variant="primary" size="large" onClick={handleDelete}>
-              {language.t("session.delete.button")}
-            </Button>
-          </div>
-        </div>
-      </Dialog>
-    )
-  }
-
   const emptyUserMessages: UserMessage[] = []
   const userMessages = createMemo(
     () => messages().filter((m) => m.role === "user") as UserMessage[],
@@ -555,8 +285,6 @@ export default function Page() {
   )
 
   const [store, setStore] = createStore({
-    activeDraggable: undefined as string | undefined,
-    activeTerminalDraggable: undefined as string | undefined,
     messageId: undefined as string | undefined,
     turnStart: 0,
     mobileTab: "session" as "session" | "changes",
@@ -679,43 +407,6 @@ export default function Page() {
     void sync.session.todo(id)
   })
 
-  createEffect(() => {
-    if (!view().terminal.opened()) {
-      setUi("autoCreated", false)
-      return
-    }
-    if (!terminal.ready() || terminal.all().length !== 0 || ui.autoCreated) return
-    terminal.new()
-    setUi("autoCreated", true)
-  })
-
-  createEffect(
-    on(
-      () => terminal.all().length,
-      (count, prevCount) => {
-        if (prevCount !== undefined && prevCount > 0 && count === 0) {
-          if (view().terminal.opened()) {
-            view().terminal.toggle()
-          }
-        }
-      },
-    ),
-  )
-
-  createEffect(
-    on(
-      () => terminal.active(),
-      (activeId) => {
-        if (!activeId || !view().terminal.opened()) return
-        // Immediately remove focus
-        if (document.activeElement instanceof HTMLElement) {
-          document.activeElement.blur()
-        }
-        focusTerminalById(activeId)
-      },
-    ),
-  )
-
   createEffect(
     on(
       () => visibleUserMessages().at(-1)?.id,
@@ -729,11 +420,6 @@ export default function Page() {
   )
 
   const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? idle)
-  const todos = createMemo(() => {
-    const id = params.id
-    if (!id) return []
-    return globalSync.data.session_todo[id] ?? []
-  })
 
   createEffect(
     on(
@@ -741,7 +427,6 @@ export default function Page() {
       () => {
         setStore("messageId", undefined)
         setStore("changes", "session")
-        setUi("autoCreated", false)
       },
       { defer: true },
     ),
@@ -827,53 +512,6 @@ export default function Page() {
     }
   }
 
-  const handleDragStart = (event: unknown) => {
-    const id = getDraggableId(event)
-    if (!id) return
-    setStore("activeDraggable", id)
-  }
-
-  const handleDragOver = (event: DragEvent) => {
-    const { draggable, droppable } = event
-    if (draggable && droppable) {
-      const currentTabs = tabs().all()
-      const toIndex = getTabReorderIndex(currentTabs, draggable.id.toString(), droppable.id.toString())
-      if (toIndex === undefined) return
-      tabs().move(draggable.id.toString(), toIndex)
-    }
-  }
-
-  const handleDragEnd = () => {
-    setStore("activeDraggable", undefined)
-  }
-
-  const handleTerminalDragStart = (event: unknown) => {
-    const id = getDraggableId(event)
-    if (!id) return
-    setStore("activeTerminalDraggable", id)
-  }
-
-  const handleTerminalDragOver = (event: DragEvent) => {
-    const { draggable, droppable } = event
-    if (draggable && droppable) {
-      const terminals = terminal.all()
-      const fromIndex = terminals.findIndex((t: LocalPTY) => t.id === draggable.id.toString())
-      const toIndex = terminals.findIndex((t: LocalPTY) => t.id === droppable.id.toString())
-      if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
-        terminal.move(draggable.id.toString(), toIndex)
-      }
-    }
-  }
-
-  const handleTerminalDragEnd = () => {
-    setStore("activeTerminalDraggable", undefined)
-    const activeId = terminal.active()
-    if (!activeId) return
-    setTimeout(() => {
-      focusTerminalById(activeId)
-    }, 0)
-  }
-
   const contextOpen = createMemo(() => tabs().active() === "context" || tabs().all().includes("context"))
   const openedTabs = createMemo(() =>
     tabs()
@@ -1485,58 +1123,6 @@ export default function Page() {
     document.addEventListener("keydown", handleKeyDown)
   })
 
-  const previewPrompt = () =>
-    prompt
-      .current()
-      .map((part) => {
-        if (part.type === "file") return `[file:${part.path}]`
-        if (part.type === "agent") return `@${part.name}`
-        if (part.type === "image") return `[image:${part.filename}]`
-        return part.content
-      })
-      .join("")
-      .trim()
-
-  createEffect(() => {
-    if (!prompt.ready()) return
-    setSessionHandoff(sessionKey(), { prompt: previewPrompt() })
-  })
-
-  createEffect(() => {
-    if (!terminal.ready()) return
-    language.locale()
-
-    touch(
-      handoff.terminal,
-      params.dir!,
-      terminal.all().map((pty) =>
-        terminalTabLabel({
-          title: pty.title,
-          titleNumber: pty.titleNumber,
-          t: language.t as (key: string, vars?: Record<string, string | number | boolean>) => string,
-        }),
-      ),
-    )
-  })
-
-  createEffect(() => {
-    if (!file.ready()) return
-    setSessionHandoff(sessionKey(), {
-      files: tabs()
-        .all()
-        .reduce<Record<string, SelectedLineRange | null>>((acc, tab) => {
-          const path = file.pathFromTab(tab)
-          if (!path) return acc
-          const selected = file.selectedLines(path)
-          acc[path] =
-            selected && typeof selected === "object" && "start" in selected && "end" in selected
-              ? (selected as SelectedLineRange)
-              : null
-          return acc
-        }, {}),
-    })
-  })
-
   onCleanup(() => {
     cancelTurnBackfill()
     document.removeEventListener("keydown", handleKeyDown)
@@ -1555,7 +1141,6 @@ export default function Page() {
           reviewCount={reviewCount()}
           onSession={() => setStore("mobileTab", "session")}
           onChanges={() => setStore("mobileTab", "changes")}
-          t={language.t as (key: string, vars?: Record<string, string | number | boolean>) => string}
         />
 
         {/* Session panel */}
@@ -1595,27 +1180,7 @@ export default function Page() {
                     isDesktop={isDesktop()}
                     onScrollSpyScroll={scrollSpy.onScroll}
                     onAutoScrollInteraction={autoScroll.handleInteraction}
-                    showHeader={!!(info()?.title || info()?.parentID)}
                     centered={centered()}
-                    title={info()?.title}
-                    parentID={info()?.parentID}
-                    openTitleEditor={openTitleEditor}
-                    closeTitleEditor={closeTitleEditor}
-                    saveTitleEditor={saveTitleEditor}
-                    titleRef={(el) => {
-                      titleRef = el
-                    }}
-                    titleState={title}
-                    onTitleDraft={(value) => setTitle("draft", value)}
-                    onTitleMenuOpen={(open) => setTitle("menuOpen", open)}
-                    onTitlePendingRename={(value) => setTitle("pendingRename", value)}
-                    onNavigateParent={() => {
-                      navigate(`/${params.dir}/session/${info()?.parentID}`)
-                    }}
-                    sessionID={params.id!}
-                    onArchiveSession={(sessionID) => void archiveSession(sessionID)}
-                    onDeleteSession={(sessionID) => dialog.show(() => <DialogDeleteSession sessionID={sessionID} />)}
-                    t={language.t as (key: string, vars?: Record<string, string | number | boolean>) => string}
                     setContentRef={(el) => {
                       content = el
                       autoScroll.contentRef(el)
@@ -1670,15 +1235,6 @@ export default function Page() {
 
           <SessionPromptDock
             centered={centered()}
-            questionRequest={questionRequest}
-            permissionRequest={permRequest}
-            blocked={blocked()}
-            todos={todos()}
-            promptReady={prompt.ready()}
-            handoffPrompt={handoff.session.get(sessionKey())?.prompt}
-            t={language.t as (key: string, vars?: Record<string, string | number | boolean>) => string}
-            responding={ui.responding}
-            onDecide={decide}
             inputRef={(el) => {
               inputRef = el
             }}
@@ -1688,7 +1244,9 @@ export default function Page() {
               comments.clear()
               resumeScroll()
             }}
-            setPromptDockRef={(el) => (promptDock = el)}
+            setPromptDockRef={(el) => {
+              promptDock = el
+            }}
           />
 
           <Show when={desktopReviewOpen()}>
@@ -1702,64 +1260,10 @@ export default function Page() {
           </Show>
         </div>
 
-        <SessionSidePanel
-          open={desktopSidePanelOpen()}
-          reviewOpen={desktopReviewOpen()}
-          language={language}
-          layout={layout}
-          command={command}
-          dialog={dialog}
-          file={file}
-          comments={comments}
-          hasReview={hasReview()}
-          reviewCount={reviewCount()}
-          reviewTab={reviewTab()}
-          contextOpen={contextOpen}
-          openedTabs={openedTabs}
-          activeTab={activeTab}
-          activeFileTab={activeFileTab}
-          tabs={tabs}
-          openTab={openTab}
-          showAllFiles={showAllFiles}
-          reviewPanel={reviewPanel}
-          vm={{
-            messages,
-            visibleUserMessages,
-            view,
-            info,
-          }}
-          handoffFiles={() => handoff.session.get(sessionKey())?.files}
-          codeComponent={codeComponent}
-          addCommentToContext={addCommentToContext}
-          activeDraggable={() => store.activeDraggable}
-          onDragStart={handleDragStart}
-          onDragEnd={handleDragEnd}
-          onDragOver={handleDragOver}
-          fileTreeTab={fileTreeTab}
-          setFileTreeTabValue={setFileTreeTabValue}
-          diffsReady={diffsReady()}
-          diffFiles={diffFiles()}
-          kinds={kinds()}
-          activeDiff={tree.activeDiff}
-          focusReviewDiff={focusReviewDiff}
-        />
+        <SessionSidePanel reviewPanel={reviewPanel} activeDiff={tree.activeDiff} focusReviewDiff={focusReviewDiff} />
       </div>
 
-      <TerminalPanel
-        open={isDesktop() && view().terminal.opened()}
-        height={layout.terminal.height()}
-        resize={layout.terminal.resize}
-        close={view().terminal.close}
-        terminal={terminal}
-        language={language}
-        command={command}
-        handoff={() => handoff.terminal.get(params.dir!) ?? []}
-        activeTerminalDraggable={() => store.activeTerminalDraggable}
-        handleTerminalDragStart={handleTerminalDragStart}
-        handleTerminalDragOver={handleTerminalDragOver}
-        handleTerminalDragEnd={handleTerminalDragEnd}
-        onCloseTab={() => setUi("autoCreated", false)}
-      />
+      <TerminalPanel />
     </div>
   )
 }

+ 81 - 46
packages/app/src/pages/session/file-tabs.tsx

@@ -1,6 +1,8 @@
-import { type ValidComponent, createEffect, createMemo, For, Match, on, onCleanup, Show, Switch } from "solid-js"
+import { createEffect, createMemo, For, Match, on, onCleanup, Show, Switch } from "solid-js"
 import { createStore, produce } from "solid-js/store"
 import { Dynamic } from "solid-js/web"
+import { useParams } from "@solidjs/router"
+import { useCodeComponent } from "@opencode-ai/ui/context/code"
 import { sampledChecksum } from "@opencode-ai/util/encode"
 import { decode64 } from "@/utils/base64"
 import { showToast } from "@opencode-ai/ui/toast"
@@ -8,9 +10,11 @@ import { LineComment as LineCommentView, LineCommentEditor } from "@opencode-ai/
 import { Mark } from "@opencode-ai/ui/logo"
 import { Tabs } from "@opencode-ai/ui/tabs"
 import { useLayout } from "@/context/layout"
-import { useFile, type SelectedLineRange } from "@/context/file"
+import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
 import { useComments } from "@/context/comments"
 import { useLanguage } from "@/context/language"
+import { usePrompt } from "@/context/prompt"
+import { getSessionHandoff } from "@/pages/session/handoff"
 
 const formatCommentLabel = (range: SelectedLineRange) => {
   const start = Math.min(range.start, range.end)
@@ -19,34 +23,29 @@ const formatCommentLabel = (range: SelectedLineRange) => {
   return `lines ${start}-${end}`
 }
 
-export function FileTabContent(props: {
-  tab: string
-  activeTab: () => string
-  tabs: () => ReturnType<ReturnType<typeof useLayout>["tabs"]>
-  view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
-  handoffFiles: () => Record<string, SelectedLineRange | null> | undefined
-  file: ReturnType<typeof useFile>
-  comments: ReturnType<typeof useComments>
-  language: ReturnType<typeof useLanguage>
-  codeComponent: NonNullable<ValidComponent>
-  addCommentToContext: (input: {
-    file: string
-    selection: SelectedLineRange
-    comment: string
-    preview?: string
-    origin?: "review" | "file"
-  }) => void
-}) {
+export function FileTabContent(props: { tab: string }) {
+  const params = useParams()
+  const layout = useLayout()
+  const file = useFile()
+  const comments = useComments()
+  const language = useLanguage()
+  const prompt = usePrompt()
+  const codeComponent = useCodeComponent()
+
+  const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
+  const tabs = createMemo(() => layout.tabs(sessionKey))
+  const view = createMemo(() => layout.view(sessionKey))
+
   let scroll: HTMLDivElement | undefined
   let scrollFrame: number | undefined
   let pending: { x: number; y: number } | undefined
   let codeScroll: HTMLElement[] = []
 
-  const path = createMemo(() => props.file.pathFromTab(props.tab))
+  const path = createMemo(() => file.pathFromTab(props.tab))
   const state = createMemo(() => {
     const p = path()
     if (!p) return
-    return props.file.get(p)
+    return file.get(p)
   })
   const contents = createMemo(() => state()?.content?.content ?? "")
   const cacheKey = createMemo(() => sampledChecksum(contents()))
@@ -82,7 +81,7 @@ export function FileTabContent(props: {
     svgToast.shown = true
     showToast({
       variant: "error",
-      title: props.language.t("toast.file.loadFailed.title"),
+      title: language.t("toast.file.loadFailed.title"),
     })
   })
   const svgPreviewUrl = createMemo(() => {
@@ -100,16 +99,57 @@ export function FileTabContent(props: {
   const selectedLines = createMemo(() => {
     const p = path()
     if (!p) return null
-    if (props.file.ready()) return props.file.selectedLines(p) ?? null
-    return props.handoffFiles()?.[p] ?? null
+    if (file.ready()) return file.selectedLines(p) ?? null
+    return getSessionHandoff(sessionKey())?.files[p] ?? null
   })
 
+  const selectionPreview = (source: string, selection: FileSelection) => {
+    const start = Math.max(1, Math.min(selection.startLine, selection.endLine))
+    const end = Math.max(selection.startLine, selection.endLine)
+    const lines = source.split("\n").slice(start - 1, end)
+    if (lines.length === 0) return undefined
+    return lines.slice(0, 2).join("\n")
+  }
+
+  const addCommentToContext = (input: {
+    file: string
+    selection: SelectedLineRange
+    comment: string
+    preview?: string
+    origin?: "review" | "file"
+  }) => {
+    const selection = selectionFromLines(input.selection)
+    const preview =
+      input.preview ??
+      (() => {
+        if (input.file === path()) return selectionPreview(contents(), selection)
+        const source = file.get(input.file)?.content?.content
+        if (!source) return undefined
+        return selectionPreview(source, selection)
+      })()
+
+    const saved = comments.add({
+      file: input.file,
+      selection: input.selection,
+      comment: input.comment,
+    })
+    prompt.context.add({
+      type: "file",
+      path: input.file,
+      selection,
+      comment: input.comment,
+      commentID: saved.id,
+      commentOrigin: input.origin,
+      preview,
+    })
+  }
+
   let wrap: HTMLDivElement | undefined
 
   const fileComments = createMemo(() => {
     const p = path()
     if (!p) return []
-    return props.comments.list(p)
+    return comments.list(p)
   })
 
   const commentLayout = createMemo(() => {
@@ -228,19 +268,19 @@ export function FileTabContent(props: {
   })
 
   createEffect(() => {
-    const focus = props.comments.focus()
+    const focus = comments.focus()
     const p = path()
     if (!focus || !p) return
     if (focus.file !== p) return
-    if (props.activeTab() !== props.tab) return
+    if (tabs().active() !== props.tab) return
 
     const target = fileComments().find((comment) => comment.id === focus.id)
     if (!target) return
 
     setNote("openedComment", target.id)
     setNote("commenting", null)
-    props.file.setSelectedLines(p, target.selection)
-    requestAnimationFrame(() => props.comments.clearFocus())
+    file.setSelectedLines(p, target.selection)
+    requestAnimationFrame(() => comments.clearFocus())
   })
 
   const getCodeScroll = () => {
@@ -269,7 +309,7 @@ export function FileTabContent(props: {
       pending = undefined
       if (!out) return
 
-      props.view().setScroll(props.tab, out)
+      view().setScroll(props.tab, out)
     })
   }
 
@@ -305,7 +345,7 @@ export function FileTabContent(props: {
     const el = scroll
     if (!el) return
 
-    const s = props.view()?.scroll(props.tab)
+    const s = view().scroll(props.tab)
     if (!s) return
 
     syncCodeScroll()
@@ -343,7 +383,7 @@ export function FileTabContent(props: {
 
   createEffect(
     on(
-      () => props.file.ready(),
+      () => file.ready(),
       (ready) => {
         if (!ready) return
         requestAnimationFrame(restoreScroll)
@@ -354,7 +394,7 @@ export function FileTabContent(props: {
 
   createEffect(
     on(
-      () => props.tabs().active() === props.tab,
+      () => tabs().active() === props.tab,
       (active) => {
         if (!active) return
         if (!state()?.loaded) return
@@ -381,7 +421,7 @@ export function FileTabContent(props: {
       class={`relative overflow-hidden ${wrapperClass}`}
     >
       <Dynamic
-        component={props.codeComponent}
+        component={codeComponent}
         file={{
           name: path() ?? "",
           contents: source,
@@ -397,7 +437,7 @@ export function FileTabContent(props: {
         onLineSelected={(range: SelectedLineRange | null) => {
           const p = path()
           if (!p) return
-          props.file.setSelectedLines(p, range)
+          file.setSelectedLines(p, range)
           if (!range) setNote("commenting", null)
         }}
         onLineSelectionEnd={(range: SelectedLineRange | null) => {
@@ -423,14 +463,14 @@ export function FileTabContent(props: {
             onMouseEnter={() => {
               const p = path()
               if (!p) return
-              props.file.setSelectedLines(p, comment.selection)
+              file.setSelectedLines(p, comment.selection)
             }}
             onClick={() => {
               const p = path()
               if (!p) return
               setNote("commenting", null)
               setNote("openedComment", (current) => (current === comment.id ? null : comment.id))
-              props.file.setSelectedLines(p, comment.selection)
+              file.setSelectedLines(p, comment.selection)
             }}
           />
         )}
@@ -447,12 +487,7 @@ export function FileTabContent(props: {
               onSubmit={(value) => {
                 const p = path()
                 if (!p) return
-                props.addCommentToContext({
-                  file: p,
-                  selection: range(),
-                  comment: value,
-                  origin: "file",
-                })
+                addCommentToContext({ file: p, selection: range(), comment: value, origin: "file" })
                 setNote("commenting", null)
               }}
               onPopoverFocusOut={(e: FocusEvent) => {
@@ -509,13 +544,13 @@ export function FileTabContent(props: {
             <Mark class="w-14 opacity-10" />
             <div class="flex flex-col gap-2 max-w-md">
               <div class="text-14-semibold text-text-strong truncate">{path()?.split("/").pop()}</div>
-              <div class="text-14-regular text-text-weak">{props.language.t("session.files.binaryContent")}</div>
+              <div class="text-14-regular text-text-weak">{language.t("session.files.binaryContent")}</div>
             </div>
           </div>
         </Match>
         <Match when={state()?.loaded}>{renderCode(contents(), "pb-40")}</Match>
         <Match when={state()?.loading}>
-          <div class="px-6 py-4 text-text-weak">{props.language.t("common.loading")}...</div>
+          <div class="px-6 py-4 text-text-weak">{language.t("common.loading")}...</div>
         </Match>
         <Match when={state()?.error}>{(err) => <div class="px-6 py-4 text-text-weak">{err()}</div>}</Match>
       </Switch>

+ 36 - 0
packages/app/src/pages/session/handoff.ts

@@ -0,0 +1,36 @@
+import type { SelectedLineRange } from "@/context/file"
+
+type HandoffSession = {
+  prompt: string
+  files: Record<string, SelectedLineRange | null>
+}
+
+const MAX = 40
+
+const store = {
+  session: new Map<string, HandoffSession>(),
+  terminal: new Map<string, string[]>(),
+}
+
+const touch = <K, V>(map: Map<K, V>, key: K, value: V) => {
+  map.delete(key)
+  map.set(key, value)
+  while (map.size > MAX) {
+    const first = map.keys().next().value
+    if (first === undefined) return
+    map.delete(first)
+  }
+}
+
+export const setSessionHandoff = (key: string, patch: Partial<HandoffSession>) => {
+  const prev = store.session.get(key) ?? { prompt: "", files: {} }
+  touch(store.session, key, { ...prev, ...patch })
+}
+
+export const getSessionHandoff = (key: string) => store.session.get(key)
+
+export const setTerminalHandoff = (key: string, value: string[]) => {
+  touch(store.terminal, key, value)
+}
+
+export const getTerminalHandoff = (key: string) => store.terminal.get(key)

+ 271 - 57
packages/app/src/pages/session/message-timeline.tsx

@@ -1,13 +1,21 @@
-import { For, onCleanup, onMount, Show, type JSX } from "solid-js"
+import { For, createEffect, createMemo, on, onCleanup, onMount, Show, type JSX } from "solid-js"
+import { createStore, produce } from "solid-js/store"
+import { useNavigate, useParams } from "@solidjs/router"
 import { Button } from "@opencode-ai/ui/button"
 import { Icon } from "@opencode-ai/ui/icon"
 import { IconButton } from "@opencode-ai/ui/icon-button"
 import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
+import { Dialog } from "@opencode-ai/ui/dialog"
 import { InlineInput } from "@opencode-ai/ui/inline-input"
 import { SessionTurn } from "@opencode-ai/ui/session-turn"
 import type { UserMessage } from "@opencode-ai/sdk/v2"
+import { showToast } from "@opencode-ai/ui/toast"
 import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
 import { SessionContextUsage } from "@/components/session-context-usage"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { useLanguage } from "@/context/language"
+import { useSDK } from "@/context/sdk"
+import { useSync } from "@/context/sync"
 
 const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => {
   const current = target instanceof Element ? target : undefined
@@ -53,29 +61,7 @@ export function MessageTimeline(props: {
   isDesktop: boolean
   onScrollSpyScroll: () => void
   onAutoScrollInteraction: (event: MouseEvent) => void
-  showHeader: boolean
   centered: boolean
-  title?: string
-  parentID?: string
-  openTitleEditor: () => void
-  closeTitleEditor: () => void
-  saveTitleEditor: () => void | Promise<void>
-  titleRef: (el: HTMLInputElement) => void
-  titleState: {
-    draft: string
-    editing: boolean
-    saving: boolean
-    menuOpen: boolean
-    pendingRename: boolean
-  }
-  onTitleDraft: (value: string) => void
-  onTitleMenuOpen: (open: boolean) => void
-  onTitlePendingRename: (value: boolean) => void
-  onNavigateParent: () => void
-  sessionID: string
-  onArchiveSession: (sessionID: string) => void
-  onDeleteSession: (sessionID: string) => void
-  t: (key: string, vars?: Record<string, string | number | boolean>) => string
   setContentRef: (el: HTMLDivElement) => void
   turnStart: number
   onRenderEarlier: () => void
@@ -91,6 +77,230 @@ export function MessageTimeline(props: {
 }) {
   let touchGesture: number | undefined
 
+  const params = useParams()
+  const navigate = useNavigate()
+  const sdk = useSDK()
+  const sync = useSync()
+  const dialog = useDialog()
+  const language = useLanguage()
+
+  const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
+  const sessionID = createMemo(() => params.id)
+  const info = createMemo(() => {
+    const id = sessionID()
+    if (!id) return
+    return sync.session.get(id)
+  })
+  const titleValue = createMemo(() => info()?.title)
+  const parentID = createMemo(() => info()?.parentID)
+  const showHeader = createMemo(() => !!(titleValue() || parentID()))
+
+  const [title, setTitle] = createStore({
+    draft: "",
+    editing: false,
+    saving: false,
+    menuOpen: false,
+    pendingRename: false,
+  })
+  let titleRef: HTMLInputElement | undefined
+
+  const errorMessage = (err: unknown) => {
+    if (err && typeof err === "object" && "data" in err) {
+      const data = (err as { data?: { message?: string } }).data
+      if (data?.message) return data.message
+    }
+    if (err instanceof Error) return err.message
+    return language.t("common.requestFailed")
+  }
+
+  createEffect(
+    on(
+      sessionKey,
+      () => setTitle({ draft: "", editing: false, saving: false, menuOpen: false, pendingRename: false }),
+      { defer: true },
+    ),
+  )
+
+  const openTitleEditor = () => {
+    if (!sessionID()) return
+    setTitle({ editing: true, draft: titleValue() ?? "" })
+    requestAnimationFrame(() => {
+      titleRef?.focus()
+      titleRef?.select()
+    })
+  }
+
+  const closeTitleEditor = () => {
+    if (title.saving) return
+    setTitle({ editing: false, saving: false })
+  }
+
+  const saveTitleEditor = async () => {
+    const id = sessionID()
+    if (!id) return
+    if (title.saving) return
+
+    const next = title.draft.trim()
+    if (!next || next === (titleValue() ?? "")) {
+      setTitle({ editing: false, saving: false })
+      return
+    }
+
+    setTitle("saving", true)
+    await sdk.client.session
+      .update({ sessionID: id, title: next })
+      .then(() => {
+        sync.set(
+          produce((draft) => {
+            const index = draft.session.findIndex((s) => s.id === id)
+            if (index !== -1) draft.session[index].title = next
+          }),
+        )
+        setTitle({ editing: false, saving: false })
+      })
+      .catch((err) => {
+        setTitle("saving", false)
+        showToast({
+          title: language.t("common.requestFailed"),
+          description: errorMessage(err),
+        })
+      })
+  }
+
+  const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => {
+    if (params.id !== sessionID) return
+    if (parentID) {
+      navigate(`/${params.dir}/session/${parentID}`)
+      return
+    }
+    if (nextSessionID) {
+      navigate(`/${params.dir}/session/${nextSessionID}`)
+      return
+    }
+    navigate(`/${params.dir}/session`)
+  }
+
+  const archiveSession = async (sessionID: string) => {
+    const session = sync.session.get(sessionID)
+    if (!session) return
+
+    const sessions = sync.data.session ?? []
+    const index = sessions.findIndex((s) => s.id === sessionID)
+    const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
+
+    await sdk.client.session
+      .update({ sessionID, time: { archived: Date.now() } })
+      .then(() => {
+        sync.set(
+          produce((draft) => {
+            const index = draft.session.findIndex((s) => s.id === sessionID)
+            if (index !== -1) draft.session.splice(index, 1)
+          }),
+        )
+        navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
+      })
+      .catch((err) => {
+        showToast({
+          title: language.t("common.requestFailed"),
+          description: errorMessage(err),
+        })
+      })
+  }
+
+  const deleteSession = async (sessionID: string) => {
+    const session = sync.session.get(sessionID)
+    if (!session) return false
+
+    const sessions = (sync.data.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
+    const index = sessions.findIndex((s) => s.id === sessionID)
+    const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
+
+    const result = await sdk.client.session
+      .delete({ sessionID })
+      .then((x) => x.data)
+      .catch((err) => {
+        showToast({
+          title: language.t("session.delete.failed.title"),
+          description: errorMessage(err),
+        })
+        return false
+      })
+
+    if (!result) return false
+
+    sync.set(
+      produce((draft) => {
+        const removed = new Set<string>([sessionID])
+
+        const byParent = new Map<string, string[]>()
+        for (const item of draft.session) {
+          const parentID = item.parentID
+          if (!parentID) continue
+          const existing = byParent.get(parentID)
+          if (existing) {
+            existing.push(item.id)
+            continue
+          }
+          byParent.set(parentID, [item.id])
+        }
+
+        const stack = [sessionID]
+        while (stack.length) {
+          const parentID = stack.pop()
+          if (!parentID) continue
+
+          const children = byParent.get(parentID)
+          if (!children) continue
+
+          for (const child of children) {
+            if (removed.has(child)) continue
+            removed.add(child)
+            stack.push(child)
+          }
+        }
+
+        draft.session = draft.session.filter((s) => !removed.has(s.id))
+      }),
+    )
+
+    navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
+    return true
+  }
+
+  const navigateParent = () => {
+    const id = parentID()
+    if (!id) return
+    navigate(`/${params.dir}/session/${id}`)
+  }
+
+  function DialogDeleteSession(props: { sessionID: string }) {
+    const name = createMemo(() => sync.session.get(props.sessionID)?.title ?? language.t("command.session.new"))
+    const handleDelete = async () => {
+      await deleteSession(props.sessionID)
+      dialog.close()
+    }
+
+    return (
+      <Dialog title={language.t("session.delete.title")} fit>
+        <div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
+          <div class="flex flex-col gap-1">
+            <span class="text-14-regular text-text-strong">
+              {language.t("session.delete.confirm", { name: name() })}
+            </span>
+          </div>
+          <div class="flex justify-end gap-2">
+            <Button variant="ghost" size="large" onClick={() => dialog.close()}>
+              {language.t("common.cancel")}
+            </Button>
+            <Button variant="primary" size="large" onClick={handleDelete}>
+              {language.t("session.delete.button")}
+            </Button>
+          </div>
+        </div>
+      </Dialog>
+    )
+  }
+
   return (
     <Show
       when={!props.mobileChanges}
@@ -157,9 +367,9 @@ export function MessageTimeline(props: {
           }}
           onClick={props.onAutoScrollInteraction}
           class="relative min-w-0 w-full h-full overflow-y-auto session-scroller"
-          style={{ "--session-title-height": props.showHeader ? "40px" : "0px" }}
+          style={{ "--session-title-height": showHeader() ? "40px" : "0px" }}
         >
-          <Show when={props.showHeader}>
+          <Show when={showHeader()}>
             <div
               classList={{
                 "sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
@@ -171,92 +381,96 @@ export function MessageTimeline(props: {
             >
               <div class="h-12 w-full flex items-center justify-between gap-2">
                 <div class="flex items-center gap-1 min-w-0 flex-1 pr-3">
-                  <Show when={props.parentID}>
+                  <Show when={parentID()}>
                     <IconButton
                       tabIndex={-1}
                       icon="arrow-left"
                       variant="ghost"
-                      onClick={props.onNavigateParent}
-                      aria-label={props.t("common.goBack")}
+                      onClick={navigateParent}
+                      aria-label={language.t("common.goBack")}
                     />
                   </Show>
-                  <Show when={props.title || props.titleState.editing}>
+                  <Show when={titleValue() || title.editing}>
                     <Show
-                      when={props.titleState.editing}
+                      when={title.editing}
                       fallback={
                         <h1
                           class="text-14-medium text-text-strong truncate grow-1 min-w-0 pl-2"
-                          onDblClick={props.openTitleEditor}
+                          onDblClick={openTitleEditor}
                         >
-                          {props.title}
+                          {titleValue()}
                         </h1>
                       }
                     >
                       <InlineInput
-                        ref={props.titleRef}
-                        value={props.titleState.draft}
-                        disabled={props.titleState.saving}
+                        ref={(el) => {
+                          titleRef = el
+                        }}
+                        value={title.draft}
+                        disabled={title.saving}
                         class="text-14-medium text-text-strong grow-1 min-w-0 pl-2 rounded-[6px]"
                         style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
-                        onInput={(event) => props.onTitleDraft(event.currentTarget.value)}
+                        onInput={(event) => setTitle("draft", event.currentTarget.value)}
                         onKeyDown={(event) => {
                           event.stopPropagation()
                           if (event.key === "Enter") {
                             event.preventDefault()
-                            void props.saveTitleEditor()
+                            void saveTitleEditor()
                             return
                           }
                           if (event.key === "Escape") {
                             event.preventDefault()
-                            props.closeTitleEditor()
+                            closeTitleEditor()
                           }
                         }}
-                        onBlur={props.closeTitleEditor}
+                        onBlur={closeTitleEditor}
                       />
                     </Show>
                   </Show>
                 </div>
-                <Show when={props.sessionID}>
+                <Show when={sessionID()}>
                   {(id) => (
                     <div class="shrink-0 flex items-center gap-3">
                       <SessionContextUsage placement="bottom" />
                       <DropdownMenu
                         gutter={4}
                         placement="bottom-end"
-                        open={props.titleState.menuOpen}
-                        onOpenChange={props.onTitleMenuOpen}
+                        open={title.menuOpen}
+                        onOpenChange={(open) => setTitle("menuOpen", open)}
                       >
                         <DropdownMenu.Trigger
                           as={IconButton}
                           icon="dot-grid"
                           variant="ghost"
                           class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
-                          aria-label={props.t("common.moreOptions")}
+                          aria-label={language.t("common.moreOptions")}
                         />
                         <DropdownMenu.Portal>
                           <DropdownMenu.Content
                             style={{ "min-width": "104px" }}
                             onCloseAutoFocus={(event) => {
-                              if (!props.titleState.pendingRename) return
+                              if (!title.pendingRename) return
                               event.preventDefault()
-                              props.onTitlePendingRename(false)
-                              props.openTitleEditor()
+                              setTitle("pendingRename", false)
+                              openTitleEditor()
                             }}
                           >
                             <DropdownMenu.Item
                               onSelect={() => {
-                                props.onTitlePendingRename(true)
-                                props.onTitleMenuOpen(false)
+                                setTitle("pendingRename", true)
+                                setTitle("menuOpen", false)
                               }}
                             >
-                              <DropdownMenu.ItemLabel>{props.t("common.rename")}</DropdownMenu.ItemLabel>
+                              <DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
                             </DropdownMenu.Item>
-                            <DropdownMenu.Item onSelect={() => props.onArchiveSession(id())}>
-                              <DropdownMenu.ItemLabel>{props.t("common.archive")}</DropdownMenu.ItemLabel>
+                            <DropdownMenu.Item onSelect={() => void archiveSession(id())}>
+                              <DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
                             </DropdownMenu.Item>
                             <DropdownMenu.Separator />
-                            <DropdownMenu.Item onSelect={() => props.onDeleteSession(id())}>
-                              <DropdownMenu.ItemLabel>{props.t("common.delete")}</DropdownMenu.ItemLabel>
+                            <DropdownMenu.Item
+                              onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
+                            >
+                              <DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
                             </DropdownMenu.Item>
                           </DropdownMenu.Content>
                         </DropdownMenu.Portal>
@@ -282,7 +496,7 @@ export function MessageTimeline(props: {
             <Show when={props.turnStart > 0}>
               <div class="w-full flex justify-center">
                 <Button variant="ghost" size="large" class="text-12-medium opacity-50" onClick={props.onRenderEarlier}>
-                  {props.t("session.messages.renderEarlier")}
+                  {language.t("session.messages.renderEarlier")}
                 </Button>
               </div>
             </Show>
@@ -296,8 +510,8 @@ export function MessageTimeline(props: {
                   onClick={props.onLoadEarlier}
                 >
                   {props.historyLoading
-                    ? props.t("session.messages.loadingEarlier")
-                    : props.t("session.messages.loadEarlier")}
+                    ? language.t("session.messages.loadingEarlier")
+                    : language.t("session.messages.loadEarlier")}
                 </Button>
               </div>
             </Show>
@@ -321,7 +535,7 @@ export function MessageTimeline(props: {
                     }}
                   >
                     <SessionTurn
-                      sessionID={props.sessionID}
+                      sessionID={sessionID() ?? ""}
                       messageID={message.id}
                       lastUserMessageID={props.lastUserMessageID}
                       classes={{

+ 6 - 4
packages/app/src/pages/session/session-mobile-tabs.tsx

@@ -1,5 +1,6 @@
 import { Show } from "solid-js"
 import { Tabs } from "@opencode-ai/ui/tabs"
+import { useLanguage } from "@/context/language"
 
 export function SessionMobileTabs(props: {
   open: boolean
@@ -8,8 +9,9 @@ export function SessionMobileTabs(props: {
   reviewCount: number
   onSession: () => void
   onChanges: () => void
-  t: (key: string, vars?: Record<string, string | number | boolean>) => string
 }) {
+  const language = useLanguage()
+
   return (
     <Show when={props.open}>
       <Tabs value={props.mobileTab} class="h-auto">
@@ -20,7 +22,7 @@ export function SessionMobileTabs(props: {
             classes={{ button: "w-full" }}
             onClick={props.onSession}
           >
-            {props.t("session.tab.session")}
+            {language.t("session.tab.session")}
           </Tabs.Trigger>
           <Tabs.Trigger
             value="changes"
@@ -29,8 +31,8 @@ export function SessionMobileTabs(props: {
             onClick={props.onChanges}
           >
             {props.hasReview
-              ? props.t("session.review.filesChanged", { count: props.reviewCount })
-              : props.t("session.review.change.other")}
+              ? language.t("session.review.filesChanged", { count: props.reviewCount })
+              : language.t("session.review.change.other")}
           </Tabs.Trigger>
         </Tabs.List>
       </Tabs>

+ 102 - 42
packages/app/src/pages/session/session-prompt-dock.tsx

@@ -1,35 +1,105 @@
 import { For, Show, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
-import type { QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
+import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
+import { useParams } from "@solidjs/router"
 import { Button } from "@opencode-ai/ui/button"
 import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
 import { Icon } from "@opencode-ai/ui/icon"
+import { showToast } from "@opencode-ai/ui/toast"
 import { PromptInput } from "@/components/prompt-input"
 import { QuestionDock } from "@/components/question-dock"
 import { SessionTodoDock } from "@/components/session-todo-dock"
+import { useGlobalSync } from "@/context/global-sync"
+import { useLanguage } from "@/context/language"
+import { usePrompt } from "@/context/prompt"
+import { useSDK } from "@/context/sdk"
+import { useSync } from "@/context/sync"
+import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
 
 export function SessionPromptDock(props: {
   centered: boolean
-  questionRequest: () => QuestionRequest | undefined
-  permissionRequest: () => { patterns: string[]; permission: string } | undefined
-  blocked: boolean
-  todos: Todo[]
-  promptReady: boolean
-  handoffPrompt?: string
-  t: (key: string, vars?: Record<string, string | number | boolean>) => string
-  responding: boolean
-  onDecide: (response: "once" | "always" | "reject") => void
   inputRef: (el: HTMLDivElement) => void
   newSessionWorktree: string
   onNewSessionWorktreeReset: () => void
   onSubmit: () => void
   setPromptDockRef: (el: HTMLDivElement) => void
 }) {
+  const params = useParams()
+  const sdk = useSDK()
+  const sync = useSync()
+  const globalSync = useGlobalSync()
+  const prompt = usePrompt()
+  const language = useLanguage()
+
+  const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
+  const handoffPrompt = createMemo(() => getSessionHandoff(sessionKey())?.prompt)
+
+  const todos = createMemo((): Todo[] => {
+    const id = params.id
+    if (!id) return []
+    return globalSync.data.session_todo[id] ?? []
+  })
+
+  const questionRequest = createMemo((): QuestionRequest | undefined => {
+    const sessionID = params.id
+    if (!sessionID) return
+    return sync.data.question[sessionID]?.[0]
+  })
+
+  const permissionRequest = createMemo((): PermissionRequest | undefined => {
+    const sessionID = params.id
+    if (!sessionID) return
+    return sync.data.permission[sessionID]?.[0]
+  })
+
+  const blocked = createMemo(() => !!permissionRequest() || !!questionRequest())
+
+  const previewPrompt = () =>
+    prompt
+      .current()
+      .map((part) => {
+        if (part.type === "file") return `[file:${part.path}]`
+        if (part.type === "agent") return `@${part.name}`
+        if (part.type === "image") return `[image:${part.filename}]`
+        return part.content
+      })
+      .join("")
+      .trim()
+
+  createEffect(() => {
+    if (!prompt.ready()) return
+    setSessionHandoff(sessionKey(), { prompt: previewPrompt() })
+  })
+
+  const [responding, setResponding] = createSignal(false)
+
+  createEffect(
+    on(
+      () => permissionRequest()?.id,
+      () => setResponding(false),
+      { defer: true },
+    ),
+  )
+
+  const decide = (response: "once" | "always" | "reject") => {
+    const perm = permissionRequest()
+    if (!perm) return
+    if (responding()) return
+
+    setResponding(true)
+    sdk.client.permission
+      .respond({ sessionID: perm.sessionID, permissionID: perm.id, response })
+      .catch((err: unknown) => {
+        const message = err instanceof Error ? err.message : String(err)
+        showToast({ title: language.t("common.requestFailed"), description: message })
+      })
+      .finally(() => setResponding(false))
+  }
+
   const done = createMemo(
-    () =>
-      props.todos.length > 0 && props.todos.every((todo) => todo.status === "completed" || todo.status === "cancelled"),
+    () => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"),
   )
 
-  const [dock, setDock] = createSignal(props.todos.length > 0)
+  const [dock, setDock] = createSignal(todos().length > 0)
   const [closing, setClosing] = createSignal(false)
   const [opening, setOpening] = createSignal(false)
   let timer: number | undefined
@@ -46,7 +116,7 @@ export function SessionPromptDock(props: {
 
   createEffect(
     on(
-      () => [props.todos.length, done()] as const,
+      () => [todos().length, done()] as const,
       ([count, complete], prev) => {
         if (raf) cancelAnimationFrame(raf)
         raf = undefined
@@ -113,7 +183,7 @@ export function SessionPromptDock(props: {
           "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
         }}
       >
-        <Show when={props.questionRequest()} keyed>
+        <Show when={questionRequest()} keyed>
           {(req) => {
             return (
               <div>
@@ -123,11 +193,11 @@ export function SessionPromptDock(props: {
           }}
         </Show>
 
-        <Show when={props.permissionRequest()} keyed>
+        <Show when={permissionRequest()} keyed>
           {(perm) => {
             const toolDescription = () => {
               const key = `settings.permissions.tool.${perm.permission}.description`
-              const value = props.t(key)
+              const value = language.t(key as Parameters<typeof language.t>[0])
               if (value === key) return ""
               return value
             }
@@ -141,36 +211,26 @@ export function SessionPromptDock(props: {
                       <span data-slot="permission-icon">
                         <Icon name="warning" size="normal" />
                       </span>
-                      <div data-slot="permission-header-title">{props.t("notification.permission.title")}</div>
+                      <div data-slot="permission-header-title">{language.t("notification.permission.title")}</div>
                     </div>
                   }
                   footer={
                     <>
                       <div />
                       <div data-slot="permission-footer-actions">
-                        <Button
-                          variant="ghost"
-                          size="normal"
-                          onClick={() => props.onDecide("reject")}
-                          disabled={props.responding}
-                        >
-                          {props.t("ui.permission.deny")}
+                        <Button variant="ghost" size="normal" onClick={() => decide("reject")} disabled={responding()}>
+                          {language.t("ui.permission.deny")}
                         </Button>
                         <Button
                           variant="secondary"
                           size="normal"
-                          onClick={() => props.onDecide("always")}
-                          disabled={props.responding}
+                          onClick={() => decide("always")}
+                          disabled={responding()}
                         >
-                          {props.t("ui.permission.allowAlways")}
+                          {language.t("ui.permission.allowAlways")}
                         </Button>
-                        <Button
-                          variant="primary"
-                          size="normal"
-                          onClick={() => props.onDecide("once")}
-                          disabled={props.responding}
-                        >
-                          {props.t("ui.permission.allowOnce")}
+                        <Button variant="primary" size="normal" onClick={() => decide("once")} disabled={responding()}>
+                          {language.t("ui.permission.allowOnce")}
                         </Button>
                       </div>
                     </>
@@ -199,12 +259,12 @@ export function SessionPromptDock(props: {
           }}
         </Show>
 
-        <Show when={!props.blocked}>
+        <Show when={!blocked()}>
           <Show
-            when={props.promptReady}
+            when={prompt.ready()}
             fallback={
               <div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none">
-                {props.handoffPrompt || props.t("prompt.loading")}
+                {handoffPrompt() || language.t("prompt.loading")}
               </div>
             }
           >
@@ -219,10 +279,10 @@ export function SessionPromptDock(props: {
                 }}
               >
                 <SessionTodoDock
-                  todos={props.todos}
-                  title={props.t("session.todo.title")}
-                  collapseLabel={props.t("session.todo.collapse")}
-                  expandLabel={props.t("session.todo.expand")}
+                  todos={todos()}
+                  title={language.t("session.todo.title")}
+                  collapseLabel={language.t("session.todo.collapse")}
+                  expandLabel={language.t("session.todo.expand")}
                 />
               </div>
             </Show>

+ 230 - 143
packages/app/src/pages/session/session-side-panel.tsx

@@ -1,156 +1,269 @@
-import { For, Match, Show, Switch, createMemo, onCleanup, type JSX, type ValidComponent } from "solid-js"
+import { For, Match, Show, Switch, createEffect, createMemo, onCleanup, type JSX } from "solid-js"
+import { createStore } from "solid-js/store"
+import { createMediaQuery } from "@solid-primitives/media"
+import { useParams } from "@solidjs/router"
 import { Tabs } from "@opencode-ai/ui/tabs"
 import { IconButton } from "@opencode-ai/ui/icon-button"
 import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
 import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
 import { Mark } from "@opencode-ai/ui/logo"
+import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
+import type { DragEvent } from "@thisbeyond/solid-dnd"
+import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+
 import FileTree from "@/components/file-tree"
 import { SessionContextUsage } from "@/components/session-context-usage"
-import { SessionContextTab, SortableTab, FileVisual } from "@/components/session"
 import { DialogSelectFile } from "@/components/dialog-select-file"
-import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
-import { FileTabContent } from "@/pages/session/file-tabs"
-import { StickyAddButton } from "@/pages/session/review-tab"
-import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
-import { ConstrainDragYAxis } from "@/utils/solid-dnd"
-import type { DragEvent } from "@thisbeyond/solid-dnd"
-import { useComments } from "@/context/comments"
+import { SessionContextTab, SortableTab, FileVisual } from "@/components/session"
 import { useCommand } from "@/context/command"
-import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { useFile, type SelectedLineRange } from "@/context/file"
 import { useLanguage } from "@/context/language"
 import { useLayout } from "@/context/layout"
 import { useSync } from "@/context/sync"
-import type { Message, UserMessage } from "@opencode-ai/sdk/v2/client"
-
-type SessionSidePanelViewModel = {
-  messages: () => Message[]
-  visibleUserMessages: () => UserMessage[]
-  view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
-  info: () => ReturnType<ReturnType<typeof useSync>["session"]["get"]>
-}
+import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
+import { FileTabContent } from "@/pages/session/file-tabs"
+import { getTabReorderIndex } from "@/pages/session/helpers"
+import { StickyAddButton } from "@/pages/session/review-tab"
+import { setSessionHandoff } from "@/pages/session/handoff"
 
 export function SessionSidePanel(props: {
-  open: boolean
-  reviewOpen: boolean
-  language: ReturnType<typeof useLanguage>
-  layout: ReturnType<typeof useLayout>
-  command: ReturnType<typeof useCommand>
-  dialog: ReturnType<typeof useDialog>
-  file: ReturnType<typeof useFile>
-  comments: ReturnType<typeof useComments>
-  hasReview: boolean
-  reviewCount: number
-  reviewTab: boolean
-  contextOpen: () => boolean
-  openedTabs: () => string[]
-  activeTab: () => string
-  activeFileTab: () => string | undefined
-  tabs: () => ReturnType<ReturnType<typeof useLayout>["tabs"]>
-  openTab: (value: string) => void
-  showAllFiles: () => void
   reviewPanel: () => JSX.Element
-  vm: SessionSidePanelViewModel
-  handoffFiles: () => Record<string, SelectedLineRange | null> | undefined
-  codeComponent: NonNullable<ValidComponent>
-  addCommentToContext: (input: {
-    file: string
-    selection: SelectedLineRange
-    comment: string
-    preview?: string
-    origin?: "review" | "file"
-  }) => void
-  activeDraggable: () => string | undefined
-  onDragStart: (event: unknown) => void
-  onDragEnd: () => void
-  onDragOver: (event: DragEvent) => void
-  fileTreeTab: () => "changes" | "all"
-  setFileTreeTabValue: (value: string) => void
-  diffsReady: boolean
-  diffFiles: string[]
-  kinds: Map<string, "add" | "del" | "mix">
   activeDiff?: string
   focusReviewDiff: (path: string) => void
 }) {
-  const openedTabs = createMemo(() => props.openedTabs())
+  const params = useParams()
+  const layout = useLayout()
+  const sync = useSync()
+  const file = useFile()
+  const language = useLanguage()
+  const command = useCommand()
+  const dialog = useDialog()
+
+  const isDesktop = createMediaQuery("(min-width: 768px)")
+  const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
+  const tabs = createMemo(() => layout.tabs(sessionKey))
+  const view = createMemo(() => layout.view(sessionKey))
+
+  const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
+  const open = createMemo(() => isDesktop() && (view().reviewPanel.opened() || layout.fileTree.opened()))
+  const reviewTab = createMemo(() => isDesktop() && !layout.fileTree.opened())
+
+  const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
+  const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
+  const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
+  const hasReview = createMemo(() => reviewCount() > 0)
+  const diffsReady = createMemo(() => {
+    const id = params.id
+    if (!id) return true
+    if (!hasReview()) return true
+    return sync.data.session_diff[id] !== undefined
+  })
+
+  const diffFiles = createMemo(() => diffs().map((d) => d.file))
+  const kinds = createMemo(() => {
+    const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
+      if (!a) return b
+      if (a === b) return a
+      return "mix" as const
+    }
+
+    const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
+
+    const out = new Map<string, "add" | "del" | "mix">()
+    for (const diff of diffs()) {
+      const file = normalize(diff.file)
+      const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
+
+      out.set(file, kind)
+
+      const parts = file.split("/")
+      for (const [idx] of parts.slice(0, -1).entries()) {
+        const dir = parts.slice(0, idx + 1).join("/")
+        if (!dir) continue
+        out.set(dir, merge(out.get(dir), kind))
+      }
+    }
+    return out
+  })
+
+  const normalizeTab = (tab: string) => {
+    if (!tab.startsWith("file://")) return tab
+    return file.tab(tab)
+  }
+
+  const openReviewPanel = () => {
+    if (!view().reviewPanel.opened()) view().reviewPanel.open()
+  }
+
+  const openTab = (value: string) => {
+    const next = normalizeTab(value)
+    tabs().open(next)
+
+    const path = file.pathFromTab(next)
+    if (!path) return
+    file.load(path)
+    openReviewPanel()
+  }
+
+  const contextOpen = createMemo(() => tabs().active() === "context" || tabs().all().includes("context"))
+  const openedTabs = createMemo(() =>
+    tabs()
+      .all()
+      .filter((tab) => tab !== "context" && tab !== "review"),
+  )
+
+  const activeTab = createMemo(() => {
+    const active = tabs().active()
+    if (active === "context") return "context"
+    if (active === "review" && reviewTab()) return "review"
+    if (active && file.pathFromTab(active)) return normalizeTab(active)
+
+    const first = openedTabs()[0]
+    if (first) return first
+    if (contextOpen()) return "context"
+    if (reviewTab() && hasReview()) return "review"
+    return "empty"
+  })
+
+  const activeFileTab = createMemo(() => {
+    const active = activeTab()
+    if (!openedTabs().includes(active)) return
+    return active
+  })
+
+  const fileTreeTab = () => layout.fileTree.tab()
+
+  const setFileTreeTabValue = (value: string) => {
+    if (value !== "changes" && value !== "all") return
+    layout.fileTree.setTab(value)
+  }
+
+  const showAllFiles = () => {
+    if (fileTreeTab() !== "changes") return
+    layout.fileTree.setTab("all")
+  }
+
+  const [store, setStore] = createStore({
+    activeDraggable: undefined as string | undefined,
+  })
+
+  const handleDragStart = (event: unknown) => {
+    const id = getDraggableId(event)
+    if (!id) return
+    setStore("activeDraggable", id)
+  }
+
+  const handleDragOver = (event: DragEvent) => {
+    const { draggable, droppable } = event
+    if (!draggable || !droppable) return
+
+    const currentTabs = tabs().all()
+    const toIndex = getTabReorderIndex(currentTabs, draggable.id.toString(), droppable.id.toString())
+    if (toIndex === undefined) return
+    tabs().move(draggable.id.toString(), toIndex)
+  }
+
+  const handleDragEnd = () => {
+    setStore("activeDraggable", undefined)
+  }
+
+  createEffect(() => {
+    if (!file.ready()) return
+
+    setSessionHandoff(sessionKey(), {
+      files: tabs()
+        .all()
+        .reduce<Record<string, SelectedLineRange | null>>((acc, tab) => {
+          const path = file.pathFromTab(tab)
+          if (!path) return acc
+
+          const selected = file.selectedLines(path)
+          acc[path] =
+            selected && typeof selected === "object" && "start" in selected && "end" in selected
+              ? (selected as SelectedLineRange)
+              : null
+
+          return acc
+        }, {}),
+    })
+  })
 
   return (
-    <Show when={props.open}>
+    <Show when={open()}>
       <aside
         id="review-panel"
-        aria-label={props.language.t("session.panel.reviewAndFiles")}
+        aria-label={language.t("session.panel.reviewAndFiles")}
         class="relative min-w-0 h-full border-l border-border-weak-base flex"
         classList={{
-          "flex-1": props.reviewOpen,
-          "shrink-0": !props.reviewOpen,
+          "flex-1": reviewOpen(),
+          "shrink-0": !reviewOpen(),
         }}
-        style={{ width: props.reviewOpen ? undefined : `${props.layout.fileTree.width()}px` }}
+        style={{ width: reviewOpen() ? undefined : `${layout.fileTree.width()}px` }}
       >
-        <Show when={props.reviewOpen}>
+        <Show when={reviewOpen()}>
           <div class="flex-1 min-w-0 h-full">
             <Show
-              when={props.layout.fileTree.opened() && props.fileTreeTab() === "changes"}
+              when={layout.fileTree.opened() && fileTreeTab() === "changes"}
               fallback={
                 <DragDropProvider
-                  onDragStart={props.onDragStart}
-                  onDragEnd={props.onDragEnd}
-                  onDragOver={props.onDragOver}
+                  onDragStart={handleDragStart}
+                  onDragEnd={handleDragEnd}
+                  onDragOver={handleDragOver}
                   collisionDetector={closestCenter}
                 >
                   <DragDropSensors />
                   <ConstrainDragYAxis />
-                  <Tabs value={props.activeTab()} onChange={props.openTab}>
+                  <Tabs value={activeTab()} onChange={openTab}>
                     <div class="sticky top-0 shrink-0 flex">
                       <Tabs.List
                         ref={(el: HTMLDivElement) => {
-                          const stop = createFileTabListSync({ el, contextOpen: props.contextOpen })
+                          const stop = createFileTabListSync({ el, contextOpen })
                           onCleanup(stop)
                         }}
                       >
-                        <Show when={props.reviewTab}>
+                        <Show when={reviewTab()}>
                           <Tabs.Trigger value="review" classes={{ button: "!pl-6" }}>
                             <div class="flex items-center gap-1.5">
-                              <div>{props.language.t("session.tab.review")}</div>
-                              <Show when={props.hasReview}>
+                              <div>{language.t("session.tab.review")}</div>
+                              <Show when={hasReview()}>
                                 <div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
-                                  {props.reviewCount}
+                                  {reviewCount()}
                                 </div>
                               </Show>
                             </div>
                           </Tabs.Trigger>
                         </Show>
-                        <Show when={props.contextOpen()}>
+                        <Show when={contextOpen()}>
                           <Tabs.Trigger
                             value="context"
                             closeButton={
-                              <Tooltip value={props.language.t("common.closeTab")} placement="bottom">
+                              <Tooltip value={language.t("common.closeTab")} placement="bottom">
                                 <IconButton
                                   icon="close-small"
                                   variant="ghost"
                                   class="h-5 w-5"
-                                  onClick={() => props.tabs().close("context")}
-                                  aria-label={props.language.t("common.closeTab")}
+                                  onClick={() => tabs().close("context")}
+                                  aria-label={language.t("common.closeTab")}
                                 />
                               </Tooltip>
                             }
                             hideCloseButton
-                            onMiddleClick={() => props.tabs().close("context")}
+                            onMiddleClick={() => tabs().close("context")}
                           >
                             <div class="flex items-center gap-2">
                               <SessionContextUsage variant="indicator" />
-                              <div>{props.language.t("session.tab.context")}</div>
+                              <div>{language.t("session.tab.context")}</div>
                             </div>
                           </Tabs.Trigger>
                         </Show>
                         <SortableProvider ids={openedTabs()}>
-                          <For each={openedTabs()}>
-                            {(tab) => <SortableTab tab={tab} onTabClose={props.tabs().close} />}
-                          </For>
+                          <For each={openedTabs()}>{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}</For>
                         </SortableProvider>
                         <StickyAddButton>
                           <TooltipKeybind
-                            title={props.language.t("command.file.open")}
-                            keybind={props.command.keybind("file.open")}
+                            title={language.t("command.file.open")}
+                            keybind={command.keybind("file.open")}
                             class="flex items-center"
                           >
                             <IconButton
@@ -158,72 +271,52 @@ export function SessionSidePanel(props: {
                               variant="ghost"
                               iconSize="large"
                               onClick={() =>
-                                props.dialog.show(() => (
-                                  <DialogSelectFile mode="files" onOpenFile={props.showAllFiles} />
-                                ))
+                                dialog.show(() => <DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
                               }
-                              aria-label={props.language.t("command.file.open")}
+                              aria-label={language.t("command.file.open")}
                             />
                           </TooltipKeybind>
                         </StickyAddButton>
                       </Tabs.List>
                     </div>
 
-                    <Show when={props.reviewTab}>
+                    <Show when={reviewTab()}>
                       <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
-                        <Show when={props.activeTab() === "review"}>{props.reviewPanel()}</Show>
+                        <Show when={activeTab() === "review"}>{props.reviewPanel()}</Show>
                       </Tabs.Content>
                     </Show>
 
                     <Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
-                      <Show when={props.activeTab() === "empty"}>
+                      <Show when={activeTab() === "empty"}>
                         <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
                           <div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6">
                             <Mark class="w-14 opacity-10" />
                             <div class="text-14-regular text-text-weak max-w-56">
-                              {props.language.t("session.files.selectToOpen")}
+                              {language.t("session.files.selectToOpen")}
                             </div>
                           </div>
                         </div>
                       </Show>
                     </Tabs.Content>
 
-                    <Show when={props.contextOpen()}>
+                    <Show when={contextOpen()}>
                       <Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
-                        <Show when={props.activeTab() === "context"}>
+                        <Show when={activeTab() === "context"}>
                           <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
-                            <SessionContextTab
-                              messages={props.vm.messages}
-                              visibleUserMessages={props.vm.visibleUserMessages}
-                              view={props.vm.view}
-                              info={props.vm.info}
-                            />
+                            <SessionContextTab />
                           </div>
                         </Show>
                       </Tabs.Content>
                     </Show>
 
-                    <Show when={props.activeFileTab()} keyed>
-                      {(tab) => (
-                        <FileTabContent
-                          tab={tab}
-                          activeTab={props.activeTab}
-                          tabs={props.tabs}
-                          view={props.vm.view}
-                          handoffFiles={props.handoffFiles}
-                          file={props.file}
-                          comments={props.comments}
-                          language={props.language}
-                          codeComponent={props.codeComponent}
-                          addCommentToContext={props.addCommentToContext}
-                        />
-                      )}
+                    <Show when={activeFileTab()} keyed>
+                      {(tab) => <FileTabContent tab={tab} />}
                     </Show>
                   </Tabs>
                   <DragOverlay>
-                    <Show when={props.activeDraggable()}>
+                    <Show when={store.activeDraggable} keyed>
                       {(tab) => {
-                        const path = createMemo(() => props.file.pathFromTab(tab()))
+                        const path = createMemo(() => file.pathFromTab(tab))
                         return (
                           <div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
                             <Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show>
@@ -240,50 +333,44 @@ export function SessionSidePanel(props: {
           </div>
         </Show>
 
-        <Show when={props.layout.fileTree.opened()}>
-          <div
-            id="file-tree-panel"
-            class="relative shrink-0 h-full"
-            style={{ width: `${props.layout.fileTree.width()}px` }}
-          >
+        <Show when={layout.fileTree.opened()}>
+          <div id="file-tree-panel" class="relative shrink-0 h-full" style={{ width: `${layout.fileTree.width()}px` }}>
             <div
               class="h-full flex flex-col overflow-hidden group/filetree"
-              classList={{ "border-l border-border-weak-base": props.reviewOpen }}
+              classList={{ "border-l border-border-weak-base": reviewOpen() }}
             >
               <Tabs
                 variant="pill"
-                value={props.fileTreeTab()}
-                onChange={props.setFileTreeTabValue}
+                value={fileTreeTab()}
+                onChange={setFileTreeTabValue}
                 class="h-full"
                 data-scope="filetree"
               >
                 <Tabs.List>
                   <Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
-                    {props.reviewCount}{" "}
-                    {props.language.t(
-                      props.reviewCount === 1 ? "session.review.change.one" : "session.review.change.other",
-                    )}
+                    {reviewCount()}{" "}
+                    {language.t(reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other")}
                   </Tabs.Trigger>
                   <Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
-                    {props.language.t("session.files.all")}
+                    {language.t("session.files.all")}
                   </Tabs.Trigger>
                 </Tabs.List>
                 <Tabs.Content value="changes" class="bg-background-base px-3 py-0">
                   <Switch>
-                    <Match when={props.hasReview}>
+                    <Match when={hasReview()}>
                       <Show
-                        when={props.diffsReady}
+                        when={diffsReady()}
                         fallback={
                           <div class="px-2 py-2 text-12-regular text-text-weak">
-                            {props.language.t("common.loading")}
-                            {props.language.t("common.loading.ellipsis")}
+                            {language.t("common.loading")}
+                            {language.t("common.loading.ellipsis")}
                           </div>
                         }
                       >
                         <FileTree
                           path=""
-                          allowed={props.diffFiles}
-                          kinds={props.kinds}
+                          allowed={diffFiles()}
+                          kinds={kinds()}
                           draggable={false}
                           active={props.activeDiff}
                           onFileClick={(node) => props.focusReviewDiff(node.path)}
@@ -292,7 +379,7 @@ export function SessionSidePanel(props: {
                     </Match>
                     <Match when={true}>
                       <div class="mt-8 text-center text-12-regular text-text-weak">
-                        {props.language.t("session.review.noChanges")}
+                        {language.t("session.review.noChanges")}
                       </div>
                     </Match>
                   </Switch>
@@ -300,9 +387,9 @@ export function SessionSidePanel(props: {
                 <Tabs.Content value="all" class="bg-background-base px-3 py-0">
                   <FileTree
                     path=""
-                    modified={props.diffFiles}
-                    kinds={props.kinds}
-                    onFileClick={(node) => props.openTab(props.file.tab(node.path))}
+                    modified={diffFiles()}
+                    kinds={kinds()}
+                    onFileClick={(node) => openTab(file.tab(node.path))}
                   />
                 </Tabs.Content>
               </Tabs>
@@ -310,12 +397,12 @@ export function SessionSidePanel(props: {
             <ResizeHandle
               direction="horizontal"
               edge="start"
-              size={props.layout.fileTree.width()}
+              size={layout.fileTree.width()}
               min={200}
               max={480}
               collapseThreshold={160}
-              onResize={props.layout.fileTree.resize}
-              onCollapse={props.layout.fileTree.close}
+              onResize={layout.fileTree.resize}
+              onCollapse={layout.fileTree.close}
             />
           </div>
         </Show>

+ 157 - 78
packages/app/src/pages/session/terminal-panel.tsx

@@ -1,61 +1,161 @@
-import { For, Show, createMemo } from "solid-js"
+import { For, Show, createEffect, createMemo, on } from "solid-js"
+import { createStore } from "solid-js/store"
+import { createMediaQuery } from "@solid-primitives/media"
+import { useParams } from "@solidjs/router"
 import { Tabs } from "@opencode-ai/ui/tabs"
 import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
 import { IconButton } from "@opencode-ai/ui/icon-button"
 import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
 import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
 import type { DragEvent } from "@thisbeyond/solid-dnd"
-import { ConstrainDragYAxis } from "@/utils/solid-dnd"
+import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
+
 import { SortableTerminalTab } from "@/components/session"
 import { Terminal } from "@/components/terminal"
-import { useTerminal } from "@/context/terminal"
-import { useLanguage } from "@/context/language"
 import { useCommand } from "@/context/command"
+import { useLanguage } from "@/context/language"
+import { useLayout } from "@/context/layout"
+import { useTerminal, type LocalPTY } from "@/context/terminal"
 import { terminalTabLabel } from "@/pages/session/terminal-label"
+import { focusTerminalById } from "@/pages/session/helpers"
+import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff"
+
+export function TerminalPanel() {
+  const params = useParams()
+  const layout = useLayout()
+  const terminal = useTerminal()
+  const language = useLanguage()
+  const command = useCommand()
+
+  const isDesktop = createMediaQuery("(min-width: 768px)")
+  const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
+  const view = createMemo(() => layout.view(sessionKey))
+
+  const opened = createMemo(() => view().terminal.opened())
+  const open = createMemo(() => isDesktop() && opened())
+  const height = createMemo(() => layout.terminal.height())
+  const close = () => view().terminal.close()
+
+  const [store, setStore] = createStore({
+    autoCreated: false,
+    activeDraggable: undefined as string | undefined,
+  })
+
+  createEffect(() => {
+    if (!opened()) {
+      setStore("autoCreated", false)
+      return
+    }
+
+    if (!terminal.ready() || terminal.all().length !== 0 || store.autoCreated) return
+    terminal.new()
+    setStore("autoCreated", true)
+  })
+
+  createEffect(
+    on(
+      () => terminal.all().length,
+      (count, prevCount) => {
+        if (prevCount !== undefined && prevCount > 0 && count === 0) {
+          if (opened()) view().terminal.toggle()
+        }
+      },
+    ),
+  )
 
-export function TerminalPanel(props: {
-  open: boolean
-  height: number
-  resize: (value: number) => void
-  close: () => void
-  terminal: ReturnType<typeof useTerminal>
-  language: ReturnType<typeof useLanguage>
-  command: ReturnType<typeof useCommand>
-  handoff: () => string[]
-  activeTerminalDraggable: () => string | undefined
-  handleTerminalDragStart: (event: unknown) => void
-  handleTerminalDragOver: (event: DragEvent) => void
-  handleTerminalDragEnd: () => void
-  onCloseTab: () => void
-}) {
-  const all = createMemo(() => props.terminal.all())
+  createEffect(
+    on(
+      () => terminal.active(),
+      (activeId) => {
+        if (!activeId || !opened()) return
+        if (document.activeElement instanceof HTMLElement) {
+          document.activeElement.blur()
+        }
+        focusTerminalById(activeId)
+      },
+    ),
+  )
+
+  createEffect(() => {
+    const dir = params.dir
+    if (!dir) return
+    if (!terminal.ready()) return
+    language.locale()
+
+    setTerminalHandoff(
+      dir,
+      terminal.all().map((pty) =>
+        terminalTabLabel({
+          title: pty.title,
+          titleNumber: pty.titleNumber,
+          t: language.t as (key: string, vars?: Record<string, string | number | boolean>) => string,
+        }),
+      ),
+    )
+  })
+
+  const handoff = createMemo(() => {
+    const dir = params.dir
+    if (!dir) return []
+    return getTerminalHandoff(dir) ?? []
+  })
+
+  const all = createMemo(() => terminal.all())
   const ids = createMemo(() => all().map((pty) => pty.id))
   const byId = createMemo(() => new Map(all().map((pty) => [pty.id, pty])))
 
+  const handleTerminalDragStart = (event: unknown) => {
+    const id = getDraggableId(event)
+    if (!id) return
+    setStore("activeDraggable", id)
+  }
+
+  const handleTerminalDragOver = (event: DragEvent) => {
+    const { draggable, droppable } = event
+    if (!draggable || !droppable) return
+
+    const terminals = terminal.all()
+    const fromIndex = terminals.findIndex((t: LocalPTY) => t.id === draggable.id.toString())
+    const toIndex = terminals.findIndex((t: LocalPTY) => t.id === droppable.id.toString())
+    if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
+      terminal.move(draggable.id.toString(), toIndex)
+    }
+  }
+
+  const handleTerminalDragEnd = () => {
+    setStore("activeDraggable", undefined)
+
+    const activeId = terminal.active()
+    if (!activeId) return
+    setTimeout(() => {
+      focusTerminalById(activeId)
+    }, 0)
+  }
+
   return (
-    <Show when={props.open}>
+    <Show when={open()}>
       <div
         id="terminal-panel"
         role="region"
-        aria-label={props.language.t("terminal.title")}
+        aria-label={language.t("terminal.title")}
         class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base"
-        style={{ height: `${props.height}px` }}
+        style={{ height: `${height()}px` }}
       >
         <ResizeHandle
           direction="vertical"
-          size={props.height}
+          size={height()}
           min={100}
           max={typeof window === "undefined" ? 1000 : window.innerHeight * 0.6}
           collapseThreshold={50}
-          onResize={props.resize}
-          onCollapse={props.close}
+          onResize={layout.terminal.resize}
+          onCollapse={close}
         />
         <Show
-          when={props.terminal.ready()}
+          when={terminal.ready()}
           fallback={
             <div class="flex flex-col h-full pointer-events-none">
               <div class="h-10 flex items-center gap-2 px-2 border-b border-border-weak-base bg-background-stronger overflow-hidden">
-                <For each={props.handoff()}>
+                <For each={handoff()}>
                   {(title) => (
                     <div class="px-2 py-1 rounded-md bg-surface-base text-14-regular text-text-weak truncate max-w-40">
                       {title}
@@ -64,20 +164,18 @@ export function TerminalPanel(props: {
                 </For>
                 <div class="flex-1" />
                 <div class="text-text-weak pr-2">
-                  {props.language.t("common.loading")}
-                  {props.language.t("common.loading.ellipsis")}
+                  {language.t("common.loading")}
+                  {language.t("common.loading.ellipsis")}
                 </div>
               </div>
-              <div class="flex-1 flex items-center justify-center text-text-weak">
-                {props.language.t("terminal.loading")}
-              </div>
+              <div class="flex-1 flex items-center justify-center text-text-weak">{language.t("terminal.loading")}</div>
             </div>
           }
         >
           <DragDropProvider
-            onDragStart={props.handleTerminalDragStart}
-            onDragEnd={props.handleTerminalDragEnd}
-            onDragOver={props.handleTerminalDragOver}
+            onDragStart={handleTerminalDragStart}
+            onDragEnd={handleTerminalDragEnd}
+            onDragOver={handleTerminalDragOver}
             collisionDetector={closestCenter}
           >
             <DragDropSensors />
@@ -85,36 +183,26 @@ export function TerminalPanel(props: {
             <div class="flex flex-col h-full">
               <Tabs
                 variant="alt"
-                value={props.terminal.active()}
-                onChange={(id) => props.terminal.open(id)}
+                value={terminal.active()}
+                onChange={(id) => terminal.open(id)}
                 class="!h-auto !flex-none"
               >
                 <Tabs.List class="h-10">
                   <SortableProvider ids={ids()}>
-                    <For each={all()}>
-                      {(pty) => (
-                        <SortableTerminalTab
-                          terminal={pty}
-                          onClose={() => {
-                            props.close()
-                            props.onCloseTab()
-                          }}
-                        />
-                      )}
-                    </For>
+                    <For each={all()}>{(pty) => <SortableTerminalTab terminal={pty} onClose={close} />}</For>
                   </SortableProvider>
                   <div class="h-full flex items-center justify-center">
                     <TooltipKeybind
-                      title={props.language.t("command.terminal.new")}
-                      keybind={props.command.keybind("terminal.new")}
+                      title={language.t("command.terminal.new")}
+                      keybind={command.keybind("terminal.new")}
                       class="flex items-center"
                     >
                       <IconButton
                         icon="plus-small"
                         variant="ghost"
                         iconSize="large"
-                        onClick={props.terminal.new}
-                        aria-label={props.language.t("command.terminal.new")}
+                        onClick={terminal.new}
+                        aria-label={language.t("command.terminal.new")}
                       />
                     </TooltipKeybind>
                   </div>
@@ -127,15 +215,11 @@ export function TerminalPanel(props: {
                       id={`terminal-wrapper-${pty.id}`}
                       class="absolute inset-0"
                       style={{
-                        display: props.terminal.active() === pty.id ? "block" : "none",
+                        display: terminal.active() === pty.id ? "block" : "none",
                       }}
                     >
                       <Show when={pty.id} keyed>
-                        <Terminal
-                          pty={pty}
-                          onCleanup={props.terminal.update}
-                          onConnectError={() => props.terminal.clone(pty.id)}
-                        />
+                        <Terminal pty={pty} onCleanup={terminal.update} onConnectError={() => terminal.clone(pty.id)} />
                       </Show>
                     </div>
                   )}
@@ -143,25 +227,20 @@ export function TerminalPanel(props: {
               </div>
             </div>
             <DragOverlay>
-              <Show when={props.activeTerminalDraggable()}>
-                {(draggedId) => {
-                  return (
-                    <Show when={byId().get(draggedId())}>
-                      {(t) => (
-                        <div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
-                          {terminalTabLabel({
-                            title: t().title,
-                            titleNumber: t().titleNumber,
-                            t: props.language.t as (
-                              key: string,
-                              vars?: Record<string, string | number | boolean>,
-                            ) => string,
-                          })}
-                        </div>
-                      )}
-                    </Show>
-                  )
-                }}
+              <Show when={store.activeDraggable}>
+                {(draggedId) => (
+                  <Show when={byId().get(draggedId())}>
+                    {(t) => (
+                      <div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
+                        {terminalTabLabel({
+                          title: t().title,
+                          titleNumber: t().titleNumber,
+                          t: language.t as (key: string, vars?: Record<string, string | number | boolean>) => string,
+                        })}
+                      </div>
+                    )}
+                  </Show>
+                )}
               </Show>
             </DragOverlay>
           </DragDropProvider>