mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
feat: Load workspace hooks for V1 conversations and add hooks viewer UI (#12773)
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: enyst <engel.nyst@gmail.com> Co-authored-by: Alona King <alona@all-hands.dev>
This commit is contained in:
@@ -44,6 +44,7 @@ describe("SystemMessage UI Rendering", () => {
|
||||
<ToolsContextMenu
|
||||
onClose={() => {}}
|
||||
onShowSkills={() => {}}
|
||||
onShowHooks={() => {}}
|
||||
onShowAgentTools={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
import { HookEventItem } from "#/components/features/conversation-panel/hook-event-item";
|
||||
import { HooksEmptyState } from "#/components/features/conversation-panel/hooks-empty-state";
|
||||
import { HooksLoadingState } from "#/components/features/conversation-panel/hooks-loading-state";
|
||||
import { HooksModalHeader } from "#/components/features/conversation-panel/hooks-modal-header";
|
||||
import { HookEvent } from "#/api/conversation-service/v1-conversation-service.types";
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock("react-i18next", async () => {
|
||||
const actual = await vi.importActual("react-i18next");
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({
|
||||
t: (key: string, params?: Record<string, unknown>) => {
|
||||
const translations: Record<string, string> = {
|
||||
HOOKS_MODAL$TITLE: "Available Hooks",
|
||||
HOOKS_MODAL$HOOK_COUNT: `${params?.count ?? 0} hooks`,
|
||||
HOOKS_MODAL$EVENT_PRE_TOOL_USE: "Pre Tool Use",
|
||||
HOOKS_MODAL$EVENT_POST_TOOL_USE: "Post Tool Use",
|
||||
HOOKS_MODAL$EVENT_USER_PROMPT_SUBMIT: "User Prompt Submit",
|
||||
HOOKS_MODAL$EVENT_SESSION_START: "Session Start",
|
||||
HOOKS_MODAL$EVENT_SESSION_END: "Session End",
|
||||
HOOKS_MODAL$EVENT_STOP: "Stop",
|
||||
HOOKS_MODAL$MATCHER: "Matcher",
|
||||
HOOKS_MODAL$COMMANDS: "Commands",
|
||||
HOOKS_MODAL$TYPE: `Type: ${params?.type ?? ""}`,
|
||||
HOOKS_MODAL$TIMEOUT: `Timeout: ${params?.timeout ?? 0}s`,
|
||||
HOOKS_MODAL$ASYNC: "Async",
|
||||
COMMON$FETCH_ERROR: "Failed to fetch data",
|
||||
CONVERSATION$NO_HOOKS: "No hooks configured",
|
||||
BUTTON$REFRESH: "Refresh",
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
i18n: {
|
||||
changeLanguage: () => new Promise(() => {}),
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe("HooksLoadingState", () => {
|
||||
it("should render loading spinner", () => {
|
||||
render(<HooksLoadingState />);
|
||||
const spinner = document.querySelector(".animate-spin");
|
||||
expect(spinner).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("HooksEmptyState", () => {
|
||||
it("should render no hooks message when not error", () => {
|
||||
render(<HooksEmptyState isError={false} />);
|
||||
expect(screen.getByText("No hooks configured")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render error message when isError is true", () => {
|
||||
render(<HooksEmptyState isError={true} />);
|
||||
expect(screen.getByText("Failed to fetch data")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("HooksModalHeader", () => {
|
||||
const defaultProps = {
|
||||
isAgentReady: true,
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
onRefresh: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render title", () => {
|
||||
render(<HooksModalHeader {...defaultProps} />);
|
||||
expect(screen.getByText("Available Hooks")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render refresh button when agent is ready", () => {
|
||||
render(<HooksModalHeader {...defaultProps} />);
|
||||
expect(screen.getByTestId("refresh-hooks")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not render refresh button when agent is not ready", () => {
|
||||
render(<HooksModalHeader {...defaultProps} isAgentReady={false} />);
|
||||
expect(screen.queryByTestId("refresh-hooks")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onRefresh when refresh button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onRefresh = vi.fn();
|
||||
render(<HooksModalHeader {...defaultProps} onRefresh={onRefresh} />);
|
||||
|
||||
await user.click(screen.getByTestId("refresh-hooks"));
|
||||
expect(onRefresh).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should disable refresh button when loading", () => {
|
||||
render(<HooksModalHeader {...defaultProps} isLoading={true} />);
|
||||
expect(screen.getByTestId("refresh-hooks")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should disable refresh button when refetching", () => {
|
||||
render(<HooksModalHeader {...defaultProps} isRefetching={true} />);
|
||||
expect(screen.getByTestId("refresh-hooks")).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("HookEventItem", () => {
|
||||
const mockHookEvent: HookEvent = {
|
||||
event_type: "stop",
|
||||
matchers: [
|
||||
{
|
||||
matcher: "*",
|
||||
hooks: [
|
||||
{
|
||||
type: "command",
|
||||
command: ".openhands/hooks/on_stop.sh",
|
||||
timeout: 30,
|
||||
async: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
hookEvent: mockHookEvent,
|
||||
isExpanded: false,
|
||||
onToggle: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render event type label using i18n", () => {
|
||||
render(<HookEventItem {...defaultProps} />);
|
||||
expect(screen.getByText("Stop")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render hook count", () => {
|
||||
render(<HookEventItem {...defaultProps} />);
|
||||
expect(screen.getByText("1 hooks")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onToggle when clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onToggle = vi.fn();
|
||||
render(<HookEventItem {...defaultProps} onToggle={onToggle} />);
|
||||
|
||||
await user.click(screen.getByRole("button"));
|
||||
expect(onToggle).toHaveBeenCalledWith("stop");
|
||||
});
|
||||
|
||||
it("should show collapsed state by default", () => {
|
||||
render(<HookEventItem {...defaultProps} isExpanded={false} />);
|
||||
// Matcher content should not be visible when collapsed
|
||||
expect(screen.queryByText("*")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show expanded state with matcher content", () => {
|
||||
render(<HookEventItem {...defaultProps} isExpanded={true} />);
|
||||
// Matcher content should be visible when expanded
|
||||
expect(screen.getByText("*")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render async badge for async hooks", () => {
|
||||
render(<HookEventItem {...defaultProps} isExpanded={true} />);
|
||||
expect(screen.getByText("Async")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render different event types with correct i18n labels", () => {
|
||||
const eventTypes = [
|
||||
{ type: "pre_tool_use", label: "Pre Tool Use" },
|
||||
{ type: "post_tool_use", label: "Post Tool Use" },
|
||||
{ type: "user_prompt_submit", label: "User Prompt Submit" },
|
||||
{ type: "session_start", label: "Session Start" },
|
||||
{ type: "session_end", label: "Session End" },
|
||||
{ type: "stop", label: "Stop" },
|
||||
];
|
||||
|
||||
eventTypes.forEach(({ type, label }) => {
|
||||
const { unmount } = render(
|
||||
<HookEventItem
|
||||
{...defaultProps}
|
||||
hookEvent={{ ...mockHookEvent, event_type: type }}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText(label)).toBeInTheDocument();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fallback to event_type when no i18n key exists", () => {
|
||||
render(
|
||||
<HookEventItem
|
||||
{...defaultProps}
|
||||
hookEvent={{ ...mockHookEvent, event_type: "unknown_event" }}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("unknown_event")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
V1AppConversation,
|
||||
V1AppConversationPage,
|
||||
GetSkillsResponse,
|
||||
GetHooksResponse,
|
||||
V1RuntimeConversationInfo,
|
||||
} from "./v1-conversation-service.types";
|
||||
|
||||
@@ -400,6 +401,18 @@ class V1ConversationService {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all hooks associated with a V1 conversation
|
||||
* @param conversationId The conversation ID
|
||||
* @returns The available hooks associated with the conversation
|
||||
*/
|
||||
static async getHooks(conversationId: string): Promise<GetHooksResponse> {
|
||||
const { data } = await openHands.get<GetHooksResponse>(
|
||||
`/api/v1/app-conversations/${conversationId}/hooks`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conversation info directly from the runtime for a V1 conversation
|
||||
* Uses the custom runtime URL from the conversation
|
||||
|
||||
@@ -135,6 +135,27 @@ export interface GetSkillsResponse {
|
||||
skills: Skill[];
|
||||
}
|
||||
|
||||
export interface HookDefinition {
|
||||
type: string; // 'command' or 'prompt'
|
||||
command: string;
|
||||
timeout: number;
|
||||
async?: boolean;
|
||||
}
|
||||
|
||||
export interface HookMatcher {
|
||||
matcher: string; // Pattern: '*', exact match, or regex
|
||||
hooks: HookDefinition[];
|
||||
}
|
||||
|
||||
export interface HookEvent {
|
||||
event_type: string; // e.g., 'stop', 'pre_tool_use', 'post_tool_use'
|
||||
matchers: HookMatcher[];
|
||||
}
|
||||
|
||||
export interface GetHooksResponse {
|
||||
hooks: HookEvent[];
|
||||
}
|
||||
|
||||
// Runtime conversation types (from agent server)
|
||||
export interface V1RuntimeConversationStats {
|
||||
usage_to_metrics: Record<string, V1RuntimeMetrics>;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { HookExecutionEventMessage } from "#/components/shared/hook-execution-event-message";
|
||||
@@ -8,3 +8,4 @@ export { ObservationPairEventMessage } from "./observation-pair-event-message";
|
||||
export { GenericEventMessageWrapper } from "./generic-event-message-wrapper";
|
||||
export { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
export { LikertScaleWrapper } from "./likert-scale-wrapper";
|
||||
export { HookExecutionEventMessage } from "./hook-execution-event-message";
|
||||
|
||||
@@ -27,15 +27,19 @@ const contextMenuListItemClassName = cn(
|
||||
interface ToolsContextMenuProps {
|
||||
onClose: () => void;
|
||||
onShowSkills: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onShowHooks: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onShowAgentTools: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
shouldShowAgentTools?: boolean;
|
||||
shouldShowHooks?: boolean;
|
||||
}
|
||||
|
||||
export function ToolsContextMenu({
|
||||
onClose,
|
||||
onShowSkills,
|
||||
onShowHooks,
|
||||
onShowAgentTools,
|
||||
shouldShowAgentTools = true,
|
||||
shouldShowHooks = false,
|
||||
}: ToolsContextMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
@@ -141,6 +145,21 @@ export function ToolsContextMenu({
|
||||
/>
|
||||
</ContextMenuListItem>
|
||||
|
||||
{/* Show Hooks - Only show for V1 conversations */}
|
||||
{shouldShowHooks && (
|
||||
<ContextMenuListItem
|
||||
testId="show-hooks-button"
|
||||
onClick={onShowHooks}
|
||||
className={contextMenuListItemClassName}
|
||||
>
|
||||
<ToolsContextMenuIconText
|
||||
icon={<ToolsIcon width={16} height={16} />}
|
||||
text={t(I18nKey.CONVERSATION$SHOW_HOOKS)}
|
||||
className={CONTEXT_MENU_ICON_TEXT_CLASSNAME}
|
||||
/>
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
|
||||
{/* Show Agent Tools and Metadata - Only show if system message is available */}
|
||||
{shouldShowAgentTools && (
|
||||
<ContextMenuListItem
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useConversationNameContextMenu } from "#/hooks/use-conversation-name-co
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { SystemMessageModal } from "../conversation-panel/system-message-modal";
|
||||
import { SkillsModal } from "../conversation-panel/skills-modal";
|
||||
import { HooksModal } from "../conversation-panel/hooks-modal";
|
||||
|
||||
export function Tools() {
|
||||
const { t } = useTranslation();
|
||||
@@ -18,12 +19,16 @@ export function Tools() {
|
||||
const {
|
||||
handleShowAgentTools,
|
||||
handleShowSkills,
|
||||
handleShowHooks,
|
||||
systemModalVisible,
|
||||
setSystemModalVisible,
|
||||
skillsModalVisible,
|
||||
setSkillsModalVisible,
|
||||
hooksModalVisible,
|
||||
setHooksModalVisible,
|
||||
systemMessage,
|
||||
shouldShowAgentTools,
|
||||
shouldShowHooks,
|
||||
} = useConversationNameContextMenu({
|
||||
conversationId,
|
||||
conversationStatus: conversation?.status,
|
||||
@@ -52,8 +57,10 @@ export function Tools() {
|
||||
<ToolsContextMenu
|
||||
onClose={() => setContextMenuOpen(false)}
|
||||
onShowSkills={handleShowSkills}
|
||||
onShowHooks={handleShowHooks}
|
||||
onShowAgentTools={handleShowAgentTools}
|
||||
shouldShowAgentTools={shouldShowAgentTools}
|
||||
shouldShowHooks={shouldShowHooks}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -68,6 +75,11 @@ export function Tools() {
|
||||
{skillsModalVisible && (
|
||||
<SkillsModal onClose={() => setSkillsModalVisible(false)} />
|
||||
)}
|
||||
|
||||
{/* Hooks Modal */}
|
||||
{hooksModalVisible && (
|
||||
<HooksModal onClose={() => setHooksModalVisible(false)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { Typography } from "#/ui/typography";
|
||||
import { HookEvent } from "#/api/conversation-service/v1-conversation-service.types";
|
||||
import { HookMatcherContent } from "./hook-matcher-content";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface HookEventItemProps {
|
||||
hookEvent: HookEvent;
|
||||
isExpanded: boolean;
|
||||
onToggle: (eventType: string) => void;
|
||||
}
|
||||
|
||||
const EVENT_TYPE_I18N_KEYS: Record<string, I18nKey> = {
|
||||
pre_tool_use: I18nKey.HOOKS_MODAL$EVENT_PRE_TOOL_USE,
|
||||
post_tool_use: I18nKey.HOOKS_MODAL$EVENT_POST_TOOL_USE,
|
||||
user_prompt_submit: I18nKey.HOOKS_MODAL$EVENT_USER_PROMPT_SUBMIT,
|
||||
session_start: I18nKey.HOOKS_MODAL$EVENT_SESSION_START,
|
||||
session_end: I18nKey.HOOKS_MODAL$EVENT_SESSION_END,
|
||||
stop: I18nKey.HOOKS_MODAL$EVENT_STOP,
|
||||
};
|
||||
|
||||
export function HookEventItem({
|
||||
hookEvent,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
}: HookEventItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const i18nKey = EVENT_TYPE_I18N_KEYS[hookEvent.event_type];
|
||||
const eventTypeLabel = i18nKey ? t(i18nKey) : hookEvent.event_type;
|
||||
|
||||
const totalHooks = hookEvent.matchers.reduce(
|
||||
(sum, matcher) => sum + matcher.hooks.length,
|
||||
0,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="rounded-md overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onToggle(hookEvent.event_type)}
|
||||
className="w-full py-3 px-2 text-left flex items-center justify-between hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Typography.Text className="font-bold text-gray-100">
|
||||
{eventTypeLabel}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Typography.Text className="px-2 py-1 text-xs rounded-full bg-gray-800 mr-2">
|
||||
{t(I18nKey.HOOKS_MODAL$HOOK_COUNT, { count: totalHooks })}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="text-gray-300">
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={18} />
|
||||
) : (
|
||||
<ChevronRight size={18} />
|
||||
)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="px-2 pb-3 pt-1">
|
||||
{hookEvent.matchers.map((matcher, index) => (
|
||||
<HookMatcherContent
|
||||
key={`${hookEvent.event_type}-${matcher.matcher}-${index}`}
|
||||
matcher={matcher}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { Typography } from "#/ui/typography";
|
||||
import { Pre } from "#/ui/pre";
|
||||
import { HookMatcher } from "#/api/conversation-service/v1-conversation-service.types";
|
||||
|
||||
interface HookMatcherContentProps {
|
||||
matcher: HookMatcher;
|
||||
}
|
||||
|
||||
export function HookMatcherContent({ matcher }: HookMatcherContentProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="mb-4 p-3 bg-gray-800 rounded-md">
|
||||
<div className="mb-2">
|
||||
<Typography.Text className="text-sm font-semibold text-gray-300">
|
||||
{t(I18nKey.HOOKS_MODAL$MATCHER)}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="ml-2 px-2 py-1 text-xs rounded-full bg-blue-900">
|
||||
{matcher.matcher}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<Typography.Text className="text-sm font-semibold text-gray-300 mb-2">
|
||||
{t(I18nKey.HOOKS_MODAL$COMMANDS)}
|
||||
</Typography.Text>
|
||||
{matcher.hooks.map((hook, index) => (
|
||||
<div key={`${hook.command}-${index}`} className="mt-2">
|
||||
<Pre
|
||||
size="default"
|
||||
font="mono"
|
||||
lineHeight="relaxed"
|
||||
background="dark"
|
||||
textColor="light"
|
||||
padding="medium"
|
||||
borderRadius="medium"
|
||||
shadow="inner"
|
||||
maxHeight="small"
|
||||
overflow="auto"
|
||||
>
|
||||
{hook.command}
|
||||
</Pre>
|
||||
<div className="flex gap-4 mt-1 text-xs text-gray-400">
|
||||
<span>{t(I18nKey.HOOKS_MODAL$TYPE, { type: hook.type })}</span>
|
||||
<span>
|
||||
{t(I18nKey.HOOKS_MODAL$TIMEOUT, { timeout: hook.timeout })}
|
||||
</span>
|
||||
{hook.async ? (
|
||||
<span className="rounded-full bg-emerald-900 px-2 py-0.5 text-emerald-300">
|
||||
{t(I18nKey.HOOKS_MODAL$ASYNC)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
interface HooksEmptyStateProps {
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
export function HooksEmptyState({ isError }: HooksEmptyStateProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full p-4">
|
||||
<Typography.Text className="text-gray-400">
|
||||
{isError
|
||||
? t(I18nKey.COMMON$FETCH_ERROR)
|
||||
: t(I18nKey.CONVERSATION$NO_HOOKS)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export function HooksLoadingState() {
|
||||
return (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/base-modal";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
|
||||
interface HooksModalHeaderProps {
|
||||
isAgentReady: boolean;
|
||||
isLoading: boolean;
|
||||
isRefetching: boolean;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
export function HooksModalHeader({
|
||||
isAgentReady,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
onRefresh,
|
||||
}: HooksModalHeaderProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 w-full">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<BaseModalTitle title={t(I18nKey.HOOKS_MODAL$TITLE)} />
|
||||
{isAgentReady && (
|
||||
<BrandButton
|
||||
testId="refresh-hooks"
|
||||
type="button"
|
||||
variant="primary"
|
||||
className="flex items-center gap-2"
|
||||
onClick={onRefresh}
|
||||
isDisabled={isLoading || isRefetching}
|
||||
>
|
||||
<RefreshCw
|
||||
size={16}
|
||||
className={`${isRefetching ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{t(I18nKey.BUTTON$REFRESH)}
|
||||
</BrandButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useConversationHooks } from "#/hooks/query/use-conversation-hooks";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { Typography } from "#/ui/typography";
|
||||
import { HooksModalHeader } from "./hooks-modal-header";
|
||||
import { HooksLoadingState } from "./hooks-loading-state";
|
||||
import { HooksEmptyState } from "./hooks-empty-state";
|
||||
import { HookEventItem } from "./hook-event-item";
|
||||
import { useAgentState } from "#/hooks/use-agent-state";
|
||||
|
||||
interface HooksModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function HooksModal({ onClose }: HooksModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { curAgentState } = useAgentState();
|
||||
const [expandedEvents, setExpandedEvents] = useState<Record<string, boolean>>(
|
||||
{},
|
||||
);
|
||||
const {
|
||||
data: hooks,
|
||||
isLoading,
|
||||
isError,
|
||||
refetch,
|
||||
isRefetching,
|
||||
} = useConversationHooks();
|
||||
|
||||
const toggleEvent = (eventType: string) => {
|
||||
setExpandedEvents((prev) => ({
|
||||
...prev,
|
||||
[eventType]: !prev[eventType],
|
||||
}));
|
||||
};
|
||||
|
||||
const isAgentReady = ![AgentState.LOADING, AgentState.INIT].includes(
|
||||
curAgentState,
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalBackdrop onClose={onClose}>
|
||||
<ModalBody
|
||||
width="medium"
|
||||
className="max-h-[80vh] flex flex-col items-start"
|
||||
testID="hooks-modal"
|
||||
>
|
||||
<HooksModalHeader
|
||||
isAgentReady={isAgentReady}
|
||||
isLoading={isLoading}
|
||||
isRefetching={isRefetching}
|
||||
onRefresh={refetch}
|
||||
/>
|
||||
|
||||
{isAgentReady && (
|
||||
<Typography.Text className="text-sm text-gray-400">
|
||||
{t(I18nKey.HOOKS_MODAL$WARNING)}
|
||||
</Typography.Text>
|
||||
)}
|
||||
|
||||
<div className="w-full h-[60vh] overflow-auto rounded-md custom-scrollbar-always">
|
||||
{!isAgentReady && (
|
||||
<div className="w-full h-full flex items-center text-center justify-center text-2xl text-tertiary-light">
|
||||
<Typography.Text>
|
||||
{t(I18nKey.DIFF_VIEWER$WAITING_FOR_RUNTIME)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && <HooksLoadingState />}
|
||||
|
||||
{!isLoading &&
|
||||
isAgentReady &&
|
||||
(isError || !hooks || hooks.length === 0) && (
|
||||
<HooksEmptyState isError={isError} />
|
||||
)}
|
||||
|
||||
{!isLoading && isAgentReady && hooks && hooks.length > 0 && (
|
||||
<div className="p-2 space-y-3">
|
||||
{hooks.map((hookEvent) => {
|
||||
const isExpanded =
|
||||
expandedEvents[hookEvent.event_type] || false;
|
||||
|
||||
return (
|
||||
<HookEventItem
|
||||
key={hookEvent.event_type}
|
||||
hookEvent={hookEvent}
|
||||
isExpanded={isExpanded}
|
||||
onToggle={toggleEvent}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
</ModalBackdrop>
|
||||
);
|
||||
}
|
||||
@@ -35,6 +35,7 @@ interface ConversationNameContextMenuProps {
|
||||
onDisplayCost?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onShowAgentTools?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onShowSkills?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onShowHooks?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onExportConversation?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onDownloadViaVSCode?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onTogglePublic?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
@@ -52,6 +53,7 @@ export function ConversationNameContextMenu({
|
||||
onDisplayCost,
|
||||
onShowAgentTools,
|
||||
onShowSkills,
|
||||
onShowHooks,
|
||||
onExportConversation,
|
||||
onDownloadViaVSCode,
|
||||
onTogglePublic,
|
||||
@@ -77,7 +79,7 @@ export function ConversationNameContextMenu({
|
||||
|
||||
const hasDownload = Boolean(onDownloadViaVSCode || onDownloadConversation);
|
||||
const hasExport = Boolean(onExportConversation);
|
||||
const hasTools = Boolean(onShowAgentTools || onShowSkills);
|
||||
const hasTools = Boolean(onShowAgentTools || onShowSkills || onShowHooks);
|
||||
const hasInfo = Boolean(onDisplayCost);
|
||||
const hasControl = Boolean(onStop || onDelete);
|
||||
|
||||
@@ -119,6 +121,20 @@ export function ConversationNameContextMenu({
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
|
||||
{onShowHooks && (
|
||||
<ContextMenuListItem
|
||||
testId="show-hooks-button"
|
||||
onClick={onShowHooks}
|
||||
className={contextMenuListItemClassName}
|
||||
>
|
||||
<ConversationNameContextMenuIconText
|
||||
icon={<ToolsIcon width={16} height={16} />}
|
||||
text={t(I18nKey.CONVERSATION$SHOW_HOOKS)}
|
||||
className={CONTEXT_MENU_ICON_TEXT_CLASSNAME}
|
||||
/>
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
|
||||
{onShowAgentTools && (
|
||||
<ContextMenuListItem
|
||||
testId="show-agent-tools-button"
|
||||
|
||||
@@ -10,6 +10,7 @@ import { EllipsisButton } from "../conversation-panel/ellipsis-button";
|
||||
import { ConversationNameContextMenu } from "./conversation-name-context-menu";
|
||||
import { SystemMessageModal } from "../conversation-panel/system-message-modal";
|
||||
import { SkillsModal } from "../conversation-panel/skills-modal";
|
||||
import { HooksModal } from "../conversation-panel/hooks-modal";
|
||||
import { ConfirmDeleteModal } from "../conversation-panel/confirm-delete-modal";
|
||||
import { ConfirmStopModal } from "../conversation-panel/confirm-stop-modal";
|
||||
import { MetricsModal } from "./metrics-modal/metrics-modal";
|
||||
@@ -34,6 +35,7 @@ export function ConversationName() {
|
||||
handleDisplayCost,
|
||||
handleShowAgentTools,
|
||||
handleShowSkills,
|
||||
handleShowHooks,
|
||||
handleExportConversation,
|
||||
handleTogglePublic,
|
||||
handleCopyShareLink,
|
||||
@@ -46,6 +48,8 @@ export function ConversationName() {
|
||||
setSystemModalVisible,
|
||||
skillsModalVisible,
|
||||
setSkillsModalVisible,
|
||||
hooksModalVisible,
|
||||
setHooksModalVisible,
|
||||
confirmDeleteModalVisible,
|
||||
setConfirmDeleteModalVisible,
|
||||
confirmStopModalVisible,
|
||||
@@ -58,6 +62,7 @@ export function ConversationName() {
|
||||
shouldShowDisplayCost,
|
||||
shouldShowAgentTools,
|
||||
shouldShowSkills,
|
||||
shouldShowHooks,
|
||||
} = useConversationNameContextMenu({
|
||||
conversationId,
|
||||
conversationStatus: conversation?.status,
|
||||
@@ -180,6 +185,7 @@ export function ConversationName() {
|
||||
shouldShowAgentTools ? handleShowAgentTools : undefined
|
||||
}
|
||||
onShowSkills={shouldShowSkills ? handleShowSkills : undefined}
|
||||
onShowHooks={shouldShowHooks ? handleShowHooks : undefined}
|
||||
onExportConversation={
|
||||
shouldShowExport ? handleExportConversation : undefined
|
||||
}
|
||||
@@ -219,6 +225,11 @@ export function ConversationName() {
|
||||
<SkillsModal onClose={() => setSkillsModalVisible(false)} />
|
||||
)}
|
||||
|
||||
{/* Hooks Modal */}
|
||||
{hooksModalVisible && (
|
||||
<HooksModal onClose={() => setHooksModalVisible(false)} />
|
||||
)}
|
||||
|
||||
{/* Confirm Delete Modal */}
|
||||
{confirmDeleteModalVisible && (
|
||||
<ConfirmDeleteModal
|
||||
|
||||
152
frontend/src/components/shared/hook-execution-event-message.tsx
Normal file
152
frontend/src/components/shared/hook-execution-event-message.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { isHookExecutionEvent } from "#/types/v1/type-guards";
|
||||
import { OpenHandsEvent } from "#/types/v1/core";
|
||||
import { GenericEventMessage } from "#/components/features/chat/generic-event-message";
|
||||
|
||||
interface HookExecutionEventMessageProps {
|
||||
event: OpenHandsEvent;
|
||||
}
|
||||
|
||||
function getHookIcon(hookType: string, blocked: boolean): string {
|
||||
if (blocked) {
|
||||
return "🚫";
|
||||
}
|
||||
|
||||
switch (hookType) {
|
||||
case "PreToolUse":
|
||||
return "⏳";
|
||||
case "PostToolUse":
|
||||
return "✅";
|
||||
case "UserPromptSubmit":
|
||||
return "📝";
|
||||
case "SessionStart":
|
||||
return "🚀";
|
||||
case "SessionEnd":
|
||||
return "🏁";
|
||||
case "Stop":
|
||||
return "⏹️";
|
||||
default:
|
||||
return "🔗";
|
||||
}
|
||||
}
|
||||
|
||||
function formatHookCommand(command: string): string {
|
||||
// Truncate long commands for display
|
||||
if (command.length > 80) {
|
||||
return `${command.slice(0, 77)}...`;
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
function getStatusText(blocked: boolean, success: boolean): string {
|
||||
if (blocked) return "blocked";
|
||||
if (success) return "ok";
|
||||
return "failed";
|
||||
}
|
||||
|
||||
function getStatusClassName(blocked: boolean, success: boolean): string {
|
||||
if (blocked) return "bg-amber-900/50 text-amber-300";
|
||||
if (success) return "bg-green-900/50 text-green-300";
|
||||
return "bg-red-900/50 text-red-300";
|
||||
}
|
||||
|
||||
export function HookExecutionEventMessage({
|
||||
event,
|
||||
}: HookExecutionEventMessageProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!isHookExecutionEvent(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const icon = getHookIcon(event.hook_event_type, event.blocked);
|
||||
const statusText = getStatusText(event.blocked, event.success);
|
||||
const statusClassName = getStatusClassName(event.blocked, event.success);
|
||||
|
||||
// Determine the overall success indicator for GenericEventMessage.
|
||||
// When blocked, suppress the success indicator entirely — the amber "blocked"
|
||||
// badge in the title is the authoritative status signal.
|
||||
const getSuccessStatus = (): "success" | "error" | undefined => {
|
||||
if (event.blocked) return undefined;
|
||||
return event.success ? "success" : "error";
|
||||
};
|
||||
const successStatus = getSuccessStatus();
|
||||
|
||||
const title = (
|
||||
<span>
|
||||
{icon} {t("HOOK$HOOK_LABEL")}: {event.hook_event_type}
|
||||
{event.tool_name && (
|
||||
<span className="text-neutral-400 ml-2">({event.tool_name})</span>
|
||||
)}
|
||||
<span className={`ml-2 px-1 py-0.5 rounded text-xs ${statusClassName}`}>
|
||||
{statusText}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
const details = (
|
||||
<div className="flex flex-col gap-2 text-neutral-400">
|
||||
<div>
|
||||
<span className="text-neutral-500">{t("HOOK$COMMAND")}:</span>{" "}
|
||||
<code className="text-xs bg-neutral-800 px-1 py-0.5 rounded">
|
||||
{formatHookCommand(event.hook_command)}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
{event.exit_code !== null && (
|
||||
<div>
|
||||
<span className="text-neutral-500">{t("HOOK$EXIT_CODE")}:</span>{" "}
|
||||
{event.exit_code}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{event.blocked && event.reason && (
|
||||
<div className="text-amber-400">
|
||||
<span className="text-neutral-500">{t("HOOK$BLOCKED_REASON")}:</span>{" "}
|
||||
{event.reason}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{event.additional_context && (
|
||||
<div>
|
||||
<span className="text-neutral-500">{t("HOOK$CONTEXT")}:</span>{" "}
|
||||
{event.additional_context}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{event.error && (
|
||||
<div className="text-red-400">
|
||||
<span className="text-neutral-500">{t("HOOK$ERROR")}:</span>{" "}
|
||||
{event.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{event.stdout && (
|
||||
<div>
|
||||
<span className="text-neutral-500">{t("HOOK$OUTPUT")}:</span>
|
||||
<pre className="text-xs bg-neutral-800 p-2 rounded mt-1 overflow-x-auto max-h-40 overflow-y-auto">
|
||||
{event.stdout}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{event.stderr && (
|
||||
<div>
|
||||
<span className="text-neutral-500">{t("HOOK$STDERR")}:</span>
|
||||
<pre className="text-xs bg-neutral-800 p-2 rounded mt-1 overflow-x-auto max-h-40 overflow-y-auto text-amber-300">
|
||||
{event.stderr}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<GenericEventMessage
|
||||
title={title}
|
||||
details={details}
|
||||
success={successStatus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
isMessageEvent,
|
||||
isAgentErrorEvent,
|
||||
isConversationStateUpdateEvent,
|
||||
isHookExecutionEvent,
|
||||
} from "#/types/v1/type-guards";
|
||||
|
||||
export const shouldRenderEvent = (event: OpenHandsEvent) => {
|
||||
@@ -50,6 +51,11 @@ export const shouldRenderEvent = (event: OpenHandsEvent) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Render hook execution events
|
||||
if (isHookExecutionEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Don't render any other event types (system events, etc.)
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { HookExecutionEventMessage } from "#/components/shared/hook-execution-event-message";
|
||||
@@ -4,3 +4,4 @@ export { ErrorEventMessage } from "./error-event-message";
|
||||
export { FinishEventMessage } from "./finish-event-message";
|
||||
export { GenericEventMessageWrapper } from "./generic-event-message-wrapper";
|
||||
export { ThoughtEventMessage } from "./thought-event-message";
|
||||
export { HookExecutionEventMessage } from "./hook-execution-event-message";
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
isAgentErrorEvent,
|
||||
isUserMessageEvent,
|
||||
isPlanningFileEditorObservationEvent,
|
||||
isHookExecutionEvent,
|
||||
} from "#/types/v1/type-guards";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
FinishEventMessage,
|
||||
GenericEventMessageWrapper,
|
||||
ThoughtEventMessage,
|
||||
HookExecutionEventMessage,
|
||||
} from "./event-message-components";
|
||||
import { createSkillReadyEvent } from "./event-content-helpers/create-skill-ready-event";
|
||||
import { PlanPreview } from "../../features/chat/plan-preview";
|
||||
@@ -188,6 +190,11 @@ export function EventMessage({
|
||||
return <ErrorEventMessage event={event} {...commonProps} />;
|
||||
}
|
||||
|
||||
// Hook execution events
|
||||
if (isHookExecutionEvent(event)) {
|
||||
return <HookExecutionEventMessage event={event} />;
|
||||
}
|
||||
|
||||
// Finish actions
|
||||
if (isActionEvent(event) && event.action.kind === "FinishAction") {
|
||||
return (
|
||||
|
||||
36
frontend/src/hooks/query/use-conversation-hooks.ts
Normal file
36
frontend/src/hooks/query/use-conversation-hooks.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api";
|
||||
import { useConversationId } from "../use-conversation-id";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { useAgentState } from "#/hooks/use-agent-state";
|
||||
import { useSettings } from "./use-settings";
|
||||
|
||||
export const useConversationHooks = () => {
|
||||
const { conversationId } = useConversationId();
|
||||
const { curAgentState } = useAgentState();
|
||||
const { data: settings } = useSettings();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["conversation", conversationId, "hooks", settings?.v1_enabled],
|
||||
queryFn: async () => {
|
||||
if (!conversationId) {
|
||||
throw new Error("No conversation ID provided");
|
||||
}
|
||||
|
||||
// Hooks are only available for V1 conversations
|
||||
if (!settings?.v1_enabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await V1ConversationService.getHooks(conversationId);
|
||||
return data.hooks;
|
||||
},
|
||||
enabled:
|
||||
!!conversationId &&
|
||||
!!settings?.v1_enabled &&
|
||||
curAgentState !== AgentState.LOADING &&
|
||||
curAgentState !== AgentState.INIT,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
};
|
||||
@@ -53,6 +53,7 @@ export function useConversationNameContextMenu({
|
||||
const [metricsModalVisible, setMetricsModalVisible] = React.useState(false);
|
||||
const [systemModalVisible, setSystemModalVisible] = React.useState(false);
|
||||
const [skillsModalVisible, setSkillsModalVisible] = React.useState(false);
|
||||
const [hooksModalVisible, setHooksModalVisible] = React.useState(false);
|
||||
const [confirmDeleteModalVisible, setConfirmDeleteModalVisible] =
|
||||
React.useState(false);
|
||||
const [confirmStopModalVisible, setConfirmStopModalVisible] =
|
||||
@@ -187,6 +188,12 @@ export function useConversationNameContextMenu({
|
||||
onContextMenuToggle?.(false);
|
||||
};
|
||||
|
||||
const handleShowHooks = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
setHooksModalVisible(true);
|
||||
onContextMenuToggle?.(false);
|
||||
};
|
||||
|
||||
const handleTogglePublic = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -233,6 +240,7 @@ export function useConversationNameContextMenu({
|
||||
handleDisplayCost,
|
||||
handleShowAgentTools,
|
||||
handleShowSkills,
|
||||
handleShowHooks,
|
||||
handleTogglePublic,
|
||||
handleCopyShareLink,
|
||||
shareUrl,
|
||||
@@ -246,6 +254,8 @@ export function useConversationNameContextMenu({
|
||||
setSystemModalVisible,
|
||||
skillsModalVisible,
|
||||
setSkillsModalVisible,
|
||||
hooksModalVisible,
|
||||
setHooksModalVisible,
|
||||
confirmDeleteModalVisible,
|
||||
setConfirmDeleteModalVisible,
|
||||
confirmStopModalVisible,
|
||||
@@ -267,5 +277,11 @@ export function useConversationNameContextMenu({
|
||||
shouldShowDisplayCost: showOptions,
|
||||
shouldShowAgentTools: Boolean(showOptions && systemMessage),
|
||||
shouldShowSkills: Boolean(showOptions && conversationId),
|
||||
shouldShowHooks: Boolean(
|
||||
showOptions &&
|
||||
conversationId &&
|
||||
conversation?.conversation_version === "V1" &&
|
||||
conversationStatus === "RUNNING",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -683,6 +683,8 @@ export enum I18nKey {
|
||||
TOS$ERROR_ACCEPTING = "TOS$ERROR_ACCEPTING",
|
||||
TIPS$CUSTOMIZE_MICROAGENT = "TIPS$CUSTOMIZE_MICROAGENT",
|
||||
CONVERSATION$NO_SKILLS = "CONVERSATION$NO_SKILLS",
|
||||
CONVERSATION$NO_HOOKS = "CONVERSATION$NO_HOOKS",
|
||||
CONVERSATION$SHOW_HOOKS = "CONVERSATION$SHOW_HOOKS",
|
||||
CONVERSATION$FAILED_TO_FETCH_MICROAGENTS = "CONVERSATION$FAILED_TO_FETCH_MICROAGENTS",
|
||||
MICROAGENTS_MODAL$TITLE = "MICROAGENTS_MODAL$TITLE",
|
||||
SKILLS_MODAL$WARNING = "SKILLS_MODAL$WARNING",
|
||||
@@ -1078,6 +1080,28 @@ export enum I18nKey {
|
||||
CONVERSATION$NO_HISTORY_AVAILABLE = "CONVERSATION$NO_HISTORY_AVAILABLE",
|
||||
CONVERSATION$SHARED_CONVERSATION = "CONVERSATION$SHARED_CONVERSATION",
|
||||
CONVERSATION$LINK_COPIED = "CONVERSATION$LINK_COPIED",
|
||||
HOOKS_MODAL$TITLE = "HOOKS_MODAL$TITLE",
|
||||
HOOKS_MODAL$WARNING = "HOOKS_MODAL$WARNING",
|
||||
HOOKS_MODAL$MATCHER = "HOOKS_MODAL$MATCHER",
|
||||
HOOKS_MODAL$COMMANDS = "HOOKS_MODAL$COMMANDS",
|
||||
HOOKS_MODAL$HOOK_COUNT = "HOOKS_MODAL$HOOK_COUNT",
|
||||
HOOKS_MODAL$TYPE = "HOOKS_MODAL$TYPE",
|
||||
HOOKS_MODAL$TIMEOUT = "HOOKS_MODAL$TIMEOUT",
|
||||
HOOKS_MODAL$ASYNC = "HOOKS_MODAL$ASYNC",
|
||||
HOOKS_MODAL$EVENT_PRE_TOOL_USE = "HOOKS_MODAL$EVENT_PRE_TOOL_USE",
|
||||
HOOKS_MODAL$EVENT_POST_TOOL_USE = "HOOKS_MODAL$EVENT_POST_TOOL_USE",
|
||||
HOOKS_MODAL$EVENT_USER_PROMPT_SUBMIT = "HOOKS_MODAL$EVENT_USER_PROMPT_SUBMIT",
|
||||
HOOKS_MODAL$EVENT_SESSION_START = "HOOKS_MODAL$EVENT_SESSION_START",
|
||||
HOOKS_MODAL$EVENT_SESSION_END = "HOOKS_MODAL$EVENT_SESSION_END",
|
||||
HOOKS_MODAL$EVENT_STOP = "HOOKS_MODAL$EVENT_STOP",
|
||||
HOOK$HOOK_LABEL = "HOOK$HOOK_LABEL",
|
||||
HOOK$COMMAND = "HOOK$COMMAND",
|
||||
HOOK$EXIT_CODE = "HOOK$EXIT_CODE",
|
||||
HOOK$BLOCKED_REASON = "HOOK$BLOCKED_REASON",
|
||||
HOOK$CONTEXT = "HOOK$CONTEXT",
|
||||
HOOK$ERROR = "HOOK$ERROR",
|
||||
HOOK$OUTPUT = "HOOK$OUTPUT",
|
||||
HOOK$STDERR = "HOOK$STDERR",
|
||||
COMMON$TYPE_EMAIL_AND_PRESS_SPACE = "COMMON$TYPE_EMAIL_AND_PRESS_SPACE",
|
||||
ORG$INVITE_ORG_MEMBERS = "ORG$INVITE_ORG_MEMBERS",
|
||||
ORG$MANAGE_ORGANIZATION = "ORG$MANAGE_ORGANIZATION",
|
||||
|
||||
@@ -7359,7 +7359,7 @@
|
||||
"es": "Actualmente no hay un plan para este repositorio",
|
||||
"tr": "Şu anda bu depo için bir plan yok"
|
||||
},
|
||||
"SIDEBAR$NAVIGATION_LABEL": {
|
||||
"SIDEBAR$NAVIGATION_LABEL": {
|
||||
"en": "Sidebar navigation",
|
||||
"zh-CN": "侧边栏导航",
|
||||
"zh-TW": "側邊欄導航",
|
||||
@@ -9327,7 +9327,6 @@
|
||||
"de": "Abonnement kündigen",
|
||||
"uk": "Скасувати підписку"
|
||||
},
|
||||
|
||||
"PAYMENT$SUBSCRIPTION_CANCELLED": {
|
||||
"en": "Subscription cancelled successfully",
|
||||
"ja": "サブスクリプションが正常にキャンセルされました",
|
||||
@@ -9344,7 +9343,6 @@
|
||||
"de": "Abonnement erfolgreich gekündigt",
|
||||
"uk": "Підписку успішно скасовано"
|
||||
},
|
||||
|
||||
"PAYMENT$NEXT_BILLING_DATE": {
|
||||
"en": "Next billing date: {{date}}",
|
||||
"ja": "次回請求日: {{date}}",
|
||||
@@ -10529,7 +10527,7 @@
|
||||
"de": "klicken Sie hier für Anweisungen",
|
||||
"uk": "натисніть тут, щоб отримати інструкції"
|
||||
},
|
||||
"BITBUCKET_DATA_CENTER$TOKEN_LABEL": {
|
||||
"BITBUCKET_DATA_CENTER$TOKEN_LABEL": {
|
||||
"en": "Bitbucket Data Center Token",
|
||||
"ja": "Bitbucket Data Centerトークン",
|
||||
"zh-CN": "Bitbucket Data Center令牌",
|
||||
@@ -10929,6 +10927,38 @@
|
||||
"tr": "Bu sohbet için kullanılabilir yetenek bulunamadı.",
|
||||
"uk": "У цій розмові не знайдено доступних навичок."
|
||||
},
|
||||
"CONVERSATION$NO_HOOKS": {
|
||||
"en": "No hooks configured for this conversation.",
|
||||
"ja": "この会話にはフックが設定されていません。",
|
||||
"zh-CN": "此会话未配置钩子。",
|
||||
"zh-TW": "此對話未配置鉤子。",
|
||||
"ko-KR": "이 대화에 구성된 훅이 없습니다.",
|
||||
"no": "Ingen kroker konfigurert for denne samtalen.",
|
||||
"ar": "لم يتم تكوين أي خطافات لهذه المحادثة.",
|
||||
"de": "Keine Hooks für diese Unterhaltung konfiguriert.",
|
||||
"fr": "Aucun hook configuré pour cette conversation.",
|
||||
"it": "Nessun hook configurato per questa conversazione.",
|
||||
"pt": "Nenhum hook configurado para esta conversa.",
|
||||
"es": "No hay hooks configurados para esta conversación.",
|
||||
"tr": "Bu sohbet için yapılandırılmış kanca yok.",
|
||||
"uk": "Для цієї розмови не налаштовано хуків."
|
||||
},
|
||||
"CONVERSATION$SHOW_HOOKS": {
|
||||
"en": "Show Available Hooks",
|
||||
"ja": "利用可能なフックを表示",
|
||||
"zh-CN": "显示可用钩子",
|
||||
"zh-TW": "顯示可用鉤子",
|
||||
"ko-KR": "사용 가능한 훅 표시",
|
||||
"no": "Vis tilgjengelige kroker",
|
||||
"ar": "عرض الخطافات المتاحة",
|
||||
"de": "Verfügbare Hooks anzeigen",
|
||||
"fr": "Afficher les hooks disponibles",
|
||||
"it": "Mostra hook disponibili",
|
||||
"pt": "Mostrar hooks disponíveis",
|
||||
"es": "Mostrar hooks disponibles",
|
||||
"tr": "Kullanılabilir kancaları göster",
|
||||
"uk": "Показати доступні хуки"
|
||||
},
|
||||
"CONVERSATION$FAILED_TO_FETCH_MICROAGENTS": {
|
||||
"en": "Failed to fetch available microagents",
|
||||
"ja": "利用可能なマイクロエージェントの取得に失敗しました",
|
||||
@@ -11777,7 +11807,6 @@
|
||||
"tr": "Git sağlayıcısını bağla",
|
||||
"uk": "Підключити постачальник Git"
|
||||
},
|
||||
|
||||
"TASKS$NO_GIT_PROVIDERS_DESCRIPTION": {
|
||||
"en": "Connect a Git provider to see suggested tasks from your repositories.",
|
||||
"ja": "Gitプロバイダーを接続して、リポジトリからの提案タスクを表示します。",
|
||||
@@ -11794,7 +11823,6 @@
|
||||
"tr": "Depolarınızdan önerilen görevleri görmek için bir Git sağlayıcısı bağlayın.",
|
||||
"uk": "Підключіть постачальник Git, щоб бачити запропоновані завдання з ваших репозиторіїв."
|
||||
},
|
||||
|
||||
"TASKS$NO_GIT_PROVIDERS_CTA": {
|
||||
"en": "Go to Integrations",
|
||||
"ja": "統合へ移動",
|
||||
@@ -17251,6 +17279,358 @@
|
||||
"de": "Link in die Zwischenablage kopiert",
|
||||
"uk": "Посилання скопійовано в буфер обміну"
|
||||
},
|
||||
"HOOKS_MODAL$TITLE": {
|
||||
"en": "Available Hooks",
|
||||
"ja": "利用可能なフック",
|
||||
"zh-CN": "可用钩子",
|
||||
"zh-TW": "可用鉤子",
|
||||
"ko-KR": "사용 가능한 훅",
|
||||
"no": "Tilgjengelige kroker",
|
||||
"ar": "الخطافات المتاحة",
|
||||
"de": "Verfügbare Hooks",
|
||||
"fr": "Hooks disponibles",
|
||||
"it": "Hook disponibili",
|
||||
"pt": "Hooks disponíveis",
|
||||
"es": "Hooks disponibles",
|
||||
"tr": "Kullanılabilir kancalar",
|
||||
"uk": "Доступні хуки"
|
||||
},
|
||||
"HOOKS_MODAL$WARNING": {
|
||||
"en": "Hooks are loaded from your workspace. This view refreshes on demand and may differ from the hooks that were active when the conversation started. Stop and restart the conversation to apply changes.",
|
||||
"ja": "フックはワークスペースから読み込まれます。この表示は要求時にワークスペースから再読み込みするため、会話開始時に有効だったフックと異なる場合があります。変更を適用するには会話を停止して再開してください。",
|
||||
"zh-CN": "Hooks 从工作区读取。本视图会在请求时从工作区刷新,因此可能与会话启动时生效的 hooks 不一致。要应用更改,请停止并重新开始会话。",
|
||||
"zh-TW": "Hooks 從工作區讀取。本視圖會在請求時從工作區重新整理,因此可能與會話啟動時生效的 hooks 不一致。要套用變更,請停止並重新開始會話。",
|
||||
"ko-KR": "훅은 작업공간에서 로드됩니다. 이 화면은 요청 시 작업공간에서 다시 읽어 오므로 대화 시작 시 적용된 훅과 다를 수 있습니다. 변경을 적용하려면 대화를 중지한 뒤 다시 시작하세요.",
|
||||
"no": "Hooks lastes fra arbeidsområdet. Denne visningen leser filen på nytt ved forespørsel og kan derfor avvike fra hookene som var aktive da samtalen startet. Stopp og start samtalen på nytt for å ta i bruk endringer.",
|
||||
"ar": "يتم تحميل الخطافات من مساحة العمل. تقوم هذه الشاشة بإعادة قراءة الملف عند الطلب وقد تختلف عن الخطافات التي كانت فعّالة عند بدء المحادثة. لتطبيق التغييرات، أوقف المحادثة وأعد تشغيلها.",
|
||||
"de": "Hooks werden aus dem Workspace geladen. Diese Ansicht liest die Datei bei Bedarf neu ein und kann daher von den Hooks abweichen, die beim Start der Unterhaltung aktiv waren. Stoppen und starten Sie die Unterhaltung neu, um Änderungen anzuwenden.",
|
||||
"fr": "Les hooks sont chargés depuis votre espace de travail. Cette vue se rafraîchit à la demande depuis l’espace de travail et peut différer des hooks actifs au démarrage de la conversation. Arrêtez puis redémarrez la conversation pour appliquer les modifications.",
|
||||
"it": "Gli hook vengono caricati dal tuo workspace. Questa vista si aggiorna su richiesta dal workspace e può differire dagli hook attivi all’avvio della conversazione. Interrompi e riavvia la conversazione per applicare le modifiche.",
|
||||
"pt": "Os hooks são carregados do seu workspace. Esta visualização é atualizada sob demanda a partir do workspace e pode ser diferente dos hooks que estavam ativos quando a conversa foi iniciada. Pare e reinicie a conversa para aplicar as alterações.",
|
||||
"es": "Los hooks se cargan desde tu espacio de trabajo. Esta vista se actualiza bajo demanda desde el workspace y puede diferir de los hooks que estaban activos cuando comenzó la conversación. Detén y reinicia la conversación para aplicar los cambios.",
|
||||
"tr": "Kancalar çalışma alanınızdan yüklenir. Bu görünüm istek üzerine çalışma alanından yenilenir ve sohbet başlatıldığında etkin olan kancalardan farklı olabilir. Değişiklikleri uygulamak için sohbeti durdurup yeniden başlatın.",
|
||||
"uk": "Хуки завантажуються з вашого робочого простору. Це подання оновлюється з робочого простору на вимогу й може відрізнятися від хуків, які були активні під час запуску розмови. Щоб застосувати зміни, зупиніть і перезапустіть розмову."
|
||||
},
|
||||
"HOOKS_MODAL$MATCHER": {
|
||||
"en": "Matcher",
|
||||
"ja": "マッチャー",
|
||||
"zh-CN": "匹配器",
|
||||
"zh-TW": "匹配器",
|
||||
"ko-KR": "매처",
|
||||
"no": "Matcher",
|
||||
"ar": "المطابق",
|
||||
"de": "Matcher",
|
||||
"fr": "Matcher",
|
||||
"it": "Matcher",
|
||||
"pt": "Matcher",
|
||||
"es": "Matcher",
|
||||
"tr": "Eşleştirici",
|
||||
"uk": "Матчер"
|
||||
},
|
||||
"HOOKS_MODAL$COMMANDS": {
|
||||
"en": "Commands",
|
||||
"ja": "コマンド",
|
||||
"zh-CN": "命令",
|
||||
"zh-TW": "命令",
|
||||
"ko-KR": "명령",
|
||||
"no": "Kommandoer",
|
||||
"ar": "الأوامر",
|
||||
"de": "Befehle",
|
||||
"fr": "Commandes",
|
||||
"it": "Comandi",
|
||||
"pt": "Comandos",
|
||||
"es": "Comandos",
|
||||
"tr": "Komutlar",
|
||||
"uk": "Команди"
|
||||
},
|
||||
"HOOKS_MODAL$HOOK_COUNT": {
|
||||
"en": "{{count}} hook(s)",
|
||||
"ja": "{{count}}個のフック",
|
||||
"zh-CN": "{{count}}个钩子",
|
||||
"zh-TW": "{{count}}個鉤子",
|
||||
"ko-KR": "{{count}}개 훅",
|
||||
"no": "{{count}} krok",
|
||||
"ar": "{{count}} خطاف",
|
||||
"de": "{{count}} Hook",
|
||||
"fr": "{{count}} hook",
|
||||
"it": "{{count}} hook",
|
||||
"pt": "{{count}} hook",
|
||||
"es": "{{count}} hook",
|
||||
"tr": "{{count}} kanca",
|
||||
"uk": "{{count}} хук"
|
||||
},
|
||||
"HOOKS_MODAL$TYPE": {
|
||||
"en": "Type: {{type}}",
|
||||
"ja": "タイプ: {{type}}",
|
||||
"zh-CN": "类型: {{type}}",
|
||||
"zh-TW": "類型: {{type}}",
|
||||
"ko-KR": "유형: {{type}}",
|
||||
"no": "Type: {{type}}",
|
||||
"ar": "النوع: {{type}}",
|
||||
"de": "Typ: {{type}}",
|
||||
"fr": "Type: {{type}}",
|
||||
"it": "Tipo: {{type}}",
|
||||
"pt": "Tipo: {{type}}",
|
||||
"es": "Tipo: {{type}}",
|
||||
"tr": "Tür: {{type}}",
|
||||
"uk": "Тип: {{type}}"
|
||||
},
|
||||
"HOOKS_MODAL$TIMEOUT": {
|
||||
"en": "Timeout: {{timeout}}s",
|
||||
"ja": "タイムアウト: {{timeout}}秒",
|
||||
"zh-CN": "超时: {{timeout}}秒",
|
||||
"zh-TW": "超時: {{timeout}}秒",
|
||||
"ko-KR": "타임아웃: {{timeout}}초",
|
||||
"no": "Tidsavbrudd: {{timeout}}s",
|
||||
"ar": "المهلة: {{timeout}} ثانية",
|
||||
"de": "Timeout: {{timeout}}s",
|
||||
"fr": "Délai: {{timeout}}s",
|
||||
"it": "Timeout: {{timeout}}s",
|
||||
"pt": "Tempo limite: {{timeout}}s",
|
||||
"es": "Tiempo de espera: {{timeout}}s",
|
||||
"tr": "Zaman aşımı: {{timeout}}s",
|
||||
"uk": "Таймаут: {{timeout}}с"
|
||||
},
|
||||
"HOOKS_MODAL$ASYNC": {
|
||||
"en": "Async",
|
||||
"ja": "非同期",
|
||||
"zh-CN": "异步",
|
||||
"zh-TW": "非同步",
|
||||
"ko-KR": "비동기",
|
||||
"no": "Asynkron",
|
||||
"ar": "غير متزامن",
|
||||
"de": "Asynchron",
|
||||
"fr": "Asynchrone",
|
||||
"it": "Asincrono",
|
||||
"pt": "Assíncrono",
|
||||
"es": "Asíncrono",
|
||||
"tr": "Asenkron",
|
||||
"uk": "Асинхронний"
|
||||
},
|
||||
"HOOKS_MODAL$EVENT_PRE_TOOL_USE": {
|
||||
"en": "Pre Tool Use",
|
||||
"ja": "ツール使用前",
|
||||
"zh-CN": "工具使用前",
|
||||
"zh-TW": "工具使用前",
|
||||
"ko-KR": "도구 사용 전",
|
||||
"no": "Før verktøybruk",
|
||||
"ar": "قبل استخدام الأداة",
|
||||
"de": "Vor Werkzeugnutzung",
|
||||
"fr": "Avant utilisation de l'outil",
|
||||
"it": "Prima dell'uso dello strumento",
|
||||
"pt": "Antes do uso da ferramenta",
|
||||
"es": "Antes del uso de la herramienta",
|
||||
"tr": "Araç kullanımı öncesi",
|
||||
"uk": "Перед використанням інструменту"
|
||||
},
|
||||
"HOOKS_MODAL$EVENT_POST_TOOL_USE": {
|
||||
"en": "Post Tool Use",
|
||||
"ja": "ツール使用後",
|
||||
"zh-CN": "工具使用后",
|
||||
"zh-TW": "工具使用後",
|
||||
"ko-KR": "도구 사용 후",
|
||||
"no": "Etter verktøybruk",
|
||||
"ar": "بعد استخدام الأداة",
|
||||
"de": "Nach Werkzeugnutzung",
|
||||
"fr": "Après utilisation de l'outil",
|
||||
"it": "Dopo l'uso dello strumento",
|
||||
"pt": "Após o uso da ferramenta",
|
||||
"es": "Después del uso de la herramienta",
|
||||
"tr": "Araç kullanımı sonrası",
|
||||
"uk": "Після використання інструменту"
|
||||
},
|
||||
"HOOKS_MODAL$EVENT_USER_PROMPT_SUBMIT": {
|
||||
"en": "User Prompt Submit",
|
||||
"ja": "ユーザープロンプト送信",
|
||||
"zh-CN": "用户提示提交",
|
||||
"zh-TW": "使用者提示提交",
|
||||
"ko-KR": "사용자 프롬프트 제출",
|
||||
"no": "Brukerforespørsel sendt",
|
||||
"ar": "إرسال طلب المستخدم",
|
||||
"de": "Benutzeranfrage gesendet",
|
||||
"fr": "Soumission de l'invite utilisateur",
|
||||
"it": "Invio prompt utente",
|
||||
"pt": "Envio de prompt do usuário",
|
||||
"es": "Envío de solicitud del usuario",
|
||||
"tr": "Kullanıcı istemi gönderimi",
|
||||
"uk": "Надсилання запиту користувача"
|
||||
},
|
||||
"HOOKS_MODAL$EVENT_SESSION_START": {
|
||||
"en": "Session Start",
|
||||
"ja": "セッション開始",
|
||||
"zh-CN": "会话开始",
|
||||
"zh-TW": "會話開始",
|
||||
"ko-KR": "세션 시작",
|
||||
"no": "Øktstart",
|
||||
"ar": "بدء الجلسة",
|
||||
"de": "Sitzungsstart",
|
||||
"fr": "Début de session",
|
||||
"it": "Inizio sessione",
|
||||
"pt": "Início da sessão",
|
||||
"es": "Inicio de sesión",
|
||||
"tr": "Oturum başlangıcı",
|
||||
"uk": "Початок сесії"
|
||||
},
|
||||
"HOOKS_MODAL$EVENT_SESSION_END": {
|
||||
"en": "Session End",
|
||||
"ja": "セッション終了",
|
||||
"zh-CN": "会话结束",
|
||||
"zh-TW": "會話結束",
|
||||
"ko-KR": "세션 종료",
|
||||
"no": "Øktslutt",
|
||||
"ar": "نهاية الجلسة",
|
||||
"de": "Sitzungsende",
|
||||
"fr": "Fin de session",
|
||||
"it": "Fine sessione",
|
||||
"pt": "Fim da sessão",
|
||||
"es": "Fin de sesión",
|
||||
"tr": "Oturum sonu",
|
||||
"uk": "Кінець сесії"
|
||||
},
|
||||
"HOOKS_MODAL$EVENT_STOP": {
|
||||
"en": "Stop",
|
||||
"ja": "停止",
|
||||
"zh-CN": "停止",
|
||||
"zh-TW": "停止",
|
||||
"ko-KR": "중지",
|
||||
"no": "Stopp",
|
||||
"ar": "إيقاف",
|
||||
"de": "Stopp",
|
||||
"fr": "Arrêt",
|
||||
"it": "Stop",
|
||||
"pt": "Parar",
|
||||
"es": "Detener",
|
||||
"tr": "Durdur",
|
||||
"uk": "Зупинка"
|
||||
},
|
||||
"HOOK$HOOK_LABEL": {
|
||||
"en": "Hook",
|
||||
"ja": "フック",
|
||||
"zh-CN": "钩子",
|
||||
"zh-TW": "鈎子",
|
||||
"ko-KR": "훅",
|
||||
"no": "Krok",
|
||||
"ar": "خطاف",
|
||||
"de": "Hook",
|
||||
"fr": "Crochet",
|
||||
"it": "Hook",
|
||||
"pt": "Hook",
|
||||
"es": "Gancho",
|
||||
"tr": "Kanca",
|
||||
"uk": "Хук"
|
||||
},
|
||||
"HOOK$COMMAND": {
|
||||
"en": "Command",
|
||||
"ja": "コマンド",
|
||||
"zh-CN": "命令",
|
||||
"zh-TW": "命令",
|
||||
"ko-KR": "명령",
|
||||
"no": "Kommando",
|
||||
"ar": "أمر",
|
||||
"de": "Befehl",
|
||||
"fr": "Commande",
|
||||
"it": "Comando",
|
||||
"pt": "Comando",
|
||||
"es": "Comando",
|
||||
"tr": "Komut",
|
||||
"uk": "Команда"
|
||||
},
|
||||
"HOOK$EXIT_CODE": {
|
||||
"en": "Exit code",
|
||||
"ja": "終了コード",
|
||||
"zh-CN": "退出码",
|
||||
"zh-TW": "退出碼",
|
||||
"ko-KR": "종료 코드",
|
||||
"no": "Avslutningskode",
|
||||
"ar": "رمز الخروج",
|
||||
"de": "Exit-Code",
|
||||
"fr": "Code de sortie",
|
||||
"it": "Codice di uscita",
|
||||
"pt": "Código de saída",
|
||||
"es": "Código de salida",
|
||||
"tr": "Çıkış kodu",
|
||||
"uk": "Код виходу"
|
||||
},
|
||||
"HOOK$BLOCKED_REASON": {
|
||||
"en": "Blocked reason",
|
||||
"ja": "ブロック理由",
|
||||
"zh-CN": "阻止原因",
|
||||
"zh-TW": "阻止原因",
|
||||
"ko-KR": "차단 이유",
|
||||
"no": "Blokkert grunn",
|
||||
"ar": "سبب الحظر",
|
||||
"de": "Blockierungsgrund",
|
||||
"fr": "Raison du blocage",
|
||||
"it": "Motivo del blocco",
|
||||
"pt": "Motivo do bloqueio",
|
||||
"es": "Motivo del bloqueo",
|
||||
"tr": "Engelleme nedeni",
|
||||
"uk": "Причина блокування"
|
||||
},
|
||||
"HOOK$CONTEXT": {
|
||||
"en": "Context",
|
||||
"ja": "コンテキスト",
|
||||
"zh-CN": "上下文",
|
||||
"zh-TW": "上下文",
|
||||
"ko-KR": "컨텍스트",
|
||||
"no": "Kontekst",
|
||||
"ar": "سياق",
|
||||
"de": "Kontext",
|
||||
"fr": "Contexte",
|
||||
"it": "Contesto",
|
||||
"pt": "Contexto",
|
||||
"es": "Contexto",
|
||||
"tr": "Bağlam",
|
||||
"uk": "Контекст"
|
||||
},
|
||||
"HOOK$ERROR": {
|
||||
"en": "Error",
|
||||
"ja": "エラー",
|
||||
"zh-CN": "错误",
|
||||
"zh-TW": "錯誤",
|
||||
"ko-KR": "오류",
|
||||
"no": "Feil",
|
||||
"ar": "خطأ",
|
||||
"de": "Fehler",
|
||||
"fr": "Erreur",
|
||||
"it": "Errore",
|
||||
"pt": "Erro",
|
||||
"es": "Error",
|
||||
"tr": "Hata",
|
||||
"uk": "Помилка"
|
||||
},
|
||||
"HOOK$OUTPUT": {
|
||||
"en": "Output",
|
||||
"ja": "出力",
|
||||
"zh-CN": "输出",
|
||||
"zh-TW": "輸出",
|
||||
"ko-KR": "출력",
|
||||
"no": "Utdata",
|
||||
"ar": "الإخراج",
|
||||
"de": "Ausgabe",
|
||||
"fr": "Sortie",
|
||||
"it": "Output",
|
||||
"pt": "Saída",
|
||||
"es": "Salida",
|
||||
"tr": "Çıktı",
|
||||
"uk": "Вивід"
|
||||
},
|
||||
"HOOK$STDERR": {
|
||||
"en": "Stderr",
|
||||
"ja": "標準エラー",
|
||||
"zh-CN": "标准错误",
|
||||
"zh-TW": "標準錯誤",
|
||||
"ko-KR": "표준 오류",
|
||||
"no": "Standardfeil",
|
||||
"ar": "خطأ قياسي",
|
||||
"de": "Standardfehler",
|
||||
"fr": "Erreur standard",
|
||||
"it": "Errore standard",
|
||||
"pt": "Erro padrão",
|
||||
"es": "Error estándar",
|
||||
"tr": "Standart hata",
|
||||
"uk": "Стандартна помилка"
|
||||
},
|
||||
"COMMON$TYPE_EMAIL_AND_PRESS_SPACE": {
|
||||
"en": "Type email and press Space",
|
||||
"ja": "メールアドレスを入力してスペースキーを押してください",
|
||||
|
||||
@@ -21,7 +21,7 @@ export type OpenHandsEventType =
|
||||
| "task_tracking"
|
||||
| "user_rejected";
|
||||
|
||||
export type OpenHandsSourceType = "agent" | "user" | "environment";
|
||||
export type OpenHandsSourceType = "agent" | "user" | "environment" | "hook";
|
||||
|
||||
interface OpenHandsBaseEvent {
|
||||
id: number;
|
||||
|
||||
@@ -53,7 +53,7 @@ export type EventID = string;
|
||||
export type ToolCallID = string;
|
||||
|
||||
// Source type for events
|
||||
export type SourceType = "agent" | "user" | "environment";
|
||||
export type SourceType = "agent" | "user" | "environment" | "hook";
|
||||
|
||||
// Security risk levels
|
||||
export enum SecurityRisk {
|
||||
|
||||
100
frontend/src/types/v1/core/events/hook-execution-event.ts
Normal file
100
frontend/src/types/v1/core/events/hook-execution-event.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { BaseEvent } from "../base/event";
|
||||
|
||||
/**
|
||||
* Hook event types supported by the system
|
||||
*/
|
||||
export type HookEventType =
|
||||
| "PreToolUse"
|
||||
| "PostToolUse"
|
||||
| "UserPromptSubmit"
|
||||
| "SessionStart"
|
||||
| "SessionEnd"
|
||||
| "Stop";
|
||||
|
||||
/**
|
||||
* HookExecutionEvent - emitted when a hook script executes
|
||||
*
|
||||
* Provides observability into hook execution for PreToolUse, PostToolUse,
|
||||
* UserPromptSubmit, SessionStart, SessionEnd, and Stop hooks.
|
||||
*/
|
||||
export interface HookExecutionEvent extends BaseEvent {
|
||||
/**
|
||||
* Discriminator field for type guards
|
||||
*/
|
||||
kind: "HookExecutionEvent";
|
||||
|
||||
/**
|
||||
* The source is always "hook" for hook execution events
|
||||
*/
|
||||
source: "hook";
|
||||
|
||||
/**
|
||||
* Type of hook that was executed
|
||||
*/
|
||||
hook_event_type: HookEventType;
|
||||
|
||||
/**
|
||||
* The command that was executed
|
||||
*/
|
||||
hook_command: string;
|
||||
|
||||
/**
|
||||
* Whether the hook executed successfully
|
||||
*/
|
||||
success: boolean;
|
||||
|
||||
/**
|
||||
* Whether the hook blocked the action
|
||||
*/
|
||||
blocked: boolean;
|
||||
|
||||
/**
|
||||
* Exit code from the hook script (null if not applicable)
|
||||
*/
|
||||
exit_code: number | null;
|
||||
|
||||
/**
|
||||
* Reason provided by the hook for blocking (if blocked)
|
||||
*/
|
||||
reason: string | null;
|
||||
|
||||
/**
|
||||
* Name of the tool (for PreToolUse/PostToolUse hooks)
|
||||
*/
|
||||
tool_name: string | null;
|
||||
|
||||
/**
|
||||
* ID of the related action event (for tool hooks)
|
||||
*/
|
||||
action_id: string | null;
|
||||
|
||||
/**
|
||||
* ID of the related message event (for UserPromptSubmit hooks)
|
||||
*/
|
||||
message_id: string | null;
|
||||
|
||||
/**
|
||||
* Standard output from the hook script
|
||||
*/
|
||||
stdout: string | null;
|
||||
|
||||
/**
|
||||
* Standard error from the hook script
|
||||
*/
|
||||
stderr: string | null;
|
||||
|
||||
/**
|
||||
* Error message if the hook failed
|
||||
*/
|
||||
error: string | null;
|
||||
|
||||
/**
|
||||
* Additional context provided by the hook
|
||||
*/
|
||||
additional_context: string | null;
|
||||
|
||||
/**
|
||||
* Input data that was passed to the hook
|
||||
*/
|
||||
hook_input: Record<string, unknown> | null;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
export * from "./action-event";
|
||||
export * from "./condensation-event";
|
||||
export * from "./conversation-state-event";
|
||||
export * from "./hook-execution-event";
|
||||
export * from "./message-event";
|
||||
export * from "./observation-event";
|
||||
export * from "./pause-event";
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
CondensationSummaryEvent,
|
||||
ConversationStateUpdateEvent,
|
||||
ConversationErrorEvent,
|
||||
HookExecutionEvent,
|
||||
PauseEvent,
|
||||
} from "./events/index";
|
||||
|
||||
@@ -26,6 +27,8 @@ export type OpenHandsEvent =
|
||||
| UserRejectObservation
|
||||
| AgentErrorEvent
|
||||
| SystemPromptEvent
|
||||
// Hook events
|
||||
| HookExecutionEvent
|
||||
// Conversation management events
|
||||
| CondensationEvent
|
||||
| CondensationRequestEvent
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
ConversationStateUpdateEventStats,
|
||||
ConversationErrorEvent,
|
||||
} from "./core/events/conversation-state-event";
|
||||
import { HookExecutionEvent } from "./core/events/hook-execution-event";
|
||||
import { SystemPromptEvent } from "./core/events/system-event";
|
||||
import type { OpenHandsParsedEvent } from "../core/index";
|
||||
|
||||
@@ -42,7 +43,8 @@ export function isBaseEvent(value: unknown): value is BaseEvent {
|
||||
typeof value.source === "string" &&
|
||||
(value.source === "agent" ||
|
||||
value.source === "user" ||
|
||||
value.source === "environment")
|
||||
value.source === "environment" ||
|
||||
value.source === "hook")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -191,6 +193,14 @@ export const isConversationErrorEvent = (
|
||||
): event is ConversationErrorEvent =>
|
||||
"kind" in event && event.kind === "ConversationErrorEvent";
|
||||
|
||||
/**
|
||||
* Type guard function to check if an event is a hook execution event
|
||||
*/
|
||||
export const isHookExecutionEvent = (
|
||||
event: OpenHandsEvent,
|
||||
): event is HookExecutionEvent =>
|
||||
"kind" in event && event.kind === "HookExecutionEvent";
|
||||
|
||||
// =============================================================================
|
||||
// TEMPORARY COMPATIBILITY TYPE GUARDS
|
||||
// These will be removed once we fully migrate to V1 events
|
||||
|
||||
Reference in New Issue
Block a user