feat: add prompt based hooks (#795)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
jnjpng
2026-02-05 17:55:00 -08:00
committed by GitHub
parent bbe02e90e8
commit ee28095ebc
11 changed files with 967 additions and 42 deletions

View File

@@ -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();

View File

@@ -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>

View File

@@ -22,7 +22,7 @@ export function createContextTracker(): ContextTracker {
return {
lastContextTokens: 0,
contextTokensHistory: [],
currentTurnId: 0,
currentTurnId: 0, // simple in-memory counter for now
pendingCompaction: false,
};
}