Skip to content

Commit ac54308

Browse files
fix(ui): align managed resources hooks with v0.19
1 parent 0a79400 commit ac54308

6 files changed

Lines changed: 1961 additions & 730 deletions

File tree

ui/src/components/HookEditor.tsx

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ export default function HookEditor({
8787
}, REMOVE_ANIMATION_MS);
8888
};
8989

90+
const isCodexTool = value.tool.trim().toLowerCase() === 'codex';
91+
9092
return (
9193
<Card className="space-y-4">
9294
<form
@@ -199,20 +201,15 @@ export default function HookEditor({
199201

200202
<div className="grid gap-4 md:grid-cols-2">
201203
<HandInput
202-
label="Timeout"
204+
label={isCodexTool ? 'Timeout Sec' : 'Timeout'}
203205
value={handler.timeout}
204206
onChange={(event) => updateHandler(index, { ...handler, timeout: event.target.value })}
205-
placeholder="30s"
206-
/>
207-
<HandInput
208-
label="Timeout Sec"
209207
type="text"
210-
value={handler.timeoutSec}
211-
onChange={(event) => updateHandler(index, { ...handler, timeoutSec: event.target.value })}
212-
placeholder="30"
213-
inputMode="numeric"
214-
pattern="[0-9]*"
208+
placeholder={isCodexTool ? '30' : '30s'}
209+
inputMode={isCodexTool ? 'numeric' : undefined}
210+
pattern={isCodexTool ? '[0-9]*' : undefined}
215211
/>
212+
<div />
216213
</div>
217214
</Card>
218215
))}

ui/src/components/hookEditorState.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ export interface HookEditorHandlerValue {
44
url: string;
55
prompt: string;
66
timeout: string;
7-
timeoutSec: string;
87
statusMessage: string;
98
}
109

@@ -22,7 +21,6 @@ function emptyHandler(): HookEditorHandlerValue {
2221
url: '',
2322
prompt: '',
2423
timeout: '',
25-
timeoutSec: '',
2624
statusMessage: '',
2725
};
2826
}

ui/src/pages/HookDetailPage.test.tsx

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ describe('HookDetailPage', () => {
161161
expect(screen.getByText(/compiled preview/i)).toBeInTheDocument();
162162
});
163163

164-
it('uses the custom handler type select, prompt textarea, and text timeout input', async () => {
164+
it('uses the custom handler type select with a single timeout field for claude hooks', async () => {
165165
const createdHook: ManagedHookDetailResponse = {
166166
hook: {
167167
id: 'claude/pre-tool-use/bash.yaml',
@@ -197,11 +197,11 @@ describe('HookDetailPage', () => {
197197
expect(typeControl).toHaveTextContent('prompt');
198198
expect(screen.getByLabelText(/prompt/i).tagName).toBe('TEXTAREA');
199199
expect(screen.getByLabelText(/prompt/i)).toHaveAttribute('rows', '5');
200-
expect(screen.getByLabelText(/timeout sec/i)).toHaveAttribute('type', 'text');
201-
expect(screen.getByLabelText(/timeout sec/i)).toHaveAttribute('inputmode', 'numeric');
200+
expect(screen.getByLabelText(/^timeout$/i)).not.toHaveAttribute('inputmode');
201+
expect(screen.queryByLabelText(/timeout sec/i)).not.toBeInTheDocument();
202202

203203
await userEvent.type(screen.getByLabelText(/prompt/i), 'Review the action');
204-
await userEvent.type(screen.getByLabelText(/timeout sec/i), '15');
204+
await userEvent.type(screen.getByLabelText(/^timeout$/i), '15s');
205205
await userEvent.click(screen.getByRole('button', { name: /save hook/i }));
206206

207207
expect(createManagedHook).toHaveBeenCalledWith({
@@ -214,8 +214,83 @@ describe('HookDetailPage', () => {
214214
command: undefined,
215215
url: undefined,
216216
prompt: 'Review the action',
217+
timeout: '15s',
218+
timeoutSec: undefined,
219+
statusMessage: undefined,
220+
},
221+
],
222+
});
223+
});
224+
225+
it('uses a single numeric timeout field for codex hooks', async () => {
226+
getManagedHook.mockResolvedValue({
227+
hook: {
228+
id: 'codex/pre-tool-use/bash.yaml',
229+
tool: 'codex',
230+
event: 'PreToolUse',
231+
matcher: 'Bash',
232+
targets: ['codex-work'],
233+
sourceType: 'tracked',
234+
disabled: false,
235+
handlers: [
236+
{
237+
type: 'command',
238+
command: './bin/check',
239+
timeout: '30',
240+
},
241+
],
242+
},
243+
previews: [],
244+
});
245+
updateManagedHook.mockResolvedValue({
246+
hook: {
247+
id: 'codex/pre-tool-use/bash.yaml',
248+
tool: 'codex',
249+
event: 'PreToolUse',
250+
matcher: 'Bash',
251+
targets: ['codex-work'],
252+
sourceType: 'tracked',
253+
disabled: false,
254+
handlers: [
255+
{
256+
type: 'command',
257+
command: './bin/check',
258+
timeoutSec: 45,
259+
},
260+
],
261+
},
262+
previews: [],
263+
});
264+
265+
renderPage('/hooks/manage/codex/pre-tool-use/bash.yaml');
266+
267+
const timeoutInput = await screen.findByLabelText(/timeout sec/i);
268+
expect(timeoutInput).toHaveValue('30');
269+
expect(timeoutInput).toHaveAttribute('type', 'text');
270+
expect(timeoutInput).toHaveAttribute('inputmode', 'numeric');
271+
expect(timeoutInput).toHaveAttribute('pattern', '[0-9]*');
272+
expect(timeoutInput).toHaveAttribute('placeholder', '30');
273+
expect(screen.queryByLabelText(/^timeout$/i)).not.toBeInTheDocument();
274+
275+
await userEvent.clear(timeoutInput);
276+
await userEvent.type(timeoutInput, '45');
277+
await userEvent.click(screen.getByRole('button', { name: /save hook/i }));
278+
279+
expect(updateManagedHook).toHaveBeenCalledWith('codex/pre-tool-use/bash.yaml', {
280+
tool: 'codex',
281+
event: 'PreToolUse',
282+
matcher: 'Bash',
283+
targets: ['codex-work'],
284+
sourceType: 'tracked',
285+
disabled: false,
286+
handlers: [
287+
{
288+
type: 'command',
289+
command: './bin/check',
290+
url: undefined,
291+
prompt: undefined,
217292
timeout: undefined,
218-
timeoutSec: 15,
293+
timeoutSec: 45,
219294
statusMessage: undefined,
220295
},
221296
],

ui/src/pages/HookDetailPage.tsx

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import HandButton from '../components/HandButton';
77
import ConfirmDialog from '../components/ConfirmDialog';
88
import { PageSkeleton } from '../components/Skeleton';
99
import { useToast } from '../components/Toast';
10-
import { api, type ManagedHookDetailResponse, type ManagedHookSaveRequest } from '../api/client';
10+
import { api, type ManagedHookDetailResponse, type ManagedHookHandler, type ManagedHookSaveRequest } from '../api/client';
1111
import { queryKeys, staleTimes } from '../lib/queryKeys';
1212
import HookEditor from '../components/HookEditor';
1313
import { createEmptyHookEditorValue, type HookEditorValue } from '../components/hookEditorState';
@@ -17,20 +17,56 @@ function emptyValue(): HookEditorValue {
1717
return createEmptyHookEditorValue();
1818
}
1919

20+
function isCodexTool(tool: string): boolean {
21+
return tool.trim().toLowerCase() === 'codex';
22+
}
23+
24+
function normalizeTimeoutValue(handler: Pick<ManagedHookHandler, 'timeout' | 'timeoutSec'>): string {
25+
const timeout = handler.timeout?.trim();
26+
if (timeout) return timeout;
27+
return handler.timeoutSec === undefined ? '' : String(handler.timeoutSec);
28+
}
29+
30+
function toRequestHandler(tool: string, handler: HookEditorValue['handlers'][number]): ManagedHookHandler {
31+
const timeout = handler.timeout.trim();
32+
const base = {
33+
type: handler.type,
34+
command: handler.command || undefined,
35+
url: handler.url || undefined,
36+
prompt: handler.prompt || undefined,
37+
statusMessage: handler.statusMessage || undefined,
38+
} satisfies Omit<ManagedHookHandler, 'timeout' | 'timeoutSec'>;
39+
40+
if (!timeout) {
41+
return {
42+
...base,
43+
timeout: undefined,
44+
timeoutSec: undefined,
45+
};
46+
}
47+
48+
if (isCodexTool(tool)) {
49+
const numericTimeout = Number(timeout);
50+
return {
51+
...base,
52+
timeout: Number.isInteger(numericTimeout) ? undefined : timeout,
53+
timeoutSec: Number.isInteger(numericTimeout) ? numericTimeout : undefined,
54+
};
55+
}
56+
57+
return {
58+
...base,
59+
timeout,
60+
timeoutSec: undefined,
61+
};
62+
}
63+
2064
function toRequest(value: HookEditorValue): ManagedHookSaveRequest {
2165
return {
2266
tool: value.tool,
2367
event: value.event,
2468
matcher: value.matcher || undefined,
25-
handlers: value.handlers.map((handler) => ({
26-
type: handler.type,
27-
command: handler.command || undefined,
28-
url: handler.url || undefined,
29-
prompt: handler.prompt || undefined,
30-
timeout: handler.timeout || undefined,
31-
timeoutSec: handler.timeoutSec ? Number(handler.timeoutSec) : undefined,
32-
statusMessage: handler.statusMessage || undefined,
33-
})),
69+
handlers: value.handlers.map((handler) => toRequestHandler(value.tool, handler)),
3470
};
3571
}
3672

@@ -45,8 +81,7 @@ function fromDetail(detail: ManagedHookDetailResponse): HookEditorValue {
4581
command: handler.command ?? '',
4682
url: handler.url ?? '',
4783
prompt: handler.prompt ?? '',
48-
timeout: handler.timeout ?? '',
49-
timeoutSec: handler.timeoutSec === undefined ? '' : String(handler.timeoutSec),
84+
timeout: normalizeTimeoutValue(handler),
5085
statusMessage: handler.statusMessage ?? '',
5186
}))
5287
: emptyValue().handlers,

0 commit comments

Comments
 (0)