{entry.value.content}
))}
-
+
{turn.questions.map((question) => (
@@ -464,6 +466,10 @@ export function ConversationView({
)}
))}
+
))}
{assistant?.value ? (
diff --git a/web/packages/superagent-ui/src/conversationPresentation.test.ts b/web/packages/superagent-ui/src/conversationPresentation.test.ts
index 6b48ebc..48f9782 100644
--- a/web/packages/superagent-ui/src/conversationPresentation.test.ts
+++ b/web/packages/superagent-ui/src/conversationPresentation.test.ts
@@ -254,6 +254,75 @@ describe("conversation presentation", () => {
expect(required(view.turns[1]).activities).toContain(activity);
});
+ it("keeps live unanchored activity in the current user turn", () => {
+ const messages = [
+ message(1, "user", "first"),
+ message(2, "assistant", "first answer"),
+ message(3, "user", "continue"),
+ ];
+ const toolActivity: TimelineActivityEntry = {
+ resumeToken: "live-tool",
+ source: "tool-source",
+ createdAt: iso(4_000),
+ value: {
+ kind: "model_tool_call",
+ message: "Calling read_file.",
+ callId: "not-durable-yet",
+ toolName: "read_file",
+ messageSequence: 5,
+ },
+ };
+ const modelActivity: TimelineActivityEntry = {
+ resumeToken: "live-model",
+ source: "model-source",
+ createdAt: iso(5_000),
+ value: {
+ kind: "model_started",
+ message: "Calling model.",
+ messageSequence: 6,
+ },
+ };
+
+ const view = buildConversationPresentation({
+ messages,
+ activities: [toolActivity, modelActivity],
+ isModelRunning: true,
+ isExecutionLive: true,
+ });
+
+ expect(view.earlierActivity).toHaveLength(0);
+ expect(required(view.turns[1]).activities).toContain(toolActivity);
+ expect(required(view.turns[1]).liveActivities).toEqual([toolActivity]);
+ expect(required(view.turns[1]).models).toHaveLength(1);
+ });
+
+ it("leaves unanchored terminal activity in historical activity", () => {
+ const messages = [
+ message(1, "user", "first"),
+ message(2, "assistant", "done"),
+ ];
+ const activity: TimelineActivityEntry = {
+ resumeToken: "unanchored-terminal",
+ source: "tool-source",
+ createdAt: iso(3_000),
+ value: {
+ kind: "model_tool_call",
+ message: "Calling read_file.",
+ callId: "missing",
+ toolName: "read_file",
+ messageSequence: 4,
+ },
+ };
+
+ const view = buildConversationPresentation({
+ messages,
+ activities: [activity],
+ isExecutionLive: false,
+ });
+
+ expect(view.earlierActivity).toEqual([activity]);
+ });
+
it("keeps current and resolved waits separate from tool execution", () => {
const messages = [
message(1, "user", "build"),
diff --git a/web/packages/superagent-ui/src/conversationPresentation.ts b/web/packages/superagent-ui/src/conversationPresentation.ts
index 4d1acb4..0bc6390 100644
--- a/web/packages/superagent-ui/src/conversationPresentation.ts
+++ b/web/packages/superagent-ui/src/conversationPresentation.ts
@@ -86,6 +86,7 @@ export interface ConversationTurn {
operations: ConversationWorkItem[];
questions: ConversationQuestionItem[];
activities: TimelineActivityEntry[];
+ liveActivities: TimelineActivityEntry[];
historicalActivities: TimelineActivityEntry[];
durationMs: number | null;
workDurationMs: number | null;
@@ -110,6 +111,13 @@ export interface ConversationPresentationInput {
const MODEL_TERMINAL = new Set(["model_completed", "model_failed"]);
const MODEL_LIFECYCLE = new Set(["model_started", ...MODEL_TERMINAL]);
+const LIVE_WORK_ACTIVITY = new Set([
+ ...MODEL_LIFECYCLE,
+ "model_tool_call",
+ "tool_progress",
+ "tool_failed",
+ "tool_completed",
+]);
const HISTORICAL_ACTIVITY = new Set([
"compacted",
"compaction_failed",
@@ -202,6 +210,7 @@ function groupDurableTurns(messages: readonly TimelineSequencedMessage[]) {
operations: [],
questions: [],
activities: [],
+ liveActivities: [],
historicalActivities: [],
durationMs: null,
workDurationMs: null,
@@ -227,6 +236,10 @@ export function buildConversationPresentation(
(timestamp(left.createdAt) ?? 0) - (timestamp(right.createdAt) ?? 0),
);
const { turns, sequenceToTurn } = groupDurableTurns(messages);
+ const currentLiveTurn = liveTurnForUnanchoredActivity(
+ turns,
+ input.isExecutionLive,
+ );
const earlierActivity: (TimelineActivityEntry | TimelineLiveTextEntry)[] = [];
const resultByCall = new Map