@tomismeta/paperclip-aperture 0.4.1 → 0.4.2
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 +31 -12
- package/dist/manifest.js +9 -2
- package/dist/ui/index.js +1102 -1327
- package/dist/worker.js +4463 -2626
- package/package.json +11 -10
package/dist/ui/index.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
// src/ui/index.tsx
|
|
2
|
-
import {
|
|
3
|
-
usePluginAction,
|
|
4
|
-
usePluginData,
|
|
5
|
-
usePluginStream
|
|
6
|
-
} from "@paperclipai/plugin-sdk/ui";
|
|
7
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useEffect as useEffect5, useMemo as useMemo2, useRef as useRef5, useState as useState5 } from "react";
|
|
8
3
|
|
|
9
4
|
// src/aperture/frame-model.ts
|
|
10
5
|
var AMBIENT_MAX_AGE_MS = 5 * 60 * 1e3;
|
|
@@ -14,227 +9,6 @@ function isBudgetOverride(frame) {
|
|
|
14
9
|
function frameUpdatedAt(frame, snapshotUpdatedAt) {
|
|
15
10
|
return frame.timing.updatedAt || snapshotUpdatedAt;
|
|
16
11
|
}
|
|
17
|
-
function parseIsoMillis(value) {
|
|
18
|
-
if (!value) return null;
|
|
19
|
-
const parsed = Date.parse(value);
|
|
20
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
21
|
-
}
|
|
22
|
-
function isAmbientExpired(frame, snapshotUpdatedAt, nowIso) {
|
|
23
|
-
const updatedAt = parseIsoMillis(frameUpdatedAt(frame, snapshotUpdatedAt));
|
|
24
|
-
const now = parseIsoMillis(nowIso);
|
|
25
|
-
if (updatedAt === null || now === null) return false;
|
|
26
|
-
return now - updatedAt > AMBIENT_MAX_AGE_MS;
|
|
27
|
-
}
|
|
28
|
-
function frameSemanticWeight(frame) {
|
|
29
|
-
const factors = new Set(frame.provenance?.factors ?? []);
|
|
30
|
-
let weight = 0;
|
|
31
|
-
if (factors.has("run failed")) weight += 20;
|
|
32
|
-
if (factors.has("waiting on human")) weight += 16;
|
|
33
|
-
if (factors.has("needs clarification")) weight += 16;
|
|
34
|
-
if (factors.has("blocked")) weight += 12;
|
|
35
|
-
if (factors.has("pending approval")) weight += 12;
|
|
36
|
-
if (factors.has("paused work")) weight += 6;
|
|
37
|
-
if (factors.has("comment follow-up")) weight += 4;
|
|
38
|
-
return weight;
|
|
39
|
-
}
|
|
40
|
-
function frameSortScore(frame, lane) {
|
|
41
|
-
const toneWeight = frame.tone === "critical" ? 40 : frame.tone === "focused" ? 25 : 5;
|
|
42
|
-
const consequenceWeight = frame.consequence === "high" ? 30 : frame.consequence === "medium" ? 15 : 0;
|
|
43
|
-
const modeWeight = frame.mode === "approval" ? 12 : frame.mode === "choice" ? 8 : 0;
|
|
44
|
-
const laneWeight = lane === "now" ? 20 : lane === "next" ? 10 : 0;
|
|
45
|
-
const budgetWeight = isBudgetOverride(frame) ? 10 : 0;
|
|
46
|
-
return toneWeight + consequenceWeight + modeWeight + laneWeight + budgetWeight + frameSemanticWeight(frame);
|
|
47
|
-
}
|
|
48
|
-
function emptyLaneCounts() {
|
|
49
|
-
return {
|
|
50
|
-
now: 0,
|
|
51
|
-
next: 0,
|
|
52
|
-
ambient: 0,
|
|
53
|
-
total: 0
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
function compareCandidates(left, right, snapshotUpdatedAt) {
|
|
57
|
-
const byScore = frameSortScore(right.frame, right.lane) - frameSortScore(left.frame, left.lane);
|
|
58
|
-
if (byScore !== 0) return byScore;
|
|
59
|
-
return frameUpdatedAt(right.frame, snapshotUpdatedAt).localeCompare(frameUpdatedAt(left.frame, snapshotUpdatedAt));
|
|
60
|
-
}
|
|
61
|
-
function frameReviewState(review, taskId) {
|
|
62
|
-
return review?.frames[taskId];
|
|
63
|
-
}
|
|
64
|
-
function isFrameSuppressed(frame, snapshotUpdatedAt, review) {
|
|
65
|
-
const suppressedAt = frameReviewState(review, frame.taskId)?.suppressedAt;
|
|
66
|
-
if (!suppressedAt) return false;
|
|
67
|
-
return frameUpdatedAt(frame, snapshotUpdatedAt).localeCompare(suppressedAt) <= 0;
|
|
68
|
-
}
|
|
69
|
-
function isFrameUnread(frame, snapshotUpdatedAt, review) {
|
|
70
|
-
const updatedAt = frameUpdatedAt(frame, snapshotUpdatedAt);
|
|
71
|
-
const seenAt = frameReviewState(review, frame.taskId)?.lastSeenAt ?? review?.lastSeenAt;
|
|
72
|
-
if (!seenAt) return true;
|
|
73
|
-
return updatedAt.localeCompare(seenAt) > 0;
|
|
74
|
-
}
|
|
75
|
-
function calculateUnreadCounts(snapshot, review) {
|
|
76
|
-
const unread = emptyLaneCounts();
|
|
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;
|
|
79
|
-
unread.ambient = snapshot.ambient.filter((frame) => isFrameUnread(frame, snapshot.updatedAt, review)).length;
|
|
80
|
-
unread.total = unread.now + unread.next + unread.ambient;
|
|
81
|
-
return unread;
|
|
82
|
-
}
|
|
83
|
-
function attachReviewState(snapshot, review) {
|
|
84
|
-
return {
|
|
85
|
-
...snapshot,
|
|
86
|
-
review: {
|
|
87
|
-
lastSeenAt: review?.lastSeenAt,
|
|
88
|
-
unread: calculateUnreadCounts(snapshot, review)
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
function mergeStoredFrames(snapshot, companyId, candidates, review, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
93
|
-
const base = snapshot ?? {
|
|
94
|
-
companyId,
|
|
95
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
96
|
-
now: null,
|
|
97
|
-
next: [],
|
|
98
|
-
ambient: [],
|
|
99
|
-
counts: emptyLaneCounts()
|
|
100
|
-
};
|
|
101
|
-
const candidateTaskIds = new Set(candidates.map((candidate) => candidate.frame.taskId));
|
|
102
|
-
const baseEntries = [
|
|
103
|
-
...base.now ? [{ frame: base.now, lane: "now" }] : [],
|
|
104
|
-
...base.next.map((frame) => ({ frame, lane: "next" })),
|
|
105
|
-
...base.ambient.map((frame) => ({ frame, lane: "ambient" }))
|
|
106
|
-
].filter((candidate) => !candidateTaskIds.has(candidate.frame.taskId));
|
|
107
|
-
const filtered = [...baseEntries, ...candidates].filter((candidate) => !isFrameSuppressed(candidate.frame, base.updatedAt, review));
|
|
108
|
-
const actionable = filtered.filter((candidate) => candidate.lane !== "ambient").sort((left, right) => compareCandidates(left, right, base.updatedAt));
|
|
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));
|
|
110
|
-
const now = actionable.shift()?.frame ?? null;
|
|
111
|
-
const next = actionable.map((candidate) => candidate.frame);
|
|
112
|
-
const ambientFrames = ambient.map((candidate) => candidate.frame);
|
|
113
|
-
return attachReviewState({
|
|
114
|
-
...base,
|
|
115
|
-
now,
|
|
116
|
-
next,
|
|
117
|
-
ambient: ambientFrames,
|
|
118
|
-
counts: {
|
|
119
|
-
now: now ? 1 : 0,
|
|
120
|
-
next: next.length,
|
|
121
|
-
ambient: ambientFrames.length,
|
|
122
|
-
total: (now ? 1 : 0) + next.length + ambientFrames.length
|
|
123
|
-
}
|
|
124
|
-
}, review);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// src/aperture/explainability.ts
|
|
128
|
-
function asRecord(value) {
|
|
129
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
130
|
-
}
|
|
131
|
-
function readStringArray(value) {
|
|
132
|
-
if (!Array.isArray(value)) return [];
|
|
133
|
-
return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
134
|
-
}
|
|
135
|
-
function readSemanticConfidence(frame) {
|
|
136
|
-
const semantic = asRecord(frame.metadata?.semantic);
|
|
137
|
-
const confidence = semantic?.confidence;
|
|
138
|
-
return confidence === "low" || confidence === "medium" || confidence === "high" ? confidence : null;
|
|
139
|
-
}
|
|
140
|
-
function readSemanticRelationHints(frame) {
|
|
141
|
-
const semantic = asRecord(frame.metadata?.semantic);
|
|
142
|
-
const relationHints = semantic?.relationHints;
|
|
143
|
-
if (!Array.isArray(relationHints)) return [];
|
|
144
|
-
return relationHints.filter((entry) => {
|
|
145
|
-
if (typeof entry !== "object" || entry === null) return false;
|
|
146
|
-
const hint = entry;
|
|
147
|
-
return hint.kind === "same_issue" || hint.kind === "resolves" || hint.kind === "supersedes" || hint.kind === "repeats" || hint.kind === "escalates";
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
function relationHintLabel(hint) {
|
|
151
|
-
switch (hint.kind) {
|
|
152
|
-
case "same_issue":
|
|
153
|
-
return "Part of the same thread";
|
|
154
|
-
case "resolves":
|
|
155
|
-
return "Resolves an earlier blocker";
|
|
156
|
-
case "supersedes":
|
|
157
|
-
return "Moves the request forward";
|
|
158
|
-
case "repeats":
|
|
159
|
-
return "Repeats an earlier ask";
|
|
160
|
-
case "escalates":
|
|
161
|
-
return "Raises the urgency";
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
function laneReason(lane) {
|
|
165
|
-
switch (lane) {
|
|
166
|
-
case "now":
|
|
167
|
-
return "This is the most urgent item in the queue right now.";
|
|
168
|
-
case "next":
|
|
169
|
-
return "This is staged behind the current top item.";
|
|
170
|
-
case "ambient":
|
|
171
|
-
return "This is visible for awareness without needing action yet.";
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
function continuitySummary(frame) {
|
|
175
|
-
const episode = asRecord(frame.metadata?.episode);
|
|
176
|
-
const size = typeof episode?.size === "number" ? episode.size : null;
|
|
177
|
-
const state = typeof episode?.state === "string" ? episode.state.replace(/_/g, " ") : null;
|
|
178
|
-
if (!size || size <= 1) return null;
|
|
179
|
-
if (state) return `Part of a ${state} thread with ${size} related interactions.`;
|
|
180
|
-
return `Part of a thread with ${size} related interactions.`;
|
|
181
|
-
}
|
|
182
|
-
function frameSignals(frame) {
|
|
183
|
-
const attention = asRecord(frame.metadata?.attention);
|
|
184
|
-
const rationale = readStringArray(attention?.rationale);
|
|
185
|
-
const factors = readStringArray(frame.provenance?.factors);
|
|
186
|
-
return [.../* @__PURE__ */ new Set([...rationale.length > 0 ? rationale : factors, ...factors])];
|
|
187
|
-
}
|
|
188
|
-
function signalStrengthLabel(confidence) {
|
|
189
|
-
return `${confidence} confidence`;
|
|
190
|
-
}
|
|
191
|
-
function explainFrame(frame, lane) {
|
|
192
|
-
const relationLabels = [...new Set(readSemanticRelationHints(frame).map(relationHintLabel))];
|
|
193
|
-
return {
|
|
194
|
-
whyNow: frame.provenance?.whyNow ?? frame.summary ?? null,
|
|
195
|
-
laneReason: laneReason(lane),
|
|
196
|
-
signalStrength: readSemanticConfidence(frame),
|
|
197
|
-
signals: frameSignals(frame),
|
|
198
|
-
relationLabels,
|
|
199
|
-
continuity: continuitySummary(frame)
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// src/aperture/attention-context.ts
|
|
204
|
-
var ATTENTION_CONTEXT_IDS = {
|
|
205
|
-
approvalType: "approval-type",
|
|
206
|
-
requestedAmount: "requested-amount",
|
|
207
|
-
budgetReason: "budget-reason",
|
|
208
|
-
decisionContext: "decision-context",
|
|
209
|
-
linkedIssues: "linked-issues",
|
|
210
|
-
needsActionFrom: "needs-action-from",
|
|
211
|
-
blocksTarget: "blocks-target",
|
|
212
|
-
recommendedMove: "recommended-move",
|
|
213
|
-
issueStatus: "issue-status",
|
|
214
|
-
issuePriority: "issue-priority",
|
|
215
|
-
latestComment: "latest-comment",
|
|
216
|
-
agentStatus: "agent-status",
|
|
217
|
-
pauseReason: "pause-reason",
|
|
218
|
-
agentTitle: "agent-title"
|
|
219
|
-
};
|
|
220
|
-
function item(id, label, value) {
|
|
221
|
-
return { id, label, value };
|
|
222
|
-
}
|
|
223
|
-
function humanizeToken(value) {
|
|
224
|
-
return value.split(/[_\-.]/g).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
225
|
-
}
|
|
226
|
-
function approvalTypeItem(value) {
|
|
227
|
-
return item(ATTENTION_CONTEXT_IDS.approvalType, "Type", value);
|
|
228
|
-
}
|
|
229
|
-
function requestedAmountItem(value) {
|
|
230
|
-
return item(ATTENTION_CONTEXT_IDS.requestedAmount, "Requested amount", value);
|
|
231
|
-
}
|
|
232
|
-
function budgetReasonItem(value) {
|
|
233
|
-
return item(ATTENTION_CONTEXT_IDS.budgetReason, "Reason", value);
|
|
234
|
-
}
|
|
235
|
-
function decisionContextItem(value) {
|
|
236
|
-
return item(ATTENTION_CONTEXT_IDS.decisionContext, "Decision context", value);
|
|
237
|
-
}
|
|
238
12
|
|
|
239
13
|
// src/aperture/attention-language.ts
|
|
240
14
|
var GENERIC_QUEUED_JUDGMENT = "Important enough to keep visible, but not enough to displace now.";
|
|
@@ -242,9 +16,6 @@ var GENERIC_NEXT_JUDGMENT = GENERIC_QUEUED_JUDGMENT;
|
|
|
242
16
|
function approvalBlockingSummary(budgetOverride) {
|
|
243
17
|
return budgetOverride ? "Budget controls are blocking work until a board decision lands." : "A board decision is blocking work in Paperclip.";
|
|
244
18
|
}
|
|
245
|
-
function approvalBlockingWhyNow(budgetOverride) {
|
|
246
|
-
return budgetOverride ? "Budget controls are blocking work until a board decision lands." : "Paperclip is waiting on a human approval before work can continue.";
|
|
247
|
-
}
|
|
248
19
|
function genericJudgmentLine(frame, lane) {
|
|
249
20
|
if (lane === "now") {
|
|
250
21
|
if (isBudgetOverride(frame)) return approvalBlockingSummary(true);
|
|
@@ -257,170 +28,15 @@ function genericJudgmentLine(frame, lane) {
|
|
|
257
28
|
return "Useful for awareness, but not interrupting.";
|
|
258
29
|
}
|
|
259
30
|
|
|
260
|
-
// src/
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
companyId,
|
|
264
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
265
|
-
now: null,
|
|
266
|
-
next: [],
|
|
267
|
-
ambient: [],
|
|
268
|
-
counts: {
|
|
269
|
-
now: 0,
|
|
270
|
-
next: 0,
|
|
271
|
-
ambient: 0,
|
|
272
|
-
total: 0
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// src/aperture/approval-frames.ts
|
|
278
|
-
function approvalTitle(record) {
|
|
279
|
-
const payload = record.payload ?? {};
|
|
280
|
-
const explicitTitle = typeof payload.title === "string" ? payload.title : typeof payload.plan === "string" ? payload.plan : typeof payload.name === "string" ? payload.name : null;
|
|
281
|
-
if (explicitTitle) return explicitTitle;
|
|
282
|
-
return `${humanizeToken(record.type)} approval`;
|
|
283
|
-
}
|
|
284
|
-
function isBudgetOverrideRecord(record) {
|
|
285
|
-
return record.type === "budget_override_required";
|
|
286
|
-
}
|
|
287
|
-
function actionableApprovalRecords(records) {
|
|
288
|
-
if (!records) return [];
|
|
289
|
-
return records.filter((record) => record.status === "pending" || record.status === "revision_requested").sort((left, right) => {
|
|
290
|
-
const leftScore = isBudgetOverrideRecord(left) ? 1 : 0;
|
|
291
|
-
const rightScore = isBudgetOverrideRecord(right) ? 1 : 0;
|
|
292
|
-
if (leftScore !== rightScore) return rightScore - leftScore;
|
|
293
|
-
return (right.updatedAt ?? right.createdAt).localeCompare(left.updatedAt ?? left.createdAt);
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
function approvalRecordToFrame(record) {
|
|
297
|
-
const payload = record.payload ?? {};
|
|
298
|
-
const budgetOverride = isBudgetOverrideRecord(record);
|
|
299
|
-
const requestedAmount2 = typeof payload.requestedAmount === "string" ? payload.requestedAmount : void 0;
|
|
300
|
-
const reason = typeof payload.reason === "string" ? payload.reason : void 0;
|
|
301
|
-
const decisionContext = typeof payload.decisionContext === "string" ? payload.decisionContext : void 0;
|
|
302
|
-
const summary = typeof payload.summary === "string" ? payload.summary : approvalBlockingSummary(budgetOverride);
|
|
303
|
-
const updatedAt = record.updatedAt ?? record.createdAt;
|
|
304
|
-
const provenance = {
|
|
305
|
-
whyNow: approvalBlockingWhyNow(budgetOverride),
|
|
306
|
-
factors: budgetOverride ? ["budget stop", "approval", "operator decision"] : ["approval", "operator decision"]
|
|
307
|
-
};
|
|
308
|
-
return {
|
|
309
|
-
id: `approval-bootstrap:${record.id}`,
|
|
310
|
-
taskId: `approval:${record.id}`,
|
|
311
|
-
interactionId: `approval:${record.id}:approval`,
|
|
312
|
-
source: {
|
|
313
|
-
id: "paperclip:approval",
|
|
314
|
-
kind: "paperclip",
|
|
315
|
-
label: "Paperclip approval"
|
|
316
|
-
},
|
|
317
|
-
version: 1,
|
|
318
|
-
mode: "approval",
|
|
319
|
-
tone: "focused",
|
|
320
|
-
consequence: budgetOverride ? "high" : "medium",
|
|
321
|
-
title: approvalTitle(record),
|
|
322
|
-
summary,
|
|
323
|
-
context: {
|
|
324
|
-
items: [
|
|
325
|
-
{
|
|
326
|
-
...approvalTypeItem(humanizeToken(record.type))
|
|
327
|
-
},
|
|
328
|
-
...requestedAmount2 ? [requestedAmountItem(requestedAmount2)] : [],
|
|
329
|
-
...reason ? [budgetReasonItem(reason)] : [],
|
|
330
|
-
...decisionContext ? [decisionContextItem(decisionContext)] : []
|
|
331
|
-
]
|
|
332
|
-
},
|
|
333
|
-
responseSpec: {
|
|
334
|
-
kind: "approval",
|
|
335
|
-
actions: [
|
|
336
|
-
{ id: "approve", label: "Approve", kind: "approve", emphasis: "primary" },
|
|
337
|
-
{ id: "reject", label: "Reject", kind: "reject", emphasis: "danger" },
|
|
338
|
-
...budgetOverride ? [{ id: "request-revision", label: "Request revision", kind: "cancel", emphasis: "secondary" }] : []
|
|
339
|
-
]
|
|
340
|
-
},
|
|
341
|
-
provenance,
|
|
342
|
-
timing: {
|
|
343
|
-
createdAt: record.createdAt,
|
|
344
|
-
updatedAt
|
|
345
|
-
},
|
|
346
|
-
metadata: {
|
|
347
|
-
approvalStatus: record.status,
|
|
348
|
-
approvalType: record.type,
|
|
349
|
-
attention: {
|
|
350
|
-
rationale: provenance.factors
|
|
351
|
-
},
|
|
352
|
-
semantic: {
|
|
353
|
-
confidence: "high"
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
function mergeSnapshotWithApprovals(snapshot, companyId, approvals, review) {
|
|
359
|
-
const approvalFrames = actionableApprovalRecords(approvals).map(approvalRecordToFrame);
|
|
360
|
-
const base = snapshot ?? createEmptySnapshot(companyId);
|
|
361
|
-
const merged = mergeStoredFrames(base, companyId, [
|
|
362
|
-
...approvalFrames.map((frame, index) => ({ frame, lane: index === 0 ? "now" : "next" }))
|
|
363
|
-
], review);
|
|
364
|
-
return merged;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// src/aperture/issue-intelligence.ts
|
|
368
|
-
function issueBlocksTargetLine(target) {
|
|
369
|
-
return `Blocks ${target} until the review lands.`;
|
|
370
|
-
}
|
|
371
|
-
function issueNeedsActionFromLine(owner) {
|
|
372
|
-
return `Needs action from ${owner}.`;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// src/ui/interaction-copy.ts
|
|
376
|
-
function actionErrorMessage(error, fallback) {
|
|
377
|
-
return error instanceof Error ? error.message : fallback;
|
|
378
|
-
}
|
|
379
|
-
function acknowledgeSuccessMessage(title) {
|
|
380
|
-
return `Acknowledged ${title}.`;
|
|
381
|
-
}
|
|
382
|
-
function acknowledgeFailureMessage(error) {
|
|
383
|
-
return actionErrorMessage(error, "Failed to acknowledge frame.");
|
|
384
|
-
}
|
|
385
|
-
function issueFrameUnsupportedMessage() {
|
|
386
|
-
return "This frame is not backed by a Paperclip issue.";
|
|
387
|
-
}
|
|
388
|
-
function commentSuccessMessage(title) {
|
|
389
|
-
return `Posted a comment on ${title}.`;
|
|
390
|
-
}
|
|
391
|
-
function commentFailureMessage(error) {
|
|
392
|
-
return actionErrorMessage(error, "Failed to post comment.");
|
|
393
|
-
}
|
|
394
|
-
function approvalFrameUnsupportedMessage() {
|
|
395
|
-
return "This frame is not backed by a Paperclip approval.";
|
|
396
|
-
}
|
|
397
|
-
function approvalDecisionSuccessMessage(title, decision) {
|
|
398
|
-
return `${decision === "approve" ? "Approved" : "Rejected"} ${title}.`;
|
|
399
|
-
}
|
|
400
|
-
function approvalDecisionFailureMessage(error) {
|
|
401
|
-
return actionErrorMessage(error, "Failed to submit approval decision.");
|
|
402
|
-
}
|
|
403
|
-
function approvalRevisionSuccessMessage(title) {
|
|
404
|
-
return `Requested revision for ${title}.`;
|
|
405
|
-
}
|
|
406
|
-
function approvalRevisionFailureMessage(error) {
|
|
407
|
-
return actionErrorMessage(error, "Failed to request approval revision.");
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// src/ui/index.tsx
|
|
411
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
31
|
+
// src/ui/chrome.tsx
|
|
32
|
+
import { useEffect, useRef, useState } from "react";
|
|
33
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
412
34
|
var ACCENT_COLOR = "#007ACC";
|
|
413
35
|
var ACCENT_BG = `${ACCENT_COLOR}14`;
|
|
414
36
|
var ACCENT_BORDER = `${ACCENT_COLOR}33`;
|
|
415
37
|
var ACCENT_BG_STYLE = { backgroundColor: ACCENT_COLOR };
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
return {
|
|
419
|
-
key: "focus",
|
|
420
|
-
wordmark: "Focus",
|
|
421
|
-
headingEmptyState: "No focus state yet.",
|
|
422
|
-
loadingLabel: "Loading Focus\u2026"
|
|
423
|
-
};
|
|
38
|
+
function cn(...parts) {
|
|
39
|
+
return parts.filter(Boolean).join(" ");
|
|
424
40
|
}
|
|
425
41
|
function useAccentColor() {
|
|
426
42
|
const ref = useRef(null);
|
|
@@ -456,259 +72,40 @@ function useWideLayout(minWidth = 1024) {
|
|
|
456
72
|
}, [minWidth]);
|
|
457
73
|
return matches;
|
|
458
74
|
}
|
|
459
|
-
function
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
return
|
|
464
|
-
}
|
|
465
|
-
function formatRelativeTime(value) {
|
|
466
|
-
const timestamp = new Date(value).getTime();
|
|
467
|
-
if (Number.isNaN(timestamp)) return "recently";
|
|
468
|
-
const elapsedMs = timestamp - Date.now();
|
|
469
|
-
const elapsedMinutes = Math.round(elapsedMs / 6e4);
|
|
470
|
-
const formatter = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
|
|
471
|
-
if (Math.abs(elapsedMinutes) < 60) return formatter.format(elapsedMinutes, "minute");
|
|
472
|
-
const elapsedHours = Math.round(elapsedMinutes / 60);
|
|
473
|
-
if (Math.abs(elapsedHours) < 24) return formatter.format(elapsedHours, "hour");
|
|
474
|
-
const elapsedDays = Math.round(elapsedHours / 24);
|
|
475
|
-
return formatter.format(elapsedDays, "day");
|
|
476
|
-
}
|
|
477
|
-
function sourceLabel(frame) {
|
|
478
|
-
return frame.source?.label ?? "Paperclip";
|
|
479
|
-
}
|
|
480
|
-
function contextValue(frame, id) {
|
|
481
|
-
const item2 = frame.context?.items?.find((entry) => entry.id === id);
|
|
482
|
-
return typeof item2?.value === "string" && item2.value.trim().length > 0 ? item2.value : void 0;
|
|
483
|
-
}
|
|
484
|
-
function requestedAmount(frame) {
|
|
485
|
-
return contextValue(frame, ATTENTION_CONTEXT_IDS.requestedAmount);
|
|
486
|
-
}
|
|
487
|
-
function budgetReason(frame) {
|
|
488
|
-
return contextValue(frame, ATTENTION_CONTEXT_IDS.budgetReason);
|
|
75
|
+
function UnreadDot(props) {
|
|
76
|
+
const tone = props.tone ?? "default";
|
|
77
|
+
if (!props.visible && !props.reserveSpace) return null;
|
|
78
|
+
const style = props.visible ? tone === "muted" ? { backgroundColor: ACCENT_BORDER } : ACCENT_BG_STYLE : { backgroundColor: "transparent" };
|
|
79
|
+
return /* @__PURE__ */ jsx("span", { className: "inline-block h-2 w-2 rounded-full shrink-0", style, "aria-label": props.visible ? "Unread attention item" : void 0 });
|
|
489
80
|
}
|
|
490
|
-
function
|
|
491
|
-
return
|
|
81
|
+
function ExternalLinkIcon() {
|
|
82
|
+
return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", className: "h-3 w-3", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: [
|
|
83
|
+
/* @__PURE__ */ jsx("path", { d: "M6.5 3.5h-3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-3" }),
|
|
84
|
+
/* @__PURE__ */ jsx("path", { d: "M8.5 1.5h6v6" }),
|
|
85
|
+
/* @__PURE__ */ jsx("path", { d: "M14.5 1.5l-7 7" })
|
|
86
|
+
] });
|
|
492
87
|
}
|
|
493
|
-
function
|
|
494
|
-
return
|
|
88
|
+
function SkeletonLine(props) {
|
|
89
|
+
return /* @__PURE__ */ jsx("div", { className: cn("animate-pulse rounded bg-secondary/80", props.className), "aria-hidden": "true" });
|
|
495
90
|
}
|
|
496
|
-
function
|
|
497
|
-
return
|
|
91
|
+
function MessageCard(props) {
|
|
92
|
+
return /* @__PURE__ */ jsxs("div", { className: "border border-border bg-card p-4 shadow-sm", children: [
|
|
93
|
+
/* @__PURE__ */ jsx("div", { className: cn("text-sm font-medium", props.accent && "text-foreground"), children: props.title }),
|
|
94
|
+
/* @__PURE__ */ jsx("div", { className: "mt-1 text-sm text-muted-foreground", children: props.body })
|
|
95
|
+
] });
|
|
498
96
|
}
|
|
499
|
-
function
|
|
500
|
-
return
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
function driverLabel(frame) {
|
|
513
|
-
if (isBudgetOverride(frame)) return "budget stop";
|
|
514
|
-
const issueStatus = frame.metadata?.issueStatus;
|
|
515
|
-
if (typeof issueStatus === "string" && issueStatus.trim().length > 0) {
|
|
516
|
-
return operatorDriverLabel(issueStatus);
|
|
517
|
-
}
|
|
518
|
-
const pauseReason = frame.metadata?.pauseReason;
|
|
519
|
-
if (typeof pauseReason === "string" && pauseReason.trim().length > 0) {
|
|
520
|
-
return operatorDriverLabel(pauseReason);
|
|
521
|
-
}
|
|
522
|
-
const agentStatus = frame.metadata?.agentStatus;
|
|
523
|
-
if (typeof agentStatus === "string" && agentStatus.trim().length > 0) {
|
|
524
|
-
return operatorDriverLabel(agentStatus);
|
|
525
|
-
}
|
|
526
|
-
return null;
|
|
527
|
-
}
|
|
528
|
-
function driverBadgeStyle() {
|
|
529
|
-
return {
|
|
530
|
-
className: "border-transparent",
|
|
531
|
-
style: { color: ACCENT_COLOR, backgroundColor: ACCENT_BG, borderColor: ACCENT_BORDER }
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
function requestDescriptor(frame) {
|
|
535
|
-
const driver = driverLabel(frame);
|
|
536
|
-
return driver ? `${sourceLabel(frame)} \xB7 ${driver}` : sourceLabel(frame);
|
|
537
|
-
}
|
|
538
|
-
function postureForSnapshot(snapshot) {
|
|
539
|
-
const current = snapshot.now;
|
|
540
|
-
if (current?.tone === "critical" || current?.consequence === "high" || snapshot.counts.now + snapshot.counts.next >= 3) {
|
|
541
|
-
return { glyph: "\u25CF", label: "busy" };
|
|
542
|
-
}
|
|
543
|
-
if (snapshot.counts.now > 0 || snapshot.counts.next > 0 || snapshot.counts.ambient > 0) {
|
|
544
|
-
return { glyph: "\u25D0", label: "elevated" };
|
|
545
|
-
}
|
|
546
|
-
return { glyph: "\u25CB", label: "calm" };
|
|
547
|
-
}
|
|
548
|
-
function actionableCount(snapshot) {
|
|
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;
|
|
554
|
-
}
|
|
555
|
-
function judgmentLine(frame, lane) {
|
|
556
|
-
if (frame.provenance?.whyNow) return frame.provenance.whyNow;
|
|
557
|
-
return genericJudgmentLine(frame, lane);
|
|
558
|
-
}
|
|
559
|
-
function nextPrimaryText(frame) {
|
|
560
|
-
const recommendedMove = recommendedMoveValue(frame);
|
|
561
|
-
if (recommendedMove) return recommendedMove;
|
|
562
|
-
const summary = frame.summary?.trim();
|
|
563
|
-
if (summary) return summary;
|
|
564
|
-
const fallback = judgmentLine(frame, "next");
|
|
565
|
-
return fallback === GENERIC_QUEUED_JUDGMENT ? null : fallback;
|
|
566
|
-
}
|
|
567
|
-
function supportingLine(frame, lane) {
|
|
568
|
-
const owner = actionOwnerValue(frame);
|
|
569
|
-
const target = blockingTargetValue(frame);
|
|
570
|
-
if (target && lane === "now" && entityTypeFromFrame(frame) === "issue" && driverLabel(frame) === "review required") {
|
|
571
|
-
return issueBlocksTargetLine(target);
|
|
572
|
-
}
|
|
573
|
-
if (owner && lane === "now" && entityTypeFromFrame(frame) === "issue") {
|
|
574
|
-
return issueNeedsActionFromLine(owner);
|
|
575
|
-
}
|
|
576
|
-
if (lane === "now") {
|
|
577
|
-
const recommendedMove = recommendedMoveValue(frame)?.trim();
|
|
578
|
-
const summary = frame.summary?.trim();
|
|
579
|
-
const judgment2 = judgmentLine(frame, lane).trim();
|
|
580
|
-
if (recommendedMove && summary && summary !== recommendedMove && summary !== judgment2) {
|
|
581
|
-
return summary;
|
|
582
|
-
}
|
|
583
|
-
return null;
|
|
584
|
-
}
|
|
585
|
-
const judgment = judgmentLine(frame, lane);
|
|
586
|
-
return judgment.trim().length > 0 ? judgment : null;
|
|
587
|
-
}
|
|
588
|
-
function approvalIdForFrame(frame) {
|
|
589
|
-
const [kind, id] = frame.taskId.split(":");
|
|
590
|
-
return kind === "approval" && id ? id : null;
|
|
591
|
-
}
|
|
592
|
-
function entityIdFromFrame(frame) {
|
|
593
|
-
const parts = frame.taskId.split(":");
|
|
594
|
-
return parts.length >= 2 ? parts.slice(1).join(":") : null;
|
|
595
|
-
}
|
|
596
|
-
function entityTypeFromFrame(frame) {
|
|
597
|
-
const parts = frame.taskId.split(":");
|
|
598
|
-
return parts.length >= 2 ? parts[0] : null;
|
|
599
|
-
}
|
|
600
|
-
function itemHref(frame, companyPrefix) {
|
|
601
|
-
const entityType = entityTypeFromFrame(frame);
|
|
602
|
-
const entityId = entityIdFromFrame(frame);
|
|
603
|
-
if (!entityType || !entityId || !companyPrefix) return null;
|
|
604
|
-
const pluralType = entityType === "run" ? "runs" : entityType === "approval" ? "approvals" : entityType === "issue" ? "issues" : entityType === "agent" ? "agents" : null;
|
|
605
|
-
if (!pluralType) return null;
|
|
606
|
-
return `/${companyPrefix}/${pluralType}/${entityId}`;
|
|
607
|
-
}
|
|
608
|
-
function costsHref(companyPrefix) {
|
|
609
|
-
return companyPrefix ? `/${companyPrefix}/costs` : null;
|
|
610
|
-
}
|
|
611
|
-
function activityHref(frame, companyPrefix) {
|
|
612
|
-
if (!companyPrefix) return null;
|
|
613
|
-
return frame.metadata?.activityPath ? `/${companyPrefix}/${frame.metadata.activityPath}` : null;
|
|
614
|
-
}
|
|
615
|
-
function primaryLinkLabel(frame) {
|
|
616
|
-
const entityType = entityTypeFromFrame(frame);
|
|
617
|
-
switch (entityType) {
|
|
618
|
-
case "approval":
|
|
619
|
-
return "Open approval";
|
|
620
|
-
case "issue":
|
|
621
|
-
return "Open issue";
|
|
622
|
-
case "run":
|
|
623
|
-
return "Open run";
|
|
624
|
-
case "agent":
|
|
625
|
-
return "Open agent";
|
|
626
|
-
default:
|
|
627
|
-
return "Open in Paperclip";
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
function responseKind(frame, lane) {
|
|
631
|
-
if (approvalIdForFrame(frame) && (frame.responseSpec?.kind === "approval" || frame.mode === "approval")) {
|
|
632
|
-
return "approval";
|
|
633
|
-
}
|
|
634
|
-
if (lane !== "ambient" && (frame.responseSpec?.kind === "acknowledge" || frame.mode === "status")) {
|
|
635
|
-
return "acknowledge";
|
|
636
|
-
}
|
|
637
|
-
return "none";
|
|
638
|
-
}
|
|
639
|
-
function unreadCount(snapshot) {
|
|
640
|
-
return snapshot?.review?.unread.total ?? 0;
|
|
641
|
-
}
|
|
642
|
-
function applyLocalSuppressions(snapshot, suppressedAtByTaskId) {
|
|
643
|
-
if (!snapshot || Object.keys(suppressedAtByTaskId).length === 0) return snapshot;
|
|
644
|
-
const keepFrame = (frame) => {
|
|
645
|
-
const suppressedAt = suppressedAtByTaskId[frame.taskId];
|
|
646
|
-
if (!suppressedAt) return true;
|
|
647
|
-
return frameUpdatedAt(frame, snapshot.updatedAt).localeCompare(suppressedAt) > 0;
|
|
648
|
-
};
|
|
649
|
-
const next = snapshot.next.filter(keepFrame);
|
|
650
|
-
const ambient = snapshot.ambient.filter(keepFrame);
|
|
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);
|
|
654
|
-
return {
|
|
655
|
-
...snapshot,
|
|
656
|
-
now,
|
|
657
|
-
next: nextFrames,
|
|
658
|
-
ambient,
|
|
659
|
-
counts: {
|
|
660
|
-
now: now ? 1 : 0,
|
|
661
|
-
next: nextFrames.length,
|
|
662
|
-
ambient: ambient.length,
|
|
663
|
-
total: (now ? 1 : 0) + nextFrames.length + ambient.length
|
|
664
|
-
}
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
function suppressionMapFromReview(review) {
|
|
668
|
-
if (!review) return {};
|
|
669
|
-
return Object.fromEntries(
|
|
670
|
-
Object.entries(review.frames).filter(([, state]) => typeof state?.suppressedAt === "string" && state.suppressedAt.length > 0).map(([taskId, state]) => [taskId, state.suppressedAt])
|
|
671
|
-
);
|
|
672
|
-
}
|
|
673
|
-
function isFrameUnreadInSnapshot(frame, snapshot) {
|
|
674
|
-
const seenAt = snapshot.review?.lastSeenAt;
|
|
675
|
-
if (!seenAt) return true;
|
|
676
|
-
return frameUpdatedAt(frame, snapshot.updatedAt).localeCompare(seenAt) > 0;
|
|
677
|
-
}
|
|
678
|
-
function UnreadDot(props) {
|
|
679
|
-
const tone = props.tone ?? "default";
|
|
680
|
-
if (!props.visible && !props.reserveSpace) return null;
|
|
681
|
-
const style = props.visible ? tone === "muted" ? { backgroundColor: ACCENT_BORDER } : ACCENT_BG_STYLE : { backgroundColor: "transparent" };
|
|
682
|
-
return /* @__PURE__ */ jsx("span", { className: "inline-block h-2 w-2 rounded-full shrink-0", style, "aria-label": props.visible ? "Unread attention item" : void 0 });
|
|
683
|
-
}
|
|
684
|
-
function ExternalLinkIcon() {
|
|
685
|
-
return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", className: "h-3 w-3", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: [
|
|
686
|
-
/* @__PURE__ */ jsx("path", { d: "M6.5 3.5h-3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-3" }),
|
|
687
|
-
/* @__PURE__ */ jsx("path", { d: "M8.5 1.5h6v6" }),
|
|
688
|
-
/* @__PURE__ */ jsx("path", { d: "M14.5 1.5l-7 7" })
|
|
689
|
-
] });
|
|
690
|
-
}
|
|
691
|
-
function SkeletonLine(props) {
|
|
692
|
-
return /* @__PURE__ */ jsx("div", { className: cn("animate-pulse rounded bg-secondary/80", props.className), "aria-hidden": "true" });
|
|
693
|
-
}
|
|
694
|
-
function MessageCard(props) {
|
|
695
|
-
return /* @__PURE__ */ jsxs("div", { className: "border border-border bg-card p-4 shadow-sm", children: [
|
|
696
|
-
/* @__PURE__ */ jsx("div", { className: cn("text-sm font-medium", props.accent && "text-foreground"), children: props.title }),
|
|
697
|
-
/* @__PURE__ */ jsx("div", { className: "mt-1 text-sm text-muted-foreground", children: props.body })
|
|
698
|
-
] });
|
|
699
|
-
}
|
|
700
|
-
function WidgetLoadingState({ label }) {
|
|
701
|
-
return /* @__PURE__ */ jsxs("div", { className: "border border-border bg-card px-4 py-3 shadow-sm", children: [
|
|
702
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
703
|
-
/* @__PURE__ */ jsx(SkeletonLine, { className: "mt-0.5 h-3 w-3 rounded-full" }),
|
|
704
|
-
/* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-2", children: [
|
|
705
|
-
/* @__PURE__ */ jsx(SkeletonLine, { className: "h-3 w-20" }),
|
|
706
|
-
/* @__PURE__ */ jsx(SkeletonLine, { className: "h-4 w-48" })
|
|
707
|
-
] }),
|
|
708
|
-
/* @__PURE__ */ jsx(SkeletonLine, { className: "h-3 w-24" })
|
|
709
|
-
] }),
|
|
710
|
-
/* @__PURE__ */ jsx("div", { className: "sr-only", children: label })
|
|
711
|
-
] });
|
|
97
|
+
function WidgetLoadingState({ label }) {
|
|
98
|
+
return /* @__PURE__ */ jsxs("div", { className: "border border-border bg-card px-4 py-3 shadow-sm", children: [
|
|
99
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
100
|
+
/* @__PURE__ */ jsx(SkeletonLine, { className: "mt-0.5 h-3 w-3 rounded-full" }),
|
|
101
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-2", children: [
|
|
102
|
+
/* @__PURE__ */ jsx(SkeletonLine, { className: "h-3 w-20" }),
|
|
103
|
+
/* @__PURE__ */ jsx(SkeletonLine, { className: "h-4 w-48" })
|
|
104
|
+
] }),
|
|
105
|
+
/* @__PURE__ */ jsx(SkeletonLine, { className: "h-3 w-24" })
|
|
106
|
+
] }),
|
|
107
|
+
/* @__PURE__ */ jsx("div", { className: "sr-only", children: label })
|
|
108
|
+
] });
|
|
712
109
|
}
|
|
713
110
|
function PageLoadingState({ label }) {
|
|
714
111
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-5", "aria-label": label, children: [
|
|
@@ -753,124 +150,117 @@ function StatusToast({ message }) {
|
|
|
753
150
|
}
|
|
754
151
|
);
|
|
755
152
|
}
|
|
756
|
-
function
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
headers,
|
|
767
|
-
...init
|
|
768
|
-
} = options;
|
|
769
|
-
let lastError = null;
|
|
770
|
-
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
771
|
-
try {
|
|
772
|
-
const response = await window.fetch(path, {
|
|
773
|
-
credentials: "same-origin",
|
|
774
|
-
...init,
|
|
775
|
-
headers: {
|
|
776
|
-
Accept: "application/json",
|
|
777
|
-
"X-Requested-With": "XMLHttpRequest",
|
|
778
|
-
...csrfToken ? { "X-CSRF-Token": csrfToken } : {},
|
|
779
|
-
...headers ?? {}
|
|
780
|
-
}
|
|
781
|
-
});
|
|
782
|
-
if (!response.ok) {
|
|
783
|
-
const responseBody = await response.text().catch(() => "");
|
|
784
|
-
const detail = responseBody.trim().length > 0 ? `: ${responseBody.trim()}` : "";
|
|
785
|
-
throw new Error(`Request failed (${response.status})${detail}`);
|
|
786
|
-
}
|
|
787
|
-
if (!expectJson) return void 0;
|
|
788
|
-
return await response.json();
|
|
789
|
-
} catch (error) {
|
|
790
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
791
|
-
if (attempt >= retries) break;
|
|
792
|
-
await new Promise((resolve) => window.setTimeout(resolve, 250 * (attempt + 1)));
|
|
153
|
+
function Badge(props) {
|
|
154
|
+
return /* @__PURE__ */ jsx(
|
|
155
|
+
"span",
|
|
156
|
+
{
|
|
157
|
+
className: cn(
|
|
158
|
+
"inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap shrink-0",
|
|
159
|
+
props.className
|
|
160
|
+
),
|
|
161
|
+
style: props.style,
|
|
162
|
+
children: props.children
|
|
793
163
|
}
|
|
794
|
-
|
|
795
|
-
throw lastError ?? new Error("Request failed.");
|
|
796
|
-
}
|
|
797
|
-
function compactTitle(frame) {
|
|
798
|
-
return frame.title.trim().length > 0 ? frame.title : "Untitled frame";
|
|
164
|
+
);
|
|
799
165
|
}
|
|
800
|
-
function
|
|
801
|
-
const [
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
166
|
+
function ActionButton(props) {
|
|
167
|
+
const [hovered, setHovered] = useState(false);
|
|
168
|
+
const toneClass = props.tone === "primary" ? "bg-green-700 text-white hover:bg-green-600" : props.tone === "accent" ? "text-white" : props.tone === "danger" ? "bg-destructive text-white hover:bg-destructive/90 dark:bg-destructive/60" : "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50";
|
|
169
|
+
return /* @__PURE__ */ jsx(
|
|
170
|
+
"button",
|
|
171
|
+
{
|
|
172
|
+
type: "button",
|
|
173
|
+
onClick: props.onClick,
|
|
174
|
+
onMouseEnter: () => setHovered(true),
|
|
175
|
+
onMouseLeave: () => setHovered(false),
|
|
176
|
+
disabled: props.disabled,
|
|
177
|
+
className: cn(
|
|
178
|
+
"inline-flex h-8 items-center justify-center gap-2 rounded-md px-3 text-sm font-medium",
|
|
179
|
+
"transition-[color,background-color,border-color,box-shadow,opacity,filter]",
|
|
180
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
181
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
182
|
+
props.disabled && /…|\.\.\./.test(props.label) && "animate-pulse",
|
|
183
|
+
toneClass,
|
|
184
|
+
props.className
|
|
185
|
+
),
|
|
186
|
+
style: props.tone === "accent" ? { ...ACCENT_BG_STYLE, filter: hovered && !props.disabled ? "brightness(1.08)" : void 0 } : void 0,
|
|
187
|
+
children: props.label
|
|
811
188
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
}
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
function QueueMovementBadge({ movement }) {
|
|
192
|
+
if (!movement) return null;
|
|
193
|
+
return /* @__PURE__ */ jsxs(
|
|
194
|
+
"span",
|
|
195
|
+
{
|
|
196
|
+
className: "inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-medium",
|
|
197
|
+
style: {
|
|
198
|
+
color: ACCENT_COLOR,
|
|
199
|
+
backgroundColor: ACCENT_BG,
|
|
200
|
+
borderColor: ACCENT_BORDER
|
|
201
|
+
},
|
|
202
|
+
children: [
|
|
203
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: movement === "up" ? "\u2191" : "\u2193" }),
|
|
204
|
+
movement === "up" ? "moved up" : "moved down"
|
|
205
|
+
]
|
|
830
206
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/ui/focus-model.tsx
|
|
211
|
+
import { usePluginData, usePluginStream } from "@paperclipai/plugin-sdk/ui";
|
|
212
|
+
import { useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
|
|
213
|
+
var ATTENTION_UPDATES_STREAM = "attention-updates";
|
|
214
|
+
function currentSurfaceBrand() {
|
|
837
215
|
return {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
setRefreshVersion((value) => value + 1);
|
|
843
|
-
}
|
|
216
|
+
key: "focus",
|
|
217
|
+
wordmark: "Focus",
|
|
218
|
+
headingEmptyState: "No focus state yet.",
|
|
219
|
+
loadingLabel: "Loading Focus\u2026"
|
|
844
220
|
};
|
|
845
221
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
222
|
+
function pluginPagePath(companyPrefix) {
|
|
223
|
+
return companyPrefix ? `/${companyPrefix}/aperture` : "/aperture";
|
|
224
|
+
}
|
|
225
|
+
function formatRelativeTime(value) {
|
|
226
|
+
const timestamp = new Date(value).getTime();
|
|
227
|
+
if (Number.isNaN(timestamp)) return "recently";
|
|
228
|
+
const elapsedMs = timestamp - Date.now();
|
|
229
|
+
const elapsedMinutes = Math.round(elapsedMs / 6e4);
|
|
230
|
+
const formatter = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
|
|
231
|
+
if (Math.abs(elapsedMinutes) < 60) return formatter.format(elapsedMinutes, "minute");
|
|
232
|
+
const elapsedHours = Math.round(elapsedMinutes / 60);
|
|
233
|
+
if (Math.abs(elapsedHours) < 24) return formatter.format(elapsedHours, "hour");
|
|
234
|
+
const elapsedDays = Math.round(elapsedHours / 24);
|
|
235
|
+
return formatter.format(elapsedDays, "day");
|
|
236
|
+
}
|
|
237
|
+
function sourceLabel(frame) {
|
|
238
|
+
return frame.source?.label ?? "Paperclip";
|
|
239
|
+
}
|
|
240
|
+
function postureForSnapshot(snapshot) {
|
|
241
|
+
const current = snapshot.now;
|
|
242
|
+
if (current?.tone === "critical" || current?.consequence === "high" || snapshot.counts.now + snapshot.counts.next >= 3) {
|
|
243
|
+
return { glyph: "\u25CF", label: "busy" };
|
|
861
244
|
}
|
|
862
|
-
if (
|
|
863
|
-
|
|
245
|
+
if (snapshot.counts.now > 0 || snapshot.counts.next > 0 || snapshot.counts.ambient > 0) {
|
|
246
|
+
return { glyph: "\u25D0", label: "elevated" };
|
|
864
247
|
}
|
|
865
|
-
return
|
|
248
|
+
return { glyph: "\u25CB", label: "calm" };
|
|
866
249
|
}
|
|
867
|
-
function
|
|
868
|
-
return
|
|
250
|
+
function actionableCount(snapshot) {
|
|
251
|
+
return (snapshot.now ? 1 : 0) + snapshot.next.length;
|
|
252
|
+
}
|
|
253
|
+
function actionableLabel(snapshot) {
|
|
254
|
+
const actionable = actionableCount(snapshot);
|
|
255
|
+
return actionable > 0 ? `${actionable} action${actionable === 1 ? "" : "s"}` : null;
|
|
256
|
+
}
|
|
257
|
+
function unreadCount(snapshot) {
|
|
258
|
+
return snapshot?.review?.unread.total ?? 0;
|
|
869
259
|
}
|
|
870
|
-
function useAttentionPolling(companyId, refreshers, intervalMs =
|
|
871
|
-
const refreshersRef =
|
|
260
|
+
function useAttentionPolling(companyId, refreshers, intervalMs = 15e3) {
|
|
261
|
+
const refreshersRef = useRef2(refreshers);
|
|
872
262
|
refreshersRef.current = refreshers;
|
|
873
|
-
|
|
263
|
+
useEffect2(() => {
|
|
874
264
|
if (!companyId) return;
|
|
875
265
|
function refreshVisible() {
|
|
876
266
|
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
@@ -892,10 +282,10 @@ function useAttentionPolling(companyId, refreshers, intervalMs = 5e3) {
|
|
|
892
282
|
}, [companyId, intervalMs]);
|
|
893
283
|
}
|
|
894
284
|
function useQueueMovement(frames, ttlMs = 3500) {
|
|
895
|
-
const previousPositionsRef =
|
|
896
|
-
const timersRef =
|
|
897
|
-
const [movementById, setMovementById] =
|
|
898
|
-
|
|
285
|
+
const previousPositionsRef = useRef2({});
|
|
286
|
+
const timersRef = useRef2({});
|
|
287
|
+
const [movementById, setMovementById] = useState2({});
|
|
288
|
+
useEffect2(() => {
|
|
899
289
|
const nextPositions = Object.fromEntries(frames.map((frame, index) => [frame.interactionId, index]));
|
|
900
290
|
setMovementById((current) => {
|
|
901
291
|
const updates = {};
|
|
@@ -930,94 +320,801 @@ function useQueueMovement(frames, ttlMs = 3500) {
|
|
|
930
320
|
}
|
|
931
321
|
function useAttentionModel(companyId) {
|
|
932
322
|
const displayQuery = usePluginData("attention-display", companyId ? { companyId } : void 0);
|
|
933
|
-
const approvalsQuery = usePendingApprovals(companyId);
|
|
934
323
|
const updates = usePluginStream(
|
|
935
324
|
ATTENTION_UPDATES_STREAM,
|
|
936
325
|
companyId ? { companyId } : void 0
|
|
937
326
|
);
|
|
938
|
-
useAttentionPolling(companyId, [displayQuery.refresh
|
|
939
|
-
|
|
327
|
+
useAttentionPolling(companyId, [displayQuery.refresh]);
|
|
328
|
+
useEffect2(() => {
|
|
940
329
|
if (!companyId || !updates.lastEvent) return;
|
|
941
330
|
displayQuery.refresh();
|
|
942
|
-
approvalsQuery.refresh();
|
|
943
331
|
}, [companyId, updates.lastEvent?.updatedAt, updates.lastEvent?.eventType]);
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
[displayQuery.data, approvalsQuery.data, companyId]
|
|
947
|
-
);
|
|
948
|
-
return {
|
|
949
|
-
snapshot,
|
|
332
|
+
return useMemo(() => ({
|
|
333
|
+
snapshot: displayQuery.data?.snapshot ?? null,
|
|
950
334
|
review: displayQuery.data?.reviewState ?? null,
|
|
951
|
-
loading: displayQuery.loading
|
|
952
|
-
error: displayQuery.error
|
|
335
|
+
loading: displayQuery.loading,
|
|
336
|
+
error: displayQuery.error,
|
|
953
337
|
refresh() {
|
|
954
338
|
displayQuery.refresh();
|
|
955
|
-
approvalsQuery.refresh();
|
|
956
339
|
}
|
|
340
|
+
}), [displayQuery]);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/aperture/contracts.ts
|
|
344
|
+
function asRecord(value) {
|
|
345
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
346
|
+
}
|
|
347
|
+
function readString(value) {
|
|
348
|
+
return typeof value === "string" && value.trim().length > 0 ? value : void 0;
|
|
349
|
+
}
|
|
350
|
+
function readNullableString(value) {
|
|
351
|
+
if (value === null) return null;
|
|
352
|
+
return readString(value);
|
|
353
|
+
}
|
|
354
|
+
function readStringArray(value) {
|
|
355
|
+
if (!Array.isArray(value)) return void 0;
|
|
356
|
+
const entries = value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
357
|
+
return entries.length > 0 ? entries : void 0;
|
|
358
|
+
}
|
|
359
|
+
function readRelationHints(value) {
|
|
360
|
+
if (!Array.isArray(value)) return void 0;
|
|
361
|
+
const hints = value.filter((entry) => {
|
|
362
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
363
|
+
const hint = entry;
|
|
364
|
+
return hint.kind === "same_issue" || hint.kind === "resolves" || hint.kind === "supersedes" || hint.kind === "repeats" || hint.kind === "escalates";
|
|
365
|
+
});
|
|
366
|
+
return hints.length > 0 ? hints : void 0;
|
|
367
|
+
}
|
|
368
|
+
function readFocusMetadata(frame) {
|
|
369
|
+
const metadata = asRecord(frame.metadata);
|
|
370
|
+
const attention = asRecord(metadata?.attention);
|
|
371
|
+
const semantic = asRecord(metadata?.semantic);
|
|
372
|
+
const episode = asRecord(metadata?.episode);
|
|
373
|
+
const issueIntelligence = asRecord(metadata?.issueIntelligence);
|
|
374
|
+
return {
|
|
375
|
+
entityType: readString(metadata?.entityType),
|
|
376
|
+
issueStatus: readString(metadata?.issueStatus),
|
|
377
|
+
issuePriority: readString(metadata?.issuePriority),
|
|
378
|
+
approvalStatus: readString(metadata?.approvalStatus),
|
|
379
|
+
approvalType: readString(metadata?.approvalType),
|
|
380
|
+
agentStatus: readString(metadata?.agentStatus),
|
|
381
|
+
pauseReason: readNullableString(metadata?.pauseReason),
|
|
382
|
+
liveReconciled: typeof metadata?.liveReconciled === "boolean" ? metadata.liveReconciled : void 0,
|
|
383
|
+
activityPath: readString(metadata?.activityPath),
|
|
384
|
+
attention: attention ? {
|
|
385
|
+
rationale: readStringArray(attention.rationale)
|
|
386
|
+
} : void 0,
|
|
387
|
+
semantic: semantic ? {
|
|
388
|
+
confidence: semantic.confidence === "low" || semantic.confidence === "medium" || semantic.confidence === "high" ? semantic.confidence : void 0,
|
|
389
|
+
relationHints: readRelationHints(semantic.relationHints)
|
|
390
|
+
} : void 0,
|
|
391
|
+
episode: episode ? {
|
|
392
|
+
size: typeof episode.size === "number" ? episode.size : void 0,
|
|
393
|
+
state: readString(episode.state)
|
|
394
|
+
} : void 0,
|
|
395
|
+
issueIntelligence: issueIntelligence ? {
|
|
396
|
+
matchedRuleIds: readStringArray(issueIntelligence.matchedRuleIds),
|
|
397
|
+
owner: readNullableString(issueIntelligence.owner),
|
|
398
|
+
ownerKind: issueIntelligence.ownerKind === "agent" || issueIntelligence.ownerKind === "human" ? issueIntelligence.ownerKind : issueIntelligence.ownerKind === null ? null : void 0,
|
|
399
|
+
blockingTarget: readNullableString(issueIntelligence.blockingTarget)
|
|
400
|
+
} : void 0
|
|
957
401
|
};
|
|
958
402
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
403
|
+
|
|
404
|
+
// src/aperture/attention-context.ts
|
|
405
|
+
var ATTENTION_CONTEXT_IDS = {
|
|
406
|
+
approvalType: "approval-type",
|
|
407
|
+
requestedAmount: "requested-amount",
|
|
408
|
+
budgetReason: "budget-reason",
|
|
409
|
+
decisionContext: "decision-context",
|
|
410
|
+
linkedIssues: "linked-issues",
|
|
411
|
+
needsActionFrom: "needs-action-from",
|
|
412
|
+
blocksTarget: "blocks-target",
|
|
413
|
+
recommendedMove: "recommended-move",
|
|
414
|
+
issueStatus: "issue-status",
|
|
415
|
+
issuePriority: "issue-priority",
|
|
416
|
+
latestComment: "latest-comment",
|
|
417
|
+
agentStatus: "agent-status",
|
|
418
|
+
pauseReason: "pause-reason",
|
|
419
|
+
agentTitle: "agent-title"
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// src/aperture/task-ref.ts
|
|
423
|
+
function decodeComponent(value) {
|
|
424
|
+
try {
|
|
425
|
+
const decoded = decodeURIComponent(value);
|
|
426
|
+
return decoded.trim().length > 0 ? decoded : null;
|
|
427
|
+
} catch {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
971
430
|
}
|
|
972
|
-
function
|
|
973
|
-
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
431
|
+
function parseTaskId(taskId) {
|
|
432
|
+
if (typeof taskId !== "string") return null;
|
|
433
|
+
const separatorIndex = taskId.indexOf(":");
|
|
434
|
+
if (separatorIndex <= 0 || separatorIndex >= taskId.length - 1) return null;
|
|
435
|
+
const kind = decodeComponent(taskId.slice(0, separatorIndex));
|
|
436
|
+
const id = decodeComponent(taskId.slice(separatorIndex + 1));
|
|
437
|
+
if (!kind || !id) return null;
|
|
438
|
+
return { kind, id };
|
|
439
|
+
}
|
|
440
|
+
function taskKind(taskId) {
|
|
441
|
+
return parseTaskId(taskId)?.kind ?? null;
|
|
442
|
+
}
|
|
443
|
+
function taskEntityId(taskId) {
|
|
444
|
+
return parseTaskId(taskId)?.id ?? null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/aperture/issue-intelligence.ts
|
|
448
|
+
function issueBlocksTargetLine(target) {
|
|
449
|
+
return `Blocks ${target} until the review lands.`;
|
|
450
|
+
}
|
|
451
|
+
function issueNeedsActionFromLine(owner) {
|
|
452
|
+
return `Needs action from ${owner}.`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/ui/frame-helpers.tsx
|
|
456
|
+
import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
|
|
457
|
+
function contextValue(frame, id) {
|
|
458
|
+
const item = frame.context?.items?.find((entry) => entry.id === id);
|
|
459
|
+
return typeof item?.value === "string" && item.value.trim().length > 0 ? item.value : void 0;
|
|
460
|
+
}
|
|
461
|
+
function requestedAmount(frame) {
|
|
462
|
+
return contextValue(frame, ATTENTION_CONTEXT_IDS.requestedAmount);
|
|
463
|
+
}
|
|
464
|
+
function budgetReason(frame) {
|
|
465
|
+
return contextValue(frame, ATTENTION_CONTEXT_IDS.budgetReason);
|
|
466
|
+
}
|
|
467
|
+
function recommendedMoveValue(frame) {
|
|
468
|
+
return contextValue(frame, ATTENTION_CONTEXT_IDS.recommendedMove);
|
|
469
|
+
}
|
|
470
|
+
function actionOwnerValue(frame) {
|
|
471
|
+
return contextValue(frame, ATTENTION_CONTEXT_IDS.needsActionFrom);
|
|
472
|
+
}
|
|
473
|
+
function blockingTargetValue(frame) {
|
|
474
|
+
return contextValue(frame, ATTENTION_CONTEXT_IDS.blocksTarget);
|
|
475
|
+
}
|
|
476
|
+
function impactLabel(frame) {
|
|
477
|
+
return `${frame.consequence} impact`;
|
|
478
|
+
}
|
|
479
|
+
function operatorDriverLabel(value) {
|
|
480
|
+
switch (value) {
|
|
481
|
+
case "in_review":
|
|
482
|
+
return "review required";
|
|
483
|
+
case "pending_approval":
|
|
484
|
+
return "approval required";
|
|
485
|
+
default:
|
|
486
|
+
return value.replace(/_/g, " ");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function driverLabel(frame) {
|
|
490
|
+
if (isBudgetOverride(frame)) return "budget stop";
|
|
491
|
+
const metadata = readFocusMetadata(frame);
|
|
492
|
+
const issueStatus = metadata.issueStatus;
|
|
493
|
+
if (typeof issueStatus === "string" && issueStatus.trim().length > 0) {
|
|
494
|
+
return operatorDriverLabel(issueStatus);
|
|
495
|
+
}
|
|
496
|
+
const pauseReason = metadata.pauseReason;
|
|
497
|
+
if (typeof pauseReason === "string" && pauseReason.trim().length > 0) {
|
|
498
|
+
return operatorDriverLabel(pauseReason);
|
|
499
|
+
}
|
|
500
|
+
const agentStatus = metadata.agentStatus;
|
|
501
|
+
if (typeof agentStatus === "string" && agentStatus.trim().length > 0) {
|
|
502
|
+
return operatorDriverLabel(agentStatus);
|
|
503
|
+
}
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
function driverBadgeStyle() {
|
|
507
|
+
return {
|
|
508
|
+
className: "border-transparent",
|
|
509
|
+
style: { color: ACCENT_COLOR, backgroundColor: ACCENT_BG, borderColor: ACCENT_BORDER }
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function requestDescriptor(frame) {
|
|
513
|
+
const driver = driverLabel(frame);
|
|
514
|
+
return driver ? `${sourceLabel(frame)} \xB7 ${driver}` : sourceLabel(frame);
|
|
515
|
+
}
|
|
516
|
+
function judgmentLine(frame, lane) {
|
|
517
|
+
if (frame.provenance?.whyNow) return frame.provenance.whyNow;
|
|
518
|
+
return genericJudgmentLine(frame, lane);
|
|
519
|
+
}
|
|
520
|
+
function nextPrimaryText(frame) {
|
|
521
|
+
const recommendedMove = recommendedMoveValue(frame);
|
|
522
|
+
if (recommendedMove) return recommendedMove;
|
|
523
|
+
const summary = frame.summary?.trim();
|
|
524
|
+
if (summary) return summary;
|
|
525
|
+
const fallback = judgmentLine(frame, "next");
|
|
526
|
+
return fallback === GENERIC_QUEUED_JUDGMENT ? null : fallback;
|
|
527
|
+
}
|
|
528
|
+
function supportingLine(frame, lane) {
|
|
529
|
+
const owner = actionOwnerValue(frame);
|
|
530
|
+
const target = blockingTargetValue(frame);
|
|
531
|
+
if (target && lane === "now" && entityTypeFromFrame(frame) === "issue" && driverLabel(frame) === "review required") {
|
|
532
|
+
return issueBlocksTargetLine(target);
|
|
533
|
+
}
|
|
534
|
+
if (owner && lane === "now" && entityTypeFromFrame(frame) === "issue") {
|
|
535
|
+
return issueNeedsActionFromLine(owner);
|
|
536
|
+
}
|
|
537
|
+
if (lane === "now") {
|
|
538
|
+
const recommendedMove = recommendedMoveValue(frame)?.trim();
|
|
539
|
+
const summary = frame.summary?.trim();
|
|
540
|
+
const judgment2 = judgmentLine(frame, lane).trim();
|
|
541
|
+
if (recommendedMove && summary && summary !== recommendedMove && summary !== judgment2) {
|
|
542
|
+
return summary;
|
|
994
543
|
}
|
|
995
|
-
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
const judgment = judgmentLine(frame, lane);
|
|
547
|
+
return judgment.trim().length > 0 ? judgment : null;
|
|
996
548
|
}
|
|
997
|
-
function
|
|
998
|
-
|
|
999
|
-
return
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
549
|
+
function approvalIdForFrame(frame) {
|
|
550
|
+
const taskRef = parseTaskId(frame.taskId);
|
|
551
|
+
return taskRef?.kind === "approval" ? taskRef.id : null;
|
|
552
|
+
}
|
|
553
|
+
function entityIdFromFrame(frame) {
|
|
554
|
+
return taskEntityId(frame.taskId);
|
|
555
|
+
}
|
|
556
|
+
function entityTypeFromFrame(frame) {
|
|
557
|
+
return taskKind(frame.taskId);
|
|
558
|
+
}
|
|
559
|
+
function itemHref(frame, companyPrefix) {
|
|
560
|
+
const entityType = entityTypeFromFrame(frame);
|
|
561
|
+
const entityId = entityIdFromFrame(frame);
|
|
562
|
+
if (!entityType || !entityId || !companyPrefix) return null;
|
|
563
|
+
const pluralType = entityType === "run" ? "runs" : entityType === "approval" ? "approvals" : entityType === "issue" ? "issues" : entityType === "agent" ? "agents" : null;
|
|
564
|
+
if (!pluralType) return null;
|
|
565
|
+
return `/${companyPrefix}/${pluralType}/${entityId}`;
|
|
566
|
+
}
|
|
567
|
+
function costsHref(companyPrefix) {
|
|
568
|
+
return companyPrefix ? `/${companyPrefix}/costs` : null;
|
|
569
|
+
}
|
|
570
|
+
function activityHref(frame, companyPrefix) {
|
|
571
|
+
if (!companyPrefix) return null;
|
|
572
|
+
const metadata = readFocusMetadata(frame);
|
|
573
|
+
return metadata.activityPath ? `/${companyPrefix}/${metadata.activityPath}` : null;
|
|
574
|
+
}
|
|
575
|
+
function primaryLinkLabel(frame) {
|
|
576
|
+
const entityType = entityTypeFromFrame(frame);
|
|
577
|
+
switch (entityType) {
|
|
578
|
+
case "approval":
|
|
579
|
+
return "Open approval";
|
|
580
|
+
case "issue":
|
|
581
|
+
return "Open issue";
|
|
582
|
+
case "run":
|
|
583
|
+
return "Open run";
|
|
584
|
+
case "agent":
|
|
585
|
+
return "Open agent";
|
|
586
|
+
default:
|
|
587
|
+
return "Open in Paperclip";
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function responseKind(frame, lane) {
|
|
591
|
+
if (approvalIdForFrame(frame) && (frame.responseSpec?.kind === "approval" || frame.mode === "approval")) {
|
|
592
|
+
return "approval";
|
|
593
|
+
}
|
|
594
|
+
if (lane !== "ambient" && (frame.responseSpec?.kind === "acknowledge" || frame.mode === "status")) {
|
|
595
|
+
return "acknowledge";
|
|
596
|
+
}
|
|
597
|
+
return "none";
|
|
598
|
+
}
|
|
599
|
+
function isFrameUnreadInSnapshot(frame, snapshot) {
|
|
600
|
+
const seenAt = snapshot.review?.lastSeenAt;
|
|
601
|
+
if (!seenAt) return true;
|
|
602
|
+
return frameUpdatedAt(frame, snapshot.updatedAt).localeCompare(seenAt) > 0;
|
|
603
|
+
}
|
|
604
|
+
function compactTitle(frame) {
|
|
605
|
+
return frame.title.trim().length > 0 ? frame.title : "Untitled frame";
|
|
606
|
+
}
|
|
607
|
+
var ENTITY_PATTERN = /\b([A-Z][A-Z0-9]+-\d+)\b|"([^"]+)"/g;
|
|
608
|
+
function HighlightedTitle({ text }) {
|
|
609
|
+
const parts = [];
|
|
610
|
+
let lastIndex = 0;
|
|
611
|
+
let match;
|
|
612
|
+
const re = new RegExp(ENTITY_PATTERN.source, "g");
|
|
613
|
+
while ((match = re.exec(text)) !== null) {
|
|
614
|
+
if (match.index > lastIndex) {
|
|
615
|
+
parts.push(text.slice(lastIndex, match.index));
|
|
616
|
+
}
|
|
617
|
+
const token = match[1] ?? match[2] ?? match[0];
|
|
618
|
+
parts.push(
|
|
619
|
+
/* @__PURE__ */ jsx2(Accent, { children: match[1] ? token : `"${token}"` }, match.index)
|
|
620
|
+
);
|
|
621
|
+
lastIndex = re.lastIndex;
|
|
622
|
+
}
|
|
623
|
+
if (lastIndex < text.length) {
|
|
624
|
+
parts.push(text.slice(lastIndex));
|
|
625
|
+
}
|
|
626
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: parts });
|
|
627
|
+
}
|
|
628
|
+
function renderTitle(frame) {
|
|
629
|
+
return /* @__PURE__ */ jsx2(HighlightedTitle, { text: compactTitle(frame) });
|
|
630
|
+
}
|
|
631
|
+
function visibleContextItems(frame) {
|
|
632
|
+
return (frame.context?.items ?? []).filter((item) => item.id !== ATTENTION_CONTEXT_IDS.recommendedMove);
|
|
633
|
+
}
|
|
634
|
+
function isIssueFrame(frame) {
|
|
635
|
+
return entityTypeFromFrame(frame) === "issue";
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/aperture/explainability.ts
|
|
639
|
+
function readStringArray2(value) {
|
|
640
|
+
if (!Array.isArray(value)) return [];
|
|
641
|
+
return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
642
|
+
}
|
|
643
|
+
function readSemanticConfidence(frame) {
|
|
644
|
+
const confidence = readFocusMetadata(frame).semantic?.confidence;
|
|
645
|
+
return confidence === "low" || confidence === "medium" || confidence === "high" ? confidence : null;
|
|
646
|
+
}
|
|
647
|
+
function readSemanticRelationHints(frame) {
|
|
648
|
+
return readFocusMetadata(frame).semantic?.relationHints ?? [];
|
|
649
|
+
}
|
|
650
|
+
function relationHintLabel(hint) {
|
|
651
|
+
switch (hint.kind) {
|
|
652
|
+
case "same_issue":
|
|
653
|
+
return "Part of the same thread";
|
|
654
|
+
case "resolves":
|
|
655
|
+
return "Resolves an earlier blocker";
|
|
656
|
+
case "supersedes":
|
|
657
|
+
return "Moves the request forward";
|
|
658
|
+
case "repeats":
|
|
659
|
+
return "Repeats an earlier ask";
|
|
660
|
+
case "escalates":
|
|
661
|
+
return "Raises the urgency";
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
function laneReason(lane) {
|
|
665
|
+
switch (lane) {
|
|
666
|
+
case "now":
|
|
667
|
+
return "This is the most urgent item in the queue right now.";
|
|
668
|
+
case "next":
|
|
669
|
+
return "This is staged behind the current top item.";
|
|
670
|
+
case "ambient":
|
|
671
|
+
return "This is visible for awareness without needing action yet.";
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function continuitySummary(frame) {
|
|
675
|
+
const episode = readFocusMetadata(frame).episode;
|
|
676
|
+
const size = episode?.size ?? null;
|
|
677
|
+
const state = episode?.state?.replace(/_/g, " ") ?? null;
|
|
678
|
+
if (!size || size <= 1) return null;
|
|
679
|
+
if (state) return `Part of a ${state} thread with ${size} related interactions.`;
|
|
680
|
+
return `Part of a thread with ${size} related interactions.`;
|
|
681
|
+
}
|
|
682
|
+
function frameSignals(frame) {
|
|
683
|
+
const rationale = readFocusMetadata(frame).attention?.rationale ?? [];
|
|
684
|
+
const factors = readStringArray2(frame.provenance?.factors);
|
|
685
|
+
return [.../* @__PURE__ */ new Set([...rationale.length > 0 ? rationale : factors, ...factors])];
|
|
686
|
+
}
|
|
687
|
+
function signalStrengthLabel(confidence) {
|
|
688
|
+
return `${confidence} confidence`;
|
|
689
|
+
}
|
|
690
|
+
function explainFrame(frame, lane) {
|
|
691
|
+
const relationLabels = [...new Set(readSemanticRelationHints(frame).map(relationHintLabel))];
|
|
692
|
+
return {
|
|
693
|
+
whyNow: frame.provenance?.whyNow ?? frame.summary ?? null,
|
|
694
|
+
laneReason: laneReason(lane),
|
|
695
|
+
signalStrength: readSemanticConfidence(frame),
|
|
696
|
+
signals: frameSignals(frame),
|
|
697
|
+
relationLabels,
|
|
698
|
+
continuity: continuitySummary(frame)
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/ui/frame-explainability.tsx
|
|
703
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
704
|
+
function ContextItems({ frame }) {
|
|
705
|
+
const items = visibleContextItems(frame);
|
|
706
|
+
if (items.length === 0) return null;
|
|
707
|
+
return /* @__PURE__ */ jsx3("div", { className: "grid gap-2 md:grid-cols-2", children: items.map((item) => /* @__PURE__ */ jsxs2(
|
|
708
|
+
"div",
|
|
709
|
+
{
|
|
710
|
+
className: cn(
|
|
711
|
+
"border border-border bg-secondary/50 px-3 py-2",
|
|
712
|
+
item.id === ATTENTION_CONTEXT_IDS.latestComment && "md:col-span-2"
|
|
713
|
+
),
|
|
714
|
+
children: [
|
|
715
|
+
/* @__PURE__ */ jsx3("div", { className: "text-xs font-medium text-muted-foreground", children: item.label }),
|
|
716
|
+
/* @__PURE__ */ jsx3("div", { className: "mt-1 text-sm text-foreground", children: item.value ?? "Available" })
|
|
717
|
+
]
|
|
718
|
+
},
|
|
719
|
+
item.id
|
|
720
|
+
)) });
|
|
721
|
+
}
|
|
722
|
+
function ExplainabilityBadges(props) {
|
|
723
|
+
if (props.values.length === 0) return null;
|
|
724
|
+
return /* @__PURE__ */ jsx3("div", { className: "flex flex-wrap items-center gap-2", children: props.values.map((value) => /* @__PURE__ */ jsx3(
|
|
725
|
+
Badge,
|
|
726
|
+
{
|
|
727
|
+
className: props.accent ? "border-transparent" : "border-border bg-background/70 text-foreground/80",
|
|
728
|
+
style: props.accent ? { color: ACCENT_COLOR, backgroundColor: ACCENT_BG, borderColor: ACCENT_BORDER } : void 0,
|
|
729
|
+
children: value
|
|
730
|
+
},
|
|
731
|
+
value
|
|
732
|
+
)) });
|
|
733
|
+
}
|
|
734
|
+
function InlineExplainability(props) {
|
|
735
|
+
const explanation = explainFrame(props.frame, props.lane);
|
|
736
|
+
const chips = [
|
|
737
|
+
...explanation.signalStrength ? [signalStrengthLabel(explanation.signalStrength)] : [],
|
|
738
|
+
...explanation.signals.slice(0, 2),
|
|
739
|
+
...explanation.relationLabels.slice(0, 1)
|
|
740
|
+
];
|
|
741
|
+
const label = props.lane === "next" ? "Why next" : props.lane === "ambient" ? "Why ambient" : "Why now";
|
|
742
|
+
const whyNow = explanation.whyNow ?? judgmentLine(props.frame, props.lane);
|
|
743
|
+
const primaryLine = props.lane === "now" ? props.preferLaneReason ? explanation.laneReason : whyNow : whyNow;
|
|
744
|
+
const secondaryLine = props.lane === "now" ? !props.preferLaneReason && whyNow !== explanation.laneReason ? explanation.laneReason : null : explanation.laneReason;
|
|
745
|
+
return /* @__PURE__ */ jsxs2("div", { className: "space-y-2 border-l-2 pl-4", style: { borderColor: ACCENT_COLOR }, children: [
|
|
746
|
+
/* @__PURE__ */ jsx3("div", { className: "text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: label }),
|
|
747
|
+
/* @__PURE__ */ jsx3("p", { className: "max-w-3xl text-sm leading-relaxed text-foreground/90", children: primaryLine }),
|
|
748
|
+
secondaryLine ? /* @__PURE__ */ jsx3("p", { className: "text-xs leading-relaxed text-muted-foreground", children: secondaryLine }) : null,
|
|
749
|
+
chips.length > 0 ? /* @__PURE__ */ jsx3(ExplainabilityBadges, { values: chips, accent: true }) : null
|
|
750
|
+
] });
|
|
751
|
+
}
|
|
752
|
+
function ExplainabilityPanel(props) {
|
|
753
|
+
const explanation = explainFrame(props.frame, props.lane);
|
|
754
|
+
const strength = !props.detailOnly && explanation.signalStrength ? signalStrengthLabel(explanation.signalStrength) : null;
|
|
755
|
+
const signalValues = props.detailOnly ? explanation.signals.slice(2) : explanation.signals;
|
|
756
|
+
const relationValues = props.detailOnly ? explanation.relationLabels.slice(1) : explanation.relationLabels;
|
|
757
|
+
const reasoningLabel = props.lane === "now" ? "Reasoning" : props.lane === "next" ? "Why it sits here" : "Why it stays quiet";
|
|
758
|
+
if (props.detailOnly && signalValues.length === 0 && relationValues.length === 0 && !explanation.continuity) {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
return /* @__PURE__ */ jsxs2("div", { className: "space-y-4", children: [
|
|
762
|
+
!props.detailOnly ? /* @__PURE__ */ jsxs2("div", { className: "grid gap-4 lg:grid-cols-[minmax(0,1fr)_16rem]", children: [
|
|
763
|
+
/* @__PURE__ */ jsxs2("div", { children: [
|
|
764
|
+
/* @__PURE__ */ jsx3("div", { className: "text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: reasoningLabel }),
|
|
765
|
+
/* @__PURE__ */ jsx3("p", { className: "mt-1 text-sm leading-relaxed text-muted-foreground", children: explanation.laneReason })
|
|
766
|
+
] }),
|
|
767
|
+
strength ? /* @__PURE__ */ jsxs2("div", { children: [
|
|
768
|
+
/* @__PURE__ */ jsx3("div", { className: "text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: "Confidence" }),
|
|
769
|
+
/* @__PURE__ */ jsx3("p", { className: "mt-1 text-sm text-foreground/90", children: strength })
|
|
770
|
+
] }) : null
|
|
771
|
+
] }) : null,
|
|
772
|
+
signalValues.length > 0 ? /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
|
|
773
|
+
/* @__PURE__ */ jsx3("div", { className: "text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: props.detailOnly ? "More signals" : "Signals" }),
|
|
774
|
+
/* @__PURE__ */ jsx3(ExplainabilityBadges, { values: signalValues })
|
|
775
|
+
] }) : null,
|
|
776
|
+
relationValues.length > 0 ? /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
|
|
777
|
+
/* @__PURE__ */ jsx3("div", { className: "text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: props.detailOnly ? "More thread context" : "Thread context" }),
|
|
778
|
+
/* @__PURE__ */ jsx3(ExplainabilityBadges, { values: relationValues, accent: true })
|
|
779
|
+
] }) : null,
|
|
780
|
+
explanation.continuity ? /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
|
|
781
|
+
/* @__PURE__ */ jsx3("div", { className: "text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: "Related activity" }),
|
|
782
|
+
/* @__PURE__ */ jsx3("p", { className: "text-sm leading-relaxed text-muted-foreground", children: explanation.continuity })
|
|
783
|
+
] }) : null
|
|
784
|
+
] });
|
|
785
|
+
}
|
|
786
|
+
function ExplainabilityStrip(props) {
|
|
787
|
+
return /* @__PURE__ */ jsx3("div", { className: "space-y-2 border-t border-border/60 pt-3", children: /* @__PURE__ */ jsx3(InlineExplainability, { frame: props.frame, lane: props.lane }) });
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// src/ui/issue-comment-composer.tsx
|
|
791
|
+
import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
|
|
792
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
793
|
+
function IssueCommentComposer(props) {
|
|
794
|
+
const [open, setOpen] = useState3(false);
|
|
795
|
+
const [body, setBody] = useState3("");
|
|
796
|
+
const textareaRef = useRef3(null);
|
|
797
|
+
const issueFrame = isIssueFrame(props.frame);
|
|
798
|
+
const isPending = props.pendingId === props.frame.id;
|
|
799
|
+
useEffect3(() => {
|
|
800
|
+
setOpen(false);
|
|
801
|
+
setBody("");
|
|
802
|
+
}, [props.frame.id]);
|
|
803
|
+
useEffect3(() => {
|
|
804
|
+
if (open && issueFrame) textareaRef.current?.focus();
|
|
805
|
+
}, [open, issueFrame]);
|
|
806
|
+
if (!issueFrame) return null;
|
|
807
|
+
async function submit() {
|
|
808
|
+
const nextBody = body.trim();
|
|
809
|
+
if (!nextBody) return;
|
|
810
|
+
await props.onComment(props.frame, nextBody);
|
|
811
|
+
setBody("");
|
|
812
|
+
setOpen(false);
|
|
813
|
+
}
|
|
814
|
+
if (!open) {
|
|
815
|
+
const label = props.triggerLabel ?? "Leave comment";
|
|
816
|
+
const triggerTone = props.triggerTone ?? "link";
|
|
817
|
+
if (triggerTone === "link") {
|
|
818
|
+
return /* @__PURE__ */ jsx4(
|
|
819
|
+
"button",
|
|
820
|
+
{
|
|
821
|
+
type: "button",
|
|
822
|
+
className: "text-xs font-medium text-muted-foreground underline underline-offset-2 hover:text-foreground",
|
|
823
|
+
onClick: () => setOpen(true),
|
|
824
|
+
children: label
|
|
825
|
+
}
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
return /* @__PURE__ */ jsx4(
|
|
829
|
+
ActionButton,
|
|
830
|
+
{
|
|
831
|
+
label,
|
|
832
|
+
tone: triggerTone,
|
|
833
|
+
className: props.triggerFullWidth ? "w-full" : void 0,
|
|
834
|
+
disabled: isPending,
|
|
835
|
+
onClick: () => setOpen(true)
|
|
836
|
+
}
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
return /* @__PURE__ */ jsxs3("div", { className: "w-full space-y-2 rounded-md border border-border bg-secondary/40 p-3", children: [
|
|
840
|
+
/* @__PURE__ */ jsxs3("div", { className: "text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: [
|
|
841
|
+
"Commenting on ",
|
|
842
|
+
compactTitle(props.frame)
|
|
843
|
+
] }),
|
|
844
|
+
/* @__PURE__ */ jsx4("div", { className: "text-[11px] text-muted-foreground", children: "Posts to the issue thread without leaving Focus." }),
|
|
845
|
+
/* @__PURE__ */ jsx4(
|
|
846
|
+
"textarea",
|
|
847
|
+
{
|
|
848
|
+
ref: textareaRef,
|
|
849
|
+
value: body,
|
|
850
|
+
onChange: (event) => setBody(event.target.value),
|
|
851
|
+
onKeyDown: (event) => {
|
|
852
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
853
|
+
event.preventDefault();
|
|
854
|
+
void submit();
|
|
855
|
+
}
|
|
856
|
+
if (event.key === "Escape") {
|
|
857
|
+
event.preventDefault();
|
|
858
|
+
setOpen(false);
|
|
859
|
+
setBody("");
|
|
860
|
+
}
|
|
861
|
+
},
|
|
862
|
+
rows: 3,
|
|
863
|
+
placeholder: "Add a short operator note back to the issue\u2026",
|
|
864
|
+
className: "w-full resize-y rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground shadow-xs outline-none transition focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]"
|
|
865
|
+
}
|
|
866
|
+
),
|
|
867
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-end gap-2", children: [
|
|
868
|
+
/* @__PURE__ */ jsx4(
|
|
869
|
+
ActionButton,
|
|
870
|
+
{
|
|
871
|
+
label: "Cancel",
|
|
872
|
+
tone: "secondary",
|
|
873
|
+
disabled: isPending,
|
|
874
|
+
onClick: () => {
|
|
875
|
+
setOpen(false);
|
|
876
|
+
setBody("");
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
),
|
|
880
|
+
/* @__PURE__ */ jsx4(
|
|
881
|
+
ActionButton,
|
|
882
|
+
{
|
|
883
|
+
label: isPending ? "Posting\u2026" : "Post comment",
|
|
884
|
+
tone: "accent",
|
|
885
|
+
disabled: isPending || body.trim().length === 0,
|
|
886
|
+
onClick: () => void submit()
|
|
887
|
+
}
|
|
888
|
+
)
|
|
889
|
+
] })
|
|
890
|
+
] });
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// src/ui/use-focus-page-actions.ts
|
|
894
|
+
import { usePluginAction } from "@paperclipai/plugin-sdk/ui";
|
|
895
|
+
import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
|
|
896
|
+
|
|
897
|
+
// src/ui/interaction-copy.ts
|
|
898
|
+
function actionErrorMessage(error, fallback) {
|
|
899
|
+
return error instanceof Error ? error.message : fallback;
|
|
900
|
+
}
|
|
901
|
+
function acknowledgeSuccessMessage(title) {
|
|
902
|
+
return `Acknowledged ${title}.`;
|
|
903
|
+
}
|
|
904
|
+
function acknowledgeFailureMessage(error) {
|
|
905
|
+
return actionErrorMessage(error, "Failed to acknowledge frame.");
|
|
906
|
+
}
|
|
907
|
+
function issueFrameUnsupportedMessage() {
|
|
908
|
+
return "This frame is not backed by a Paperclip issue.";
|
|
909
|
+
}
|
|
910
|
+
function commentSuccessMessage(title) {
|
|
911
|
+
return `Posted a comment on ${title}.`;
|
|
912
|
+
}
|
|
913
|
+
function commentFailureMessage(error) {
|
|
914
|
+
return actionErrorMessage(error, "Failed to post comment.");
|
|
915
|
+
}
|
|
916
|
+
function approvalFrameUnsupportedMessage() {
|
|
917
|
+
return "This frame is not backed by a Paperclip approval.";
|
|
918
|
+
}
|
|
919
|
+
function approvalDecisionSuccessMessage(title, decision) {
|
|
920
|
+
return `${decision === "approve" ? "Approved" : "Rejected"} ${title}.`;
|
|
921
|
+
}
|
|
922
|
+
function approvalDecisionFailureMessage(error) {
|
|
923
|
+
return actionErrorMessage(error, "Failed to submit approval decision.");
|
|
924
|
+
}
|
|
925
|
+
function approvalRevisionSuccessMessage(title) {
|
|
926
|
+
return `Requested revision for ${title}.`;
|
|
927
|
+
}
|
|
928
|
+
function approvalRevisionFailureMessage(error) {
|
|
929
|
+
return actionErrorMessage(error, "Failed to request approval revision.");
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/ui/use-focus-page-actions.ts
|
|
933
|
+
var FOCUS_HOLD_CONTEXT_MS = 2e4;
|
|
934
|
+
var FOCUS_HOLD_COMPOSE_MS = 45e3;
|
|
935
|
+
function useFocusPageActions(input) {
|
|
936
|
+
const acknowledge = usePluginAction("acknowledge-frame");
|
|
937
|
+
const commentOnIssue = usePluginAction("comment-on-issue");
|
|
938
|
+
const engageFocus = usePluginAction("engage-focus");
|
|
939
|
+
const markAttentionViewed = usePluginAction("mark-attention-viewed");
|
|
940
|
+
const recordApprovalResponse = usePluginAction("record-approval-response");
|
|
941
|
+
const setFocusPresence = usePluginAction("set-focus-presence");
|
|
942
|
+
const [pendingId, setPendingId] = useState4(null);
|
|
943
|
+
const [statusOverride, setStatusOverride] = useState4(null);
|
|
944
|
+
const focusReleaseTimerRef = useRef4(null);
|
|
945
|
+
const lastViewedInteractionRef = useRef4(null);
|
|
946
|
+
const markAttentionViewedRef = useRef4(markAttentionViewed);
|
|
947
|
+
const setFocusPresenceRef = useRef4(setFocusPresence);
|
|
948
|
+
markAttentionViewedRef.current = markAttentionViewed;
|
|
949
|
+
setFocusPresenceRef.current = setFocusPresence;
|
|
950
|
+
useEffect4(() => {
|
|
951
|
+
if (!statusOverride) return;
|
|
952
|
+
const timer = window.setTimeout(() => {
|
|
953
|
+
setStatusOverride(null);
|
|
954
|
+
}, 4e3);
|
|
955
|
+
return () => {
|
|
956
|
+
window.clearTimeout(timer);
|
|
957
|
+
};
|
|
958
|
+
}, [statusOverride]);
|
|
959
|
+
useEffect4(() => {
|
|
960
|
+
return () => {
|
|
961
|
+
if (focusReleaseTimerRef.current) {
|
|
962
|
+
window.clearTimeout(focusReleaseTimerRef.current);
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
}, []);
|
|
966
|
+
useEffect4(() => {
|
|
967
|
+
const companyId = input.companyId;
|
|
968
|
+
if (!companyId) return;
|
|
969
|
+
void setFocusPresenceRef.current({ companyId, presence: "present" }).catch(() => {
|
|
970
|
+
});
|
|
971
|
+
return () => {
|
|
972
|
+
void setFocusPresenceRef.current({ companyId, presence: "absent" }).catch(() => {
|
|
973
|
+
});
|
|
974
|
+
};
|
|
975
|
+
}, [input.companyId]);
|
|
976
|
+
useEffect4(() => {
|
|
977
|
+
const companyId = input.companyId;
|
|
978
|
+
const frame = input.displaySnapshot?.now;
|
|
979
|
+
if (!companyId || !frame) {
|
|
980
|
+
lastViewedInteractionRef.current = null;
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const viewKey = `${companyId}:${frame.interactionId}`;
|
|
984
|
+
if (lastViewedInteractionRef.current === viewKey) return;
|
|
985
|
+
lastViewedInteractionRef.current = viewKey;
|
|
986
|
+
void markAttentionViewedRef.current({
|
|
987
|
+
companyId,
|
|
988
|
+
taskId: frame.taskId,
|
|
989
|
+
interactionId: frame.interactionId,
|
|
990
|
+
surface: "focus"
|
|
991
|
+
}).catch(() => {
|
|
992
|
+
if (lastViewedInteractionRef.current === viewKey) {
|
|
993
|
+
lastViewedInteractionRef.current = null;
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
}, [input.companyId, input.displaySnapshot?.now?.interactionId]);
|
|
997
|
+
async function holdNowFrame(frame, reason) {
|
|
998
|
+
const companyId = input.companyId;
|
|
999
|
+
if (!companyId) return;
|
|
1000
|
+
if (input.displaySnapshot?.now?.interactionId !== frame.interactionId) return;
|
|
1001
|
+
const durationMs = reason === "comment_compose" ? FOCUS_HOLD_COMPOSE_MS : FOCUS_HOLD_CONTEXT_MS;
|
|
1002
|
+
try {
|
|
1003
|
+
await engageFocus({
|
|
1004
|
+
companyId,
|
|
1005
|
+
taskId: frame.taskId,
|
|
1006
|
+
interactionId: frame.interactionId,
|
|
1007
|
+
reason,
|
|
1008
|
+
durationMs
|
|
1009
|
+
});
|
|
1010
|
+
input.refresh();
|
|
1011
|
+
if (focusReleaseTimerRef.current) {
|
|
1012
|
+
window.clearTimeout(focusReleaseTimerRef.current);
|
|
1013
|
+
}
|
|
1014
|
+
focusReleaseTimerRef.current = window.setTimeout(() => {
|
|
1015
|
+
input.refresh();
|
|
1016
|
+
focusReleaseTimerRef.current = null;
|
|
1017
|
+
}, durationMs + 150);
|
|
1018
|
+
} catch {
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
async function acknowledgeFrame(frame) {
|
|
1022
|
+
const companyId = input.companyId;
|
|
1023
|
+
if (!companyId) return;
|
|
1024
|
+
setPendingId(frame.id);
|
|
1025
|
+
try {
|
|
1026
|
+
await acknowledge({ companyId, taskId: frame.taskId, interactionId: frame.interactionId });
|
|
1027
|
+
setStatusOverride(acknowledgeSuccessMessage(compactTitle(frame)));
|
|
1028
|
+
input.refresh();
|
|
1029
|
+
} catch (actionError) {
|
|
1030
|
+
setStatusOverride(acknowledgeFailureMessage(actionError));
|
|
1031
|
+
} finally {
|
|
1032
|
+
setPendingId(null);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
async function commentOnIssueFrame(frame, body) {
|
|
1036
|
+
const companyId = input.companyId;
|
|
1037
|
+
const issueId = entityIdFromFrame(frame);
|
|
1038
|
+
if (!companyId || !issueId || entityTypeFromFrame(frame) !== "issue") {
|
|
1039
|
+
setStatusOverride(issueFrameUnsupportedMessage());
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
setPendingId(frame.id);
|
|
1043
|
+
try {
|
|
1044
|
+
await commentOnIssue({ companyId, taskId: frame.taskId, issueId, body });
|
|
1045
|
+
setStatusOverride(commentSuccessMessage(compactTitle(frame)));
|
|
1046
|
+
input.refresh();
|
|
1047
|
+
} catch (actionError) {
|
|
1048
|
+
setStatusOverride(commentFailureMessage(actionError));
|
|
1049
|
+
} finally {
|
|
1050
|
+
setPendingId(null);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
async function submitApprovalDecision(frame, decision) {
|
|
1054
|
+
const companyId = input.companyId;
|
|
1055
|
+
const approvalId = approvalIdForFrame(frame);
|
|
1056
|
+
if (!companyId || !approvalId) {
|
|
1057
|
+
setStatusOverride(approvalFrameUnsupportedMessage());
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
setPendingId(frame.id);
|
|
1061
|
+
try {
|
|
1062
|
+
await recordApprovalResponse({
|
|
1063
|
+
companyId,
|
|
1064
|
+
taskId: frame.taskId,
|
|
1065
|
+
interactionId: frame.interactionId,
|
|
1066
|
+
decision
|
|
1067
|
+
});
|
|
1068
|
+
setStatusOverride(approvalDecisionSuccessMessage(compactTitle(frame), decision));
|
|
1069
|
+
input.refresh();
|
|
1070
|
+
} catch (actionError) {
|
|
1071
|
+
setStatusOverride(approvalDecisionFailureMessage(actionError));
|
|
1072
|
+
} finally {
|
|
1073
|
+
setPendingId(null);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
async function requestApprovalRevision(frame) {
|
|
1077
|
+
const companyId = input.companyId;
|
|
1078
|
+
const approvalId = approvalIdForFrame(frame);
|
|
1079
|
+
if (!companyId || !approvalId) {
|
|
1080
|
+
setStatusOverride(approvalFrameUnsupportedMessage());
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
setPendingId(frame.id);
|
|
1084
|
+
try {
|
|
1085
|
+
await recordApprovalResponse({
|
|
1086
|
+
companyId,
|
|
1087
|
+
taskId: frame.taskId,
|
|
1088
|
+
interactionId: frame.interactionId,
|
|
1089
|
+
decision: "request-revision"
|
|
1090
|
+
});
|
|
1091
|
+
setStatusOverride(approvalRevisionSuccessMessage(compactTitle(frame)));
|
|
1092
|
+
input.refresh();
|
|
1093
|
+
} catch (actionError) {
|
|
1094
|
+
setStatusOverride(approvalRevisionFailureMessage(actionError));
|
|
1095
|
+
} finally {
|
|
1096
|
+
setPendingId(null);
|
|
1012
1097
|
}
|
|
1013
|
-
|
|
1098
|
+
}
|
|
1099
|
+
return {
|
|
1100
|
+
pendingId,
|
|
1101
|
+
statusOverride,
|
|
1102
|
+
holdNowFrame,
|
|
1103
|
+
acknowledgeFrame,
|
|
1104
|
+
commentOnIssueFrame,
|
|
1105
|
+
submitApprovalDecision,
|
|
1106
|
+
requestApprovalRevision
|
|
1107
|
+
};
|
|
1014
1108
|
}
|
|
1109
|
+
|
|
1110
|
+
// src/ui/index.tsx
|
|
1111
|
+
import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1015
1112
|
function FrameActions(props) {
|
|
1016
1113
|
const actionMode = responseKind(props.frame, props.lane);
|
|
1017
1114
|
const isPending = props.pendingId === props.frame.id;
|
|
1018
1115
|
if (actionMode === "none") return null;
|
|
1019
|
-
return /* @__PURE__ */
|
|
1020
|
-
isBudgetOverride(props.frame) ? /* @__PURE__ */
|
|
1116
|
+
return /* @__PURE__ */ jsx5("div", { className: cn("flex items-center gap-2", props.compact && "justify-end"), children: actionMode === "approval" ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
1117
|
+
isBudgetOverride(props.frame) ? /* @__PURE__ */ jsx5(
|
|
1021
1118
|
ActionButton,
|
|
1022
1119
|
{
|
|
1023
1120
|
label: "Request revision",
|
|
@@ -1026,7 +1123,7 @@ function FrameActions(props) {
|
|
|
1026
1123
|
onClick: () => void props.onRequestRevision(props.frame)
|
|
1027
1124
|
}
|
|
1028
1125
|
) : null,
|
|
1029
|
-
/* @__PURE__ */
|
|
1126
|
+
/* @__PURE__ */ jsx5(
|
|
1030
1127
|
ActionButton,
|
|
1031
1128
|
{
|
|
1032
1129
|
label: isPending ? "Submitting\u2026" : "Approve",
|
|
@@ -1035,7 +1132,7 @@ function FrameActions(props) {
|
|
|
1035
1132
|
onClick: () => void props.onApprove(props.frame)
|
|
1036
1133
|
}
|
|
1037
1134
|
),
|
|
1038
|
-
/* @__PURE__ */
|
|
1135
|
+
/* @__PURE__ */ jsx5(
|
|
1039
1136
|
ActionButton,
|
|
1040
1137
|
{
|
|
1041
1138
|
label: "Reject",
|
|
@@ -1044,7 +1141,7 @@ function FrameActions(props) {
|
|
|
1044
1141
|
onClick: () => void props.onReject(props.frame)
|
|
1045
1142
|
}
|
|
1046
1143
|
)
|
|
1047
|
-
] }) : /* @__PURE__ */
|
|
1144
|
+
] }) : /* @__PURE__ */ jsx5(
|
|
1048
1145
|
ActionButton,
|
|
1049
1146
|
{
|
|
1050
1147
|
label: isPending ? "Saving\u2026" : "Acknowledge",
|
|
@@ -1054,216 +1151,30 @@ function FrameActions(props) {
|
|
|
1054
1151
|
}
|
|
1055
1152
|
) });
|
|
1056
1153
|
}
|
|
1057
|
-
function visibleContextItems(frame) {
|
|
1058
|
-
return (frame.context?.items ?? []).filter((item2) => item2.id !== ATTENTION_CONTEXT_IDS.recommendedMove);
|
|
1059
|
-
}
|
|
1060
|
-
function ContextItems({ frame }) {
|
|
1061
|
-
const items = visibleContextItems(frame);
|
|
1062
|
-
if (items.length === 0) return null;
|
|
1063
|
-
return /* @__PURE__ */ jsx("div", { className: "grid gap-2 md:grid-cols-2", children: items.map((item2) => /* @__PURE__ */ jsxs(
|
|
1064
|
-
"div",
|
|
1065
|
-
{
|
|
1066
|
-
className: cn(
|
|
1067
|
-
"border border-border bg-secondary/50 px-3 py-2",
|
|
1068
|
-
item2.id === ATTENTION_CONTEXT_IDS.latestComment && "md:col-span-2"
|
|
1069
|
-
),
|
|
1070
|
-
children: [
|
|
1071
|
-
/* @__PURE__ */ jsx("div", { className: "text-xs font-medium text-muted-foreground", children: item2.label }),
|
|
1072
|
-
/* @__PURE__ */ jsx("div", { className: "mt-1 text-sm text-foreground", children: item2.value ?? "Available" })
|
|
1073
|
-
]
|
|
1074
|
-
},
|
|
1075
|
-
item2.id
|
|
1076
|
-
)) });
|
|
1077
|
-
}
|
|
1078
|
-
function ExplainabilityBadges(props) {
|
|
1079
|
-
if (props.values.length === 0) return null;
|
|
1080
|
-
return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2", children: props.values.map((value) => /* @__PURE__ */ jsx(
|
|
1081
|
-
Badge,
|
|
1082
|
-
{
|
|
1083
|
-
className: props.accent ? "border-transparent" : "border-border bg-background/70 text-foreground/80",
|
|
1084
|
-
style: props.accent ? { color: ACCENT_COLOR, backgroundColor: ACCENT_BG, borderColor: ACCENT_BORDER } : void 0,
|
|
1085
|
-
children: value
|
|
1086
|
-
},
|
|
1087
|
-
value
|
|
1088
|
-
)) });
|
|
1089
|
-
}
|
|
1090
|
-
function InlineExplainability(props) {
|
|
1091
|
-
const explanation = explainFrame(props.frame, props.lane);
|
|
1092
|
-
const chips = [
|
|
1093
|
-
...explanation.signalStrength ? [signalStrengthLabel(explanation.signalStrength)] : [],
|
|
1094
|
-
...explanation.signals.slice(0, 2),
|
|
1095
|
-
...explanation.relationLabels.slice(0, 1)
|
|
1096
|
-
];
|
|
1097
|
-
const label = props.lane === "next" ? "Why next" : props.lane === "ambient" ? "Why ambient" : "Why now";
|
|
1098
|
-
const whyNow = explanation.whyNow ?? judgmentLine(props.frame, props.lane);
|
|
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;
|
|
1101
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-2 border-l-2 pl-4", style: { borderColor: ACCENT_COLOR }, children: [
|
|
1102
|
-
/* @__PURE__ */ jsx("div", { className: "text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: label }),
|
|
1103
|
-
/* @__PURE__ */ jsx("p", { className: "max-w-3xl text-sm leading-relaxed text-foreground/90", children: primaryLine }),
|
|
1104
|
-
secondaryLine ? /* @__PURE__ */ jsx("p", { className: "text-xs leading-relaxed text-muted-foreground", children: secondaryLine }) : null,
|
|
1105
|
-
chips.length > 0 ? /* @__PURE__ */ jsx(ExplainabilityBadges, { values: chips, accent: true }) : null
|
|
1106
|
-
] });
|
|
1107
|
-
}
|
|
1108
|
-
function ExplainabilityPanel(props) {
|
|
1109
|
-
const explanation = explainFrame(props.frame, props.lane);
|
|
1110
|
-
const strength = !props.detailOnly && explanation.signalStrength ? signalStrengthLabel(explanation.signalStrength) : null;
|
|
1111
|
-
const signalValues = props.detailOnly ? explanation.signals.slice(2) : explanation.signals;
|
|
1112
|
-
const relationValues = props.detailOnly ? explanation.relationLabels.slice(1) : explanation.relationLabels;
|
|
1113
|
-
const reasoningLabel = props.lane === "now" ? "Reasoning" : props.lane === "next" ? "Why it sits here" : "Why it stays quiet";
|
|
1114
|
-
if (props.detailOnly && signalValues.length === 0 && relationValues.length === 0 && !explanation.continuity) {
|
|
1115
|
-
return null;
|
|
1116
|
-
}
|
|
1117
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
1118
|
-
!props.detailOnly ? /* @__PURE__ */ jsxs("div", { className: "grid gap-4 lg:grid-cols-[minmax(0,1fr)_16rem]", children: [
|
|
1119
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
1120
|
-
/* @__PURE__ */ jsx("div", { className: "text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: reasoningLabel }),
|
|
1121
|
-
/* @__PURE__ */ jsx("p", { className: "mt-1 text-sm leading-relaxed text-muted-foreground", children: explanation.laneReason })
|
|
1122
|
-
] }),
|
|
1123
|
-
strength ? /* @__PURE__ */ jsxs("div", { children: [
|
|
1124
|
-
/* @__PURE__ */ jsx("div", { className: "text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: "Confidence" }),
|
|
1125
|
-
/* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-foreground/90", children: strength })
|
|
1126
|
-
] }) : null
|
|
1127
|
-
] }) : null,
|
|
1128
|
-
signalValues.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
1129
|
-
/* @__PURE__ */ jsx("div", { className: "text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: props.detailOnly ? "More signals" : "Signals" }),
|
|
1130
|
-
/* @__PURE__ */ jsx(ExplainabilityBadges, { values: signalValues })
|
|
1131
|
-
] }) : null,
|
|
1132
|
-
relationValues.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
1133
|
-
/* @__PURE__ */ jsx("div", { className: "text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: props.detailOnly ? "More thread context" : "Thread context" }),
|
|
1134
|
-
/* @__PURE__ */ jsx(ExplainabilityBadges, { values: relationValues, accent: true })
|
|
1135
|
-
] }) : null,
|
|
1136
|
-
explanation.continuity ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
1137
|
-
/* @__PURE__ */ jsx("div", { className: "text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: "Related activity" }),
|
|
1138
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm leading-relaxed text-muted-foreground", children: explanation.continuity })
|
|
1139
|
-
] }) : null
|
|
1140
|
-
] });
|
|
1141
|
-
}
|
|
1142
|
-
function ExplainabilityStrip(props) {
|
|
1143
|
-
return /* @__PURE__ */ jsx("div", { className: "space-y-2 border-t border-border/60 pt-3", children: /* @__PURE__ */ jsx(InlineExplainability, { frame: props.frame, lane: props.lane }) });
|
|
1144
|
-
}
|
|
1145
|
-
function isIssueFrame(frame) {
|
|
1146
|
-
return entityTypeFromFrame(frame) === "issue";
|
|
1147
|
-
}
|
|
1148
|
-
function IssueCommentComposer(props) {
|
|
1149
|
-
const [open, setOpen] = useState(false);
|
|
1150
|
-
const [body, setBody] = useState("");
|
|
1151
|
-
const textareaRef = useRef(null);
|
|
1152
|
-
const issueFrame = isIssueFrame(props.frame);
|
|
1153
|
-
const isPending = props.pendingId === props.frame.id;
|
|
1154
|
-
useEffect(() => {
|
|
1155
|
-
setOpen(false);
|
|
1156
|
-
setBody("");
|
|
1157
|
-
}, [props.frame.id]);
|
|
1158
|
-
useEffect(() => {
|
|
1159
|
-
if (open && issueFrame) textareaRef.current?.focus();
|
|
1160
|
-
}, [open, issueFrame]);
|
|
1161
|
-
if (!issueFrame) return null;
|
|
1162
|
-
async function submit() {
|
|
1163
|
-
const nextBody = body.trim();
|
|
1164
|
-
if (!nextBody) return;
|
|
1165
|
-
await props.onComment(props.frame, nextBody);
|
|
1166
|
-
setBody("");
|
|
1167
|
-
setOpen(false);
|
|
1168
|
-
}
|
|
1169
|
-
if (!open) {
|
|
1170
|
-
const label = props.triggerLabel ?? "Leave comment";
|
|
1171
|
-
const triggerTone = props.triggerTone ?? "link";
|
|
1172
|
-
if (triggerTone === "link") {
|
|
1173
|
-
return /* @__PURE__ */ jsx(
|
|
1174
|
-
"button",
|
|
1175
|
-
{
|
|
1176
|
-
type: "button",
|
|
1177
|
-
className: "text-xs font-medium text-muted-foreground underline underline-offset-2 hover:text-foreground",
|
|
1178
|
-
onClick: () => setOpen(true),
|
|
1179
|
-
children: label
|
|
1180
|
-
}
|
|
1181
|
-
);
|
|
1182
|
-
}
|
|
1183
|
-
return /* @__PURE__ */ jsx(
|
|
1184
|
-
ActionButton,
|
|
1185
|
-
{
|
|
1186
|
-
label,
|
|
1187
|
-
tone: triggerTone,
|
|
1188
|
-
className: props.triggerFullWidth ? "w-full" : void 0,
|
|
1189
|
-
disabled: isPending,
|
|
1190
|
-
onClick: () => setOpen(true)
|
|
1191
|
-
}
|
|
1192
|
-
);
|
|
1193
|
-
}
|
|
1194
|
-
return /* @__PURE__ */ jsxs("div", { className: "w-full space-y-2 rounded-md border border-border bg-secondary/40 p-3", children: [
|
|
1195
|
-
/* @__PURE__ */ jsx("div", { className: "text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: "Posts to the issue thread without leaving Focus." }),
|
|
1196
|
-
/* @__PURE__ */ jsx(
|
|
1197
|
-
"textarea",
|
|
1198
|
-
{
|
|
1199
|
-
ref: textareaRef,
|
|
1200
|
-
value: body,
|
|
1201
|
-
onChange: (event) => setBody(event.target.value),
|
|
1202
|
-
onKeyDown: (event) => {
|
|
1203
|
-
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
1204
|
-
event.preventDefault();
|
|
1205
|
-
void submit();
|
|
1206
|
-
}
|
|
1207
|
-
if (event.key === "Escape") {
|
|
1208
|
-
event.preventDefault();
|
|
1209
|
-
setOpen(false);
|
|
1210
|
-
setBody("");
|
|
1211
|
-
}
|
|
1212
|
-
},
|
|
1213
|
-
rows: 3,
|
|
1214
|
-
placeholder: "Add a short operator note back to the issue\u2026",
|
|
1215
|
-
className: "w-full resize-y rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground shadow-xs outline-none transition focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]"
|
|
1216
|
-
}
|
|
1217
|
-
),
|
|
1218
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2", children: [
|
|
1219
|
-
/* @__PURE__ */ jsx(
|
|
1220
|
-
ActionButton,
|
|
1221
|
-
{
|
|
1222
|
-
label: "Cancel",
|
|
1223
|
-
tone: "secondary",
|
|
1224
|
-
disabled: isPending,
|
|
1225
|
-
onClick: () => {
|
|
1226
|
-
setOpen(false);
|
|
1227
|
-
setBody("");
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
),
|
|
1231
|
-
/* @__PURE__ */ jsx(
|
|
1232
|
-
ActionButton,
|
|
1233
|
-
{
|
|
1234
|
-
label: isPending ? "Posting\u2026" : "Post comment",
|
|
1235
|
-
tone: "accent",
|
|
1236
|
-
disabled: isPending || body.trim().length === 0,
|
|
1237
|
-
onClick: () => void submit()
|
|
1238
|
-
}
|
|
1239
|
-
)
|
|
1240
|
-
] })
|
|
1241
|
-
] });
|
|
1242
|
-
}
|
|
1243
1154
|
function NowDetails(props) {
|
|
1244
1155
|
const { frame, snapshotUpdatedAt } = props;
|
|
1245
1156
|
const href = itemHref(frame, props.companyPrefix);
|
|
1246
1157
|
const budgetHref = isBudgetOverride(frame) ? costsHref(props.companyPrefix) : null;
|
|
1247
1158
|
const activityLink = activityHref(frame, props.companyPrefix);
|
|
1248
1159
|
const hasContext = visibleContextItems(frame).length > 0;
|
|
1249
|
-
return /* @__PURE__ */
|
|
1250
|
-
/* @__PURE__ */
|
|
1251
|
-
/* @__PURE__ */
|
|
1252
|
-
/* @__PURE__ */
|
|
1253
|
-
/* @__PURE__ */
|
|
1254
|
-
/* @__PURE__ */
|
|
1255
|
-
requestedAmount(frame) ? /* @__PURE__ */
|
|
1160
|
+
return /* @__PURE__ */ jsxs4("div", { className: "space-y-3 border-t border-border pt-4", children: [
|
|
1161
|
+
/* @__PURE__ */ jsx5(ExplainabilityPanel, { frame, lane: "now", detailOnly: true }),
|
|
1162
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
|
|
1163
|
+
/* @__PURE__ */ jsx5("span", { children: requestDescriptor(frame) }),
|
|
1164
|
+
/* @__PURE__ */ jsx5("span", { children: impactLabel(frame) }),
|
|
1165
|
+
/* @__PURE__ */ jsx5("span", { children: formatRelativeTime(frameUpdatedAt(frame, snapshotUpdatedAt)) }),
|
|
1166
|
+
requestedAmount(frame) ? /* @__PURE__ */ jsxs4("span", { children: [
|
|
1256
1167
|
"Amount requested: ",
|
|
1257
1168
|
requestedAmount(frame)
|
|
1258
1169
|
] }) : null,
|
|
1259
|
-
budgetReason(frame) ? /* @__PURE__ */
|
|
1170
|
+
budgetReason(frame) ? /* @__PURE__ */ jsx5("span", { children: budgetReason(frame) }) : null
|
|
1260
1171
|
] }),
|
|
1261
|
-
hasContext ? /* @__PURE__ */
|
|
1262
|
-
/* @__PURE__ */
|
|
1263
|
-
/* @__PURE__ */
|
|
1172
|
+
hasContext ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
1173
|
+
/* @__PURE__ */ jsx5("div", { className: "text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: "Context" }),
|
|
1174
|
+
/* @__PURE__ */ jsx5(ContextItems, { frame })
|
|
1264
1175
|
] }) : null,
|
|
1265
|
-
/* @__PURE__ */
|
|
1266
|
-
href ? /* @__PURE__ */
|
|
1176
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-3", children: [
|
|
1177
|
+
href ? /* @__PURE__ */ jsxs4(
|
|
1267
1178
|
"a",
|
|
1268
1179
|
{
|
|
1269
1180
|
href,
|
|
@@ -1272,11 +1183,11 @@ function NowDetails(props) {
|
|
|
1272
1183
|
className: "inline-flex items-center gap-1 text-xs font-medium text-muted-foreground underline underline-offset-2 hover:text-foreground",
|
|
1273
1184
|
children: [
|
|
1274
1185
|
primaryLinkLabel(frame),
|
|
1275
|
-
/* @__PURE__ */
|
|
1186
|
+
/* @__PURE__ */ jsx5(ExternalLinkIcon, {})
|
|
1276
1187
|
]
|
|
1277
1188
|
}
|
|
1278
1189
|
) : null,
|
|
1279
|
-
activityLink ? /* @__PURE__ */
|
|
1190
|
+
activityLink ? /* @__PURE__ */ jsxs4(
|
|
1280
1191
|
"a",
|
|
1281
1192
|
{
|
|
1282
1193
|
href: activityLink,
|
|
@@ -1285,11 +1196,11 @@ function NowDetails(props) {
|
|
|
1285
1196
|
className: "inline-flex items-center gap-1 text-xs font-medium text-muted-foreground underline underline-offset-2 hover:text-foreground",
|
|
1286
1197
|
children: [
|
|
1287
1198
|
"Open activity",
|
|
1288
|
-
/* @__PURE__ */
|
|
1199
|
+
/* @__PURE__ */ jsx5(ExternalLinkIcon, {})
|
|
1289
1200
|
]
|
|
1290
1201
|
}
|
|
1291
1202
|
) : null,
|
|
1292
|
-
budgetHref ? /* @__PURE__ */
|
|
1203
|
+
budgetHref ? /* @__PURE__ */ jsx5(Accent, { children: /* @__PURE__ */ jsxs4(
|
|
1293
1204
|
"a",
|
|
1294
1205
|
{
|
|
1295
1206
|
href: budgetHref,
|
|
@@ -1299,7 +1210,7 @@ function NowDetails(props) {
|
|
|
1299
1210
|
style: { color: "inherit" },
|
|
1300
1211
|
children: [
|
|
1301
1212
|
"Review in Costs",
|
|
1302
|
-
/* @__PURE__ */
|
|
1213
|
+
/* @__PURE__ */ jsx5(ExternalLinkIcon, {})
|
|
1303
1214
|
]
|
|
1304
1215
|
}
|
|
1305
1216
|
) }) : null
|
|
@@ -1307,23 +1218,23 @@ function NowDetails(props) {
|
|
|
1307
1218
|
] });
|
|
1308
1219
|
}
|
|
1309
1220
|
function QuietNow() {
|
|
1310
|
-
return /* @__PURE__ */
|
|
1311
|
-
/* @__PURE__ */
|
|
1312
|
-
/* @__PURE__ */
|
|
1221
|
+
return /* @__PURE__ */ jsxs4("div", { className: "flex min-h-20 items-center gap-3", children: [
|
|
1222
|
+
/* @__PURE__ */ jsx5(QuietMark, { size: "md" }),
|
|
1223
|
+
/* @__PURE__ */ jsx5("div", { className: "text-sm text-muted-foreground", children: "Nothing active right now." })
|
|
1313
1224
|
] });
|
|
1314
1225
|
}
|
|
1315
1226
|
function NowActionRail(props) {
|
|
1316
1227
|
const actionMode = responseKind(props.frame, props.lane);
|
|
1317
1228
|
const isPending = props.pendingId === props.frame.id;
|
|
1318
1229
|
const commentEnabled = actionMode === "acknowledge" && isIssueFrame(props.frame);
|
|
1319
|
-
const [commentOpen, setCommentOpen] =
|
|
1320
|
-
const [commentBody, setCommentBody] =
|
|
1321
|
-
const textareaRef =
|
|
1322
|
-
|
|
1230
|
+
const [commentOpen, setCommentOpen] = useState5(false);
|
|
1231
|
+
const [commentBody, setCommentBody] = useState5("");
|
|
1232
|
+
const textareaRef = useRef5(null);
|
|
1233
|
+
useEffect5(() => {
|
|
1323
1234
|
setCommentOpen(false);
|
|
1324
1235
|
setCommentBody("");
|
|
1325
1236
|
}, [props.frame.id]);
|
|
1326
|
-
|
|
1237
|
+
useEffect5(() => {
|
|
1327
1238
|
if (commentOpen) textareaRef.current?.focus();
|
|
1328
1239
|
}, [commentOpen]);
|
|
1329
1240
|
async function submitComment() {
|
|
@@ -1333,16 +1244,16 @@ function NowActionRail(props) {
|
|
|
1333
1244
|
setCommentBody("");
|
|
1334
1245
|
setCommentOpen(false);
|
|
1335
1246
|
}
|
|
1336
|
-
return /* @__PURE__ */
|
|
1247
|
+
return /* @__PURE__ */ jsxs4(
|
|
1337
1248
|
"div",
|
|
1338
1249
|
{
|
|
1339
1250
|
className: "space-y-3 rounded-md border border-border/60 bg-secondary/20 px-3 py-3",
|
|
1340
1251
|
style: props.wide ? { width: 320, flexShrink: 0, position: "sticky", top: 16, alignSelf: "flex-start" } : void 0,
|
|
1341
1252
|
children: [
|
|
1342
|
-
/* @__PURE__ */
|
|
1343
|
-
actionMode === "approval" ? /* @__PURE__ */
|
|
1344
|
-
/* @__PURE__ */
|
|
1345
|
-
/* @__PURE__ */
|
|
1253
|
+
/* @__PURE__ */ jsx5("div", { className: "text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: "Actions" }),
|
|
1254
|
+
actionMode === "approval" ? /* @__PURE__ */ jsxs4("div", { className: "space-y-2", children: [
|
|
1255
|
+
/* @__PURE__ */ jsxs4("div", { className: "grid grid-cols-2 gap-2", children: [
|
|
1256
|
+
/* @__PURE__ */ jsx5(
|
|
1346
1257
|
ActionButton,
|
|
1347
1258
|
{
|
|
1348
1259
|
label: isPending ? "Submitting\u2026" : "Approve",
|
|
@@ -1352,7 +1263,7 @@ function NowActionRail(props) {
|
|
|
1352
1263
|
onClick: () => void props.onApprove(props.frame)
|
|
1353
1264
|
}
|
|
1354
1265
|
),
|
|
1355
|
-
/* @__PURE__ */
|
|
1266
|
+
/* @__PURE__ */ jsx5(
|
|
1356
1267
|
ActionButton,
|
|
1357
1268
|
{
|
|
1358
1269
|
label: "Reject",
|
|
@@ -1363,7 +1274,7 @@ function NowActionRail(props) {
|
|
|
1363
1274
|
}
|
|
1364
1275
|
)
|
|
1365
1276
|
] }),
|
|
1366
|
-
isBudgetOverride(props.frame) ? /* @__PURE__ */
|
|
1277
|
+
isBudgetOverride(props.frame) ? /* @__PURE__ */ jsx5(
|
|
1367
1278
|
ActionButton,
|
|
1368
1279
|
{
|
|
1369
1280
|
label: "Request revision",
|
|
@@ -1373,17 +1284,20 @@ function NowActionRail(props) {
|
|
|
1373
1284
|
onClick: () => void props.onRequestRevision(props.frame)
|
|
1374
1285
|
}
|
|
1375
1286
|
) : null
|
|
1376
|
-
] }) : actionMode === "acknowledge" ? /* @__PURE__ */
|
|
1377
|
-
commentEnabled ? /* @__PURE__ */
|
|
1287
|
+
] }) : actionMode === "acknowledge" ? /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
1288
|
+
commentEnabled ? /* @__PURE__ */ jsx5(
|
|
1378
1289
|
ActionButton,
|
|
1379
1290
|
{
|
|
1380
1291
|
label: "Comment",
|
|
1381
1292
|
tone: "accent",
|
|
1382
1293
|
disabled: isPending,
|
|
1383
|
-
onClick: () =>
|
|
1294
|
+
onClick: () => {
|
|
1295
|
+
props.onEngageFocus(props.frame, "comment_compose");
|
|
1296
|
+
setCommentOpen(true);
|
|
1297
|
+
}
|
|
1384
1298
|
}
|
|
1385
1299
|
) : null,
|
|
1386
|
-
/* @__PURE__ */
|
|
1300
|
+
/* @__PURE__ */ jsx5(
|
|
1387
1301
|
ActionButton,
|
|
1388
1302
|
{
|
|
1389
1303
|
label: isPending ? "Saving\u2026" : "Acknowledge",
|
|
@@ -1393,7 +1307,7 @@ function NowActionRail(props) {
|
|
|
1393
1307
|
}
|
|
1394
1308
|
)
|
|
1395
1309
|
] }) : null,
|
|
1396
|
-
props.itemLink ? /* @__PURE__ */
|
|
1310
|
+
props.itemLink ? /* @__PURE__ */ jsxs4(
|
|
1397
1311
|
"a",
|
|
1398
1312
|
{
|
|
1399
1313
|
href: props.itemLink,
|
|
@@ -1402,17 +1316,17 @@ function NowActionRail(props) {
|
|
|
1402
1316
|
className: "inline-flex items-center gap-1 text-xs font-medium text-muted-foreground underline underline-offset-2 hover:text-foreground",
|
|
1403
1317
|
children: [
|
|
1404
1318
|
primaryLinkLabel(props.frame),
|
|
1405
|
-
/* @__PURE__ */
|
|
1319
|
+
/* @__PURE__ */ jsx5(ExternalLinkIcon, {})
|
|
1406
1320
|
]
|
|
1407
1321
|
}
|
|
1408
1322
|
) : null,
|
|
1409
|
-
commentOpen ? /* @__PURE__ */
|
|
1410
|
-
/* @__PURE__ */
|
|
1323
|
+
commentOpen ? /* @__PURE__ */ jsxs4("div", { className: "space-y-2 rounded-md border border-border bg-background/80 p-3", children: [
|
|
1324
|
+
/* @__PURE__ */ jsxs4("div", { className: "text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: [
|
|
1411
1325
|
"Commenting on ",
|
|
1412
1326
|
compactTitle(props.frame)
|
|
1413
1327
|
] }),
|
|
1414
|
-
/* @__PURE__ */
|
|
1415
|
-
/* @__PURE__ */
|
|
1328
|
+
/* @__PURE__ */ jsx5("div", { className: "text-xs text-muted-foreground", children: "Posts to the issue thread without leaving Focus." }),
|
|
1329
|
+
/* @__PURE__ */ jsx5(
|
|
1416
1330
|
"textarea",
|
|
1417
1331
|
{
|
|
1418
1332
|
ref: textareaRef,
|
|
@@ -1434,8 +1348,8 @@ function NowActionRail(props) {
|
|
|
1434
1348
|
className: "w-full resize-y rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground shadow-xs outline-none transition focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]"
|
|
1435
1349
|
}
|
|
1436
1350
|
),
|
|
1437
|
-
/* @__PURE__ */
|
|
1438
|
-
/* @__PURE__ */
|
|
1351
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-end gap-2", children: [
|
|
1352
|
+
/* @__PURE__ */ jsx5(
|
|
1439
1353
|
ActionButton,
|
|
1440
1354
|
{
|
|
1441
1355
|
label: "Cancel",
|
|
@@ -1447,7 +1361,7 @@ function NowActionRail(props) {
|
|
|
1447
1361
|
}
|
|
1448
1362
|
}
|
|
1449
1363
|
),
|
|
1450
|
-
/* @__PURE__ */
|
|
1364
|
+
/* @__PURE__ */ jsx5(
|
|
1451
1365
|
ActionButton,
|
|
1452
1366
|
{
|
|
1453
1367
|
label: isPending ? "Posting\u2026" : "Post comment",
|
|
@@ -1464,7 +1378,7 @@ function NowActionRail(props) {
|
|
|
1464
1378
|
}
|
|
1465
1379
|
function NowPane(props) {
|
|
1466
1380
|
const frame = props.snapshot.now;
|
|
1467
|
-
const [detailsOpen, setDetailsOpen] =
|
|
1381
|
+
const [detailsOpen, setDetailsOpen] = useState5(false);
|
|
1468
1382
|
const wideLayout = useWideLayout(1120);
|
|
1469
1383
|
const activeFrameId = frame?.id ?? null;
|
|
1470
1384
|
const activeUnread = !!frame && (props.snapshot.review?.unread.now ?? 0) > 0;
|
|
@@ -1472,77 +1386,82 @@ function NowPane(props) {
|
|
|
1472
1386
|
const recommendedMove = frame ? recommendedMoveValue(frame) : null;
|
|
1473
1387
|
const helperLine = frame ? supportingLine(frame, "now") : null;
|
|
1474
1388
|
const driverBadge = driverBadgeStyle();
|
|
1475
|
-
|
|
1389
|
+
useEffect5(() => {
|
|
1476
1390
|
setDetailsOpen(false);
|
|
1477
1391
|
}, [activeFrameId]);
|
|
1478
|
-
return /* @__PURE__ */
|
|
1392
|
+
return /* @__PURE__ */ jsxs4(
|
|
1479
1393
|
"section",
|
|
1480
1394
|
{
|
|
1481
1395
|
className: "border border-border bg-card shadow-sm",
|
|
1482
1396
|
style: frame ? { borderLeftWidth: 3, borderLeftColor: ACCENT_COLOR } : void 0,
|
|
1483
1397
|
children: [
|
|
1484
|
-
/* @__PURE__ */
|
|
1485
|
-
/* @__PURE__ */
|
|
1486
|
-
/* @__PURE__ */
|
|
1487
|
-
/* @__PURE__ */
|
|
1488
|
-
/* @__PURE__ */
|
|
1489
|
-
/* @__PURE__ */
|
|
1398
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between gap-3 border-b border-border bg-accent px-4 py-3 sm:px-6", children: [
|
|
1399
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-3", children: [
|
|
1400
|
+
/* @__PURE__ */ jsx5(Accent, { className: "text-sm leading-none", children: props.posture.glyph }),
|
|
1401
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-end gap-2", children: [
|
|
1402
|
+
/* @__PURE__ */ jsx5(Accent, { className: "text-base leading-none font-semibold tracking-[0.04em]", children: props.brand.wordmark }),
|
|
1403
|
+
/* @__PURE__ */ jsx5("span", { className: "text-[11px] leading-none text-muted-foreground", children: props.posture.label })
|
|
1490
1404
|
] })
|
|
1491
1405
|
] }),
|
|
1492
|
-
/* @__PURE__ */
|
|
1493
|
-
/* @__PURE__ */
|
|
1406
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-3 text-xs tabular-nums text-muted-foreground", children: [
|
|
1407
|
+
/* @__PURE__ */ jsxs4("span", { className: props.counts.now > 0 ? "font-semibold" : "", children: [
|
|
1494
1408
|
"now ",
|
|
1495
|
-
props.counts.now > 0 ? /* @__PURE__ */
|
|
1409
|
+
props.counts.now > 0 ? /* @__PURE__ */ jsx5(Accent, { children: props.counts.now }) : props.counts.now
|
|
1496
1410
|
] }),
|
|
1497
|
-
/* @__PURE__ */
|
|
1411
|
+
/* @__PURE__ */ jsxs4("span", { className: props.counts.next > 0 ? "font-semibold" : "", children: [
|
|
1498
1412
|
"next ",
|
|
1499
|
-
props.counts.next > 0 ? /* @__PURE__ */
|
|
1413
|
+
props.counts.next > 0 ? /* @__PURE__ */ jsx5(Accent, { children: props.counts.next }) : props.counts.next
|
|
1500
1414
|
] }),
|
|
1501
|
-
/* @__PURE__ */
|
|
1415
|
+
/* @__PURE__ */ jsxs4("span", { className: props.counts.ambient > 0 ? "font-semibold" : "", children: [
|
|
1502
1416
|
"ambient ",
|
|
1503
|
-
props.counts.ambient > 0 ? /* @__PURE__ */
|
|
1417
|
+
props.counts.ambient > 0 ? /* @__PURE__ */ jsx5(Accent, { children: props.counts.ambient }) : props.counts.ambient
|
|
1504
1418
|
] })
|
|
1505
1419
|
] })
|
|
1506
1420
|
] }),
|
|
1507
|
-
/* @__PURE__ */
|
|
1508
|
-
/* @__PURE__ */
|
|
1421
|
+
/* @__PURE__ */ jsx5("div", { className: "px-5 py-5 sm:px-6", children: !frame ? /* @__PURE__ */ jsx5(QuietNow, {}) : /* @__PURE__ */ jsxs4("div", { className: "space-y-5", children: [
|
|
1422
|
+
/* @__PURE__ */ jsxs4(
|
|
1509
1423
|
"div",
|
|
1510
1424
|
{
|
|
1511
1425
|
className: wideLayout ? "flex items-start gap-6" : "space-y-4",
|
|
1512
1426
|
children: [
|
|
1513
|
-
/* @__PURE__ */
|
|
1514
|
-
/* @__PURE__ */
|
|
1515
|
-
/* @__PURE__ */
|
|
1516
|
-
/* @__PURE__ */
|
|
1427
|
+
/* @__PURE__ */ jsxs4("div", { className: "space-y-4", style: wideLayout ? { minWidth: 0, flex: 1 } : void 0, children: [
|
|
1428
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2 text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground", children: [
|
|
1429
|
+
/* @__PURE__ */ jsx5("span", { children: "Now" }),
|
|
1430
|
+
/* @__PURE__ */ jsx5("span", { className: "normal-case tracking-normal text-muted-foreground/70", children: sourceLabel(frame) })
|
|
1517
1431
|
] }),
|
|
1518
|
-
recommendedMove ? /* @__PURE__ */
|
|
1519
|
-
/* @__PURE__ */
|
|
1520
|
-
/* @__PURE__ */
|
|
1521
|
-
/* @__PURE__ */
|
|
1432
|
+
recommendedMove ? /* @__PURE__ */ jsx5("div", { className: "max-w-4xl text-3xl font-semibold leading-tight text-foreground", children: recommendedMove }) : frame.summary ? /* @__PURE__ */ jsx5("div", { className: "max-w-4xl text-3xl font-semibold leading-tight text-foreground", children: frame.summary }) : null,
|
|
1433
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-start gap-2 text-xl font-medium leading-snug text-foreground/80", children: [
|
|
1434
|
+
/* @__PURE__ */ jsx5(UnreadDot, { visible: activeUnread }),
|
|
1435
|
+
/* @__PURE__ */ jsx5("span", { children: renderTitle(frame) })
|
|
1522
1436
|
] }),
|
|
1523
|
-
helperLine ? /* @__PURE__ */
|
|
1524
|
-
/* @__PURE__ */
|
|
1525
|
-
/* @__PURE__ */
|
|
1526
|
-
driverLabel(frame) ? /* @__PURE__ */
|
|
1437
|
+
helperLine ? /* @__PURE__ */ jsx5("p", { className: "max-w-3xl text-sm leading-relaxed text-muted-foreground", children: helperLine }) : null,
|
|
1438
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
1439
|
+
/* @__PURE__ */ jsx5(Badge, { className: "border-border bg-secondary text-foreground/80", children: impactLabel(frame) }),
|
|
1440
|
+
driverLabel(frame) ? /* @__PURE__ */ jsx5(Badge, { className: driverBadge.className, style: driverBadge.style, children: driverLabel(frame) }) : null
|
|
1527
1441
|
] }),
|
|
1528
|
-
/* @__PURE__ */
|
|
1529
|
-
/* @__PURE__ */
|
|
1442
|
+
/* @__PURE__ */ jsx5(InlineExplainability, { frame, lane: "now", preferLaneReason: !!helperLine }),
|
|
1443
|
+
/* @__PURE__ */ jsx5("div", { className: "flex items-center gap-4", children: /* @__PURE__ */ jsx5(Accent, { children: /* @__PURE__ */ jsxs4(
|
|
1530
1444
|
"button",
|
|
1531
1445
|
{
|
|
1532
1446
|
type: "button",
|
|
1533
|
-
onClick: () =>
|
|
1447
|
+
onClick: () => {
|
|
1448
|
+
if (!detailsOpen) {
|
|
1449
|
+
props.onEngageFocus(frame, "show_context");
|
|
1450
|
+
}
|
|
1451
|
+
setDetailsOpen(!detailsOpen);
|
|
1452
|
+
},
|
|
1534
1453
|
"aria-expanded": detailsOpen,
|
|
1535
1454
|
className: "flex items-center gap-1.5 text-xs font-medium transition-opacity hover:opacity-100",
|
|
1536
1455
|
style: { color: "inherit" },
|
|
1537
1456
|
children: [
|
|
1538
|
-
/* @__PURE__ */
|
|
1457
|
+
/* @__PURE__ */ jsx5(
|
|
1539
1458
|
"svg",
|
|
1540
1459
|
{
|
|
1541
1460
|
viewBox: "0 0 16 16",
|
|
1542
1461
|
className: cn("h-3 w-3 transition-transform", detailsOpen && "rotate-90"),
|
|
1543
1462
|
fill: "currentColor",
|
|
1544
1463
|
"aria-hidden": "true",
|
|
1545
|
-
children: /* @__PURE__ */
|
|
1464
|
+
children: /* @__PURE__ */ jsx5("path", { d: "M6.22 3.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 0 1 0-1.06Z" })
|
|
1546
1465
|
}
|
|
1547
1466
|
),
|
|
1548
1467
|
detailsOpen ? "Hide context" : "Show context"
|
|
@@ -1550,7 +1469,7 @@ function NowPane(props) {
|
|
|
1550
1469
|
}
|
|
1551
1470
|
) }) })
|
|
1552
1471
|
] }),
|
|
1553
|
-
/* @__PURE__ */
|
|
1472
|
+
/* @__PURE__ */ jsx5(
|
|
1554
1473
|
NowActionRail,
|
|
1555
1474
|
{
|
|
1556
1475
|
frame,
|
|
@@ -1558,6 +1477,7 @@ function NowPane(props) {
|
|
|
1558
1477
|
wide: wideLayout,
|
|
1559
1478
|
pendingId: props.pendingId,
|
|
1560
1479
|
itemLink,
|
|
1480
|
+
onEngageFocus: props.onEngageFocus,
|
|
1561
1481
|
onApprove: props.onApprove,
|
|
1562
1482
|
onReject: props.onReject,
|
|
1563
1483
|
onRequestRevision: props.onRequestRevision,
|
|
@@ -1568,7 +1488,7 @@ function NowPane(props) {
|
|
|
1568
1488
|
]
|
|
1569
1489
|
}
|
|
1570
1490
|
),
|
|
1571
|
-
detailsOpen ? /* @__PURE__ */
|
|
1491
|
+
detailsOpen ? /* @__PURE__ */ jsx5(
|
|
1572
1492
|
NowDetails,
|
|
1573
1493
|
{
|
|
1574
1494
|
frame,
|
|
@@ -1583,7 +1503,7 @@ function NowPane(props) {
|
|
|
1583
1503
|
}
|
|
1584
1504
|
function NextRow(props) {
|
|
1585
1505
|
const { frame } = props;
|
|
1586
|
-
const [expanded, setExpanded] =
|
|
1506
|
+
const [expanded, setExpanded] = useState5(false);
|
|
1587
1507
|
const unread = isFrameUnreadInSnapshot(frame, props.snapshot);
|
|
1588
1508
|
const activityLink = activityHref(frame, props.companyPrefix);
|
|
1589
1509
|
const itemLink = itemHref(frame, props.companyPrefix);
|
|
@@ -1593,8 +1513,8 @@ function NextRow(props) {
|
|
|
1593
1513
|
const detailText = judgmentLine(frame, "next");
|
|
1594
1514
|
const showDetailText = detailText.trim().length > 0 && detailText !== secondaryText && detailText !== GENERIC_QUEUED_JUDGMENT;
|
|
1595
1515
|
const rankOpacity = Math.max(0.4, 1 - (props.rank - 1) * 0.14);
|
|
1596
|
-
return /* @__PURE__ */
|
|
1597
|
-
/* @__PURE__ */
|
|
1516
|
+
return /* @__PURE__ */ jsxs4("div", { className: "border-b border-border/80 last:border-b-0", style: expanded ? { borderLeftWidth: 2, borderLeftColor: ACCENT_COLOR } : void 0, children: [
|
|
1517
|
+
/* @__PURE__ */ jsxs4(
|
|
1598
1518
|
"button",
|
|
1599
1519
|
{
|
|
1600
1520
|
type: "button",
|
|
@@ -1602,40 +1522,40 @@ function NextRow(props) {
|
|
|
1602
1522
|
"aria-expanded": expanded,
|
|
1603
1523
|
className: "flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-accent/50 sm:px-6",
|
|
1604
1524
|
children: [
|
|
1605
|
-
/* @__PURE__ */
|
|
1606
|
-
/* @__PURE__ */
|
|
1607
|
-
/* @__PURE__ */
|
|
1608
|
-
/* @__PURE__ */
|
|
1609
|
-
secondaryText ? /* @__PURE__ */
|
|
1525
|
+
/* @__PURE__ */ jsx5(UnreadDot, { visible: unread }),
|
|
1526
|
+
/* @__PURE__ */ jsx5("span", { className: "w-5 shrink-0", style: { opacity: rankOpacity }, children: /* @__PURE__ */ jsx5(Accent, { className: "text-xs font-medium tabular-nums", children: String(props.rank).padStart(2, "0") }) }),
|
|
1527
|
+
/* @__PURE__ */ jsxs4("div", { className: "min-w-0 flex-1", children: [
|
|
1528
|
+
/* @__PURE__ */ jsx5("div", { className: "text-sm font-medium leading-5 text-foreground", children: renderTitle(frame) }),
|
|
1529
|
+
secondaryText ? /* @__PURE__ */ jsx5("div", { className: "mt-0.5 max-w-3xl text-xs leading-5 text-muted-foreground", children: secondaryText }) : null
|
|
1610
1530
|
] }),
|
|
1611
|
-
/* @__PURE__ */
|
|
1612
|
-
driver ? /* @__PURE__ */
|
|
1613
|
-
/* @__PURE__ */
|
|
1614
|
-
/* @__PURE__ */
|
|
1531
|
+
/* @__PURE__ */ jsx5(QueueMovementBadge, { movement: props.movement }),
|
|
1532
|
+
driver ? /* @__PURE__ */ jsx5(Badge, { className: cn("shrink-0", driverBadge.className), style: driverBadge.style, children: driver }) : null,
|
|
1533
|
+
/* @__PURE__ */ jsx5("span", { className: "shrink-0 text-xs text-muted-foreground", style: { opacity: 0.7 }, children: formatRelativeTime(frameUpdatedAt(frame, props.snapshotUpdatedAt)) }),
|
|
1534
|
+
/* @__PURE__ */ jsx5(
|
|
1615
1535
|
"svg",
|
|
1616
1536
|
{
|
|
1617
1537
|
viewBox: "0 0 16 16",
|
|
1618
1538
|
className: cn("h-3 w-3 shrink-0 text-muted-foreground transition-transform", expanded && "rotate-90"),
|
|
1619
1539
|
fill: "currentColor",
|
|
1620
1540
|
"aria-hidden": "true",
|
|
1621
|
-
children: /* @__PURE__ */
|
|
1541
|
+
children: /* @__PURE__ */ jsx5("path", { d: "M6.22 3.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 0 1 0-1.06Z" })
|
|
1622
1542
|
}
|
|
1623
1543
|
)
|
|
1624
1544
|
]
|
|
1625
1545
|
}
|
|
1626
1546
|
),
|
|
1627
|
-
expanded ? /* @__PURE__ */
|
|
1628
|
-
showDetailText ? /* @__PURE__ */
|
|
1629
|
-
/* @__PURE__ */
|
|
1630
|
-
/* @__PURE__ */
|
|
1631
|
-
/* @__PURE__ */
|
|
1632
|
-
driver ? /* @__PURE__ */
|
|
1633
|
-
props.movement ? /* @__PURE__ */
|
|
1634
|
-
/* @__PURE__ */
|
|
1547
|
+
expanded ? /* @__PURE__ */ jsxs4("div", { className: "space-y-3 px-4 pb-4 pl-12 sm:px-6 sm:pl-14", children: [
|
|
1548
|
+
showDetailText ? /* @__PURE__ */ jsx5("p", { className: "text-sm leading-relaxed text-muted-foreground", children: detailText }) : null,
|
|
1549
|
+
/* @__PURE__ */ jsx5(ExplainabilityStrip, { frame, lane: "next" }),
|
|
1550
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground", children: [
|
|
1551
|
+
/* @__PURE__ */ jsx5("span", { children: impactLabel(frame) }),
|
|
1552
|
+
driver ? /* @__PURE__ */ jsx5(Accent, { children: driver }) : null,
|
|
1553
|
+
props.movement ? /* @__PURE__ */ jsx5(Accent, { children: props.movement === "up" ? "rising in queue" : "falling in queue" }) : null,
|
|
1554
|
+
/* @__PURE__ */ jsxs4("span", { children: [
|
|
1635
1555
|
"updated ",
|
|
1636
1556
|
formatRelativeTime(frameUpdatedAt(frame, props.snapshotUpdatedAt))
|
|
1637
1557
|
] }),
|
|
1638
|
-
itemLink ? /* @__PURE__ */
|
|
1558
|
+
itemLink ? /* @__PURE__ */ jsx5(
|
|
1639
1559
|
"a",
|
|
1640
1560
|
{
|
|
1641
1561
|
href: itemLink,
|
|
@@ -1643,7 +1563,7 @@ function NextRow(props) {
|
|
|
1643
1563
|
children: primaryLinkLabel(frame)
|
|
1644
1564
|
}
|
|
1645
1565
|
) : null,
|
|
1646
|
-
activityLink ? /* @__PURE__ */
|
|
1566
|
+
activityLink ? /* @__PURE__ */ jsx5(
|
|
1647
1567
|
"a",
|
|
1648
1568
|
{
|
|
1649
1569
|
href: activityLink,
|
|
@@ -1652,9 +1572,9 @@ function NextRow(props) {
|
|
|
1652
1572
|
}
|
|
1653
1573
|
) : null
|
|
1654
1574
|
] }),
|
|
1655
|
-
/* @__PURE__ */
|
|
1656
|
-
/* @__PURE__ */
|
|
1657
|
-
/* @__PURE__ */
|
|
1575
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-2 pt-1", children: [
|
|
1576
|
+
/* @__PURE__ */ jsx5(IssueCommentComposer, { frame, pendingId: props.pendingId, onComment: props.onComment }),
|
|
1577
|
+
/* @__PURE__ */ jsx5(
|
|
1658
1578
|
FrameActions,
|
|
1659
1579
|
{
|
|
1660
1580
|
frame,
|
|
@@ -1671,9 +1591,9 @@ function NextRow(props) {
|
|
|
1671
1591
|
] });
|
|
1672
1592
|
}
|
|
1673
1593
|
function NextLane(props) {
|
|
1674
|
-
return /* @__PURE__ */
|
|
1675
|
-
/* @__PURE__ */
|
|
1676
|
-
props.rows.length === 0 ? /* @__PURE__ */
|
|
1594
|
+
return /* @__PURE__ */ jsxs4("section", { className: "border border-border/80 bg-card", children: [
|
|
1595
|
+
/* @__PURE__ */ jsx5("div", { className: "border-b border-border/80 px-4 py-2.5 sm:px-6", children: /* @__PURE__ */ jsx5("h2", { className: "text-xs font-medium uppercase tracking-wider text-muted-foreground", children: "Next" }) }),
|
|
1596
|
+
props.rows.length === 0 ? /* @__PURE__ */ jsx5("div", { className: "px-4 py-4 text-sm text-muted-foreground sm:px-6", style: { opacity: 0.7 }, children: "Nothing is staged behind the current focus." }) : props.rows.map((row, i) => /* @__PURE__ */ jsx5(
|
|
1677
1597
|
NextRow,
|
|
1678
1598
|
{
|
|
1679
1599
|
rank: i + 1,
|
|
@@ -1697,21 +1617,21 @@ function AmbientRow(props) {
|
|
|
1697
1617
|
const { frame, snapshotUpdatedAt } = props;
|
|
1698
1618
|
const href = itemHref(frame, props.companyPrefix);
|
|
1699
1619
|
const unread = isFrameUnreadInSnapshot(frame, props.snapshot);
|
|
1700
|
-
const content = /* @__PURE__ */
|
|
1701
|
-
/* @__PURE__ */
|
|
1702
|
-
/* @__PURE__ */
|
|
1703
|
-
/* @__PURE__ */
|
|
1704
|
-
/* @__PURE__ */
|
|
1620
|
+
const content = /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-3 px-4 py-2.5 sm:px-6", children: [
|
|
1621
|
+
/* @__PURE__ */ jsx5(UnreadDot, { visible: unread, tone: "muted", reserveSpace: true }),
|
|
1622
|
+
/* @__PURE__ */ jsx5("span", { className: "min-w-0 flex-1 truncate text-[13px] text-muted-foreground", children: renderTitle(frame) }),
|
|
1623
|
+
/* @__PURE__ */ jsx5("span", { className: "shrink-0 text-xs text-muted-foreground/80", children: sourceLabel(frame) }),
|
|
1624
|
+
/* @__PURE__ */ jsx5("span", { className: "shrink-0 text-xs text-muted-foreground/70", children: formatRelativeTime(frameUpdatedAt(frame, snapshotUpdatedAt)) })
|
|
1705
1625
|
] });
|
|
1706
|
-
return /* @__PURE__ */
|
|
1626
|
+
return /* @__PURE__ */ jsx5("div", { style: { borderBottomWidth: 1, borderBottomColor: "var(--border)" }, className: "last:border-b-0", children: href ? /* @__PURE__ */ jsx5("a", { href, className: "block transition-colors hover:bg-accent/40", children: content }) : content });
|
|
1707
1627
|
}
|
|
1708
1628
|
function AmbientLane(props) {
|
|
1709
|
-
return /* @__PURE__ */
|
|
1710
|
-
/* @__PURE__ */
|
|
1711
|
-
props.rows.length === 0 ? /* @__PURE__ */
|
|
1712
|
-
/* @__PURE__ */
|
|
1713
|
-
/* @__PURE__ */
|
|
1714
|
-
] }) : props.rows.map((row) => /* @__PURE__ */
|
|
1629
|
+
return /* @__PURE__ */ jsxs4("section", { style: { borderWidth: 1, borderColor: "var(--border)" }, className: "mt-2 bg-card", "aria-label": "Ambient attention", children: [
|
|
1630
|
+
/* @__PURE__ */ jsx5("div", { style: { borderBottomWidth: 1, borderBottomColor: "var(--border)" }, className: "px-4 py-2.5 sm:px-6", children: /* @__PURE__ */ jsx5("h2", { className: "text-xs font-medium uppercase tracking-wider text-muted-foreground/80", children: "Ambient" }) }),
|
|
1631
|
+
props.rows.length === 0 ? /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-3 px-4 py-4 text-sm text-muted-foreground sm:px-6", children: [
|
|
1632
|
+
/* @__PURE__ */ jsx5(QuietMark, {}),
|
|
1633
|
+
/* @__PURE__ */ jsx5("span", { children: "Nothing in peripheral view." })
|
|
1634
|
+
] }) : props.rows.map((row) => /* @__PURE__ */ jsx5(
|
|
1715
1635
|
AmbientRow,
|
|
1716
1636
|
{
|
|
1717
1637
|
frame: row.frame,
|
|
@@ -1731,23 +1651,23 @@ function DashboardWidget(props) {
|
|
|
1731
1651
|
const data = model.snapshot;
|
|
1732
1652
|
const loading = model.loading;
|
|
1733
1653
|
const error = model.error;
|
|
1734
|
-
if (!companyId) return /* @__PURE__ */
|
|
1735
|
-
if (loading) return /* @__PURE__ */
|
|
1736
|
-
if (error) return /* @__PURE__ */
|
|
1737
|
-
if (!data) return /* @__PURE__ */
|
|
1654
|
+
if (!companyId) return /* @__PURE__ */ jsx5(MessageCard, { title: `Open ${brand.wordmark}`, body: `Open a company to see ${brand.wordmark}.` });
|
|
1655
|
+
if (loading) return /* @__PURE__ */ jsx5(WidgetLoadingState, { label: brand.loadingLabel });
|
|
1656
|
+
if (error) return /* @__PURE__ */ jsx5(MessageCard, { title: "Plugin error", body: error.message });
|
|
1657
|
+
if (!data) return /* @__PURE__ */ jsx5(MessageCard, { title: brand.wordmark, body: brand.headingEmptyState });
|
|
1738
1658
|
const posture = postureForSnapshot(data);
|
|
1739
1659
|
const leadingText = data.now ? recommendedMoveValue(data.now) ?? data.now.title : null;
|
|
1740
1660
|
const actionable = actionableLabel(data);
|
|
1741
1661
|
const borderStyle = posture.label === "busy" ? { borderLeftWidth: 2, borderLeftColor: ACCENT_COLOR } : posture.label === "elevated" ? { borderLeftWidth: 2, borderLeftColor: ACCENT_BORDER } : void 0;
|
|
1742
|
-
return /* @__PURE__ */
|
|
1743
|
-
/* @__PURE__ */
|
|
1744
|
-
/* @__PURE__ */
|
|
1745
|
-
/* @__PURE__ */
|
|
1746
|
-
/* @__PURE__ */
|
|
1747
|
-
/* @__PURE__ */
|
|
1748
|
-
actionable ? /* @__PURE__ */
|
|
1662
|
+
return /* @__PURE__ */ jsxs4("a", { href, className: "block border border-border bg-card px-4 py-3 shadow-sm transition-colors hover:bg-accent/40", style: borderStyle, children: [
|
|
1663
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-start gap-2 text-xs", children: [
|
|
1664
|
+
/* @__PURE__ */ jsx5(Accent, { className: "pt-0.5 text-sm", children: posture.glyph }),
|
|
1665
|
+
/* @__PURE__ */ jsx5("div", { className: "min-w-0", children: /* @__PURE__ */ jsxs4("div", { className: "flex items-end gap-2", children: [
|
|
1666
|
+
/* @__PURE__ */ jsx5(Accent, { className: "font-semibold tracking-[0.04em]", children: brand.wordmark }),
|
|
1667
|
+
/* @__PURE__ */ jsx5(Accent, { className: "text-[11px]", children: posture.label }),
|
|
1668
|
+
actionable ? /* @__PURE__ */ jsx5(Accent, { className: "text-[11px]", children: actionable }) : null
|
|
1749
1669
|
] }) }),
|
|
1750
|
-
/* @__PURE__ */
|
|
1670
|
+
/* @__PURE__ */ jsxs4("span", { className: "ml-auto tabular-nums text-muted-foreground", children: [
|
|
1751
1671
|
"now ",
|
|
1752
1672
|
data.counts.now,
|
|
1753
1673
|
" \xB7 next ",
|
|
@@ -1756,7 +1676,7 @@ function DashboardWidget(props) {
|
|
|
1756
1676
|
data.counts.ambient
|
|
1757
1677
|
] })
|
|
1758
1678
|
] }),
|
|
1759
|
-
leadingText ? /* @__PURE__ */
|
|
1679
|
+
leadingText ? /* @__PURE__ */ jsx5(
|
|
1760
1680
|
"div",
|
|
1761
1681
|
{
|
|
1762
1682
|
className: "mt-2 text-sm font-medium leading-5 text-foreground",
|
|
@@ -1768,7 +1688,7 @@ function DashboardWidget(props) {
|
|
|
1768
1688
|
},
|
|
1769
1689
|
children: leadingText
|
|
1770
1690
|
}
|
|
1771
|
-
) : /* @__PURE__ */
|
|
1691
|
+
) : /* @__PURE__ */ jsx5("div", { className: "mt-2 text-sm text-muted-foreground", children: "No active interruption right now." })
|
|
1772
1692
|
] });
|
|
1773
1693
|
}
|
|
1774
1694
|
function AttentionSidebarLink({ context }) {
|
|
@@ -1780,7 +1700,7 @@ function AttentionSidebarLink({ context }) {
|
|
|
1780
1700
|
const merged = model.snapshot;
|
|
1781
1701
|
const actionable = merged ? actionableCount(merged) : 0;
|
|
1782
1702
|
const unread = unreadCount(merged);
|
|
1783
|
-
return /* @__PURE__ */
|
|
1703
|
+
return /* @__PURE__ */ jsxs4(
|
|
1784
1704
|
"a",
|
|
1785
1705
|
{
|
|
1786
1706
|
href,
|
|
@@ -1790,8 +1710,8 @@ function AttentionSidebarLink({ context }) {
|
|
|
1790
1710
|
isActive ? "bg-accent text-foreground" : "text-foreground/80 hover:bg-accent/50 hover:text-foreground"
|
|
1791
1711
|
),
|
|
1792
1712
|
children: [
|
|
1793
|
-
/* @__PURE__ */
|
|
1794
|
-
/* @__PURE__ */
|
|
1713
|
+
/* @__PURE__ */ jsxs4("span", { className: "relative shrink-0", "aria-hidden": "true", children: [
|
|
1714
|
+
/* @__PURE__ */ jsxs4(
|
|
1795
1715
|
"svg",
|
|
1796
1716
|
{
|
|
1797
1717
|
viewBox: "0 0 24 24",
|
|
@@ -1802,17 +1722,17 @@ function AttentionSidebarLink({ context }) {
|
|
|
1802
1722
|
strokeLinecap: "round",
|
|
1803
1723
|
strokeLinejoin: "round",
|
|
1804
1724
|
children: [
|
|
1805
|
-
/* @__PURE__ */
|
|
1806
|
-
/* @__PURE__ */
|
|
1807
|
-
/* @__PURE__ */
|
|
1808
|
-
/* @__PURE__ */
|
|
1809
|
-
/* @__PURE__ */
|
|
1810
|
-
/* @__PURE__ */
|
|
1811
|
-
/* @__PURE__ */
|
|
1725
|
+
/* @__PURE__ */ jsx5("circle", { cx: "12", cy: "12", r: "9" }),
|
|
1726
|
+
/* @__PURE__ */ jsx5("path", { d: "m14.3 8 5.2 9" }),
|
|
1727
|
+
/* @__PURE__ */ jsx5("path", { d: "M9.7 8h10.4" }),
|
|
1728
|
+
/* @__PURE__ */ jsx5("path", { d: "m7.4 12 5.2-9" }),
|
|
1729
|
+
/* @__PURE__ */ jsx5("path", { d: "m9.7 16-5.2-9" }),
|
|
1730
|
+
/* @__PURE__ */ jsx5("path", { d: "M14.3 16H3.9" }),
|
|
1731
|
+
/* @__PURE__ */ jsx5("path", { d: "m16.6 12-5.2 9" })
|
|
1812
1732
|
]
|
|
1813
1733
|
}
|
|
1814
1734
|
),
|
|
1815
|
-
unread > 0 ? /* @__PURE__ */
|
|
1735
|
+
unread > 0 ? /* @__PURE__ */ jsx5(
|
|
1816
1736
|
"span",
|
|
1817
1737
|
{
|
|
1818
1738
|
className: "absolute -right-1 -top-1 h-2 w-2 rounded-full",
|
|
@@ -1820,8 +1740,8 @@ function AttentionSidebarLink({ context }) {
|
|
|
1820
1740
|
}
|
|
1821
1741
|
) : null
|
|
1822
1742
|
] }),
|
|
1823
|
-
/* @__PURE__ */
|
|
1824
|
-
actionable > 0 ? /* @__PURE__ */
|
|
1743
|
+
/* @__PURE__ */ jsx5("span", { className: "flex-1 truncate", children: brand.wordmark }),
|
|
1744
|
+
actionable > 0 ? /* @__PURE__ */ jsx5("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__ */ jsx5(
|
|
1825
1745
|
"span",
|
|
1826
1746
|
{
|
|
1827
1747
|
className: "inline-flex items-center justify-center rounded-full border px-1.5 py-0.5 text-xs leading-none font-medium",
|
|
@@ -1840,182 +1760,36 @@ function AttentionPage(props) {
|
|
|
1840
1760
|
const snapshot = model.snapshot;
|
|
1841
1761
|
const loading = model.loading;
|
|
1842
1762
|
const error = model.error;
|
|
1843
|
-
const
|
|
1844
|
-
const commentOnIssue = usePluginAction("comment-on-issue");
|
|
1845
|
-
const recordApprovalResponse = usePluginAction("record-approval-response");
|
|
1846
|
-
const [pendingId, setPendingId] = useState(null);
|
|
1847
|
-
const [statusOverride, setStatusOverride] = useState(null);
|
|
1848
|
-
const [localSuppressions, setLocalSuppressions] = useState({});
|
|
1849
|
-
useEffect(() => {
|
|
1850
|
-
if (!statusOverride) return;
|
|
1851
|
-
const timer = window.setTimeout(() => {
|
|
1852
|
-
setStatusOverride(null);
|
|
1853
|
-
}, 4e3);
|
|
1854
|
-
return () => {
|
|
1855
|
-
window.clearTimeout(timer);
|
|
1856
|
-
};
|
|
1857
|
-
}, [statusOverride]);
|
|
1858
|
-
const persistedSuppressions = useMemo(
|
|
1859
|
-
() => suppressionMapFromReview(model.review),
|
|
1860
|
-
[model.review]
|
|
1861
|
-
);
|
|
1862
|
-
const effectiveSuppressions = useMemo(
|
|
1863
|
-
() => ({ ...persistedSuppressions, ...localSuppressions }),
|
|
1864
|
-
[persistedSuppressions, localSuppressions]
|
|
1865
|
-
);
|
|
1866
|
-
const displaySnapshot = useMemo(
|
|
1867
|
-
() => applyLocalSuppressions(snapshot, effectiveSuppressions),
|
|
1868
|
-
[snapshot, effectiveSuppressions]
|
|
1869
|
-
);
|
|
1763
|
+
const displaySnapshot = snapshot;
|
|
1870
1764
|
const backgroundErrorMessage = displaySnapshot && error ? `Sync issue: ${error.message}` : null;
|
|
1871
|
-
const posture =
|
|
1872
|
-
const nextRows =
|
|
1765
|
+
const posture = useMemo2(() => displaySnapshot ? postureForSnapshot(displaySnapshot) : null, [displaySnapshot]);
|
|
1766
|
+
const nextRows = useMemo2(
|
|
1873
1767
|
() => displaySnapshot ? displaySnapshot.next.map((frame) => ({ frame, lane: "next" })) : [],
|
|
1874
1768
|
[displaySnapshot]
|
|
1875
1769
|
);
|
|
1876
|
-
const ambientRows =
|
|
1770
|
+
const ambientRows = useMemo2(
|
|
1877
1771
|
() => displaySnapshot ? displaySnapshot.ambient.map((frame) => ({ frame, lane: "ambient" })) : [],
|
|
1878
1772
|
[displaySnapshot]
|
|
1879
1773
|
);
|
|
1880
1774
|
const nextMovement = useQueueMovement(nextRows.map((row) => row.frame));
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
});
|
|
1898
|
-
}, [snapshot, persistedSuppressions]);
|
|
1899
|
-
function nudgeRefresh() {
|
|
1900
|
-
model.refresh();
|
|
1901
|
-
window.setTimeout(() => model.refresh(), 500);
|
|
1902
|
-
}
|
|
1903
|
-
async function acknowledgeFrame(frame) {
|
|
1904
|
-
setPendingId(frame.id);
|
|
1905
|
-
const suppressedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1906
|
-
setLocalSuppressions((current) => ({ ...current, [frame.taskId]: suppressedAt }));
|
|
1907
|
-
try {
|
|
1908
|
-
await acknowledge({ companyId, taskId: frame.taskId, interactionId: frame.interactionId });
|
|
1909
|
-
setStatusOverride(acknowledgeSuccessMessage(compactTitle(frame)));
|
|
1910
|
-
nudgeRefresh();
|
|
1911
|
-
} catch (actionError) {
|
|
1912
|
-
setLocalSuppressions((current) => {
|
|
1913
|
-
const next = { ...current };
|
|
1914
|
-
delete next[frame.taskId];
|
|
1915
|
-
return next;
|
|
1916
|
-
});
|
|
1917
|
-
setStatusOverride(acknowledgeFailureMessage(actionError));
|
|
1918
|
-
} finally {
|
|
1919
|
-
setPendingId(null);
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
async function commentOnIssueFrame(frame, body) {
|
|
1923
|
-
const issueId = entityIdFromFrame(frame);
|
|
1924
|
-
if (!companyId || !issueId || entityTypeFromFrame(frame) !== "issue") {
|
|
1925
|
-
setStatusOverride(issueFrameUnsupportedMessage());
|
|
1926
|
-
return;
|
|
1927
|
-
}
|
|
1928
|
-
setPendingId(frame.id);
|
|
1929
|
-
try {
|
|
1930
|
-
await commentOnIssue({ companyId, taskId: frame.taskId, issueId, body });
|
|
1931
|
-
setStatusOverride(commentSuccessMessage(compactTitle(frame)));
|
|
1932
|
-
nudgeRefresh();
|
|
1933
|
-
} catch (actionError) {
|
|
1934
|
-
setStatusOverride(commentFailureMessage(actionError));
|
|
1935
|
-
} finally {
|
|
1936
|
-
setPendingId(null);
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
async function submitApprovalDecision(frame, decision) {
|
|
1940
|
-
const approvalId = approvalIdForFrame(frame);
|
|
1941
|
-
if (!approvalId) {
|
|
1942
|
-
setStatusOverride(approvalFrameUnsupportedMessage());
|
|
1943
|
-
return;
|
|
1944
|
-
}
|
|
1945
|
-
setPendingId(frame.id);
|
|
1946
|
-
const suppressedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1947
|
-
setLocalSuppressions((current) => ({ ...current, [frame.taskId]: suppressedAt }));
|
|
1948
|
-
try {
|
|
1949
|
-
const actionPath = decision === "approve" ? "approve" : "reject";
|
|
1950
|
-
await paperclipApiFetch(`/api/approvals/${approvalId}/${actionPath}`, {
|
|
1951
|
-
method: "POST",
|
|
1952
|
-
headers: {
|
|
1953
|
-
"Content-Type": "application/json"
|
|
1954
|
-
},
|
|
1955
|
-
body: JSON.stringify({}),
|
|
1956
|
-
expectJson: false
|
|
1957
|
-
});
|
|
1958
|
-
await recordApprovalResponse({
|
|
1959
|
-
companyId,
|
|
1960
|
-
taskId: frame.taskId,
|
|
1961
|
-
interactionId: frame.interactionId,
|
|
1962
|
-
decision
|
|
1963
|
-
});
|
|
1964
|
-
setStatusOverride(approvalDecisionSuccessMessage(compactTitle(frame), decision));
|
|
1965
|
-
nudgeRefresh();
|
|
1966
|
-
} catch (actionError) {
|
|
1967
|
-
setLocalSuppressions((current) => {
|
|
1968
|
-
const next = { ...current };
|
|
1969
|
-
delete next[frame.taskId];
|
|
1970
|
-
return next;
|
|
1971
|
-
});
|
|
1972
|
-
setStatusOverride(approvalDecisionFailureMessage(actionError));
|
|
1973
|
-
} finally {
|
|
1974
|
-
setPendingId(null);
|
|
1975
|
-
}
|
|
1976
|
-
}
|
|
1977
|
-
async function requestApprovalRevision(frame) {
|
|
1978
|
-
const approvalId = approvalIdForFrame(frame);
|
|
1979
|
-
if (!approvalId) {
|
|
1980
|
-
setStatusOverride(approvalFrameUnsupportedMessage());
|
|
1981
|
-
return;
|
|
1982
|
-
}
|
|
1983
|
-
setPendingId(frame.id);
|
|
1984
|
-
const suppressedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1985
|
-
setLocalSuppressions((current) => ({ ...current, [frame.taskId]: suppressedAt }));
|
|
1986
|
-
try {
|
|
1987
|
-
await paperclipApiFetch(`/api/approvals/${approvalId}/request-revision`, {
|
|
1988
|
-
method: "POST",
|
|
1989
|
-
headers: {
|
|
1990
|
-
"Content-Type": "application/json"
|
|
1991
|
-
},
|
|
1992
|
-
body: JSON.stringify({}),
|
|
1993
|
-
expectJson: false
|
|
1994
|
-
});
|
|
1995
|
-
await recordApprovalResponse({
|
|
1996
|
-
companyId,
|
|
1997
|
-
taskId: frame.taskId,
|
|
1998
|
-
interactionId: frame.interactionId,
|
|
1999
|
-
decision: "request-revision"
|
|
2000
|
-
});
|
|
2001
|
-
setStatusOverride(approvalRevisionSuccessMessage(compactTitle(frame)));
|
|
2002
|
-
nudgeRefresh();
|
|
2003
|
-
} catch (actionError) {
|
|
2004
|
-
setLocalSuppressions((current) => {
|
|
2005
|
-
const next = { ...current };
|
|
2006
|
-
delete next[frame.taskId];
|
|
2007
|
-
return next;
|
|
2008
|
-
});
|
|
2009
|
-
setStatusOverride(approvalRevisionFailureMessage(actionError));
|
|
2010
|
-
} finally {
|
|
2011
|
-
setPendingId(null);
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
if (!companyId) return /* @__PURE__ */ jsx(MessageCard, { title: `Open ${brand.wordmark}`, body: `Select a company to open ${brand.wordmark}.` });
|
|
2015
|
-
if (!displaySnapshot && loading) return /* @__PURE__ */ jsx(PageLoadingState, { label: "Loading attention center" });
|
|
2016
|
-
if (!displaySnapshot && error) return /* @__PURE__ */ jsx(MessageCard, { title: "Plugin error", body: error.message });
|
|
1775
|
+
const {
|
|
1776
|
+
pendingId,
|
|
1777
|
+
statusOverride,
|
|
1778
|
+
holdNowFrame,
|
|
1779
|
+
acknowledgeFrame,
|
|
1780
|
+
commentOnIssueFrame,
|
|
1781
|
+
submitApprovalDecision,
|
|
1782
|
+
requestApprovalRevision
|
|
1783
|
+
} = useFocusPageActions({
|
|
1784
|
+
companyId,
|
|
1785
|
+
displaySnapshot,
|
|
1786
|
+
refresh: model.refresh
|
|
1787
|
+
});
|
|
1788
|
+
if (!companyId) return /* @__PURE__ */ jsx5(MessageCard, { title: `Open ${brand.wordmark}`, body: `Select a company to open ${brand.wordmark}.` });
|
|
1789
|
+
if (!displaySnapshot && loading) return /* @__PURE__ */ jsx5(PageLoadingState, { label: "Loading attention center" });
|
|
1790
|
+
if (!displaySnapshot && error) return /* @__PURE__ */ jsx5(MessageCard, { title: "Plugin error", body: error.message });
|
|
2017
1791
|
if (!displaySnapshot || !posture) {
|
|
2018
|
-
return /* @__PURE__ */
|
|
1792
|
+
return /* @__PURE__ */ jsx5(
|
|
2019
1793
|
MessageCard,
|
|
2020
1794
|
{
|
|
2021
1795
|
title: `No ${brand.wordmark} State Yet`,
|
|
@@ -2023,8 +1797,8 @@ function AttentionPage(props) {
|
|
|
2023
1797
|
}
|
|
2024
1798
|
);
|
|
2025
1799
|
}
|
|
2026
|
-
return /* @__PURE__ */
|
|
2027
|
-
/* @__PURE__ */
|
|
1800
|
+
return /* @__PURE__ */ jsxs4("div", { className: "space-y-4", children: [
|
|
1801
|
+
/* @__PURE__ */ jsx5(
|
|
2028
1802
|
NowPane,
|
|
2029
1803
|
{
|
|
2030
1804
|
snapshot: displaySnapshot,
|
|
@@ -2033,6 +1807,7 @@ function AttentionPage(props) {
|
|
|
2033
1807
|
companyPrefix: props.context.companyPrefix,
|
|
2034
1808
|
counts: displaySnapshot.counts,
|
|
2035
1809
|
pendingId,
|
|
1810
|
+
onEngageFocus: holdNowFrame,
|
|
2036
1811
|
onApprove: (frame) => submitApprovalDecision(frame, "approve"),
|
|
2037
1812
|
onReject: (frame) => submitApprovalDecision(frame, "reject"),
|
|
2038
1813
|
onRequestRevision: requestApprovalRevision,
|
|
@@ -2040,7 +1815,7 @@ function AttentionPage(props) {
|
|
|
2040
1815
|
onComment: commentOnIssueFrame
|
|
2041
1816
|
}
|
|
2042
1817
|
),
|
|
2043
|
-
/* @__PURE__ */
|
|
1818
|
+
/* @__PURE__ */ jsx5(
|
|
2044
1819
|
NextLane,
|
|
2045
1820
|
{
|
|
2046
1821
|
snapshot: displaySnapshot,
|
|
@@ -2056,7 +1831,7 @@ function AttentionPage(props) {
|
|
|
2056
1831
|
onComment: commentOnIssueFrame
|
|
2057
1832
|
}
|
|
2058
1833
|
),
|
|
2059
|
-
/* @__PURE__ */
|
|
1834
|
+
/* @__PURE__ */ jsx5(
|
|
2060
1835
|
AmbientLane,
|
|
2061
1836
|
{
|
|
2062
1837
|
snapshot: displaySnapshot,
|
|
@@ -2065,7 +1840,7 @@ function AttentionPage(props) {
|
|
|
2065
1840
|
companyPrefix: props.context.companyPrefix
|
|
2066
1841
|
}
|
|
2067
1842
|
),
|
|
2068
|
-
/* @__PURE__ */
|
|
1843
|
+
/* @__PURE__ */ jsx5(StatusToast, { message: statusOverride ?? backgroundErrorMessage })
|
|
2069
1844
|
] });
|
|
2070
1845
|
}
|
|
2071
1846
|
export {
|