feat: add background task notification system (#827)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user