feat(tui): add quark timeline experiment
This commit is contained in:
parent
e6607fb58d
commit
daf8e539bf
22 changed files with 2804 additions and 122 deletions
|
|
@ -28,6 +28,19 @@ async function wait(fn: () => boolean, timeout = 2000) {
|
|||
}
|
||||
}
|
||||
|
||||
function readRows(rows: ReturnType<typeof createSessionRows>) {
|
||||
return rows.values()
|
||||
}
|
||||
|
||||
function withoutRowID(row: SessionRow) {
|
||||
const { id: _id, ...value } = row
|
||||
if (value.type === "group") {
|
||||
const { origin: _origin, ...group } = value
|
||||
return group
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function emitEvent(events: ReturnType<typeof createEventStream>, event: OpenCodeEvent) {
|
||||
events.emit({ ...event, location: { directory } })
|
||||
}
|
||||
|
|
@ -717,10 +730,16 @@ test("completes exploration when a queued prompt is promoted", async () => {
|
|||
}, events)
|
||||
let rows!: ReturnType<typeof createSessionRows>
|
||||
let client!: ReturnType<typeof useClient>
|
||||
let structuralUpdates = 0
|
||||
const metrics = { slotPublications: 0, structuralPublications: 0, equivalenceSuppressions: 0 }
|
||||
|
||||
function Probe() {
|
||||
client = useClient()
|
||||
rows = createSessionRows(() => sessionID)
|
||||
rows = createSessionRows(() => sessionID, { metrics })
|
||||
createEffect(() => {
|
||||
rows.slots()
|
||||
structuralUpdates++
|
||||
})
|
||||
return <box />
|
||||
}
|
||||
|
||||
|
|
@ -762,31 +781,81 @@ test("completes exploration when a queued prompt is promoted", async () => {
|
|||
name: "read",
|
||||
},
|
||||
})
|
||||
await wait(() => rows.some((row) => row.type === "group" && !row.completed))
|
||||
await wait(() => readRows(rows).some((row) => row.type === "group" && !row.completed))
|
||||
expect(metrics.slotPublications).toBe(0)
|
||||
expect(metrics.structuralPublications).toBe(1)
|
||||
|
||||
emitEvent(events, {
|
||||
id: "evt_tool_started_2",
|
||||
created: 2,
|
||||
type: "session.tool.input.started",
|
||||
durable: durable(sessionID, 2),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID: "message-assistant",
|
||||
callID: "call-read-2",
|
||||
name: "read",
|
||||
},
|
||||
})
|
||||
await wait(() => {
|
||||
const group = readRows(rows).find((row) => row.type === "group")
|
||||
return group?.refs.length === 2
|
||||
})
|
||||
expect(metrics.slotPublications).toBe(1)
|
||||
expect(metrics.structuralPublications).toBe(1)
|
||||
const groupSlot = rows.slots().find((slot) => slot().type === "group")
|
||||
expect(groupSlot).toBeDefined()
|
||||
const beforePermission = structuralUpdates
|
||||
|
||||
emitEvent(events, {
|
||||
id: "evt_permission_asked",
|
||||
created: 2,
|
||||
type: "permission.v2.asked",
|
||||
data: {
|
||||
id: "permission-read",
|
||||
sessionID,
|
||||
action: "read",
|
||||
resources: ["src/example.ts"],
|
||||
source: { type: "tool", messageID: "message-assistant", callID: "call-read" },
|
||||
},
|
||||
})
|
||||
await wait(() => {
|
||||
const group = readRows(rows).find((row) => row.type === "group" && row.kind === "exploration")
|
||||
return group?.pending[0]?.partID === "call-read"
|
||||
})
|
||||
expect(rows.slots().find((slot) => slot().type === "group")).toBe(groupSlot)
|
||||
expect(structuralUpdates).toBe(beforePermission)
|
||||
expect(metrics.slotPublications).toBe(2)
|
||||
expect(metrics.structuralPublications).toBe(1)
|
||||
|
||||
emitEvent(events, {
|
||||
id: "evt_prompt_admitted",
|
||||
created: 3,
|
||||
type: "session.input.admitted",
|
||||
durable: durable(sessionID, 2),
|
||||
durable: durable(sessionID, 3),
|
||||
data: {
|
||||
sessionID,
|
||||
inputID: "message-user",
|
||||
input: { type: "user", data: { text: "Continue" }, delivery: "steer" },
|
||||
},
|
||||
})
|
||||
await wait(() => rows.at(-1)?.type === "message")
|
||||
expect(rows.find((row) => row.type === "group")?.completed).toBe(false)
|
||||
await wait(() => readRows(rows).at(-1)?.type === "message")
|
||||
expect(readRows(rows).find((row) => row.type === "group")?.completed).toBe(false)
|
||||
expect(metrics.slotPublications).toBe(2)
|
||||
expect(metrics.structuralPublications).toBe(2)
|
||||
|
||||
emitEvent(events, {
|
||||
id: "evt_prompt_promoted",
|
||||
created: 4,
|
||||
type: "session.input.promoted",
|
||||
durable: durable(sessionID, 3),
|
||||
durable: durable(sessionID, 4),
|
||||
data: { sessionID, inputID: "message-user" },
|
||||
})
|
||||
await wait(() => rows.find((row) => row.type === "group")?.completed === true)
|
||||
expect(rows.at(-1)).toEqual({ type: "message", messageID: "message-user" })
|
||||
await wait(() => readRows(rows).find((row) => row.type === "group")?.completed === true)
|
||||
expect(rows.slots().find((slot) => slot().type === "group")).toBe(groupSlot)
|
||||
expect(withoutRowID(readRows(rows).at(-1)!)).toEqual({ type: "message", messageID: "message-user" })
|
||||
expect(metrics.slotPublications).toBe(3)
|
||||
expect(metrics.structuralPublications).toBe(2)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
|
|
@ -834,8 +903,90 @@ test("classifies live tool rows independently of their call ID", async () => {
|
|||
},
|
||||
})
|
||||
|
||||
await wait(() => rows.length > 0)
|
||||
expect(rows).toEqual([{ type: "part", ref: { messageID: "message-assistant", partID: "reasoning:0" } }])
|
||||
await wait(() => readRows(rows).length > 0)
|
||||
expect(readRows(rows).map(withoutRowID)).toEqual([
|
||||
{ type: "part", ref: { messageID: "message-assistant", partID: "reasoning:0" } },
|
||||
])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("does not publish timeline rows for duplicate streaming deltas", async () => {
|
||||
const events = createEventStream()
|
||||
const sessionID = "session-stream-metrics"
|
||||
const calls = createFetch((url) => {
|
||||
if (url.pathname === `/api/session/${sessionID}/message`) return json({ data: [], cursor: {} })
|
||||
}, events)
|
||||
const metrics = { slotPublications: 0, structuralPublications: 0, equivalenceSuppressions: 0 }
|
||||
let data!: ReturnType<typeof useData>
|
||||
let rows!: ReturnType<typeof createSessionRows>
|
||||
let client!: ReturnType<typeof useClient>
|
||||
|
||||
function Probe() {
|
||||
data = useData()
|
||||
client = useClient()
|
||||
rows = createSessionRows(() => sessionID, { metrics })
|
||||
return <box />
|
||||
}
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts>
|
||||
<ClientProvider api={createApi(calls.fetch)}>
|
||||
<ProjectProvider>
|
||||
<DataProvider>
|
||||
<Probe />
|
||||
</DataProvider>
|
||||
</ProjectProvider>
|
||||
</ClientProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
|
||||
try {
|
||||
await wait(() => client.connection.status() === "connected")
|
||||
emitEvent(events, {
|
||||
id: "evt_stream_step",
|
||||
created: 1,
|
||||
type: "session.step.started",
|
||||
durable: durable(sessionID),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID: "message-assistant",
|
||||
agent: "build",
|
||||
model: { id: "model", providerID: "provider" },
|
||||
},
|
||||
})
|
||||
emitEvent(events, {
|
||||
id: "evt_stream_started",
|
||||
created: 2,
|
||||
type: "session.text.started",
|
||||
durable: durable(sessionID, 1),
|
||||
data: { sessionID, assistantMessageID: "message-assistant", ordinal: 0 },
|
||||
})
|
||||
emitEvent(events, {
|
||||
id: "evt_stream_delta_1",
|
||||
created: 3,
|
||||
type: "session.text.delta",
|
||||
data: { sessionID, assistantMessageID: "message-assistant", ordinal: 0, delta: "one" },
|
||||
})
|
||||
await wait(() => readRows(rows).some((row) => row.type === "part"))
|
||||
const afterFirst = { ...metrics }
|
||||
|
||||
emitEvent(events, {
|
||||
id: "evt_stream_delta_2",
|
||||
created: 4,
|
||||
type: "session.text.delta",
|
||||
data: { sessionID, assistantMessageID: "message-assistant", ordinal: 0, delta: "two" },
|
||||
})
|
||||
await wait(() => {
|
||||
const message = data.session.message.get(sessionID, "message-assistant")
|
||||
return (
|
||||
message?.type === "assistant" && message.content.some((part) => part.type === "text" && part.text === "onetwo")
|
||||
)
|
||||
})
|
||||
|
||||
expect(afterFirst).toEqual({ slotPublications: 0, structuralPublications: 1, equivalenceSuppressions: 0 })
|
||||
expect(metrics).toEqual(afterFirst)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
|
|
@ -987,13 +1138,15 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
})
|
||||
}, events)
|
||||
let data!: ReturnType<typeof useData>
|
||||
let rows!: SessionRow[]
|
||||
let manualRows!: SessionRow[]
|
||||
let rows!: ReturnType<typeof createSessionRows>
|
||||
let manualRows!: ReturnType<typeof createSessionRows>
|
||||
let liveRows!: ReturnType<typeof createSessionRows>
|
||||
|
||||
function Probe() {
|
||||
data = useData()
|
||||
rows = createSessionRows(() => "session-retry")
|
||||
manualRows = createSessionRows(() => "session-manual")
|
||||
liveRows = createSessionRows(() => "session-live")
|
||||
return <box />
|
||||
}
|
||||
|
||||
|
|
@ -1189,7 +1342,7 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
const assistant = data.session.message.get("session-retry", "message-retry")
|
||||
return assistant?.type === "assistant" && assistant.retry?.attempt === 2
|
||||
})
|
||||
await wait(() => rows.some((row) => row.type === "assistant-footer" && row.messageID === "message-retry"))
|
||||
await wait(() => readRows(rows).some((row) => row.type === "assistant-footer" && row.messageID === "message-retry"))
|
||||
emitEvent(events, {
|
||||
id: "evt_retry_next_step",
|
||||
created: 2_000,
|
||||
|
|
@ -1206,7 +1359,9 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
const assistant = data.session.message.get("session-retry", "message-retry")
|
||||
return assistant?.type === "assistant" && assistant.retry === undefined
|
||||
})
|
||||
await wait(() => !rows.some((row) => row.type === "assistant-footer" && row.messageID === "message-retry"))
|
||||
await wait(
|
||||
() => !readRows(rows).some((row) => row.type === "assistant-footer" && row.messageID === "message-retry"),
|
||||
)
|
||||
expect(data.session.message.list("session-retry").filter((message) => message.type === "assistant")).toHaveLength(1)
|
||||
emitEvent(events, {
|
||||
id: "evt_retry_scheduled_again",
|
||||
|
|
@ -1261,7 +1416,9 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
return message?.type === "compaction" && message.status === "running" && message.summary === "Streamed summary"
|
||||
})
|
||||
expect(data.session.pending.list("session-manual")).toEqual([])
|
||||
const compactionRow = manualRows.find((row) => row.type === "message" && row.messageID === "message-compaction")
|
||||
const compactionRow = readRows(manualRows).find(
|
||||
(row) => row.type === "message" && row.messageID === "message-compaction",
|
||||
)
|
||||
emitEvent(events, {
|
||||
id: "evt_manual_compaction_ended",
|
||||
created: 3,
|
||||
|
|
@ -1273,10 +1430,12 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
const message = data.session.message.get("session-manual", "message-compaction")
|
||||
return message?.type === "compaction" && message.status === "completed"
|
||||
})
|
||||
expect(manualRows.filter((row) => row.type === "message")).toEqual([
|
||||
{ type: "message", messageID: "message-compaction" },
|
||||
])
|
||||
expect(manualRows.find((row) => row.type === "message" && row.messageID === "message-compaction")).toBe(
|
||||
expect(
|
||||
readRows(manualRows)
|
||||
.filter((row) => row.type === "message")
|
||||
.map(withoutRowID),
|
||||
).toEqual([{ type: "message", messageID: "message-compaction" }])
|
||||
expect(readRows(manualRows).find((row) => row.type === "message" && row.messageID === "message-compaction")).toBe(
|
||||
compactionRow,
|
||||
)
|
||||
|
||||
|
|
@ -1303,7 +1462,10 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
const message = data.session.message.get("session-live", "msg_compaction_started")
|
||||
return message?.type === "compaction" && message.status === "running" && message.summary === "Live summary"
|
||||
})
|
||||
const autoCompactionRow = rows.find((row) => row.type === "message" && row.messageID === "msg_compaction_started")
|
||||
const autoCompactionRow = readRows(liveRows).find(
|
||||
(row) => row.type === "message" && row.messageID === "msg_compaction_started",
|
||||
)
|
||||
expect(autoCompactionRow).toBeDefined()
|
||||
|
||||
emitEvent(events, {
|
||||
id: "evt_compaction_ended",
|
||||
|
|
@ -1321,10 +1483,12 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
status: "completed",
|
||||
summary: "Live summary",
|
||||
})
|
||||
expect(rows.find((row) => row.type === "message" && row.messageID === "msg_compaction_started")).toBe(
|
||||
expect(readRows(liveRows).find((row) => row.type === "message" && row.messageID === "msg_compaction_started")).toBe(
|
||||
autoCompactionRow,
|
||||
)
|
||||
expect(rows.some((row) => row.type === "message" && row.messageID === "msg_compaction_ended")).toBeFalse()
|
||||
expect(
|
||||
readRows(liveRows).some((row) => row.type === "message" && row.messageID === "msg_compaction_ended"),
|
||||
).toBeFalse()
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
|
|
@ -1383,8 +1547,12 @@ test("restores queued compaction from durable pending input", async () => {
|
|||
"message-compaction-queued",
|
||||
"message-compaction-later",
|
||||
])
|
||||
await wait(() => rows.filter((row) => row.type === "compaction-queued").length === 2)
|
||||
expect(rows.filter((row) => row.type === "compaction-queued")).toEqual([
|
||||
await wait(() => readRows(rows).filter((row) => row.type === "compaction-queued").length === 2)
|
||||
expect(
|
||||
readRows(rows)
|
||||
.filter((row) => row.type === "compaction-queued")
|
||||
.map(withoutRowID),
|
||||
).toEqual([
|
||||
{ type: "compaction-queued", inputID: "message-compaction-queued" },
|
||||
{ type: "compaction-queued", inputID: "message-compaction-later" },
|
||||
])
|
||||
|
|
@ -1401,8 +1569,8 @@ test("restores queued compaction from durable pending input", async () => {
|
|||
text: "Active output",
|
||||
},
|
||||
})
|
||||
await wait(() => rows.some((row) => row.type === "part"))
|
||||
expect(rows.map((row) => row.type)).toEqual(["part", "compaction-queued", "compaction-queued"])
|
||||
await wait(() => readRows(rows).some((row) => row.type === "part"))
|
||||
expect(readRows(rows).map((row) => row.type)).toEqual(["part", "compaction-queued", "compaction-queued"])
|
||||
|
||||
emitEvent(events, {
|
||||
id: "evt_compaction_started",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client"
|
||||
import { messageBoundaryIDs, reduceSessionRows } from "../../../src/routes/session/rows"
|
||||
import { messageBoundaryIDs, reduceSessionRows, type SessionRow } from "../../../src/routes/session/rows"
|
||||
|
||||
const withoutIDs = (rows: ReturnType<typeof reduceSessionRows>) =>
|
||||
rows.map(({ id: _id, ...row }) => {
|
||||
if (row.type !== "group") return row
|
||||
const { origin: _origin, ...group } = row
|
||||
return group
|
||||
})
|
||||
|
||||
test("assigns assistant boundaries to the first rendered row instead of the first text row", () => {
|
||||
const messages: SessionMessageInfo[] = [
|
||||
|
|
@ -16,6 +23,22 @@ test("assigns assistant boundaries to the first rendered row instead of the firs
|
|||
expect(messageBoundaryIDs(rows, messages)).toEqual(["user-1", "assistant-1", undefined, undefined])
|
||||
})
|
||||
|
||||
test("keeps a group boundary at its immutable origin while visible refs repartition", () => {
|
||||
const messages = [assistant("assistant-1", []), assistant("assistant-2", [])]
|
||||
const origin = { messageID: "assistant-1", partID: "read-1" }
|
||||
const group: SessionRow = {
|
||||
id: "group",
|
||||
type: "group",
|
||||
kind: "exploration",
|
||||
origin,
|
||||
refs: [{ messageID: "assistant-2", partID: "grep-1" }],
|
||||
pending: [origin],
|
||||
completed: false,
|
||||
}
|
||||
|
||||
expect(messageBoundaryIDs([group], messages)).toEqual(["assistant-1"])
|
||||
})
|
||||
|
||||
test("groups exploration parts across assistant messages until a delimiter", () => {
|
||||
const messages: SessionMessageInfo[] = [
|
||||
{ type: "user", id: "user-1", text: "Explore", time: { created: 0 } },
|
||||
|
|
@ -30,7 +53,7 @@ test("groups exploration parts across assistant messages until a delimiter", ()
|
|||
]),
|
||||
]
|
||||
|
||||
expect(reduceSessionRows(messages)).toEqual([
|
||||
expect(withoutIDs(reduceSessionRows(messages))).toEqual([
|
||||
{ type: "message", messageID: "user-1" },
|
||||
{ type: "part", ref: { messageID: "assistant-1", partID: "text:0" } },
|
||||
{
|
||||
|
|
@ -57,7 +80,7 @@ test("keeps non-exploration tools as individual part rows", () => {
|
|||
]),
|
||||
]
|
||||
|
||||
expect(reduceSessionRows(messages)).toEqual([
|
||||
expect(withoutIDs(reduceSessionRows(messages))).toEqual([
|
||||
{
|
||||
type: "group",
|
||||
kind: "exploration",
|
||||
|
|
@ -86,7 +109,7 @@ test("assigns stable kind ordinals within an assistant message", () => {
|
|||
]),
|
||||
]
|
||||
|
||||
expect(reduceSessionRows(messages)).toEqual([
|
||||
expect(withoutIDs(reduceSessionRows(messages))).toEqual([
|
||||
{ type: "part", ref: { messageID: "assistant-1", partID: "text:0" } },
|
||||
{
|
||||
type: "group",
|
||||
|
|
@ -114,7 +137,7 @@ test("groups adjacent reasoning parts until a visible boundary", () => {
|
|||
]),
|
||||
]
|
||||
|
||||
expect(reduceSessionRows(messages)).toEqual([
|
||||
expect(withoutIDs(reduceSessionRows(messages))).toEqual([
|
||||
{
|
||||
type: "group",
|
||||
kind: "reasoning",
|
||||
|
|
@ -146,7 +169,7 @@ test("groups across empty assistant reasoning parts", () => {
|
|||
]),
|
||||
]
|
||||
|
||||
expect(reduceSessionRows(messages)).toEqual([
|
||||
expect(withoutIDs(reduceSessionRows(messages))).toEqual([
|
||||
{
|
||||
type: "group",
|
||||
kind: "reasoning",
|
||||
|
|
@ -177,7 +200,7 @@ test("completes exploration groups when another row follows", () => {
|
|||
finished,
|
||||
]
|
||||
|
||||
expect(reduceSessionRows(messages)).toEqual([
|
||||
expect(withoutIDs(reduceSessionRows(messages))).toEqual([
|
||||
{
|
||||
type: "group",
|
||||
kind: "exploration",
|
||||
|
|
@ -209,7 +232,7 @@ test("hides synthetic messages without descriptions", () => {
|
|||
assistant("assistant-2", [{ type: "tool", id: "grep-1", name: "grep", state: pending(), time: { created: 3 } }]),
|
||||
]
|
||||
|
||||
expect(reduceSessionRows(messages)).toEqual([
|
||||
expect(withoutIDs(reduceSessionRows(messages))).toEqual([
|
||||
{
|
||||
type: "group",
|
||||
kind: "exploration",
|
||||
|
|
@ -236,7 +259,7 @@ test("renders synthetic messages with descriptions", () => {
|
|||
assistant("assistant-2", [{ type: "tool", id: "grep-1", name: "grep", state: pending(), time: { created: 3 } }]),
|
||||
]
|
||||
|
||||
expect(reduceSessionRows(messages)).toEqual([
|
||||
expect(withoutIDs(reduceSessionRows(messages))).toEqual([
|
||||
{
|
||||
type: "group",
|
||||
kind: "exploration",
|
||||
|
|
@ -263,7 +286,7 @@ test("renders a footer for a pre-output retry assistant after replay", () => {
|
|||
error: { type: "provider.transport", message: "Disconnected" },
|
||||
}
|
||||
|
||||
expect(reduceSessionRows([message])).toEqual([{ type: "assistant-footer", messageID: "assistant-retry" }])
|
||||
expect(withoutIDs(reduceSessionRows([message]))).toEqual([{ type: "assistant-footer", messageID: "assistant-retry" }])
|
||||
})
|
||||
|
||||
test("places a running compaction barrier before every queued user message", () => {
|
||||
|
|
@ -287,7 +310,7 @@ test("places a running compaction barrier before every queued user message", ()
|
|||
queued("user-after", "After", 3),
|
||||
]
|
||||
|
||||
expect(reduceSessionRows(messages, new Set(["user-before", "user-after"]))).toEqual([
|
||||
expect(withoutIDs(reduceSessionRows(messages, new Set(["user-before", "user-after"])))).toEqual([
|
||||
{ type: "message", messageID: "compaction" },
|
||||
{ type: "message", messageID: "user-before" },
|
||||
{ type: "message", messageID: "user-after" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue