(
+ ,
+ />
+ ),
+ },
+ {},
);
+ const markup = slot.container.innerHTML;
expect(markup.match(/| )/g)).toHaveLength(3);
expect(markup.match(/ | )/g)).toHaveLength(3);
@@ -41,5 +58,27 @@ After the table.`}
expect(markup).toContain("uses | safely");
expect(markup).toContain(" {
+ const slot = renderSlot(
+ {
+ component: () => (
+
+ ),
+ },
+ {},
+ { openUrl: () => true },
+ );
+
+ slot.getByRole("link", { name: "Open issue" }).click();
+ expect(slot.navigateCalls).toEqual([
+ {
+ method: "experimental_openUrl",
+ url: "https://github.com/get-bb/bb/issues/1",
+ },
+ ]);
+ slot.unmount();
});
});
diff --git a/plugins/github/components/markdown-lite.tsx b/plugins/github/components/markdown-lite.tsx
index f8494880fc..2b39ce926a 100644
--- a/plugins/github/components/markdown-lite.tsx
+++ b/plugins/github/components/markdown-lite.tsx
@@ -5,6 +5,7 @@
// markdown). Everything is built as React elements — img attributes are
// extracted and whitelisted, so no HTML is ever injected.
import { cn } from "@bb/shared-ui/lib/utils";
+import { experimental_UrlLink as UrlLink } from "@get-bb/plugin-sdk/app";
const INLINE_PATTERN =
// Image forms first: `` must win over the link pattern (which
@@ -91,7 +92,7 @@ function renderInline(text: string): React.ReactNode[] {
const label = token.slice(1, closeBracket);
const href = token.slice(closeBracket + 2, -1);
nodes.push(
-
{renderInline(label)}
- ,
+ ,
);
}
last = index + token.length;
diff --git a/plugins/github/package.json b/plugins/github/package.json
index 6155dcc166..7c56aa83db 100644
--- a/plugins/github/package.json
+++ b/plugins/github/package.json
@@ -23,7 +23,7 @@
],
"engines": {
"bb": ">=0.0",
- "bbPluginSdk": ">=0.4.3"
+ "bbPluginSdk": ">=0.4.10"
},
"bb": {
"name": "GitHub",
diff --git a/plugins/github/server.ts b/plugins/github/server.ts
index f6c8a19a31..f368de857c 100644
--- a/plugins/github/server.ts
+++ b/plugins/github/server.ts
@@ -228,7 +228,18 @@ export const githubRpcContract = defineRpcContract({
},
pullForThread: {
input: z.object({ threadId: z.string().min(1) }).strict(),
- output: z.object({ pull: itemInputSchema.nullable() }).strict(),
+ output: z
+ .object({
+ pull: z
+ .object({
+ repo: repoNameSchema,
+ number: itemNumberSchema,
+ environmentId: z.string().nullable(),
+ })
+ .strict()
+ .nullable(),
+ })
+ .strict(),
},
commentIssue: {
input: itemInputSchema.extend({ body: nonBlankStringSchema }).strict(),
@@ -1365,11 +1376,13 @@ export default async function plugin(bb: BbPluginApi) {
environment PR (the branch the agent pushed) first, else a PR this
thread was spawned to review. Null when neither exists. */
async pullForThread({ threadId }) {
+ let environmentId: string | null = null;
try {
const thread = (await bb.sdk.threads.get({ threadId })) as unknown as {
environmentId?: string | null;
};
if (thread?.environmentId) {
+ environmentId = thread.environmentId;
const result = await bb.sdk.environments.pullRequest({
environmentId: thread.environmentId,
});
@@ -1380,7 +1393,13 @@ export default async function plugin(bb: BbPluginApi) {
? url.match(/github\.com\/([\w.-]+\/[\w.-]+)\/pull\/(\d+)/)
: null;
if (match !== null) {
- return { pull: { repo: match[1], number: Number(match[2]) } };
+ return {
+ pull: {
+ repo: match[1],
+ number: Number(match[2]),
+ environmentId,
+ },
+ };
}
}
} catch {
@@ -1391,7 +1410,13 @@ export default async function plugin(bb: BbPluginApi) {
const match = key.match(/^pr:([\w.-]+\/[\w.-]+)#(\d+)$/);
if (match === null) continue;
if (threadLinks.some((link) => link.threadId === threadId)) {
- return { pull: { repo: match[1], number: Number(match[2]) } };
+ return {
+ pull: {
+ repo: match[1],
+ number: Number(match[2]),
+ environmentId,
+ },
+ };
}
}
return { pull: null };
diff --git a/plugins/tasks/app.tsx b/plugins/tasks/app.tsx
index 4a6c41f069..69aa68db86 100644
--- a/plugins/tasks/app.tsx
+++ b/plugins/tasks/app.tsx
@@ -14,6 +14,7 @@ export default definePluginApp((app) => {
experimental_sidebarAccessory: TasksSidebarAccessory,
experimental_fixedTabs: [
{
+ panelId: "tasks",
id: "navigation",
title: "Navigation",
icon: "ListView",
diff --git a/plugins/tasks/package.json b/plugins/tasks/package.json
index f6aab97621..ffb845b6aa 100644
--- a/plugins/tasks/package.json
+++ b/plugins/tasks/package.json
@@ -23,7 +23,7 @@
],
"engines": {
"bb": ">=0.0",
- "bbPluginSdk": ">=0.4.8"
+ "bbPluginSdk": ">=0.4.10"
},
"bb": {
"name": "Tasks",
diff --git a/plugins/tasks/views/detail/threads.test.tsx b/plugins/tasks/views/detail/threads.test.tsx
index 00b359b5e8..8e4b7bc1d5 100644
--- a/plugins/tasks/views/detail/threads.test.tsx
+++ b/plugins/tasks/views/detail/threads.test.tsx
@@ -1,5 +1,5 @@
// @vitest-environment jsdom
-import { cleanup, waitFor } from "@testing-library/react";
+import { cleanup, fireEvent, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { loadPluginApp, renderSlot } from "@get-bb/plugin-sdk/testing/app";
@@ -101,6 +101,7 @@ describe("task detail pull request pills", () => {
app.navPanels[0]!,
{ subPath: "task/TSK-5" },
{
+ openUrl: () => true,
rpc: detailRpc({
listTaskPullRequests: () => ({
pullRequests: [
@@ -127,6 +128,11 @@ describe("task detail pull request pills", () => {
expect(link.target).toBe("_blank");
expect(link.rel).toContain("noopener");
expect(link.textContent).toContain("#12");
+ fireEvent.click(link);
+ expect(slot.navigateCalls).toContainEqual({
+ method: "experimental_openUrl",
+ url: "https://github.com/acme/bb/pull/12",
+ });
});
it("marks threads whose PR lookup failed and stays quiet otherwise", async () => {
diff --git a/plugins/tasks/views/detail/threads.tsx b/plugins/tasks/views/detail/threads.tsx
index 36b90bb5c5..597bef0089 100644
--- a/plugins/tasks/views/detail/threads.tsx
+++ b/plugins/tasks/views/detail/threads.tsx
@@ -1,5 +1,9 @@
import { useState } from "react";
-import { useBbNavigate, useRpc } from "@get-bb/plugin-sdk/app";
+import {
+ experimental_UrlLink as UrlLink,
+ useBbNavigate,
+ useRpc,
+} from "@get-bb/plugin-sdk/app";
import type { DelegationRpcContract } from "../../delegate/contract.js";
import type {
Preset,
@@ -40,7 +44,7 @@ function ThreadPullRequestPill({
if (pullRequest) {
const meta = PR_STATE_META[pullRequest.state];
return (
-
#
{pullRequest.number}
-
+
);
}
if (unavailable) {
diff --git a/plugins/workflows/src/app.test.tsx b/plugins/workflows/src/app.test.tsx
index e7d28f0d9e..6ff44de568 100644
--- a/plugins/workflows/src/app.test.tsx
+++ b/plugins/workflows/src/app.test.tsx
@@ -95,7 +95,12 @@ describe("workflows app registration", () => {
"workflow-preview",
]);
expect(app.threadPanelActions).toMatchObject([
- { id: "workflow-run", title: "Workflow run", icon: "Workflow" },
+ {
+ id: "workflow-run",
+ title: "Workflow run",
+ icon: "Workflow",
+ layout: "flush",
+ },
]);
});
});
@@ -713,6 +718,7 @@ describe("workflow thread panel", () => {
expect((await slot.findByRole("alert")).textContent).toMatch(
/invalid run parameters/i,
);
+ expect(slot.getByRole("alert").parentElement?.className).toContain("p-4");
expect(slot.rpcCalls).toEqual([]);
});
});
diff --git a/plugins/workflows/src/app.tsx b/plugins/workflows/src/app.tsx
index a9eb2c0fd0..5dff85e37a 100644
--- a/plugins/workflows/src/app.tsx
+++ b/plugins/workflows/src/app.tsx
@@ -860,14 +860,17 @@ function WorkflowPreviewLoaded({
function WorkflowRunPanel({ threadId, params }: PluginThreadPanelProps) {
const runId = panelRunId(params);
- if (runId === undefined) {
- return (
-
- This workflow panel has invalid run parameters.
-
- );
- }
- return ;
+ return (
+
+ {runId === undefined ? (
+
+ This workflow panel has invalid run parameters.
+
+ ) : (
+
+ )}
+
+ );
}
function WorkflowRunPanelLoaded({
@@ -921,7 +924,7 @@ function WorkflowRunPanelLoaded({
@@ -1043,5 +1046,6 @@ export default definePluginApp((app) => {
title: "Workflow run",
icon: "Workflow",
component: WorkflowRunPanel,
+ layout: "flush",
});
});
|