@@ -48,6 +48,7 @@ import { AssistantMessage } from "./components/AssistantMessageRich";
|
||||
import { CommandMessage } from "./components/CommandMessage";
|
||||
import { EnterPlanModeDialog } from "./components/EnterPlanModeDialog";
|
||||
import { ErrorMessage } from "./components/ErrorMessageRich";
|
||||
import { FeedbackDialog } from "./components/FeedbackDialog";
|
||||
import { Input } from "./components/InputRich";
|
||||
import { MessageSearch } from "./components/MessageSearch";
|
||||
import { ModelSelector } from "./components/ModelSelector";
|
||||
@@ -391,6 +392,7 @@ export default function App({
|
||||
| "profile"
|
||||
| "search"
|
||||
| "subagent"
|
||||
| "feedback"
|
||||
| null;
|
||||
const [activeOverlay, setActiveOverlay] = useState<ActiveOverlay>(null);
|
||||
const closeOverlay = useCallback(() => setActiveOverlay(null), []);
|
||||
@@ -2536,6 +2538,12 @@ ${recentCommits}
|
||||
return { submitted: true };
|
||||
}
|
||||
|
||||
// Special handling for /feedback command - open feedback dialog
|
||||
if (trimmed === "/feedback") {
|
||||
setActiveOverlay("feedback");
|
||||
return { submitted: true };
|
||||
}
|
||||
|
||||
// Immediately add command to transcript with "running" phase
|
||||
const cmdId = uid("cmd");
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
@@ -3380,6 +3388,81 @@ ${recentCommits}
|
||||
);
|
||||
|
||||
// Handle escape when profile confirmation is pending
|
||||
const handleFeedbackSubmit = useCallback(
|
||||
async (message: string) => {
|
||||
closeOverlay();
|
||||
|
||||
await withCommandLock(async () => {
|
||||
const cmdId = uid("cmd");
|
||||
|
||||
try {
|
||||
// Immediately add command to transcript with "running" phase
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: "/feedback",
|
||||
output: "Sending feedback...",
|
||||
phase: "running",
|
||||
});
|
||||
buffersRef.current.order.push(cmdId);
|
||||
refreshDerived();
|
||||
|
||||
const settings = settingsManager.getSettings();
|
||||
const baseURL =
|
||||
process.env.LETTA_BASE_URL ||
|
||||
settings.env?.LETTA_BASE_URL ||
|
||||
"https://api.letta.com";
|
||||
const apiKey =
|
||||
process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY;
|
||||
|
||||
// Send feedback request manually since it's not in the SDK
|
||||
const response = await fetch(`${baseURL}/v1/metadata/feedback`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"X-Letta-Source": "letta-code",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: message,
|
||||
feature: "letta-code",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(
|
||||
`Failed to send feedback (${response.status}): ${errorText}`,
|
||||
);
|
||||
}
|
||||
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: "/feedback",
|
||||
output:
|
||||
"Thank you for your feedback! Your message has been sent to the Letta team.",
|
||||
phase: "finished",
|
||||
success: true,
|
||||
});
|
||||
refreshDerived();
|
||||
} catch (error) {
|
||||
const errorDetails = formatErrorDetails(error, agentId);
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: "/feedback",
|
||||
output: `Failed to send feedback: ${errorDetails}`,
|
||||
phase: "finished",
|
||||
success: false,
|
||||
});
|
||||
refreshDerived();
|
||||
}
|
||||
});
|
||||
},
|
||||
[agentId, refreshDerived, withCommandLock, closeOverlay],
|
||||
);
|
||||
|
||||
const handleProfileEscapeCancel = useCallback(() => {
|
||||
if (profileConfirmPending) {
|
||||
const { cmdId, name } = profileConfirmPending;
|
||||
@@ -3962,6 +4045,14 @@ Plan file path: ${planFilePath}`;
|
||||
<MessageSearch onClose={closeOverlay} />
|
||||
)}
|
||||
|
||||
{/* Feedback Dialog - conditionally mounted as overlay */}
|
||||
{activeOverlay === "feedback" && (
|
||||
<FeedbackDialog
|
||||
onSubmit={handleFeedbackSubmit}
|
||||
onCancel={closeOverlay}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Plan Mode Dialog - for ExitPlanMode tool */}
|
||||
{currentApproval?.toolName === "ExitPlanMode" && (
|
||||
<>
|
||||
|
||||
@@ -166,6 +166,13 @@ export const commands: Record<string, Command> = {
|
||||
return "Opening subagent manager...";
|
||||
},
|
||||
},
|
||||
"/feedback": {
|
||||
desc: "Send feedback to the Letta team",
|
||||
handler: () => {
|
||||
// Handled specially in App.tsx to send feedback request
|
||||
return "Sending feedback...";
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
74
src/cli/components/FeedbackDialog.tsx
Normal file
74
src/cli/components/FeedbackDialog.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Box, Text, useInput } from "ink";
|
||||
import { useState } from "react";
|
||||
import { colors } from "./colors";
|
||||
import { PasteAwareTextInput } from "./PasteAwareTextInput";
|
||||
|
||||
interface FeedbackDialogProps {
|
||||
onSubmit: (message: string) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function FeedbackDialog({ onSubmit, onCancel }: FeedbackDialogProps) {
|
||||
const [feedbackText, setFeedbackText] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useInput((input, key) => {
|
||||
if (key.escape) {
|
||||
onCancel();
|
||||
}
|
||||
});
|
||||
|
||||
const handleSubmit = (text: string) => {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) {
|
||||
setError("Feedback message cannot be empty");
|
||||
return;
|
||||
}
|
||||
if (trimmed.length > 10000) {
|
||||
setError("Feedback message is too long (max 10,000 characters)");
|
||||
return;
|
||||
}
|
||||
onSubmit(trimmed);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingY={1}>
|
||||
<Box marginBottom={1}>
|
||||
<Text color={colors.approval.header} bold>
|
||||
Send Feedback to Letta Team
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginBottom={1}>
|
||||
<Text dimColor>
|
||||
Share your thoughts, report issues, or suggest improvements.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box marginBottom={1}>
|
||||
<Text>Enter your feedback:</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={colors.approval.header}>> </Text>
|
||||
<PasteAwareTextInput
|
||||
value={feedbackText}
|
||||
onChange={setFeedbackText}
|
||||
onSubmit={handleSubmit}
|
||||
placeholder="Type your feedback here..."
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Box marginBottom={1}>
|
||||
<Text color="red">{error}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
<Text dimColor>Press Enter to submit • Esc to cancel</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user