@tomismeta/paperclip-aperture 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,7 @@ It is designed as a live attention layer, not an inbox clone:
20
20
  - `now` shows the single most interrupting item
21
21
  - `next` stages the queue behind it
22
22
  - `ambient` keeps low-pressure awareness visible without demanding action
23
+ - unread state is secondary to action pressure, so Focus stays judgment-first instead of becoming a second inbox
23
24
 
24
25
  Put differently:
25
26
 
package/dist/manifest.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const manifest = {
2
2
  id: "tomismeta.paperclip-aperture",
3
3
  apiVersion: 1,
4
- version: "0.3.2",
4
+ version: "0.4.0",
5
5
  displayName: "Paperclip Aperture",
6
6
  description: "The live attention layer for Paperclip, powered by Aperture's deterministic attention engine.",
7
7
  author: "@tomismeta",
@@ -13,8 +13,10 @@ const manifest = {
13
13
  "issue.comments.read",
14
14
  "issue.comments.create",
15
15
  "issue.documents.read",
16
+ "activity.log.write",
16
17
  "plugin.state.read",
17
18
  "plugin.state.write",
19
+ "telemetry.track",
18
20
  "ui.dashboardWidget.register",
19
21
  "ui.page.register",
20
22
  "ui.sidebar.register"
package/dist/ui/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // src/ui/index.tsx
2
2
  import {
3
3
  usePluginAction,
4
- usePluginData
4
+ usePluginData,
5
+ usePluginStream
5
6
  } from "@paperclipai/plugin-sdk/ui";
6
7
  import { useEffect, useMemo, useRef, useState } from "react";
7
8
 
@@ -40,14 +41,14 @@ function frameSortScore(frame, lane) {
40
41
  const toneWeight = frame.tone === "critical" ? 40 : frame.tone === "focused" ? 25 : 5;
41
42
  const consequenceWeight = frame.consequence === "high" ? 30 : frame.consequence === "medium" ? 15 : 0;
42
43
  const modeWeight = frame.mode === "approval" ? 12 : frame.mode === "choice" ? 8 : 0;
43
- const laneWeight = lane === "active" ? 20 : lane === "queued" ? 10 : 0;
44
+ const laneWeight = lane === "now" ? 20 : lane === "next" ? 10 : 0;
44
45
  const budgetWeight = isBudgetOverride(frame) ? 10 : 0;
45
46
  return toneWeight + consequenceWeight + modeWeight + laneWeight + budgetWeight + frameSemanticWeight(frame);
46
47
  }
47
48
  function emptyLaneCounts() {
48
49
  return {
49
- active: 0,
50
- queued: 0,
50
+ now: 0,
51
+ next: 0,
51
52
  ambient: 0,
52
53
  total: 0
53
54
  };
@@ -73,10 +74,10 @@ function isFrameUnread(frame, snapshotUpdatedAt, review) {
73
74
  }
74
75
  function calculateUnreadCounts(snapshot, review) {
75
76
  const unread = emptyLaneCounts();
76
- if (snapshot.active && isFrameUnread(snapshot.active, snapshot.updatedAt, review)) unread.active += 1;
77
- unread.queued = snapshot.queued.filter((frame) => isFrameUnread(frame, snapshot.updatedAt, review)).length;
77
+ if (snapshot.now && isFrameUnread(snapshot.now, snapshot.updatedAt, review)) unread.now += 1;
78
+ unread.next = snapshot.next.filter((frame) => isFrameUnread(frame, snapshot.updatedAt, review)).length;
78
79
  unread.ambient = snapshot.ambient.filter((frame) => isFrameUnread(frame, snapshot.updatedAt, review)).length;
79
- unread.total = unread.active + unread.queued + unread.ambient;
80
+ unread.total = unread.now + unread.next + unread.ambient;
80
81
  return unread;
81
82
  }
82
83
  function attachReviewState(snapshot, review) {
@@ -92,33 +93,33 @@ function mergeStoredFrames(snapshot, companyId, candidates, review, nowIso = (/*
92
93
  const base = snapshot ?? {
93
94
  companyId,
94
95
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
95
- active: null,
96
- queued: [],
96
+ now: null,
97
+ next: [],
97
98
  ambient: [],
98
99
  counts: emptyLaneCounts()
99
100
  };
100
101
  const candidateTaskIds = new Set(candidates.map((candidate) => candidate.frame.taskId));
101
102
  const baseEntries = [
102
- ...base.active ? [{ frame: base.active, lane: "active" }] : [],
103
- ...base.queued.map((frame) => ({ frame, lane: "queued" })),
103
+ ...base.now ? [{ frame: base.now, lane: "now" }] : [],
104
+ ...base.next.map((frame) => ({ frame, lane: "next" })),
104
105
  ...base.ambient.map((frame) => ({ frame, lane: "ambient" }))
105
106
  ].filter((candidate) => !candidateTaskIds.has(candidate.frame.taskId));
106
107
  const filtered = [...baseEntries, ...candidates].filter((candidate) => !isFrameSuppressed(candidate.frame, base.updatedAt, review));
107
108
  const actionable = filtered.filter((candidate) => candidate.lane !== "ambient").sort((left, right) => compareCandidates(left, right, base.updatedAt));
108
109
  const ambient = filtered.filter((candidate) => candidate.lane === "ambient").filter((candidate) => !isAmbientExpired(candidate.frame, base.updatedAt, nowIso)).sort((left, right) => compareCandidates(left, right, base.updatedAt));
109
- const active = actionable.shift()?.frame ?? null;
110
- const queued = actionable.map((candidate) => candidate.frame);
110
+ const now = actionable.shift()?.frame ?? null;
111
+ const next = actionable.map((candidate) => candidate.frame);
111
112
  const ambientFrames = ambient.map((candidate) => candidate.frame);
112
113
  return attachReviewState({
113
114
  ...base,
114
- active,
115
- queued,
115
+ now,
116
+ next,
116
117
  ambient: ambientFrames,
117
118
  counts: {
118
- active: active ? 1 : 0,
119
- queued: queued.length,
119
+ now: now ? 1 : 0,
120
+ next: next.length,
120
121
  ambient: ambientFrames.length,
121
- total: (active ? 1 : 0) + queued.length + ambientFrames.length
122
+ total: (now ? 1 : 0) + next.length + ambientFrames.length
122
123
  }
123
124
  }, review);
124
125
  }
@@ -162,10 +163,10 @@ function relationHintLabel(hint) {
162
163
  }
163
164
  function laneReason(lane) {
164
165
  switch (lane) {
165
- case "active":
166
+ case "now":
166
167
  return "This is the most urgent item in the queue right now.";
167
- case "queued":
168
- return "This is queued behind the current top item.";
168
+ case "next":
169
+ return "This is staged behind the current top item.";
169
170
  case "ambient":
170
171
  return "This is visible for awareness without needing action yet.";
171
172
  }
@@ -237,6 +238,7 @@ function decisionContextItem(value) {
237
238
 
238
239
  // src/aperture/attention-language.ts
239
240
  var GENERIC_QUEUED_JUDGMENT = "Important enough to keep visible, but not enough to displace now.";
241
+ var GENERIC_NEXT_JUDGMENT = GENERIC_QUEUED_JUDGMENT;
240
242
  function approvalBlockingSummary(budgetOverride) {
241
243
  return budgetOverride ? "Budget controls are blocking work until a board decision lands." : "A board decision is blocking work in Paperclip.";
242
244
  }
@@ -244,14 +246,14 @@ function approvalBlockingWhyNow(budgetOverride) {
244
246
  return budgetOverride ? "Budget controls are blocking work until a board decision lands." : "Paperclip is waiting on a human approval before work can continue.";
245
247
  }
246
248
  function genericJudgmentLine(frame, lane) {
247
- if (lane === "active") {
249
+ if (lane === "now") {
248
250
  if (isBudgetOverride(frame)) return approvalBlockingSummary(true);
249
251
  if (frame.mode === "approval") return "A human decision is blocking work right now.";
250
252
  if (frame.tone === "critical") return "This surfaced because it can displace the operator now.";
251
253
  return "This is the clearest current operator focus.";
252
254
  }
253
255
  if (isBudgetOverride(frame)) return "Budget review is staged behind the current focus.";
254
- if (lane === "queued") return GENERIC_QUEUED_JUDGMENT;
256
+ if (lane === "next") return GENERIC_NEXT_JUDGMENT;
255
257
  return "Useful for awareness, but not interrupting.";
256
258
  }
257
259
 
@@ -260,12 +262,12 @@ function createEmptySnapshot(companyId) {
260
262
  return {
261
263
  companyId,
262
264
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
263
- active: null,
264
- queued: [],
265
+ now: null,
266
+ next: [],
265
267
  ambient: [],
266
268
  counts: {
267
- active: 0,
268
- queued: 0,
269
+ now: 0,
270
+ next: 0,
269
271
  ambient: 0,
270
272
  total: 0
271
273
  }
@@ -357,7 +359,7 @@ function mergeSnapshotWithApprovals(snapshot, companyId, approvals, review) {
357
359
  const approvalFrames = actionableApprovalRecords(approvals).map(approvalRecordToFrame);
358
360
  const base = snapshot ?? createEmptySnapshot(companyId);
359
361
  const merged = mergeStoredFrames(base, companyId, [
360
- ...approvalFrames.map((frame, index) => ({ frame, lane: index === 0 ? "active" : "queued" }))
362
+ ...approvalFrames.map((frame, index) => ({ frame, lane: index === 0 ? "now" : "next" }))
361
363
  ], review);
362
364
  return merged;
363
365
  }
@@ -411,6 +413,7 @@ var ACCENT_COLOR = "#007ACC";
411
413
  var ACCENT_BG = `${ACCENT_COLOR}14`;
412
414
  var ACCENT_BORDER = `${ACCENT_COLOR}33`;
413
415
  var ACCENT_BG_STYLE = { backgroundColor: ACCENT_COLOR };
416
+ var ATTENTION_UPDATES_STREAM = "attention-updates";
414
417
  function currentSurfaceBrand() {
415
418
  return {
416
419
  key: "focus",
@@ -533,17 +536,21 @@ function requestDescriptor(frame) {
533
536
  return driver ? `${sourceLabel(frame)} \xB7 ${driver}` : sourceLabel(frame);
534
537
  }
535
538
  function postureForSnapshot(snapshot) {
536
- const current = snapshot.active;
537
- if (current?.tone === "critical" || current?.consequence === "high" || snapshot.counts.active + snapshot.counts.queued >= 3) {
539
+ const current = snapshot.now;
540
+ if (current?.tone === "critical" || current?.consequence === "high" || snapshot.counts.now + snapshot.counts.next >= 3) {
538
541
  return { glyph: "\u25CF", label: "busy" };
539
542
  }
540
- if (snapshot.counts.active > 0 || snapshot.counts.queued > 0 || snapshot.counts.ambient > 0) {
543
+ if (snapshot.counts.now > 0 || snapshot.counts.next > 0 || snapshot.counts.ambient > 0) {
541
544
  return { glyph: "\u25D0", label: "elevated" };
542
545
  }
543
546
  return { glyph: "\u25CB", label: "calm" };
544
547
  }
545
548
  function actionableCount(snapshot) {
546
- return (snapshot.active ? 1 : 0) + snapshot.queued.length;
549
+ return (snapshot.now ? 1 : 0) + snapshot.next.length;
550
+ }
551
+ function actionableLabel(snapshot) {
552
+ const actionable = actionableCount(snapshot);
553
+ return actionable > 0 ? `${actionable} action${actionable === 1 ? "" : "s"}` : null;
547
554
  }
548
555
  function judgmentLine(frame, lane) {
549
556
  if (frame.provenance?.whyNow) return frame.provenance.whyNow;
@@ -554,19 +561,19 @@ function nextPrimaryText(frame) {
554
561
  if (recommendedMove) return recommendedMove;
555
562
  const summary = frame.summary?.trim();
556
563
  if (summary) return summary;
557
- const fallback = judgmentLine(frame, "queued");
564
+ const fallback = judgmentLine(frame, "next");
558
565
  return fallback === GENERIC_QUEUED_JUDGMENT ? null : fallback;
559
566
  }
560
567
  function supportingLine(frame, lane) {
561
568
  const owner = actionOwnerValue(frame);
562
569
  const target = blockingTargetValue(frame);
563
- if (target && lane === "active" && entityTypeFromFrame(frame) === "issue" && driverLabel(frame) === "review required") {
570
+ if (target && lane === "now" && entityTypeFromFrame(frame) === "issue" && driverLabel(frame) === "review required") {
564
571
  return issueBlocksTargetLine(target);
565
572
  }
566
- if (owner && lane === "active" && entityTypeFromFrame(frame) === "issue") {
573
+ if (owner && lane === "now" && entityTypeFromFrame(frame) === "issue") {
567
574
  return issueNeedsActionFromLine(owner);
568
575
  }
569
- if (lane === "active") {
576
+ if (lane === "now") {
570
577
  const recommendedMove = recommendedMoveValue(frame)?.trim();
571
578
  const summary = frame.summary?.trim();
572
579
  const judgment2 = judgmentLine(frame, lane).trim();
@@ -639,21 +646,21 @@ function applyLocalSuppressions(snapshot, suppressedAtByTaskId) {
639
646
  if (!suppressedAt) return true;
640
647
  return frameUpdatedAt(frame, snapshot.updatedAt).localeCompare(suppressedAt) > 0;
641
648
  };
642
- const queued = snapshot.queued.filter(keepFrame);
649
+ const next = snapshot.next.filter(keepFrame);
643
650
  const ambient = snapshot.ambient.filter(keepFrame);
644
- const activeCandidate = snapshot.active && keepFrame(snapshot.active) ? snapshot.active : null;
645
- const active = activeCandidate ?? queued[0] ?? null;
646
- const nextQueued = activeCandidate ? queued : queued.slice(1);
651
+ const nowCandidate = snapshot.now && keepFrame(snapshot.now) ? snapshot.now : null;
652
+ const now = nowCandidate ?? next[0] ?? null;
653
+ const nextFrames = nowCandidate ? next : next.slice(1);
647
654
  return {
648
655
  ...snapshot,
649
- active,
650
- queued: nextQueued,
656
+ now,
657
+ next: nextFrames,
651
658
  ambient,
652
659
  counts: {
653
- active: active ? 1 : 0,
654
- queued: nextQueued.length,
660
+ now: now ? 1 : 0,
661
+ next: nextFrames.length,
655
662
  ambient: ambient.length,
656
- total: (active ? 1 : 0) + nextQueued.length + ambient.length
663
+ total: (now ? 1 : 0) + nextFrames.length + ambient.length
657
664
  }
658
665
  };
659
666
  }
@@ -924,7 +931,16 @@ function useQueueMovement(frames, ttlMs = 3500) {
924
931
  function useAttentionModel(companyId) {
925
932
  const displayQuery = usePluginData("attention-display", companyId ? { companyId } : void 0);
926
933
  const approvalsQuery = usePendingApprovals(companyId);
934
+ const updates = usePluginStream(
935
+ ATTENTION_UPDATES_STREAM,
936
+ companyId ? { companyId } : void 0
937
+ );
927
938
  useAttentionPolling(companyId, [displayQuery.refresh, approvalsQuery.refresh]);
939
+ useEffect(() => {
940
+ if (!companyId || !updates.lastEvent) return;
941
+ displayQuery.refresh();
942
+ approvalsQuery.refresh();
943
+ }, [companyId, updates.lastEvent?.updatedAt, updates.lastEvent?.eventType]);
928
944
  const snapshot = useMemo(
929
945
  () => companyId ? mergeSnapshotWithApprovals(displayQuery.data?.snapshot ?? null, companyId, approvalsQuery.data, displayQuery.data?.reviewState ?? null) : null,
930
946
  [displayQuery.data, approvalsQuery.data, companyId]
@@ -1078,10 +1094,10 @@ function InlineExplainability(props) {
1078
1094
  ...explanation.signals.slice(0, 2),
1079
1095
  ...explanation.relationLabels.slice(0, 1)
1080
1096
  ];
1081
- const label = props.lane === "queued" ? "Why next" : props.lane === "ambient" ? "Why ambient" : "Why now";
1097
+ const label = props.lane === "next" ? "Why next" : props.lane === "ambient" ? "Why ambient" : "Why now";
1082
1098
  const whyNow = explanation.whyNow ?? judgmentLine(props.frame, props.lane);
1083
- const primaryLine = props.lane === "active" ? props.preferLaneReason ? explanation.laneReason : whyNow : whyNow;
1084
- const secondaryLine = props.lane === "active" ? !props.preferLaneReason && whyNow !== explanation.laneReason ? explanation.laneReason : null : explanation.laneReason;
1099
+ const primaryLine = props.lane === "now" ? props.preferLaneReason ? explanation.laneReason : whyNow : whyNow;
1100
+ const secondaryLine = props.lane === "now" ? !props.preferLaneReason && whyNow !== explanation.laneReason ? explanation.laneReason : null : explanation.laneReason;
1085
1101
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2 border-l-2 pl-4", style: { borderColor: ACCENT_COLOR }, children: [
1086
1102
  /* @__PURE__ */ jsx("div", { className: "text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: label }),
1087
1103
  /* @__PURE__ */ jsx("p", { className: "max-w-3xl text-sm leading-relaxed text-foreground/90", children: primaryLine }),
@@ -1094,7 +1110,7 @@ function ExplainabilityPanel(props) {
1094
1110
  const strength = !props.detailOnly && explanation.signalStrength ? signalStrengthLabel(explanation.signalStrength) : null;
1095
1111
  const signalValues = props.detailOnly ? explanation.signals.slice(2) : explanation.signals;
1096
1112
  const relationValues = props.detailOnly ? explanation.relationLabels.slice(1) : explanation.relationLabels;
1097
- const reasoningLabel = props.lane === "active" ? "Reasoning" : props.lane === "queued" ? "Why it sits here" : "Why it stays quiet";
1113
+ const reasoningLabel = props.lane === "now" ? "Reasoning" : props.lane === "next" ? "Why it sits here" : "Why it stays quiet";
1098
1114
  if (props.detailOnly && signalValues.length === 0 && relationValues.length === 0 && !explanation.continuity) {
1099
1115
  return null;
1100
1116
  }
@@ -1231,7 +1247,7 @@ function NowDetails(props) {
1231
1247
  const activityLink = activityHref(frame, props.companyPrefix);
1232
1248
  const hasContext = visibleContextItems(frame).length > 0;
1233
1249
  return /* @__PURE__ */ jsxs("div", { className: "space-y-3 border-t border-border pt-4", children: [
1234
- /* @__PURE__ */ jsx(ExplainabilityPanel, { frame, lane: "active", detailOnly: true }),
1250
+ /* @__PURE__ */ jsx(ExplainabilityPanel, { frame, lane: "now", detailOnly: true }),
1235
1251
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
1236
1252
  /* @__PURE__ */ jsx("span", { children: requestDescriptor(frame) }),
1237
1253
  /* @__PURE__ */ jsx("span", { children: impactLabel(frame) }),
@@ -1447,14 +1463,14 @@ function NowActionRail(props) {
1447
1463
  );
1448
1464
  }
1449
1465
  function NowPane(props) {
1450
- const frame = props.snapshot.active;
1466
+ const frame = props.snapshot.now;
1451
1467
  const [detailsOpen, setDetailsOpen] = useState(false);
1452
1468
  const wideLayout = useWideLayout(1120);
1453
1469
  const activeFrameId = frame?.id ?? null;
1454
- const activeUnread = !!frame && (props.snapshot.review?.unread.active ?? 0) > 0;
1470
+ const activeUnread = !!frame && (props.snapshot.review?.unread.now ?? 0) > 0;
1455
1471
  const itemLink = frame ? itemHref(frame, props.companyPrefix) : null;
1456
1472
  const recommendedMove = frame ? recommendedMoveValue(frame) : null;
1457
- const helperLine = frame ? supportingLine(frame, "active") : null;
1473
+ const helperLine = frame ? supportingLine(frame, "now") : null;
1458
1474
  const driverBadge = driverBadgeStyle();
1459
1475
  useEffect(() => {
1460
1476
  setDetailsOpen(false);
@@ -1474,13 +1490,13 @@ function NowPane(props) {
1474
1490
  ] })
1475
1491
  ] }),
1476
1492
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 text-xs tabular-nums text-muted-foreground", children: [
1477
- /* @__PURE__ */ jsxs("span", { className: props.counts.active > 0 ? "font-semibold" : "", children: [
1493
+ /* @__PURE__ */ jsxs("span", { className: props.counts.now > 0 ? "font-semibold" : "", children: [
1478
1494
  "now ",
1479
- props.counts.active > 0 ? /* @__PURE__ */ jsx(Accent, { children: props.counts.active }) : props.counts.active
1495
+ props.counts.now > 0 ? /* @__PURE__ */ jsx(Accent, { children: props.counts.now }) : props.counts.now
1480
1496
  ] }),
1481
- /* @__PURE__ */ jsxs("span", { className: props.counts.queued > 0 ? "font-semibold" : "", children: [
1497
+ /* @__PURE__ */ jsxs("span", { className: props.counts.next > 0 ? "font-semibold" : "", children: [
1482
1498
  "next ",
1483
- props.counts.queued > 0 ? /* @__PURE__ */ jsx(Accent, { children: props.counts.queued }) : props.counts.queued
1499
+ props.counts.next > 0 ? /* @__PURE__ */ jsx(Accent, { children: props.counts.next }) : props.counts.next
1484
1500
  ] }),
1485
1501
  /* @__PURE__ */ jsxs("span", { className: props.counts.ambient > 0 ? "font-semibold" : "", children: [
1486
1502
  "ambient ",
@@ -1509,7 +1525,7 @@ function NowPane(props) {
1509
1525
  /* @__PURE__ */ jsx(Badge, { className: "border-border bg-secondary text-foreground/80", children: impactLabel(frame) }),
1510
1526
  driverLabel(frame) ? /* @__PURE__ */ jsx(Badge, { className: driverBadge.className, style: driverBadge.style, children: driverLabel(frame) }) : null
1511
1527
  ] }),
1512
- /* @__PURE__ */ jsx(InlineExplainability, { frame, lane: "active", preferLaneReason: !!helperLine }),
1528
+ /* @__PURE__ */ jsx(InlineExplainability, { frame, lane: "now", preferLaneReason: !!helperLine }),
1513
1529
  /* @__PURE__ */ jsx("div", { className: "flex items-center gap-4", children: /* @__PURE__ */ jsx(Accent, { children: /* @__PURE__ */ jsxs(
1514
1530
  "button",
1515
1531
  {
@@ -1538,7 +1554,7 @@ function NowPane(props) {
1538
1554
  NowActionRail,
1539
1555
  {
1540
1556
  frame,
1541
- lane: "active",
1557
+ lane: "now",
1542
1558
  wide: wideLayout,
1543
1559
  pendingId: props.pendingId,
1544
1560
  itemLink,
@@ -1574,7 +1590,7 @@ function NextRow(props) {
1574
1590
  const secondaryText = nextPrimaryText(frame);
1575
1591
  const driver = driverLabel(frame);
1576
1592
  const driverBadge = driverBadgeStyle();
1577
- const detailText = judgmentLine(frame, "queued");
1593
+ const detailText = judgmentLine(frame, "next");
1578
1594
  const showDetailText = detailText.trim().length > 0 && detailText !== secondaryText && detailText !== GENERIC_QUEUED_JUDGMENT;
1579
1595
  const rankOpacity = Math.max(0.4, 1 - (props.rank - 1) * 0.14);
1580
1596
  return /* @__PURE__ */ jsxs("div", { className: "border-b border-border/80 last:border-b-0", style: expanded ? { borderLeftWidth: 2, borderLeftColor: ACCENT_COLOR } : void 0, children: [
@@ -1610,7 +1626,7 @@ function NextRow(props) {
1610
1626
  ),
1611
1627
  expanded ? /* @__PURE__ */ jsxs("div", { className: "space-y-3 px-4 pb-4 pl-12 sm:px-6 sm:pl-14", children: [
1612
1628
  showDetailText ? /* @__PURE__ */ jsx("p", { className: "text-sm leading-relaxed text-muted-foreground", children: detailText }) : null,
1613
- /* @__PURE__ */ jsx(ExplainabilityStrip, { frame, lane: "queued" }),
1629
+ /* @__PURE__ */ jsx(ExplainabilityStrip, { frame, lane: "next" }),
1614
1630
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground", children: [
1615
1631
  /* @__PURE__ */ jsx("span", { children: impactLabel(frame) }),
1616
1632
  driver ? /* @__PURE__ */ jsx(Accent, { children: driver }) : null,
@@ -1642,7 +1658,7 @@ function NextRow(props) {
1642
1658
  FrameActions,
1643
1659
  {
1644
1660
  frame,
1645
- lane: "queued",
1661
+ lane: "next",
1646
1662
  pendingId: props.pendingId,
1647
1663
  onApprove: props.onApprove,
1648
1664
  onReject: props.onReject,
@@ -1720,7 +1736,8 @@ function DashboardWidget(props) {
1720
1736
  if (error) return /* @__PURE__ */ jsx(MessageCard, { title: "Plugin error", body: error.message });
1721
1737
  if (!data) return /* @__PURE__ */ jsx(MessageCard, { title: brand.wordmark, body: brand.headingEmptyState });
1722
1738
  const posture = postureForSnapshot(data);
1723
- const leadingText = data.active ? recommendedMoveValue(data.active) ?? data.active.title : null;
1739
+ const leadingText = data.now ? recommendedMoveValue(data.now) ?? data.now.title : null;
1740
+ const actionable = actionableLabel(data);
1724
1741
  const borderStyle = posture.label === "busy" ? { borderLeftWidth: 2, borderLeftColor: ACCENT_COLOR } : posture.label === "elevated" ? { borderLeftWidth: 2, borderLeftColor: ACCENT_BORDER } : void 0;
1725
1742
  return /* @__PURE__ */ jsxs("a", { href, className: "block border border-border bg-card px-4 py-3 shadow-sm transition-colors hover:bg-accent/40", style: borderStyle, children: [
1726
1743
  /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 text-xs", children: [
@@ -1728,16 +1745,13 @@ function DashboardWidget(props) {
1728
1745
  /* @__PURE__ */ jsx("div", { className: "min-w-0", children: /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2", children: [
1729
1746
  /* @__PURE__ */ jsx(Accent, { className: "font-semibold tracking-[0.04em]", children: brand.wordmark }),
1730
1747
  /* @__PURE__ */ jsx(Accent, { className: "text-[11px]", children: posture.label }),
1731
- unreadCount(data) > 0 ? /* @__PURE__ */ jsxs(Accent, { className: "text-[11px]", children: [
1732
- "new ",
1733
- unreadCount(data)
1734
- ] }) : null
1748
+ actionable ? /* @__PURE__ */ jsx(Accent, { className: "text-[11px]", children: actionable }) : null
1735
1749
  ] }) }),
1736
1750
  /* @__PURE__ */ jsxs("span", { className: "ml-auto tabular-nums text-muted-foreground", children: [
1737
1751
  "now ",
1738
- data.counts.active,
1752
+ data.counts.now,
1739
1753
  " \xB7 next ",
1740
- data.counts.queued,
1754
+ data.counts.next,
1741
1755
  " \xB7 ambient ",
1742
1756
  data.counts.ambient
1743
1757
  ] })
@@ -1776,34 +1790,43 @@ function AttentionSidebarLink({ context }) {
1776
1790
  isActive ? "bg-accent text-foreground" : "text-foreground/80 hover:bg-accent/50 hover:text-foreground"
1777
1791
  ),
1778
1792
  children: [
1779
- /* @__PURE__ */ jsx("span", { className: "relative shrink-0", "aria-hidden": "true", children: /* @__PURE__ */ jsxs(
1780
- "svg",
1781
- {
1782
- viewBox: "0 0 24 24",
1783
- className: "h-4 w-4",
1784
- fill: "none",
1785
- stroke: "currentColor",
1786
- strokeWidth: "1.9",
1787
- strokeLinecap: "round",
1788
- strokeLinejoin: "round",
1789
- children: [
1790
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
1791
- /* @__PURE__ */ jsx("path", { d: "m14.3 8 5.2 9" }),
1792
- /* @__PURE__ */ jsx("path", { d: "M9.7 8h10.4" }),
1793
- /* @__PURE__ */ jsx("path", { d: "m7.4 12 5.2-9" }),
1794
- /* @__PURE__ */ jsx("path", { d: "m9.7 16-5.2-9" }),
1795
- /* @__PURE__ */ jsx("path", { d: "M14.3 16H3.9" }),
1796
- /* @__PURE__ */ jsx("path", { d: "m16.6 12-5.2 9" })
1797
- ]
1798
- }
1799
- ) }),
1793
+ /* @__PURE__ */ jsxs("span", { className: "relative shrink-0", "aria-hidden": "true", children: [
1794
+ /* @__PURE__ */ jsxs(
1795
+ "svg",
1796
+ {
1797
+ viewBox: "0 0 24 24",
1798
+ className: "h-4 w-4",
1799
+ fill: "none",
1800
+ stroke: "currentColor",
1801
+ strokeWidth: "1.9",
1802
+ strokeLinecap: "round",
1803
+ strokeLinejoin: "round",
1804
+ children: [
1805
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
1806
+ /* @__PURE__ */ jsx("path", { d: "m14.3 8 5.2 9" }),
1807
+ /* @__PURE__ */ jsx("path", { d: "M9.7 8h10.4" }),
1808
+ /* @__PURE__ */ jsx("path", { d: "m7.4 12 5.2-9" }),
1809
+ /* @__PURE__ */ jsx("path", { d: "m9.7 16-5.2-9" }),
1810
+ /* @__PURE__ */ jsx("path", { d: "M14.3 16H3.9" }),
1811
+ /* @__PURE__ */ jsx("path", { d: "m16.6 12-5.2 9" })
1812
+ ]
1813
+ }
1814
+ ),
1815
+ unread > 0 ? /* @__PURE__ */ jsx(
1816
+ "span",
1817
+ {
1818
+ className: "absolute -right-1 -top-1 h-2 w-2 rounded-full",
1819
+ style: { backgroundColor: ACCENT_COLOR }
1820
+ }
1821
+ ) : null
1822
+ ] }),
1800
1823
  /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: brand.wordmark }),
1801
- unread > 0 || actionable > 0 ? unread > 0 ? /* @__PURE__ */ jsx("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, children: unread }) : /* @__PURE__ */ jsx(
1824
+ actionable > 0 ? /* @__PURE__ */ jsx("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, children: actionable }) : unread > 0 ? /* @__PURE__ */ jsx(
1802
1825
  "span",
1803
1826
  {
1804
1827
  className: "inline-flex items-center justify-center rounded-full border px-1.5 py-0.5 text-xs leading-none font-medium",
1805
1828
  style: { color: ACCENT_COLOR, borderColor: ACCENT_BORDER, backgroundColor: "transparent" },
1806
- children: actionable
1829
+ children: unread
1807
1830
  }
1808
1831
  ) : null
1809
1832
  ]
@@ -1847,7 +1870,7 @@ function AttentionPage(props) {
1847
1870
  const backgroundErrorMessage = displaySnapshot && error ? `Sync issue: ${error.message}` : null;
1848
1871
  const posture = useMemo(() => displaySnapshot ? postureForSnapshot(displaySnapshot) : null, [displaySnapshot]);
1849
1872
  const nextRows = useMemo(
1850
- () => displaySnapshot ? displaySnapshot.queued.map((frame) => ({ frame, lane: "queued" })) : [],
1873
+ () => displaySnapshot ? displaySnapshot.next.map((frame) => ({ frame, lane: "next" })) : [],
1851
1874
  [displaySnapshot]
1852
1875
  );
1853
1876
  const ambientRows = useMemo(
@@ -1860,8 +1883,8 @@ function AttentionPage(props) {
1860
1883
  setLocalSuppressions((current) => {
1861
1884
  const nextEntries = Object.entries(current).filter(([taskId, suppressedAt]) => {
1862
1885
  const frames = [
1863
- ...snapshot.active ? [snapshot.active] : [],
1864
- ...snapshot.queued,
1886
+ ...snapshot.now ? [snapshot.now] : [],
1887
+ ...snapshot.next,
1865
1888
  ...snapshot.ambient
1866
1889
  ];
1867
1890
  const matching = frames.filter((frame) => frame.taskId === taskId);