1import type {
2 RelayAgentActivityAggregateState,
3 RelayAgentActivityState,
4} from "@t3tools/contracts/relay";
5import * as DateTime from "effect/DateTime";
6import * as Option from "effect/Option";
7import {
8 isExpiredAgentActivityState,
9 isTerminalPhase,
10 MAX_ACTIVITY_ROWS,
11 sanitizeAgentActivityAggregateState,
12} from "./agentActivityPayloads.ts";
13
14export function statusForPhase(phase: RelayAgentActivityState["phase"]): string {
15 switch (phase) {
16 case "waiting_for_approval":
17 return "Approval";
18 case "waiting_for_input":
19 return "Input";
20 case "completed":
21 return "Done";
22 case "failed":
23 return "Failed";
24 case "starting":
25 // Match the web sidebar's wording in Sidebar.logic.ts.
26 return "Connecting";
27 case "running":
28 return "Working";
29 case "stale":
30 return "Waiting";
31 }
32}
33
34function aggregateRowForState(state: RelayAgentActivityState) {
35 return {
36 environmentId: state.environmentId,
37 threadId: state.threadId,
38 projectTitle: state.projectTitle,
39 threadTitle: state.threadTitle,
40 modelTitle: state.modelTitle,
41 phase: state.phase,
42 status: statusForPhase(state.phase),
43 updatedAt: state.updatedAt,
44 deepLink: state.deepLink,
45 };
46}
47
48function terminalAggregateState(state: RelayAgentActivityState): RelayAgentActivityAggregateState {
49 return sanitizeAgentActivityAggregateState({
50 title: "T3 Code",
51 subtitle: state.phase === "failed" ? "Agent work failed" : "Agent work completed",
52 activeCount: 0,
53 updatedAt: state.updatedAt,
54 activities: [aggregateRowForState(state)],
55 });
56}
57
58export const TERMINAL_AGENT_ACTIVITY_DISPLAY_TTL_MS = 15 * 60 * 1_000;
59
60function isRecentTerminalState(state: RelayAgentActivityState, nowMs: number): boolean {
61 if (!isTerminalPhase(state)) {
62 return false;
63 }
64 const updatedAtMs = Option.match(DateTime.make(state.updatedAt), {
65 onNone: () => Number.NaN,
66 onSome: (dt) => dt.epochMilliseconds,
67 });
68 if (Number.isNaN(updatedAtMs)) {
69 return false;
70 }
71 return nowMs - updatedAtMs <= TERMINAL_AGENT_ACTIVITY_DISPLAY_TTL_MS;
72}
73
74export function makeAggregateState(input: {
75 readonly activeStates: ReadonlyArray<RelayAgentActivityState>;
76 readonly terminalState: RelayAgentActivityState | null;
77 readonly nowMs: number;
78}): RelayAgentActivityAggregateState | null {
79 const activeStates = input.activeStates.filter(
80 (state) => !isTerminalPhase(state) && !isExpiredAgentActivityState(state, input.nowMs),
81 );
82 if (activeStates.length === 0) {
83 if (input.terminalState !== null) {
84 return terminalAggregateState(input.terminalState);
85 }
86 // Alerts compare with the delivered aggregate; replays must repaint without buzzing.
87 const recentTerminal = input.activeStates
88 .filter((state) => isRecentTerminalState(state, input.nowMs))
89 .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
90 const newest = recentTerminal[0];
91 if (!newest) {
92 return null;
93 }
94 return sanitizeAgentActivityAggregateState({
95 title: "T3 Code",
96 subtitle: newest.phase === "failed" ? "Agent work failed" : "Agent work completed",
97 activeCount: 0,
98 updatedAt: newest.updatedAt,
99 activities: recentTerminal.slice(0, MAX_ACTIVITY_ROWS).map(aggregateRowForState),
100 });
101 }
102 const recentTerminalStates = input.activeStates
103 .filter((state) => isRecentTerminalState(state, input.nowMs))
104 .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
105 const displayedStates = [
106 ...activeStates
107 .toSorted((a, b) => activityPhasePriority(a.phase) - activityPhasePriority(b.phase))
108 .slice(0, MAX_ACTIVITY_ROWS),
109 ...recentTerminalStates,
110 ].slice(0, MAX_ACTIVITY_ROWS);
111 const updatedAt = [...activeStates, ...recentTerminalStates].reduce((latest, state) =>
112 state.updatedAt.localeCompare(latest.updatedAt) > 0 ? state : latest,
113 ).updatedAt;
114 return sanitizeAgentActivityAggregateState({
115 title: "T3 Code",
116 subtitle: "Agent work in progress",
117 activeCount: activeStates.length,
118 updatedAt,
119 activities: displayedStates.map(aggregateRowForState),
120 });
121}
122
123export function activityPhasePriority(phase: RelayAgentActivityState["phase"]): number {
124 if (phase === "waiting_for_approval" || phase === "waiting_for_input") return 0;
125 if (phase === "failed") return 1;
126 if (phase === "starting" || phase === "running") return 2;
127 return 3;
128}