feat: show update notification in TUI and footer (#1204)
This commit is contained in:
@@ -967,6 +967,7 @@ export default function App({
|
||||
showCompactions = false,
|
||||
agentProvenance = null,
|
||||
releaseNotes = null,
|
||||
updateNotification = null,
|
||||
sessionContextReminderEnabled = true,
|
||||
}: {
|
||||
agentId: string;
|
||||
@@ -988,6 +989,7 @@ export default function App({
|
||||
showCompactions?: boolean;
|
||||
agentProvenance?: AgentProvenance | null;
|
||||
releaseNotes?: string | null; // Markdown release notes to display above header
|
||||
updateNotification?: string | null; // Latest version when a significant auto-update was applied
|
||||
sessionContextReminderEnabled?: boolean;
|
||||
}) {
|
||||
// Warm the model-access cache in the background so /model is fast on first open.
|
||||
@@ -1707,6 +1709,31 @@ export default function App({
|
||||
// Static items (things that are done rendering and can be frozen)
|
||||
const [staticItems, setStaticItems] = useState<StaticItem[]>([]);
|
||||
|
||||
// Show in-transcript notification when auto-update applied a significant new version
|
||||
const [footerUpdateText, setFooterUpdateText] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (!updateNotification) return;
|
||||
setStaticItems((prev) => {
|
||||
if (prev.some((item) => item.id === "update-notification")) return prev;
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
kind: "status" as const,
|
||||
id: "update-notification",
|
||||
lines: [
|
||||
`A new version of Letta Code is available (**${updateNotification}**). Restart to update!`,
|
||||
],
|
||||
},
|
||||
];
|
||||
});
|
||||
// Also show briefly in the footer placeholder area
|
||||
setFooterUpdateText(
|
||||
`New version available (${updateNotification}). Restart to update!`,
|
||||
);
|
||||
const timer = setTimeout(() => setFooterUpdateText(null), 8000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [updateNotification]);
|
||||
|
||||
// Track committed ids to avoid duplicates
|
||||
const emittedIdsRef = useRef<Set<string>>(new Set());
|
||||
|
||||
@@ -12743,6 +12770,7 @@ If using apply_patch, use this exact relative patch path: ${applyPatchRelativePa
|
||||
statusLineRight={statusLine.rightText || undefined}
|
||||
statusLinePadding={statusLine.padding || 0}
|
||||
statusLinePrompt={statusLine.prompt}
|
||||
footerNotification={footerUpdateText}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -234,6 +234,7 @@ const InputFooter = memo(function InputFooter({
|
||||
statusLineText,
|
||||
statusLineRight,
|
||||
statusLinePadding,
|
||||
footerNotification,
|
||||
}: {
|
||||
ctrlCPressed: boolean;
|
||||
escapePressed: boolean;
|
||||
@@ -252,6 +253,7 @@ const InputFooter = memo(function InputFooter({
|
||||
statusLineText?: string;
|
||||
statusLineRight?: string;
|
||||
statusLinePadding?: number;
|
||||
footerNotification?: string | null;
|
||||
}) {
|
||||
const hideFooterContent = hideFooter;
|
||||
const maxAgentChars = Math.max(10, Math.floor(rightColumnWidth * 0.45));
|
||||
@@ -328,6 +330,10 @@ const InputFooter = memo(function InputFooter({
|
||||
(shift+tab to {showExitHint ? "exit" : "cycle"})
|
||||
</Text>
|
||||
</Text>
|
||||
) : footerNotification ? (
|
||||
<Text color={colors.status.processingShimmer}>
|
||||
{footerNotification}
|
||||
</Text>
|
||||
) : (
|
||||
<Text dimColor>Press / for commands</Text>
|
||||
)}
|
||||
@@ -621,6 +627,7 @@ export function Input({
|
||||
statusLinePadding = 0,
|
||||
statusLinePrompt,
|
||||
onCycleReasoningEffort,
|
||||
footerNotification,
|
||||
}: {
|
||||
visible?: boolean;
|
||||
streaming: boolean;
|
||||
@@ -662,6 +669,7 @@ export function Input({
|
||||
statusLinePadding?: number;
|
||||
statusLinePrompt?: string;
|
||||
onCycleReasoningEffort?: () => void;
|
||||
footerNotification?: string | null;
|
||||
}) {
|
||||
const [value, setValue] = useState("");
|
||||
const [escapePressed, setEscapePressed] = useState(false);
|
||||
@@ -1484,6 +1492,7 @@ export function Input({
|
||||
statusLineText={statusLineText}
|
||||
statusLineRight={statusLineRight}
|
||||
statusLinePadding={statusLinePadding}
|
||||
footerNotification={footerNotification}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
@@ -1530,6 +1539,7 @@ export function Input({
|
||||
statusLineText,
|
||||
statusLineRight,
|
||||
statusLinePadding,
|
||||
footerNotification,
|
||||
promptChar,
|
||||
promptVisualWidth,
|
||||
suppressDividers,
|
||||
|
||||
17
src/index.ts
17
src/index.ts
@@ -365,7 +365,7 @@ async function main(): Promise<void> {
|
||||
|
||||
// Check for updates on startup (non-blocking)
|
||||
const { checkAndAutoUpdate } = await import("./updater/auto-update");
|
||||
startStartupAutoUpdateCheck(checkAndAutoUpdate);
|
||||
const autoUpdatePromise = startStartupAutoUpdateCheck(checkAndAutoUpdate);
|
||||
|
||||
// Clean up old overflow files (non-blocking, 24h retention)
|
||||
const { cleanupOldOverflowFiles } = await import("./tools/impl/overflow");
|
||||
@@ -1032,6 +1032,20 @@ async function main(): Promise<void> {
|
||||
// Release notes to display (checked once on mount)
|
||||
const [releaseNotes, setReleaseNotes] = useState<string | null>(null);
|
||||
|
||||
// Update notification: set when auto-update applied a significant new version
|
||||
const [updateNotification, setUpdateNotification] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
useEffect(() => {
|
||||
autoUpdatePromise
|
||||
.then((result) => {
|
||||
if (result?.latestVersion) {
|
||||
setUpdateNotification(result.latestVersion);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Auto-install Shift+Enter keybinding for VS Code/Cursor/Windsurf (silent, no prompt)
|
||||
useEffect(() => {
|
||||
async function autoInstallKeybinding() {
|
||||
@@ -2067,6 +2081,7 @@ async function main(): Promise<void> {
|
||||
showCompactions: settings.showCompactions,
|
||||
agentProvenance,
|
||||
releaseNotes,
|
||||
updateNotification,
|
||||
sessionContextReminderEnabled: !noSystemInfoReminderFlag,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
export interface UpdateNotification {
|
||||
latestVersion: string;
|
||||
}
|
||||
|
||||
export function startStartupAutoUpdateCheck(
|
||||
checkAndAutoUpdate: () => Promise<{ enotemptyFailed?: boolean } | undefined>,
|
||||
checkAndAutoUpdate: () => Promise<
|
||||
| {
|
||||
enotemptyFailed?: boolean;
|
||||
latestVersion?: string;
|
||||
updateApplied?: boolean;
|
||||
}
|
||||
| undefined
|
||||
>,
|
||||
logError: (
|
||||
message?: unknown,
|
||||
...optionalParams: unknown[]
|
||||
) => void = console.error,
|
||||
): void {
|
||||
checkAndAutoUpdate()
|
||||
): Promise<UpdateNotification | undefined> {
|
||||
return checkAndAutoUpdate()
|
||||
.then((result) => {
|
||||
// Surface ENOTEMPTY failures so users know how to fix
|
||||
if (result?.enotemptyFailed) {
|
||||
@@ -14,8 +25,11 @@ export function startStartupAutoUpdateCheck(
|
||||
"Fix: rm -rf $(npm prefix -g)/lib/node_modules/@letta-ai/letta-code && npm i -g @letta-ai/letta-code\n",
|
||||
);
|
||||
}
|
||||
// Return notification payload for the TUI to consume
|
||||
if (result?.updateApplied && result.latestVersion) {
|
||||
return { latestVersion: result.latestVersion };
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently ignore other update failures (network timeouts, etc.)
|
||||
});
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
@@ -357,6 +357,23 @@ async function performUpdate(): Promise<{
|
||||
export interface AutoUpdateResult {
|
||||
/** Whether an ENOTEMPTY error persisted after cleanup and retry */
|
||||
enotemptyFailed?: boolean;
|
||||
/** Latest version available (set when a significant update was applied) */
|
||||
latestVersion?: string;
|
||||
/** True when the binary was updated and the user should restart */
|
||||
updateApplied?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when `latest` is at least one minor version ahead of `current`.
|
||||
* Used to gate the in-app "restart to update" notification — patch-only bumps
|
||||
* are applied silently without interrupting the user.
|
||||
*/
|
||||
function isSignificantUpdate(current: string, latest: string): boolean {
|
||||
const [cMajor = 0, cMinor = 0] = current.split(".").map(Number);
|
||||
const [lMajor = 0, lMinor = 0] = latest.split(".").map(Number);
|
||||
if (lMajor > cMajor) return true;
|
||||
if (lMajor === cMajor && lMinor > cMinor) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function checkAndAutoUpdate(): Promise<
|
||||
@@ -384,6 +401,13 @@ export async function checkAndAutoUpdate(): Promise<
|
||||
if (updateResult.enotemptyFailed) {
|
||||
return { enotemptyFailed: true };
|
||||
}
|
||||
if (
|
||||
updateResult.success &&
|
||||
result.latestVersion &&
|
||||
isSignificantUpdate(result.currentVersion, result.latestVersion)
|
||||
) {
|
||||
return { updateApplied: true, latestVersion: result.latestVersion };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,8 +451,9 @@ export async function manualUpdate(): Promise<{
|
||||
};
|
||||
}
|
||||
|
||||
const installCmd = buildInstallCommand(detectPackageManager());
|
||||
return {
|
||||
success: false,
|
||||
message: `Update failed: ${updateResult.error}`,
|
||||
message: `Update failed: ${updateResult.error}\n\nTo update manually: ${installCmd}`,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user