mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
refactor(server): share the question card's multi-select answer splitting
OpenCode v2 and OMP both read the question card's comma-joined multi-select answer back into option labels. Keep that in one helper next to the providers so the two cannot drift. Co-authored-by: Grant Ammons <grant@grant.dev>
This commit is contained in:
co-authored by
Grant Ammons
parent
7ae1ffc913
commit
85fdc875f6
@@ -3,6 +3,7 @@ import type {
|
||||
AgentPermissionResponse,
|
||||
AgentProvider,
|
||||
} from "../../agent-sdk-types.js";
|
||||
import { splitMultiSelectAnswer } from "../question-card-answer.js";
|
||||
import type { OmpAgentMessage, OmpRuntimeEvent } from "./rpc-types.js";
|
||||
|
||||
type UiRequest = Extract<OmpRuntimeEvent, { type: "extension_ui_request" }>;
|
||||
@@ -176,21 +177,7 @@ function selectedAnswer(
|
||||
return labels.includes(answer)
|
||||
? { selected: [answer], custom: null }
|
||||
: { selected: [], custom: answer };
|
||||
|
||||
// The shared question UI sends a comma-joined answer in click order. Consume
|
||||
// exact option labels from the front; any remaining text is the Other answer.
|
||||
let remaining = answer;
|
||||
const selected: string[] = [];
|
||||
while (remaining.length > 0) {
|
||||
const label = labels
|
||||
.filter((candidate) => !selected.includes(candidate))
|
||||
.sort((left, right) => right.length - left.length)
|
||||
.find((candidate) => remaining === candidate || remaining.startsWith(`${candidate}, `));
|
||||
if (!label) break;
|
||||
selected.push(label);
|
||||
remaining = remaining === label ? "" : remaining.slice(label.length + 2);
|
||||
}
|
||||
return { selected, custom: remaining || null };
|
||||
return splitMultiSelectAnswer(answer, labels);
|
||||
}
|
||||
|
||||
/** Owns OMP question mapping and the ask tool's multi-RPC answer sequence. */
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
AgentSessionConfig,
|
||||
AgentStreamEvent,
|
||||
} from "../../../agent-sdk-types.js";
|
||||
import { splitMultiSelectAnswer } from "../../question-card-answer.js";
|
||||
|
||||
export class SessionPermissions {
|
||||
private readonly pending = new Map<string, AgentPermissionRequest>();
|
||||
@@ -163,31 +164,10 @@ function multiSelectAnswer(field: MultiSelectField, value: unknown): string[] |
|
||||
if (Array.isArray(value) && value.every((item: unknown) => typeof item === "string"))
|
||||
return value;
|
||||
if (typeof value !== "string") return undefined;
|
||||
const { selected, custom } = splitJoinedLabels(
|
||||
const { selected, custom } = splitMultiSelectAnswer(
|
||||
value,
|
||||
field.options.map((option) => option.label),
|
||||
);
|
||||
if (custom === null) return selected;
|
||||
return field.custom === true ? [...selected, custom] : undefined;
|
||||
}
|
||||
|
||||
// The shared question card sends a multi-select answer as one comma-joined string in
|
||||
// click order. Consume exact option labels from the front, longest first so a label
|
||||
// containing ", " wins over its prefix; any remaining text is the typed answer.
|
||||
function splitJoinedLabels(
|
||||
answer: string,
|
||||
labels: string[],
|
||||
): { selected: string[]; custom: string | null } {
|
||||
let remaining = answer;
|
||||
const selected: string[] = [];
|
||||
while (remaining.length > 0) {
|
||||
const label = labels
|
||||
.filter((candidate) => !selected.includes(candidate))
|
||||
.sort((left, right) => right.length - left.length)
|
||||
.find((candidate) => remaining === candidate || remaining.startsWith(`${candidate}, `));
|
||||
if (!label) break;
|
||||
selected.push(label);
|
||||
remaining = remaining === label ? "" : remaining.slice(label.length + 2);
|
||||
}
|
||||
return { selected, custom: remaining || null };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { splitMultiSelectAnswer } from "./question-card-answer.js";
|
||||
|
||||
describe("splitMultiSelectAnswer", () => {
|
||||
const labels = ["Delete both", "Delete both, fix code", "Email the user"];
|
||||
|
||||
test("splits checked labels in click order", () => {
|
||||
expect(splitMultiSelectAnswer("Email the user, Delete both", labels)).toEqual({
|
||||
selected: ["Email the user", "Delete both"],
|
||||
custom: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("keeps a label that contains a comma as one selection", () => {
|
||||
expect(splitMultiSelectAnswer("Delete both, fix code, Email the user", labels)).toEqual({
|
||||
selected: ["Delete both, fix code", "Email the user"],
|
||||
custom: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("returns trailing text as the typed answer", () => {
|
||||
expect(splitMultiSelectAnswer("Delete both, Also check the logs", labels)).toEqual({
|
||||
selected: ["Delete both"],
|
||||
custom: "Also check the logs",
|
||||
});
|
||||
expect(splitMultiSelectAnswer("Only typed text", labels)).toEqual({
|
||||
selected: [],
|
||||
custom: "Only typed text",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* The shared question card sends a multi-select answer as one string: the checked option
|
||||
* labels in click order, then any typed answer, joined with ", ". Consume exact option
|
||||
* labels from the front, longest first so a label containing ", " wins over its prefix;
|
||||
* any remaining text is the typed answer.
|
||||
*/
|
||||
export function splitMultiSelectAnswer(
|
||||
answer: string,
|
||||
labels: string[],
|
||||
): { selected: string[]; custom: string | null } {
|
||||
let remaining = answer;
|
||||
const selected: string[] = [];
|
||||
while (remaining.length > 0) {
|
||||
const label = labels
|
||||
.filter((candidate) => !selected.includes(candidate))
|
||||
.sort((left, right) => right.length - left.length)
|
||||
.find((candidate) => remaining === candidate || remaining.startsWith(`${candidate}, `));
|
||||
if (!label) break;
|
||||
selected.push(label);
|
||||
remaining = remaining === label ? "" : remaining.slice(label.length + 2);
|
||||
}
|
||||
return { selected, custom: remaining || null };
|
||||
}
|
||||
Reference in New Issue
Block a user