@schandlergarcia/sf-web-components 1.9.82 → 1.9.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -114,14 +114,17 @@ Only create `Flight__c.Weather_Impact__c` (PRD section 14a) AFTER the user confi
114
114
 
115
115
  **How to work:** Make surgical edits to the existing `EngineDashboard.tsx`. Do NOT rewrite the file.
116
116
 
117
+ > **Important:** We do NOT use `AgentforceConversationClient` (the iframe widget). All agent communication goes through the REST-based Agent API via the `useEvaAgent` hook. The Vite dev server proxies the requests (`/sf-oauth` and `/sf-agent`) to avoid CORS issues.
118
+
117
119
  **Files to read first (only these):**
118
120
  1. `src/pages/EngineDashboard.tsx` — the current dashboard
119
121
 
120
122
  **Edits to make (in order):**
121
123
 
122
- 1. **Add ChatBar import** (if not already imported from the barrel):
124
+ 1. **Add imports:**
123
125
  ```tsx
124
126
  import { ChatBar } from "@/components/library";
127
+ import useEvaAgent from "@/hooks/useEvaAgent";
125
128
  ```
126
129
 
127
130
  2. **Add chat suggestions constant** at module scope:
@@ -134,9 +137,15 @@ Only create `Flight__c.Weather_Impact__c` (PRD section 14a) AFTER the user confi
134
137
  ];
135
138
  ```
136
139
 
137
- 3. **Add handleChat function** at module scope:
140
+ 3. **Add agent hook + handler** inside the component:
138
141
  ```tsx
139
- const handleChat = (msg: string) => ({ text: `Looking into: "${msg}"…` });
142
+ const { messages: evaMessages, isReady, isSending, connect, sendMessage } = useEvaAgent();
143
+ useEffect(() => { connect(); }, [connect]);
144
+
145
+ const handleChat = (text: string) => {
146
+ sendMessage(text);
147
+ return { text: `Looking into: "${text}"…` };
148
+ };
140
149
  ```
141
150
 
142
151
  4. **Insert ChatBar JSX** between the map section and the data panels:
@@ -151,7 +160,7 @@ Only create `Flight__c.Weather_Impact__c` (PRD section 14a) AFTER the user confi
151
160
  </div>
152
161
  ```
153
162
 
154
- 5. **Add AgentforceConversationClient** with `agentId="0Xxa5000000relhCAA"` and `agentLabel="Eva"`
163
+ 5. Do **NOT** add `AgentforceConversationClient` the `useEvaAgent` hook replaces it entirely.
155
164
 
156
165
  **After adding Eva — STOP and suggest (do not create yet):**
157
166
 
@@ -166,6 +175,7 @@ Say: *"Now I'll write an Apex service class to publish disruption records to tha
166
175
  Only create `TravelDisruptionEventService` (PRD section 14c) AFTER the user confirms.
167
176
 
168
177
  **What NOT to build without asking:**
178
+ - Do NOT use `AgentforceConversationClient` — use `useEvaAgent` hook instead
169
179
  - Do NOT create platform events or Apex classes without user confirmation
170
180
  - Do NOT rewrite EngineDashboard.tsx from scratch
171
181
  - Do NOT read library component source files
@@ -182,8 +192,10 @@ Only create `TravelDisruptionEventService` (PRD section 14c) AFTER the user conf
182
192
  | Flip `dataStrategy.ts` to `false` | Skip | Edit | — |
183
193
  | Custom field metadata | Skip | Suggest → wait | — |
184
194
  | ChatBar (Eva) | Skip | Skip | Surgical edit |
185
- | AgentforceConversationClient | Skip | Skip | Surgical edit |
195
+ | `useEvaAgent` hook (Agent API) | Skip | Skip | Surgical edit |
186
196
  | Platform event | Skip | Skip | Suggest → wait |
187
197
  | Apex service | Skip | Skip | Suggest → wait |
188
198
 
189
199
  **Data mode control:** `ENABLE_SAMPLE_DATA_CACHE` in `src/lib/dataStrategy.ts` is the backend switch. `CommandCenter.tsx` reads this flag and passes `initialMode` to `DataModeProvider`. Phase 1 leaves it `true` (sample data). Phase 2 flips it to `false` (live data — different travelers, cities, and metrics). There is no UI toggle.
200
+
201
+ **Agent communication:** `useEvaAgent` hook in `src/hooks/useEvaAgent.ts` handles OAuth → session → messages → cleanup via the Agent REST API. Config is in `src/config/agentApi.ts`. Vite proxy rules (`/sf-oauth`, `/sf-agent`) in `vite.config.ts` forward requests to the Salesforce org and Agent API host.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.9.84] - 2026-04-07
9
+
10
+ ### Fixed
11
+ - **Reset script no longer deletes custom fields** — `.field-meta.xml` files in `force-app/main/default/objects/` are now preserved across resets. Only Apex classes and platform events are cleaned.
12
+ - **Reset script preserves `TCC_TravelMetricsController`** — Apex class cleanup now skips permanent classes listed in `KEEP_CLASSES` array.
13
+
14
+ ## [1.9.83] - 2026-04-07
15
+
16
+ ### Added
17
+ - **Agentforce Agent REST API integration** — Replaced iframe-based `AgentforceConversationClient` with direct API calls via the new `useEvaAgent` hook (`data/useEvaAgent.ts`). The hook handles the full lifecycle: OAuth client-credentials → create session → send messages → end session on unmount.
18
+ - **Agent API config** (`data/agentApiConfig.ts`) — Credentials and endpoint configuration for the Agentforce Agent API, installed to `src/config/agentApi.ts`.
19
+ - **Vite proxy rules** — Added `/sf-oauth` and `/sf-agent` proxy entries in `vite.config.ts` template so Agent API calls from the browser avoid CORS.
20
+ - **Postinstall** — Now copies `useEvaAgent.ts` to `src/hooks/` and `agentApiConfig.ts` to `src/config/agentApi.ts`.
21
+
22
+ ### Changed
23
+ - **PRD section 9** — Replaced `AgentforceConversationClient` instructions with `useEvaAgent` hook usage and Agent API flow.
24
+ - **Build guide Phase 3** — Updated surgical edit instructions to use `useEvaAgent` instead of the iframe widget.
25
+
8
26
  ## [1.9.82] - 2026-04-06
9
27
 
10
28
  ### Fixed
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Agentforce Agent API Configuration
3
+ *
4
+ * These values connect the ChatBar to the real Agentforce agent (Eva)
5
+ * via the REST-based Agent API instead of the iframe-based
6
+ * AgentforceConversationClient.
7
+ *
8
+ * Flow: OAuth token → create session → send messages → end session
9
+ *
10
+ * All requests are proxied through the Vite dev server to avoid CORS.
11
+ * See vite.config.ts proxy rules:
12
+ * /sf-oauth/* → myDomainUrl (for OAuth token)
13
+ * /sf-agent/* → agentApiBaseUrl (for Agent API calls)
14
+ */
15
+
16
+ export const AGENT_API_CONFIG = {
17
+ myDomainUrl: "https://tdx26-keynote-org-1com.my.salesforce.com",
18
+
19
+ clientId:
20
+ "3MVG9Gm6vbdjgMWSOIAuIN3VSB5Rju6PgYQ5rl1yH3bVTTg9E2as4.C61Q0cyT.zqv2vUWNaxrm.A7SW5o3t7",
21
+ clientSecret:
22
+ "9ADF795A183A6B074A2E4B4CB1748B8DF7090C74191AF1C190213B512A733E03",
23
+
24
+ agentId: "0Xxa5000000rQlxCAE",
25
+
26
+ agentApiBaseUrl: "https://api.salesforce.com",
27
+
28
+ bypassUser: true,
29
+
30
+ demoTraveler: {
31
+ contactId: "003a500000mj4TlAAI",
32
+ email: "sarah.chen@arcline.ai",
33
+ firstName: "Sarah",
34
+ lastName: "Chen",
35
+ },
36
+ };
@@ -254,9 +254,13 @@ const spendChartData = useDataSource({
254
254
 
255
255
  ## 9. Eva — Agentforce Integration
256
256
 
257
- Two components work together:
257
+ Two pieces work together: a **ChatBar** for user input and the **`useEvaAgent` hook** which talks to the Agentforce Agent REST API.
258
258
 
259
- **ChatBar** a command-palette strip below the hero map for quick suggested prompts. NOT in the header. NOT a FAB or sliding panel.
259
+ > **Important:** We do NOT use `AgentforceConversationClient` (the iframe widget). All agent communication goes through the REST-based Agent API via the `useEvaAgent` hook. The Vite dev server proxies the requests to avoid CORS issues.
260
+
261
+ ### ChatBar
262
+
263
+ A command-palette strip below the hero map. NOT in the header. NOT a FAB or sliding panel.
260
264
 
261
265
  ```tsx
262
266
  <div className="px-4 pt-4">
@@ -271,20 +275,35 @@ Two components work together:
271
275
 
272
276
  Suggestions: "Storm warning in the Midwest — which travelers are affected?", "What's our severe weather rebooking policy?", "Notify Anna and Sofia about the Chicago delays", "Create a support case for Anna Johansson's flight"
273
277
 
274
- Define `handleChat` and `CHAT_SUGGESTIONS` at module scope.
278
+ ### useEvaAgent Hook
275
279
 
276
- **AgentforceConversationClient** the real Salesforce agent. Wire it with the Eva agent ID. Place it at the bottom of the dashboard component (it renders as a floating widget).
280
+ The hook manages the full Agent API lifecycle OAuth, session, messaging, cleanup.
277
281
 
278
282
  ```tsx
279
- import { AgentforceConversationClient } from "@/components/AgentforceConversationClient";
283
+ import useEvaAgent from "@/hooks/useEvaAgent";
280
284
 
281
- <AgentforceConversationClient
282
- agentId="0Xxa5000000relhCAA"
283
- agentLabel="Eva"
284
- />
285
+ // Inside the component:
286
+ const { messages, isReady, isSending, connect, sendMessage } = useEvaAgent();
287
+
288
+ // Connect on mount (or on first ChatBar focus):
289
+ useEffect(() => { connect(); }, [connect]);
290
+
291
+ // Wire to ChatBar:
292
+ const handleChat = (text: string) => {
293
+ sendMessage(text);
294
+ return { text: `Looking into: "${text}"…` };
295
+ };
285
296
  ```
286
297
 
287
- The `AgentforceConversationClient` is already in the codebase (commented out in `appLayout.tsx`). Uncomment it or add it directly to the dashboard with the agent ID above.
298
+ The hook:
299
+ 1. **Authenticates** via OAuth client-credentials (`POST /sf-oauth/services/oauth2/token`)
300
+ 2. **Creates a session** (`POST /sf-agent/einstein/ai-agent/v1/agents/{agentId}/sessions`)
301
+ 3. **Sends messages** (`POST /sf-agent/…/messages`) — returns agent response text
302
+ 4. **Ends the session** (`DELETE /sf-agent/…`) on unmount
303
+
304
+ Config is in `src/config/agentApi.ts`. Proxy rules are in `vite.config.ts` (`/sf-oauth` and `/sf-agent`).
305
+
306
+ Define `CHAT_SUGGESTIONS` at module scope.
288
307
 
289
308
  ---
290
309
 
@@ -382,11 +401,13 @@ Build incrementally in 3 prompts. Each prompt builds on the previous result. The
382
401
 
383
402
  1. Make **surgical edits** to the existing `EngineDashboard.tsx` — do NOT rewrite the entire file
384
403
  2. Add `ChatBar` with suggestions from section 9, placed between map and data panels
385
- 3. Add `AgentforceConversationClient` with `agentId="0Xxa5000000relhCAA"` and `agentLabel="Eva"`
386
- 4. After adding Eva, **STOP and suggest** the platform eventdo NOT create it until the user confirms
387
- 5. After user confirms and platform event is created, **STOP and suggest** the Apex class — do NOT create it until the user confirms
404
+ 3. Add `useEvaAgent` hook — import from `@/hooks/useEvaAgent`, call `connect()` on mount, wire `sendMessage` to `handleChat`
405
+ 4. Do NOT use `AgentforceConversationClient` (the iframe widget)we use the Agent REST API via the `useEvaAgent` hook
406
+ 5. After adding Eva, **STOP and suggest** the platform event — do NOT create it until the user confirms
407
+ 6. After user confirms and platform event is created, **STOP and suggest** the Apex class — do NOT create it until the user confirms
388
408
 
389
409
  **What the agent must NOT do in this prompt:**
410
+ - Do NOT use `AgentforceConversationClient` — use `useEvaAgent` hook instead (Agent REST API)
390
411
  - Do NOT rewrite `EngineDashboard.tsx` from scratch — make targeted edits to the existing Phase 2 file
391
412
  - Do NOT create platform events or Apex classes without user confirmation — each is a scripted demo moment where you suggest, wait, then create
392
413
  - Do NOT read library component source files
@@ -0,0 +1,288 @@
1
+ /**
2
+ * useEvaAgent — React hook for the Agentforce Agent REST API
3
+ *
4
+ * Handles the full lifecycle:
5
+ * 1. OAuth client-credentials → access_token
6
+ * 2. POST …/sessions → sessionId + message href
7
+ * 3. POST …/messages → agent response text
8
+ * 4. DELETE …/sessions → cleanup on unmount
9
+ *
10
+ * All HTTP calls go through the Vite proxy (see vite.config.ts):
11
+ * /sf-oauth/* → myDomainUrl
12
+ * /sf-agent/* → agentApiBaseUrl
13
+ */
14
+
15
+ import { useCallback, useEffect, useRef, useState } from "react";
16
+ import { AGENT_API_CONFIG } from "@/config/agentApi";
17
+
18
+ export interface AgentMessage {
19
+ role: "user" | "agent";
20
+ text: string;
21
+ id: string;
22
+ timestamp: string;
23
+ }
24
+
25
+ interface SessionState {
26
+ accessToken: string;
27
+ sessionId: string;
28
+ messagesHref: string;
29
+ endHref: string;
30
+ }
31
+
32
+ export default function useEvaAgent() {
33
+ const [messages, setMessages] = useState<AgentMessage[]>([]);
34
+ const [isConnecting, setIsConnecting] = useState(false);
35
+ const [isReady, setIsReady] = useState(false);
36
+ const [isSending, setIsSending] = useState(false);
37
+ const [error, setError] = useState<string | null>(null);
38
+
39
+ const sessionRef = useRef<SessionState | null>(null);
40
+ const sequenceRef = useRef(1);
41
+ const abortRef = useRef<AbortController | null>(null);
42
+
43
+ /* ── Step 1: OAuth token ──────────────────────────────── */
44
+ async function authenticate(signal: AbortSignal): Promise<string> {
45
+ const { clientId, clientSecret } = AGENT_API_CONFIG;
46
+
47
+ const body = new URLSearchParams({
48
+ grant_type: "client_credentials",
49
+ client_id: clientId,
50
+ client_secret: clientSecret,
51
+ });
52
+
53
+ const res = await fetch("/sf-oauth/services/oauth2/token", {
54
+ method: "POST",
55
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
56
+ body,
57
+ signal,
58
+ });
59
+
60
+ if (!res.ok) {
61
+ const text = await res.text();
62
+ throw new Error(`OAuth failed (${res.status}): ${text}`);
63
+ }
64
+
65
+ const data = await res.json();
66
+ return data.access_token;
67
+ }
68
+
69
+ /* ── Step 2: Create session ───────────────────────────── */
70
+ async function createSession(
71
+ accessToken: string,
72
+ signal: AbortSignal
73
+ ): Promise<Omit<SessionState, "accessToken">> {
74
+ const { agentId, bypassUser, demoTraveler, myDomainUrl } =
75
+ AGENT_API_CONFIG;
76
+
77
+ const payload = {
78
+ externalSessionKey: crypto.randomUUID(),
79
+ instanceConfig: { endpoint: myDomainUrl },
80
+ streamingCapabilities: { chunkTypes: ["Text"] },
81
+ bypassUser,
82
+ variables: [
83
+ {
84
+ name: "$Context.EndUserId",
85
+ type: "Text",
86
+ value: demoTraveler.contactId,
87
+ },
88
+ {
89
+ name: "$Context.EndUserEmail",
90
+ type: "Text",
91
+ value: demoTraveler.email,
92
+ },
93
+ {
94
+ name: "$Context.EndUserFirstName",
95
+ type: "Text",
96
+ value: demoTraveler.firstName,
97
+ },
98
+ {
99
+ name: "$Context.EndUserLastName",
100
+ type: "Text",
101
+ value: demoTraveler.lastName,
102
+ },
103
+ ],
104
+ };
105
+
106
+ const res = await fetch(
107
+ `/sf-agent/einstein/ai-agent/v1/agents/${agentId}/sessions`,
108
+ {
109
+ method: "POST",
110
+ headers: {
111
+ Authorization: `Bearer ${accessToken}`,
112
+ "Content-Type": "application/json",
113
+ },
114
+ body: JSON.stringify(payload),
115
+ signal,
116
+ }
117
+ );
118
+
119
+ if (!res.ok) {
120
+ const text = await res.text();
121
+ throw new Error(`Session creation failed (${res.status}): ${text}`);
122
+ }
123
+
124
+ const data = await res.json();
125
+ return {
126
+ sessionId: data.sessionId,
127
+ messagesHref: data._links?.messages?.href ?? "",
128
+ endHref: data._links?.end?.href ?? "",
129
+ };
130
+ }
131
+
132
+ /* ── Step 3: Send message ─────────────────────────────── */
133
+ const sendMessage = useCallback(async (text: string) => {
134
+ const session = sessionRef.current;
135
+ if (!session) return;
136
+
137
+ const userMsg: AgentMessage = {
138
+ role: "user",
139
+ text,
140
+ id: `user-${Date.now()}`,
141
+ timestamp: new Date().toISOString(),
142
+ };
143
+ setMessages((prev) => [...prev, userMsg]);
144
+ setIsSending(true);
145
+
146
+ try {
147
+ const seqId = sequenceRef.current++;
148
+
149
+ const messagesUrl = session.messagesHref.startsWith("http")
150
+ ? `/sf-agent${new URL(session.messagesHref).pathname}`
151
+ : `/sf-agent${session.messagesHref}`;
152
+
153
+ const res = await fetch(messagesUrl, {
154
+ method: "POST",
155
+ headers: {
156
+ Authorization: `Bearer ${session.accessToken}`,
157
+ "Content-Type": "application/json",
158
+ Accept: "application/json",
159
+ },
160
+ body: JSON.stringify({
161
+ message: { sequenceId: seqId, type: "Text", text },
162
+ }),
163
+ });
164
+
165
+ if (!res.ok) {
166
+ const errText = await res.text();
167
+ throw new Error(`Agent message failed (${res.status}): ${errText}`);
168
+ }
169
+
170
+ const data = await res.json();
171
+
172
+ const agentTexts: string[] = [];
173
+ if (Array.isArray(data.messages)) {
174
+ for (const m of data.messages) {
175
+ if (m.type === "Inform" && m.message) {
176
+ agentTexts.push(m.message);
177
+ } else if (m.type === "Text" && m.message) {
178
+ agentTexts.push(m.message);
179
+ }
180
+ }
181
+ }
182
+
183
+ const responseText =
184
+ agentTexts.length > 0
185
+ ? agentTexts.join("\n\n")
186
+ : data.message ?? "No response from agent.";
187
+
188
+ const agentMsg: AgentMessage = {
189
+ role: "agent",
190
+ text: responseText,
191
+ id: `agent-${Date.now()}`,
192
+ timestamp: new Date().toISOString(),
193
+ };
194
+ setMessages((prev) => [...prev, agentMsg]);
195
+ } catch (err: unknown) {
196
+ const msg = err instanceof Error ? err.message : "Unknown error";
197
+ setError(msg);
198
+ const errMsg: AgentMessage = {
199
+ role: "agent",
200
+ text: `Error: ${msg}`,
201
+ id: `error-${Date.now()}`,
202
+ timestamp: new Date().toISOString(),
203
+ };
204
+ setMessages((prev) => [...prev, errMsg]);
205
+ } finally {
206
+ setIsSending(false);
207
+ }
208
+ }, []);
209
+
210
+ /* ── Step 4: End session ──────────────────────────────── */
211
+ const endSession = useCallback(async () => {
212
+ const session = sessionRef.current;
213
+ if (!session) return;
214
+
215
+ try {
216
+ const endUrl = session.endHref.startsWith("http")
217
+ ? `/sf-agent${new URL(session.endHref).pathname}`
218
+ : `/sf-agent${session.endHref}`;
219
+
220
+ await fetch(endUrl, {
221
+ method: "DELETE",
222
+ headers: { Authorization: `Bearer ${session.accessToken}` },
223
+ });
224
+ } catch {
225
+ /* best-effort cleanup */
226
+ }
227
+
228
+ sessionRef.current = null;
229
+ setIsReady(false);
230
+ }, []);
231
+
232
+ /* ── Init: authenticate + create session on mount ─────── */
233
+ const connect = useCallback(async () => {
234
+ if (sessionRef.current) return;
235
+
236
+ abortRef.current = new AbortController();
237
+ const { signal } = abortRef.current;
238
+
239
+ setIsConnecting(true);
240
+ setError(null);
241
+
242
+ try {
243
+ const accessToken = await authenticate(signal);
244
+ const session = await createSession(accessToken, signal);
245
+
246
+ sessionRef.current = { accessToken, ...session };
247
+ sequenceRef.current = 1;
248
+ setIsReady(true);
249
+ } catch (err: unknown) {
250
+ if ((err as Error).name !== "AbortError") {
251
+ const msg = err instanceof Error ? err.message : "Connection failed";
252
+ setError(msg);
253
+ }
254
+ } finally {
255
+ setIsConnecting(false);
256
+ }
257
+ }, []);
258
+
259
+ /* ── Cleanup on unmount ───────────────────────────────── */
260
+ useEffect(() => {
261
+ return () => {
262
+ abortRef.current?.abort();
263
+ if (sessionRef.current) {
264
+ const session = sessionRef.current;
265
+ const endUrl = session.endHref.startsWith("http")
266
+ ? `/sf-agent${new URL(session.endHref).pathname}`
267
+ : `/sf-agent${session.endHref}`;
268
+
269
+ fetch(endUrl, {
270
+ method: "DELETE",
271
+ headers: { Authorization: `Bearer ${session.accessToken}` },
272
+ }).catch(() => {});
273
+ sessionRef.current = null;
274
+ }
275
+ };
276
+ }, []);
277
+
278
+ return {
279
+ messages,
280
+ isConnecting,
281
+ isReady,
282
+ isSending,
283
+ error,
284
+ connect,
285
+ sendMessage,
286
+ endSession,
287
+ };
288
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schandlergarcia/sf-web-components",
3
- "version": "1.9.82",
3
+ "version": "1.9.84",
4
4
  "description": "Reusable Salesforce web components library with Tailwind CSS v4 and shadcn/ui",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -315,6 +315,41 @@ if (fs.existsSync(dataSourceDir)) {
315
315
  }
316
316
  }
317
317
 
318
+ // Copy useEvaAgent.ts (Agentforce Agent API hook)
319
+ const useEvaAgentSource = path.join(dataSourceDir, 'useEvaAgent.ts');
320
+ const useEvaAgentTarget = path.join(targetHooksDir, 'useEvaAgent.ts');
321
+
322
+ if (fs.existsSync(useEvaAgentSource)) {
323
+ try {
324
+ if (!fs.existsSync(targetHooksDir)) {
325
+ fs.mkdirSync(targetHooksDir, { recursive: true });
326
+ }
327
+ fs.copyFileSync(useEvaAgentSource, useEvaAgentTarget);
328
+ console.log(' ✓ Installed useEvaAgent.ts');
329
+ dataFilesInstalled++;
330
+ } catch (error) {
331
+ console.error(` ✗ Failed to install useEvaAgent.ts: ${error.message}`);
332
+ }
333
+ }
334
+
335
+ // Copy agentApiConfig.ts → src/config/agentApi.ts
336
+ const agentApiConfigSource = path.join(dataSourceDir, 'agentApiConfig.ts');
337
+ const targetConfigDir = path.join(cwd, 'src/config');
338
+ const agentApiConfigTarget = path.join(targetConfigDir, 'agentApi.ts');
339
+
340
+ if (fs.existsSync(agentApiConfigSource)) {
341
+ try {
342
+ if (!fs.existsSync(targetConfigDir)) {
343
+ fs.mkdirSync(targetConfigDir, { recursive: true });
344
+ }
345
+ fs.copyFileSync(agentApiConfigSource, agentApiConfigTarget);
346
+ console.log(' ✓ Installed src/config/agentApi.ts');
347
+ dataFilesInstalled++;
348
+ } catch (error) {
349
+ console.error(` ✗ Failed to install agentApiConfig.ts: ${error.message}`);
350
+ }
351
+ }
352
+
318
353
  // Copy engine_logo.png
319
354
  const assetsSourceDir = path.join(packageRoot, 'assets/images');
320
355
  const targetAssetsDir = path.join(cwd, 'src/assets/images');
@@ -771,35 +771,36 @@ fi
771
771
  if [ -n "$SFDX_DEFAULT" ]; then
772
772
  metadata_cleaned=0
773
773
 
774
- # Remove all Apex classes (.cls and .cls-meta.xml)
774
+ # Remove Apex classes (.cls and .cls-meta.xml), preserving permanent classes
775
+ KEEP_CLASSES=("TCC_TravelMetricsController")
775
776
  CLASSES_DIR="$SFDX_DEFAULT/classes"
776
777
  if [ -d "$CLASSES_DIR" ]; then
777
- cls_count=$(find "$CLASSES_DIR" -maxdepth 1 \( -name "*.cls" -o -name "*.cls-meta.xml" \) 2>/dev/null | wc -l | tr -d ' ')
778
- if [ "$cls_count" -gt 0 ]; then
779
- [ "$metadata_cleaned" -eq 0 ] && echo "→ Cleaning Salesforce metadata…"
780
- rm -f "$CLASSES_DIR"/*.cls "$CLASSES_DIR"/*.cls-meta.xml
781
- echo " ✓ Removed $cls_count Apex class files"
778
+ cls_removed=0
779
+ for f in "$CLASSES_DIR"/*.cls "$CLASSES_DIR"/*.cls-meta.xml; do
780
+ [ -f "$f" ] || continue
781
+ base="$(basename "$f")"
782
+ skip=false
783
+ for keep in "${KEEP_CLASSES[@]}"; do
784
+ if [[ "$base" == "$keep".cls || "$base" == "$keep".cls-meta.xml ]]; then
785
+ skip=true
786
+ break
787
+ fi
788
+ done
789
+ if [ "$skip" = false ]; then
790
+ [ "$metadata_cleaned" -eq 0 ] && [ "$cls_removed" -eq 0 ] && echo "→ Cleaning Salesforce metadata…"
791
+ rm "$f"
792
+ cls_removed=$((cls_removed + 1))
793
+ fi
794
+ done
795
+ if [ "$cls_removed" -gt 0 ]; then
796
+ echo " ✓ Removed $cls_removed Apex class files (kept ${KEEP_CLASSES[*]})"
782
797
  metadata_cleaned=1
783
798
  fi
784
799
  fi
785
800
 
786
- # Remove custom fields created during the demo
801
+ # Remove platform event directories (but NOT custom fields those persist across resets)
787
802
  OBJECTS_DIR="$SFDX_DEFAULT/objects"
788
803
  if [ -d "$OBJECTS_DIR" ]; then
789
- for fields_dir in "$OBJECTS_DIR"/*/fields; do
790
- if [ -d "$fields_dir" ]; then
791
- for f in "$fields_dir"/*.field-meta.xml; do
792
- if [ -f "$f" ]; then
793
- [ "$metadata_cleaned" -eq 0 ] && echo "→ Cleaning Salesforce metadata…"
794
- rm "$f"
795
- echo " ✓ Removed $(basename "$f")"
796
- metadata_cleaned=1
797
- fi
798
- done
799
- fi
800
- done
801
-
802
- # Remove platform event directories
803
804
  for evt_dir in "$OBJECTS_DIR"/*__e; do
804
805
  if [ -d "$evt_dir" ]; then
805
806
  [ "$metadata_cleaned" -eq 0 ] && echo "→ Cleaning Salesforce metadata…"
@@ -827,7 +828,6 @@ echo "║ • Component library + theme providers ║"
827
828
  echo "║ ║"
828
829
  echo "║ Cleaned: ║"
829
830
  echo "║ • Apex classes (force-app classes/) ║"
830
- echo "║ • Custom fields (force-app objects/) ║"
831
831
  echo "║ • Platform events (force-app *__e/) ║"
832
832
  echo "║ ║"
833
833
  echo "║ Layout: ║"
@@ -34,6 +34,24 @@ export default defineConfig(({ mode }) => {
34
34
  : []),
35
35
  ] as import('vite').PluginOption[],
36
36
 
37
+ // Proxy Agentforce Agent API calls through the dev server to avoid CORS.
38
+ // /sf-oauth/* → Salesforce org (for OAuth client-credentials token)
39
+ // /sf-agent/* → Agent API base (for session + message calls)
40
+ server: {
41
+ proxy: {
42
+ '/sf-oauth': {
43
+ target: 'https://tdx26-keynote-org-1com.my.salesforce.com',
44
+ changeOrigin: true,
45
+ rewrite: (p: string) => p.replace(/^\/sf-oauth/, ''),
46
+ },
47
+ '/sf-agent': {
48
+ target: 'https://api.salesforce.com',
49
+ changeOrigin: true,
50
+ rewrite: (p: string) => p.replace(/^\/sf-agent/, ''),
51
+ },
52
+ },
53
+ },
54
+
37
55
  // Build configuration for MPA
38
56
  build: {
39
57
  outDir: resolve(__dirname, 'dist'),