import { MessageId, RunId, ThreadId, TurnItemId, type OrchestrationV2ProjectedTurnItem, type OrchestrationV2TurnItem, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; import { describe, expect, it } from "vite-plus/test"; import { buildThreadFeed, deriveThreadFeedPresentation, threadFeedRunIsUnsettled, type ThreadFeedActivity, type ThreadFeedEntry, } from "./threadActivity"; const threadId = ThreadId.make("thread-1"); const sourceThreadId = ThreadId.make("thread-source"); const runId = RunId.make("run-1"); function base(id: string, updatedAt: string, ordinal: number) { const timestamp = DateTime.makeUnsafe(updatedAt); return { id: TurnItemId.make(id), threadId, runId, nodeId: null, providerThreadId: null, providerTurnId: null, nativeItemRef: null, parentItemId: null, ordinal, status: "completed" as const, title: null, startedAt: timestamp, completedAt: timestamp, updatedAt: timestamp, }; } function projected( item: OrchestrationV2TurnItem, position: number, visibility: OrchestrationV2ProjectedTurnItem["visibility"] = "local", ): OrchestrationV2ProjectedTurnItem { return { position, visibility, sourceThreadId: visibility === "local" ? threadId : sourceThreadId, sourceItemId: item.id, item, }; } function userMessage(updatedAt = "2026-06-20T00:00:01.000Z") { return { ...base("item-user", updatedAt, 0), type: "user_message" as const, messageId: MessageId.make("message-user"), createdBy: "user" as const, creationSource: "mobile" as const, inputIntent: "turn_start" as const, text: "Run checks", attachments: [], }; } function command(updatedAt = "2026-06-20T00:00:02.000Z") { return { ...base("item-command", updatedAt, 1), type: "command_execution" as const, input: "vp check", output: "ok", exitCode: 0, }; } function assistantMessage(updatedAt = "2026-06-20T00:00:03.000Z") { return { ...base("item-assistant", updatedAt, 2), type: "assistant_message" as const, messageId: MessageId.make("message-assistant"), text: "Done", streaming: false, }; } describe("buildThreadFeed", () => { it("does not treat a queued-only run as live feed activity", () => { expect( threadFeedRunIsUnsettled({ runId, status: "queued", startedAt: null, completedAt: null, }), ).toBe(false); expect( threadFeedRunIsUnsettled({ runId, status: "running", startedAt: "2026-06-20T00:00:01.000Z", completedAt: null, }), ).toBe(true); }); it("adds queued input only after dispatch creates its turn item", () => { const dispatchedRunId = RunId.make("run-dispatched-queued"); const dispatchedMessageId = MessageId.make("message-dispatched-queued"); expect(buildThreadFeed([])).toEqual([]); const promotedEntries = buildThreadFeed([ projected( { ...userMessage(), id: TurnItemId.make("item-dispatched-queued"), runId: dispatchedRunId, messageId: dispatchedMessageId, inputIntent: "turn_start", }, 0, ), ]); expect(promotedEntries.map((entry) => entry.id)).toEqual([dispatchedMessageId]); expect( promotedEntries[0]?.type === "message" ? promotedEntries[0].message.inputIntent : undefined, ).toBe("turn_start"); }); it("preserves authoritative V2 order instead of sorting reconstructed collections", () => { const rows = [ projected(userMessage("2026-06-20T00:00:03.000Z"), 0), projected(command("2026-06-20T00:00:01.000Z"), 1), projected(assistantMessage("2026-06-20T00:00:02.000Z"), 2), ]; const feed = buildThreadFeed(rows); expect(feed.map((entry) => entry.type)).toEqual(["message", "activity-group", "message"]); expect(feed.map((entry) => entry.id)).toEqual([ "message-user", "local:thread-1:item-command", "message-assistant", ]); const activity = feed.find((entry) => entry.type === "activity-group")?.activities[0]; expect(activity?.projectedItem).toBe(rows[1]); expect(activity?.getFullDetail()).toContain('"input": "vp check"'); }); it("retains inherited and synthetic rows with their original projected identity", () => { const inherited = projected(command(), 0, "inherited"); const { providerThreadId: _providerThreadId, ...forkBase } = base( "item-fork", "2026-06-20T00:00:03.000Z", 2, ); const synthetic = projected( { ...forkBase, type: "fork", source: { type: "run", threadId: sourceThreadId, runId }, targetThreadId: threadId, }, 1, "synthetic", ); const feed = buildThreadFeed([inherited, synthetic]); const activities = feed.flatMap((entry) => entry.type === "activity-group" ? entry.activities : [], ); expect(activities.map((activity) => activity.projectedItem)).toEqual([inherited, synthetic]); expect(activities.map((activity) => activity.projectedItem.visibility)).toEqual([ "inherited", "synthetic", ]); expect(activities.at(-1)?.prominent).toBe(true); }); it("keeps orchestration relationship cards visible when a completed run is folded", () => { const { providerThreadId: _providerThreadId, ...forkBase } = base( "item-fork", "2026-06-20T00:00:02.500Z", 2, ); const feed = buildThreadFeed([ projected(userMessage(), 0), projected(command(), 1), projected( { ...forkBase, type: "fork", source: { type: "run", threadId, runId }, targetThreadId: sourceThreadId, }, 2, ), projected(assistantMessage(), 3), ]); const collapsed = deriveThreadFeedPresentation( feed, { runId, status: "completed", startedAt: "2026-06-20T00:00:01.000Z", completedAt: "2026-06-20T00:00:03.000Z", }, new Set(), ); expect( collapsed.some( (entry) => entry.type === "activity-group" && entry.activities.some((activity) => activity.projectedItem.item.type === "fork"), ), ).toBe(true); expect( collapsed.some( (entry) => entry.type === "activity-group" && entry.activities.some( (activity) => activity.projectedItem.item.type === "command_execution", ), ), ).toBe(false); }); it("folds settled V2 run work while keeping the terminal assistant message visible", () => { const feed = buildThreadFeed([ projected(userMessage(), 0), projected(command(), 1), projected(assistantMessage(), 2), ]); const latestRun = { runId, status: "completed" as const, startedAt: "2026-06-20T00:00:01.000Z", completedAt: "2026-06-20T00:00:03.000Z", }; const collapsed = deriveThreadFeedPresentation(feed, latestRun, new Set()); expect(collapsed.map((entry) => entry.type)).toEqual(["message", "run-fold", "message"]); const expanded = deriveThreadFeedPresentation(feed, latestRun, new Set([runId])); expect(expanded.map((entry) => entry.type)).toEqual([ "message", "run-fold", "activity-group", "message", ]); }); it("keeps an active run expanded and marks failed tools as failures", () => { const failedCommand: OrchestrationV2TurnItem = { ...command(), status: "failed", completedAt: DateTime.makeUnsafe("2026-06-20T00:00:02.000Z"), }; const feed = buildThreadFeed([projected(userMessage(), 0), projected(failedCommand, 1)]); const presented = deriveThreadFeedPresentation( feed, { runId, status: "running", startedAt: "2026-06-20T00:00:01.000Z", completedAt: null, }, new Set(), ); expect(presented.some((entry) => entry.type === "run-fold")).toBe(false); expect(presented.find((entry) => entry.type === "activity-group")?.activities[0]?.status).toBe( "failure", ); }); it("appends active work as a normal timeline row", () => { const startedAt = "2026-04-01T00:00:01.000Z"; const presented = deriveThreadFeedPresentation([], null, new Set(), new Set(), startedAt); expect(presented).toEqual([ { type: "working", id: "working-indicator-row", createdAt: startedAt, }, ]); expect(deriveThreadFeedPresentation(presented, null, new Set())).toEqual([]); }); it("models work-log overflow as list rows", () => { const activity = ( id: string, createdAt: string, status: ThreadFeedActivity["status"] = "success", ): ThreadFeedActivity => ({ id, createdAt, runId: null, summary: `Tool ${id}`, detail: null, canExpand: false, getFullDetail: () => null, getCopyText: () => id, icon: "command", logo: null, toolLike: true, prominent: false, status, projectedItem: projected(command(createdAt), 0), }); const feed: ThreadFeedEntry[] = [ { type: "activity-group", id: "work-group-1", createdAt: "2026-04-01T00:00:01.000Z", runId: null, activities: [ activity("activity-1", "2026-04-01T00:00:01.000Z"), activity("activity-neutral", "2026-04-01T00:00:02.000Z", "neutral"), activity("activity-2", "2026-04-01T00:00:03.000Z"), activity("activity-3", "2026-04-01T00:00:04.000Z"), ], }, ]; const collapsed = deriveThreadFeedPresentation(feed, null, new Set()); expect(collapsed.map((entry) => entry.id)).toEqual(["activity-3", "work-toggle:work-group-1"]); expect(collapsed[1]).toMatchObject({ type: "work-toggle", groupId: "work-group-1", hiddenCount: 2, expanded: false, }); const expanded = deriveThreadFeedPresentation(feed, null, new Set(), new Set(["work-group-1"])); expect(expanded.map((entry) => entry.id)).toEqual([ "activity-1", "activity-2", "activity-3", "work-toggle:work-group-1", ]); expect(expanded.at(-1)).toMatchObject({ type: "work-toggle", expanded: true, }); }); it("pretty prints T3 MCP dynamic tool activities and attaches the product logo", () => { const toolItem: OrchestrationV2TurnItem = { ...base("item-t3-tool", "2026-06-20T00:00:04.000Z", 3), type: "dynamic_tool", toolName: "mcp__t3-code__t3_thread_read", input: { threadId: "thread-child" }, output: { messages: [] }, }; const feed = buildThreadFeed([projected(toolItem, 0)]); const activity = feed[0]?.type === "activity-group" ? feed[0].activities[0] : null; expect(activity?.summary).toBe("Read a T3 thread"); expect(activity?.logo).toBe("t3-code"); expect(activity?.getCopyText().split("\n")[0]).toBe("Read a T3 thread"); }); });