refactor(tui): extract session timeline state

This commit is contained in:
Kit Langton 2026-07-17 21:47:34 -04:00
commit a653744d78
15 changed files with 1897 additions and 586 deletions

View file

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

View file

@ -14,11 +14,14 @@ export namespace Keyed {
readonly values: Readable<readonly A[]>
has(key: Key): boolean
get(key: Key): Readable<A> | undefined
set(values: readonly A[]): void
set(values: readonly A[]): boolean
update(value: A): boolean
modify(key: Key, f: (value: A) => A): boolean
insert(value: A, position?: Position<Key>): Readable<A>
remove(key: Key): boolean
move(key: Key, position?: Position<Key>): boolean
before(key: Key): Readable<A> | undefined
after(key: Key): Readable<A> | undefined
}
export function make<A, Key>(options: {
@ -44,7 +47,8 @@ export namespace Keyed {
const retained = new Set(keys)
if (retained.size !== keys.length) throw new Error("Keyed values must have unique keys")
Transaction.run(() => {
return Transaction.run(() => {
let changed = false
const previous = slots()
const reconciled = next.map((value, index) => {
const key = keys[index]
@ -54,12 +58,7 @@ export namespace Keyed {
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++
}
if (publish(slot, value)) changed = true
return slot
})
byKey.forEach((_slot, key) => {
@ -68,20 +67,23 @@ export namespace Keyed {
if (!same(previous, reconciled)) {
slots.set(reconciled)
if (options.metrics) options.metrics.structuralPublications++
changed = true
}
return changed
})
},
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
return publish(slot, value)
},
modify(key, f) {
const slot = byKey.get(key)
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
const value = f(slot())
if (byKey.get(options.key(value)) !== slot) throw new Error("Keyed modify must preserve the value key")
return publish(slot, value)
},
insert(value, position) {
const key = options.key(value)
@ -118,6 +120,22 @@ export namespace Keyed {
if (options.metrics) options.metrics.structuralPublications++
return true
},
before(key) {
return neighbor(key, -1)
},
after(key) {
return neighbor(key, 1)
},
}
function publish(slot: Writable<A>, value: A) {
if (equivalent(slot(), value)) {
if (options.metrics) options.metrics.equivalenceSuppressions++
return false
}
slot.set(value)
if (options.metrics) options.metrics.slotPublications++
return true
}
function positionIndex(current: readonly Writable<A>[], position?: Position<Key>) {
@ -131,6 +149,13 @@ export namespace Keyed {
if (!target) throw new Error(`Keyed value does not exist: ${String(key)}`)
return current.indexOf(target)
}
function neighbor(key: Key, offset: -1 | 1) {
const slot = byKey.get(key)
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
const current = slots()
return current[current.indexOf(slot) + offset]
}
}
export function metrics(): Metrics {

View file

@ -0,0 +1,660 @@
import { Keyed } from "./keyed"
import { Transaction, type Readable } from "./reactivity"
export namespace Layout {
export interface Field<A> {
readonly isKey?: true
readonly primitive?: true
readonly immutable?: true
equivalent(left: A, right: A): boolean
}
export interface KeyField<A> extends Field<A> {
readonly isKey: true
}
export interface NamedKey<Name extends PropertyKey, A> {
readonly name: Name
readonly field: Field<A>
}
export type Type<Field> = Field extends Layout.Field<infer A> ? A : never
type Fields = Readonly<Record<PropertyKey, Field<unknown>>>
type Value<StructFields> = { readonly [Key in keyof StructFields]: Type<StructFields[Key]> }
type KeyName<StructFields> = {
readonly [Key in keyof StructFields]: StructFields[Key] extends KeyField<unknown> ? Key : never
}[keyof StructFields]
type Variant<Tag extends PropertyKey, Variants> = {
readonly [Name in keyof Variants & string]: { readonly [Key in Tag]: Name } & Type<Variants[Name]>
}[keyof Variants & string]
type KeyedVariant<Name extends PropertyKey, A, Tag extends PropertyKey, Variants> = {
readonly [Key in Name]: A
} & Variant<Tag, Variants>
export interface Struct<StructFields extends Fields> extends Field<Value<StructFields>> {
readonly type: "struct"
readonly fields: StructFields
}
export interface Union<Tag extends PropertyKey, Variants extends Readonly<Record<string, Field<unknown>>>>
extends Field<Variant<Tag, Variants>> {
readonly type: "union"
readonly tag: Tag
readonly variants: Variants
}
export interface KeyedUnion<
Name extends PropertyKey,
A,
Tag extends PropertyKey,
Variants extends Readonly<Record<string, Field<unknown>>>,
> extends Field<KeyedVariant<Name, A, Tag, Variants>> {
readonly type: "keyed-union"
readonly key: NamedKey<Name, A>
readonly tag: Tag
readonly variants: Variants
}
export interface Plan<A, Key> {
readonly key: PropertyKey
readonly fields?: Fields
readonly equivalent: (left: A, right: A) => boolean
readonly keyOf: (value: A) => Key
make(initial?: readonly A[], options?: { readonly metrics?: Keyed.Metrics }): Keyed.Keyed<A, Key>
}
export interface MembersIndex<A, Member> {
readonly type: "members"
readonly extract: (value: A) => Iterable<Member>
}
export interface FirstIndex<A> {
readonly type: "first"
readonly matches: (value: A) => boolean
}
export type Index<A> = MembersIndex<A, unknown> | FirstIndex<A>
type Indexes<A> = Readonly<Record<PropertyKey, Index<A>>>
type MembersNames<Definitions> = {
readonly [Name in keyof Definitions]: Definitions[Name] extends { readonly type: "members" } ? Name : never
}[keyof Definitions]
type FirstNames<Definitions> = {
readonly [Name in keyof Definitions]: Definitions[Name] extends { readonly type: "first" } ? Name : never
}[keyof Definitions]
type Member<Definition> = Definition extends { readonly extract: (value: never) => Iterable<infer A> } ? A : never
type MemberChanges<Definitions> = {
readonly [Name in MembersNames<Definitions>]?: {
readonly add?: readonly Member<Definitions[Name]>[]
readonly remove?: readonly Member<Definitions[Name]>[]
}
}
export interface IndexBuilder<A> {
members<Member>(extract: (value: A) => Iterable<Member>): MembersIndex<A, Member>
first(matches: (value: A) => boolean): FirstIndex<A>
}
export interface Collection<A, Key, Definitions extends Indexes<A>> extends Keyed.Keyed<A, Key> {
modify(key: Key, f: (value: A) => A, changes?: { readonly members?: MemberChanges<Definitions> }): boolean
hasMember<Name extends MembersNames<Definitions>>(name: Name, member: Member<Definitions[Name]>): boolean
first<Name extends FirstNames<Definitions>>(name: Name): Readable<A> | undefined
}
export interface CollectionPlan<A, Key, Definitions extends Indexes<A>> extends Plan<A, Key> {
make(initial?: readonly A[], options?: { readonly metrics?: Keyed.Metrics }): Collection<A, Key, Definitions>
}
export const string: Field<string> = primitive()
export const number: Field<number> = primitive()
export const boolean: Field<boolean> = primitive()
export function array<A>(item: Field<A>): Field<readonly A[]> {
return make((left, right) => {
if (left.length !== right.length) return false
for (let index = 0; index < left.length; index++) {
if (!item.equivalent(left[index], right[index])) return false
}
return true
})
}
export function immutable<A>(field: Field<A>): Field<A> {
return { ...field, immutable: true, equivalent: () => true }
}
export function key<A>(field: Field<A>): KeyField<A>
export function key<const Name extends PropertyKey, A>(name: Name, field: Field<A>): NamedKey<Name, A>
export function key<A>(name: Field<A> | PropertyKey, field?: Field<A>): KeyField<A> | NamedKey<PropertyKey, A> {
if (field) return { name: name as PropertyKey, field }
return { ...(name as Field<A>), isKey: true }
}
export function struct<const StructFields extends Fields>(fields: StructFields): Struct<StructFields> {
return {
type: "struct",
fields,
equivalent: compileFields<Value<StructFields>>(fields),
}
}
export function union<
const Tag extends PropertyKey,
const Variants extends Readonly<Record<string, Field<unknown>>>,
>(options: { readonly tag: Tag; readonly variants: Variants }): Union<Tag, Variants> {
const equivalent = compileUnion<Tag, Variants>(options.tag, options.variants)
return { type: "union", ...options, equivalent }
}
export function keyedUnion<
const Name extends PropertyKey,
A,
const Tag extends PropertyKey,
const Variants extends Readonly<Record<string, Field<unknown>>>,
>(options: {
readonly key: NamedKey<Name, A>
readonly tag: Tag
readonly variants: Variants
}): KeyedUnion<Name, A, Tag, Variants> {
const equivalentVariant = compileUnion<Tag, Variants>(options.tag, options.variants)
const key = (value: KeyedVariant<Name, A, Tag, Variants>) => value[options.key.name]
const equivalent = (left: KeyedVariant<Name, A, Tag, Variants>, right: KeyedVariant<Name, A, Tag, Variants>) =>
options.key.field.equivalent(key(left), key(right)) && equivalentVariant(left, right)
return { type: "keyed-union", ...options, equivalent }
}
export function compile<const StructFields extends Fields>(
layout: Struct<StructFields>,
options?: { readonly backend?: "closure" | "generated" },
): Plan<Value<StructFields>, Value<StructFields>[KeyName<StructFields>]>
export function compile<
const Name extends PropertyKey,
A,
const Tag extends PropertyKey,
const Variants extends Readonly<Record<string, Field<unknown>>>,
>(
layout: KeyedUnion<Name, A, Tag, Variants>,
options?: { readonly backend?: "closure" | "generated" },
): Plan<KeyedVariant<Name, A, Tag, Variants>, A>
export function compile(input: unknown, options: { readonly backend?: "closure" | "generated" } = {}): unknown {
const layout = input as
| Struct<Fields>
| KeyedUnion<PropertyKey, unknown, PropertyKey, Readonly<Record<string, Field<unknown>>>>
if (layout.type === "keyed-union") {
return makePlan(
layout.key.name,
(value: unknown) => (value as Record<PropertyKey, unknown>)[layout.key.name],
(options.backend === "generated"
? generateUnion(layout.tag, layout.variants)
: compileUnion(layout.tag, layout.variants)) as (left: unknown, right: unknown) => boolean,
)
}
const keys = Reflect.ownKeys(layout.fields).filter((name) => layout.fields[name].isKey)
if (keys.length !== 1) throw new Error("Keyed layout must declare exactly one key field")
const key = keys[0]
const fields = Reflect.ownKeys(layout.fields)
.filter((name) => name !== key && !layout.fields[name].immutable)
.map((name) => ({ name, field: layout.fields[name] }))
const equivalent =
options.backend === "generated"
? generateEquivalent<unknown>(generated(fields))
: compileEquivalent<unknown>(fields)
return {
fields: layout.fields,
...makePlan(key, (value: unknown) => (value as Record<PropertyKey, unknown>)[key], equivalent),
}
}
export function collection<const StructFields extends Fields, const Definitions extends Indexes<Value<StructFields>>>(
layout: Struct<StructFields>,
define: (index: IndexBuilder<Value<StructFields>>) => Definitions,
options?: { readonly backend?: "closure" | "generated" },
): CollectionPlan<Value<StructFields>, Value<StructFields>[KeyName<StructFields>], Definitions>
export function collection<
const Name extends PropertyKey,
A,
const Tag extends PropertyKey,
const Variants extends Readonly<Record<string, Field<unknown>>>,
const Definitions extends Indexes<KeyedVariant<Name, A, Tag, Variants>>,
>(
layout: KeyedUnion<Name, A, Tag, Variants>,
define: (index: IndexBuilder<KeyedVariant<Name, A, Tag, Variants>>) => Definitions,
options?: { readonly backend?: "closure" | "generated" },
): CollectionPlan<KeyedVariant<Name, A, Tag, Variants>, A, Definitions>
export function collection(
input: unknown,
define: unknown,
options?: { readonly backend?: "closure" | "generated" },
): unknown {
const plan = compile(input as never, options) as Plan<unknown, unknown>
const definitions = (define as (index: IndexBuilder<unknown>) => Indexes<unknown>)({
members: (extract) => ({ type: "members", extract }),
first: (matches) => ({ type: "first", matches }),
})
return {
...plan,
make(initial: readonly unknown[] = [], makeOptions?: { readonly metrics?: Keyed.Metrics }) {
return makeCollection(plan, definitions, initial, makeOptions)
},
}
}
function makePlan<A, Key>(key: PropertyKey, getKey: (value: A) => Key, equivalent: (left: A, right: A) => boolean) {
return {
key,
equivalent,
keyOf: getKey,
make(initial: readonly A[] = [], options?: { readonly metrics?: Keyed.Metrics }) {
const values = Keyed.make({ key: getKey, equivalent, metrics: options?.metrics })
values.set(initial)
return values
},
}
}
function makeCollection<A, Key, Definitions extends Indexes<A>>(
plan: Plan<A, Key>,
definitions: Definitions,
initial: readonly A[],
options?: { readonly metrics?: Keyed.Metrics },
): Collection<A, Key, Definitions> {
const values = plan.make([], options)
type MemberEntry = {
readonly type: "members"
readonly name: PropertyKey
readonly extract: (value: A) => Iterable<unknown>
readonly counts: Map<unknown, number>
readonly byKey: Map<Key, { readonly source: Iterable<unknown>; readonly members: Set<unknown> }>
}
type FirstEntry = {
readonly type: "first"
readonly name: PropertyKey
readonly matches: (value: A) => boolean
readonly matching: Set<Key>
slot?: Readable<A>
}
type Entry = MemberEntry | FirstEntry
type Inspection =
| {
readonly type: "members"
readonly value: { readonly source: Iterable<unknown>; readonly members: Set<unknown> }
}
| { readonly type: "members-change"; readonly add: readonly unknown[]; readonly remove: readonly unknown[] }
| { readonly type: "first"; readonly value: boolean }
const entries: Entry[] = Reflect.ownKeys(definitions).map((name) => {
const definition = definitions[name]
if (definition.type === "members") {
return { type: "members", name, extract: definition.extract, counts: new Map(), byKey: new Map() }
}
return { type: "first", name, matches: definition.matches, matching: new Set() }
})
const byName = new Map(entries.map((entry) => [entry.name, entry]))
const emptyMembers: readonly unknown[] = []
const collection: Collection<A, Key, Definitions> = {
...values,
set(next) {
const keys = next.map(plan.keyOf)
if (new Set(keys).size !== keys.length) return values.set(next)
const prepared = new Map(
next.map((value, index) => {
const key = keys[index]
const previous = values.get(key)?.()
return [key, previous && plan.equivalent(previous, value) ? current(key) : inspect(value, key)]
}),
)
return Transaction.run(() => {
const changed = values.set(next)
if (!changed) return false
entries.forEach(clear)
values.slots().forEach((slot) => {
const key = plan.keyOf(slot())
const inspection = prepared.get(key)!
entries.forEach((entry, index) => add(entry, key, slot, inspection[index]))
})
entries.forEach((entry) => entry.type === "first" && findFirst(entry))
return true
})
},
update(value) {
const key = plan.keyOf(value)
const slot = values.get(key)
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
const inspection = inspect(value, key)
return Transaction.run(() => {
const changed = values.update(value)
if (changed) entries.forEach((entry, index) => replace(entry, key, slot, inspection[index]))
return changed
})
},
modify(key, f, changes) {
const slot = values.get(key)
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
let inspection: readonly Inspection[] | undefined
return Transaction.run(() => {
const changed = values.modify(key, (previous) => {
const value = f(previous)
if (values.get(plan.keyOf(value)) !== slot) throw new Error("Keyed modify must preserve the value key")
inspection = inspect(value, key, changes?.members)
return value
})
if (changed) entries.forEach((entry, index) => replace(entry, key, slot, inspection![index]))
return changed
})
},
insert(value, position) {
const key = plan.keyOf(value)
if (values.has(key)) return values.insert(value, position)
requirePosition(position)
const inspection = inspect(value, key)
return Transaction.run(() => {
const slot = values.insert(value, position)
entries.forEach((entry, index) => add(entry, key, slot, inspection[index]))
entries.forEach((entry) => entry.type === "first" && updateFirstAfterPlacement(entry, slot))
return slot
})
},
remove(key) {
const slot = values.get(key)
if (!slot) return false
return Transaction.run(() => {
const removed = values.remove(key)
entries.forEach((entry) => remove(entry, key))
entries.forEach((entry) => entry.type === "first" && entry.slot === slot && findFirst(entry))
return removed
})
},
move(key, position) {
const slot = values.get(key)
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
return Transaction.run(() => {
const moved = values.move(key, position)
if (moved) entries.forEach((entry) => entry.type === "first" && updateFirstAfterPlacement(entry, slot))
return moved
})
},
hasMember(name, member) {
const entry = byName.get(normalizeName(name))
return entry?.type === "members" && entry.counts.has(member)
},
first(name) {
const entry = byName.get(normalizeName(name))
return entry?.type === "first" ? entry.slot : undefined
},
}
collection.set(initial)
return collection
function inspect(value: A, key: Key, changes?: Readonly<Record<PropertyKey, unknown>>): readonly Inspection[] {
return entries.map((entry) =>
entry.type === "members"
? (() => {
const change = changes?.[entry.name] as
| { readonly add?: readonly unknown[]; readonly remove?: readonly unknown[] }
| undefined
if (change)
return {
type: "members-change" as const,
add: change.add ?? emptyMembers,
remove: change.remove ?? emptyMembers,
}
const source = entry.extract(value)
const previous = entry.byKey.get(key)
return {
type: "members" as const,
value: { source, members: source === previous?.source ? previous.members : new Set(source) },
}
})()
: { type: "first", value: entry.matches(value) },
)
}
function current(key: Key): readonly Inspection[] {
return entries.map((entry) => {
if (entry.type === "members") return { type: "members", value: entry.byKey.get(key)! }
return { type: "first", value: entry.matching.has(key) }
})
}
function clear(entry: Entry) {
if (entry.type === "members") entry.byKey.clear()
if (entry.type === "first") entry.matching.clear()
if (entry.type === "members") entry.counts.clear()
if (entry.type === "first") entry.slot = undefined
}
function add(entry: Entry, key: Key, slot: Readable<A>, inspection: Inspection) {
if (entry.type === "members" && inspection.type === "members") {
entry.byKey.set(key, inspection.value)
inspection.value.members.forEach((member) => entry.counts.set(member, (entry.counts.get(member) ?? 0) + 1))
return
}
if (entry.type === "first" && inspection.type === "first" && inspection.value) entry.matching.add(key)
}
function remove(entry: Entry, key: Key) {
if (entry.type === "first") {
entry.matching.delete(key)
return
}
entry.byKey.get(key)?.members.forEach((member) => adjust(entry.counts, member, -1))
entry.byKey.delete(key)
}
function replace(entry: Entry, key: Key, slot: Readable<A>, inspection: Inspection) {
if (entry.type === "members" && inspection.type === "members-change") {
const current = entry.byKey.get(key)!
const members = current.members
inspection.remove.forEach((member) => {
if (!members.delete(member)) return
adjust(entry.counts, member, -1)
})
inspection.add.forEach((member) => {
if (members.has(member)) return
members.add(member)
adjust(entry.counts, member, 1)
})
entry.byKey.set(key, { source: members, members })
return
}
if (entry.type === "members" && inspection.type === "members") {
const previous = entry.byKey.get(key)!.members
if (previous === inspection.value.members) {
entry.byKey.set(key, inspection.value)
return
}
inspection.value.members.forEach((member) => !previous.has(member) && adjust(entry.counts, member, 1))
previous.forEach((member) => !inspection.value.members.has(member) && adjust(entry.counts, member, -1))
entry.byKey.set(key, inspection.value)
return
}
if (entry.type !== "first" || inspection.type !== "first") return
const previous = entry.matching.has(key)
if (inspection.value) entry.matching.add(key)
if (!inspection.value) entry.matching.delete(key)
if (entry.slot === slot && !inspection.value) findFirst(entry)
if (entry.slot !== slot && !previous && inspection.value) updateFirstAfterPlacement(entry, slot)
}
function adjust(counts: Map<unknown, number>, member: unknown, amount: 1 | -1) {
const count = (counts.get(member) ?? 0) + amount
if (count === 0) counts.delete(member)
if (count > 0) counts.set(member, count)
}
function updateFirstAfterPlacement(entry: FirstEntry, slot: Readable<A>) {
if (!entry.matching.has(plan.keyOf(slot()))) return
if (!entry.slot) {
entry.slot = slot
return
}
if (entry.slot === slot) return findFirst(entry)
const slots = values.slots()
if (slots.indexOf(slot) < slots.indexOf(entry.slot)) entry.slot = slot
}
function findFirst(entry: FirstEntry) {
entry.slot = values.slots().find((slot) => entry.matching.has(plan.keyOf(slot())))
}
function requirePosition(position?: Keyed.Position<Key>) {
if (!position || position === "end") return
const key = "before" in position ? position.before : position.after
if (!values.has(key)) throw new Error(`Keyed value does not exist: ${String(key)}`)
}
function normalizeName(name: PropertyKey) {
return typeof name === "number" ? String(name) : name
}
}
function make<A>(equivalent: (left: A, right: A) => boolean): Field<A> {
return { equivalent }
}
function primitive<A>(): Field<A> {
return { primitive: true, equivalent: Object.is }
}
function compileUnion<Tag extends PropertyKey, Variants extends Readonly<Record<string, Field<unknown>>>>(
tag: Tag,
variants: Variants,
) {
type A = Variant<Tag, Variants>
return (left: A, right: A) => {
const name = left[tag]
if (name !== right[tag] || typeof name !== "string") return false
const variant = variants[name]
return variant ? variant.equivalent(left, right) : false
}
}
function compileFields<A>(fields: Fields) {
return compileEquivalent<A>(
Reflect.ownKeys(fields)
.filter((name) => !fields[name].immutable)
.map((name) => ({ name, field: fields[name] })),
)
}
function generateEquivalent<A>(
fields: ReadonlyArray<{ readonly name: PropertyKey; readonly field: Field<unknown> }>,
) {
if (fields.some((field) => typeof field.name === "symbol")) return compileEquivalent<A>(fields)
const custom: Array<Field<unknown>["equivalent"]> = []
const comparisons = fields.map((field) => {
const name = JSON.stringify(String(field.name))
if (field.field.primitive) return `Object.is(left[${name}], right[${name}])`
const index = custom.push(field.field.equivalent) - 1
return `custom[${index}](left[${name}], right[${name}])`
})
const factory = Function("custom", `return (left, right) => ${comparisons.join(" && ") || "true"}`) as (
custom: ReadonlyArray<Field<unknown>["equivalent"]>,
) => (left: A, right: A) => boolean
return factory(custom)
}
function generateUnion<Tag extends PropertyKey, Variants extends Readonly<Record<string, Field<unknown>>>>(
tag: Tag,
variants: Variants,
) {
if (typeof tag === "symbol") return compileUnion(tag, variants)
const names = Object.keys(variants)
const custom = names.map((name) => generateField(variants[name]))
const cases = names
.map((name, index) => `case ${JSON.stringify(name)}: return custom[${index}](left, right)`)
.join(";")
const property = JSON.stringify(String(tag))
const factory = Function(
"custom",
`return (left, right) => { if (left[${property}] !== right[${property}]) return false; switch (left[${property}]) { ${cases}; default: return false } }`,
) as (custom: ReadonlyArray<Field<unknown>["equivalent"]>) => (left: unknown, right: unknown) => boolean
return factory(custom)
}
function generateField(field: Field<unknown>): Field<unknown>["equivalent"] {
if (field.immutable) return () => true
const layout = field as Field<unknown> & {
readonly type?: "struct" | "union" | "keyed-union"
readonly fields?: Fields
readonly tag?: PropertyKey
readonly variants?: Readonly<Record<string, Field<unknown>>>
readonly key?: NamedKey<PropertyKey, unknown>
}
if (layout.type === "struct") {
const fields = Reflect.ownKeys(layout.fields!)
.filter((name) => !layout.fields![name].immutable)
.map((name) => ({ name, field: layout.fields![name] }))
return generateEquivalent(generated(fields))
}
if (layout.type === "union") return generateUnion(layout.tag!, layout.variants!)
if (layout.type !== "keyed-union") return field.equivalent
const equivalent = generateUnion(layout.tag!, layout.variants!) as (left: unknown, right: unknown) => boolean
return (left, right) => {
const a = left as Record<PropertyKey, unknown>
const b = right as Record<PropertyKey, unknown>
return layout.key!.field.equivalent(a[layout.key!.name], b[layout.key!.name]) && equivalent(left, right)
}
}
function generated(fields: ReadonlyArray<{ readonly name: PropertyKey; readonly field: Field<unknown> }>) {
return fields.map((field) => ({ ...field, field: { ...field.field, equivalent: generateField(field.field) } }))
}
function compileEquivalent<A>(fields: ReadonlyArray<{ readonly name: PropertyKey; readonly field: Field<unknown> }>) {
const value = (input: A) => input as Record<PropertyKey, unknown>
if (fields.length === 0) return (_left: A, _right: A) => true
if (fields.length === 1) {
const first = fields[0]
return (left: A, right: A) => first.field.equivalent(value(left)[first.name], value(right)[first.name])
}
if (fields.length === 2) {
const first = fields[0]
const second = fields[1]
return (left: A, right: A) => {
const a = value(left)
const b = value(right)
return (
first.field.equivalent(a[first.name], b[first.name]) &&
second.field.equivalent(a[second.name], b[second.name])
)
}
}
if (fields.length === 3) {
const first = fields[0]
const second = fields[1]
const third = fields[2]
return (left: A, right: A) => {
const a = value(left)
const b = value(right)
return (
first.field.equivalent(a[first.name], b[first.name]) &&
second.field.equivalent(a[second.name], b[second.name]) &&
third.field.equivalent(a[third.name], b[third.name])
)
}
}
if (fields.length === 4) {
const first = fields[0]
const second = fields[1]
const third = fields[2]
const fourth = fields[3]
return (left: A, right: A) => {
const a = value(left)
const b = value(right)
return (
first.field.equivalent(a[first.name], b[first.name]) &&
second.field.equivalent(a[second.name], b[second.name]) &&
third.field.equivalent(a[third.name], b[third.name]) &&
fourth.field.equivalent(a[fourth.name], b[fourth.name])
)
}
}
return (left: A, right: A) => {
const a = value(left)
const b = value(right)
return fields.every((field) => field.field.equivalent(a[field.name], b[field.name]))
}
}
}

View file

@ -131,6 +131,20 @@ describe("Keyed", () => {
dispose()
})
it("modifies one existing slot while preserving its key", () => {
const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
keyed.set([item(1, "one")])
const slot = keyed.slots()[0]
expect(keyed.modify(1, (value) => ({ ...value, label: "ONE" }))).toBe(true)
expect(keyed.modify(1, (value) => value)).toBe(false)
expect(keyed.slots()[0]).toBe(slot)
expect(slot()).toEqual(item(1, "ONE"))
expect(() => keyed.modify(1, (value) => ({ ...value, id: 2 }))).toThrow("Keyed modify must preserve the value key")
expect(() => keyed.modify(2, (value) => value)).toThrow("Keyed value does not exist: 2")
})
it("checks key membership without reading the aggregate", () => {
const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
keyed.set([item(1, "one")])
@ -139,6 +153,8 @@ describe("Keyed", () => {
expect(keyed.has(2)).toBe(false)
expect(keyed.get(1)).toBe(keyed.slots()[0])
expect(keyed.get(2)).toBeUndefined()
expect(keyed.before(1)).toBeUndefined()
expect(keyed.after(1)).toBeUndefined()
keyed.remove(1)
expect(keyed.has(1)).toBe(false)
expect(keyed.get(1)).toBeUndefined()
@ -154,6 +170,8 @@ describe("Keyed", () => {
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.before(3)).toBe(two)
expect(keyed.after(3)).toBe(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)

View file

@ -0,0 +1,253 @@
import { describe, expect, it } from "bun:test"
import { Layout } from "../src"
const Item = Layout.struct({
id: Layout.key(Layout.number),
label: Layout.string,
})
const Job = Layout.struct({
id: Layout.key(Layout.string),
labels: Layout.array(Layout.string),
status: Layout.string,
})
const Jobs = Layout.collection(Job, ({ members, first }) => ({
labels: members((job) => job.labels),
nextRetry: first((job) => job.status === "retrying"),
}))
describe("Layout", () => {
it("compiles a keyed collection from trusted structural metadata", () => {
const plan = Layout.compile(Item)
const original = { id: 1, label: "one" }
const values = plan.make([original])
const slot = values.slots()[0]
expect(plan.key).toBe("id")
expect(plan.fields).toBe(Item.fields)
expect(values.update({ id: 1, label: "one" })).toBe(false)
expect(slot()).toBe(original)
expect(values.update({ id: 1, label: "ONE" })).toBe(true)
expect(slot()).toEqual({ id: 1, label: "ONE" })
})
it("requires exactly one key field", () => {
expect(() => Layout.compile(Layout.struct({ value: Layout.number }))).toThrow(
"Keyed layout must declare exactly one key field",
)
expect(() =>
Layout.compile(Layout.struct({ left: Layout.key(Layout.number), right: Layout.key(Layout.number) })),
).toThrow("Keyed layout must declare exactly one key field")
})
it("generates the same trusted equivalence as the closure backend", () => {
const closure = Layout.compile(Item)
const generated = Layout.compile(Item, { backend: "generated" })
const values = [
{ id: 1, label: "one" },
{ id: 1, label: "ONE" },
{ id: 2, label: "one" },
]
values.forEach((left) => {
values.forEach((right) => {
expect(generated.equivalent(left, right)).toBe(closure.equivalent(left, right))
})
})
})
it("compiles nested discriminated unions and skips immutable fields", () => {
const Ref = Layout.struct({ messageID: Layout.string, partID: Layout.string })
const Row = Layout.keyedUnion({
key: Layout.key("id", Layout.string),
tag: "type",
variants: {
message: Layout.struct({ messageID: Layout.string }),
group: Layout.union({
tag: "kind",
variants: {
reasoning: Layout.struct({
origin: Layout.immutable(Ref),
refs: Layout.array(Ref),
completed: Layout.boolean,
}),
exploration: Layout.struct({
origin: Layout.immutable(Ref),
refs: Layout.array(Ref),
pending: Layout.array(Ref),
completed: Layout.boolean,
}),
},
}),
},
})
const plan = Layout.compile(Row)
const generated = Layout.compile(Row, { backend: "generated" })
const group = {
id: "group-1",
type: "group" as const,
kind: "exploration" as const,
origin: { messageID: "assistant-1", partID: "read-1" },
refs: [{ messageID: "assistant-1", partID: "read-1" }],
pending: [] as Array<{ messageID: string; partID: string }>,
completed: false,
}
expect(plan.equivalent(group, { ...group, origin: { messageID: "ignored", partID: "ignored" } })).toBe(true)
expect(plan.equivalent(group, { ...group, completed: true })).toBe(false)
expect(plan.equivalent(group, { ...group, pending: [{ messageID: "assistant-1", partID: "read-1" }] })).toBe(false)
expect(
plan.equivalent(group, {
id: "group-1",
type: "group",
kind: "reasoning",
origin: group.origin,
refs: group.refs,
completed: false,
}),
).toBe(false)
const candidates = [
group,
{ ...group, origin: { messageID: "ignored", partID: "ignored" } },
{ ...group, completed: true },
{ ...group, pending: [{ messageID: "assistant-1", partID: "read-1" }] },
{
id: "group-1",
type: "group" as const,
kind: "reasoning" as const,
origin: group.origin,
refs: group.refs,
completed: false,
},
]
candidates.forEach((left) => {
candidates.forEach((right) => expect(generated.equivalent(left, right)).toBe(plan.equivalent(left, right)))
})
})
it("includes nested keys in structural equivalence", () => {
const Child = Layout.struct({ id: Layout.key(Layout.number), value: Layout.string })
const Parent = Layout.struct({ id: Layout.key(Layout.number), child: Child })
const parent = Layout.compile(Parent)
expect(
parent.equivalent({ id: 1, child: { id: 1, value: "same" } }, { id: 1, child: { id: 2, value: "same" } }),
).toBe(false)
})
it("composes indexed collections without domain-specific behavior", () => {
const jobs = Jobs.make([
{ id: "one", labels: ["billing", "urgent"], status: "running" },
{ id: "two", labels: ["billing"], status: "retrying" },
{ id: "three", labels: [], status: "retrying" },
])
expect(jobs.hasMember("labels", "billing")).toBe(true)
expect(jobs.hasMember("labels", "missing")).toBe(false)
expect(jobs.first("nextRetry")?.().id).toBe("two")
expect(jobs.before("two")?.().id).toBe("one")
expect(jobs.after("two")?.().id).toBe("three")
jobs.modify("one", (job) => ({ ...job, labels: [], status: "retrying" }))
expect(jobs.hasMember("labels", "urgent")).toBe(false)
expect(jobs.hasMember("labels", "billing")).toBe(true)
expect(jobs.first("nextRetry")?.().id).toBe("one")
jobs.remove("two")
expect(jobs.hasMember("labels", "billing")).toBe(false)
jobs.move("three", { before: "one" })
expect(jobs.first("nextRetry")?.().id).toBe("three")
})
it("keeps indexes synchronized across inserts, updates, and replacement", () => {
const jobs = Jobs.make([{ id: "one", labels: ["one"], status: "running" }])
jobs.insert({ id: "three", labels: ["shared"], status: "retrying" })
jobs.insert({ id: "two", labels: ["shared"], status: "retrying" }, { before: "three" })
expect(jobs.first("nextRetry")?.().id).toBe("two")
jobs.update({ id: "two", labels: [], status: "done" })
expect(jobs.first("nextRetry")?.().id).toBe("three")
expect(jobs.hasMember("labels", "shared")).toBe(true)
jobs.remove("three")
expect(jobs.first("nextRetry")).toBeUndefined()
expect(jobs.hasMember("labels", "shared")).toBe(false)
jobs.set([
{ id: "four", labels: ["replacement"], status: "retrying" },
{ id: "five", labels: [], status: "running" },
])
expect(jobs.hasMember("labels", "replacement")).toBe(true)
expect(jobs.hasMember("labels", "one")).toBe(false)
expect(jobs.first("nextRetry")?.().id).toBe("four")
})
it("applies explicit member deltas without re-extracting unchanged membership", () => {
let extractions = 0
const IndexedJobs = Layout.collection(Job, ({ members }) => ({
labels: members((job) => {
extractions++
return job.labels
}),
}))
const jobs = IndexedJobs.make([
{ id: "one", labels: ["billing"], status: "running" },
{ id: "two", labels: ["billing"], status: "running" },
])
const before = extractions
jobs.modify("one", (job) => ({ ...job, labels: ["urgent"] }), {
members: { labels: { add: ["urgent"], remove: ["billing"] } },
})
expect(extractions).toBe(before)
expect(jobs.hasMember("labels", "billing")).toBe(true)
expect(jobs.hasMember("labels", "urgent")).toBe(true)
jobs.modify("two", (job) => ({ ...job, labels: [] }), { members: { labels: { remove: ["billing"] } } })
expect(jobs.hasMember("labels", "billing")).toBe(false)
if (false) {
// @ts-expect-error Member deltas require arrays so string members are not split into characters.
jobs.modify("one", (job) => job, { members: { labels: { add: "urgent" } } })
}
})
it("does not commit mutations when an index callback throws", () => {
const ThrowingJobs = Layout.collection(Job, ({ members, first }) => ({
labels: members((job) => {
if (job.labels.includes("boom")) throw new Error("boom")
return job.labels
}),
nextRetry: first((job) => job.status === "retrying"),
}))
const jobs = ThrowingJobs.make([{ id: "one", labels: ["safe"], status: "running" }])
expect(() => jobs.update({ id: "one", labels: ["boom"], status: "retrying" })).toThrow("boom")
expect(() => jobs.set([{ id: "two", labels: ["boom"], status: "retrying" }])).toThrow("boom")
expect(jobs.values()).toEqual([{ id: "one", labels: ["safe"], status: "running" }])
expect(jobs.hasMember("labels", "safe")).toBe(true)
expect(jobs.hasMember("labels", "boom")).toBe(false)
expect(jobs.first("nextRetry")).toBeUndefined()
expect(() => jobs.insert({ id: "two", labels: ["boom"], status: "retrying" }, { before: "missing" })).toThrow(
"Keyed value does not exist: missing",
)
})
it("tracks first matches whose key is undefined", () => {
const undefinedField: Layout.Field<undefined> = { equivalent: Object.is }
const OptionalJobs = Layout.collection(
Layout.struct({ id: Layout.key(undefinedField), status: Layout.string }),
({ first }) => ({ retry: first((job) => job.status === "retrying") }),
)
const jobs = OptionalJobs.make([{ id: undefined, status: "running" }])
jobs.update({ id: undefined, status: "retrying" })
expect(jobs.first("retry")?.()).toEqual({ id: undefined, status: "retrying" })
})
})

View file

@ -0,0 +1,120 @@
import { Keyed } from "effect-quark"
import { createStore, produce } from "solid-js/store"
import { SessionTimeline, type PartRef } from "../src/routes/session/timeline"
import { createHarness, type Workload } from "../../quark/bench/harness"
type Group = {
readonly id: "group"
readonly type: "group"
readonly refs: readonly PartRef[]
}
const bench = createHarness({ warmup: 500 })
function timelineAppend(): Workload {
const timeline = SessionTimeline.make()
let ordinal = 0
return {
run() {
timeline.appendPart({ messageID: "assistant", partID: `reasoning:${ordinal++}` }, { type: "reasoning" })
},
consume: () => {
const row = timeline.values()[0]
return row?.type === "group" ? row.refs.length : 0
},
}
}
function keyedAppend(): Workload {
const seen = new Set<string>()
const rows = Keyed.make<Group, Group["id"]>({
key: (row) => row.id,
equivalent: (left, right) =>
left.refs.length === right.refs.length &&
left.refs.every(
(ref, index) => ref.messageID === right.refs[index].messageID && ref.partID === right.refs[index].partID,
),
})
rows.set([{ id: "group", type: "group", refs: [] }])
let ordinal = 0
return {
run() {
const ref = { messageID: "assistant", partID: `reasoning:${ordinal++}` }
if (seen.has(ref.partID)) return
rows.modify("group", (group) => ({ ...group, refs: [...group.refs, ref] }))
seen.add(ref.partID)
},
consume: () => rows.get("group")!().refs.length,
}
}
function solidAppend(): Workload {
const [rows, setRows] = createStore<Array<{ type: "group"; refs: PartRef[] }>>([{ type: "group", refs: [] }])
let ordinal = 0
return {
run() {
const ref = { messageID: "assistant", partID: `reasoning:${ordinal++}` }
setRows(
produce((draft) => {
if (draft[0].refs.some((item) => item.messageID === ref.messageID && item.partID === ref.partID)) return
draft[0].refs.push(ref)
}),
)
},
consume: () => rows[0].refs.length,
}
}
function timelineDuplicate(size: number): Workload {
const timeline = SessionTimeline.make()
Array.from({ length: size }, (_, ordinal) =>
timeline.appendPart({ messageID: "assistant", partID: `reasoning:${ordinal}` }, { type: "reasoning" }),
)
const duplicate = { messageID: "assistant", partID: `reasoning:${size - 1}` }
return {
run: () => timeline.appendPart(duplicate, { type: "reasoning" }),
consume: () => timeline.values().length,
}
}
function solidDuplicate(size: number): Workload {
const refs = Array.from(
{ length: size },
(_, ordinal): PartRef => ({ messageID: "assistant", partID: `reasoning:${ordinal}` }),
)
const [rows, setRows] = createStore([{ type: "group" as const, refs }])
const duplicate = refs.at(-1)!
return {
run() {
setRows(
produce((draft) => {
if (draft[0].refs.some((item) => item.messageID === duplicate.messageID && item.partID === duplicate.partID))
return
draft[0].refs.push(duplicate)
}),
)
},
consume: () => rows.length,
}
}
console.log(`Session timeline benchmark (${bench.samples} samples)\n`)
const append = bench.compare(2_000, [
{ name: "SessionTimeline grouped append", make: timelineAppend },
{ name: "Handwritten Keyed + Set append", make: keyedAppend },
{ name: "Solid Store produce append", make: solidAppend },
])
const duplicate = bench.compare(10_000, [
{ name: "SessionTimeline duplicate 1000", make: () => timelineDuplicate(1_000) },
{ name: "Solid Store duplicate 1000", make: () => solidDuplicate(1_000) },
])
console.log("\nRatios (lower is faster)")
console.log(`Timeline / handwritten append: ${append.ratio(0, 1).toFixed(3)}x`)
console.log(`Timeline / Solid append: ${append.ratio(0, 2).toFixed(3)}x`)
console.log(`Timeline / Solid duplicate: ${duplicate.ratio(0, 1).toFixed(3)}x`)
console.log(`METRIC timeline_handwritten_append_ratio=${append.ratio(0, 1).toFixed(6)}`)
console.log(`METRIC timeline_solid_append_ratio=${append.ratio(0, 2).toFixed(6)}`)
console.log(`METRIC timeline_solid_duplicate_ratio=${duplicate.ratio(0, 1).toFixed(6)}`)
bench.finish()

View file

@ -6,6 +6,7 @@
"type": "module",
"license": "MIT",
"scripts": {
"bench:timeline": "bun --conditions=browser bench/session-timeline.ts",
"test": "bun test --timeout 30000 --only-failures",
"typecheck": "tsgo --noEmit"
},

View file

@ -81,7 +81,8 @@ import { PluginSlot } from "../../plugin/context"
import { Keymap, type KeymapCommand } from "../../context/keymap"
import { usePathFormatter } from "../../context/path-format"
import { useLocation } from "../../context/location"
import { createSessionRows, messageBoundaryIDs, resolvePart, type PartRef, type SessionRow } from "./rows"
import { createSessionRows } from "./rows"
import { messageBoundaryIDs, resolvePart, type PartRef, type SessionRow } from "./timeline"
import { switchLabel } from "../../util/model"
import { findMessageBoundary, messageNavigationSlack } from "./message-navigation"
import { stringWidth } from "../../util/string-width"
@ -214,7 +215,12 @@ export function Session() {
})
const editor = useEditorContext()
const rows = createSessionRows(() => route.sessionID)
const boundaries = createMemo(() => messageBoundaryIDs(rows.slots().map((slot) => slot()), messages()))
const boundaries = createMemo(() =>
messageBoundaryIDs(
rows.slots().map((slot) => slot()),
messages(),
),
)
const [navigationMessage, setNavigationMessage] = createSignal<string>()
const [navigationSlack, setNavigationSlack] = createSignal(0)
@ -1165,7 +1171,7 @@ function SessionPartView(props: { partRef: PartRef; message: (messageID: string)
}
function SessionReasoningGroupView(props: {
refs: PartRef[]
refs: readonly PartRef[]
completed: boolean
message: (messageID: string) => SessionMessageInfo | undefined
}) {
@ -1286,8 +1292,8 @@ function SessionReasoningGroupView(props: {
}
function SessionGroupView(props: {
refs: PartRef[]
pending: PartRef[]
refs: readonly PartRef[]
pending: readonly PartRef[]
completed: boolean
message: (messageID: string) => SessionMessageInfo | undefined
}) {
@ -1296,7 +1302,7 @@ function SessionGroupView(props: {
const renderer = useRenderer()
const [expanded, setExpanded] = createSignal(false)
const [hover, setHover] = createSignal(false)
const parts = (refs: PartRef[]) =>
const parts = (refs: readonly PartRef[]) =>
refs.flatMap((ref) => {
const message = props.message(ref.messageID)
if (message?.type !== "assistant") return []

View file

@ -1,79 +1,48 @@
import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client"
import { Keyed, Transaction } from "effect-quark"
import { Keyed } 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 = {
readonly messageID: string
readonly partID: string
}
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 }
)
import {
SessionTimeline,
compactionQueuedRow,
isTerminalFinish,
reduceSessionRows,
type AppendPart,
type PartRef,
type SessionRow,
} from "./timeline"
export function createSessionRows(sessionID: Accessor<string>, options?: { readonly metrics?: Keyed.Metrics }) {
const data = useData()
const client = useClient()
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 state = SessionTimeline.make({ metrics })
const rows = {
slots: useValue(state.slots),
values: state.values,
}
const revertBoundary = () => data.session.get(sessionID())?.revert?.messageID
const isPending = (messageID: string) => {
const message = data.session.message.get(sessionID(), messageID)
if (message?.type === "user" || message?.type === "synthetic") return data.session.input.has(sessionID(), messageID)
return message?.type === "compaction" && message.status === "running"
}
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)))
})
state.replace(value, isPending, pendingPermissions())
})
}
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 })
}
const mutate = (f: () => void) => batch(f)
function reduce() {
const messages = data.session.message.list(sessionID())
const inputs = new Set(data.session.input.list(sessionID()))
const boundary = revertBoundary()
const rows = reduceSessionRows(boundary ? messages.filter((message) => message.id < boundary) : messages, inputs)
partitionPending(rows, pendingPermissions())
const position = rows.findIndex((row) => row.type === "message" && inputs.has(row.messageID))
rows.splice(
position === -1 ? rows.length : position,
@ -96,20 +65,7 @@ export function createSessionRows(sessionID: Accessor<string>, options?: { reado
createEffect(() => {
const pending = pendingPermissions()
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)),
})
})
})
mutate(() => state.repartition(pending))
})
createEffect(
@ -162,6 +118,8 @@ export function createSessionRows(sessionID: Accessor<string>, options?: { reado
{
id: message.id,
created: message.time.created,
input: false,
status: message.status,
},
]
: [],
@ -172,64 +130,16 @@ export function createSessionRows(sessionID: Accessor<string>, options?: { reado
const appendMessage = (messageID: string) =>
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))
state.appendMessage(messageID, { pending, compaction: message?.type === "compaction" })
})
const appendPart = (ref: PartRef, part: AppendPart) =>
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 appendPart = (ref: PartRef, part: AppendPart) => mutate(() => state.appendPart(ref, part))
const appendFooter = (messageID: string) =>
mutate(() => {
if (state.has(footerRowID(messageID))) return
const current = state.values()
const index = queuedStart(current)
complete(current, index)
insert(current, index, footerRow(messageID))
})
const appendFooter = (messageID: string) => mutate(() => state.appendFooter(messageID))
const removeFooter = (messageID: string) =>
mutate(() => {
state.remove(footerRowID(messageID))
})
const isPending = (messageID: string) => {
const message = data.session.message.get(sessionID(), messageID)
if (message?.type === "user" || message?.type === "synthetic") return data.session.input.has(sessionID(), messageID)
return message?.type === "compaction" && message.status === "running"
}
const queuedStart = (rows: readonly SessionRow[]) => {
const index = rows.findIndex(
(row) => row.type === "compaction-queued" || (row.type === "message" && isPending(row.messageID)),
)
return index === -1 ? rows.length : index
}
const removeFooter = (messageID: string) => mutate(() => state.removeFooter(messageID))
const message = (event: { id: string; data: { sessionID: string } }) => {
if (event.data.sessionID === sessionID()) appendMessage(event.id.replace(/^evt_/, "msg_"))
@ -296,7 +206,7 @@ export function createSessionRows(sessionID: Accessor<string>, options?: { reado
if (event.data.sessionID === sessionID()) removeFooter(event.data.assistantMessageID)
}),
data.on("session.step.ended", (event) => {
if (event.data.sessionID !== sessionID() || ["tool-calls", "unknown"].includes(event.data.finish)) return
if (event.data.sessionID !== sessionID() || !isTerminalFinish(event.data.finish)) return
appendFooter(event.data.assistantMessageID)
}),
data.on("session.step.failed", (event) => {
@ -310,182 +220,3 @@ export function createSessionRows(sessionID: Accessor<string>, options?: { reado
return rows
}
export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new Set<string>()) {
const isInput = (message: SessionMessageInfo) => inputs.has(message.id)
const pendingCompactions = messages.filter((message) => message.type === "compaction" && message.status === "running")
const pending = new Set([...pendingCompactions.map((message) => message.id), ...inputs])
return [
...messages.filter((message) => !pending.has(message.id)),
...pendingCompactions,
...messages.filter(isInput),
].reduce<SessionRow[]>((rows, message) => {
if (message.type !== "assistant") {
if (message.type === "synthetic" && !message.description?.trim()) return rows
if (!pending.has(message.id)) completePrevious(rows)
rows.push(messageRow(message.id))
return rows
}
const ordinals = { text: 0, reasoning: 0 }
message.content.forEach((part) => {
const partID = part.type === "tool" ? part.id : `${part.type}:${ordinals[part.type]++}`
if ((part.type === "text" || part.type === "reasoning") && !part.text.trim()) return
append(rows, { messageID: message.id, partID }, part)
})
if ((message.finish && !["tool-calls", "unknown"].includes(message.finish)) || message.error || message.retry) {
completePrevious(rows)
rows.push(footerRow(message.id))
}
return rows
}, [])
}
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) => {
const id = rowBoundaryMessageID(row, byID)
if (!id || seen.has(id)) return undefined
seen.add(id)
return id
})
}
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
return undefined
}
const messageID =
row.type === "part"
? row.ref.messageID
: row.type === "group"
? row.origin.messageID
: row.type === "assistant-footer"
? row.messageID
: undefined
if (!messageID) return undefined
const message = messages.get(messageID)
if (message?.type === "assistant") return message.id
}
export function resolvePart(message: SessionMessageAssistant, partID: string) {
const tool = message.content.find((part) => part.type === "tool" && part.id === partID)
if (tool) return tool
const match = /^(text|reasoning):(\d+)$/.exec(partID)
if (!match) return
const ordinal = Number(match[2])
return message.content.filter((part) => part.type === match[1])[ordinal]
}
type AppendPart = { type: "text" } | { type: "reasoning" } | { type: "tool"; name: string }
function append(rows: SessionRow[], ref: PartRef, part: AppendPart, index = rows.length) {
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, 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) {
const previous = rows[index - 1]
if (previous?.type === "group") previous.completed = true
}
function partitionPending(rows: SessionRow[], pending: Set<string>) {
rows.forEach((row) => {
if (row.type !== "group" || row.kind !== "exploration") return
const refs = [...row.refs, ...row.pending]
row.refs = refs.filter((ref) => !pending.has(ref.partID))
row.pending = refs.filter((ref) => pending.has(ref.partID))
})
}
function exploration(name: string) {
return ["read", "glob", "grep"].includes(name.toLowerCase())
}
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

@ -0,0 +1,328 @@
import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client"
import { Keyed, Layout, Transaction } from "effect-quark"
const PartRefLayout = Layout.struct({
messageID: Layout.string,
partID: Layout.string,
})
const SessionRowLayout = Layout.keyedUnion({
key: Layout.key("id", Layout.string),
tag: "type",
variants: {
message: Layout.struct({ messageID: Layout.string }),
"compaction-queued": Layout.struct({ inputID: Layout.string }),
part: Layout.struct({ ref: PartRefLayout }),
group: Layout.union({
tag: "kind",
variants: {
reasoning: Layout.struct({
origin: Layout.immutable(PartRefLayout),
refs: Layout.array(PartRefLayout),
completed: Layout.boolean,
}),
exploration: Layout.struct({
origin: Layout.immutable(PartRefLayout),
refs: Layout.array(PartRefLayout),
pending: Layout.array(PartRefLayout),
completed: Layout.boolean,
}),
},
}),
"assistant-footer": Layout.struct({ messageID: Layout.string }),
},
})
export type PartRef = Layout.Type<typeof PartRefLayout>
export type SessionRow = Layout.Type<typeof SessionRowLayout>
export type AppendPart = { type: "text" } | { type: "reasoning" } | { type: "tool"; name: string }
const SessionRows = Layout.collection(
SessionRowLayout,
({ members }) => ({
parts: members((row) => {
if (row.type === "part") return [row.id]
if (row.type !== "group") return []
if (row.kind === "reasoning") return row.refs.map(partRowID)
return [...row.refs, ...row.pending].map(partRowID)
}),
}),
{ backend: "generated" },
)
export namespace SessionTimeline {
export function make(options?: { readonly metrics?: Keyed.Metrics }) {
const state = SessionRows.make([], options)
let activeGroupID: string | undefined
let queuedBoundaryID: string | undefined
const queuedRowIDs = new Set<string>()
const insert = (row: SessionRow) => state.insert(row, queuedBoundaryID ? { before: queuedBoundaryID } : "end")
const complete = () => {
if (!activeGroupID) return
state.modify(activeGroupID, (row) => (row.type === "group" ? { ...row, completed: true } : row))
activeGroupID = undefined
}
const nextQueued = (key: string) => {
const row = state.after(key)?.()
return row && queuedRowIDs.has(row.id) ? row.id : undefined
}
const replace = (
rows: readonly SessionRow[],
isQueued: (messageID: string) => boolean,
pending: ReadonlySet<string>,
) =>
Transaction.run(() => {
queuedBoundaryID = undefined
activeGroupID = undefined
queuedRowIDs.clear()
state.set(
rows.map((row) => {
const next = partition(row, pending)
const queued = next.type === "compaction-queued" || (next.type === "message" && isQueued(next.messageID))
if (queued) {
queuedRowIDs.add(next.id)
queuedBoundaryID ??= next.id
return next
}
if (queuedBoundaryID) return next
activeGroupID = next.type === "group" && !next.completed ? next.id : undefined
return next
}),
)
})
const appendMessage = (messageID: string, status: { readonly pending: boolean; readonly compaction: boolean }) => {
const id = messageRowID(messageID)
const exists = state.has(id)
if (exists && (status.pending || !queuedRowIDs.has(id))) return
Transaction.run(() => {
const row = messageRow(messageID)
if (status.pending) {
queuedRowIDs.add(row.id)
if (!status.compaction) {
state.insert(row, "end")
queuedBoundaryID ??= row.id
return
}
insert(row)
queuedBoundaryID = row.id
return
}
if (!exists) {
complete()
insert(row)
return
}
queuedRowIDs.delete(row.id)
complete()
if (queuedBoundaryID === row.id) {
queuedBoundaryID = nextQueued(row.id)
return
}
if (queuedBoundaryID) state.move(row.id, { before: queuedBoundaryID })
})
}
const appendPart = (ref: PartRef, part: AppendPart) => {
const id = partRowID(ref)
if (state.hasMember("parts", id)) return
Transaction.run(() => {
const kind = groupKind(part)
const active = activeGroupID ? state.get(activeGroupID)?.() : undefined
if (kind && active?.type === "group" && active.kind === kind) {
state.modify(active.id, (row) => (row.type === "group" ? { ...row, refs: [...row.refs, ref] } : row), {
members: { parts: { add: [id] } },
})
return
}
complete()
const row = kind ? groupRow(kind, ref) : partRow(ref)
insert(row)
activeGroupID = row.type === "group" ? row.id : undefined
})
}
const appendFooter = (messageID: string) => {
const id = footerRowID(messageID)
if (state.has(id)) return
Transaction.run(() => {
const row = footerRow(messageID)
complete()
insert(row)
})
}
const removeFooter = (messageID: string) => state.remove(footerRowID(messageID))
const repartition = (pending: ReadonlySet<string>) =>
Transaction.run(() => {
state.values().forEach((row) => {
if (row.type !== "group" || row.kind !== "exploration") return
const next = partition(row, pending)
if (next !== row) state.modify(row.id, () => next)
})
})
return {
slots: state.slots,
values: state.values,
replace,
appendMessage,
appendPart,
appendFooter,
removeFooter,
repartition,
}
}
}
export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new Set<string>()) {
const isInput = (message: SessionMessageInfo) => inputs.has(message.id)
const pendingCompactions = messages.filter((message) => message.type === "compaction" && message.status === "running")
const pending = new Set([...pendingCompactions.map((message) => message.id), ...inputs])
return [
...messages.filter((message) => !pending.has(message.id)),
...pendingCompactions,
...messages.filter(isInput),
].reduce<SessionRow[]>((rows, message) => {
if (message.type !== "assistant") {
if (message.type === "synthetic" && !message.description?.trim()) return rows
if (!pending.has(message.id)) completePrevious(rows)
rows.push(messageRow(message.id))
return rows
}
const ordinals = { text: 0, reasoning: 0 }
message.content.forEach((part) => {
const partID = part.type === "tool" ? part.id : `${part.type}:${ordinals[part.type]++}`
if ((part.type === "text" || part.type === "reasoning") && !part.text.trim()) return
append(rows, { messageID: message.id, partID }, part)
})
if (isTerminalFinish(message.finish) || message.error || message.retry) {
completePrevious(rows)
rows.push(footerRow(message.id))
}
return rows
}, [])
}
export function messageBoundaryIDs(rows: readonly SessionRow[], messages: SessionMessageInfo[]) {
const byID = new Map(messages.map((message) => [message.id, message]))
const seen = new Set<string>()
return rows.map((row) => {
const id = rowBoundaryMessageID(row, byID)
if (!id || seen.has(id)) return undefined
seen.add(id)
return id
})
}
function rowBoundaryMessageID(row: SessionRow, messages: Map<string, SessionMessageInfo>) {
if (row.type === "message") {
const message = messages.get(row.messageID)
if (message?.type === "user" && message.text.trim()) return message.id
return undefined
}
const messageID =
row.type === "part"
? row.ref.messageID
: row.type === "group"
? row.origin.messageID
: row.type === "assistant-footer"
? row.messageID
: undefined
if (!messageID) return undefined
const message = messages.get(messageID)
if (message?.type === "assistant") return message.id
}
export function resolvePart(message: SessionMessageAssistant, partID: string) {
const tool = message.content.find((part) => part.type === "tool" && part.id === partID)
if (tool) return tool
const match = /^(text|reasoning):(\d+)$/.exec(partID)
if (!match) return
const ordinal = Number(match[2])
return message.content.filter((part) => part.type === match[1])[ordinal]
}
export function isTerminalFinish(finish: string | undefined) {
return !!finish && !["tool-calls", "unknown"].includes(finish)
}
function append(rows: SessionRow[], ref: PartRef, part: AppendPart, index = rows.length) {
const previous = rows[index - 1]
const kind = groupKind(part)
if (kind && previous?.type === "group" && previous.kind === kind) {
rows[index - 1] = { ...previous, refs: [...previous.refs, ref] }
return
}
completePrevious(rows, index)
rows.splice(index, 0, 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) {
const previous = rows[index - 1]
if (previous?.type === "group") rows[index - 1] = { ...previous, completed: true }
}
function partition(row: SessionRow, pending: ReadonlySet<string>): SessionRow {
if (row.type !== "group" || row.kind !== "exploration") return row
const changed = row.refs.some((ref) => pending.has(ref.partID)) || row.pending.some((ref) => !pending.has(ref.partID))
if (!changed) return row
const refs = [...row.refs, ...row.pending]
return {
...row,
refs: refs.filter((ref) => !pending.has(ref.partID)),
pending: refs.filter((ref) => pending.has(ref.partID)),
}
}
function exploration(name: string) {
return ["read", "glob", "grep"].includes(name.toLowerCase())
}
function messageRow(messageID: string): SessionRow {
return { id: messageRowID(messageID), type: "message", messageID }
}
function messageRowID(messageID: string) {
return `m${segment(messageID)}`
}
export 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}`
}

View file

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

View file

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