Skip to content

Commit 60d6a02

Browse files
committed
Address initial 0.4.5 issue feedback
1 parent 152017c commit 60d6a02

7 files changed

Lines changed: 136 additions & 93 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ Then install the plugin with the command above.
4646

4747
After install, open Paperclip and use the `Focus` entry in the sidebar.
4848

49+
## Troubleshooting
50+
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.
52+
- `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.
53+
- 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.
54+
4955
## Napkin Diagram
5056

5157
```text

docs/RELEASE-0.4.5.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ uses the newer host attention contracts where they improve Focus judgment.
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
16+
- makes agent run failure cards acknowledgeable and clears issue-linked failures when the issue moves on
17+
- documents the Windows `spawn npm ENOENT` Paperclip installer failure mode
1518

1619
## Why This Matters
1720

@@ -21,6 +24,8 @@ uses the newer host attention contracts where they improve Focus judgment.
2124
contracts
2225
- improves operator clarity around locked review artifacts and planning-mode
2326
work without changing the plugin boundary
27+
- closes the feedback loop for stale failure cards and broken sidebar entry
28+
reports from early `0.4.4` installs
2429

2530
## Validation
2631

src/aperture/event-mapper.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@ function makeTaskId(event: MappablePluginEvent): string {
6666
return createTaskId(entityType, entityId);
6767
}
6868

69+
function relatedIssueTaskId(payload: unknown): string | null {
70+
const issueId =
71+
readPayloadString(payload, "issueId")
72+
?? readPayloadString(payload, "sourceIssueId")
73+
?? readPayloadString(payload, "parentIssueId")
74+
?? readStringArray(payload, "issueIds")?.[0];
75+
return issueId ? createTaskId("issue", issueId) : null;
76+
}
77+
6978
function makeSource(event: MappablePluginEvent): SourceRef {
7079
const entityType = event.entityType ?? "event";
7180

@@ -452,41 +461,33 @@ export function mapPluginEventToAperture(event: MappablePluginEvent): ApertureEv
452461

453462
case "agent.run.failed":
454463
{
464+
const failureTaskId = relatedIssueTaskId(event.payload) ?? taskId;
455465
const title = readPayloadString(event.payload, "title") ?? "Agent run failed";
456466
const summary = readPayloadString(event.payload, "summary") ?? runFailedSummary();
457467
return {
458468
id: `${event.eventId}:run-failed`,
459-
type: "human.input.requested",
460-
taskId,
461-
interactionId: createInteractionId(taskId, "run-failed"),
469+
type: "task.updated",
470+
taskId: failureTaskId,
462471
timestamp: event.occurredAt,
463472
source,
464473
toolFamily: "paperclip",
465474
activityClass: "tool_failure",
475+
status: "failed",
466476
title,
467477
summary,
468-
consequence: "high",
469-
tone: "critical",
470-
request: { kind: "approval" },
471478
semantic: interpretEventSemantic({
472479
id: `${event.eventId}:run-failed`,
473-
type: "human.input.requested",
474-
taskId,
475-
interactionId: createInteractionId(taskId, "run-failed"),
480+
type: "task.updated",
481+
taskId: failureTaskId,
476482
timestamp: event.occurredAt,
477483
source,
478484
toolFamily: "paperclip",
479485
activityClass: "tool_failure",
480486
title,
481487
summary,
482-
request: { kind: "approval" },
483-
riskHint: "high",
488+
status: "failed",
484489
semanticHints: runFailureSemanticHints(),
485490
}),
486-
provenance: {
487-
whyNow: runFailedWhyNow(),
488-
factors: ["run failed", "operator review"],
489-
},
490491
};
491492
}
492493

src/manifest.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,29 @@ 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-
},
7165
{
7266
type: "dashboardWidget",
7367
id: "attention-widget",
7468
displayName: "Focus",
7569
exportName: "DashboardWidget"
7670
}
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+
}
7788
]
7889
}
7990
};

src/ui/frame-helpers.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,14 @@ export function responseKind(frame: StoredAttentionFrame, lane: FrameLane): "app
196196
return "approval";
197197
}
198198

199-
if (lane !== "ambient" && (frame.responseSpec?.kind === "acknowledge" || frame.mode === "status")) {
199+
if (
200+
lane !== "ambient"
201+
&& (
202+
frame.responseSpec?.kind === "acknowledge"
203+
|| frame.mode === "status"
204+
|| frame.responseSpec?.kind === "approval"
205+
)
206+
) {
200207
return "acknowledge";
201208
}
202209

src/ui/index.tsx

Lines changed: 0 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {
22
type PluginPageProps,
3-
type PluginSidebarProps,
43
type PluginWidgetProps,
54
} from "@paperclipai/plugin-sdk/ui";
65
import { useEffect, useMemo, useState } from "react";
@@ -17,7 +16,6 @@ import {
1716
GENERIC_QUEUED_JUDGMENT,
1817
} from "../aperture/attention-language.js";
1918
import {
20-
ACCENT_BG_STYLE,
2119
ACCENT_BORDER,
2220
ACCENT_COLOR,
2321
Accent,
@@ -35,7 +33,6 @@ import {
3533
useWideLayout,
3634
} from "./chrome.js";
3735
import {
38-
actionableCount,
3936
actionableLabel,
4037
currentSurfaceBrand,
4138
formatRelativeTime,
@@ -45,7 +42,6 @@ import {
4542
type Posture,
4643
type QueueMovement,
4744
type SurfaceBrand,
48-
unreadCount,
4945
useAttentionModel,
5046
useQueueMovement,
5147
} from "./focus-model.js";
@@ -764,73 +760,6 @@ export function DashboardWidget(props: PluginWidgetProps) {
764760
);
765761
}
766762

767-
// ---------------------------------------------------------------------------
768-
// Sidebar link — already Paperclip-native
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-
834763
// ---------------------------------------------------------------------------
835764
// Attention page — single-column, calm operator surface
836765
// ---------------------------------------------------------------------------

tests/plugin.spec.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import type {
99
AttentionReplayScenario,
1010
AttentionReviewState,
1111
AttentionSnapshot,
12+
StoredAttentionFrame,
1213
} from "../src/aperture/types.js";
1314
import type { ApprovalRecord } from "../src/aperture/approval-frames.js";
15+
import { responseKind } from "../src/ui/frame-helpers.js";
1416
import plugin from "../src/worker.js";
1517
import {
1618
ATTENTION_STATE_KEY,
@@ -258,6 +260,21 @@ function mockApprovalApi(
258260
}
259261

260262
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([
266+
expect.objectContaining({
267+
id: "focus-sidebar",
268+
displayName: "Focus",
269+
placementZone: "sidebar",
270+
action: expect.objectContaining({
271+
type: "navigate",
272+
target: "/aperture",
273+
}),
274+
}),
275+
]));
276+
});
277+
261278
it("maps approval events into attention state and clears them on acknowledgement", async () => {
262279
const harness = createTestHarness({ manifest });
263280
await plugin.definition.setup(harness.ctx);
@@ -463,6 +480,73 @@ describe("paperclip aperture", () => {
463480
const snapshot = await harness.getData<AttentionSnapshot>("attention-summary", { companyId: "company-2" });
464481
expect(snapshot.now?.consequence).toBe("high");
465482
expect(snapshot.now?.title).toContain("Build failed");
483+
expect(snapshot.now?.mode).toBe("status");
484+
expect(snapshot.now?.responseSpec?.kind).toBe("acknowledge");
485+
});
486+
487+
it("clears issue-linked run failures when the issue moves out of an actionable state", async () => {
488+
const harness = createTestHarness({ manifest });
489+
await plugin.definition.setup(harness.ctx);
490+
491+
await harness.emit(
492+
"agent.run.failed",
493+
{
494+
issueId: "issue-77",
495+
title: "Issue run failed",
496+
summary: "The issue run failed during validation.",
497+
},
498+
{ companyId: "company-run-issue", entityId: "run-77", entityType: "run" },
499+
);
500+
501+
const failed = await harness.getData<AttentionSnapshot>("attention-summary", { companyId: "company-run-issue" });
502+
expect(failed.now?.taskId).toBe("issue:issue-77");
503+
expect(failed.now?.responseSpec?.kind).toBe("acknowledge");
504+
505+
await harness.emit(
506+
"issue.updated",
507+
{
508+
title: "Issue run recovered",
509+
status: "done",
510+
summary: "The issue is done.",
511+
},
512+
{ companyId: "company-run-issue", entityId: "issue-77", entityType: "issue" },
513+
);
514+
515+
const recovered = await harness.getData<AttentionSnapshot>("attention-summary", { companyId: "company-run-issue" });
516+
expect(recovered.now).toBeNull();
517+
expect(recovered.next).toHaveLength(0);
518+
});
519+
520+
it("keeps legacy non-approval approval frames acknowledgeable", () => {
521+
const frame: StoredAttentionFrame = {
522+
id: "frame:run:run-77:run-failed",
523+
taskId: "run:run-77",
524+
interactionId: "run:run-77:run-failed",
525+
source: {
526+
id: "paperclip:run",
527+
kind: "paperclip",
528+
label: "Paperclip run",
529+
},
530+
version: 1,
531+
mode: "approval",
532+
tone: "critical",
533+
consequence: "high",
534+
title: "Build failed",
535+
summary: "The deploy pipeline crashed during tests.",
536+
responseSpec: {
537+
kind: "approval",
538+
actions: [
539+
{ id: "approve", label: "Approve", kind: "approve", emphasis: "primary" },
540+
{ id: "reject", label: "Reject", kind: "reject", emphasis: "danger" },
541+
],
542+
},
543+
timing: {
544+
createdAt: "2026-05-08T09:21:47.000Z",
545+
updatedAt: "2026-05-08T09:21:47.000Z",
546+
},
547+
};
548+
549+
expect(responseKind(frame, "now")).toBe("acknowledge");
466550
});
467551

468552
it("preserves budget override semantics for approval frames", async () => {

0 commit comments

Comments
 (0)