Skip to content

Commit c37f7b9

Browse files
committed
fix(app): todos not clearing
1 parent cf7ca9b commit c37f7b9

3 files changed

Lines changed: 76 additions & 15 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const todoState = (input: {
2+
count: number
3+
done: boolean
4+
live: boolean
5+
}): "hide" | "clear" | "open" | "close" => {
6+
if (input.count === 0) return "hide"
7+
if (!input.live) return "clear"
8+
if (!input.done) return "open"
9+
return "close"
10+
}

packages/app/src/pages/session/composer/session-composer-state.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, test } from "bun:test"
22
import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
3+
import { todoState } from "./session-composer-helpers"
34
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
45

56
const session = (input: { id: string; parentID?: string }) =>
@@ -103,3 +104,25 @@ describe("sessionQuestionRequest", () => {
103104
expect(sessionQuestionRequest(sessions, questions, "root")?.id).toBe("q-grand")
104105
})
105106
})
107+
108+
describe("todoState", () => {
109+
test("hides when there are no todos", () => {
110+
expect(todoState({ count: 0, done: false, live: true })).toBe("hide")
111+
})
112+
113+
test("opens while the session is still working", () => {
114+
expect(todoState({ count: 2, done: false, live: true })).toBe("open")
115+
})
116+
117+
test("closes completed todos after a running turn", () => {
118+
expect(todoState({ count: 2, done: true, live: true })).toBe("close")
119+
})
120+
121+
test("clears stale todos when the turn ends", () => {
122+
expect(todoState({ count: 2, done: false, live: false })).toBe("clear")
123+
})
124+
125+
test("clears completed todos when the session is no longer live", () => {
126+
expect(todoState({ count: 2, done: true, live: false })).toBe("clear")
127+
})
128+
})

packages/app/src/pages/session/composer/session-composer-state.ts

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ import { useLanguage } from "@/context/language"
88
import { usePermission } from "@/context/permission"
99
import { useSDK } from "@/context/sdk"
1010
import { useSync } from "@/context/sync"
11+
import { todoState } from "./session-composer-helpers"
1112
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
1213

14+
const idle = { type: "idle" as const }
15+
1316
export function createSessionComposerBlocked() {
1417
const params = useParams()
1518
const permission = usePermission()
@@ -59,9 +62,22 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
5962
return globalSync.data.session_todo[id] ?? []
6063
})
6164

65+
const done = createMemo(
66+
() => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"),
67+
)
68+
69+
const status = createMemo(() => {
70+
const id = params.id
71+
if (!id) return idle
72+
return sync.data.session_status[id] ?? idle
73+
})
74+
75+
const busy = createMemo(() => status().type !== "idle")
76+
const live = createMemo(() => busy() || blocked())
77+
6278
const [store, setStore] = createStore({
6379
responding: undefined as string | undefined,
64-
dock: todos().length > 0,
80+
dock: todos().length > 0 && live(),
6581
closing: false,
6682
opening: false,
6783
})
@@ -89,10 +105,6 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
89105
})
90106
}
91107

92-
const done = createMemo(
93-
() => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"),
94-
)
95-
96108
let timer: number | undefined
97109
let raf: number | undefined
98110

@@ -111,21 +123,42 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
111123
}, closeMs())
112124
}
113125

126+
// Keep stale turn todos from reopening if the model never clears them.
127+
const clear = () => {
128+
const id = params.id
129+
if (!id) return
130+
globalSync.todo.set(id, [])
131+
sync.set("todo", id, [])
132+
}
133+
114134
createEffect(
115135
on(
116-
() => [todos().length, done()] as const,
117-
([count, complete], prev) => {
136+
() => [todos().length, done(), live()] as const,
137+
([count, complete, active]) => {
118138
if (raf) cancelAnimationFrame(raf)
119139
raf = undefined
120140

121-
if (count === 0) {
141+
const next = todoState({
142+
count,
143+
done: complete,
144+
live: active,
145+
})
146+
147+
if (next === "hide") {
122148
if (timer) window.clearTimeout(timer)
123149
timer = undefined
124150
setStore({ dock: false, closing: false, opening: false })
125151
return
126152
}
127153

128-
if (!complete) {
154+
if (next === "clear") {
155+
if (timer) window.clearTimeout(timer)
156+
timer = undefined
157+
clear()
158+
return
159+
}
160+
161+
if (next === "open") {
129162
if (timer) window.clearTimeout(timer)
130163
timer = undefined
131164
const hidden = !store.dock || store.closing
@@ -142,13 +175,8 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
142175
return
143176
}
144177

145-
if (prev && prev[1]) {
146-
if (store.closing && !timer) scheduleClose()
147-
return
148-
}
149-
150178
setStore({ dock: true, opening: false, closing: true })
151-
scheduleClose()
179+
if (!timer) scheduleClose()
152180
},
153181
),
154182
)

0 commit comments

Comments
 (0)