Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 6b60e36

Browse files
refactor(core): extract permission option presentation
Generated-By: PostHog Code Task-Id: c1bbe3cf-742b-4b24-bf96-d11a18b4cf22
1 parent f79661b commit 6b60e36

4 files changed

Lines changed: 131 additions & 39 deletions

File tree

‎packages/core/src/sessions/permissionResponse.test.ts‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,62 @@ import type { PermissionRequest } from "@posthog/shared";
22
import { describe, expect, it } from "vitest";
33
import {
44
formatPermissionAnswerPrompt,
5+
getPermissionOptionMeta,
56
isOtherPermissionOption,
7+
isPermissionApproval,
8+
isPermissionRejection,
9+
permissionOptionUsesCustomInput,
610
planPermissionResponse,
11+
resolveInitialPlanApprovalOption,
12+
selectPlanPermissionOptions,
713
} from "./permissionResponse";
814

15+
describe("permission option presentation", () => {
16+
const approveOnce = {
17+
optionId: "default",
18+
name: "Approve",
19+
kind: "allow_once" as const,
20+
};
21+
const approveAuto = {
22+
optionId: "auto",
23+
name: "Approve automatically",
24+
kind: "allow_always" as const,
25+
};
26+
const reject = {
27+
optionId: "reject_with_feedback",
28+
name: "Reject",
29+
kind: "reject_once" as const,
30+
_meta: { customInput: true, description: "Explain why" },
31+
};
32+
33+
it("classifies approval, rejection, and custom-input options", () => {
34+
expect(isPermissionApproval(approveOnce)).toBe(true);
35+
expect(isPermissionRejection(reject)).toBe(true);
36+
expect(permissionOptionUsesCustomInput(reject)).toBe(true);
37+
expect(getPermissionOptionMeta(reject)).toEqual({
38+
customInput: true,
39+
description: "Explain why",
40+
});
41+
});
42+
43+
it("selects plan options and prefers a feedback rejection", () => {
44+
expect(selectPlanPermissionOptions([approveOnce, reject])).toEqual({
45+
approvals: [approveOnce],
46+
rejection: reject,
47+
});
48+
});
49+
50+
it.each([
51+
["default", "default"],
52+
[null, "auto"],
53+
["missing", "auto"],
54+
])("resolves preferred approval %s", (preferred, expected) => {
55+
expect(
56+
resolveInitialPlanApprovalOption([approveOnce, approveAuto], preferred),
57+
).toBe(expected);
58+
});
59+
});
60+
961
function makePermission(
1062
options: Array<{
1163
optionId: string;

‎packages/core/src/sessions/permissionResponse.ts‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,72 @@
11
import type { PermissionRequest } from "@posthog/shared";
22

3+
export type PermissionOption = PermissionRequest["options"][number];
4+
5+
export function getPermissionOptionMeta(option: PermissionOption): {
6+
customInput: boolean;
7+
description?: string;
8+
} {
9+
const meta = option._meta as
10+
| { customInput?: boolean; description?: string }
11+
| null
12+
| undefined;
13+
return {
14+
customInput: meta?.customInput === true,
15+
...(meta?.description ? { description: meta.description } : {}),
16+
};
17+
}
18+
19+
export function isPermissionApproval(option: PermissionOption): boolean {
20+
return option.kind === "allow_once" || option.kind === "allow_always";
21+
}
22+
23+
export function isPermissionRejection(option: PermissionOption): boolean {
24+
return (
25+
option.kind === "reject_once" ||
26+
option.kind === "reject_always" ||
27+
option.optionId.includes("reject")
28+
);
29+
}
30+
31+
export function permissionOptionUsesCustomInput(
32+
option: PermissionOption,
33+
): boolean {
34+
return (
35+
isOtherPermissionOption(option.optionId) ||
36+
getPermissionOptionMeta(option).customInput
37+
);
38+
}
39+
40+
export function selectPlanPermissionOptions(options: PermissionOption[]): {
41+
approvals: PermissionOption[];
42+
rejection: PermissionOption | null;
43+
} {
44+
const approvals = options.filter(isPermissionApproval);
45+
const rejections = options.filter(isPermissionRejection);
46+
return {
47+
approvals,
48+
rejection:
49+
rejections.find(permissionOptionUsesCustomInput) ?? rejections[0] ?? null,
50+
};
51+
}
52+
53+
export function resolveInitialPlanApprovalOption(
54+
approvals: PermissionOption[],
55+
preferredOptionId?: string | null,
56+
): string | undefined {
57+
const has = (optionId: string): boolean =>
58+
approvals.some((option) => option.optionId === optionId);
59+
return (
60+
(preferredOptionId && has(preferredOptionId)
61+
? preferredOptionId
62+
: undefined) ??
63+
(has("auto") ? "auto" : undefined) ??
64+
approvals.find((option) => option.optionId === "default")?.optionId ??
65+
approvals.find((option) => option.kind === "allow_once")?.optionId ??
66+
approvals[0]?.optionId
67+
);
68+
}
69+
370
const OTHER_OPTION_ID = "_other";
471
const OTHER_OPTION_ID_ALT = "other";
572

‎packages/ui/src/features/permissions/PlanApprovalSelector.tsx‎

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import type {
2-
PermissionOption,
3-
SessionConfigOption,
4-
} from "@agentclientprotocol/sdk";
1+
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
2+
import {
3+
resolveInitialPlanApprovalOption,
4+
selectPlanPermissionOptions,
5+
} from "@posthog/core/sessions/permissionResponse";
56
import type { ExecutionMode } from "@posthog/shared";
67
import { ModeSelector } from "@posthog/ui/features/message-editor/components/ModeSelector";
78
import { MODE_LABELS } from "@posthog/ui/features/sessions/modeStyles";
@@ -17,21 +18,6 @@ import { type BasePermissionProps, toSelectorOptions } from "./types";
1718
const TITLE = "Implementation Plan";
1819
const QUESTION = "Approve this plan to proceed?";
1920

20-
function isApprove(option: PermissionOption): boolean {
21-
return option.kind === "allow_once" || option.kind === "allow_always";
22-
}
23-
24-
function isReject(option: PermissionOption): boolean {
25-
return option.kind === "reject_once" || option.kind === "reject_always";
26-
}
27-
28-
function hasCustomInput(option: PermissionOption): boolean {
29-
return (
30-
(option._meta as { customInput?: boolean } | null | undefined)
31-
?.customInput === true
32-
);
33-
}
34-
3521
// Don't steal focus from an interactive element in a different grid cell
3622
// (multi-task view). Mirrors the guard in useActionSelectorState.
3723
function isInteractiveElementInDifferentCell(
@@ -65,11 +51,8 @@ export function PlanApprovalSelector({
6551
onSelect,
6652
onCancel,
6753
}: BasePermissionProps) {
68-
const approveOptions = useMemo(() => options.filter(isApprove), [options]);
69-
const rejectOption = useMemo(
70-
() =>
71-
options.find((o) => isReject(o) && hasCustomInput(o)) ??
72-
options.find(isReject),
54+
const { approvals: approveOptions, rejection: rejectOption } = useMemo(
55+
() => selectPlanPermissionOptions(options),
7356
[options],
7457
);
7558

@@ -81,16 +64,7 @@ export function PlanApprovalSelector({
8164
// Resolution order: the mode last approved with (remembered preference),
8265
// then "auto", then manual-approve, then any single-use mode, then the first.
8366
const initialMode = useMemo(() => {
84-
const has = (id: string) => approveOptions.some((o) => o.optionId === id);
85-
return (
86-
(lastApprovalMode && has(lastApprovalMode)
87-
? lastApprovalMode
88-
: undefined) ??
89-
(has("auto") ? "auto" : undefined) ??
90-
approveOptions.find((o) => o.optionId === "default")?.optionId ??
91-
approveOptions.find((o) => o.kind === "allow_once")?.optionId ??
92-
approveOptions[0]?.optionId
93-
);
67+
return resolveInitialPlanApprovalOption(approveOptions, lastApprovalMode);
9468
}, [approveOptions, lastApprovalMode]);
9569

9670
const [selectedMode, setSelectedMode] = useState(initialMode);

‎packages/ui/src/features/permissions/types.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
RequestPermissionRequest,
44
ToolCallContent,
55
} from "@agentclientprotocol/sdk";
6+
import { getPermissionOptionMeta } from "@posthog/core/sessions/permissionResponse";
67
import type { CodeToolKind } from "@posthog/ui/features/sessions/types";
78
import type { SelectorOption } from "@posthog/ui/primitives/ActionSelector";
89

@@ -26,14 +27,12 @@ export function toSelectorOptions(
2627
options: PermissionOption[],
2728
): SelectorOption[] {
2829
return options.map((opt) => {
29-
const meta = opt._meta as
30-
| { description?: string; customInput?: boolean }
31-
| undefined;
30+
const meta = getPermissionOptionMeta(opt);
3231
return {
3332
id: opt.optionId,
3433
label: opt.name,
35-
description: meta?.description,
36-
customInput: meta?.customInput,
34+
description: meta.description,
35+
customInput: meta.customInput,
3736
};
3837
});
3938
}

0 commit comments

Comments
 (0)