@omercnet/paseo-omp 4.0.0-next.294.1 → 4.0.0-next.312.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Release Please keeps this bundle-safe constant aligned with package.json. */
|
|
2
|
-
export const PASEO_OMP_PACKAGE_VERSION = "4.0.0-next.
|
|
2
|
+
export const PASEO_OMP_PACKAGE_VERSION = "4.0.0-next.312.1"; // x-release-please-version
|
|
@@ -155,7 +155,7 @@ export const OMP_RPC_COMMAND_POLICIES = {
|
|
|
155
155
|
predict_word_feedback: "unsupported",
|
|
156
156
|
promote_queued_message: "unsupported",
|
|
157
157
|
remove_queued_message: "unsupported",
|
|
158
|
-
set_ask_dialog: "
|
|
158
|
+
set_ask_dialog: "implemented",
|
|
159
159
|
set_cache_warming: "unsupported",
|
|
160
160
|
set_slow_mode: "unsupported",
|
|
161
161
|
steer_subagent: "unsupported",
|
|
@@ -524,6 +524,10 @@ const OMP_PROVIDER_NAME = NAME.refine((provider) => !provider.includes("/"));
|
|
|
524
524
|
const TEXT = boundedString(MAX_TEXT_LENGTH);
|
|
525
525
|
const RAW_DISPLAY_TEXT = boundedString(MAX_IMAGE_DATA_LENGTH);
|
|
526
526
|
const DISPLAY_TRUNCATION_MARKER = "<truncated>";
|
|
527
|
+
/** OMP's `ask` tool has no upstream caps; these bound one dialog like the select schema does. */
|
|
528
|
+
export const MAX_ASK_QUESTIONS = 32;
|
|
529
|
+
export const MAX_ASK_OPTIONS = 128;
|
|
530
|
+
const MAX_ASK_QUESTION_ID_BYTES = 4_096;
|
|
527
531
|
|
|
528
532
|
function boundRawDisplayContent(value: unknown): unknown {
|
|
529
533
|
if (typeof value === "string") {
|
|
@@ -634,6 +638,7 @@ export const OMP_RPC_DIAGNOSTIC_COMMANDS = [
|
|
|
634
638
|
"get_messages",
|
|
635
639
|
"abort",
|
|
636
640
|
"set_host_tools",
|
|
641
|
+
"set_ask_dialog",
|
|
637
642
|
] as const;
|
|
638
643
|
export type OmpRpcDiagnosticCommand = (typeof OMP_RPC_DIAGNOSTIC_COMMANDS)[number];
|
|
639
644
|
type JsonBoundViolation = {
|
|
@@ -1695,6 +1700,44 @@ const OmpExtensionUiRequestSchema = z.discriminatedUnion("method", [
|
|
|
1695
1700
|
timeout: z.number().nonnegative().finite().optional(),
|
|
1696
1701
|
})
|
|
1697
1702
|
.strict(),
|
|
1703
|
+
z
|
|
1704
|
+
.object({
|
|
1705
|
+
...ExtensionUiBase,
|
|
1706
|
+
method: z.literal("ask"),
|
|
1707
|
+
questions: z
|
|
1708
|
+
.array(
|
|
1709
|
+
z
|
|
1710
|
+
.object({
|
|
1711
|
+
id: boundedString(MAX_ASK_QUESTION_ID_BYTES, 1),
|
|
1712
|
+
question: boundedString(64 * 1024),
|
|
1713
|
+
header: boundedString(4_096).optional(),
|
|
1714
|
+
options: z
|
|
1715
|
+
.array(
|
|
1716
|
+
z
|
|
1717
|
+
.object({
|
|
1718
|
+
label: boundedString(4_096),
|
|
1719
|
+
description: boundedString(16_384).optional(),
|
|
1720
|
+
preview: boundedString(64 * 1024).optional(),
|
|
1721
|
+
})
|
|
1722
|
+
.strict(),
|
|
1723
|
+
)
|
|
1724
|
+
.max(MAX_ASK_OPTIONS),
|
|
1725
|
+
multi: z.boolean().optional(),
|
|
1726
|
+
recommended: z.number().int().nonnegative().optional(),
|
|
1727
|
+
})
|
|
1728
|
+
.strict(),
|
|
1729
|
+
)
|
|
1730
|
+
.min(1)
|
|
1731
|
+
.max(MAX_ASK_QUESTIONS),
|
|
1732
|
+
timeout: z.number().nonnegative().finite().optional(),
|
|
1733
|
+
})
|
|
1734
|
+
.strict()
|
|
1735
|
+
.superRefine((request, context) => {
|
|
1736
|
+
const ids = new Set(request.questions.map((question) => question.id));
|
|
1737
|
+
if (ids.size !== request.questions.length) {
|
|
1738
|
+
context.addIssue({ code: "custom", message: "invalid extension UI request" });
|
|
1739
|
+
}
|
|
1740
|
+
}),
|
|
1698
1741
|
z.object({ ...ExtensionUiBase, method: z.literal("cancel"), targetId: IDENTIFIER }).strict(),
|
|
1699
1742
|
z
|
|
1700
1743
|
.object({
|
|
@@ -1980,7 +2023,37 @@ export interface OmpPersistedSessionMessages {
|
|
|
1980
2023
|
|
|
1981
2024
|
export type OmpAvailableCommand = z.infer<typeof OmpAvailableCommandSchema>;
|
|
1982
2025
|
export type OmpImage = { type: "image"; data: string; mimeType: string };
|
|
1983
|
-
export
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
2026
|
+
export const OmpExtensionUiResponseSchema = z.union([
|
|
2027
|
+
z.object({ type: z.literal("extension_ui_response"), id: IDENTIFIER, value: TEXT }).strict(),
|
|
2028
|
+
z
|
|
2029
|
+
.object({ type: z.literal("extension_ui_response"), id: IDENTIFIER, confirmed: z.boolean() })
|
|
2030
|
+
.strict(),
|
|
2031
|
+
z
|
|
2032
|
+
.object({
|
|
2033
|
+
type: z.literal("extension_ui_response"),
|
|
2034
|
+
id: IDENTIFIER,
|
|
2035
|
+
cancelled: z.literal(true),
|
|
2036
|
+
timedOut: z.boolean().optional(),
|
|
2037
|
+
})
|
|
2038
|
+
.strict(),
|
|
2039
|
+
z
|
|
2040
|
+
.object({
|
|
2041
|
+
type: z.literal("extension_ui_response"),
|
|
2042
|
+
id: IDENTIFIER,
|
|
2043
|
+
answers: z
|
|
2044
|
+
.array(
|
|
2045
|
+
z
|
|
2046
|
+
.object({
|
|
2047
|
+
id: boundedString(MAX_ASK_QUESTION_ID_BYTES, 1),
|
|
2048
|
+
selectedOptions: z.array(boundedString(4_096)).max(MAX_ASK_OPTIONS),
|
|
2049
|
+
customInput: TEXT.optional(),
|
|
2050
|
+
})
|
|
2051
|
+
.strict(),
|
|
2052
|
+
)
|
|
2053
|
+
.min(1)
|
|
2054
|
+
.max(MAX_ASK_QUESTIONS),
|
|
2055
|
+
})
|
|
2056
|
+
.strict(),
|
|
2057
|
+
]);
|
|
2058
|
+
export type OmpExtensionUiResponse = z.infer<typeof OmpExtensionUiResponseSchema>;
|
|
2059
|
+
export const OmpAskDialogEnabledSchema = z.object({ enabled: z.boolean() }).strict();
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
MAX_REPLAY_MESSAGES,
|
|
11
11
|
NAME,
|
|
12
12
|
OMP_MESSAGE_PAGE_LIMIT,
|
|
13
|
+
OmpAskDialogEnabledSchema,
|
|
13
14
|
type OmpAvailableCommand,
|
|
14
15
|
OmpAvailableCommandsResultSchema,
|
|
15
16
|
OmpBranchMessagesResultSchema,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
type OmpCompactionResult,
|
|
19
20
|
OmpCompactionResultSchema,
|
|
20
21
|
type OmpExtensionUiResponse,
|
|
22
|
+
OmpExtensionUiResponseSchema,
|
|
21
23
|
type OmpHostToolDefinition,
|
|
22
24
|
OmpHostToolDefinitionSchema,
|
|
23
25
|
type OmpHostToolResult,
|
|
@@ -546,7 +548,10 @@ class OmpRpcSession implements OmpRuntimeSession {
|
|
|
546
548
|
}
|
|
547
549
|
|
|
548
550
|
respondToExtensionUi(response: OmpExtensionUiResponse): Promise<void> {
|
|
549
|
-
|
|
551
|
+
// Callers chain `.catch` without a surrounding try, so a bad frame must reject, not throw.
|
|
552
|
+
const parsed = OmpExtensionUiResponseSchema.safeParse(response);
|
|
553
|
+
if (!parsed.success) return Promise.reject(parsed.error);
|
|
554
|
+
return this.process.sendFrame(parsed.data);
|
|
550
555
|
}
|
|
551
556
|
respondToToolApproval(response: OmpToolApprovalResponse): Promise<void> {
|
|
552
557
|
return this.process.sendFrame(OmpToolApprovalResponseSchema.parse(response));
|
|
@@ -652,6 +657,7 @@ export class OmpRpcRuntime implements OmpRuntime {
|
|
|
652
657
|
}),
|
|
653
658
|
);
|
|
654
659
|
options.signal?.throwIfAborted();
|
|
660
|
+
enableAskDialog(process);
|
|
655
661
|
return new OmpRpcSession(
|
|
656
662
|
process,
|
|
657
663
|
removeAbortListener,
|
|
@@ -671,3 +677,17 @@ export class OmpRpcRuntime implements OmpRuntime {
|
|
|
671
677
|
}
|
|
672
678
|
}
|
|
673
679
|
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Opts into OMP's single-request `ask` dialog so every question reaches Paseo as one card that can
|
|
683
|
+
* be revisited before submitting. OMP applies the flag in command order, so it is set before any
|
|
684
|
+
* later prompt can ask; startup does not wait for the acknowledgement. OMP releases without
|
|
685
|
+
* `set_ask_dialog` reject it and keep the per-question select/editor fallback, which the
|
|
686
|
+
* permission bridge still handles.
|
|
687
|
+
*/
|
|
688
|
+
function enableAskDialog(process: OmpRpcProcess): void {
|
|
689
|
+
process
|
|
690
|
+
.request({ type: "set_ask_dialog", enabled: true })
|
|
691
|
+
.then((data) => OmpAskDialogEnabledSchema.parse(data))
|
|
692
|
+
.catch(() => undefined);
|
|
693
|
+
}
|
|
@@ -25,14 +25,23 @@ import type { OmpTimelineProjector, OmpTimelineScheduler } from "./timeline-proj
|
|
|
25
25
|
type SessionPermissionInput = Extract<ProviderInput, { type: "session.permission" }>;
|
|
26
26
|
type OmpQuestionRequest = Extract<
|
|
27
27
|
Extract<OmpRpcEvent, { type: "extension_ui_request" }>,
|
|
28
|
-
{ method: "select" | "confirm" | "input" | "editor" }
|
|
28
|
+
{ method: "select" | "confirm" | "input" | "editor" | "ask" }
|
|
29
29
|
>;
|
|
30
|
+
type OmpAskRequest = Extract<OmpQuestionRequest, { method: "ask" }>;
|
|
30
31
|
type Emit = (event: ProviderEvent) => void;
|
|
31
32
|
|
|
32
33
|
const MAX_PENDING_PERMISSIONS = 32;
|
|
33
34
|
const MAX_PENDING_PERMISSION_BYTES = 2 * 1024 * 1024;
|
|
34
35
|
const MAX_RESOLVED_TOOL_APPROVAL_IDS = 1_024;
|
|
35
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Select rows that hand off to a follow-up free-text request: `ask_user` extensions follow with
|
|
38
|
+
* `input`, OMP's own `ask` fallback (hosts without `set_ask_dialog`) follows with `editor`.
|
|
39
|
+
*/
|
|
40
|
+
const OMP_FREEFORM_SENTINELS: readonly string[] = [
|
|
41
|
+
"✏️ Type custom response...",
|
|
42
|
+
"Other (type your own)",
|
|
43
|
+
];
|
|
44
|
+
const OMP_ASK_RECOMMENDED_SUFFIX = " (Recommended)";
|
|
36
45
|
const MAX_FREEFORM_RESPONSE_BYTES = 64 * 1024;
|
|
37
46
|
|
|
38
47
|
type PendingPermission = {
|
|
@@ -50,6 +59,14 @@ type PendingPermission = {
|
|
|
50
59
|
turnId?: string;
|
|
51
60
|
request: OmpQuestionRequest;
|
|
52
61
|
freeformSentinel?: string;
|
|
62
|
+
askQuestions?: readonly PendingAskQuestion[];
|
|
63
|
+
};
|
|
64
|
+
/** One `ask` question as rendered on the Paseo card, with display labels mapped back to OMP. */
|
|
65
|
+
type PendingAskQuestion = {
|
|
66
|
+
id: string;
|
|
67
|
+
header: string;
|
|
68
|
+
multi: boolean;
|
|
69
|
+
nativeLabels: ReadonlyMap<string, string>;
|
|
53
70
|
};
|
|
54
71
|
type PendingToolPermission = {
|
|
55
72
|
nativeId: string;
|
|
@@ -75,6 +92,96 @@ function permissionFingerprint(request: OmpQuestionRequest): string {
|
|
|
75
92
|
return createHash("sha256").update(JSON.stringify(request)).digest("base64url");
|
|
76
93
|
}
|
|
77
94
|
|
|
95
|
+
type CardQuestion = {
|
|
96
|
+
header: string;
|
|
97
|
+
question: string;
|
|
98
|
+
options: Array<{ label: string; value?: string; description?: string; preview?: string }>;
|
|
99
|
+
multiSelect: boolean;
|
|
100
|
+
allowOther?: boolean;
|
|
101
|
+
placeholder?: string;
|
|
102
|
+
prefill?: string;
|
|
103
|
+
};
|
|
104
|
+
type AskAnswer = { id: string; selectedOptions: string[]; customInput?: string };
|
|
105
|
+
|
|
106
|
+
function uniqueLabel(base: string, used: Set<string>): string {
|
|
107
|
+
let label = base;
|
|
108
|
+
let suffix = 2;
|
|
109
|
+
while (used.has(label)) {
|
|
110
|
+
label = `${base} (${suffix})`;
|
|
111
|
+
suffix += 1;
|
|
112
|
+
}
|
|
113
|
+
used.add(label);
|
|
114
|
+
return label;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Paseo's question card submits a multi-select answer as one string: checked option labels in
|
|
119
|
+
* click order, then any typed answer, joined with ", ". Exact labels are consumed from the front,
|
|
120
|
+
* longest first so a label containing ", " wins over its prefix; the rest is the typed answer.
|
|
121
|
+
*/
|
|
122
|
+
function splitMultiSelectAnswer(
|
|
123
|
+
answer: string,
|
|
124
|
+
labels: readonly string[],
|
|
125
|
+
): { selected: string[]; custom: string } {
|
|
126
|
+
const candidates = [...labels].sort((left, right) => right.length - left.length);
|
|
127
|
+
const selected: string[] = [];
|
|
128
|
+
let remaining = answer;
|
|
129
|
+
while (remaining.length > 0) {
|
|
130
|
+
const label = candidates.find(
|
|
131
|
+
(candidate) =>
|
|
132
|
+
!selected.includes(candidate) &&
|
|
133
|
+
(remaining === candidate || remaining.startsWith(`${candidate}, `)),
|
|
134
|
+
);
|
|
135
|
+
if (label === undefined) break;
|
|
136
|
+
selected.push(label);
|
|
137
|
+
remaining = remaining === label ? "" : remaining.slice(label.length + 2);
|
|
138
|
+
}
|
|
139
|
+
return { selected, custom: remaining };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Converts one card answer to OMP's shape. OMP rejects unknown or repeated labels and, for
|
|
144
|
+
* single-select questions, a selection combined with typed text, so neither is ever produced.
|
|
145
|
+
*/
|
|
146
|
+
function askAnswer(question: PendingAskQuestion, answer: unknown): AskAnswer {
|
|
147
|
+
const parts =
|
|
148
|
+
answer === undefined || answer === null
|
|
149
|
+
? []
|
|
150
|
+
: typeof answer === "string"
|
|
151
|
+
? [answer]
|
|
152
|
+
: Array.isArray(answer) && answer.every((part) => typeof part === "string")
|
|
153
|
+
? answer
|
|
154
|
+
: undefined;
|
|
155
|
+
if (!parts) throw new OmpPublicError("OMP question response is invalid");
|
|
156
|
+
const labels = [...question.nativeLabels.keys()];
|
|
157
|
+
const selectedOptions: string[] = [];
|
|
158
|
+
const custom: string[] = [];
|
|
159
|
+
const select = (label: string) => {
|
|
160
|
+
const native = question.nativeLabels.get(label);
|
|
161
|
+
if (native !== undefined && !selectedOptions.includes(native)) selectedOptions.push(native);
|
|
162
|
+
};
|
|
163
|
+
for (const part of parts) {
|
|
164
|
+
if (question.nativeLabels.has(part)) {
|
|
165
|
+
select(part);
|
|
166
|
+
} else if (question.multi) {
|
|
167
|
+
const split = splitMultiSelectAnswer(part, labels);
|
|
168
|
+
for (const label of split.selected) select(label);
|
|
169
|
+
if (split.custom.trim()) custom.push(split.custom);
|
|
170
|
+
} else if (part.trim()) {
|
|
171
|
+
custom.push(part);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const customInput = custom.join(", ").trim();
|
|
175
|
+
if (
|
|
176
|
+
customInput.includes("\0") ||
|
|
177
|
+
utf8Bytes(customInput) > MAX_FREEFORM_RESPONSE_BYTES ||
|
|
178
|
+
(!question.multi && selectedOptions.length + (customInput ? 1 : 0) > 1)
|
|
179
|
+
) {
|
|
180
|
+
throw new OmpPublicError("OMP question response is invalid");
|
|
181
|
+
}
|
|
182
|
+
return { id: question.id, selectedOptions, ...(customInput ? { customInput } : {}) };
|
|
183
|
+
}
|
|
184
|
+
|
|
78
185
|
interface PermissionContext {
|
|
79
186
|
runtime: OmpRuntimeSession;
|
|
80
187
|
generation: number;
|
|
@@ -279,7 +386,7 @@ export class OmpSessionPermissions {
|
|
|
279
386
|
}
|
|
280
387
|
|
|
281
388
|
submitPendingFreeformSelection(
|
|
282
|
-
request: Extract<OmpQuestionRequest, { method: "input" }>,
|
|
389
|
+
request: Extract<OmpQuestionRequest, { method: "input" | "editor" }>,
|
|
283
390
|
): boolean {
|
|
284
391
|
const pending = this.pendingFreeformSelection;
|
|
285
392
|
if (!pending) return false;
|
|
@@ -505,10 +612,17 @@ export class OmpSessionPermissions {
|
|
|
505
612
|
if (!existingId) this.permissionSequence += 1;
|
|
506
613
|
const id =
|
|
507
614
|
existingId ?? `omp:permission:${this.permissionNamespace}:${this.permissionSequence}`;
|
|
508
|
-
const header = this.dataFilter.text(
|
|
615
|
+
const header = this.dataFilter.text(
|
|
616
|
+
request.method === "ask"
|
|
617
|
+
? request.questions.length === 1
|
|
618
|
+
? "OMP question"
|
|
619
|
+
: `OMP questions (${request.questions.length})`
|
|
620
|
+
: (request.title ?? "OMP question"),
|
|
621
|
+
4_096,
|
|
622
|
+
);
|
|
509
623
|
const freeformSentinel =
|
|
510
|
-
request.method === "select"
|
|
511
|
-
?
|
|
624
|
+
request.method === "select"
|
|
625
|
+
? request.options.find((option) => OMP_FREEFORM_SENTINELS.includes(option))
|
|
512
626
|
: undefined;
|
|
513
627
|
const optionValues = new Map<string, string>();
|
|
514
628
|
const displayValues = new Map<string, string>();
|
|
@@ -523,14 +637,7 @@ export class OmpSessionPermissions {
|
|
|
523
637
|
const options =
|
|
524
638
|
request.method === "select"
|
|
525
639
|
? selectableOptions.map(({ nativeValue, index }) => {
|
|
526
|
-
const
|
|
527
|
-
let label = baseLabel;
|
|
528
|
-
let suffix = 2;
|
|
529
|
-
while (usedOptionLabels.has(label)) {
|
|
530
|
-
label = `${baseLabel} (${suffix})`;
|
|
531
|
-
suffix += 1;
|
|
532
|
-
}
|
|
533
|
-
usedOptionLabels.add(label);
|
|
640
|
+
const label = uniqueLabel(this.dataFilter.text(nativeValue, 4_096), usedOptionLabels);
|
|
534
641
|
const value = `${id}:option:${index}`;
|
|
535
642
|
optionValues.set(value, nativeValue);
|
|
536
643
|
displayValues.set(label, nativeValue);
|
|
@@ -550,6 +657,28 @@ export class OmpSessionPermissions {
|
|
|
550
657
|
: undefined;
|
|
551
658
|
const questionOptions =
|
|
552
659
|
request.method === "confirm" ? [{ label: "Yes" }, { label: "No" }] : (options ?? []);
|
|
660
|
+
const ask = request.method === "ask" ? this.askCard(request) : undefined;
|
|
661
|
+
const cardQuestions: CardQuestion[] =
|
|
662
|
+
request.method === "ask"
|
|
663
|
+
? (ask?.questions ?? [])
|
|
664
|
+
: [
|
|
665
|
+
{
|
|
666
|
+
header,
|
|
667
|
+
question: this.dataFilter.text(
|
|
668
|
+
request.method === "confirm" ? request.message : request.title,
|
|
669
|
+
64 * 1024,
|
|
670
|
+
),
|
|
671
|
+
options: questionOptions,
|
|
672
|
+
multiSelect: false,
|
|
673
|
+
...(freeformSentinel ? { allowOther: true } : {}),
|
|
674
|
+
...(request.method === "input" && request.placeholder
|
|
675
|
+
? { placeholder: this.dataFilter.text(request.placeholder, 4_096) }
|
|
676
|
+
: {}),
|
|
677
|
+
...((request.method === "input" || request.method === "editor") && request.prefill
|
|
678
|
+
? { prefill: this.dataFilter.text(request.prefill) }
|
|
679
|
+
: {}),
|
|
680
|
+
},
|
|
681
|
+
];
|
|
553
682
|
const actions =
|
|
554
683
|
request.method === "select"
|
|
555
684
|
? [
|
|
@@ -591,7 +720,7 @@ export class OmpSessionPermissions {
|
|
|
591
720
|
runtime: this.runtime,
|
|
592
721
|
request,
|
|
593
722
|
retainedBytes: boundedJsonBytes(
|
|
594
|
-
{ request, header,
|
|
723
|
+
{ request, header, questions: cardQuestions, actions },
|
|
595
724
|
MAX_PENDING_PERMISSION_BYTES,
|
|
596
725
|
512,
|
|
597
726
|
MAX_PENDING_PERMISSION_BYTES,
|
|
@@ -600,6 +729,7 @@ export class OmpSessionPermissions {
|
|
|
600
729
|
...(this.activeTurn ? { turnId: this.activeTurn.turnId } : {}),
|
|
601
730
|
...(request.timeout !== undefined ? { expiresAt: Date.now() + request.timeout } : {}),
|
|
602
731
|
...(freeformSentinel ? { freeformSentinel } : {}),
|
|
732
|
+
...(ask ? { askQuestions: ask.pending } : {}),
|
|
603
733
|
};
|
|
604
734
|
let retainedPermissionBytes = pending.retainedBytes;
|
|
605
735
|
for (const [permissionId, retained] of this.pendingPermissions) {
|
|
@@ -647,26 +777,7 @@ export class OmpSessionPermissions {
|
|
|
647
777
|
...(request.method === "confirm"
|
|
648
778
|
? { description: this.dataFilter.text(request.message, 64 * 1024) }
|
|
649
779
|
: {}),
|
|
650
|
-
input: {
|
|
651
|
-
questions: [
|
|
652
|
-
{
|
|
653
|
-
header,
|
|
654
|
-
question: this.dataFilter.text(
|
|
655
|
-
request.method === "confirm" ? request.message : request.title,
|
|
656
|
-
64 * 1024,
|
|
657
|
-
),
|
|
658
|
-
options: questionOptions,
|
|
659
|
-
multiSelect: false,
|
|
660
|
-
...(freeformSentinel ? { allowOther: true } : {}),
|
|
661
|
-
...(request.method === "input" && request.placeholder
|
|
662
|
-
? { placeholder: this.dataFilter.text(request.placeholder, 4_096) }
|
|
663
|
-
: {}),
|
|
664
|
-
...((request.method === "input" || request.method === "editor") && request.prefill
|
|
665
|
-
? { prefill: this.dataFilter.text(request.prefill) }
|
|
666
|
-
: {}),
|
|
667
|
-
},
|
|
668
|
-
],
|
|
669
|
-
},
|
|
780
|
+
input: { questions: cardQuestions },
|
|
670
781
|
actions,
|
|
671
782
|
},
|
|
672
783
|
});
|
|
@@ -688,6 +799,76 @@ export class OmpSessionPermissions {
|
|
|
688
799
|
.catch(() => this.handleRuntimeFailure());
|
|
689
800
|
}
|
|
690
801
|
|
|
802
|
+
/**
|
|
803
|
+
* Renders every `ask` question on one card. Headers key the card's answers and option labels
|
|
804
|
+
* come back verbatim, so both are made unique here and mapped back to OMP's labels on submit.
|
|
805
|
+
*/
|
|
806
|
+
private askCard(request: OmpAskRequest): {
|
|
807
|
+
questions: CardQuestion[];
|
|
808
|
+
pending: PendingAskQuestion[];
|
|
809
|
+
} {
|
|
810
|
+
const usedHeaders = new Set<string>();
|
|
811
|
+
const questions: CardQuestion[] = [];
|
|
812
|
+
const pending: PendingAskQuestion[] = [];
|
|
813
|
+
for (const [index, question] of request.questions.entries()) {
|
|
814
|
+
const header = uniqueLabel(
|
|
815
|
+
this.dataFilter.text(question.header?.trim() || `Question ${index + 1}`, 4_096),
|
|
816
|
+
usedHeaders,
|
|
817
|
+
);
|
|
818
|
+
const usedLabels = new Set<string>();
|
|
819
|
+
const nativeLabels = new Map<string, string>();
|
|
820
|
+
const options = question.options.map((option, optionIndex) => {
|
|
821
|
+
const label = uniqueLabel(
|
|
822
|
+
`${this.dataFilter.text(option.label, 4_096)}${
|
|
823
|
+
optionIndex === question.recommended ? OMP_ASK_RECOMMENDED_SUFFIX : ""
|
|
824
|
+
}`,
|
|
825
|
+
usedLabels,
|
|
826
|
+
);
|
|
827
|
+
nativeLabels.set(label, option.label);
|
|
828
|
+
return {
|
|
829
|
+
label,
|
|
830
|
+
...(option.description
|
|
831
|
+
? { description: this.dataFilter.text(option.description, 16_384) }
|
|
832
|
+
: {}),
|
|
833
|
+
...(option.preview ? { preview: this.dataFilter.text(option.preview, 64 * 1024) } : {}),
|
|
834
|
+
};
|
|
835
|
+
});
|
|
836
|
+
const multi = question.multi === true;
|
|
837
|
+
questions.push({
|
|
838
|
+
header,
|
|
839
|
+
question: this.dataFilter.text(question.question, 64 * 1024),
|
|
840
|
+
options,
|
|
841
|
+
multiSelect: multi,
|
|
842
|
+
allowOther: true,
|
|
843
|
+
});
|
|
844
|
+
pending.push({ id: question.id, header, multi, nativeLabels });
|
|
845
|
+
}
|
|
846
|
+
return { questions, pending };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/** Maps card answers to OMP's per-question `selectedOptions`/`customInput` in question order. */
|
|
850
|
+
private askResponse(
|
|
851
|
+
pending: PendingPermission,
|
|
852
|
+
response: ProviderPermissionResponse,
|
|
853
|
+
): OmpExtensionUiResponse {
|
|
854
|
+
const answers = response.behavior === "allow" ? response.updatedInput?.answers : undefined;
|
|
855
|
+
if (
|
|
856
|
+
!pending.askQuestions ||
|
|
857
|
+
!answers ||
|
|
858
|
+
typeof answers !== "object" ||
|
|
859
|
+
Array.isArray(answers)
|
|
860
|
+
) {
|
|
861
|
+
throw new OmpPublicError("OMP question response requires answers");
|
|
862
|
+
}
|
|
863
|
+
return {
|
|
864
|
+
type: "extension_ui_response",
|
|
865
|
+
id: pending.nativeId,
|
|
866
|
+
answers: pending.askQuestions.map((question) =>
|
|
867
|
+
askAnswer(question, (answers as Record<string, unknown>)[question.header]),
|
|
868
|
+
),
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
691
872
|
private freeformSelection(
|
|
692
873
|
pending: PendingPermission,
|
|
693
874
|
response: ProviderPermissionResponse,
|
|
@@ -754,6 +935,7 @@ export class OmpSessionPermissions {
|
|
|
754
935
|
if (response.behavior === "deny") {
|
|
755
936
|
return { type: "extension_ui_response", id: nativeId, cancelled: true };
|
|
756
937
|
}
|
|
938
|
+
if (request.method === "ask") return this.askResponse(pending, response);
|
|
757
939
|
const selectedValue = response.selectedActionId
|
|
758
940
|
? pending.optionValues.get(response.selectedActionId)
|
|
759
941
|
: undefined;
|
|
@@ -2280,13 +2280,17 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2280
2280
|
this.permissions.resolvePermissionByNativeId(event.targetId ?? "");
|
|
2281
2281
|
return;
|
|
2282
2282
|
}
|
|
2283
|
-
if (
|
|
2283
|
+
if (
|
|
2284
|
+
(event.method === "input" || event.method === "editor") &&
|
|
2285
|
+
this.permissions.submitPendingFreeformSelection(event)
|
|
2286
|
+
)
|
|
2284
2287
|
return;
|
|
2285
2288
|
if (
|
|
2286
2289
|
event.method === "select" ||
|
|
2287
2290
|
event.method === "confirm" ||
|
|
2288
2291
|
event.method === "input" ||
|
|
2289
|
-
event.method === "editor"
|
|
2292
|
+
event.method === "editor" ||
|
|
2293
|
+
event.method === "ask"
|
|
2290
2294
|
) {
|
|
2291
2295
|
if (!this.capabilities.includes("permission")) {
|
|
2292
2296
|
this.handleRuntimeFailure();
|
|
@@ -2459,7 +2463,9 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2459
2463
|
if (event.type === "tool_execution_start") turn.activeToolCallIds.add(event.toolCallId);
|
|
2460
2464
|
if (event.type === "tool_execution_end") {
|
|
2461
2465
|
turn.activeToolCallIds.delete(event.toolCallId);
|
|
2462
|
-
if (event.toolName === "ask_user"
|
|
2466
|
+
if (event.toolName === "ask_user" || event.toolName === "ask") {
|
|
2467
|
+
this.permissions.clearPendingFreeformSelection();
|
|
2468
|
+
}
|
|
2463
2469
|
}
|
|
2464
2470
|
if (event.type === "tool_execution_start" || event.type === "tool_execution_end") {
|
|
2465
2471
|
try {
|