Skip to content
Draft
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
4 changes: 4 additions & 0 deletions packages/thread-view/src/apply-turn-message-detail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ function applyTurnMessageDetail(
const includeMessages =
turn.status === "pending" ||
turnMessageDetail === "full" ||
(turn.externalUserBoundarySeqs?.length ?? 0) > 0 ||
shouldIncludeSummaryTurnMessages(messages, terminalMessage);

const detailedTurn: EventProjectionTurn = {
Expand All @@ -120,6 +121,9 @@ function applyTurnMessageDetail(
completedAt: turn.completedAt,
status: turn.status,
summaryCount,
...(turn.externalUserBoundarySeqs
? { externalUserBoundarySeqs: turn.externalUserBoundarySeqs }
: {}),
};
if (terminalMessage) {
detailedTurn.terminalMessage = terminalMessage;
Expand Down
15 changes: 14 additions & 1 deletion packages/thread-view/src/build-thread-timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,19 @@ function hasTurnSummaryRows(rows: TimelineRow[]): boolean {
return rows.some((row) => row.kind === "turn");
}

function compareTimelineRowsBySource(
left: TimelineRow,
right: TimelineRow,
): number {
if (left.sourceSeqStart !== right.sourceSeqStart) {
return left.sourceSeqStart - right.sourceSeqStart;
}
if (left.sourceSeqEnd !== right.sourceSeqEnd) {
return left.sourceSeqEnd - right.sourceSeqEnd;
}
return 0;
}

function buildTimelineRows(
projection: EventProjection,
options: BuildTimelineRowsOptions,
Expand Down Expand Up @@ -1092,7 +1105,7 @@ function buildTimelineRows(
}
}

return rows;
return rows.sort(compareTimelineRowsBySource);
}

export function buildThreadTimelineFromEvents(
Expand Down
19 changes: 18 additions & 1 deletion packages/thread-view/src/completed-turn-grouping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,11 @@ function groupCompletedTurnSummaryMessages(
turn: EventProjectionTurn,
summaryMessages: EventProjectionMessage[],
): CompletedTurnSummaryItem[] {
if (!summaryMessages.some(isTimelineUngroupableMessage)) {
const externalBoundarySeqs = turn.externalUserBoundarySeqs ?? [];
if (
externalBoundarySeqs.length === 0 &&
!summaryMessages.some(isTimelineUngroupableMessage)
) {
return [
{
kind: "summary",
Expand All @@ -139,6 +143,7 @@ function groupCompletedTurnSummaryMessages(
const items: CompletedTurnSummaryItem[] = [];
let groupedMessages: EventProjectionMessage[] = [];
let segmentIndex = 0;
let externalBoundaryIndex = 0;

function flushGroupedMessages(): void {
if (groupedMessages.length === 0) {
Expand All @@ -159,7 +164,19 @@ function groupCompletedTurnSummaryMessages(
groupedMessages = [];
}

function flushExternalBoundariesBefore(message: EventProjectionMessage): void {
while (
externalBoundaryIndex < externalBoundarySeqs.length &&
(externalBoundarySeqs[externalBoundaryIndex] ?? 0) <
message.sourceSeqStart
) {
flushGroupedMessages();
externalBoundaryIndex += 1;
}
}

for (const message of summaryMessages) {
flushExternalBoundariesBefore(message);
if (isTimelineUngroupableMessage(message)) {
flushGroupedMessages();
items.push({
Expand Down
1 change: 1 addition & 0 deletions packages/thread-view/src/event-projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface EventProjectionTurn {
completedAt: number | null;
status: EventProjectionTurnStatus;
summaryCount: number;
externalUserBoundarySeqs?: number[];
terminalMessage?: EventProjectionMessage;
messages?: EventProjectionMessage[];
}
Expand Down
38 changes: 38 additions & 0 deletions packages/thread-view/src/group-event-projection-turns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,42 @@ function addProjectionTurnMessage(
});
}

function isExternalUserBoundaryForTurn(
turnId: string,
turn: EventProjectionTurn,
message: EventProjectionMessage,
): boolean {
if (message.kind !== "user" || message.initiator !== "user") {
return false;
}
if (
message.sourceSeqStart <= turn.sourceSeqStart ||
message.sourceSeqStart >= turn.sourceSeqEnd
) {
return false;
}
return message.scope.kind !== "turn" || message.scope.turnId !== turnId;
}

function applyExternalUserBoundaries(
turnsById: Map<string, ProjectionTurnDraft>,
messages: EventProjectionMessage[],
): void {
for (const [turnId, draft] of turnsById) {
const boundarySeqs = new Set<number>();
for (const message of messages) {
if (isExternalUserBoundaryForTurn(turnId, draft.turn, message)) {
boundarySeqs.add(message.sourceSeqStart);
}
}
if (boundarySeqs.size > 0) {
draft.turn.externalUserBoundarySeqs = [...boundarySeqs].sort(
(left, right) => left - right,
);
}
}
}

function createEventProjectionEntry(
draft: ProjectionEntryDraft,
turnsById: Map<string, ProjectionTurnDraft>,
Expand Down Expand Up @@ -290,6 +326,8 @@ export function groupEventProjectionTurns(
addProjectionTurnMessage(turnDraft, message);
}

applyExternalUserBoundaries(turnsById, args.messages);

const orderedEntryDrafts = [...entryDrafts].sort((left, right) => {
if (left.sourceSeqStart !== right.sourceSeqStart) {
return left.sourceSeqStart - right.sourceSeqStart;
Expand Down
66 changes: 66 additions & 0 deletions packages/thread-view/test/completed-turn-summary-rendering.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,72 @@ describe("completed turn summary rendering", () => {
).toEqual([["work:command"], ["work:command"]]);
});

it("splits background parent turn summaries at later user turn boundaries", () => {
const event = createTimelineEventFactory({ threadId: "thread-1" });
const reviewRequest = event.clientTurnRequested({
target: { kind: "new-turn" },
text: "$ottonomous:review",
});
const events: TimelineFixtureEvent[] = [
reviewRequest,
event.turnStarted({ turnId: "parent-review" }),
event.inputAccepted({
clientRequestId: reviewRequest.data.requestId,
turnId: "parent-review",
}),
event.commandCompleted({
command: "pnpm exec turbo run test --filter=@bb/thread-view",
itemId: "parent-before-user",
turnId: "parent-review",
}),
event.turnStarted({ turnId: "background-worker" }),
event.commandCompleted({
command: "rg timeline packages/thread-view",
itemId: "worker-command",
turnId: "background-worker",
}),
event.assistantCompleted({
itemId: "worker-terminal",
text: "Worker finished.",
turnId: "background-worker",
}),
event.turnCompleted({ turnId: "background-worker" }),
];
const followUpRequest = event.clientTurnRequested({
target: { kind: "new-turn" },
text: "one more change",
});
events.push(
followUpRequest,
event.commandCompleted({
command: "rg links apps packages",
itemId: "parent-after-user",
turnId: "parent-review",
}),
event.assistantCompleted({
itemId: "parent-terminal",
text: "I will handle the follow-up.",
turnId: "parent-review",
}),
event.turnCompleted({ turnId: "parent-review" }),
);

const timeline = renderCompletedTimeline({ events });

expect(rowSignatures(timeline.rows)).toEqual([
"conversation:user",
"turn:4-4",
"turn:5-8",
"conversation:assistant",
"conversation:user",
"turn:10-10",
"conversation:assistant",
]);
expect(
turnRows(timeline.rows).map((row) => rowSignatures(row.children ?? [])),
).toEqual([["work:command"], ["work:command"], ["work:command"]]);
});

it("does not split completed turn summaries around accepted assistant steers", () => {
const event = createTimelineEventFactory({ threadId: "thread-1" });
const events: TimelineFixtureEvent[] = [
Expand Down
Loading