Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b2ac6de
Test pin layout without an empty overflow menu
brsbl Oct 21, 2026
057816c
Hide the pin overflow button when its list is empty
brsbl Oct 21, 2026
bef7082
Publish File Pins version tags with the existing root package layout
brsbl Oct 21, 2026
901685f
Select the root package in the marketplace install regression
brsbl Oct 21, 2026
1a95085
Use whole-label pin overflow at every width
brsbl Oct 22, 2026
23fb51f
Align pinning instructions with automatic overflow
brsbl Oct 22, 2026
efd67bb
Replace pin overflow menus with a count chip and direct unpin
brsbl Oct 22, 2026
11dc75f
Style pin overflow count as a quiet text button
brsbl Oct 22, 2026
608f24b
Merge main and preserve public pin documentation
brsbl Oct 22, 2026
95c0fe5
Tint and round the pin overflow count button
brsbl Oct 22, 2026
a30f2cc
Follow the selected app theme for overflow button tint
brsbl Oct 22, 2026
998ead6
Show count-button press feedback during touch input
brsbl Oct 22, 2026
78b775d
Strengthen overflow button contrast and center its count
brsbl Oct 22, 2026
5fdf2b4
Scroll pins sideways in one row and unpin from each pin
brsbl Oct 23, 2026
76ffcd4
Show the pin Unpin control over the icon and refresh strip images
brsbl Oct 23, 2026
78c622d
Keep focused pins clear of the strip fades and pointer clicks intact
brsbl Oct 23, 2026
4daebff
Refresh the pin menu image without the removed controls
brsbl Oct 23, 2026
c935b5e
Refresh the Pin pill image without the removed overflow button
brsbl Oct 23, 2026
d12f078
Refresh stale link previews in open cards instead of from the menu
brsbl Oct 23, 2026
02c7d31
Skip discarded staging for root plugin releases
brsbl Oct 24, 2026
6c1283f
Merge remote-tracking branch 'origin/main' into bb/pins-release-533
brsbl Oct 24, 2026
cb28107
fix: preserve preview refresh compatibility
brsbl Oct 24, 2026
d587b39
Merge remote-tracking branch 'origin/main' into bb/pins-release-531
brsbl Oct 24, 2026
f1823fc
Merge branch 'bb/pins-release-531' into bb/pins-release-533
brsbl Oct 24, 2026
f8af98b
Merge remote-tracking branch 'origin/main' into bb/pins-release-533
brsbl Oct 24, 2026
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
5 changes: 5 additions & 0 deletions tooling/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ package tree, lockfile digest, SDK archive, and builder provenance in
no install-time package dependencies. Source manifests on `main` retain the
dependencies needed for development.

Pinned Files & Links also publishes `file-pins/v<version>` tags. Its existing
marketplace entry has no subdirectory, so these tags contain the same root package
tree as `plugin/file-pins`. The tag commit records its source as its parent; the
root package does not include the monorepo provenance file.

Published version tags are immutable. A rerun accepts an existing tag only when
its plugin contents match; otherwise bump the version. Other plugins keep their
existing release process. The remote compatibility test installs the generated
Expand Down
49 changes: 49 additions & 0 deletions tooling/compatibility/plugin-sdk-upgrade.external.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,3 +362,52 @@ it("preserves Prompt Improver's marketplace prefix and package subdirectory", as
expect(manifest.bb.app).toBe("./dist/install-app.mjs");
expect(manifest.dependencies).toBeUndefined();
});


it("installs the latest File Pins tag at the marketplace root", async () => {
if (!pluginRepository) throw new Error("BB_PLUGIN_COMPAT_REPOSITORY is required");
const repository = pluginRepository;
const moduleUrl = pathToFileURL(resolve(repository, "tooling/publish-install-refs.mjs")).href;
const { prepareVersionedRelease, assertVersionTagUnchanged } = await import(moduleUrl);
await run(process.execPath, [resolve(repository, "tooling/build-plugin.mjs"), resolve(repository, "plugins/file-pins")]);
const release = await prepareVersionedRelease("file-pins");
expect(release.tag).toBe(`file-pins/v${release.plugin.manifest.version}`);
expect(release.subdirectory).toBe("");
expect(await git(repository, ["rev-parse", `${release.commit}^{tree}`])).toBe(release.packageTree);
expect((await prepareVersionedRelease("file-pins")).commit).toBe(release.commit);
expect(() => assertVersionTagUnchanged(release, release.commit)).not.toThrow();
expect(() => assertVersionTagUnchanged(release, release.sourceRevision))
.toThrow(/already exists with different contents/);

const fixture = await mkdtemp(join(tmpdir(), "file-pins-version-install-"));
let server: Awaited<ReturnType<typeof startTestServer>> | undefined;
try {
const remote = join(fixture, "release.git");
await git(repository, ["init", "--bare", remote]);
await git(remote, ["fetch", "--depth=1", "--no-tags", repository, release.commit]);
await git(remote, ["tag", release.tag, release.commit]);
const previousRevision = "80b3e89e31da407b84e9de8bee777b16eacead44";
await git(remote, ["fetch", "--depth=1", "--no-tags", "https://github.com/brsbl/bb-plugins.git", previousRevision]);
await git(remote, ["tag", "file-pins/v0.1.8", previousRevision]);

server = await startTestServer({ appVersion: "0.43.4" });
server.pluginService.bindSdk({ baseUrl: server.baseUrl });
await server.pluginService.install(`git:${remote}@semver:file-pins/:^0.1.2`, { kind: "root" });
const installed = server.pluginService.list().find((entry) => entry.id === "file-pins")!;
expect(installed).toMatchObject({ version: release.plugin.manifest.version, status: "running" });
expect(getInstalledPluginRegistration(server.db, "file-pins"))
.toMatchObject({ gitResolvedCommit: release.commit });
const manifest = JSON.parse(await readFile(join(installed.rootDir, "package.json"), "utf8"));
expect(manifest.bb.host).toBe("./dist/install-host.mjs");
expect(manifest.dependencies).toBeUndefined();
expect(manifest.devDependencies).toBeUndefined();
expect(await readFile(join(installed.rootDir, "dist/app.css"), "utf8"))
.toContain("bb-plugin-release-style");
} finally {
if (server) {
await server.pluginService.stop();
await server.close();
}
await rm(fixture, { recursive: true, force: true });
}
}, 180_000);
64 changes: 37 additions & 27 deletions tooling/publish-install-refs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ const productionDependencyFields = [
"peerDependencies",
];
// Opt in per plugin so existing release histories are never rewritten.
const versionTaggedPlugins = new Set(["thread-organizer", "context-katamari", "ambient", "server-switcher", "improve-prompt", "desktop"]);
const versionTaggedPlugins = new Set(["thread-organizer", "context-katamari", "ambient", "server-switcher", "improve-prompt", "desktop", "file-pins"]);
// Preserve the published prefix when a workspace directory differs from its stable ID.
const versionTagPrefixes = { "improve-prompt": "prompt-shaper" };
// File Pins was published at the repository root, with no marketplace subdirectory.
const versionTagSubdirectories = { "file-pins": "" };
const explicitlyRetiredInstallRefs = Object.freeze([
"plugin/omegacode",
"plugin/saved-places",
Expand Down Expand Up @@ -267,32 +269,38 @@ export async function prepareVersionedRelease(slug) {
throw new Error(`${slug}: stable release version required`);
}
const tag = `${versionTagPrefixes[slug] ?? slug}/v${version}`;
const subdirectory = versionTagSubdirectories[slug] ?? plugin.source;
const temporary = await mkdtemp(resolve(tmpdir(), "bb-plugin-tag-"));
const indexPath = resolve(temporary, "index");
try {
git(["read-tree", sourceRevision], { env: { GIT_INDEX_FILE: indexPath } });
const paths = git(["ls-files", "-z", "--", plugin.source], {
env: { GIT_INDEX_FILE: indexPath },
});
git(["update-index", "--force-remove", "-z", "--stdin"], {
env: { GIT_INDEX_FILE: indexPath }, input: paths,
});
git(["read-tree", `--prefix=${plugin.source}/`, packageTree], {
env: { GIT_INDEX_FILE: indexPath },
});
const readJson = async (path) => JSON.parse(await readFile(resolve(root, path), "utf8"));
const provenance = {
sourceCommit: sourceRevision,
plugin: slug,
version,
packageTree,
lockfileSha256: createHash("sha256")
.update(await readFile(resolve(root, "package-lock.json"))).digest("hex"),
sdk: await readJson("tooling/vendor/sdk-provenance.json"),
builder: await readJson("tooling/vendor/plugin-build-provenance.json"),
};
addBlob(indexPath, "plugin-release.json", `${JSON.stringify(provenance, null, 2)}\n`);
const tree = git(["write-tree"], { env: { GIT_INDEX_FILE: indexPath } });
// Root-shaped tags use the install package unchanged; only nested tags
// need a repository tree and a separate provenance file.
let tree = packageTree;
if (subdirectory) {
git(["read-tree", sourceRevision], { env: { GIT_INDEX_FILE: indexPath } });
const paths = git(["ls-files", "-z", "--", plugin.source], {
env: { GIT_INDEX_FILE: indexPath },
});
git(["update-index", "--force-remove", "-z", "--stdin"], {
env: { GIT_INDEX_FILE: indexPath }, input: paths,
});
git(["read-tree", `--prefix=${plugin.source}/`, packageTree], {
env: { GIT_INDEX_FILE: indexPath },
});
const readJson = async (path) => JSON.parse(await readFile(resolve(root, path), "utf8"));
const provenance = {
sourceCommit: sourceRevision,
plugin: slug,
version,
packageTree,
lockfileSha256: createHash("sha256")
.update(await readFile(resolve(root, "package-lock.json"))).digest("hex"),
sdk: await readJson("tooling/vendor/sdk-provenance.json"),
builder: await readJson("tooling/vendor/plugin-build-provenance.json"),
};
addBlob(indexPath, "plugin-release.json", `${JSON.stringify(provenance, null, 2)}\n`);
tree = git(["write-tree"], { env: { GIT_INDEX_FILE: indexPath } });
}
const sourceDate = git(["show", "-s", "--format=%cI", sourceRevision]);
const commit = git([
"commit-tree", tree, "-p", sourceRevision,
Expand All @@ -305,15 +313,17 @@ export async function prepareVersionedRelease(slug) {
GIT_AUTHOR_DATE: sourceDate,
GIT_COMMITTER_DATE: sourceDate,
} });
return { plugin, tag, commit, packageTree, sourceRevision };
return { plugin, tag, commit, packageTree, sourceRevision, subdirectory };
} finally {
await rm(temporary, { recursive: true, force: true });
}
}

export function assertVersionTagUnchanged(release, existingCommit) {
const existingPackageTree = git([
"rev-parse", `${existingCommit}:${release.plugin.source}`,
"rev-parse", release.subdirectory
? `${existingCommit}:${release.subdirectory}`
: `${existingCommit}^{tree}`,
], { allowFailure: true });
if (existingPackageTree !== release.packageTree) {
throw new Error(`${release.tag} already exists with different contents; bump the plugin version`);
Expand All @@ -335,7 +345,7 @@ async function publishVersionTag(release, push) {
console.log(`${release.tag} ${existingCommit} already published and unchanged`);
return;
}
await verifyReleaseCommit(release.plugin, release.commit, release.plugin.source);
await verifyReleaseCommit(release.plugin, release.commit, release.subdirectory);
if (push) {
// A normal tag push cannot overwrite an existing release, including a
// concurrent publisher that won the race after the lookup above.
Expand Down
Loading