feat: add release notes display system (#573)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-01-16 20:30:47 -08:00
committed by GitHub
parent 54e9a4038b
commit 2d6d3baa5e
5 changed files with 139 additions and 6 deletions

View File

@@ -161,8 +161,10 @@ export async function getResumeData(
// Fetch the last in-context message directly by ID
// (We already checked inContextMessageIds.length > 0 above)
const lastInContextId =
inContextMessageIds[inContextMessageIds.length - 1]!;
const lastInContextId = inContextMessageIds.at(-1);
if (!lastInContextId) {
throw new Error("Expected at least one in-context message");
}
const retrievedMessages = await client.messages.retrieve(lastInContextId);
// Fetch message history separately for backfill (desc then reverse for last N chronological)
@@ -243,8 +245,10 @@ export async function getResumeData(
// Fetch the last in-context message directly by ID
// (We already checked inContextMessageIds.length > 0 above)
const lastInContextId =
inContextMessageIds[inContextMessageIds.length - 1]!;
const lastInContextId = inContextMessageIds.at(-1);
if (!lastInContextId) {
throw new Error("Expected at least one in-context message");
}
const retrievedMessages = await client.messages.retrieve(lastInContextId);
// Fetch message history separately for backfill (desc then reverse for last N chronological)

View File

@@ -537,6 +537,7 @@ export default function App({
resumedExistingConversation = false,
tokenStreaming = false,
agentProvenance = null,
releaseNotes = null,
}: {
agentId: string;
agentState?: AgentState | null;
@@ -554,6 +555,7 @@ export default function App({
resumedExistingConversation?: boolean; // True if we explicitly resumed via --resume
tokenStreaming?: boolean;
agentProvenance?: AgentProvenance | null;
releaseNotes?: string | null; // Markdown release notes to display above header
}) {
// Warm the model-access cache in the background so /model is fast on first open.
useEffect(() => {
@@ -1434,7 +1436,18 @@ export default function App({
"→ **/remember** teach your agent",
];
const statusLines = [headerMessage, ...commandHints];
// Build status lines with optional release notes above header
const statusLines: string[] = [];
// Add release notes first (above everything) - same styling as rest of status block
if (releaseNotes) {
statusLines.push(releaseNotes);
statusLines.push(""); // blank line separator
}
statusLines.push(headerMessage);
statusLines.push(...commandHints);
buffersRef.current.byId.set(statusId, {
kind: "status",
id: statusId,
@@ -1455,6 +1468,7 @@ export default function App({
agentState,
agentProvenance,
resumedExistingConversation,
releaseNotes,
]);
// Fetch llmConfig when agent is ready
@@ -7184,7 +7198,17 @@ Plan file path: ${planFilePath}`;
"→ **/remember** teach your agent",
];
const statusLines = [headerMessage, ...commandHints];
// Build status lines with optional release notes above header
const statusLines: string[] = [];
// Add release notes first (above everything) - same styling as rest of status block
if (releaseNotes) {
statusLines.push(releaseNotes);
statusLines.push(""); // blank line separator
}
statusLines.push(headerMessage);
statusLines.push(...commandHints);
buffersRef.current.byId.set(statusId, {
kind: "status",
@@ -7205,6 +7229,7 @@ Plan file path: ${planFilePath}`;
agentProvenance,
agentState,
refreshDerived,
releaseNotes,
]);
return (

View File

@@ -916,6 +916,9 @@ async function main(): Promise<void> {
// Track when user explicitly requested new agent from selector (not via --new flag)
const [userRequestedNewAgent, setUserRequestedNewAgent] = useState(false);
// Release notes to display (checked once on mount)
const [releaseNotes, setReleaseNotes] = useState<string | null>(null);
// Auto-install Shift+Enter keybinding for VS Code/Cursor/Windsurf (silent, no prompt)
useEffect(() => {
async function autoInstallKeybinding() {
@@ -989,6 +992,16 @@ async function main(): Promise<void> {
autoInstallWezTermFix();
}, []);
// Check for release notes to display (runs once on mount)
useEffect(() => {
async function checkNotes() {
const { checkReleaseNotes } = await import("./release-notes");
const notes = await checkReleaseNotes();
setReleaseNotes(notes);
}
checkNotes();
}, []);
// Initialize on mount - check if we should show global agent selector
useEffect(() => {
async function checkAndStart() {
@@ -1764,6 +1777,7 @@ async function main(): Promise<void> {
resumedExistingConversation,
tokenStreaming: settings.tokenStreaming,
agentProvenance,
releaseNotes,
});
}
@@ -1779,6 +1793,7 @@ async function main(): Promise<void> {
resumedExistingConversation,
tokenStreaming: settings.tokenStreaming,
agentProvenance,
releaseNotes,
});
}

87
src/release-notes.ts Normal file
View File

@@ -0,0 +1,87 @@
/**
* Release notes displayed to users once per version when they upgrade Letta Code.
* Notes appear above the "Starting new conversation with..." line in the transcript.
*
* To add release notes for a new version:
* 1. Add an entry keyed by the base version (e.g., "0.13.0", not "0.13.0-next.5")
* 2. Use markdown formatting (rendered with MarkdownDisplay)
* 3. Keep notes concise - 2-4 bullet points max
*/
import { settingsManager } from "./settings-manager";
import { getVersion } from "./version";
// Map of base version → markdown string
// Notes are looked up by base version (pre-release suffix stripped)
export const releaseNotes: Record<string, string> = {
// Add release notes for new versions here.
// Keep concise - 3-4 bullet points max.
// Use → for bullets to match the command hints below.
"0.13.0": `🎁 **Letta Code 0.13.0: Introducing Conversations!**
→ Letta Code now starts a new conversation on each startup (memory is shared across all conversations)
→ Use **/resume** to switch conversations, or run **letta --continue** to continue an existing conversation
→ Read more: https://docs.letta.com/letta-code/changelog#0130`,
};
/**
* Get release notes for a specific base version (or null if none exist).
*/
export function getReleaseNotes(baseVersion: string): string | null {
return releaseNotes[baseVersion] ?? null;
}
/**
* Strip pre-release suffix from version string.
* "0.13.0-next.5" → "0.13.0"
*/
function getBaseVersion(version: string): string {
return version.split("-")[0] ?? version;
}
/**
* Check if there are release notes to display for the current version.
* Returns the notes markdown string if:
* - Notes exist for the current base version
* - User hasn't seen them yet (tracked in settings)
*
* Also updates settings to mark notes as seen.
*
* Debug: Set LETTA_SHOW_RELEASE_NOTES=1 to force display.
*/
export async function checkReleaseNotes(): Promise<string | null> {
// Skip for subagents (background processes)
if (process.env.LETTA_CODE_AGENT_ROLE === "subagent") {
return null;
}
const currentVersion = getVersion();
const baseVersion = getBaseVersion(currentVersion);
// Debug flag to force show (still respects whether notes exist)
if (process.env.LETTA_SHOW_RELEASE_NOTES === "1") {
return getReleaseNotes(baseVersion);
}
const settings = settingsManager.getSettings();
// Compare BASE versions so 0.13.0-next.5 → 0.13.0-next.6 doesn't re-show notes
// This ensures users on `next` channel only see notes once per major version
const lastSeenBase = settings.lastSeenReleaseNotesVersion
? getBaseVersion(settings.lastSeenReleaseNotesVersion)
: undefined;
if (lastSeenBase === baseVersion) {
return null;
}
// Look up notes by base version (so 0.13.0-next.5 finds 0.13.0 notes)
const notes = getReleaseNotes(baseVersion);
if (notes) {
// Store BASE version so future pre-releases of same version don't re-show
await settingsManager.updateSettings({
lastSeenReleaseNotesVersion: baseVersion,
});
}
return notes;
}

View File

@@ -40,6 +40,8 @@ export interface Settings {
refreshToken?: string; // DEPRECATED: kept for migration, now stored in secrets
tokenExpiresAt?: number; // Unix timestamp in milliseconds
deviceId?: string;
// Release notes tracking
lastSeenReleaseNotesVersion?: string; // Base version of last seen release notes (e.g., "0.13.0")
// Pending OAuth state (for PKCE flow)
oauthState?: {
state: string;