feat: support suspending ui (#173)

Co-authored-by: Shubham Naik <shub@memgpt.ai>
This commit is contained in:
Shubham Naik
2025-12-10 16:14:34 -08:00
committed by GitHub
parent 6db2fcfc05
commit 410b1ffb97
3 changed files with 53 additions and 4 deletions

View File

@@ -65,6 +65,7 @@ import { generatePlanFilePath } from "./helpers/planName";
import { safeJsonParseOr } from "./helpers/safeJsonParse";
import { type ApprovalRequest, drainStreamWithResume } from "./helpers/stream";
import { getRandomThinkingMessage } from "./helpers/thinkingMessages";
import { useSuspend } from "./hooks/useSuspend/useSuspend.ts";
import { useTerminalWidth } from "./hooks/useTerminalWidth";
const CLEAR_SCREEN_AND_HOME = "\u001B[2J\u001B[H";
@@ -240,6 +241,8 @@ export default function App({
const [agentId, setAgentId] = useState(initialAgentId);
const [agentState, setAgentState] = useState(initialAgentState);
const resumeKey = useSuspend();
// Sync with prop changes (e.g., when parent updates from "loading" to actual ID)
useEffect(() => {
if (initialAgentId !== agentId) {
@@ -2899,7 +2902,7 @@ Plan file path: ${planFilePath}`;
]);
return (
<Box flexDirection="column" gap={1}>
<Box key={resumeKey} flexDirection="column" gap={1}>
<Static
key={staticRenderEpoch}
items={staticItems}

View File

@@ -1,8 +1,7 @@
// Import useInput from vendored Ink for bracketed paste support
import { Box, Text, useInput } from "ink";
import SpinnerLib from "ink-spinner";
import type { ComponentType } from "react";
import { useEffect, useRef, useState } from "react";
import { type ComponentType, useEffect, useRef, useState } from "react";
import { LETTA_CLOUD_API_URL } from "../../auth/oauth";
import type { PermissionMode } from "../../permissions/mode";
import { permissionMode } from "../../permissions/mode";
@@ -155,9 +154,10 @@ export function Input({
}
});
// Handle CTRL-C for double-ctrl-c-to-exit
useInput((input, key) => {
if (!visible) return;
// Handle CTRL-C for double-ctrl-c-to-exit
if (input === "c" && key.ctrl) {
if (ctrlCPressed) {
// Second CTRL-C - call onExit callback which handles stats and exit

View File

@@ -0,0 +1,46 @@
import { useInput, useStdin } from "ink";
import { useCallback, useEffect, useState } from "react";
export function useSuspend() {
const { stdin, isRawModeSupported } = useStdin();
// Use a state variable to force a re-render when needed
const [resumeKey, setResumeKey] = useState(0);
const forceUpdate = useCallback(() => {
setResumeKey((prev) => prev + 1);
}, []);
useInput((input, key) => {
// Handle CTRL-Z for suspend
if (key.ctrl && input === "z") {
if (stdin && isRawModeSupported) {
stdin.setRawMode(false);
}
process.kill(process.pid, "SIGTSTP");
return;
}
});
// Handle the SIGCONT (fg command) resume
useEffect(() => {
const handleResume = () => {
if (stdin && isRawModeSupported && stdin.setRawMode) {
stdin.setRawMode(true);
}
// clear the console
process.stdout.write("\x1B[H\x1B[2J");
forceUpdate();
};
process.on("SIGCONT", handleResume);
return () => {
process.off("SIGCONT", handleResume);
};
}, [stdin, isRawModeSupported, forceUpdate]);
return resumeKey;
}