1import type {
2 EnvironmentProject,
3 EnvironmentThreadShell,
4} from "@t3tools/client-runtime/state/shell";
5
6import type {
7 AgentActivityPhase,
8 AgentActivityProps,
9 AgentActivityRowProps,
10} from "../../widgets/AgentActivity";
11
12/**
13 * Agent work shown by the agent-activity scene. The rows point at
14 * seeded showcase threads so the copy matches the thread list, but the phases
15 * are staged: the relay that normally derives them is not part of the capture.
16 * Rows follow the relay's order: attention first, then running, then finished.
17 */
18export const SHOWCASE_AGENT_ACTIVITY_ROWS = [
19 { threadId: "pocket-command-center", phase: "waiting_for_approval", minutesAgo: 1 },
20 { threadId: "beautiful-boot", phase: "waiting_for_input", minutesAgo: 4 },
21 { threadId: "buttery-suspense", phase: "running", minutesAgo: 2 },
22 { threadId: "remote-command-center", phase: "completed", minutesAgo: 3 },
23] as const satisfies ReadonlyArray<{
24 readonly threadId: string;
25 readonly phase: AgentActivityPhase;
26 readonly minutesAgo: number;
27}>;
28
29// Matches the relay's row wording (AgentActivityPublisher.statusForPhase).
30const STATUS_BY_PHASE: Record<AgentActivityPhase, string> = {
31 starting: "Connecting",
32 running: "Working",
33 waiting_for_approval: "Approval",
34 waiting_for_input: "Input",
35 stale: "Waiting",
36 completed: "Done",
37 failed: "Failed",
38};
39
40const ACTIVE_PHASES: ReadonlySet<AgentActivityPhase> = new Set([
41 "starting",
42 "running",
43 "waiting_for_approval",
44 "waiting_for_input",
45]);
46
47/** Returns null until every staged thread and its project have loaded. */
48export function buildShowcaseAgentActivity(
49 threads: ReadonlyArray<EnvironmentThreadShell>,
50 projects: ReadonlyArray<EnvironmentProject>,
51 now: number,
52): AgentActivityProps | null {
53 const rows: AgentActivityRowProps[] = [];
54 for (const definition of SHOWCASE_AGENT_ACTIVITY_ROWS) {
55 const thread = threads.find((candidate) => String(candidate.id) === definition.threadId);
56 const project = thread
57 ? projects.find(
58 (candidate) =>
59 candidate.environmentId === thread.environmentId && candidate.id === thread.projectId,
60 )
61 : undefined;
62 if (!thread || !project) return null;
63 const environmentId = String(thread.environmentId);
64 rows.push({
65 environmentId,
66 threadId: definition.threadId,
67 projectTitle: project.title,
68 threadTitle: thread.title,
69 modelTitle: "",
70 phase: definition.phase,
71 status: STATUS_BY_PHASE[definition.phase],
72 updatedAt: new Date(now - definition.minutesAgo * 60_000).toISOString(),
73 deepLink: `/threads/${encodeURIComponent(environmentId)}/${encodeURIComponent(definition.threadId)}`,
74 });
75 }
76 return {
77 title: "The Knife",
78 subtitle: "Agent work in progress",
79 activeCount: rows.filter((row) => ACTIVE_PHASES.has(row.phase)).length,
80 updatedAt: new Date(now).toISOString(),
81 activities: rows,
82 };
83}
84
85/** The alert the relay sends when the hero row starts waiting on the user. */
86export function showcaseAgentAlert(activity: AgentActivityProps) {
87 const row = activity.activities[0];
88 if (!row) return null;
89 return {
90 title: row.threadTitle,
91 body: `${row.status}: ${row.projectTitle}`,
92 path: row.deepLink,
93 environmentId: row.environmentId,
94 threadId: row.threadId,
95 };
96}
97
98/**
99 * The FCM data map Android's native receiver renders, mirroring the relay's
100 * androidActivityData so the shade shows exactly what a real push produces.
101 */
102export function showcaseAndroidActivityData(
103 activity: AgentActivityProps,
104 now: number,
105): Record<string, string> {
106 const attentionCount = activity.activities.filter(
107 (row) => row.phase === "waiting_for_approval" || row.phase === "waiting_for_input",
108 ).length;
109 const hero = activity.activities[0];
110 const alert = showcaseAgentAlert(activity);
111 return {
112 t3_kind: "agent_activity",
113 updated_at: String(now),
114 active: String(activity.activeCount > 0),
115 activity_chip: attentionCount > 0 ? "Review" : "Active",
116 activity_title: `${activity.activeCount} active agents · ${attentionCount} need attention`,
117 activity_phase: hero?.phase ?? "",
118 activity_active_count: String(activity.activeCount),
119 activity_attention_count: String(attentionCount),
120 activity_body: hero ? `${hero.status}: ${hero.threadTitle} · ${hero.projectTitle}` : "",
121 ...Object.fromEntries(
122 activity.activities.map((row, index) => [
123 `activity_line_${index}`,
124 [row.status, row.threadTitle, row.projectTitle].join("\t"),
125 ]),
126 ),
127 activity_path: hero?.deepLink ?? "/",
128 activity_expires_at: String(now + 2 * 60 * 60_000),
129 ...(alert
130 ? {
131 alert_id: "showcase-alert",
132 alert_title: alert.title,
133 alert_body: alert.body,
134 alert_path: alert.path,
135 }
136 : {}),
137 };
138}