diff --git a/internal/gateway/methods/hooks.go b/internal/gateway/methods/hooks.go index 4a3f28c2..9642cee1 100644 --- a/internal/gateway/methods/hooks.go +++ b/internal/gateway/methods/hooks.go @@ -239,6 +239,9 @@ func (m *HookMethods) handleUpdate(ctx context.Context, client *gateway.Client, // columns). Used pre-validation so the Validate() contract runs on the // full, merged config exactly as it will exist after Update commits. func applyHookPatch(cur hooks.HookConfig, p map[string]any) hooks.HookConfig { + if v, ok := p["name"].(string); ok { + cur.Name = v + } if v, ok := p["event"].(string); ok && v != "" { cur.Event = hooks.HookEvent(v) } diff --git a/internal/hooks/builtin/seed.go b/internal/hooks/builtin/seed.go index 94887896..bc1c0b7b 100644 --- a/internal/hooks/builtin/seed.go +++ b/internal/hooks/builtin/seed.go @@ -94,6 +94,7 @@ func reconcile( cfg := hooks.HookConfig{ ID: id, TenantID: hooks.SentinelTenantID, + Name: s.ID, Event: hooks.HookEvent(event), HandlerType: hooks.HandlerScript, Scope: hooks.Scope(s.Scope), diff --git a/internal/hooks/types.go b/internal/hooks/types.go index 6fe51da5..98ca67e7 100644 --- a/internal/hooks/types.go +++ b/internal/hooks/types.go @@ -164,6 +164,7 @@ type HookConfig struct { Event HookEvent `json:"event"` HandlerType HandlerType `json:"handler_type"` Scope Scope `json:"scope"` + Name string `json:"name,omitempty"` // Config holds handler-specific options (command path, HTTP URL, prompt template). Config map[string]any `json:"config"` Matcher string `json:"matcher,omitempty"` diff --git a/internal/store/pg/hooks.go b/internal/store/pg/hooks.go index 871f8153..ce8da980 100644 --- a/internal/store/pg/hooks.go +++ b/internal/store/pg/hooks.go @@ -67,24 +67,27 @@ func (s *PGHookStore) Create(ctx context.Context, cfg hooks.HookConfig) (uuid.UU tid = tenantIDForInsert(ctx) } - var matcher, ifExpr *string + var matcher, ifExpr, name *string if cfg.Matcher != "" { matcher = &cfg.Matcher } if cfg.IfExpr != "" { ifExpr = &cfg.IfExpr } + if cfg.Name != "" { + name = &cfg.Name + } _, err = s.db.ExecContext(ctx, ` INSERT INTO agent_hooks (id, tenant_id, agent_id, scope, event, handler_type, config, matcher, if_expr, timeout_ms, on_timeout, - priority, enabled, version, source, metadata, created_by, + priority, enabled, version, source, metadata, name, created_by, created_at, updated_at) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,1,$14,$15,$16,$17,$17)`, + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,1,$14,$15,$16,$17,$18,$18)`, id, tid, cfg.AgentID, string(cfg.Scope), string(cfg.Event), string(cfg.HandlerType), cfgJSON, matcher, ifExpr, cfg.TimeoutMS, string(cfg.OnTimeout), - cfg.Priority, cfg.Enabled, string(cfg.Source), metaJSON, cfg.CreatedBy, + cfg.Priority, cfg.Enabled, string(cfg.Source), metaJSON, name, cfg.CreatedBy, now, ) if err != nil { @@ -100,7 +103,7 @@ func (s *PGHookStore) GetByID(ctx context.Context, id uuid.UUID) (*hooks.HookCon q := ` SELECT id, tenant_id, agent_id, scope, event, handler_type, config, matcher, if_expr, timeout_ms, on_timeout, - priority, enabled, version, source, metadata, created_by, + priority, enabled, version, source, metadata, name, created_by, created_at, updated_at FROM agent_hooks WHERE id = $1` args := []any{id} @@ -132,7 +135,7 @@ func (s *PGHookStore) GetByID(ctx context.Context, id uuid.UUID) (*hooks.HookCon func (s *PGHookStore) List(ctx context.Context, filter hooks.ListFilter) ([]hooks.HookConfig, error) { q := `SELECT id, tenant_id, agent_id, scope, event, handler_type, config, matcher, if_expr, timeout_ms, on_timeout, - priority, enabled, version, source, metadata, created_by, + priority, enabled, version, source, metadata, name, created_by, created_at, updated_at FROM agent_hooks WHERE 1=1` var args []any n := 1 @@ -342,7 +345,7 @@ func (s *PGHookStore) ResolveForEvent(ctx context.Context, event hooks.Event) ([ rows, err := s.db.QueryContext(ctx, ` SELECT id, tenant_id, agent_id, scope, event, handler_type, config, matcher, if_expr, timeout_ms, on_timeout, - priority, enabled, version, source, metadata, created_by, + priority, enabled, version, source, metadata, name, created_by, created_at, updated_at FROM agent_hooks WHERE enabled = TRUE AND event = $1 @@ -449,7 +452,7 @@ func scanHookPGRow(row pgRowScanner) (*hooks.HookConfig, error) { scope, event string handlerType, onTimeout string source string - matcher, ifExpr sql.NullString + matcher, ifExpr, name sql.NullString cfgJSON, metaJSON []byte ) err := row.Scan( @@ -458,7 +461,7 @@ func scanHookPGRow(row pgRowScanner) (*hooks.HookConfig, error) { &cfgJSON, &matcher, &ifExpr, &cfg.TimeoutMS, &onTimeout, &cfg.Priority, &cfg.Enabled, &cfg.Version, - &source, &metaJSON, &createdBy, + &source, &metaJSON, &name, &createdBy, &cfg.CreatedAt, &cfg.UpdatedAt, ) if err != nil { @@ -479,6 +482,9 @@ func scanHookPGRow(row pgRowScanner) (*hooks.HookConfig, error) { if ifExpr.Valid { cfg.IfExpr = ifExpr.String } + if name.Valid { + cfg.Name = name.String + } if len(cfgJSON) > 0 { _ = json.Unmarshal(cfgJSON, &cfg.Config) diff --git a/internal/store/sqlitestore/hooks.go b/internal/store/sqlitestore/hooks.go index f99da3fc..bd325e1b 100644 --- a/internal/store/sqlitestore/hooks.go +++ b/internal/store/sqlitestore/hooks.go @@ -68,7 +68,7 @@ func (s *SqliteHookStore) Create(ctx context.Context, cfg hooks.HookConfig) (uui tid = tenantIDForInsert(ctx) } - var agentID, createdBy, matcher, ifExpr *string + var agentID, createdBy, matcher, ifExpr, name *string if cfg.AgentID != nil { s := cfg.AgentID.String() agentID = &s @@ -83,20 +83,23 @@ func (s *SqliteHookStore) Create(ctx context.Context, cfg hooks.HookConfig) (uui if cfg.IfExpr != "" { ifExpr = &cfg.IfExpr } + if cfg.Name != "" { + name = &cfg.Name + } _, err = s.db.ExecContext(ctx, ` INSERT INTO agent_hooks (id, tenant_id, agent_id, scope, event, handler_type, config, matcher, if_expr, timeout_ms, on_timeout, - priority, enabled, version, source, metadata, created_by, + priority, enabled, version, source, metadata, name, created_by, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?,?,?)`, + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?,?,?,?)`, id.String(), tid.String(), agentID, string(cfg.Scope), string(cfg.Event), string(cfg.HandlerType), string(cfgJSON), matcher, ifExpr, cfg.TimeoutMS, string(cfg.OnTimeout), cfg.Priority, cfg.Enabled, - string(cfg.Source), string(metaJSON), createdBy, + string(cfg.Source), string(metaJSON), name, createdBy, now, now, ) if err != nil { @@ -112,7 +115,7 @@ func (s *SqliteHookStore) GetByID(ctx context.Context, id uuid.UUID) (*hooks.Hoo q := ` SELECT id, tenant_id, agent_id, scope, event, handler_type, config, matcher, if_expr, timeout_ms, on_timeout, - priority, enabled, version, source, metadata, created_by, + priority, enabled, version, source, metadata, name, created_by, created_at, updated_at FROM agent_hooks WHERE id = ?` args := []any{id.String()} @@ -143,7 +146,7 @@ func (s *SqliteHookStore) GetByID(ctx context.Context, id uuid.UUID) (*hooks.Hoo func (s *SqliteHookStore) List(ctx context.Context, filter hooks.ListFilter) ([]hooks.HookConfig, error) { q := `SELECT id, tenant_id, agent_id, scope, event, handler_type, config, matcher, if_expr, timeout_ms, on_timeout, - priority, enabled, version, source, metadata, created_by, + priority, enabled, version, source, metadata, name, created_by, created_at, updated_at FROM agent_hooks WHERE 1=1` var args []any @@ -343,7 +346,7 @@ func (s *SqliteHookStore) ResolveForEvent(ctx context.Context, event hooks.Event rows, err := s.db.QueryContext(ctx, ` SELECT id, tenant_id, agent_id, scope, event, handler_type, config, matcher, if_expr, timeout_ms, on_timeout, - priority, enabled, version, source, metadata, created_by, + priority, enabled, version, source, metadata, name, created_by, created_at, updated_at FROM agent_hooks WHERE enabled = 1 AND event = ? @@ -455,7 +458,7 @@ func scanHookSQLiteRow(row sqliteRowScanner) (*hooks.HookConfig, error) { scope, event string handlerType, onTimeout string source string - matcher, ifExpr sql.NullString + matcher, ifExpr, name sql.NullString cfgStr, metaStr string enabledInt int createdAt, updatedAt sqliteTime @@ -466,7 +469,7 @@ func scanHookSQLiteRow(row sqliteRowScanner) (*hooks.HookConfig, error) { &cfgStr, &matcher, &ifExpr, &cfg.TimeoutMS, &onTimeout, &cfg.Priority, &enabledInt, &cfg.Version, - &source, &metaStr, &createdByStr, + &source, &metaStr, &name, &createdByStr, &createdAt, &updatedAt, ) if err != nil { @@ -505,6 +508,9 @@ func scanHookSQLiteRow(row sqliteRowScanner) (*hooks.HookConfig, error) { if ifExpr.Valid { cfg.IfExpr = ifExpr.String } + if name.Valid { + cfg.Name = name.String + } if cfgStr != "" { _ = json.Unmarshal([]byte(cfgStr), &cfg.Config) diff --git a/internal/store/sqlitestore/schema.go b/internal/store/sqlitestore/schema.go index 323434f2..6244827a 100644 --- a/internal/store/sqlitestore/schema.go +++ b/internal/store/sqlitestore/schema.go @@ -16,7 +16,7 @@ var schemaSQL string // SchemaVersion is the current SQLite schema version. // Bump this when adding new migration steps below. -const SchemaVersion = 21 +const SchemaVersion = 22 // migrations maps version → SQL to apply when upgrading FROM that version. // schema.sql always represents the LATEST full schema (for fresh DBs). @@ -468,6 +468,9 @@ WHERE context_pruning IS NOT NULL // rebuild runs in rebuildAgentHooksV21 triggered after the v20→21 migration // tx commits (parallel to backfillV16 at EnsureSchema:677). 20: `SELECT 1;`, + + // Version 21 → 22: add name column to agent_hooks. + 21: `ALTER TABLE agent_hooks ADD COLUMN name TEXT;`, } // addAgentHooksTables is the SQLite incremental migration for schema v19 → v20. diff --git a/internal/store/sqlitestore/schema.sql b/internal/store/sqlitestore/schema.sql index 9fffd5bf..b6635e2e 100644 --- a/internal/store/sqlitestore/schema.sql +++ b/internal/store/sqlitestore/schema.sql @@ -1595,6 +1595,7 @@ CREATE TABLE IF NOT EXISTS agent_hooks ( version INTEGER NOT NULL DEFAULT 1, source TEXT NOT NULL DEFAULT 'ui' CHECK (source IN ('ui', 'api', 'seed', 'builtin')), metadata TEXT NOT NULL DEFAULT '{}', + name TEXT, created_by TEXT, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) diff --git a/internal/upgrade/version.go b/internal/upgrade/version.go index 2ff3bd42..0c64f086 100644 --- a/internal/upgrade/version.go +++ b/internal/upgrade/version.go @@ -2,4 +2,4 @@ package upgrade // RequiredSchemaVersion is the schema migration version this binary requires. // Bump this whenever adding a new SQL migration file. -const RequiredSchemaVersion uint = 53 +const RequiredSchemaVersion uint = 54 diff --git a/migrations/000054_agent_hooks_name.down.sql b/migrations/000054_agent_hooks_name.down.sql new file mode 100644 index 00000000..c75bef66 --- /dev/null +++ b/migrations/000054_agent_hooks_name.down.sql @@ -0,0 +1,2 @@ +-- 000054 down — Remove name column from agent_hooks. +ALTER TABLE agent_hooks DROP COLUMN IF EXISTS name; diff --git a/migrations/000054_agent_hooks_name.up.sql b/migrations/000054_agent_hooks_name.up.sql new file mode 100644 index 00000000..353620b7 --- /dev/null +++ b/migrations/000054_agent_hooks_name.up.sql @@ -0,0 +1,6 @@ +-- 000054 — Add `name` column to agent_hooks. +-- +-- User-facing label for hooks. Nullable so existing rows don't break. +-- Builtin hooks set name from the YAML spec at seed time. + +ALTER TABLE agent_hooks ADD COLUMN IF NOT EXISTS name VARCHAR(255); diff --git a/ui/web/src/hooks/use-hooks.ts b/ui/web/src/hooks/use-hooks.ts index 243427d9..dfe1c091 100644 --- a/ui/web/src/hooks/use-hooks.ts +++ b/ui/web/src/hooks/use-hooks.ts @@ -9,6 +9,7 @@ export interface HookConfig { id: string; tenant_id: string; agent_id?: string | null; + name?: string; event: string; handler_type: "command" | "http" | "prompt" | "script"; scope: "global" | "tenant" | "agent"; diff --git a/ui/web/src/i18n/locales/en/hooks.json b/ui/web/src/i18n/locales/en/hooks.json index e0339913..47870c6b 100644 --- a/ui/web/src/i18n/locales/en/hooks.json +++ b/ui/web/src/i18n/locales/en/hooks.json @@ -13,7 +13,17 @@ "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" + "collapse": "Hide", + "learnMore": "How hooks work with Skills & MCP", + "modalTitle": "Hooks + Skills + MCP", + "modalIntro": "Hooks integrate with GoClaw's Skill system and MCP (Model Context Protocol) servers to give you fine-grained control over the agent pipeline.", + "modalSkillTitle": "Hooks × Skills", + "modalSkillBody": "When a skill is invoked, the agent fires pre_tool_use and post_tool_use events. A hook on pre_tool_use can inspect the skill name and arguments, then allow, block, or rewrite them before execution. Post-tool hooks observe results for logging or chaining.", + "modalMcpTitle": "Hooks × MCP Servers", + "modalMcpBody": "MCP tools registered from external servers appear as regular tools in the agent pipeline. Hooks fire on the same pre_tool_use / post_tool_use events — no special wiring needed. Use a matcher regex to target specific MCP tool names (e.g. mcp__.*).", + "modalBuiltinTitle": "Built-in hooks (System)", + "modalBuiltinBody": "Built-in hooks like the PII Redactor run as sandboxed scripts with elevated privileges: they can rewrite user input (rawInput) and tool arguments before the agent sees them. User-created scripts cannot — this is enforced at the dispatcher level.", + "modalClose": "Got it" }, "actions": { "create": "Create hook", @@ -42,6 +52,8 @@ "form": { "title_create": "Create Hook", "title_edit": "Edit Hook", + "name": "Name", + "namePlaceholder": "e.g. Block sensitive tools", "event": "Event", "handlerType": "Handler Type", "scope": "Scope", diff --git a/ui/web/src/i18n/locales/vi/hooks.json b/ui/web/src/i18n/locales/vi/hooks.json index 1e164346..8433dca3 100644 --- a/ui/web/src/i18n/locales/vi/hooks.json +++ b/ui/web/src/i18n/locales/vi/hooks.json @@ -13,7 +13,17 @@ "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" + "collapse": "Ẩn", + "learnMore": "Hooks hoạt động với Skills & MCP như thế nào", + "modalTitle": "Hooks + Skills + MCP", + "modalIntro": "Hooks tích hợp với hệ thống Skill và MCP (Model Context Protocol) để cho bạn kiểm soát chi tiết pipeline agent.", + "modalSkillTitle": "Hooks × Skills", + "modalSkillBody": "Khi skill được gọi, agent phát sự kiện pre_tool_use và post_tool_use. Hook trên pre_tool_use có thể kiểm tra tên skill và tham số, rồi cho phép, chặn hoặc viết lại trước khi thực thi. Hook post-tool quan sát kết quả để ghi log hoặc chuỗi xử lý.", + "modalMcpTitle": "Hooks × MCP Servers", + "modalMcpBody": "MCP tools từ server bên ngoài xuất hiện như tool thường trong pipeline agent. Hooks vẫn phát trên pre_tool_use / post_tool_use — không cần cấu hình thêm. Dùng matcher regex để nhắm MCP tool cụ thể (ví dụ: mcp__.*).", + "modalBuiltinTitle": "Hook dựng sẵn (Hệ thống)", + "modalBuiltinBody": "Hook dựng sẵn như PII Redactor chạy script sandbox với quyền cao: có thể viết lại đầu vào người dùng (rawInput) và tham số tool trước khi agent nhìn thấy. Script do người dùng tạo không được phép — bảo vệ ở tầng dispatcher.", + "modalClose": "Đã hiểu" }, "actions": { "create": "Tạo hook", @@ -42,6 +52,8 @@ "form": { "title_create": "Tạo Hook", "title_edit": "Sửa Hook", + "name": "Tên", + "namePlaceholder": "VD: Chặn tool nhạy cảm", "event": "Sự kiện", "handlerType": "Loại xử lý", "scope": "Phạm vi", diff --git a/ui/web/src/i18n/locales/zh/hooks.json b/ui/web/src/i18n/locales/zh/hooks.json index d7b1e068..cbfaacea 100644 --- a/ui/web/src/i18n/locales/zh/hooks.json +++ b/ui/web/src/i18n/locales/zh/hooks.json @@ -13,7 +13,17 @@ "howItWorksBody2": "Script 在沙箱中运行 JavaScript。HTTP 调用你的 Webhook。Prompt 询问 LLM。内置钩子(系统徽章)由 GoClaw 提供 — 仅可切换启用状态。", "howItWorksTitle3": "3. 启用前先测试", "howItWorksBody3": "使用「运行钩子」配合示例事件预览决策、耗时及输入修改 — 启用前不会触及任何真实会话。", - "collapse": "收起" + "collapse": "收起", + "learnMore": "Hooks 如何与 Skills 和 MCP 配合", + "modalTitle": "Hooks + Skills + MCP", + "modalIntro": "Hooks 与 GoClaw 的 Skill 系统和 MCP(Model Context Protocol)服务器集成,为你提供对 Agent 管道的精细控制。", + "modalSkillTitle": "Hooks × Skills", + "modalSkillBody": "当 Skill 被调用时,Agent 触发 pre_tool_use 和 post_tool_use 事件。pre_tool_use 上的 Hook 可以检查 Skill 名称和参数,然后决定放行、阻止或改写。post-tool Hook 用于观察结果以记录日志或链式处理。", + "modalMcpTitle": "Hooks × MCP Servers", + "modalMcpBody": "来自外部 MCP 服务器的工具在 Agent 管道中作为普通工具出现。Hooks 同样在 pre_tool_use / post_tool_use 上触发 — 无需特殊配置。使用 matcher 正则来匹配特定 MCP 工具名(如 mcp__.*)。", + "modalBuiltinTitle": "内置 Hook(系统)", + "modalBuiltinBody": "内置 Hook(如 PII Redactor)以沙箱脚本运行并拥有提升的权限:可以在 Agent 看到之前改写用户输入(rawInput)和工具参数。用户创建的脚本无此权限 — 由调度器层面强制执行。", + "modalClose": "知道了" }, "actions": { "create": "创建钩子", @@ -42,6 +52,8 @@ "form": { "title_create": "创建钩子", "title_edit": "编辑钩子", + "name": "名称", + "namePlaceholder": "例如:阻止敏感工具", "event": "事件", "handlerType": "处理器类型", "scope": "范围", diff --git a/ui/web/src/pages/hooks/components/beta-info-card.tsx b/ui/web/src/pages/hooks/components/beta-info-card.tsx index 86089e33..b31a482a 100644 --- a/ui/web/src/pages/hooks/components/beta-info-card.tsx +++ b/ui/web/src/pages/hooks/components/beta-info-card.tsx @@ -1,7 +1,10 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { ChevronDown, ChevronUp, Sparkles } from "lucide-react"; +import { ChevronDown, ChevronUp, Info, Sparkles } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { + Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, +} from "@/components/ui/dialog"; const LS_KEY = "goclaw:hooks:beta-card-collapsed"; @@ -21,12 +24,10 @@ function saveCollapsed(v: boolean) { } } -// 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 [modalOpen, setModalOpen] = useState(false); const toggle = () => { const next = !collapsed; @@ -35,50 +36,96 @@ export function BetaInfoCard() { }; return ( -
- + + {!collapsed && ( +
+

{t("beta.description")}

+ +
+ + + +
+ +
+ + +
+
)} - +
- {!collapsed && ( -
-

{t("beta.description")}

+ + + ); +} -
- - - -
- -
- -
+function HooksExplainerModal({ open, onOpenChange }: { open: boolean; onOpenChange: (o: boolean) => void }) { + const { t } = useTranslation("hooks"); + return ( + + + + {t("beta.modalTitle")} + +
+

{t("beta.modalIntro")}

+ + +
- )} + + + +
+
+ ); +} + +function ExplainerSection({ 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 c379b2b2..06caf557 100644 --- a/ui/web/src/pages/hooks/components/hook-form-dialog.tsx +++ b/ui/web/src/pages/hooks/components/hook-form-dialog.tsx @@ -43,8 +43,9 @@ export function HookFormDialog({ open, onOpenChange, onSubmit, initial }: HookFo } = useForm({ resolver: zodResolver(hookFormSchema), defaultValues: { + name: "", event: "pre_tool_use", - handler_type: "http", + handler_type: "script", scope: "tenant", timeout_ms: 5000, on_timeout: "block", @@ -73,6 +74,7 @@ export function HookFormDialog({ open, onOpenChange, onSubmit, initial }: HookFo const handlerType: HookFormData["handler_type"] = initial.handler_type === "command" ? "http" : initial.handler_type; reset({ + name: initial.name ?? "", event: initial.event as HookFormData["event"], handler_type: handlerType, scope: initial.scope, @@ -116,6 +118,17 @@ export function HookFormDialog({ open, onOpenChange, onSubmit, initial }: HookFo
)} + {/* Name — optional user-facing label */} +
+ + +
+ {/* Event + Scope side-by-side on wider viewports */}
@@ -204,7 +217,8 @@ export function HookFormDialog({ open, onOpenChange, onSubmit, initial }: HookFo {/* Handler-specific sub-forms */} {handlerType === "script" && ( -
+
+
+
)} {handlerType === "http" && ( diff --git a/ui/web/src/pages/hooks/components/hook-list-row.tsx b/ui/web/src/pages/hooks/components/hook-list-row.tsx index a3eeab09..bb24f7d8 100644 --- a/ui/web/src/pages/hooks/components/hook-list-row.tsx +++ b/ui/web/src/pages/hooks/components/hook-list-row.tsx @@ -44,7 +44,10 @@ export function HookListRow({ hook, onClick, onToggle, onEdit, onDelete, onTest {/* Main info */}