Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/tui/src/routes/session/grouping/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,18 @@ export function hasPart(rows: SessionRow[], ref: PartRef) {
return groupRefs(row, true).some((item) => item.messageID === ref.messageID && item.partID === ref.partID)
})
}

export function partKey(ref: PartRef) {
return `${ref.messageID} ${ref.partID}`
}

/** Index of every part in the rows, mirroring hasPart membership. */
export function collectPartKeys(rows: SessionRow[]) {
return new Set(
rows.flatMap((row) => {
if (row.type === "part") return [partKey(row.ref)]
if (row.type !== "group") return []
return groupRefs(row, true).map(partKey)
}),
)
}
39 changes: 30 additions & 9 deletions packages/tui/src/routes/session/rows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import { useData } from "../../context/data"
import { useClient } from "../../context/client"
import {
append,
collectPartKeys,
completePrevious,
groupRefs,
hasPart,
partKey,
partitionPending,
projectEntries,
type AppendPart,
Expand Down Expand Up @@ -61,6 +63,18 @@ export function createSessionRows(sessionID: Accessor<string>, onSynced?: (sessi
)
}

// Index of every part in the rows. Rebuilt from the plain reduce() output on
// each reconcile and extended on appendPart, so the streaming fast path below
// can skip no-op store updates with an O(1) lookup. The other mutation sites
// only add message/footer rows or reorder group children, never part membership.
let parts = new Set<string>()

const syncRows = () => {
const next = reduce()
parts = collectPartKeys(next)
setRows(reconcile(next))
}

createEffect(() => {
const pending = pendingPermissions()
setRows(
Expand All @@ -73,12 +87,12 @@ export function createSessionRows(sessionID: Accessor<string>, onSynced?: (sessi
createEffect(
on([sessionID, () => client.connection.status()], ([id, status]) => {
if (status !== "connected") return
setRows(reconcile(reduce()))
syncRows()
void data.session.pending.sync(id).catch(() => undefined)
void data.session.message.sync(id).then(
() => {
if (sessionID() !== id) return
setRows(reconcile(reduce()))
syncRows()
onSynced?.(id)
},
() => undefined,
Expand All @@ -92,7 +106,7 @@ export function createSessionRows(sessionID: Accessor<string>, onSynced?: (sessi
on(
revertBoundary,
() => {
setRows(reconcile(reduce()))
syncRows()
},
{ defer: true },
),
Expand All @@ -106,7 +120,7 @@ export function createSessionRows(sessionID: Accessor<string>, onSynced?: (sessi
if (item.type === "user" && item.delivery === "queue") return [`${item.id}:queue`]
return []
}),
() => setRows(reconcile(reduce())),
() => syncRows(),
{ defer: true },
),
)
Expand All @@ -132,12 +146,12 @@ export function createSessionRows(sessionID: Accessor<string>, onSynced?: (sessi
]
: [],
),
() => setRows(reconcile(reduce())),
() => syncRows(),
{ defer: true },
),
)

createEffect(on(turnTokens, () => setRows(reconcile(reduce())), { defer: true }))
createEffect(on(turnTokens, () => syncRows(), { defer: true }))

const appendMessage = (messageID: string) =>
setRows(
Expand All @@ -152,7 +166,12 @@ export function createSessionRows(sessionID: Accessor<string>, onSynced?: (sessi
}),
)

const appendPart = (ref: PartRef, part: AppendPart) =>
const appendPart = (ref: PartRef, part: AppendPart) => {
const key = partKey(ref)
// Streaming deltas re-assert an existing part. Skip the store update without
// touching it: even a no-op root-array setRows traverses the whole tree.
// Reasoning completion still flows through to flip the group flag below.
if ((part.type !== "reasoning" || part.time?.completed === undefined) && parts.has(key)) return
setRows(
produce((draft) => {
if (!hasPart(draft, ref)) {
Expand All @@ -169,6 +188,8 @@ export function createSessionRows(sessionID: Accessor<string>, onSynced?: (sessi
if (row?.type === "group" && row.kind === "reasoning") row.completed = true
}),
)
parts.add(key)
}

const appendFooter = (messageID: string) =>
setRows(
Expand Down Expand Up @@ -263,12 +284,12 @@ export function createSessionRows(sessionID: Accessor<string>, onSynced?: (sessi
data.on("session.step.ended", (event) => {
if (event.data.sessionID !== sessionID() || ["tool-calls", "unknown"].includes(event.data.finish)) return
appendFooter(event.data.assistantMessageID)
if (turnTokens()) setRows(reconcile(reduce()))
if (turnTokens()) syncRows()
}),
data.on("session.step.failed", (event) => {
if (event.data.sessionID !== sessionID()) return
appendFooter(event.data.assistantMessageID)
if (turnTokens()) setRows(reconcile(reduce()))
if (turnTokens()) syncRows()
}),
]
onCleanup(() => subscriptions.forEach((unsubscribe) => unsubscribe()))
Expand Down
Loading