feat: add prompt based hooks (#795)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -3336,7 +3336,7 @@ export default function App({
|
||||
lastDequeuedMessageRef.current = null; // Clear - message was processed successfully
|
||||
lastSentInputRef.current = null; // Clear - no recovery needed
|
||||
|
||||
// Get last assistant message and reasoning for Stop hook
|
||||
// Get last assistant message, user message, and reasoning for Stop hook
|
||||
const lastAssistant = Array.from(
|
||||
buffersRef.current.byId.values(),
|
||||
).findLast((item) => item.kind === "assistant" && "text" in item);
|
||||
@@ -3344,6 +3344,11 @@ export default function App({
|
||||
lastAssistant && "text" in lastAssistant
|
||||
? lastAssistant.text
|
||||
: undefined;
|
||||
const firstUser = Array.from(buffersRef.current.byId.values()).find(
|
||||
(item) => item.kind === "user" && "text" in item,
|
||||
);
|
||||
const userMessage =
|
||||
firstUser && "text" in firstUser ? firstUser.text : undefined;
|
||||
const precedingReasoning = buffersRef.current.lastReasoning;
|
||||
buffersRef.current.lastReasoning = undefined; // Clear after use
|
||||
|
||||
@@ -3357,6 +3362,7 @@ export default function App({
|
||||
undefined, // workingDirectory (uses default)
|
||||
precedingReasoning,
|
||||
assistantMessage,
|
||||
userMessage,
|
||||
);
|
||||
|
||||
// If hook blocked (exit 2), inject stderr feedback and continue conversation
|
||||
@@ -3373,9 +3379,7 @@ export default function App({
|
||||
buffersRef.current.byId.set(statusId, {
|
||||
kind: "status",
|
||||
id: statusId,
|
||||
lines: [
|
||||
"Stop hook encountered blocking error, continuing loop with stderr feedback.",
|
||||
],
|
||||
lines: ["Stop hook blocked, continuing conversation."],
|
||||
});
|
||||
buffersRef.current.order.push(statusId);
|
||||
refreshDerived();
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
import { Box, useInput } from "ink";
|
||||
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
type HookCommand,
|
||||
type HookEvent,
|
||||
type HookMatcher,
|
||||
isCommandHook,
|
||||
isPromptHook,
|
||||
isToolEvent,
|
||||
type SimpleHookEvent,
|
||||
type SimpleHookMatcher,
|
||||
@@ -39,6 +42,21 @@ const BOX_BOTTOM_RIGHT = "╯";
|
||||
const BOX_HORIZONTAL = "─";
|
||||
const BOX_VERTICAL = "│";
|
||||
|
||||
/**
|
||||
* Get a display label for a hook (command or prompt).
|
||||
* For prompt hooks, returns just the prompt text (without prefix).
|
||||
*/
|
||||
function getHookDisplayLabel(hook: HookCommand | undefined): string {
|
||||
if (!hook) return "";
|
||||
if (isCommandHook(hook)) {
|
||||
return hook.command;
|
||||
}
|
||||
if (isPromptHook(hook)) {
|
||||
return `${hook.prompt.slice(0, 40)}${hook.prompt.length > 40 ? "..." : ""}`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
interface HooksManagerProps {
|
||||
onClose: () => void;
|
||||
agentId?: string;
|
||||
@@ -533,10 +551,12 @@ export const HooksManager = memo(function HooksManager({
|
||||
const matcherPattern = isToolMatcher
|
||||
? (hook as HookMatcherWithSource).matcher || "*"
|
||||
: null;
|
||||
// Both types have hooks array
|
||||
const command = "hooks" in hook ? hook.hooks[0]?.command || "" : "";
|
||||
// Both types have hooks array - get display label for first hook
|
||||
const firstHook = "hooks" in hook ? hook.hooks[0] : undefined;
|
||||
const command = getHookDisplayLabel(firstHook);
|
||||
const truncatedCommand =
|
||||
command.length > 50 ? `${command.slice(0, 47)}...` : command;
|
||||
const isPrompt = firstHook ? isPromptHook(firstHook) : false;
|
||||
|
||||
return (
|
||||
<Text key={`${hook.source}-${index}`}>
|
||||
@@ -549,6 +569,7 @@ export const HooksManager = memo(function HooksManager({
|
||||
) : (
|
||||
<Text> </Text>
|
||||
)}
|
||||
{isPrompt && <Text color={colors.status.processing}>✦ </Text>}
|
||||
<Text dimColor>{truncatedCommand}</Text>
|
||||
</Text>
|
||||
);
|
||||
@@ -691,8 +712,10 @@ export const HooksManager = memo(function HooksManager({
|
||||
const matcherPattern = isToolMatcher
|
||||
? (hook as HookMatcherWithSource).matcher || "*"
|
||||
: null;
|
||||
// Both types have hooks array
|
||||
const command = hook && "hooks" in hook ? hook.hooks[0]?.command : "";
|
||||
// Both types have hooks array - get display label for first hook
|
||||
const firstHook = hook && "hooks" in hook ? hook.hooks[0] : undefined;
|
||||
const command = getHookDisplayLabel(firstHook);
|
||||
const isPrompt = firstHook ? isPromptHook(firstHook) : false;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
@@ -702,7 +725,16 @@ export const HooksManager = memo(function HooksManager({
|
||||
<Text> </Text>
|
||||
|
||||
{matcherPattern !== null && <Text>Matcher: {matcherPattern}</Text>}
|
||||
<Text>Command: {command}</Text>
|
||||
<Text>
|
||||
{isPrompt ? (
|
||||
<>
|
||||
Hook: <Text color={colors.status.processing}>✦ </Text>
|
||||
{command}
|
||||
</>
|
||||
) : (
|
||||
<>Command: {command}</>
|
||||
)}
|
||||
</Text>
|
||||
<Text>Source: {hook ? getSourceLabel(hook.source) : ""}</Text>
|
||||
<Text> </Text>
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export function createContextTracker(): ContextTracker {
|
||||
return {
|
||||
lastContextTokens: 0,
|
||||
contextTokensHistory: [],
|
||||
currentTurnId: 0,
|
||||
currentTurnId: 0, // simple in-memory counter for now
|
||||
pendingCompaction: false,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user