feat(tui): add quark timeline experiment

This commit is contained in:
Kit Langton 2026-07-17 16:49:14 -04:00
commit daf8e539bf
22 changed files with 2804 additions and 122 deletions

View file

@ -0,0 +1,60 @@
export type Workload = {
run(index: number): void
consume(): number
dispose?(): void
}
export type Variant = {
readonly name: string
readonly make: () => Workload
}
export function createHarness(options: { readonly samples?: number; readonly warmup?: number } = {}) {
const samples = options.samples ?? 9
const warmup = options.warmup ?? 500
let checksum = 0
return {
samples,
compare(iterations: number, variants: readonly Variant[]) {
const timings = variants.map(() => [] as number[])
for (let sample = -1; sample < samples; sample++) {
const offset = sample < 0 ? 0 : sample % variants.length
variants
.map((_variant, index) => (index + offset) % variants.length)
.forEach((variantIndex) => {
const workload = variants[variantIndex].make()
for (let index = 0; index < Math.min(iterations, warmup); index++) workload.run(index)
const start = Bun.nanoseconds()
for (let index = 0; index < iterations; index++) workload.run(index)
const elapsed = Bun.nanoseconds() - start
checksum += workload.consume()
workload.dispose?.()
if (sample >= 0) timings[variantIndex].push(elapsed / iterations)
})
}
const medians = variants.map((variant, index) => {
const median = middle(timings[index])
const mad = middle(timings[index].map((value) => Math.abs(value - median)))
const metric = variant.name.toLowerCase().replaceAll(/[^a-z0-9]+/g, "_")
console.log(`${variant.name.padEnd(42)} ${median.toFixed(1).padStart(10)} ns/op +/- ${mad.toFixed(1)} MAD`)
console.log(`METRIC ${metric}_ns_per_op=${median.toFixed(3)}`)
return median
})
return {
medians,
ratio(left: number, right: number) {
return middle(timings[left].map((value, index) => value / timings[right][index]))
},
}
},
finish() {
console.log(`CHECKSUM ${checksum}`)
},
}
}
function middle(values: number[]) {
return values.toSorted((a, b) => a - b)[Math.floor(values.length / 2)]
}

View file

@ -0,0 +1,217 @@
import { createComputed, createRoot } from "solid-js"
import { createStore, reconcile } from "solid-js/store"
import { Keyed } from "../src"
import { createHarness, type Workload } from "./harness"
type Item = {
readonly id: number
readonly value: number
}
const bench = createHarness()
const results: Array<{ readonly name: string; readonly ratio: number }> = []
function initial(size: number) {
return Array.from({ length: size }, (_, id): Item => ({ id, value: 0 }))
}
function project(values: readonly Item[]) {
return values.reduce((total, value) => total + value.id + value.value, 0)
}
function quarkDirect(size: number, aggregate: boolean): Workload {
const values = initial(size)
const keyed = Keyed.make<Item, number>({
key: (item) => item.id,
equivalent: (left, right) => left.value === right.value,
})
keyed.set(values)
const target = keyed.slots()[Math.floor(size / 2)]
let sink = aggregate ? project(keyed.values()) : target().value
const dispose = aggregate
? keyed.values.subscribe((next) => (sink = project(next)))
: target.subscribe((value) => (sink = value.value))
return {
run: (index) => keyed.update({ id: Math.floor(size / 2), value: index + 1 }),
consume: () => sink,
dispose,
}
}
function solidDirect(size: number, aggregate: boolean): Workload {
let run = (_index: number) => {}
let consume = () => 0
let dispose = () => {}
createRoot((rootDispose) => {
dispose = rootDispose
const [values, setValues] = createStore(initial(size))
const target = Math.floor(size / 2)
let sink = aggregate ? project(values) : values[target].value
if (aggregate) createComputed(() => (sink = project(values)))
else createComputed(() => (sink = values[target].value))
run = (index) => setValues(target, reconcile({ id: target, value: index + 1 }))
consume = () => sink
})
return { run, consume, dispose }
}
function quarkNoSubscriber(size: number): Workload {
const keyed = Keyed.make<Item, number>({
key: (item) => item.id,
equivalent: (left, right) => left.value === right.value,
})
keyed.set(initial(size))
const target = Math.floor(size / 2)
return {
run: (index) => keyed.update({ id: target, value: index + 1 }),
consume: () => keyed.slots()[target]().value,
}
}
function solidNoSubscriber(size: number): Workload {
const [values, setValues] = createStore(initial(size))
const target = Math.floor(size / 2)
return {
run: (index) => setValues(target, reconcile({ id: target, value: index + 1 })),
consume: () => values[target].value,
}
}
function solidPathWriteNoSubscriber(size: number): Workload {
const [values, setValues] = createStore(initial(size))
const target = Math.floor(size / 2)
return {
run: (index) => setValues(target, "value", index + 1),
consume: () => values[target].value,
}
}
function quarkDense(size: number): Workload {
const keyed = Keyed.make<Item, number>({
key: (item) => item.id,
equivalent: (left, right) => left.value === right.value,
})
keyed.set(initial(size))
let sink = project(keyed.values())
const dispose = keyed.values.subscribe((values) => (sink = project(values)))
return {
run: (index) => keyed.set(initial(size).map((item) => ({ ...item, value: index + 1 }))),
consume: () => sink,
dispose,
}
}
function solidDense(size: number): Workload {
let run = (_index: number) => {}
let consume = () => 0
let dispose = () => {}
createRoot((rootDispose) => {
dispose = rootDispose
const [values, setValues] = createStore(initial(size))
let sink = project(values)
createComputed(() => (sink = project(values)))
run = (index) => setValues(reconcile(initial(size).map((item) => ({ ...item, value: index + 1 }))))
consume = () => sink
})
return { run, consume, dispose }
}
function quarkUnstable(size: number): Workload {
const keyed = Keyed.make<Item, number>({ key: (item) => item.id })
keyed.set(initial(size))
let sink = project(keyed.values())
const dispose = keyed.values.subscribe((values) => (sink = project(values)))
return {
run(index) {
const offset = (index + 1) * size
keyed.set(initial(size).map((item) => ({ id: item.id + offset, value: index })))
},
consume: () => sink,
dispose,
}
}
function solidUnstable(size: number): Workload {
let run = (_index: number) => {}
let consume = () => 0
let dispose = () => {}
createRoot((rootDispose) => {
dispose = rootDispose
const [values, setValues] = createStore(initial(size))
let sink = project(values)
createComputed(() => (sink = project(values)))
run = (index) => {
const offset = (index + 1) * size
setValues(reconcile(initial(size).map((item) => ({ id: item.id + offset, value: index }))))
}
consume = () => sink
})
return { run, consume, dispose }
}
function compare(name: string, iterations: number, quark: () => Workload, solid: () => Workload) {
console.log(`\n${name}`)
const result = bench.compare(iterations, [
{ name: `Quark ${name}`, make: quark },
{ name: `Solid ${name}`, make: solid },
])
results.push({ name, ratio: result.ratio(0, 1) })
}
console.log(`Keyed integration benchmark (${bench.samples} samples)`)
compare(
"direct no subscribers 1000",
200_000,
() => quarkNoSubscriber(1_000),
() => solidNoSubscriber(1_000),
)
compare(
"adversarial direct path write 1000",
200_000,
() => quarkNoSubscriber(1_000),
() => solidPathWriteNoSubscriber(1_000),
)
compare(
"subscribed values 10",
100_000,
() => quarkDirect(10, true),
() => solidDirect(10, true),
)
compare(
"subscribed values 100",
25_000,
() => quarkDirect(100, true),
() => solidDirect(100, true),
)
compare(
"subscribed values 1000",
2_500,
() => quarkDirect(1_000, true),
() => solidDirect(1_000, true),
)
compare(
"subscribed values 10000",
250,
() => quarkDirect(10_000, true),
() => solidDirect(10_000, true),
)
compare(
"dense update 1000",
250,
() => quarkDense(1_000),
() => solidDense(1_000),
)
compare(
"unstable keys 100",
1_000,
() => quarkUnstable(100),
() => solidUnstable(100),
)
console.log("\nRatios to Solid (lower is faster)")
results.forEach((result) => {
console.log(`${result.name.padEnd(34)} ${result.ratio.toFixed(3)}x`)
console.log(`METRIC ${result.name.replaceAll(/[^a-z0-9]+/g, "_")}_ratio=${result.ratio.toFixed(6)}`)
})
bench.finish()

View file

@ -0,0 +1,63 @@
import { createHarness, type Workload } from "./harness"
type Row =
| { readonly type: "message"; readonly messageID: string }
| { readonly type: "part"; readonly messageID: string; readonly partID: string }
| {
readonly type: "group"
readonly kind: "reasoning" | "exploration"
readonly messageID: string
readonly partID: string
}
const size = 10_000
const iterations = 1_000_000
const rows = Array.from({ length: size }, (_, index): Row => {
if (index % 3 === 0) return { type: "message", messageID: `message-${index}` }
if (index % 3 === 1) return { type: "part", messageID: `message-${index >> 2}`, partID: `text:${index}` }
return {
type: "group",
kind: index % 2 === 0 ? "reasoning" : "exploration",
messageID: `message-${index >> 2}`,
partID: `call-${index}`,
}
})
const precomputed = rows.map((row) => ({ row, id: concatenate(row) }))
const bench = createHarness()
function workload(read: (index: number) => string): Workload {
let sink = 0
return {
run(index) {
sink += read(index % size).length
},
consume: () => sink,
}
}
function json(row: Row) {
if (row.type === "message") return JSON.stringify([row.type, row.messageID])
if (row.type === "part") return JSON.stringify([row.type, row.messageID, row.partID])
return JSON.stringify([row.type, row.kind, row.messageID, row.partID])
}
function concatenate(row: Row) {
if (row.type === "message") return `m${row.messageID.length}:${row.messageID}`
if (row.type === "part") return `p${row.messageID.length}:${row.messageID}${row.partID.length}:${row.partID}`
return `g${row.kind === "reasoning" ? "r" : "e"}${row.messageID.length}:${row.messageID}${row.partID.length}:${row.partID}`
}
console.log(`Session row key benchmark (${size.toLocaleString()} rows, ${bench.samples} samples)\n`)
const result = bench.compare(iterations, [
{ name: "JSON tuple key", make: () => workload((index) => json(rows[index])) },
{ name: "Concatenated key", make: () => workload((index) => concatenate(rows[index])) },
{ name: "Precomputed key", make: () => workload((index) => precomputed[index].id) },
])
console.log("\nRatios to JSON tuple (lower is faster)")
console.log(`Concatenated: ${result.ratio(1, 0).toFixed(3)}x`)
console.log(`Precomputed: ${result.ratio(2, 0).toFixed(3)}x`)
console.log(`METRIC concatenated_key_ratio=${result.ratio(1, 0).toFixed(6)}`)
console.log(`METRIC precomputed_key_ratio=${result.ratio(2, 0).toFixed(6)}`)
bench.finish()

View file

@ -0,0 +1,20 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "effect-quark",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts",
"./solid": "./src/solid.ts"
},
"scripts": {
"bench:keyed": "bun --conditions=browser bench/keyed.ts",
"bench:row-key": "bun --conditions=browser bench/row-key.ts",
"test": "bun --conditions=browser test"
},
"dependencies": {
"alien-signals": "3.2.1",
"solid-js": "catalog:"
}
}

View file

@ -0,0 +1,2 @@
export { Keyed } from "./keyed"
export { Computed, State, Transaction, type Readable, type Writable } from "./reactivity"

143
packages/quark/src/keyed.ts Normal file
View file

@ -0,0 +1,143 @@
import { Computed, State, Transaction, type Readable, type Writable } from "./reactivity"
export namespace Keyed {
export type Position<Key> = "end" | { readonly before: Key } | { readonly after: Key }
export interface Metrics {
slotPublications: number
structuralPublications: number
equivalenceSuppressions: number
}
export interface Keyed<A, Key> {
readonly slots: Readable<readonly Readable<A>[]>
readonly values: Readable<readonly A[]>
has(key: Key): boolean
get(key: Key): Readable<A> | undefined
set(values: readonly A[]): void
update(value: A): boolean
insert(value: A, position?: Position<Key>): Readable<A>
remove(key: Key): boolean
move(key: Key, position?: Position<Key>): boolean
}
export function make<A, Key>(options: {
readonly key: (value: A) => Key
readonly equivalent?: (left: A, right: A) => boolean
readonly metrics?: Metrics
}): Keyed<A, Key> {
const slots = State.make<readonly Writable<A>[]>([])
const byKey = new Map<Key, Writable<A>>()
const equivalent = options.equivalent ?? Object.is
const values = Computed.make<readonly A[]>((previous) => {
const next = slots().map((slot) => slot())
return same(previous, next) ? previous! : next
})
return {
slots,
values,
has: (key) => byKey.has(key),
get: (key) => byKey.get(key),
set(next) {
const keys = next.map(options.key)
const retained = new Set(keys)
if (retained.size !== keys.length) throw new Error("Keyed values must have unique keys")
Transaction.run(() => {
const previous = slots()
const reconciled = next.map((value, index) => {
const key = keys[index]
const slot = byKey.get(key)
if (!slot) {
const created = State.make(value)
byKey.set(key, created)
return created
}
if (!equivalent(slot(), value)) {
slot.set(value)
if (options.metrics) options.metrics.slotPublications++
} else if (options.metrics) {
options.metrics.equivalenceSuppressions++
}
return slot
})
byKey.forEach((_slot, key) => {
if (!retained.has(key)) byKey.delete(key)
})
if (!same(previous, reconciled)) {
slots.set(reconciled)
if (options.metrics) options.metrics.structuralPublications++
}
})
},
update(value) {
const key = options.key(value)
const slot = byKey.get(key)
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
if (equivalent(slot(), value)) {
if (options.metrics) options.metrics.equivalenceSuppressions++
return false
}
slot.set(value)
if (options.metrics) options.metrics.slotPublications++
return true
},
insert(value, position) {
const key = options.key(value)
if (byKey.has(key)) throw new Error(`Keyed value already exists: ${String(key)}`)
const current = slots()
const index = positionIndex(current, position)
const slot = State.make(value)
Transaction.run(() => {
byKey.set(key, slot)
slots.set(current.toSpliced(index, 0, slot))
if (options.metrics) options.metrics.structuralPublications++
})
return slot
},
remove(key) {
const slot = byKey.get(key)
if (!slot) return false
Transaction.run(() => {
byKey.delete(key)
slots.set(slots().filter((candidate) => candidate !== slot))
if (options.metrics) options.metrics.structuralPublications++
})
return true
},
move(key, position) {
const slot = byKey.get(key)
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
const current = slots()
const from = current.indexOf(slot)
const target = positionIndex(current, position)
const to = from < target ? target - 1 : target
if (from === to) return false
slots.set(current.toSpliced(from, 1).toSpliced(to, 0, slot))
if (options.metrics) options.metrics.structuralPublications++
return true
},
}
function positionIndex(current: readonly Writable<A>[], position?: Position<Key>) {
if (position === undefined || position === "end") return current.length
if ("before" in position) return indexOf(current, position.before)
return indexOf(current, position.after) + 1
}
function indexOf(current: readonly Writable<A>[], key: Key) {
const target = byKey.get(key)
if (!target) throw new Error(`Keyed value does not exist: ${String(key)}`)
return current.indexOf(target)
}
}
export function metrics(): Metrics {
return { slotPublications: 0, structuralPublications: 0, equivalenceSuppressions: 0 }
}
function same<A>(left: readonly A[] | undefined, right: readonly A[]) {
return left?.length === right.length && left.every((value, index) => Object.is(value, right[index]))
}
}

View file

@ -0,0 +1,59 @@
import { computed, effect, endBatch, setActiveSub, signal, startBatch } from "alien-signals"
export interface Readable<A> {
(): A
subscribe(listener: (value: A) => void): () => void
}
export interface Writable<A> extends Readable<A> {
set(value: A): void
update(f: (value: A) => A): void
}
function subscribe<A>(read: () => A, listener: (value: A) => void) {
let initialized = false
return effect(() => {
const value = read()
if (!initialized) {
initialized = true
return
}
const active = setActiveSub()
try {
listener(value)
} finally {
setActiveSub(active)
}
})
}
export namespace State {
export function make<A>(initial: A): Writable<A> {
const state = signal(initial)
const read = (() => state()) as Writable<A>
read.set = (value) => state(value)
read.update = (f) => state(f(state()))
read.subscribe = (listener) => subscribe(read, listener)
return read
}
}
export namespace Computed {
export function make<A>(evaluate: (previous: A | undefined) => A): Readable<A> {
const value = computed(evaluate)
const read = (() => value()) as Readable<A>
read.subscribe = (listener) => subscribe(read, listener)
return read
}
}
export namespace Transaction {
export function run<A>(f: () => A): A {
startBatch()
try {
return f()
} finally {
endBatch()
}
}
}

View file

@ -0,0 +1,22 @@
import { For, from, type Accessor, type JSX } from "solid-js"
import type { Readable } from "./reactivity"
export function useValue<A>(readable: Readable<A>): Accessor<A> {
return from(readable, readable())
}
export function KeyedFor<A>(props: {
readonly each: Accessor<readonly Readable<A>[]>
readonly fallback?: JSX.Element
readonly children: (value: Accessor<A>, index: Accessor<number>) => JSX.Element
}) {
return For({
get each() {
return props.each()
},
get fallback() {
return props.fallback
},
children: (slot, index) => props.children(useValue(slot), index),
})
}

View file

@ -0,0 +1,201 @@
import { describe, expect, it } from "bun:test"
import { Computed, Keyed } from "../src"
type Item = {
readonly id: number
readonly label: string
}
const item = (id: number, label: string): Item => ({ id, label })
describe("Keyed", () => {
it("keeps slots stable while publishing value and structural changes separately", () => {
const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
keyed.set([item(1, "one"), item(2, "two")])
const [one, two] = keyed.slots()
const structure = keyed.slots()
const structures: number[][] = []
const values: Item[][] = []
const disposeSlots = keyed.slots.subscribe((slots) => structures.push(slots.map((slot) => slot().id)))
const disposeValues = keyed.values.subscribe((next) => values.push([...next]))
keyed.set([item(1, "ONE"), item(2, "TWO")])
expect(keyed.slots()).toBe(structure)
expect(keyed.slots()).toEqual([one, two])
expect(values).toEqual([[item(1, "ONE"), item(2, "TWO")]])
expect(structures).toEqual([])
keyed.set([item(2, "TWO"), item(1, "ONE")])
expect(keyed.slots()).toEqual([two, one])
expect(structures).toEqual([[2, 1]])
disposeSlots()
disposeValues()
})
it("uses custom equivalence to cut off slot and aggregate updates", () => {
const keyed = Keyed.make<Item, number>({
key: (value) => value.id,
equivalent: (left, right) => left.label.toLowerCase() === right.label.toLowerCase(),
})
const original = item(1, "one")
keyed.set([original])
const slot = keyed.slots()[0]
const aggregate = keyed.values()
keyed.set([item(1, "ONE")])
expect(slot()).toBe(original)
expect(keyed.values()).toBe(aggregate)
})
it("creates a fresh slot after removal and reinsertion", () => {
const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
keyed.set([item(1, "one"), item(2, "two")])
const [removed, retained] = keyed.slots()
keyed.set([item(2, "two")])
keyed.set([item(1, "new"), item(2, "TWO")])
expect(keyed.slots()[0]).not.toBe(removed)
expect(keyed.slots()[1]).toBe(retained)
expect(retained()).toEqual(item(2, "TWO"))
})
it("rejects duplicate keys without partially updating", () => {
const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
keyed.set([item(1, "one"), item(2, "two")])
const slots = keyed.slots()
const values = keyed.values()
expect(() => keyed.set([item(1, "changed"), item(1, "duplicate")])).toThrow("Keyed values must have unique keys")
expect(keyed.slots()).toBe(slots)
expect(keyed.values()).toBe(values)
})
it("uses SameValueZero key equality", () => {
const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
keyed.set([item(-0, "zero"), item(Number.NaN, "nan")])
const [zero, nan] = keyed.slots()
keyed.set([item(0, "ZERO"), item(Number.NaN, "NAN")])
expect(keyed.slots()).toEqual([zero, nan])
expect(() => keyed.set([item(0, "zero"), item(-0, "duplicate")])).toThrow("Keyed values must have unique keys")
expect(() => keyed.set([item(Number.NaN, "nan"), item(Number.NaN, "duplicate")])).toThrow(
"Keyed values must have unique keys",
)
})
it("publishes one settled aggregate when values and structure change together", () => {
const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
keyed.set([item(1, "one"), item(2, "two")])
const observations: string[] = []
const summary = Computed.make(() => {
const slots = keyed
.slots()
.map((slot) => `${slot().id}:${slot().label}`)
.join(",")
const values = keyed
.values()
.map((value) => `${value.id}:${value.label}`)
.join(",")
return `${slots}|${values}`
})
const dispose = summary.subscribe((value) => observations.push(value))
keyed.set([item(2, "TWO"), item(3, "three")])
expect(observations).toEqual(["2:TWO,3:three|2:TWO,3:three"])
dispose()
})
it("updates one existing slot without publishing structure", () => {
const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
keyed.set([item(1, "one"), item(2, "two")])
const structure = keyed.slots()
const two = keyed.slots()[1]
const structures: Array<readonly unknown[]> = []
const dispose = keyed.slots.subscribe((slots) => structures.push(slots))
const updated = item(2, "TWO")
expect(keyed.update(updated)).toBe(true)
expect(keyed.update(updated)).toBe(false)
expect(keyed.slots()).toBe(structure)
expect(keyed.slots()[1]).toBe(two)
expect(two()).toEqual(item(2, "TWO"))
expect(structures).toEqual([])
expect(() => keyed.update(item(3, "three"))).toThrow("Keyed value does not exist: 3")
dispose()
})
it("checks key membership without reading the aggregate", () => {
const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
keyed.set([item(1, "one")])
expect(keyed.has(1)).toBe(true)
expect(keyed.has(2)).toBe(false)
expect(keyed.get(1)).toBe(keyed.slots()[0])
expect(keyed.get(2)).toBeUndefined()
keyed.remove(1)
expect(keyed.has(1)).toBe(false)
expect(keyed.get(1)).toBeUndefined()
})
it("inserts, removes, and moves stable slots", () => {
const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
keyed.set([item(1, "one"), item(3, "three")])
const one = keyed.slots()[0]
const three = keyed.slots()[1]
const two = keyed.insert(item(2, "two"), { before: 3 })
expect(keyed.slots()).toEqual([one, two, three])
const four = keyed.insert(item(4, "four"), { after: 3 })
expect(keyed.slots()).toEqual([one, two, three, four])
expect(keyed.move(3, { before: 1 })).toBe(true)
expect(keyed.slots()).toEqual([three, one, two, four])
expect(keyed.move(3, { before: 1 })).toBe(false)
expect(keyed.move(3, { after: 2 })).toBe(true)
expect(keyed.slots()).toEqual([one, two, three, four])
expect(keyed.move(3, "end")).toBe(true)
expect(keyed.slots()).toEqual([one, two, four, three])
expect(keyed.remove(2)).toBe(true)
expect(keyed.remove(2)).toBe(false)
expect(keyed.slots()).toEqual([one, four, three])
})
it("counts publications and equivalence suppressions when instrumented", () => {
const metrics = Keyed.metrics()
const keyed = Keyed.make<Item, number>({ key: (value) => value.id, metrics })
keyed.set([item(1, "one")])
keyed.update(item(1, "ONE"))
const current = keyed.slots()[0]()
keyed.update(current)
keyed.insert(item(2, "two"))
keyed.move(2, { before: 1 })
keyed.remove(2)
expect(metrics).toEqual({
slotPublications: 1,
structuralPublications: 4,
equivalenceSuppressions: 1,
})
})
it("publishes nothing for a full unchanged rebuild", () => {
const metrics = Keyed.metrics()
const keyed = Keyed.make<Item, number>({ key: (value) => value.id, metrics })
const values = [item(1, "one"), item(2, "two")]
keyed.set(values)
const before = { ...metrics }
keyed.set(values)
expect(metrics.slotPublications).toBe(before.slotPublications)
expect(metrics.structuralPublications).toBe(before.structuralPublications)
expect(metrics.equivalenceSuppressions).toBe(before.equivalenceSuppressions + values.length)
})
})

View file

@ -0,0 +1,39 @@
import { describe, expect, it } from "bun:test"
import { createEffect, createRoot } from "solid-js"
import { State } from "../src"
import { useValue } from "../src/solid"
describe("Quark", () => {
it("does not track state read by subscription listeners", () => {
const source = State.make(1)
const unrelated = State.make(1)
const values: number[] = []
const dispose = source.subscribe((value) => {
unrelated()
values.push(value)
})
source.set(2)
unrelated.set(2)
expect(values).toEqual([2])
dispose()
})
it("bridges values into a Solid owner and disposes with it", () => {
const source = State.make(1)
const values: number[] = []
let dispose = () => {}
createRoot((rootDispose) => {
dispose = rootDispose
const value = useValue(source)
createEffect(() => values.push(value()))
})
source.set(2)
dispose()
source.set(3)
expect(values).toEqual([1, 2])
})
})

View file

@ -92,6 +92,7 @@
"clipboardy": "4.0.0",
"diff": "catalog:",
"effect": "catalog:",
"effect-quark": "workspace:*",
"fuzzysort": "catalog:",
"get-east-asian-width": "catalog:",
"open": "10.1.2",

View file

@ -13,6 +13,7 @@ import {
Switch,
useContext,
} from "solid-js"
import { KeyedFor } from "effect-quark/solid"
import path from "node:path"
import { EOL, tmpdir } from "node:os"
import { mkdir, writeFile } from "node:fs/promises"
@ -213,7 +214,7 @@ export function Session() {
})
const editor = useEditorContext()
const rows = createSessionRows(() => route.sessionID)
const boundaries = createMemo(() => messageBoundaryIDs(rows, messages()))
const boundaries = createMemo(() => messageBoundaryIDs(rows.slots().map((slot) => slot()), messages()))
const [navigationMessage, setNavigationMessage] = createSignal<string>()
const [navigationSlack, setNavigationSlack] = createSignal(0)
@ -928,15 +929,15 @@ export function Session() {
flexGrow={1}
scrollAcceleration={scrollAcceleration()}
>
<For each={rows}>
<KeyedFor each={rows.slots}>
{(row, index) => (
<SessionRowView
row={row}
row={row()}
message={(messageID) => data.session.message.get(route.sessionID, messageID)}
boundaryID={boundaries()[index()]}
/>
)}
</For>
</KeyedFor>
<BackgroundToolHint messages={messages()} />
<Show when={session()?.revert?.messageID}>
<RevertMessage

View file

@ -1,39 +1,73 @@
import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client"
import { createEffect, on, onCleanup, type Accessor } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store"
import { Keyed, Transaction } from "effect-quark"
import { useValue } from "effect-quark/solid"
import { batch, createEffect, on, onCleanup, type Accessor } from "solid-js"
import { useData } from "../../context/data"
import { useClient } from "../../context/client"
export type PartRef = {
messageID: string
partID: string
readonly messageID: string
readonly partID: string
}
export type SessionRow =
export type SessionRow = { readonly id: string } & (
| { type: "message"; messageID: string }
| { type: "compaction-queued"; inputID: string }
| { type: "part"; ref: PartRef }
| {
type: "group"
kind: "reasoning"
origin: PartRef
refs: PartRef[]
completed: boolean
}
| {
type: "group"
kind: "exploration"
origin: PartRef
refs: PartRef[]
pending: PartRef[]
completed: boolean
}
| { type: "assistant-footer"; messageID: string }
)
export function createSessionRows(sessionID: Accessor<string>) {
export function createSessionRows(sessionID: Accessor<string>, options?: { readonly metrics?: Keyed.Metrics }) {
const data = useData()
const client = useClient()
const [rows, setRows] = createStore<SessionRow[]>([])
const reportMetrics = process.env.OPENCODE_QUARK_METRICS === "1"
const metrics = options?.metrics ?? (reportMetrics ? Keyed.metrics() : undefined)
const state = Keyed.make({ key: rowKey, equivalent: sameRow, metrics })
const seenParts = new Set<string>()
const rows = {
slots: useValue(state.slots),
values: state.values,
}
const revertBoundary = () => data.session.get(sessionID())?.revert?.messageID
const setRows = (value: SessionRow[]) => {
batch(() => {
state.set(value)
seenParts.clear()
value.forEach((row) => {
if (row.type === "part") {
seenParts.add(row.id)
return
}
if (row.type !== "group") return
row.refs.forEach((ref) => seenParts.add(partRowID(ref)))
if (row.kind === "exploration") row.pending.forEach((ref) => seenParts.add(partRowID(ref)))
})
})
}
const mutate = (f: () => void) => batch(() => Transaction.run(f))
const insert = (current: readonly SessionRow[], index: number, row: SessionRow) =>
state.insert(row, index === current.length ? "end" : { before: current[index].id })
const complete = (current: readonly SessionRow[], index: number) => {
const previous = current[index - 1]
if (previous?.type === "group" && !previous.completed) state.update({ ...previous, completed: true })
}
function reduce() {
const messages = data.session.message.list(sessionID())
const inputs = new Set(data.session.input.list(sessionID()))
@ -47,7 +81,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
...data.session.pending
.list(sessionID())
.filter((item) => item.type === "compaction")
.map((item): SessionRow => ({ type: "compaction-queued", inputID: item.id })),
.map((item) => compactionQueuedRow(item.id)),
)
return rows
}
@ -62,22 +96,31 @@ export function createSessionRows(sessionID: Accessor<string>) {
createEffect(() => {
const pending = pendingPermissions()
setRows(
produce((draft) => {
partitionPending(draft, pending)
}),
)
mutate(() => {
state.values().forEach((row) => {
if (row.type !== "group" || row.kind !== "exploration") return
const changed =
row.refs.some((ref) => pending.has(ref.partID)) || row.pending.some((ref) => !pending.has(ref.partID))
if (!changed) return
const refs = [...row.refs, ...row.pending]
state.update({
...row,
refs: refs.filter((ref) => !pending.has(ref.partID)),
pending: refs.filter((ref) => pending.has(ref.partID)),
})
})
})
})
createEffect(
on([sessionID, () => client.connection.status()], ([id, status]) => {
if (status !== "connected") return
setRows(reconcile(reduce()))
setRows(reduce())
void data.session.pending.sync(id).catch(() => undefined)
void data.session.message.sync(id).then(
() => {
if (sessionID() !== id) return
setRows(reconcile(reduce()))
setRows(reduce())
},
() => undefined,
)
@ -87,7 +130,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
// Re-reduce when the revert boundary changes (stage/clear/commit).
createEffect(
on(revertBoundary, () => {
setRows(reconcile(reduce()))
setRows(reduce())
}),
)
@ -98,7 +141,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
.list(sessionID())
.filter((item) => item.type === "compaction")
.map((item) => item.id),
() => setRows(reconcile(reduce())),
() => setRows(reduce()),
),
)
@ -123,48 +166,57 @@ export function createSessionRows(sessionID: Accessor<string>) {
]
: [],
),
() => setRows(reconcile(reduce())),
() => setRows(reduce()),
),
)
const appendMessage = (messageID: string) =>
setRows(
produce((draft) => {
if (draft.some((row) => row.type === "message" && row.messageID === messageID)) return
const pending = isPending(messageID)
const message = data.session.message.get(sessionID(), messageID)
const index =
message?.type === "compaction" && pending ? queuedStart(draft) : pending ? draft.length : queuedStart(draft)
if (!pending) completePrevious(draft, index)
draft.splice(index, 0, { type: "message", messageID })
}),
)
mutate(() => {
if (state.has(messageRowID(messageID))) return
const current = state.values()
const pending = isPending(messageID)
const message = data.session.message.get(sessionID(), messageID)
const index =
message?.type === "compaction" && pending
? queuedStart(current)
: pending
? current.length
: queuedStart(current)
if (!pending) complete(current, index)
insert(current, index, messageRow(messageID))
})
const appendPart = (ref: PartRef, part: AppendPart) =>
setRows(
produce((draft) => {
if (hasPart(draft, ref)) return
append(draft, ref, part, queuedStart(draft))
}),
)
mutate(() => {
const id = partRowID(ref)
if (seenParts.has(id)) return
const current = state.values()
const index = queuedStart(current)
const previous = current[index - 1]
const decision = appendDecision(previous, ref, part)
if (decision.type === "join") {
state.update({ ...decision.group, refs: [...decision.group.refs, ref] })
seenParts.add(id)
return
}
complete(current, index)
insert(current, index, decision.row)
seenParts.add(id)
})
const appendFooter = (messageID: string) =>
setRows(
produce((draft) => {
if (draft.some((row) => row.type === "assistant-footer" && row.messageID === messageID)) return
const index = queuedStart(draft)
completePrevious(draft, index)
draft.splice(index, 0, { type: "assistant-footer", messageID })
}),
)
mutate(() => {
if (state.has(footerRowID(messageID))) return
const current = state.values()
const index = queuedStart(current)
complete(current, index)
insert(current, index, footerRow(messageID))
})
const removeFooter = (messageID: string) =>
setRows(
produce((draft) => {
const index = draft.findIndex((row) => row.type === "assistant-footer" && row.messageID === messageID)
if (index !== -1) draft.splice(index, 1)
}),
)
mutate(() => {
state.remove(footerRowID(messageID))
})
const isPending = (messageID: string) => {
const message = data.session.message.get(sessionID(), messageID)
@ -172,7 +224,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
return message?.type === "compaction" && message.status === "running"
}
const queuedStart = (rows: SessionRow[]) => {
const queuedStart = (rows: readonly SessionRow[]) => {
const index = rows.findIndex(
(row) => row.type === "compaction-queued" || (row.type === "message" && isPending(row.messageID)),
)
@ -252,6 +304,9 @@ export function createSessionRows(sessionID: Accessor<string>) {
}),
]
onCleanup(() => subscriptions.forEach((unsubscribe) => unsubscribe()))
onCleanup(() => {
if (reportMetrics && metrics) console.error(`QUARK_TIMELINE_METRICS ${sessionID()} ${JSON.stringify(metrics)}`)
})
return rows
}
@ -268,7 +323,7 @@ export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new S
if (message.type !== "assistant") {
if (message.type === "synthetic" && !message.description?.trim()) return rows
if (!pending.has(message.id)) completePrevious(rows)
rows.push({ type: "message", messageID: message.id })
rows.push(messageRow(message.id))
return rows
}
const ordinals = { text: 0, reasoning: 0 }
@ -279,13 +334,20 @@ export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new S
})
if ((message.finish && !["tool-calls", "unknown"].includes(message.finish)) || message.error || message.retry) {
completePrevious(rows)
rows.push({ type: "assistant-footer", messageID: message.id })
rows.push(footerRow(message.id))
}
return rows
}, [])
}
export function messageBoundaryIDs(rows: SessionRow[], messages: SessionMessageInfo[]) {
type BoundaryRow =
| Pick<Extract<SessionRow, { type: "message" }>, "type" | "messageID">
| Pick<Extract<SessionRow, { type: "compaction-queued" }>, "type">
| Pick<Extract<SessionRow, { type: "part" }>, "type" | "ref">
| Pick<Extract<SessionRow, { type: "group" }>, "type" | "origin">
| Pick<Extract<SessionRow, { type: "assistant-footer" }>, "type" | "messageID">
export function messageBoundaryIDs(rows: readonly BoundaryRow[], messages: SessionMessageInfo[]) {
const byID = new Map(messages.map((message) => [message.id, message]))
const seen = new Set<string>()
return rows.map((row) => {
@ -296,7 +358,7 @@ export function messageBoundaryIDs(rows: SessionRow[], messages: SessionMessageI
})
}
function rowBoundaryMessageID(row: SessionRow, messages: Map<string, SessionMessageInfo>) {
function rowBoundaryMessageID(row: BoundaryRow, messages: Map<string, SessionMessageInfo>) {
if (row.type === "message") {
const message = messages.get(row.messageID)
if (message?.type === "user" && message.text.trim()) return message.id
@ -306,7 +368,7 @@ function rowBoundaryMessageID(row: SessionRow, messages: Map<string, SessionMess
row.type === "part"
? row.ref.messageID
: row.type === "group"
? row.refs[0]?.messageID
? row.origin.messageID
: row.type === "assistant-footer"
? row.messageID
: undefined
@ -327,28 +389,25 @@ export function resolvePart(message: SessionMessageAssistant, partID: string) {
type AppendPart = { type: "text" } | { type: "reasoning" } | { type: "tool"; name: string }
function append(rows: SessionRow[], ref: PartRef, part: AppendPart, index = rows.length) {
if (part.type === "reasoning") {
const previous = rows[index - 1]
if (previous?.type === "group" && previous.kind === "reasoning") {
previous.refs.push(ref)
return
}
completePrevious(rows, index)
rows.splice(index, 0, { type: "group", kind: "reasoning", refs: [ref], completed: false })
return
}
if (part.type === "tool" && exploration(part.name)) {
const previous = rows[index - 1]
if (previous?.type === "group" && previous.kind === "exploration") {
previous.refs.push(ref)
return
}
completePrevious(rows, index)
rows.splice(index, 0, { type: "group", kind: "exploration", refs: [ref], pending: [], completed: false })
const previous = rows[index - 1]
const decision = appendDecision(previous, ref, part)
if (decision.type === "join") {
decision.group.refs.push(ref)
return
}
completePrevious(rows, index)
rows.splice(index, 0, { type: "part", ref })
rows.splice(index, 0, decision.row)
}
function appendDecision(previous: SessionRow | undefined, ref: PartRef, part: AppendPart) {
const kind = groupKind(part)
if (kind && previous?.type === "group" && previous.kind === kind) return { type: "join" as const, group: previous }
return { type: "insert" as const, row: kind ? groupRow(kind, ref) : partRow(ref) }
}
function groupKind(part: AppendPart) {
if (part.type === "reasoning") return "reasoning" as const
if (part.type === "tool" && exploration(part.name)) return "exploration" as const
}
function completePrevious(rows: SessionRow[], index = rows.length) {
@ -369,11 +428,64 @@ function exploration(name: string) {
return ["read", "glob", "grep"].includes(name.toLowerCase())
}
function hasPart(rows: SessionRow[], ref: PartRef) {
return rows.some((row) => {
if (row.type === "part") return row.ref.messageID === ref.messageID && row.ref.partID === ref.partID
if (row.type !== "group") return false
const refs = row.kind === "exploration" ? [...row.refs, ...row.pending] : row.refs
return refs.some((item) => item.messageID === ref.messageID && item.partID === ref.partID)
})
function messageRow(messageID: string): SessionRow {
return { id: messageRowID(messageID), type: "message", messageID }
}
function messageRowID(messageID: string) {
return `m${segment(messageID)}`
}
function compactionQueuedRow(inputID: string): SessionRow {
return { id: `c${segment(inputID)}`, type: "compaction-queued", inputID }
}
function partRow(ref: PartRef): SessionRow {
return { id: partRowID(ref), type: "part", ref }
}
function partRowID(ref: PartRef) {
return `p${segment(ref.messageID)}${segment(ref.partID)}`
}
function groupRow(kind: "reasoning" | "exploration", ref: PartRef): SessionRow {
const id = `g${kind === "reasoning" ? "r" : "e"}${segment(ref.messageID)}${segment(ref.partID)}`
if (kind === "reasoning") return { id, type: "group", kind, origin: ref, refs: [ref], completed: false }
return { id, type: "group", kind, origin: ref, refs: [ref], pending: [], completed: false }
}
function footerRow(messageID: string): SessionRow {
return { id: footerRowID(messageID), type: "assistant-footer", messageID }
}
function footerRowID(messageID: string) {
return `f${segment(messageID)}`
}
function segment(value: string) {
return `${value.length}:${value}`
}
function rowKey(row: SessionRow) {
return row.id
}
function sameRow(left: SessionRow, right: SessionRow) {
if (left.type !== right.type) return false
if (left.type === "message" && right.type === "message") return left.messageID === right.messageID
if (left.type === "compaction-queued" && right.type === "compaction-queued") return left.inputID === right.inputID
if (left.type === "part" && right.type === "part") return sameRef(left.ref, right.ref)
if (left.type === "assistant-footer" && right.type === "assistant-footer") return left.messageID === right.messageID
if (left.type !== "group" || right.type !== "group") return false
if (left.kind !== right.kind || left.completed !== right.completed || !sameRefs(left.refs, right.refs)) return false
if (left.kind === "reasoning" || right.kind === "reasoning") return true
return sameRefs(left.pending, right.pending)
}
function sameRefs(left: PartRef[], right: PartRef[]) {
return left.length === right.length && left.every((ref, index) => sameRef(ref, right[index]))
}
function sameRef(left: PartRef, right: PartRef) {
return left.messageID === right.messageID && left.partID === right.partID
}

View file

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

View file

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