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

This commit is contained in:
Brendan Allan 2026-05-12 07:59:38 +08:00 committed by GitHub
commit fe374aea46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 28 additions and 11 deletions

View file

@ -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() {

View file

@ -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()}

View file

@ -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,
}} }}