Kaynağa Gözat

feat(app): persist todo dock collapsed state (#26953)

Brendan Allan 3 ay önce
ebeveyn
işleme
fe374aea46

+ 13 - 0
packages/app/src/context/layout.tsx

@@ -43,6 +43,7 @@ type SessionView = {
   reviewOpen?: string[]
   reviewOpen?: string[]
   pendingMessage?: string
   pendingMessage?: string
   pendingMessageAt?: number
   pendingMessageAt?: number
+  todoCollapsed?: boolean
 }
 }
 
 
 type TabHandoff = {
 type TabHandoff = {
@@ -759,6 +760,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
           setScroll(tab: string, pos: SessionScroll) {
           setScroll(tab: string, pos: SessionScroll) {
             scroll.setScroll(key(), tab, pos)
             scroll.setScroll(key(), tab, pos)
           },
           },
+          todoCollapsed: {
+            get: () => s().todoCollapsed ?? false,
+            set(collapsed: boolean) {
+              const session = key()
+              const current = store.sessionView[session]
+              if (!current) {
+                setStore("sessionView", session, { scroll: {}, todoCollapsed: collapsed })
+              } else {
+                setStore("sessionView", session, "todoCollapsed", collapsed)
+              }
+            },
+          },
           terminal: {
           terminal: {
             opened: terminalOpened,
             opened: terminalOpened,
             open() {
             open() {

+ 5 - 0
packages/app/src/pages/session/composer/session-composer-region.tsx

@@ -2,6 +2,7 @@ import { Show, createEffect, createMemo, onCleanup } from "solid-js"
 import { createStore } from "solid-js/store"
 import { createStore } from "solid-js/store"
 import { useNavigate } from "@solidjs/router"
 import { useNavigate } from "@solidjs/router"
 import { useSpring } from "@opencode-ai/ui/motion-spring"
 import { useSpring } from "@opencode-ai/ui/motion-spring"
+import { useLayout } from "@/context/layout"
 import { PromptInput } from "@/components/prompt-input"
 import { PromptInput } from "@/components/prompt-input"
 import { useLanguage } from "@/context/language"
 import { useLanguage } from "@/context/language"
 import { usePrompt } from "@/context/prompt"
 import { usePrompt } from "@/context/prompt"
@@ -46,10 +47,12 @@ export function SessionComposerRegion(props: {
   setPromptDockRef: (el: HTMLDivElement) => void
   setPromptDockRef: (el: HTMLDivElement) => void
 }) {
 }) {
   const navigate = useNavigate()
   const navigate = useNavigate()
+  const layout = useLayout()
   const prompt = usePrompt()
   const prompt = usePrompt()
   const language = useLanguage()
   const language = useLanguage()
   const route = useSessionKey()
   const route = useSessionKey()
   const sync = useSync()
   const sync = useSync()
+  const view = layout.view(route.sessionKey)
 
 
   const handoffPrompt = createMemo(() => getSessionHandoff(route.sessionKey())?.prompt)
   const handoffPrompt = createMemo(() => getSessionHandoff(route.sessionKey())?.prompt)
   const info = createMemo(() => (route.params.id ? sync.session.get(route.params.id) : undefined))
   const info = createMemo(() => (route.params.id ? sync.session.get(route.params.id) : undefined))
@@ -207,6 +210,8 @@ export function SessionComposerRegion(props: {
                   <SessionTodoDock
                   <SessionTodoDock
                     sessionID={route.params.id}
                     sessionID={route.params.id}
                     todos={props.state.todos()}
                     todos={props.state.todos()}
+                    collapsed={view.todoCollapsed.get()}
+                    onToggle={() => view.todoCollapsed.set(!view.todoCollapsed.get())}
                     collapseLabel={language.t("session.todo.collapse")}
                     collapseLabel={language.t("session.todo.collapse")}
                     expandLabel={language.t("session.todo.expand")}
                     expandLabel={language.t("session.todo.expand")}
                     dockProgress={value()}
                     dockProgress={value()}

+ 10 - 11
packages/app/src/pages/session/composer/session-todo-dock.tsx

@@ -42,18 +42,17 @@ function dot(status: Todo["status"]) {
 export function SessionTodoDock(props: {
 export function SessionTodoDock(props: {
   sessionID?: string
   sessionID?: string
   todos: Todo[]
   todos: Todo[]
+  collapsed: boolean
+  onToggle: () => void
   collapseLabel: string
   collapseLabel: string
   expandLabel: string
   expandLabel: string
   dockProgress: number
   dockProgress: number
 }) {
 }) {
   const language = useLanguage()
   const language = useLanguage()
   const [store, setStore] = createStore({
   const [store, setStore] = createStore({
-    collapsed: false,
     height: 320,
     height: 320,
   })
   })
 
 
-  const toggle = () => setStore("collapsed", (value) => !value)
-
   const total = createMemo(() => props.todos.length)
   const total = createMemo(() => props.todos.length)
   const done = createMemo(() => props.todos.filter((todo) => todo.status === "completed").length)
   const done = createMemo(() => props.todos.filter((todo) => todo.status === "completed").length)
   const label = createMemo(() => language.t("session.todo.progress", { done: done(), total: total() }))
   const label = createMemo(() => language.t("session.todo.progress", { done: done(), total: total() }))
@@ -72,7 +71,7 @@ export function SessionTodoDock(props: {
   )
   )
 
 
   const preview = createMemo(() => active()?.content ?? "")
   const preview = createMemo(() => active()?.content ?? "")
-  const collapse = useSpring(() => (store.collapsed ? 1 : 0), { visualDuration: 0.3, bounce: 0 })
+  const collapse = useSpring(() => (props.collapsed ? 1 : 0), { visualDuration: 0.3, bounce: 0 })
   const dock = createMemo(() => Math.max(0, Math.min(1, props.dockProgress)))
   const dock = createMemo(() => Math.max(0, Math.min(1, props.dockProgress)))
   const shut = createMemo(() => 1 - dock())
   const shut = createMemo(() => 1 - dock())
   const value = createMemo(() => Math.max(0, Math.min(1, collapse())))
   const value = createMemo(() => Math.max(0, Math.min(1, collapse())))
@@ -107,11 +106,11 @@ export function SessionTodoDock(props: {
           class="pl-3 pr-2 py-2 flex items-center gap-2 overflow-visible"
           class="pl-3 pr-2 py-2 flex items-center gap-2 overflow-visible"
           role="button"
           role="button"
           tabIndex={0}
           tabIndex={0}
-          onClick={toggle}
+          onClick={props.onToggle}
           onKeyDown={(event) => {
           onKeyDown={(event) => {
             if (event.key !== "Enter" && event.key !== " ") return
             if (event.key !== "Enter" && event.key !== " ") return
             event.preventDefault()
             event.preventDefault()
-            toggle()
+            props.onToggle()
           }}
           }}
         >
         >
           <span
           <span
@@ -148,7 +147,7 @@ export function SessionTodoDock(props: {
           >
           >
             <TextReveal
             <TextReveal
               class="text-14-regular text-text-base cursor-default"
               class="text-14-regular text-text-base cursor-default"
-              text={store.collapsed ? preview() : undefined}
+              text={props.collapsed ? preview() : undefined}
               duration={600}
               duration={600}
               travel={25}
               travel={25}
               edge={17}
               edge={17}
@@ -161,7 +160,7 @@ export function SessionTodoDock(props: {
           <div class="ml-auto">
           <div class="ml-auto">
             <IconButton
             <IconButton
               data-action="session-todo-toggle-button"
               data-action="session-todo-toggle-button"
-              data-collapsed={store.collapsed ? "true" : "false"}
+              data-collapsed={props.collapsed ? "true" : "false"}
               icon="chevron-down"
               icon="chevron-down"
               size="normal"
               size="normal"
               variant="ghost"
               variant="ghost"
@@ -172,16 +171,16 @@ export function SessionTodoDock(props: {
               }}
               }}
               onClick={(event) => {
               onClick={(event) => {
                 event.stopPropagation()
                 event.stopPropagation()
-                toggle()
+                props.onToggle()
               }}
               }}
-              aria-label={store.collapsed ? props.expandLabel : props.collapseLabel}
+              aria-label={props.collapsed ? props.expandLabel : props.collapseLabel}
             />
             />
           </div>
           </div>
         </div>
         </div>
 
 
         <div
         <div
           data-slot="session-todo-list"
           data-slot="session-todo-list"
-          aria-hidden={store.collapsed || off()}
+          aria-hidden={props.collapsed || off()}
           classList={{
           classList={{
             "pointer-events-none": hide() > 0.1,
             "pointer-events-none": hide() > 0.1,
           }}
           }}