refactor(tui): extract session timeline state
This commit is contained in:
parent
daf8e539bf
commit
a653744d78
15 changed files with 1897 additions and 586 deletions
|
|
@ -8,7 +8,8 @@ import { createEffect, onMount, type ParentProps } from "solid-js"
|
|||
import { ClientProvider, useClient } from "../../../src/context/client"
|
||||
import { DataProvider as DataProviderBase, useData } from "../../../src/context/data"
|
||||
import { LocationProvider, useLocation } from "../../../src/context/location"
|
||||
import { createSessionRows, type SessionRow } from "../../../src/routes/session/rows"
|
||||
import { createSessionRows } from "../../../src/routes/session/rows"
|
||||
import type { SessionRow } from "../../../src/routes/session/timeline"
|
||||
import { createApi, createEventStream, createFetch, directory, json } from "../../fixture/tui-client"
|
||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client"
|
||||
import { messageBoundaryIDs, reduceSessionRows, type SessionRow } from "../../../src/routes/session/rows"
|
||||
import {
|
||||
SessionTimeline,
|
||||
messageBoundaryIDs,
|
||||
reduceSessionRows,
|
||||
type SessionRow,
|
||||
} from "../../../src/routes/session/timeline"
|
||||
|
||||
const withoutIDs = (rows: ReturnType<typeof reduceSessionRows>) =>
|
||||
rows.map(({ id: _id, ...row }) => {
|
||||
|
|
@ -317,6 +322,162 @@ test("places a running compaction barrier before every queued user message", ()
|
|||
])
|
||||
})
|
||||
|
||||
test("matches snapshot reduction through direct timeline operations", () => {
|
||||
const timeline = SessionTimeline.make()
|
||||
const response = assistant("assistant-1", [
|
||||
{ type: "reasoning", text: "First" },
|
||||
{ type: "reasoning", text: "Second" },
|
||||
{ type: "tool", id: "read-1", name: "read", state: pending(), time: { created: 2 } },
|
||||
{ type: "tool", id: "grep-1", name: "grep", state: pending(), time: { created: 3 } },
|
||||
{ type: "text", text: "Done" },
|
||||
])
|
||||
response.finish = "stop"
|
||||
const messages: SessionMessageInfo[] = [
|
||||
{ type: "user", id: "user-1", text: "Explore", time: { created: 0 } },
|
||||
response,
|
||||
{ type: "user", id: "user-queued", text: "Continue", time: { created: 4 } },
|
||||
]
|
||||
|
||||
timeline.appendMessage("user-1", { pending: false, compaction: false })
|
||||
timeline.appendPart({ messageID: "assistant-1", partID: "reasoning:0" }, { type: "reasoning" })
|
||||
timeline.appendPart({ messageID: "assistant-1", partID: "reasoning:1" }, { type: "reasoning" })
|
||||
timeline.appendPart({ messageID: "assistant-1", partID: "read-1" }, { type: "tool", name: "read" })
|
||||
timeline.appendPart({ messageID: "assistant-1", partID: "grep-1" }, { type: "tool", name: "grep" })
|
||||
timeline.appendPart({ messageID: "assistant-1", partID: "text:0" }, { type: "text" })
|
||||
timeline.appendFooter("assistant-1")
|
||||
timeline.appendMessage("user-queued", { pending: true, compaction: false })
|
||||
|
||||
expect(timeline.values()).toEqual(reduceSessionRows(messages, new Set(["user-queued"])))
|
||||
})
|
||||
|
||||
test("ignores a duplicate part through the parts membership index", () => {
|
||||
const timeline = SessionTimeline.make()
|
||||
const ref = { messageID: "assistant-1", partID: "read-1" }
|
||||
timeline.appendPart(ref, { type: "tool", name: "read" })
|
||||
const values = timeline.values()
|
||||
const slots = timeline.slots()
|
||||
|
||||
timeline.appendPart(ref, { type: "text" })
|
||||
|
||||
expect(timeline.values()).toBe(values)
|
||||
expect(timeline.slots()).toBe(slots)
|
||||
})
|
||||
|
||||
test("inserts output before the earliest queued compaction and prompt", () => {
|
||||
const timeline = SessionTimeline.make()
|
||||
timeline.appendPart({ messageID: "assistant-1", partID: "read-1" }, { type: "tool", name: "read" })
|
||||
timeline.appendMessage("user-1", { pending: true, compaction: false })
|
||||
timeline.appendMessage("user-2", { pending: true, compaction: false })
|
||||
timeline.appendMessage("compaction-1", { pending: true, compaction: true })
|
||||
timeline.appendPart({ messageID: "assistant-1", partID: "text:0" }, { type: "text" })
|
||||
|
||||
expect(withoutIDs([...timeline.values()])).toEqual([
|
||||
{
|
||||
type: "group",
|
||||
kind: "exploration",
|
||||
pending: [],
|
||||
completed: true,
|
||||
refs: [{ messageID: "assistant-1", partID: "read-1" }],
|
||||
},
|
||||
{ type: "part", ref: { messageID: "assistant-1", partID: "text:0" } },
|
||||
{ type: "message", messageID: "compaction-1" },
|
||||
{ type: "message", messageID: "user-1" },
|
||||
{ type: "message", messageID: "user-2" },
|
||||
])
|
||||
})
|
||||
|
||||
test("keeps a group slot stable through join, repartition, and completion", () => {
|
||||
const timeline = SessionTimeline.make()
|
||||
timeline.appendPart({ messageID: "assistant-1", partID: "read-1" }, { type: "tool", name: "read" })
|
||||
const slot = timeline.slots()[0]
|
||||
|
||||
timeline.appendPart({ messageID: "assistant-2", partID: "grep-1" }, { type: "tool", name: "grep" })
|
||||
expect(timeline.slots()[0]).toBe(slot)
|
||||
|
||||
timeline.repartition(new Set(["read-1"]))
|
||||
expect(timeline.slots()[0]).toBe(slot)
|
||||
expect(slot()).toMatchObject({
|
||||
refs: [{ messageID: "assistant-2", partID: "grep-1" }],
|
||||
pending: [{ messageID: "assistant-1", partID: "read-1" }],
|
||||
completed: false,
|
||||
})
|
||||
|
||||
timeline.appendMessage("user-queued", { pending: true, compaction: false })
|
||||
expect(slot()).toMatchObject({ completed: false })
|
||||
timeline.appendMessage("user-queued", { pending: false, compaction: false })
|
||||
|
||||
expect(timeline.slots()[0]).toBe(slot)
|
||||
expect(slot()).toMatchObject({ completed: true })
|
||||
})
|
||||
|
||||
test("does not complete an active group for duplicate messages or footers", () => {
|
||||
const timeline = SessionTimeline.make()
|
||||
timeline.appendMessage("user-1", { pending: false, compaction: false })
|
||||
timeline.appendPart({ messageID: "assistant-1", partID: "reasoning:0" }, { type: "reasoning" })
|
||||
const first = timeline.slots()[1]
|
||||
|
||||
timeline.appendMessage("user-1", { pending: false, compaction: false })
|
||||
expect(first()).toMatchObject({ completed: false })
|
||||
|
||||
timeline.appendFooter("assistant-1")
|
||||
timeline.appendPart({ messageID: "assistant-2", partID: "reasoning:0" }, { type: "reasoning" })
|
||||
const second = timeline.slots()[3]
|
||||
timeline.appendFooter("assistant-1")
|
||||
|
||||
expect(second()).toMatchObject({ completed: false })
|
||||
})
|
||||
|
||||
test("moves a promoted queued message before the remaining queue", () => {
|
||||
const timeline = SessionTimeline.make()
|
||||
timeline.appendPart({ messageID: "assistant-1", partID: "reasoning:0" }, { type: "reasoning" })
|
||||
timeline.appendMessage("user-1", { pending: true, compaction: false })
|
||||
timeline.appendMessage("user-2", { pending: true, compaction: false })
|
||||
|
||||
timeline.appendMessage("user-2", { pending: false, compaction: false })
|
||||
timeline.appendPart({ messageID: "assistant-2", partID: "text:0" }, { type: "text" })
|
||||
|
||||
expect(withoutIDs([...timeline.values()])).toEqual([
|
||||
{
|
||||
type: "group",
|
||||
kind: "reasoning",
|
||||
completed: true,
|
||||
refs: [{ messageID: "assistant-1", partID: "reasoning:0" }],
|
||||
},
|
||||
{ type: "message", messageID: "user-2" },
|
||||
{ type: "part", ref: { messageID: "assistant-2", partID: "text:0" } },
|
||||
{ type: "message", messageID: "user-1" },
|
||||
])
|
||||
})
|
||||
|
||||
test("advances the queue boundary when a running compaction completes", () => {
|
||||
const timeline = SessionTimeline.make()
|
||||
timeline.appendPart({ messageID: "assistant-1", partID: "read-1" }, { type: "tool", name: "read" })
|
||||
timeline.appendMessage("compaction-1", { pending: true, compaction: true })
|
||||
timeline.appendMessage("user-1", { pending: true, compaction: false })
|
||||
|
||||
timeline.appendMessage("compaction-1", { pending: false, compaction: true })
|
||||
timeline.appendPart({ messageID: "assistant-2", partID: "grep-1" }, { type: "tool", name: "grep" })
|
||||
|
||||
expect(withoutIDs([...timeline.values()])).toEqual([
|
||||
{
|
||||
type: "group",
|
||||
kind: "exploration",
|
||||
pending: [],
|
||||
completed: true,
|
||||
refs: [{ messageID: "assistant-1", partID: "read-1" }],
|
||||
},
|
||||
{ type: "message", messageID: "compaction-1" },
|
||||
{
|
||||
type: "group",
|
||||
kind: "exploration",
|
||||
pending: [],
|
||||
completed: false,
|
||||
refs: [{ messageID: "assistant-2", partID: "grep-1" }],
|
||||
},
|
||||
{ type: "message", messageID: "user-1" },
|
||||
])
|
||||
})
|
||||
|
||||
function assistant(id: string, content: SessionMessageAssistant["content"]): SessionMessageAssistant {
|
||||
return {
|
||||
type: "assistant",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue