feat: add release notes display system (#573)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
15
src/index.ts
15
src/index.ts
@@ -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
87
src/release-notes.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user