Skip to content

Commit 64fa3b5

Browse files
committed
Finalize 0.4.5 SDK compatibility
1 parent 60d6a02 commit 64fa3b5

9 files changed

Lines changed: 111 additions & 60 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ After install, open Paperclip and use the `Focus` entry in the sidebar.
4848

4949
## Troubleshooting
5050

51-
- If the sidebar shows a bordered placeholder instead of a native `Focus` entry, upgrade to Paperclip `2026.517.0` or newer and reinstall this plugin. `0.4.5` declares a host-rendered sidebar launcher instead of a custom sidebar slot.
51+
- If the sidebar shows a bordered placeholder instead of the `Focus` entry, upgrade to Paperclip `2026.525.0` or newer and reinstall this plugin. `0.4.5` preserves the original Focus sidebar icon and uses the current host invocation scope.
5252
- `paperclipApiBase` is only needed for approval overlays. Use the Paperclip origin such as `https://HOST` or `http://127.0.0.1:3100`; leaving it empty disables approval overlay reads without disabling Focus.
5353
- On Windows, `spawn npm ENOENT` during `npx paperclipai plugin install @tomismeta/paperclip-aperture` comes from the Paperclip host installer failing to spawn npm. This package does not spawn npm during install. Make sure the Paperclip process can resolve `npm.cmd`, or upgrade Paperclip once the Windows installer fix is available.
5454

@@ -132,7 +132,7 @@ For `0.4.x`, the boundary works like this:
132132
- `activity.logged` document events invalidate stale reconciled state so document-backed review blockers refresh promptly without a full browser-side merge layer
133133
- Focus exports the live Core snapshot, the reconciled/plugin-composed display snapshot, and bounded Core traces so replay/debug flows can inspect both the engine substrate and the final operator view
134134

135-
The plugin requires Paperclip `2026.517.0` or newer and has been validated against [`@tomismeta/aperture-core@0.7.0`](https://www.npmjs.com/package/@tomismeta/aperture-core) and [`@paperclipai/plugin-sdk@2026.517.0`](https://www.npmjs.com/package/@paperclipai/plugin-sdk).
135+
The plugin requires Paperclip `2026.525.0` or newer and has been validated against [`@tomismeta/aperture-core@0.7.0`](https://www.npmjs.com/package/@tomismeta/aperture-core) and [`@paperclipai/plugin-sdk@2026.525.0`](https://www.npmjs.com/package/@paperclipai/plugin-sdk).
136136

137137
Approval overlay transport is opt-in until the Paperclip plugin SDK exposes a first-class approval client. Set the plugin config field `paperclipApiBase` when the worker can reach the host approval API; leave it empty to run Focus without approval overlays in hosted or restricted-network environments.
138138

docs/RELEASE-0.4.5.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ uses the newer host attention contracts where they improve Focus judgment.
55

66
## Highlights
77

8-
- upgraded `@paperclipai/plugin-sdk` from `2026.428.0` to `2026.517.0`
9-
- declares Paperclip `2026.517.0` as the minimum host version
8+
- upgraded `@paperclipai/plugin-sdk` from `2026.428.0` to `2026.525.0`
9+
- declares Paperclip `2026.525.0` as the minimum host version
1010
- surfaces Paperclip Blocked Inbox attention as first-class Focus evidence
1111
- surfaces active Paperclip recovery actions directly in Focus
1212
- keeps planning-mode blockers calmer unless the host marks them urgent
1313
- preserves document lock metadata in review handoff copy and diagnostics
1414
- excludes plugin-operation issues from host reconciliation scans
15-
- uses a host-rendered sidebar launcher for the Focus entry
15+
- preserves the original Focus sidebar icon
16+
- preserves UI bundle import syntax so Paperclip's host-side plugin loader can rewrite React and SDK imports reliably
17+
- relies on the existing polling refresh path instead of opening the optional UI stream bridge on hosts where streams are not enabled
1618
- makes agent run failure cards acknowledgeable and clears issue-linked failures when the issue moves on
1719
- documents the Windows `spawn npm ENOENT` Paperclip installer failure mode
1820

@@ -29,4 +31,4 @@ uses the newer host attention contracts where they improve Focus judgment.
2931

3032
## Validation
3133

32-
- `pnpm verify`
34+
- `pnpm release:check`

esbuild.config.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,23 @@ if (!worker || !manifest || !ui) {
99
throw new Error("Plugin bundler presets are missing required esbuild targets.");
1010
}
1111

12-
function buildOptions(options: esbuild.BuildOptions): esbuild.BuildOptions {
12+
function buildOptions(
13+
options: esbuild.BuildOptions,
14+
buildTarget: "manifest" | "ui" | "worker",
15+
): esbuild.BuildOptions {
1316
if (watch) return options;
1417

1518
return {
1619
...options,
1720
legalComments: "none",
18-
minify: true,
21+
minify: buildTarget !== "ui",
1922
sourcemap: false,
2023
};
2124
}
2225

23-
const workerContext = await esbuild.context(buildOptions(worker));
24-
const manifestContext = await esbuild.context(buildOptions(manifest));
25-
const uiContext = await esbuild.context(buildOptions(ui));
26+
const workerContext = await esbuild.context(buildOptions(worker, "worker"));
27+
const manifestContext = await esbuild.context(buildOptions(manifest, "manifest"));
28+
const uiContext = await esbuild.context(buildOptions(ui, "ui"));
2629

2730
if (watch) {
2831
await Promise.all([

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"access": "public"
5555
},
5656
"dependencies": {
57-
"@paperclipai/plugin-sdk": "2026.517.0",
57+
"@paperclipai/plugin-sdk": "2026.525.0",
5858
"@tomismeta/aperture-core": "0.7.0"
5959
},
6060
"devDependencies": {

pnpm-lock.yaml

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/manifest.ts

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const manifest: PaperclipPluginManifestV1 = {
44
id: "tomismeta.paperclip-aperture",
55
apiVersion: 1,
66
version: "0.4.5",
7-
minimumHostVersion: "2026.517.0",
7+
minimumHostVersion: "2026.525.0",
88
displayName: "Paperclip Aperture",
99
description: "The live attention layer for Paperclip, combining Aperture Core continuity with Paperclip-native operator policy.",
1010
author: "@tomismeta",
@@ -62,29 +62,18 @@ const manifest: PaperclipPluginManifestV1 = {
6262
exportName: "AttentionPage",
6363
routePath: "aperture"
6464
},
65+
{
66+
type: "sidebar",
67+
id: "attention-sidebar-link",
68+
displayName: "Focus",
69+
exportName: "AttentionSidebarLink"
70+
},
6571
{
6672
type: "dashboardWidget",
6773
id: "attention-widget",
6874
displayName: "Focus",
6975
exportName: "DashboardWidget"
7076
}
71-
],
72-
launchers: [
73-
{
74-
id: "focus-sidebar",
75-
displayName: "Focus",
76-
description: "Open the Paperclip Aperture Focus surface.",
77-
placementZone: "sidebar",
78-
order: 40,
79-
action: {
80-
type: "navigate",
81-
target: "/aperture"
82-
},
83-
render: {
84-
environment: "hostRoute",
85-
bounds: "full"
86-
}
87-
}
8877
]
8978
}
9079
};

src/ui/focus-model.tsx

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import { usePluginData, usePluginStream } from "@paperclipai/plugin-sdk/ui";
1+
import { usePluginData } from "@paperclipai/plugin-sdk/ui";
22
import { useEffect, useMemo, useRef, useState } from "react";
33
import type { AttentionDisplayPayload, AttentionReviewState, AttentionSnapshot, StoredAttentionFrame } from "../aperture/types.js";
44

5-
const ATTENTION_UPDATES_STREAM = "attention-updates";
6-
75
export type QueueMovement = "up" | "down";
86

97
export type Posture = {
@@ -175,18 +173,9 @@ export function useAttentionModel(companyId: string | null | undefined): {
175173
refresh: () => void;
176174
} {
177175
const displayQuery = usePluginData<AttentionDisplayPayload>("attention-display", companyId ? { companyId } : undefined);
178-
const updates = usePluginStream<{ updatedAt: string; eventType: string }>(
179-
ATTENTION_UPDATES_STREAM,
180-
companyId ? { companyId } : undefined,
181-
);
182176

183177
useAttentionPolling(companyId, [displayQuery.refresh]);
184178

185-
useEffect(() => {
186-
if (!companyId || !updates.lastEvent) return;
187-
displayQuery.refresh();
188-
}, [companyId, updates.lastEvent?.updatedAt, updates.lastEvent?.eventType]);
189-
190179
return useMemo(() => ({
191180
snapshot: displayQuery.data?.snapshot ?? null,
192181
review: displayQuery.data?.reviewState ?? null,

src/ui/index.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
type PluginPageProps,
3+
type PluginSidebarProps,
34
type PluginWidgetProps,
45
} from "@paperclipai/plugin-sdk/ui";
56
import { useEffect, useMemo, useState } from "react";
@@ -16,6 +17,7 @@ import {
1617
GENERIC_QUEUED_JUDGMENT,
1718
} from "../aperture/attention-language.js";
1819
import {
20+
ACCENT_BG_STYLE,
1921
ACCENT_BORDER,
2022
ACCENT_COLOR,
2123
Accent,
@@ -33,6 +35,7 @@ import {
3335
useWideLayout,
3436
} from "./chrome.js";
3537
import {
38+
actionableCount,
3639
actionableLabel,
3740
currentSurfaceBrand,
3841
formatRelativeTime,
@@ -44,6 +47,7 @@ import {
4447
type SurfaceBrand,
4548
useAttentionModel,
4649
useQueueMovement,
50+
unreadCount,
4751
} from "./focus-model.js";
4852
import {
4953
approvalIdForFrame,
@@ -760,6 +764,73 @@ export function DashboardWidget(props: PluginWidgetProps) {
760764
);
761765
}
762766

767+
// ---------------------------------------------------------------------------
768+
// Sidebar link — Paperclip launcher chrome with the original Focus glyph
769+
// ---------------------------------------------------------------------------
770+
771+
export function AttentionSidebarLink({ context }: PluginSidebarProps) {
772+
const companyId = context.companyId;
773+
const brand = currentSurfaceBrand();
774+
const href = pluginPagePath(context.companyPrefix);
775+
const isActive = typeof window !== "undefined" && window.location.pathname === href;
776+
const model = useAttentionModel(companyId);
777+
const merged = model.snapshot;
778+
const actionable = merged ? actionableCount(merged) : 0;
779+
const unread = unreadCount(merged);
780+
781+
return (
782+
<a
783+
href={href}
784+
aria-current={isActive ? "page" : undefined}
785+
className={cn(
786+
"flex items-center gap-2.5 px-3 py-2 text-[13px] font-medium transition-colors",
787+
isActive
788+
? "bg-accent text-foreground"
789+
: "text-foreground/80 hover:bg-accent/50 hover:text-foreground",
790+
)}
791+
>
792+
<span className="relative shrink-0" aria-hidden="true">
793+
<svg
794+
viewBox="0 0 24 24"
795+
className="h-4 w-4"
796+
fill="none"
797+
stroke="currentColor"
798+
strokeWidth="1.9"
799+
strokeLinecap="round"
800+
strokeLinejoin="round"
801+
>
802+
<circle cx="12" cy="12" r="9" />
803+
<path d="m14.3 8 5.2 9" />
804+
<path d="M9.7 8h10.4" />
805+
<path d="m7.4 12 5.2-9" />
806+
<path d="m9.7 16-5.2-9" />
807+
<path d="M14.3 16H3.9" />
808+
<path d="m16.6 12-5.2 9" />
809+
</svg>
810+
{unread > 0 ? (
811+
<span
812+
className="absolute -right-1 -top-1 h-2 w-2 rounded-full"
813+
style={{ backgroundColor: ACCENT_COLOR }}
814+
/>
815+
) : null}
816+
</span>
817+
<span className="flex-1 truncate">{brand.wordmark}</span>
818+
{actionable > 0 ? (
819+
<span className="inline-flex items-center justify-center rounded-full px-1.5 py-0.5 text-xs leading-none font-medium text-white" style={ACCENT_BG_STYLE}>
820+
{actionable}
821+
</span>
822+
) : unread > 0 ? (
823+
<span
824+
className="inline-flex items-center justify-center rounded-full border px-1.5 py-0.5 text-xs leading-none font-medium"
825+
style={{ color: ACCENT_COLOR, borderColor: ACCENT_BORDER, backgroundColor: "transparent" }}
826+
>
827+
{unread}
828+
</span>
829+
) : null}
830+
</a>
831+
);
832+
}
833+
763834
// ---------------------------------------------------------------------------
764835
// Attention page — single-column, calm operator surface
765836
// ---------------------------------------------------------------------------

tests/plugin.spec.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -260,19 +260,16 @@ function mockApprovalApi(
260260
}
261261

262262
describe("paperclip aperture", () => {
263-
it("declares a host-rendered Focus sidebar launcher", () => {
264-
expect(manifest.ui?.slots?.some((slot) => slot.type === "sidebar")).toBe(false);
265-
expect(manifest.ui?.launchers).toEqual(expect.arrayContaining([
263+
it("declares the Focus sidebar entry with its custom UI", () => {
264+
expect(manifest.ui?.slots).toEqual(expect.arrayContaining([
266265
expect.objectContaining({
267-
id: "focus-sidebar",
266+
type: "sidebar",
267+
id: "attention-sidebar-link",
268268
displayName: "Focus",
269-
placementZone: "sidebar",
270-
action: expect.objectContaining({
271-
type: "navigate",
272-
target: "/aperture",
273-
}),
269+
exportName: "AttentionSidebarLink",
274270
}),
275271
]));
272+
expect(manifest.ui?.launchers ?? []).toHaveLength(0);
276273
});
277274

278275
it("maps approval events into attention state and clears them on acknowledgement", async () => {

0 commit comments

Comments
 (0)