mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 12:10:45 +03:00
refactor(tui): project production subgroups through tree engine (#48399)
This commit is contained in:
committed by
GitHub
Unverified
parent
70afbac80c
commit
4d12e01824
@@ -0,0 +1,141 @@
|
||||
import type { SessionMessageAssistant } from "@opencode/client"
|
||||
import { groupEntries, mergeGroups, splitGroups, type GroupNode } from "./tree"
|
||||
|
||||
export type PartRef = {
|
||||
messageID: string
|
||||
partID: string
|
||||
}
|
||||
|
||||
export type CacheUsage = {
|
||||
read: number
|
||||
model: SessionMessageAssistant["model"]
|
||||
}
|
||||
|
||||
export type SessionEntry =
|
||||
| { type: "message"; messageID: string }
|
||||
| { type: "compaction-queued"; inboxID: string }
|
||||
| { type: "part"; ref: PartRef }
|
||||
| { type: "assistant-footer"; messageID: string }
|
||||
| { type: "turn-usage"; messageIDs: string[]; previousCache?: CacheUsage }
|
||||
|
||||
type GroupKind = "reasoning" | "exploration"
|
||||
type SessionGroup = {
|
||||
type: "group"
|
||||
children: readonly GroupNode<SessionEntry, GroupKind>[]
|
||||
size: number
|
||||
completed: boolean
|
||||
} & ({ kind: "reasoning" } | { kind: "exploration"; pending: PartRef[] })
|
||||
|
||||
export type SessionRow = SessionEntry | SessionGroup
|
||||
|
||||
export type AppendPart =
|
||||
| { type: "text" }
|
||||
| { type: "reasoning"; time?: { completed?: number } }
|
||||
| { type: "tool"; name: string }
|
||||
|
||||
export type ProjectionEntry = {
|
||||
entry: SessionEntry
|
||||
part?: AppendPart
|
||||
closesPrevious?: boolean
|
||||
}
|
||||
|
||||
/** Hydrate a fresh history batch in one pass rather than merging one leaf at a time. */
|
||||
export function projectEntries(entries: ProjectionEntry[]): SessionRow[] {
|
||||
const nodes = groupEntries(entries, (item) => (item.part ? partPath(item.part) : []))
|
||||
return nodes.map((node, index) => {
|
||||
if (node.type === "entry") return node.entry.entry
|
||||
const next = nodes[index + 1]
|
||||
const completed =
|
||||
(next !== undefined && (next.type === "group" || next.entry.closesPrevious !== false)) ||
|
||||
(node.kind === "reasoning" &&
|
||||
node.children.every(
|
||||
(child) =>
|
||||
child.type === "entry" &&
|
||||
child.entry.part?.type === "reasoning" &&
|
||||
child.entry.part.time?.completed !== undefined,
|
||||
))
|
||||
const group = { ...node, children: node.children.map(unwrap), completed }
|
||||
return node.kind === "reasoning" ? { ...group, kind: "reasoning" } : { ...group, kind: "exploration", pending: [] }
|
||||
})
|
||||
}
|
||||
|
||||
function unwrap(node: GroupNode<ProjectionEntry, GroupKind>): GroupNode<SessionEntry, GroupKind> {
|
||||
if (node.type === "entry") return { ...node, entry: node.entry.entry }
|
||||
return { ...node, children: node.children.map(unwrap) }
|
||||
}
|
||||
|
||||
function partPath(part: AppendPart): readonly GroupKind[] {
|
||||
if (part.type === "reasoning") return ["reasoning"]
|
||||
if (part.type === "tool" && ["read", "glob", "grep"].includes(part.name.toLowerCase())) return ["exploration"]
|
||||
return []
|
||||
}
|
||||
|
||||
/** Production rules only: keep lifecycle/status decisions outside the tree engine. */
|
||||
export function append(rows: SessionRow[], ref: PartRef, part: AppendPart, index = rows.length) {
|
||||
const [node] = groupEntries<SessionEntry, GroupKind>([{ type: "part", ref }], () => partPath(part))
|
||||
if (node.type === "entry") {
|
||||
completePrevious(rows, index)
|
||||
rows.splice(index, 0, node.entry)
|
||||
return
|
||||
}
|
||||
const previous = rows[index - 1]
|
||||
if (previous?.type === "group" && previous.kind === node.kind) {
|
||||
// Permission-blocked tools remain at the end, just as the former refs/pending
|
||||
// partition did. Inserting a new ref must precede those blocked tools.
|
||||
const pending = previous.kind === "exploration" ? previous.pending.length : 0
|
||||
const [left, right] = splitGroups([previous], previous.size - pending)
|
||||
const [merged] = mergeGroups(mergeGroups(left, [node]), right)
|
||||
if (merged.type !== "group") throw new Error("Expected merged session group")
|
||||
previous.children = merged.children
|
||||
previous.size = merged.size
|
||||
if (part.type === "reasoning") previous.completed &&= part.time?.completed !== undefined
|
||||
return
|
||||
}
|
||||
completePrevious(rows, index)
|
||||
rows.splice(
|
||||
index,
|
||||
0,
|
||||
node.kind === "reasoning"
|
||||
? { ...node, kind: "reasoning", completed: part.type === "reasoning" && part.time?.completed !== undefined }
|
||||
: { ...node, kind: "exploration", pending: [], completed: false },
|
||||
)
|
||||
}
|
||||
|
||||
export function completePrevious(rows: SessionRow[], index = rows.length) {
|
||||
const previous = rows[index - 1]
|
||||
if (previous?.type === "group") previous.completed = true
|
||||
}
|
||||
|
||||
/** Part references for an existing production subgroup, not a flat timeline. */
|
||||
export function groupRefs(row: SessionGroup, includePending = false): PartRef[] {
|
||||
const pending = !includePending && row.kind === "exploration" ? row.pending : []
|
||||
const visit = (nodes: readonly GroupNode<SessionEntry, GroupKind>[]): PartRef[] =>
|
||||
nodes.flatMap((node) => {
|
||||
if (node.type === "group") return visit(node.children)
|
||||
if (node.entry.type !== "part") return []
|
||||
const ref = node.entry.ref
|
||||
if (pending.some((item) => item.messageID === ref.messageID && item.partID === ref.partID)) return []
|
||||
return [ref]
|
||||
})
|
||||
return visit(row.children)
|
||||
}
|
||||
|
||||
export function partitionPending(rows: SessionRow[], pending: Set<string>) {
|
||||
rows.forEach((row) => {
|
||||
if (row.type !== "group" || row.kind !== "exploration") return
|
||||
// The production exploration rule creates direct part children. Preserve the
|
||||
// existing stable partition order when permissions are admitted or dismissed.
|
||||
const blocked = (node: GroupNode<SessionEntry, GroupKind>) =>
|
||||
node.type === "entry" && node.entry.type === "part" && pending.has(node.entry.ref.partID)
|
||||
row.children = [...row.children.filter((node) => !blocked(node)), ...row.children.filter(blocked)]
|
||||
row.pending = groupRefs(row, true).filter((ref) => pending.has(ref.partID))
|
||||
})
|
||||
}
|
||||
|
||||
export 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
|
||||
return groupRefs(row, true).some((item) => item.messageID === ref.messageID && item.partID === ref.partID)
|
||||
})
|
||||
}
|
||||
@@ -110,6 +110,7 @@ import { isRecord } from "../../util/record"
|
||||
import { createHistoryPrepend } from "./history"
|
||||
import { context, use, type PendingAction } from "./render-context"
|
||||
import { INLINE_TOOL_ICON_WIDTH, InlineToolRow, ReasoningPart, reasoningContent, TextPart } from "./message-parts"
|
||||
import { groupRefs } from "./grouping/session"
|
||||
export { InlineToolRow } from "./message-parts"
|
||||
|
||||
addDefaultParsers(parsers.parsers)
|
||||
@@ -1442,12 +1443,14 @@ function SessionRowView(props: SessionRowViewProps) {
|
||||
{(row) => <SessionPartView partRef={row().ref} message={props.message} />}
|
||||
</Match>
|
||||
<Match when={props.row.type === "group" && props.row.kind === "reasoning" ? props.row : undefined}>
|
||||
{(row) => <SessionReasoningGroupView refs={row().refs} completed={row().completed} message={props.message} />}
|
||||
{(row) => (
|
||||
<SessionReasoningGroupView refs={groupRefs(row())} completed={row().completed} message={props.message} />
|
||||
)}
|
||||
</Match>
|
||||
<Match when={props.row.type === "group" && props.row.kind === "exploration" ? props.row : undefined}>
|
||||
{(row) => (
|
||||
<SessionGroupView
|
||||
refs={row().refs}
|
||||
refs={groupRefs(row())}
|
||||
pending={row().pending}
|
||||
completed={row().completed}
|
||||
message={props.message}
|
||||
|
||||
@@ -4,36 +4,20 @@ import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { useConfig } from "../../config"
|
||||
import { useData } from "../../context/data"
|
||||
import { useClient } from "../../context/client"
|
||||
|
||||
export type PartRef = {
|
||||
messageID: string
|
||||
partID: string
|
||||
}
|
||||
|
||||
export type CacheUsage = {
|
||||
read: number
|
||||
model: SessionMessageAssistant["model"]
|
||||
}
|
||||
|
||||
export type SessionRow =
|
||||
| { type: "message"; messageID: string }
|
||||
| { type: "compaction-queued"; inboxID: string }
|
||||
| { type: "part"; ref: PartRef }
|
||||
| {
|
||||
type: "group"
|
||||
kind: "reasoning"
|
||||
refs: PartRef[]
|
||||
completed: boolean
|
||||
}
|
||||
| {
|
||||
type: "group"
|
||||
kind: "exploration"
|
||||
refs: PartRef[]
|
||||
pending: PartRef[]
|
||||
completed: boolean
|
||||
}
|
||||
| { type: "assistant-footer"; messageID: string }
|
||||
| { type: "turn-usage"; messageIDs: string[]; previousCache?: CacheUsage }
|
||||
import {
|
||||
append,
|
||||
completePrevious,
|
||||
groupRefs,
|
||||
hasPart,
|
||||
partitionPending,
|
||||
projectEntries,
|
||||
type AppendPart,
|
||||
type CacheUsage,
|
||||
type PartRef,
|
||||
type ProjectionEntry,
|
||||
type SessionRow,
|
||||
} from "./grouping/session"
|
||||
export type { CacheUsage, PartRef, SessionRow } from "./grouping/session"
|
||||
|
||||
export function createSessionRows(sessionID: Accessor<string>, onSynced?: (sessionID: string) => void) {
|
||||
const data = useData()
|
||||
@@ -180,7 +164,7 @@ export function createSessionRows(sessionID: Accessor<string>, onSynced?: (sessi
|
||||
(row) =>
|
||||
row.type === "group" &&
|
||||
row.kind === "reasoning" &&
|
||||
row.refs.some((item) => item.messageID === ref.messageID && item.partID === ref.partID),
|
||||
groupRefs(row).some((item) => item.messageID === ref.messageID && item.partID === ref.partID),
|
||||
)
|
||||
if (row?.type === "group" && row.kind === "reasoning") row.completed = true
|
||||
}),
|
||||
@@ -299,16 +283,15 @@ export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new S
|
||||
const usage = turnTokens
|
||||
? { steps: [] as SessionMessageAssistant[], previousTurnCache: undefined as CacheUsage | undefined }
|
||||
: undefined
|
||||
return [
|
||||
const entries = [
|
||||
...messages.filter((message) => !pending.has(message.id)),
|
||||
...pendingCompactions,
|
||||
...messages.filter(isInput),
|
||||
].reduce<SessionRow[]>((rows, message) => {
|
||||
].reduce<ProjectionEntry[]>((rows, message) => {
|
||||
if (message.type !== "assistant") {
|
||||
if (message.type === "synthetic" && !message.description?.trim()) return rows
|
||||
if (message.type === "compaction" && message.status === "completed" && usage) usage.previousTurnCache = undefined
|
||||
if (!pending.has(message.id)) completePrevious(rows)
|
||||
rows.push({ type: "message", messageID: message.id })
|
||||
rows.push({ entry: { type: "message", messageID: message.id }, closesPrevious: !pending.has(message.id) })
|
||||
return rows
|
||||
}
|
||||
usage?.steps.push(message)
|
||||
@@ -316,21 +299,22 @@ export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new S
|
||||
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)
|
||||
rows.push({ entry: { type: "part", ref: { messageID: message.id, partID } }, part })
|
||||
})
|
||||
const terminal = (message.finish && !["tool-calls", "unknown"].includes(message.finish)) || message.error
|
||||
if (terminal || message.retry) {
|
||||
completePrevious(rows)
|
||||
rows.push({ type: "assistant-footer", messageID: message.id })
|
||||
rows.push({ entry: { type: "assistant-footer", messageID: message.id } })
|
||||
}
|
||||
if (terminal && usage) {
|
||||
const stepsWithUsage = usage.steps.filter(hasTokenUsage)
|
||||
const last = stepsWithUsage.at(-1)
|
||||
if (last) {
|
||||
rows.push({
|
||||
type: "turn-usage",
|
||||
messageIDs: stepsWithUsage.map((step) => step.id),
|
||||
...(usage.previousTurnCache === undefined ? {} : { previousCache: usage.previousTurnCache }),
|
||||
entry: {
|
||||
type: "turn-usage",
|
||||
messageIDs: stepsWithUsage.map((step) => step.id),
|
||||
...(usage.previousTurnCache === undefined ? {} : { previousCache: usage.previousTurnCache }),
|
||||
},
|
||||
})
|
||||
usage.previousTurnCache = { read: last.tokens.cache.read, model: last.model }
|
||||
}
|
||||
@@ -338,6 +322,7 @@ export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new S
|
||||
}
|
||||
return rows
|
||||
}, [])
|
||||
return projectEntries(entries)
|
||||
}
|
||||
|
||||
export function cacheReuseDrop(previous: CacheUsage | undefined, current: CacheUsage) {
|
||||
@@ -427,7 +412,7 @@ function rowBoundaryMessageID(row: SessionRow, messages: Map<string, SessionMess
|
||||
row.type === "part"
|
||||
? row.ref.messageID
|
||||
: row.type === "group"
|
||||
? row.refs[0]?.messageID
|
||||
? groupRefs(row)[0]?.messageID
|
||||
: row.type === "assistant-footer"
|
||||
? row.messageID
|
||||
: row.type === "turn-usage"
|
||||
@@ -446,66 +431,3 @@ export function resolvePart(message: SessionMessageAssistant, partID: string) {
|
||||
const ordinal = Number(match[2])
|
||||
return message.content.filter((part) => part.type === match[1])[ordinal]
|
||||
}
|
||||
|
||||
type AppendPart =
|
||||
| { type: "text" }
|
||||
| { type: "reasoning"; time?: { completed?: number } }
|
||||
| { 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)
|
||||
previous.completed &&= part.time?.completed !== undefined
|
||||
return
|
||||
}
|
||||
completePrevious(rows, index)
|
||||
rows.splice(index, 0, {
|
||||
type: "group",
|
||||
kind: "reasoning",
|
||||
refs: [ref],
|
||||
completed: part.time?.completed !== undefined,
|
||||
})
|
||||
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 })
|
||||
return
|
||||
}
|
||||
completePrevious(rows, index)
|
||||
rows.splice(index, 0, { type: "part", ref })
|
||||
}
|
||||
|
||||
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 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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { append, groupRefs, partitionPending, type SessionRow } from "../../../src/routes/session/grouping/session"
|
||||
|
||||
test("a pending tool does not hide a later tool reusing its call ID in another message", () => {
|
||||
const rows: SessionRow[] = []
|
||||
const blocked = { messageID: "assistant-a", partID: "call-reused" }
|
||||
const later = { messageID: "assistant-b", partID: "call-reused" }
|
||||
append(rows, blocked, { type: "tool", name: "read" })
|
||||
partitionPending(rows, new Set([blocked.partID]))
|
||||
append(rows, later, { type: "tool", name: "read" })
|
||||
|
||||
const group = rows[0]
|
||||
if (group.type !== "group" || group.kind !== "exploration") throw new Error("Expected exploration group")
|
||||
expect(groupRefs(group)).toEqual([later])
|
||||
expect(group.pending).toEqual([blocked])
|
||||
expect(groupRefs(group, true)).toEqual([later, blocked])
|
||||
|
||||
partitionPending(rows, new Set())
|
||||
expect(groupRefs(group)).toEqual([later, blocked])
|
||||
expect(group.pending).toEqual([])
|
||||
})
|
||||
@@ -280,10 +280,11 @@ test("groups exploration parts across assistant messages until a delimiter", ()
|
||||
kind: "exploration",
|
||||
pending: [],
|
||||
completed: true,
|
||||
refs: [
|
||||
{ messageID: "assistant-1", partID: "read-1" },
|
||||
{ messageID: "assistant-1", partID: "glob-1" },
|
||||
{ messageID: "assistant-2", partID: "grep-1" },
|
||||
size: 3,
|
||||
children: [
|
||||
partChild("assistant-1", "read-1"),
|
||||
partChild("assistant-1", "glob-1"),
|
||||
partChild("assistant-2", "grep-1"),
|
||||
],
|
||||
},
|
||||
{ type: "part", ref: { messageID: "assistant-2", partID: "text:0" } },
|
||||
@@ -305,7 +306,8 @@ test("keeps non-exploration tools as individual part rows", () => {
|
||||
kind: "exploration",
|
||||
pending: [],
|
||||
completed: true,
|
||||
refs: [{ messageID: "assistant-1", partID: "read-1" }],
|
||||
size: 1,
|
||||
children: [partChild("assistant-1", "read-1")],
|
||||
},
|
||||
{ type: "part", ref: { messageID: "assistant-1", partID: "reasoning:0" } },
|
||||
{
|
||||
@@ -313,7 +315,8 @@ test("keeps non-exploration tools as individual part rows", () => {
|
||||
kind: "exploration",
|
||||
pending: [],
|
||||
completed: false,
|
||||
refs: [{ messageID: "assistant-1", partID: "grep-1" }],
|
||||
size: 1,
|
||||
children: [partChild("assistant-1", "grep-1")],
|
||||
},
|
||||
])
|
||||
})
|
||||
@@ -334,14 +337,16 @@ test("assigns stable kind ordinals within an assistant message", () => {
|
||||
type: "group",
|
||||
kind: "reasoning",
|
||||
completed: true,
|
||||
refs: [{ messageID: "assistant-1", partID: "reasoning:0" }],
|
||||
size: 1,
|
||||
children: [partChild("assistant-1", "reasoning:0")],
|
||||
},
|
||||
{ type: "part", ref: { messageID: "assistant-1", partID: "text:1" } },
|
||||
{
|
||||
type: "group",
|
||||
kind: "reasoning",
|
||||
completed: false,
|
||||
refs: [{ messageID: "assistant-1", partID: "reasoning:1" }],
|
||||
size: 1,
|
||||
children: [partChild("assistant-1", "reasoning:1")],
|
||||
},
|
||||
])
|
||||
})
|
||||
@@ -361,17 +366,16 @@ test("groups adjacent reasoning parts until a visible boundary", () => {
|
||||
type: "group",
|
||||
kind: "reasoning",
|
||||
completed: true,
|
||||
refs: [
|
||||
{ messageID: "assistant-1", partID: "reasoning:0" },
|
||||
{ messageID: "assistant-1", partID: "reasoning:1" },
|
||||
],
|
||||
size: 2,
|
||||
children: [partChild("assistant-1", "reasoning:0"), partChild("assistant-1", "reasoning:1")],
|
||||
},
|
||||
{ type: "part", ref: { messageID: "assistant-1", partID: "text:0" } },
|
||||
{
|
||||
type: "group",
|
||||
kind: "reasoning",
|
||||
completed: false,
|
||||
refs: [{ messageID: "assistant-1", partID: "reasoning:2" }],
|
||||
size: 1,
|
||||
children: [partChild("assistant-1", "reasoning:2")],
|
||||
},
|
||||
])
|
||||
})
|
||||
@@ -393,17 +397,16 @@ test("groups across empty assistant reasoning parts", () => {
|
||||
type: "group",
|
||||
kind: "reasoning",
|
||||
completed: true,
|
||||
refs: [{ messageID: "assistant-1", partID: "reasoning:0" }],
|
||||
size: 1,
|
||||
children: [partChild("assistant-1", "reasoning:0")],
|
||||
},
|
||||
{
|
||||
type: "group",
|
||||
kind: "exploration",
|
||||
pending: [],
|
||||
completed: false,
|
||||
refs: [
|
||||
{ messageID: "assistant-1", partID: "read-1" },
|
||||
{ messageID: "assistant-2", partID: "grep-1" },
|
||||
],
|
||||
size: 2,
|
||||
children: [partChild("assistant-1", "read-1"), partChild("assistant-2", "grep-1")],
|
||||
},
|
||||
])
|
||||
})
|
||||
@@ -425,7 +428,8 @@ test("completes exploration groups when another row follows", () => {
|
||||
kind: "exploration",
|
||||
pending: [],
|
||||
completed: true,
|
||||
refs: [{ messageID: "assistant-1", partID: "read-1" }],
|
||||
size: 1,
|
||||
children: [partChild("assistant-1", "read-1")],
|
||||
},
|
||||
{ type: "message", messageID: "user-1" },
|
||||
{
|
||||
@@ -433,7 +437,8 @@ test("completes exploration groups when another row follows", () => {
|
||||
kind: "exploration",
|
||||
pending: [],
|
||||
completed: true,
|
||||
refs: [{ messageID: "assistant-2", partID: "grep-1" }],
|
||||
size: 1,
|
||||
children: [partChild("assistant-2", "grep-1")],
|
||||
},
|
||||
{ type: "assistant-footer", messageID: "assistant-2" },
|
||||
])
|
||||
@@ -468,10 +473,8 @@ test("hides synthetic messages without descriptions", () => {
|
||||
kind: "exploration",
|
||||
pending: [],
|
||||
completed: false,
|
||||
refs: [
|
||||
{ messageID: "assistant-1", partID: "read-1" },
|
||||
{ messageID: "assistant-2", partID: "grep-1" },
|
||||
],
|
||||
size: 2,
|
||||
children: [partChild("assistant-1", "read-1"), partChild("assistant-2", "grep-1")],
|
||||
},
|
||||
])
|
||||
expect(reduceSessionRows(messages, new Set(["synthetic-1"]))).toEqual(rows)
|
||||
@@ -496,7 +499,8 @@ test("renders synthetic messages with descriptions", () => {
|
||||
kind: "exploration",
|
||||
pending: [],
|
||||
completed: true,
|
||||
refs: [{ messageID: "assistant-1", partID: "read-1" }],
|
||||
size: 1,
|
||||
children: [partChild("assistant-1", "read-1")],
|
||||
},
|
||||
{ type: "message", messageID: "synthetic-1" },
|
||||
{
|
||||
@@ -504,11 +508,16 @@ test("renders synthetic messages with descriptions", () => {
|
||||
kind: "exploration",
|
||||
pending: [],
|
||||
completed: false,
|
||||
refs: [{ messageID: "assistant-2", partID: "grep-1" }],
|
||||
size: 1,
|
||||
children: [partChild("assistant-2", "grep-1")],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
function partChild(messageID: string, partID: string) {
|
||||
return { type: "entry" as const, entry: { type: "part" as const, ref: { messageID, partID } }, size: 1 as const }
|
||||
}
|
||||
|
||||
test("renders a footer for a pre-output retry assistant after replay", () => {
|
||||
const message = assistant("assistant-retry", [])
|
||||
message.retry = {
|
||||
|
||||
Reference in New Issue
Block a user