feat: add background task notification system (#827)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-02-04 22:45:16 -08:00
committed by GitHub
parent 84e9a6d744
commit 48ccd8f220
44 changed files with 2244 additions and 234 deletions

View File

@@ -34,6 +34,20 @@ export const EventMessage = memo(({ line }: { line: EventLine }) => {
const columns = useTerminalWidth();
const rightWidth = Math.max(0, columns - 2);
if (line.eventType === "task_notification") {
const summary = line.summary || "Agent task completed";
return (
<Box flexDirection="row">
<Box width={2} flexShrink={0}>
<Text color={colors.tool.completed}></Text>
</Box>
<Box flexGrow={1} width={rightWidth}>
<Text bold>{summary}</Text>
</Box>
</Box>
);
}
// Only handle compaction events for now
if (line.eventType !== "compaction") {
return (

View File

@@ -24,6 +24,7 @@ import { OPENAI_CODEX_PROVIDER_NAME } from "../../providers/openai-codex-provide
import { ralphMode } from "../../ralph/mode";
import { settingsManager } from "../../settings-manager";
import { charsToTokens, formatCompact } from "../helpers/format";
import type { QueuedMessage } from "../helpers/messageQueueBridge";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { colors } from "./colors";
import { InputAssist } from "./InputAssist";
@@ -236,7 +237,7 @@ export function Input({
agentName?: string | null;
currentModel?: string | null;
currentModelProvider?: string | null;
messageQueue?: string[];
messageQueue?: QueuedMessage[];
onEnterQueueEditMode?: () => void;
onEscapeCancel?: () => void;
ralphActive?: boolean;
@@ -548,7 +549,14 @@ export function Input({
) {
setAtStartBoundary(false);
// Clear the queue and load into input as one multi-line message
const queueText = messageQueue.join("\n");
const queueText = messageQueue
.filter((item) => item.kind === "user")
.map((item) => item.text.trim())
.filter((msg) => msg.length > 0)
.join("\n");
if (!queueText) {
return;
}
setValue(queueText);
// Signal to App.tsx to clear the queue
if (onEnterQueueEditMode) {

View File

@@ -1,17 +1,26 @@
import { Box } from "ink";
import { memo } from "react";
import type { QueuedMessage } from "../helpers/messageQueueBridge";
import { Text } from "./Text";
interface QueuedMessagesProps {
messages: string[];
messages: QueuedMessage[];
}
export const QueuedMessages = memo(({ messages }: QueuedMessagesProps) => {
const maxDisplay = 5;
const displayMessages = messages
.filter((msg) => msg.kind === "user")
.map((msg) => msg.text.trim())
.filter((msg) => msg.length > 0);
if (displayMessages.length === 0) {
return null;
}
return (
<Box flexDirection="column" marginBottom={1}>
{messages.slice(0, maxDisplay).map((msg, index) => (
{displayMessages.slice(0, maxDisplay).map((msg, index) => (
<Box key={`${index}-${msg.slice(0, 50)}`} flexDirection="row">
<Box width={2} flexShrink={0}>
<Text dimColor>{">"}</Text>
@@ -22,11 +31,13 @@ export const QueuedMessages = memo(({ messages }: QueuedMessagesProps) => {
</Box>
))}
{messages.length > maxDisplay && (
{displayMessages.length > maxDisplay && (
<Box flexDirection="row">
<Box width={2} flexShrink={0} />
<Box flexGrow={1}>
<Text dimColor>...and {messages.length - maxDisplay} more</Text>
<Text dimColor>
...and {displayMessages.length - maxDisplay} more
</Text>
</Box>
</Box>
)}

View File

@@ -111,8 +111,12 @@ const AgentRow = memo(
<Text dimColor>{" "}</Text>
{agent.status === "error" ? (
<Text color={colors.subagent.error}>Error</Text>
) : isComplete ? (
<Text dimColor>Done</Text>
) : agent.isBackground ? (
<Text dimColor>Running in the background</Text>
) : (
<Text dimColor>{isComplete ? "Done" : "Running..."}</Text>
<Text dimColor>Running...</Text>
)}
</Box>
</Box>
@@ -197,6 +201,14 @@ const AgentRow = memo(
</Text>
</Box>
</>
) : agent.isBackground ? (
<>
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
</Text>
<Text dimColor>{" Running in the background"}</Text>
</>
) : lastTool ? (
<>
<Text color={colors.subagent.treeChar}>

View File

@@ -28,12 +28,13 @@ export interface StaticSubagent {
id: string;
type: string;
description: string;
status: "completed" | "error";
status: "completed" | "error" | "running";
toolCount: number;
totalTokens: number;
agentURL: string | null;
error?: string;
model?: string;
isBackground?: boolean;
}
interface SubagentGroupStaticProps {
@@ -91,7 +92,7 @@ const AgentRow = memo(({ agent, isLast }: AgentRowProps) => {
{/* Status line */}
<Box flexDirection="row">
{agent.status === "completed" ? (
{agent.status === "completed" && !agent.isBackground ? (
<>
<Text color={colors.subagent.treeChar}>
{" "}
@@ -99,7 +100,7 @@ const AgentRow = memo(({ agent, isLast }: AgentRowProps) => {
</Text>
<Text dimColor>{" Done"}</Text>
</>
) : (
) : agent.status === "error" ? (
<>
<Box width={gutterWidth} flexShrink={0}>
<Text>
@@ -116,6 +117,14 @@ const AgentRow = memo(({ agent, isLast }: AgentRowProps) => {
</Text>
</Box>
</>
) : (
<>
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
</Text>
<Text dimColor>{" Running in the background"}</Text>
</>
)}
</Box>
</Box>

View File

@@ -10,6 +10,7 @@ import {
parsePatchInput,
parsePatchOperations,
} from "../helpers/formatArgsDisplay.js";
import { getSubagentByToolCallId } from "../helpers/subagentState.js";
import {
getDisplayToolName,
isFileEditTool,
@@ -112,6 +113,13 @@ export const ToolCallMessage = memo(
// and liveItems handles pending approvals via InlineGenericApproval)
if (isTaskTool(rawName)) {
const isFinished = line.phase === "finished";
const subagent = line.toolCallId
? getSubagentByToolCallId(line.toolCallId)
: undefined;
if (subagent) {
// Task tool calls with subagent data are handled by SubagentGroupDisplay/Static
return null;
}
if (!isFinished) {
// Not finished - SubagentGroupDisplay or approval UI handles this
return null;

View File

@@ -1,6 +1,7 @@
import { memo } from "react";
import stringWidth from "string-width";
import { SYSTEM_REMINDER_CLOSE, SYSTEM_REMINDER_OPEN } from "../../constants";
import { extractTaskNotificationsForDisplay } from "../helpers/taskNotifications";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { colors, hexToBgAnsi, hexToFgAnsi } from "./colors";
import { Text } from "./Text";
@@ -156,6 +157,11 @@ function renderBlock(
export const UserMessage = memo(({ line }: { line: UserLine }) => {
const columns = useTerminalWidth();
const contentWidth = Math.max(1, columns - 2);
const cleanedText = extractTaskNotificationsForDisplay(line.text).cleanedText;
const displayText = cleanedText.trim();
if (!displayText) {
return null;
}
// Build combined ANSI code for background + optional foreground
const { background, text: textColor } = colors.userMessage;
@@ -164,23 +170,20 @@ export const UserMessage = memo(({ line }: { line: UserLine }) => {
const colorAnsi = bgAnsi + fgAnsi;
// Split into system-reminder blocks and user content blocks
const blocks = splitSystemReminderBlocks(line.text);
const blocks = splitSystemReminderBlocks(displayText);
const allLines: string[] = [];
for (const block of blocks) {
if (!block.text.trim()) continue;
// Add blank line between blocks (not before first)
if (allLines.length > 0) {
allLines.push("");
}
const blockLines = renderBlock(
block.text,
contentWidth,
columns,
!block.isSystemReminder, // highlight user content, not system-reminder
!block.isSystemReminder,
colorAnsi,
);
allLines.push(...blockLines);