From d81d63045ac619bc9201df4ae2e003a2d08596d1 Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Mon, 15 Dec 2025 05:18:39 -0600
Subject: wip(desktop): session turn state consolidation
---
packages/ui/src/components/session-turn.tsx | 620 ++++++++++++++--------------
1 file changed, 303 insertions(+), 317 deletions(-)
diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx
index ad2e6c36e..e6654f480 100644
--- a/packages/ui/src/components/session-turn.tsx
+++ b/packages/ui/src/components/session-turn.tsx
@@ -40,6 +40,9 @@ export function SessionTurn(
.sort((a, b) => a.id.localeCompare(b.id)),
)
const message = createMemo(() => userMessages()?.find((m) => m.id === props.messageID))
+
+ if (!message()) return null
+
const status = createMemo(
() =>
data.store.session_status[props.sessionID] ?? {
@@ -49,379 +52,362 @@ export function SessionTurn(
const working = createMemo(() => status()?.type !== "idle")
let scrollRef: HTMLDivElement | undefined
- const [state, setState] = createStore({
+
+ const assistantMessages = createMemo(() => {
+ return messages()?.filter((m) => m.role === "assistant" && m.parentID == message()!.id) as AssistantMessage[]
+ })
+ const lastAssistantMessage = createMemo(() => assistantMessages()?.at(-1))
+ const assistantMessageParts = createMemo(() => assistantMessages()?.flatMap((m) => data.store.part[m.id]))
+ const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
+ const parts = createMemo(() => data.store.part[message()!.id])
+ const lastTextPart = createMemo(() =>
+ assistantMessageParts()
+ .filter((p) => p?.type === "text")
+ ?.at(-1),
+ )
+ const summary = createMemo(() => message()!.summary?.body ?? lastTextPart()?.text)
+ const lastTextPartShown = createMemo(() => !message()!.summary?.body && (lastTextPart()?.text?.length ?? 0) > 0)
+
+ const assistantParts = createMemo(() => assistantMessages().flatMap((m) => data.store.part[m.id]))
+ const currentTask = createMemo(
+ () =>
+ assistantParts().findLast(
+ (p) =>
+ p &&
+ p.type === "tool" &&
+ p.tool === "task" &&
+ p.state &&
+ "metadata" in p.state &&
+ p.state.metadata &&
+ p.state.metadata.sessionId &&
+ p.state.status === "running",
+ ) as ToolPart,
+ )
+ const resolvedParts = createMemo(() => {
+ let resolved = assistantParts()
+ const task = currentTask()
+ if (task && task.state && "metadata" in task.state && task.state.metadata?.sessionId) {
+ const msgs = data.store.message[task.state.metadata.sessionId as string]?.filter((m) => m.role === "assistant")
+ resolved = msgs?.flatMap((m) => data.store.part[m.id]) ?? assistantParts()
+ }
+ return resolved
+ })
+ const lastPart = createMemo(() => resolvedParts().slice(-1)?.at(0))
+ const rawStatus = createMemo(() => {
+ const last = lastPart()
+ if (!last) return undefined
+
+ if (last.type === "tool") {
+ switch (last.tool) {
+ case "task":
+ return "Delegating work"
+ case "todowrite":
+ case "todoread":
+ return "Planning next steps"
+ case "read":
+ return "Gathering context"
+ case "list":
+ case "grep":
+ case "glob":
+ return "Searching the codebase"
+ case "webfetch":
+ return "Searching the web"
+ case "edit":
+ case "write":
+ return "Making edits"
+ case "bash":
+ return "Running commands"
+ default:
+ break
+ }
+ } else if (last.type === "reasoning") {
+ const text = last.text ?? ""
+ const match = text.trimStart().match(/^\*\*(.+?)\*\*/)
+ if (match) return `Thinking · ${match[1].trim()}`
+ return "Thinking"
+ } else if (last.type === "text") {
+ return "Gathering thoughts"
+ }
+ return undefined
+ })
+
+ function duration() {
+ const completed = lastAssistantMessage()?.time.completed
+ const from = DateTime.fromMillis(message()!.time.created)
+ const to = completed ? DateTime.fromMillis(completed) : DateTime.now()
+ const interval = Interval.fromDateTimes(from, to)
+ const unit: DurationUnit[] = interval.length("seconds") > 60 ? ["minutes", "seconds"] : ["seconds"]
+
+ return interval.toDuration(unit).normalize().toHuman({
+ notation: "compact",
+ unitDisplay: "narrow",
+ compactDisplay: "short",
+ showZeros: false,
+ })
+ }
+
+ const [store, setStore] = createStore({
stickyTitleRef: undefined as HTMLDivElement | undefined,
stickyTriggerRef: undefined as HTMLDivElement | undefined,
userScrolled: false,
stickyHeaderHeight: 0,
scrollY: 0,
autoScrolling: false,
+ status: rawStatus(),
+ stepsExpanded: true,
+ duration: duration(),
+ lastStatusChange: Date.now(),
+ statusTimeout: undefined as number | undefined,
})
function handleScroll() {
if (!scrollRef) return
// prevents scroll loops
if (working() && scrollRef.scrollTop < 100) return
- setState("scrollY", scrollRef.scrollTop)
- if (state.autoScrolling) return
+ setStore("scrollY", scrollRef.scrollTop)
+ if (store.autoScrolling) return
const { scrollTop, scrollHeight, clientHeight } = scrollRef
const atBottom = scrollHeight - scrollTop - clientHeight < 50
if (!atBottom && working()) {
- setState("userScrolled", true)
+ setStore("userScrolled", true)
}
}
function handleInteraction() {
if (working()) {
- setState("userScrolled", true)
+ setStore("userScrolled", true)
}
}
function scrollToBottom() {
- if (!scrollRef || state.userScrolled || !working() || state.autoScrolling) return
- setState("autoScrolling", true)
+ if (!scrollRef || store.userScrolled || !working() || store.autoScrolling) return
+ setStore("autoScrolling", true)
requestAnimationFrame(() => {
scrollRef?.scrollTo({ top: scrollRef.scrollHeight, behavior: "instant" })
requestAnimationFrame(() => {
- setState("autoScrolling", false)
+ setStore("autoScrolling", false)
})
})
}
createEffect(() => {
if (!working()) {
- setState("userScrolled", false)
+ setStore("userScrolled", false)
}
})
createResizeObserver(
- () => state.stickyTitleRef,
+ () => store.stickyTitleRef,
({ height }) => {
- const triggerHeight = state.stickyTriggerRef?.offsetHeight ?? 0
- setState("stickyHeaderHeight", height + triggerHeight + 8)
+ const triggerHeight = store.stickyTriggerRef?.offsetHeight ?? 0
+ setStore("stickyHeaderHeight", height + triggerHeight + 8)
},
)
createResizeObserver(
- () => state.stickyTriggerRef,
+ () => store.stickyTriggerRef,
({ height }) => {
- const titleHeight = state.stickyTitleRef?.offsetHeight ?? 0
- setState("stickyHeaderHeight", titleHeight + height + 8)
+ const titleHeight = store.stickyTitleRef?.offsetHeight ?? 0
+ setStore("stickyHeaderHeight", titleHeight + height + 8)
},
)
- return (
-
-
-
-
- {(message) => {
- const assistantMessages = createMemo(() => {
- return messages()?.filter(
- (m) => m.role === "assistant" && m.parentID == message().id,
- ) as AssistantMessage[]
- })
- const lastAssistantMessage = createMemo(() => assistantMessages()?.at(-1))
- const assistantMessageParts = createMemo(() => assistantMessages()?.flatMap((m) => data.store.part[m.id]))
- const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
- const parts = createMemo(() => data.store.part[message().id])
- const lastTextPart = createMemo(() =>
- assistantMessageParts()
- .filter((p) => p?.type === "text")
- ?.at(-1),
- )
- const summary = createMemo(() => message().summary?.body ?? lastTextPart()?.text)
- const lastTextPartShown = createMemo(
- () => !message().summary?.body && (lastTextPart()?.text?.length ?? 0) > 0,
- )
-
- const assistantParts = createMemo(() => assistantMessages().flatMap((m) => data.store.part[m.id]))
- const currentTask = createMemo(
- () =>
- assistantParts().findLast(
- (p) =>
- p &&
- p.type === "tool" &&
- p.tool === "task" &&
- p.state &&
- "metadata" in p.state &&
- p.state.metadata &&
- p.state.metadata.sessionId &&
- p.state.status === "running",
- ) as ToolPart,
- )
- const resolvedParts = createMemo(() => {
- let resolved = assistantParts()
- const task = currentTask()
- if (task && task.state && "metadata" in task.state && task.state.metadata?.sessionId) {
- const messages = data.store.message[task.state.metadata.sessionId as string]?.filter(
- (m) => m.role === "assistant",
- )
- resolved = messages?.flatMap((m) => data.store.part[m.id]) ?? assistantParts()
- }
- return resolved
- })
- const lastPart = createMemo(() => resolvedParts().slice(-1)?.at(0))
- const rawStatus = createMemo(() => {
- const last = lastPart()
- if (!last) return undefined
-
- if (last.type === "tool") {
- switch (last.tool) {
- case "task":
- return "Delegating work"
- case "todowrite":
- case "todoread":
- return "Planning next steps"
- case "read":
- return "Gathering context"
- case "list":
- case "grep":
- case "glob":
- return "Searching the codebase"
- case "webfetch":
- return "Searching the web"
- case "edit":
- case "write":
- return "Making edits"
- case "bash":
- return "Running commands"
- default:
- break
- }
- } else if (last.type === "reasoning") {
- const text = last.text ?? ""
- const match = text.trimStart().match(/^\*\*(.+?)\*\*/)
- if (match) return `Thinking · ${match[1].trim()}`
- return "Thinking"
- } else if (last.type === "text") {
- return "Gathering thoughts"
- }
- return undefined
- })
-
- function duration() {
- const completed = lastAssistantMessage()?.time.completed
- const from = DateTime.fromMillis(message()!.time.created)
- const to = completed ? DateTime.fromMillis(completed) : DateTime.now()
- const interval = Interval.fromDateTimes(from, to)
- const unit: DurationUnit[] = interval.length("seconds") > 60 ? ["minutes", "seconds"] : ["seconds"]
-
- return interval.toDuration(unit).normalize().toHuman({
- notation: "compact",
- unitDisplay: "narrow",
- compactDisplay: "short",
- showZeros: false,
- })
- }
-
- createEffect(() => {
- lastPart()
- scrollToBottom()
- })
-
- const [store, setStore] = createStore({
- status: rawStatus(),
- stepsExpanded: true,
- duration: duration(),
- })
+ createEffect(() => {
+ lastPart()
+ scrollToBottom()
+ })
- createEffect(() => {
- const timer = setInterval(() => {
- setStore("duration", duration())
- }, 1000)
- onCleanup(() => clearInterval(timer))
- })
+ createEffect(() => {
+ const timer = setInterval(() => {
+ setStore("duration", duration())
+ }, 1000)
+ onCleanup(() => clearInterval(timer))
+ })
- let lastStatusChange = Date.now()
- let statusTimeout: number | undefined
- createEffect(() => {
- const newStatus = rawStatus()
- if (newStatus === store.status || !newStatus) return
+ createEffect(() => {
+ const newStatus = rawStatus()
+ if (newStatus === store.status || !newStatus) return
- const timeSinceLastChange = Date.now() - lastStatusChange
+ const timeSinceLastChange = Date.now() - store.lastStatusChange
- if (timeSinceLastChange >= 2500) {
- setStore("status", newStatus)
- lastStatusChange = Date.now()
- if (statusTimeout) {
- clearTimeout(statusTimeout)
- statusTimeout = undefined
- }
- } else {
- if (statusTimeout) clearTimeout(statusTimeout)
- statusTimeout = setTimeout(() => {
- setStore("status", rawStatus())
- lastStatusChange = Date.now()
- statusTimeout = undefined
- }, 2500 - timeSinceLastChange) as unknown as number
- }
- })
+ if (timeSinceLastChange >= 2500) {
+ setStore("status", newStatus)
+ setStore("lastStatusChange", Date.now())
+ if (store.statusTimeout) {
+ clearTimeout(store.statusTimeout)
+ setStore("statusTimeout", undefined)
+ }
+ } else {
+ if (store.statusTimeout) clearTimeout(store.statusTimeout)
+ setStore(
+ "statusTimeout",
+ setTimeout(() => {
+ setStore("status", rawStatus())
+ setStore("lastStatusChange", Date.now())
+ setStore("statusTimeout", undefined)
+ }, 2500 - timeSinceLastChange) as unknown as number,
+ )
+ }
+ })
- createEffect((prev) => {
- const isWorking = working()
- if (prev && !isWorking && !state.userScrolled) {
- setStore("stepsExpanded", false)
- }
- return isWorking
- }, working())
+ createEffect((prev) => {
+ const isWorking = working()
+ if (prev && !isWorking && !store.userScrolled) {
+ setStore("stepsExpanded", false)
+ }
+ return isWorking
+ }, working())
- return (
-
- {/* Title (sticky) */}
-
setState("stickyTitleRef", el)} data-slot="session-turn-sticky-title">
-
-
-
-
-
-
-
- {message().summary?.title}
-
-
-
-
-
- {/* User Message */}
-
-
-
- {/* Trigger (sticky) */}
-
setState("stickyTriggerRef", el)} data-slot="session-turn-response-trigger">
-