From 04354e833fa2d4799a1268cc67d56f43f3622a06 Mon Sep 17 00:00:00 2001 From: viettranx Date: Thu, 16 Apr 2026 07:57:54 +0700 Subject: [PATCH] feat(hooks/ui): wider form modal + dialog test panel + collapsible beta card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-reported polish on the hooks page. - Form modal: widen from sm:max-w-lg → sm:max-w-3xl lg:max-w-4xl. Pair Event/Scope and Matcher/if_expr into 2-col grids; collapse timeout/on_timeout/priority/enabled into a single 4-col row on lg+. Reads the same on mobile (single column). - Test panel: replaced the floating bottom-right drawer with a centered Dialog (sm:max-w-4xl). Two-column layout — left is the sample event form with per-field hints, right is the result with empty-state illustration, prominent decision pill, separated reason / stdout / stderr / error / updatedInput sections. Run button is now full-width with a Play icon. New i18n strings cover all hints + empty/error states (en/vi/zh). - Beta info card: top-of-page collapsible explainer with three "how it works" points (event → handler → test). Persists collapsed state in localStorage so dismissed users don't see it expand on every reload. i18n in en/vi/zh under the new beta.* namespace. --- ui/web/src/i18n/locales/en/hooks.json | 27 ++- ui/web/src/i18n/locales/vi/hooks.json | 27 ++- ui/web/src/i18n/locales/zh/hooks.json | 27 ++- .../pages/hooks/components/beta-info-card.tsx | 93 ++++++++ .../hooks/components/hook-form-dialog.tsx | 131 ++++++----- .../hooks/components/hook-test-panel.tsx | 219 +++++++++++------- ui/web/src/pages/hooks/hooks-page.tsx | 28 ++- 7 files changed, 380 insertions(+), 172 deletions(-) create mode 100644 ui/web/src/pages/hooks/components/beta-info-card.tsx diff --git a/ui/web/src/i18n/locales/en/hooks.json b/ui/web/src/i18n/locales/en/hooks.json index 2e41bc48..e0339913 100644 --- a/ui/web/src/i18n/locales/en/hooks.json +++ b/ui/web/src/i18n/locales/en/hooks.json @@ -3,6 +3,18 @@ "subtitle": "Lifecycle hooks: block, observe, inject context across agent events", "empty": "No hooks configured yet", "nav": "Hooks", + "beta": { + "title": "Hooks (Beta)", + "badge": "Beta", + "description": "Hooks let you intercept agent lifecycle events to allow, block, or rewrite what happens. Currently in beta — expect rough edges and watch your audit log.", + "howItWorksTitle1": "1. Pick an event", + "howItWorksBody1": "Each hook fires on one event such as user_prompt_submit, pre_tool_use, or stop. Blocking events wait for your decision before continuing.", + "howItWorksTitle2": "2. Choose a handler", + "howItWorksBody2": "Script runs sandboxed JavaScript. HTTP posts to your webhook. Prompt asks an LLM. Built-in hooks (System badge) ship with GoClaw and only the enabled toggle is editable.", + "howItWorksTitle3": "3. Test before enabling", + "howItWorksBody3": "Use Run hook with a sample event to preview the decision, the duration, and any input mutations — nothing touches a real session until enabled.", + "collapse": "Hide" + }, "actions": { "create": "Create hook", "edit": "Edit", @@ -65,11 +77,20 @@ "test": { "title": "Test hook", "sampleEvent": "Sample event", + "sampleEventHint": "Craft a fake event to fire the hook against — nothing here touches a real session.", "toolName": "Tool name", + "toolNameHint": "Used by pre/post_tool_use matchers (e.g. bash, edit, web_search).", "toolInput": "Tool input (JSON)", - "rawInput": "Raw input", - "fire": "Fire", - "firing": "Firing\u2026", + "toolInputHint": "Arguments passed to the tool. Must be valid JSON.", + "rawInput": "Raw user message", + "rawInputPlaceholder": "Optional — the user's prompt text", + "rawInputHint": "Used by user_prompt_submit hooks. Leave blank for tool events.", + "parseError": "Tool input must be valid JSON", + "resultTitle": "Result", + "resultHint": "What the dispatcher returned, including any input mutations from builtin hooks.", + "emptyState": "Run the hook to see how it would respond.", + "fire": "Run hook", + "firing": "Running\u2026", "decision": "Decision", "duration": "Duration", "reason": "Reason", diff --git a/ui/web/src/i18n/locales/vi/hooks.json b/ui/web/src/i18n/locales/vi/hooks.json index 8d3be58f..1e164346 100644 --- a/ui/web/src/i18n/locales/vi/hooks.json +++ b/ui/web/src/i18n/locales/vi/hooks.json @@ -3,6 +3,18 @@ "subtitle": "Hook vòng đời: chặn, quan sát, tiêm context qua các sự kiện agent", "empty": "Chưa có hook nào được cấu hình", "nav": "Hooks", + "beta": { + "title": "Hooks (Beta)", + "badge": "Beta", + "description": "Hooks cho phép bạn chen vào các sự kiện vòng đời của agent để cho phép, chặn hoặc viết lại nội dung. Đang trong giai đoạn beta — có thể còn lỗi, hãy theo dõi audit log.", + "howItWorksTitle1": "1. Chọn sự kiện", + "howItWorksBody1": "Mỗi hook gắn với một sự kiện như user_prompt_submit, pre_tool_use, hoặc stop. Sự kiện blocking sẽ chờ quyết định của bạn trước khi tiếp tục.", + "howItWorksTitle2": "2. Chọn loại xử lý", + "howItWorksBody2": "Script chạy JavaScript trong sandbox. HTTP gọi webhook của bạn. Prompt hỏi LLM. Hook dựng sẵn (badge Hệ thống) do GoClaw cung cấp — chỉ chỉnh được toggle bật/tắt.", + "howItWorksTitle3": "3. Test trước khi bật", + "howItWorksBody3": "Dùng Run hook với sự kiện mẫu để xem trước quyết định, thời gian chạy và mutation đầu vào — không động vào phiên thật cho tới khi bật.", + "collapse": "Ẩn" + }, "actions": { "create": "Tạo hook", "edit": "Sửa", @@ -65,11 +77,20 @@ "test": { "title": "Kiểm tra hook", "sampleEvent": "Sự kiện mẫu", + "sampleEventHint": "Tạo sự kiện giả để chạy thử hook — không ảnh hưởng phiên thật nào.", "toolName": "Tên công cụ", + "toolNameHint": "Dùng cho matcher của pre/post_tool_use (ví dụ: bash, edit, web_search).", "toolInput": "Đầu vào công cụ (JSON)", - "rawInput": "Đầu vào thô", - "fire": "Kích hoạt", - "firing": "Đang kích hoạt\u2026", + "toolInputHint": "Tham số truyền vào công cụ. Phải là JSON hợp lệ.", + "rawInput": "Tin nhắn người dùng", + "rawInputPlaceholder": "Tùy chọn — nội dung prompt của user", + "rawInputHint": "Dùng cho hook user_prompt_submit. Để trống nếu là sự kiện công cụ.", + "parseError": "Đầu vào công cụ phải là JSON hợp lệ", + "resultTitle": "Kết quả", + "resultHint": "Phản hồi từ dispatcher, bao gồm cả mutation đầu vào từ hook builtin.", + "emptyState": "Bấm Run để xem hook phản hồi thế nào.", + "fire": "Chạy hook", + "firing": "Đang chạy\u2026", "decision": "Quyết định", "duration": "Thời gian", "reason": "Lý do", diff --git a/ui/web/src/i18n/locales/zh/hooks.json b/ui/web/src/i18n/locales/zh/hooks.json index 928fd681..d7b1e068 100644 --- a/ui/web/src/i18n/locales/zh/hooks.json +++ b/ui/web/src/i18n/locales/zh/hooks.json @@ -3,6 +3,18 @@ "subtitle": "生命周期钩子:在 Agent 事件中拦截、观察、注入上下文", "empty": "暂无已配置的钩子", "nav": "Hooks", + "beta": { + "title": "Hooks(公测)", + "badge": "Beta", + "description": "钩子让你拦截 Agent 生命周期事件以放行、阻止或改写其行为。当前处于公测阶段 — 可能存在问题,请关注审计日志。", + "howItWorksTitle1": "1. 选择事件", + "howItWorksBody1": "每个钩子绑定一个事件,例如 user_prompt_submit、pre_tool_use 或 stop。阻塞型事件会等待你的决策后再继续。", + "howItWorksTitle2": "2. 选择处理器", + "howItWorksBody2": "Script 在沙箱中运行 JavaScript。HTTP 调用你的 Webhook。Prompt 询问 LLM。内置钩子(系统徽章)由 GoClaw 提供 — 仅可切换启用状态。", + "howItWorksTitle3": "3. 启用前先测试", + "howItWorksBody3": "使用「运行钩子」配合示例事件预览决策、耗时及输入修改 — 启用前不会触及任何真实会话。", + "collapse": "收起" + }, "actions": { "create": "创建钩子", "edit": "编辑", @@ -65,11 +77,20 @@ "test": { "title": "测试钩子", "sampleEvent": "示例事件", + "sampleEventHint": "构造一个虚拟事件来运行钩子 — 不会触及任何真实会话。", "toolName": "工具名称", + "toolNameHint": "用于 pre/post_tool_use 的匹配器(如 bash、edit、web_search)。", "toolInput": "工具输入(JSON)", - "rawInput": "原始输入", - "fire": "触发", - "firing": "触发中\u2026", + "toolInputHint": "传递给工具的参数,必须是有效 JSON。", + "rawInput": "用户原始消息", + "rawInputPlaceholder": "可选 — 用户的提示文本", + "rawInputHint": "用于 user_prompt_submit 钩子。工具事件请留空。", + "parseError": "工具输入必须是有效的 JSON", + "resultTitle": "结果", + "resultHint": "调度器的返回结果,包括来自内置钩子的输入修改。", + "emptyState": "运行钩子以查看响应。", + "fire": "运行钩子", + "firing": "运行中\u2026", "decision": "决策", "duration": "耗时", "reason": "原因", diff --git a/ui/web/src/pages/hooks/components/beta-info-card.tsx b/ui/web/src/pages/hooks/components/beta-info-card.tsx new file mode 100644 index 00000000..86089e33 --- /dev/null +++ b/ui/web/src/pages/hooks/components/beta-info-card.tsx @@ -0,0 +1,93 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { ChevronDown, ChevronUp, Sparkles } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +const LS_KEY = "goclaw:hooks:beta-card-collapsed"; + +function loadCollapsed(): boolean { + try { + return localStorage.getItem(LS_KEY) === "1"; + } catch { + return false; + } +} + +function saveCollapsed(v: boolean) { + try { + localStorage.setItem(LS_KEY, v ? "1" : "0"); + } catch { + // ignore quota/private-mode errors + } +} + +// BetaInfoCard explains what the Hooks feature does for end users. Renders at +// the top of the hooks list page. State persists per-browser via localStorage +// so dismissed users don't see it expand on every reload. +export function BetaInfoCard() { + const { t } = useTranslation("hooks"); + const [collapsed, setCollapsed] = useState(loadCollapsed); + + const toggle = () => { + const next = !collapsed; + setCollapsed(next); + saveCollapsed(next); + }; + + return ( +
+ + + {!collapsed && ( +
+

{t("beta.description")}

+ +
+ + + +
+ +
+ +
+
+ )} +
+ ); +} + +function BetaPoint({ title, body }: { title: string; body: string }) { + return ( +
+

{title}

+

{body}

+
+ ); +} diff --git a/ui/web/src/pages/hooks/components/hook-form-dialog.tsx b/ui/web/src/pages/hooks/components/hook-form-dialog.tsx index 3faebd69..c379b2b2 100644 --- a/ui/web/src/pages/hooks/components/hook-form-dialog.tsx +++ b/ui/web/src/pages/hooks/components/hook-form-dialog.tsx @@ -104,7 +104,7 @@ export function HookFormDialog({ open, onOpenChange, onSubmit, initial }: HookFo return ( - + {initial ? t("form.title_edit") : t("form.title_create")} @@ -116,21 +116,39 @@ export function HookFormDialog({ open, onOpenChange, onSubmit, initial }: HookFo )} - {/* Event */} -
- - ( - - )} /> + {/* Event + Scope side-by-side on wider viewports */} +
+
+ + ( + + )} /> +
+ {/* Scope — global hidden for non-master callers (advisory UI hint; backend enforces). */} +
+ + ( + + )} /> +
{/* Handler type — `command` intentionally absent post-Wave-1. Lite users keep @@ -142,7 +160,7 @@ export function HookFormDialog({ open, onOpenChange, onSubmit, initial }: HookFo {(["script", "http", "prompt"] as const).map((ht) => ( @@ -157,48 +175,31 @@ export function HookFormDialog({ open, onOpenChange, onSubmit, initial }: HookFo )} />
- {/* Scope — global hidden for non-master callers (advisory UI hint; backend enforces). */} -
- - ( - - )} /> -
- - {/* Matcher */} -
- - - {errors.matcher - ?

{errors.matcher.message}

- :

{t("form.matcherHint")}

- } -
- - {/* if_expr */} -
- - -

{t("form.ifExprHint")}

+ {/* Matcher + if_expr side-by-side */} +
+
+ + + {errors.matcher + ?

{errors.matcher.message}

+ :

{t("form.matcherHint")}

+ } +
+
+ + +

{t("form.ifExprHint")}

+
{/* Handler-specific sub-forms */} @@ -303,8 +304,10 @@ export function HookFormDialog({ open, onOpenChange, onSubmit, initial }: HookFo
)} - {/* Timeout + on_timeout */} -
+ {/* Timeout / on_timeout / priority on one row when wide, stacks on mobile. + Enabled toggle stays actionable on every row (only field user can + change for builtin hooks). */} +
)} />
-
- - {/* Priority + enabled — enabled stays actionable even on builtin rows. */} -
-
+
-

{t("test.sampleEvent")}

+
+ {/* ── Left: sample event input ─────────────────────────────── */} +
+
+
+

{t("test.sampleEvent")}

+

{t("test.sampleEventHint")}

+
+
- {/* Tool name */} -
- - setToolName(e.target.value)} - placeholder="bash" - className="text-base md:text-sm font-mono" - /> -
+
+ + setToolName(e.target.value)} + placeholder="bash" + className="text-base md:text-sm font-mono" + /> +

{t("test.toolNameHint")}

+
- {/* Tool input */} -
- -