Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions plugins/provider-pi/src/bridge/bridge.skill-command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { afterEach, beforeEach, expect, it } from "vitest";
import {
FULL_PERMISSION_OPTIONS,
type FakePiBridgeHarness,
startFakePiBridge,
} from "./test-support.js";

let harness: FakePiBridgeHarness;

beforeEach(async () => {
harness = await startFakePiBridge({
prefix: "bb-pi-skill-command-",
initialize: true,
});
});

afterEach(async () => {
await harness.teardown();
});

it("invokes a selected skill through Pi's native command", async () => {
const threadId = "thr_skill_command";
await harness.startThread(threadId);

const response = await harness.request(1, "turn/start", {
threadId,
providerThreadId: threadId,
clientRequestId: "creq_ab23456789",
input: [
{
type: "text",
text: "/inspect src",
mentions: [
{
start: 0,
end: "/inspect".length,
resource: {
kind: "command",
trigger: "/",
name: "inspect",
source: "skill",
origin: "user",
label: "inspect",
argumentHint: null,
},
},
],
},
],
options: FULL_PERMISSION_OPTIONS,
});

expect(response.error).toBeUndefined();
await harness.waitForTurnBoundary(threadId);
const output = harness
.deltasOf(threadId)
.filter((delta) => delta.kind === "item.textDelta")
.map((delta) => String(delta.text))
.join("");
expect(output).toContain("Response to: /skill:inspect src");
});

it("keeps selected provider commands in their displayed form", async () => {
const threadId = "thr_provider_command";
await harness.startThread(threadId);

const response = await harness.request(2, "turn/start", {
threadId,
providerThreadId: threadId,
clientRequestId: "creq_cd23456789",
input: [
{
type: "text",
text: "/inspect src",
mentions: [
{
start: 0,
end: "/inspect".length,
resource: {
kind: "command",
trigger: "/",
name: "inspect",
source: "command",
origin: "user",
label: "inspect",
argumentHint: null,
},
},
],
},
],
options: FULL_PERMISSION_OPTIONS,
});

expect(response.error).toBeUndefined();
await harness.waitForTurnBoundary(threadId);
const output = harness
.deltasOf(threadId)
.filter((delta) => delta.kind === "item.textDelta")
.map((delta) => String(delta.text))
.join("");
expect(output).toContain("Response to: /inspect src");
});
46 changes: 32 additions & 14 deletions plugins/provider-pi/src/bridge/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1161,27 +1161,45 @@ interface ExtractedInput {
images: ImageContent[];
}

type TextPromptInput = Extract<
TurnStartParams["input"][number],
{ type: "text" }
>;

function toPiPromptText(input: TextPromptInput): string {
let text = input.text;
const mentions = [...input.mentions].sort(
(left, right) => right.start - left.start || right.end - left.end,
);
for (const mention of mentions) {
const resource = mention.resource;
if (
resource.kind !== "command" ||
resource.source !== "skill" ||
input.text.slice(mention.start, mention.end) !==
`${resource.trigger}${resource.name}`
) {
continue;
}
text = `${text.slice(0, mention.start)}${resource.trigger}skill:${resource.name}${text.slice(mention.end)}`;
}
return text;
}

function extractInput(input: TurnStartParams["input"]): ExtractedInput {
const chunks: string[] = [];
const images: ImageContent[] = [];
for (const item of input) {
if (!item || typeof item !== "object") continue;
const typed = item as {
type?: string;
text?: string;
path?: string;
mimeType?: string;
};
if (typed.type === "text" && typeof typed.text === "string") {
chunks.push(typed.text);
} else if (typed.type === "localImage" && typeof typed.path === "string") {
if (item.type === "text") {
chunks.push(toPiPromptText(item));
} else if (item.type === "localImage") {
try {
const data = readFileSync(typed.path).toString("base64");
const mimeType = typed.mimeType ?? mimeTypeFromExtension(typed.path);
const data = readFileSync(item.path).toString("base64");
const mimeType = mimeTypeFromExtension(item.path);
images.push({ type: "image", data, mimeType });
} catch {}
} else if (typed.type === "localFile" && typeof typed.path === "string") {
chunks.push(`[Attached file: ${typed.path}]`);
} else if (item.type === "localFile") {
chunks.push(`[Attached file: ${item.path}]`);
}
}
return { text: chunks.length > 0 ? chunks.join("\n") : undefined, images };
Expand Down
Loading