Skip to content

Commit 98cb12a

Browse files
Merge pull request #255 from salesforcecli/wr/saveTranscript
Wr/save transcript @W-20091927@
2 parents 6669eee + c052964 commit 98cb12a

File tree

3 files changed

+296
-64
lines changed

3 files changed

+296
-64
lines changed

src/commands/agent/preview.ts

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
2121
import { AuthInfo, Connection, Lifecycle, Messages, SfError } from '@salesforce/core';
2222
import React from 'react';
2323
import { render } from 'ink';
24-
import { env } from '@salesforce/kit';
2524
import {
2625
AgentPreview as Preview,
2726
AgentSimulate,
@@ -30,7 +29,7 @@ import {
3029
PublishedAgent,
3130
ScriptAgent,
3231
} from '@salesforce/agents';
33-
import { confirm, input, select } from '@inquirer/prompts';
32+
import { select } from '@inquirer/prompts';
3433
import { AgentPreviewReact } from '../../components/agent-preview-react.js';
3534

3635
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
@@ -176,7 +175,9 @@ export default class AgentPreview extends SfCommand<AgentPreviewResult> {
176175
})
177176
: await Connection.create({ authInfo });
178177

179-
const outputDir = await resolveOutputDir(flags['output-dir'], flags['apex-debug']);
178+
// Only resolve outputDir if explicitly provided via flag
179+
// Otherwise, let user decide when exiting
180+
const outputDir = flags['output-dir'] ? resolve(flags['output-dir']) : undefined;
180181
// Both classes share the same interface for the methods we need
181182
const agentPreview =
182183
selectedAgent.source === AgentSource.PUBLISHED
@@ -192,6 +193,7 @@ export default class AgentPreview extends SfCommand<AgentPreviewResult> {
192193
name: selectedAgent.DeveloperName,
193194
outputDir,
194195
isLocalAgent: selectedAgent.source === AgentSource.SCRIPT,
196+
apexDebug: flags['apex-debug'],
195197
}),
196198
{ exitOnCtrlC: false }
197199
);
@@ -258,30 +260,3 @@ export const validateAgent = (agent: AgentData): boolean => {
258260

259261
export const getClientAppsFromAuth = (authInfo: AuthInfo): string[] =>
260262
Object.keys(authInfo.getFields().clientApps ?? {});
261-
262-
export const resolveOutputDir = async (
263-
outputDir: string | undefined,
264-
apexDebug: boolean | undefined
265-
): Promise<string | undefined> => {
266-
if (!outputDir) {
267-
const response = apexDebug
268-
? true
269-
: await confirm({
270-
message: 'Save transcripts to an output directory?',
271-
default: true,
272-
});
273-
274-
const outputTypes = apexDebug ? 'debug logs and transcripts' : 'transcripts';
275-
if (response) {
276-
const getDir = await input({
277-
message: `Enter the output directory for ${outputTypes}`,
278-
default: env.getString('SF_AGENT_PREVIEW_OUTPUT_DIR', join('temp', 'agent-preview')),
279-
required: true,
280-
});
281-
282-
return resolve(getDir);
283-
}
284-
} else {
285-
return resolve(outputDir);
286-
}
287-
};

src/components/agent-preview-react.tsx

Lines changed: 149 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
import path from 'node:path';
1818
import fs from 'node:fs';
1919
import * as process from 'node:process';
20+
import { resolve } from 'node:path';
2021
import React from 'react';
2122
import { Box, Text, useInput } from 'ink';
2223
import TextInput from 'ink-text-input';
2324
import { Connection, SfError, Lifecycle } from '@salesforce/core';
2425
import { AgentPreviewBase, AgentPreviewSendResponse, writeDebugLog } from '@salesforce/agents';
25-
import { sleep } from '@salesforce/kit';
26+
import { sleep, env } from '@salesforce/kit';
2627

2728
// Component to show a simple typing animation
2829
function Typing(): React.ReactNode {
@@ -48,7 +49,7 @@ function Typing(): React.ReactNode {
4849
);
4950
}
5051

51-
const saveTranscriptsToFile = (
52+
export const saveTranscriptsToFile = (
5253
outputDir: string,
5354
messages: Array<{ timestamp: Date; role: string; content: string }>,
5455
responses: AgentPreviewSendResponse[]
@@ -76,28 +77,65 @@ export function AgentPreviewReact(props: {
7677
readonly name: string;
7778
readonly outputDir: string | undefined;
7879
readonly isLocalAgent: boolean;
80+
readonly apexDebug: boolean | undefined;
7981
}): React.ReactNode {
8082
const [messages, setMessages] = React.useState<Array<{ timestamp: Date; role: string; content: string }>>([]);
8183
const [header, setHeader] = React.useState('Starting session...');
8284
const [sessionId, setSessionId] = React.useState('');
8385
const [query, setQuery] = React.useState('');
8486
const [isTyping, setIsTyping] = React.useState(true);
8587
const [sessionEnded, setSessionEnded] = React.useState(false);
88+
const [exitRequested, setExitRequested] = React.useState(false);
89+
const [showSavePrompt, setShowSavePrompt] = React.useState(false);
90+
const [showDirInput, setShowDirInput] = React.useState(false);
91+
const [saveDir, setSaveDir] = React.useState('');
92+
const [saveConfirmed, setSaveConfirmed] = React.useState(false);
8693
// @ts-expect-error: Complains if this is not defined but it's not used
8794
// eslint-disable-next-line @typescript-eslint/no-unused-vars
8895
const [timestamp, setTimestamp] = React.useState(new Date().getTime());
8996
const [tempDir, setTempDir] = React.useState('');
9097
const [responses, setResponses] = React.useState<AgentPreviewSendResponse[]>([]);
9198
const [apexDebugLogs, setApexDebugLogs] = React.useState<string[]>([]);
9299

93-
const { connection, agent, name, outputDir, isLocalAgent } = props;
100+
const { connection, agent, name, outputDir, isLocalAgent, apexDebug } = props;
94101

95102
useInput((input, key) => {
96-
if (key.escape) {
103+
// If user is in directory input and presses ESC, cancel and exit without saving
104+
if (showDirInput && (key.escape || (key.ctrl && input === 'c'))) {
97105
setSessionEnded(true);
106+
return;
98107
}
99-
if (key.ctrl && input === 'c') {
100-
setSessionEnded(true);
108+
109+
// Only handle exit if we're not already in save prompt flow
110+
if (!exitRequested && !showSavePrompt && !showDirInput) {
111+
if (key.escape || (key.ctrl && input === 'c')) {
112+
setExitRequested(true);
113+
setShowSavePrompt(true);
114+
}
115+
return;
116+
}
117+
118+
// Handle save prompt navigation
119+
if (showSavePrompt && !showDirInput) {
120+
if (input.toLowerCase() === 'y' || input.toLowerCase() === 'n') {
121+
if (input.toLowerCase() === 'y') {
122+
// If outputDir was provided via flag, use it directly
123+
if (outputDir) {
124+
setSaveDir(outputDir);
125+
setSaveConfirmed(true);
126+
setShowSavePrompt(false);
127+
} else {
128+
// Otherwise, prompt for directory
129+
setShowSavePrompt(false);
130+
setShowDirInput(true);
131+
const defaultDir = env.getString('SF_AGENT_PREVIEW_OUTPUT_DIR', path.join('temp', 'agent-preview'));
132+
setSaveDir(defaultDir);
133+
}
134+
} else {
135+
// User said no, exit without saving
136+
setSessionEnded(true);
137+
}
138+
}
101139
}
102140
});
103141

@@ -115,7 +153,7 @@ export function AgentPreviewReact(props: {
115153
}
116154
};
117155
void endSession();
118-
}, [sessionEnded]);
156+
}, [sessionEnded, sessionId, agent]);
119157

120158
React.useEffect(() => {
121159
// Set up event listeners for agent compilation and simulation events
@@ -148,16 +186,13 @@ export function AgentPreviewReact(props: {
148186
setHeader(`New session started with "${props.name}" (${session.sessionId})`);
149187
await sleep(500); // Add a short delay to make it feel more natural
150188
setIsTyping(false);
151-
if (outputDir) {
152-
const dateForDir = new Date().toISOString().replace(/:/g, '-').split('.')[0];
153-
setTempDir(path.join(outputDir, `${dateForDir}--${session.sessionId}`));
154-
}
155189
// Add disclaimer for local agents before the agent's first message
156190
const initialMessages = [];
157191
if (isLocalAgent) {
158192
initialMessages.push({
159193
role: 'system',
160-
content: 'Agent preview does not provide strict adherence to connection endpoint configuration and escalation is not supported.\n\nTo test escalation, publish your agent then use the desired connection endpoint (e.g., Web Page, SMS, etc).',
194+
content:
195+
'Agent preview does not provide strict adherence to connection endpoint configuration and escalation is not supported.\n\nTo test escalation, publish your agent then use the desired connection endpoint (e.g., Web Page, SMS, etc).',
161196
timestamp: new Date(),
162197
});
163198
}
@@ -176,9 +211,50 @@ export function AgentPreviewReact(props: {
176211
}, [agent, name, outputDir, props.name, isLocalAgent]);
177212

178213
React.useEffect(() => {
179-
saveTranscriptsToFile(tempDir, messages, responses);
214+
// Save to tempDir if it was set (during session)
215+
if (tempDir) {
216+
saveTranscriptsToFile(tempDir, messages, responses);
217+
}
180218
}, [tempDir, messages, responses]);
181219

220+
// Handle saving when user confirms save on exit
221+
React.useEffect(() => {
222+
const saveAndExit = async (): Promise<void> => {
223+
if (saveConfirmed && saveDir) {
224+
const finalDir = resolve(saveDir);
225+
fs.mkdirSync(finalDir, { recursive: true });
226+
227+
// Create a timestamped subdirectory for this session
228+
const dateForDir = new Date().toISOString().replace(/:/g, '-').split('.')[0];
229+
const sessionDir = path.join(finalDir, `${dateForDir}--${sessionId || 'session'}`);
230+
fs.mkdirSync(sessionDir, { recursive: true });
231+
232+
saveTranscriptsToFile(sessionDir, messages, responses);
233+
234+
// Write apex debug logs if any
235+
if (apexDebug) {
236+
for (const response of responses) {
237+
if (response.apexDebugLog) {
238+
// eslint-disable-next-line no-await-in-loop
239+
await writeDebugLog(connection, response.apexDebugLog, sessionDir);
240+
const logId = response.apexDebugLog.Id;
241+
if (logId) {
242+
setApexDebugLogs((prev) => [...prev, path.join(sessionDir, `${logId}.log`)]);
243+
}
244+
}
245+
}
246+
}
247+
248+
// Update tempDir so the save message shows the correct path
249+
setTempDir(sessionDir);
250+
251+
// Mark session as ended to trigger exit
252+
setSessionEnded(true);
253+
}
254+
};
255+
void saveAndExit();
256+
}, [saveConfirmed, saveDir, messages, responses, sessionId, apexDebug, connection]);
257+
182258
return (
183259
<Box flexDirection="column">
184260
<Box
@@ -218,11 +294,7 @@ export function AgentPreviewReact(props: {
218294
<Text>{role === 'user' ? 'You' : role}</Text>
219295
<Text color="grey">{ts.toLocaleString()}</Text>
220296
</Box>
221-
<Box
222-
borderStyle="round"
223-
paddingLeft={1}
224-
paddingRight={1}
225-
>
297+
<Box borderStyle="round" paddingLeft={1} paddingRight={1}>
226298
<Text>{content}</Text>
227299
</Box>
228300
</>
@@ -251,13 +323,64 @@ export function AgentPreviewReact(props: {
251323
<Text dimColor>{'─'.repeat(process.stdout.columns - 2)}</Text>
252324
</Box>
253325

254-
{sessionEnded ? null : (
326+
{showSavePrompt && !showDirInput ? (
327+
<Box
328+
flexDirection="column"
329+
width={process.stdout.columns}
330+
borderStyle="round"
331+
borderColor="yellow"
332+
marginTop={1}
333+
marginBottom={1}
334+
paddingLeft={1}
335+
paddingRight={1}
336+
>
337+
<Text bold>Save chat history before exiting? (y/n)</Text>
338+
{outputDir ? (
339+
<Text dimColor>Will save to: {outputDir}</Text>
340+
) : (
341+
<Text dimColor>Press &#39;y&#39; to save, &#39;n&#39; to exit without saving</Text>
342+
)}
343+
</Box>
344+
) : null}
345+
346+
{showDirInput ? (
347+
<Box
348+
flexDirection="column"
349+
width={process.stdout.columns}
350+
borderStyle="round"
351+
borderColor="yellow"
352+
marginTop={1}
353+
marginBottom={1}
354+
paddingLeft={1}
355+
paddingRight={1}
356+
>
357+
<Text bold>Enter output directory for {apexDebug ? 'debug logs and transcripts' : 'transcripts'}:</Text>
358+
<Box marginTop={1}>
359+
<Text>&gt; </Text>
360+
<TextInput
361+
showCursor
362+
value={saveDir}
363+
placeholder="Press Enter to confirm"
364+
onChange={setSaveDir}
365+
onSubmit={(dir) => {
366+
if (dir) {
367+
setSaveDir(dir);
368+
setSaveConfirmed(true);
369+
setShowDirInput(false);
370+
}
371+
}}
372+
/>
373+
</Box>
374+
</Box>
375+
) : null}
376+
377+
{!sessionEnded && !exitRequested && !showSavePrompt && !showDirInput ? (
255378
<Box marginBottom={1}>
256379
<Text>&gt; </Text>
257380
<TextInput
258381
showCursor
259382
value={query}
260-
placeholder="Start typing (press ESC to exit)"
383+
placeholder="Start typing (press ESC or Ctrl+C to exit)"
261384
onChange={setQuery}
262385
// eslint-disable-next-line @typescript-eslint/no-misused-promises
263386
onSubmit={async (content) => {
@@ -280,15 +403,7 @@ export function AgentPreviewReact(props: {
280403
// Add the agent's response to the chat
281404
setMessages((prev) => [...prev, { role: name, content: message, timestamp: new Date() }]);
282405

283-
// If there is an apex debug log entry, get the log and write it to the output dir
284-
if (response.apexDebugLog && tempDir) {
285-
// Write the apex debug to the output dir
286-
await writeDebugLog(connection, response.apexDebugLog, tempDir);
287-
const logId = response.apexDebugLog.Id;
288-
if (logId) {
289-
setApexDebugLogs((prev) => [...prev, path.join(tempDir, `${logId}.log`)]);
290-
}
291-
}
406+
// Apex debug logs will be saved when user exits and chooses to save
292407
} catch (e) {
293408
const sfError = SfError.wrap(e);
294409
setIsTyping(false);
@@ -299,9 +414,9 @@ export function AgentPreviewReact(props: {
299414
}}
300415
/>
301416
</Box>
302-
)}
417+
) : null}
303418

304-
{sessionEnded ? (
419+
{sessionEnded && !showSavePrompt && !showDirInput ? (
305420
<Box
306421
flexDirection="column"
307422
width={process.stdout.columns}
@@ -312,9 +427,9 @@ export function AgentPreviewReact(props: {
312427
paddingRight={1}
313428
>
314429
<Text bold>Session Ended</Text>
315-
{outputDir ? <Text>Conversation log: {tempDir}/transcript.json</Text> : null}
316-
{outputDir ? <Text>API transactions: {tempDir}/responses.json</Text> : null}
317-
{apexDebugLogs.length > 0 && <Text>Apex Debug Logs: {'\n' + apexDebugLogs.join('\n')}</Text>}
430+
{tempDir ? <Text>Conversation log: {tempDir}/transcript.json</Text> : null}
431+
{tempDir ? <Text>API transactions: {tempDir}/responses.json</Text> : null}
432+
{apexDebugLogs.length > 0 && tempDir && <Text>Apex Debug Logs saved to: {tempDir}</Text>}
318433
</Box>
319434
) : null}
320435
</Box>

0 commit comments

Comments
 (0)