From d4682421b644d5190b72eec592255698d2add84d Mon Sep 17 00:00:00 2001 From: Shubham Naik Date: Wed, 17 Dec 2025 13:41:22 -0800 Subject: [PATCH] Add feedback UI (#264) Co-authored-by: Shubham Naik --- src/cli/App.tsx | 91 +++++++++++++++++++++++++++ src/cli/commands/registry.ts | 7 +++ src/cli/components/FeedbackDialog.tsx | 74 ++++++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 src/cli/components/FeedbackDialog.tsx diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 0a85360..4380661 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -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(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}`; )} + {/* Feedback Dialog - conditionally mounted as overlay */} + {activeOverlay === "feedback" && ( + + )} + {/* Plan Mode Dialog - for ExitPlanMode tool */} {currentApproval?.toolName === "ExitPlanMode" && ( <> diff --git a/src/cli/commands/registry.ts b/src/cli/commands/registry.ts index b4625bd..a503d11 100644 --- a/src/cli/commands/registry.ts +++ b/src/cli/commands/registry.ts @@ -166,6 +166,13 @@ export const commands: Record = { 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..."; + }, + }, }; /** diff --git a/src/cli/components/FeedbackDialog.tsx b/src/cli/components/FeedbackDialog.tsx new file mode 100644 index 0000000..da97984 --- /dev/null +++ b/src/cli/components/FeedbackDialog.tsx @@ -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 ( + + + + Send Feedback to Letta Team + + + + + + Share your thoughts, report issues, or suggest improvements. + + + + + + Enter your feedback: + + + > + + + + + {error && ( + + {error} + + )} + + + Press Enter to submit • Esc to cancel + + + ); +}