feat: add terminal title and progress indicator for approval screens (#499)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-01-08 17:58:07 -08:00
committed by GitHub
parent cdcf311c64
commit 989bcfdea8
12 changed files with 73 additions and 0 deletions

View File

@@ -574,6 +574,14 @@ export default function App({
}
}, [agentId]);
// Set terminal title to "{Agent Name} | Letta Code"
useEffect(() => {
const title = agentState?.name
? `${agentState.name} | Letta Code`
: "Letta Code";
process.stdout.write(`\x1b]0;${title}\x07`);
}, [agentState?.name]);
// Whether a stream is in flight (disables input)
// Uses synced state to keep ref in sync for reliable async checks
const [streaming, setStreaming, streamingRef] = useSyncedState(false);

View File

@@ -1,5 +1,6 @@
import { Box, Text, useInput } from "ink";
import { memo, useState } from "react";
import { useProgressIndicator } from "../hooks/useProgressIndicator";
import { colors } from "./colors";
type Props = {
@@ -9,6 +10,7 @@ type Props = {
export const EnterPlanModeDialog = memo(({ onApprove, onReject }: Props) => {
const [selectedOption, setSelectedOption] = useState(0);
useProgressIndicator();
const options = [
{ label: "Yes, enter plan mode", action: onApprove },

View File

@@ -1,5 +1,6 @@
import { Box, Text, useInput } from "ink";
import { memo, useMemo, useState } from "react";
import { useProgressIndicator } from "../hooks/useProgressIndicator";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { useTextInputCursor } from "../hooks/useTextInputCursor";
import { colors } from "./colors";
@@ -49,6 +50,7 @@ export const InlineBashApproval = memo(
clear,
} = useTextInputCursor();
const columns = useTerminalWidth();
useProgressIndicator();
// Custom option index depends on whether "always" option is shown
const customOptionIndex = allowPersistence ? 2 : 1;

View File

@@ -1,5 +1,6 @@
import { Box, Text, useInput } from "ink";
import { memo, useState } from "react";
import { useProgressIndicator } from "../hooks/useProgressIndicator";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { colors } from "./colors";
@@ -49,6 +50,7 @@ export const InlineEnterPlanModeApproval = memo(
({ onApprove, onReject, isFocused = true }: Props) => {
const [selectedOption, setSelectedOption] = useState(0);
const columns = useTerminalWidth();
useProgressIndicator();
const options = [
{ label: "Yes, enter plan mode", action: onApprove },

View File

@@ -3,6 +3,7 @@ import { memo, useMemo, useState } from "react";
import type { AdvancedDiffSuccess } from "../helpers/diff";
import { parsePatchToAdvancedDiff } from "../helpers/diff";
import { parsePatchOperations } from "../helpers/formatArgsDisplay";
import { useProgressIndicator } from "../hooks/useProgressIndicator";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { useTextInputCursor } from "../hooks/useTextInputCursor";
import { AdvancedDiffRenderer } from "./AdvancedDiffRenderer";
@@ -165,6 +166,7 @@ export const InlineFileEditApproval = memo(
clear,
} = useTextInputCursor();
const columns = useTerminalWidth();
useProgressIndicator();
// Custom option index depends on whether "always" option is shown
const customOptionIndex = allowPersistence ? 2 : 1;

View File

@@ -1,5 +1,6 @@
import { Box, Text, useInput } from "ink";
import { memo, useMemo, useState } from "react";
import { useProgressIndicator } from "../hooks/useProgressIndicator";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { useTextInputCursor } from "../hooks/useTextInputCursor";
import { colors } from "./colors";
@@ -63,6 +64,7 @@ export const InlineGenericApproval = memo(
clear,
} = useTextInputCursor();
const columns = useTerminalWidth();
useProgressIndicator();
// Custom option index depends on whether "always" option is shown
const customOptionIndex = allowPersistence ? 2 : 1;

View File

@@ -1,5 +1,6 @@
import { Box, Text, useInput } from "ink";
import { memo, useMemo, useState } from "react";
import { useProgressIndicator } from "../hooks/useProgressIndicator";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { useTextInputCursor } from "../hooks/useTextInputCursor";
import { colors } from "./colors";
@@ -41,6 +42,7 @@ export const InlinePlanApproval = memo(
clear,
} = useTextInputCursor();
const columns = useTerminalWidth();
useProgressIndicator();
const customOptionIndex = 2;
const maxOptionIndex = customOptionIndex;

View File

@@ -1,5 +1,6 @@
import { Box, Text, useInput } from "ink";
import { Fragment, memo, useMemo, useState } from "react";
import { useProgressIndicator } from "../hooks/useProgressIndicator";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { useTextInputCursor } from "../hooks/useTextInputCursor";
import { colors } from "./colors";
@@ -41,6 +42,7 @@ export const InlineQuestionApproval = memo(
} = useTextInputCursor();
const [selectedMulti, setSelectedMulti] = useState<Set<number>>(new Set());
const columns = useTerminalWidth();
useProgressIndicator();
const currentQuestion = questions[currentQuestionIndex];

View File

@@ -1,5 +1,6 @@
import { Box, Text, useInput } from "ink";
import { memo, useMemo, useState } from "react";
import { useProgressIndicator } from "../hooks/useProgressIndicator";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { useTextInputCursor } from "../hooks/useTextInputCursor";
import { colors } from "./colors";
@@ -55,6 +56,7 @@ export const InlineTaskApproval = memo(
clear,
} = useTextInputCursor();
const columns = useTerminalWidth();
useProgressIndicator();
// Custom option index depends on whether "always" option is shown
const customOptionIndex = allowPersistence ? 2 : 1;

View File

@@ -1,6 +1,7 @@
import { Box, Text, useInput } from "ink";
import { memo, useState } from "react";
import { resolvePlaceholders } from "../helpers/pasteRegistry";
import { useProgressIndicator } from "../hooks/useProgressIndicator";
import { colors } from "./colors";
import { MarkdownDisplay } from "./MarkdownDisplay";
import { PasteAwareTextInput } from "./PasteAwareTextInput";
@@ -45,6 +46,7 @@ export const PlanModeDialog = memo(
const [selectedOption, setSelectedOption] = useState(0);
const [isEnteringReason, setIsEnteringReason] = useState(false);
const [denyReason, setDenyReason] = useState("");
useProgressIndicator();
const options = [
{ label: "Yes, and auto-accept edits", action: onApproveAndAcceptEdits },

View File

@@ -1,5 +1,6 @@
import { Box, Text, useInput } from "ink";
import { memo, useState } from "react";
import { useProgressIndicator } from "../hooks/useProgressIndicator";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { useTextInputCursor } from "../hooks/useTextInputCursor";
import { colors } from "./colors";
@@ -37,6 +38,7 @@ export const StaticPlanApproval = memo(
clear,
} = useTextInputCursor();
const columns = useTerminalWidth();
useProgressIndicator();
const customOptionIndex = 2;
const maxOptionIndex = customOptionIndex;

View File

@@ -0,0 +1,45 @@
import { useEffect } from "react";
/**
* Shows an indeterminate progress indicator in the terminal tab/taskbar
* while the component is mounted (useful for "waiting for user" states).
*
* Uses OSC 9;4 (ConEmu progress bar sequence) supported by:
* - iTerm2
* - Windows Terminal
* - ConEmu
* - gnome-terminal (VTE)
*
* Format: ESC ] 9 ; 4 ; <state> ; <progress> BEL
* States:
* 0 = hidden (clear)
* 1 = normal progress
* 2 = error state
* 3 = indeterminate (animated)
* 4 = warning state
*/
// Show indeterminate progress (animated green bar)
const PROGRESS_INDETERMINATE = "\x1b]9;4;3;0\x07";
// Clear/hide progress
const PROGRESS_CLEAR = "\x1b]9;4;0;0\x07";
/**
* Hook that shows an indeterminate progress indicator while mounted.
* Clears the indicator when the component unmounts.
*
* @param active - Whether the indicator should be shown (default: true)
*/
export function useProgressIndicator(active = true): void {
useEffect(() => {
if (!active) return;
// Show indeterminate progress on mount
process.stdout.write(PROGRESS_INDETERMINATE);
// Clear progress on unmount
return () => {
process.stdout.write(PROGRESS_CLEAR);
};
}, [active]);
}