Skip to content
Source
Sign inSign up

AgentActivityWidget.tsx

199 lines6.8 KB

1import { HStack, Image, Spacer, Text, VStack } from "@expo/ui/swift-ui";
2import {
3 containerBackground,
4 font,
5 foregroundStyle,
6 frame,
7 layoutPriority,
8 lineLimit,
9 resizable,
10 widgetURL,
11} from "@expo/ui/swift-ui/modifiers";
12import { createWidget, type WidgetEnvironment } from "expo-widgets";
13​
14import type {
15 AgentActivityPhase,
16 AgentActivityProps,
17 AgentActivityRowProps,
18} from "./AgentActivity";
19​
20// Home-screen and Lock Screen counterpart of the AgentActivity Live Activity.
21// It shares the Live Activity's name, but expo-widgets stores widget and Live
22// Activity layouts under different keys, so each needs its own registration.
23export function AgentActivityWidget(
24 publishedProps: Partial<AgentActivityProps> | undefined,
25 environment: WidgetEnvironment,
26) {
27 "widget";
28 // The extension evaluates this function without the app's module scope.
29 // Before the app's first publish, the embedded layout renders with no props.
30 const props = publishedProps ?? {};
31 const family = environment.widgetFamily;
32 const accessory = family === "accessoryRectangular";
33 const small = family === "systemSmall";
34 const monochrome =
35 environment.widgetRenderingMode === "accented" ||
36 environment.widgetRenderingMode === "vibrant" ||
37 environment.isLuminanceReduced === true;
38 const isLightScheme = environment.colorScheme === "light";
39​
40 type Foreground = Parameters<typeof foregroundStyle>[0];
41 const primaryForeground = { type: "hierarchical", style: "primary" } as const;
42 const secondaryForeground = { type: "hierarchical", style: "secondary" } as const;
43 const phaseTint = (phase: AgentActivityPhase): Foreground => {
44 if (monochrome) return primaryForeground;
45 switch (phase) {
46 case "waiting_for_approval":
47 return isLightScheme ? "#d97706" : "#fcd34d";
48 case "waiting_for_input":
49 return isLightScheme ? "#4f46e5" : "#a5b4fc";
50 case "failed":
51 return isLightScheme ? "#dc2626" : "#fca5a5";
52 case "completed":
53 return isLightScheme ? "#059669" : "#6ee7b7";
54 case "stale":
55 return secondaryForeground;
56 case "starting":
57 case "running":
58 default:
59 return isLightScheme ? "#0284c7" : "#7dd3fc";
60 }
61 };
62 const phasePriority = (phase: AgentActivityPhase): number => {
63 if (phase === "waiting_for_approval" || phase === "waiting_for_input") return 0;
64 if (phase === "failed") return 1;
65 if (phase === "running" || phase === "starting") return 2;
66 return 3;
67 };
68​
69 const loaded = props.activities !== undefined;
70 const rows = [...(props.activities ?? [])].sort(
71 (a, b) => phasePriority(a.phase) - phasePriority(b.phase),
72 );
73 const shown = rows.slice(0, accessory || small ? 2 : 4);
74 const attentionCount = rows.filter(
75 (row) => row.phase === "waiting_for_approval" || row.phase === "waiting_for_input",
76 ).length;
77 const workingCount = rows.filter(
78 (row) => row.phase === "running" || row.phase === "starting",
79 ).length;
80 const summary =
81 attentionCount > 0
82 ? `${attentionCount} need${attentionCount === 1 ? "s" : ""} ${small ? "you" : "attention"}`
83 : workingCount > 0
84 ? `${props.activeCount ?? workingCount} active`
85 : rows.some((row) => row.phase === "stale")
86 ? "Out of date"
87 : rows.some((row) => row.phase === "failed")
88 ? "Failed"
89 : rows.length > 0
90 ? "Done"
91 : "";
92 const summaryTint = rows[0] ? phaseTint(rows[0].phase) : secondaryForeground;
93 const emptyMessage = loaded
94 ? (props.subtitle ?? "No active agents")
95 : "Open T3 Code to load agent activity";
96​
97 // Widget taps open the containing app, and every build accepts the prod
98 // scheme as a link prefix, so dev and preview builds route this too.
99 const linkRow = rows[0];
100 const deepLink =
101 linkRow && linkRow.deepLink.startsWith("/") && !linkRow.deepLink.startsWith("//")
102 ? `t3code://${linkRow.deepLink.slice(1)}`
103 : null;
104​
105 const background = accessory ? "clear" : isLightScheme ? "#ffffff" : "#1c1c1e";
106 const rootModifiers = [
107 containerBackground(background, "widget"),
108 ...(deepLink ? [widgetURL(deepLink)] : []),
109 ];
110​
111 // The template image ignores frame/foregroundStyle applied to itself; size
112 // and tint it through a container the resizable image fills.
113 const renderLogo = (height: number) => (
114 <HStack
115 modifiers={[frame({ width: height * 1.5, height }), foregroundStyle(primaryForeground)]}
116 >
117 <Image assetName="T3Mark" modifiers={[resizable()]} />
118 </HStack>
119 );
120​
121 const renderRow = (row: AgentActivityRowProps, key: string) =>
122 small || accessory ? (
123 <VStack key={key} alignment="leading" spacing={1}>
124 <Text
125 modifiers={[
126 font({ weight: "semibold", size: accessory ? 13 : 12 }),
127 foregroundStyle(primaryForeground),
128 lineLimit(1),
129 ]}
130 >
131 {row.threadTitle}
132 </Text>
133 <Text
134 modifiers={[
135 font({ weight: "medium", size: 11 }),
136 foregroundStyle(phaseTint(row.phase)),
137 lineLimit(1),
138 ]}
139 >
140 {row.status}
141 </Text>
142 </VStack>
143 ) : (
144 <HStack key={key} spacing={7} alignment="center">
145 <Text
146 modifiers={[
147 font({ weight: "semibold", size: 13 }),
148 foregroundStyle(primaryForeground),
149 lineLimit(1),
150 ]}
151 >
152 {row.threadTitle}
153 </Text>
154 <Text modifiers={[font({ size: 11 }), foregroundStyle(secondaryForeground), lineLimit(1)]}>
155 {row.projectTitle}
156 </Text>
157 <Spacer minLength={8} />
158 <Text
159 modifiers={[
160 font({ weight: "semibold", size: 11 }),
161 foregroundStyle(phaseTint(row.phase)),
162 layoutPriority(1),
163 ]}
164 >
165 {row.status}
166 </Text>
167 </HStack>
168 );
169​
170 return (
171 <VStack alignment="leading" spacing={accessory ? 2 : 8} modifiers={rootModifiers}>
172 <HStack spacing={6} alignment="center">
173 {accessory ? null : renderLogo(13)}
174 <Text
175 modifiers={[
176 font({ weight: "semibold", size: 12 }),
177 foregroundStyle(accessory ? primaryForeground : summaryTint),
178 lineLimit(1),
179 ]}
180 >
181 {summary || (accessory ? "T3 Code" : "")}
182 </Text>
183 <Spacer minLength={0} />
184 </HStack>
185 {shown.length > 0 ? (
186 <VStack alignment="leading" spacing={accessory ? 1 : small ? 6 : 5}>
187 {shown.map((row, index) => renderRow(row, `${row.threadId}:${index}`))}
188 </VStack>
189 ) : (
190 <Text modifiers={[font({ size: 12 }), foregroundStyle(secondaryForeground), lineLimit(2)]}>
191 {emptyMessage}
192 </Text>
193 )}
194 {accessory ? null : <Spacer minLength={0} />}
195 </VStack>
196 );
198​
199export default createWidget<Partial<AgentActivityProps>>("AgentActivity", AgentActivityWidget);