@loupekit/sdk 0.10.14 → 0.10.15-next.60
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 +23 -0
- package/dist/index.global.js +226 -4
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +226 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -153,6 +153,29 @@ trackActivity({ kind: "Bash", label: "pnpm test", detail: "exit 1", level: "erro
|
|
|
153
153
|
Loupe's own operations feed the same stream, so the view is never empty. With nothing connected it
|
|
154
154
|
says *Monitor unavailable* and explains how to wire it up.
|
|
155
155
|
|
|
156
|
+
### Reviewing a change
|
|
157
|
+
|
|
158
|
+
A thread can carry the pull request its fix is riding on, which the panel turns into a lifecycle chip,
|
|
159
|
+
a PR link and a checks meter:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// Via the API (or your own tooling) — the panel picks it up on the next load.
|
|
163
|
+
await fetch(`${apiBase}/v1/comments/${id}`, {
|
|
164
|
+
method: "PATCH",
|
|
165
|
+
headers: { "Content-Type": "application/json", "X-Loupe-Admin": secret },
|
|
166
|
+
body: JSON.stringify({
|
|
167
|
+
pr: { number: 412, url: "https://github.com/acme/web/pull/412", checksPassed: 3, checksTotal: 4 },
|
|
168
|
+
}),
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Threads in **In Review** lead their detail with a review banner — **Approve** and **Add comment** —
|
|
173
|
+
and, when a change has been proposed, a *Show original* toggle that puts your original request beside
|
|
174
|
+
the proposal. Approving resolves the thread and is logged in the Activity feed.
|
|
175
|
+
|
|
176
|
+
The rule that shape enforces: **only a human resolves a thread.** An agent moves work to In Review;
|
|
177
|
+
a person closes it from there.
|
|
178
|
+
|
|
156
179
|
### Offline mode (no backend)
|
|
157
180
|
|
|
158
181
|
Omit `apiBase` and comments persist to `localStorage` — great for demos and local dev:
|
package/dist/index.global.js
CHANGED
|
@@ -238,7 +238,12 @@ var Loupe = (() => {
|
|
|
238
238
|
|
|
239
239
|
.item { border: 1px solid var(--line); border-radius: 10px; padding: 10px; cursor: pointer; background: var(--bg-2); }
|
|
240
240
|
.item:hover { border-color: var(--accent); }
|
|
241
|
-
|
|
241
|
+
/* The header row now carries the number, a detached badge, a lifecycle chip, a PR
|
|
242
|
+
chip, a checks meter and the caret \u2014 more than fits on one line in a narrow
|
|
243
|
+
panel, so it wraps rather than clipping the last chip. The caret keeps its place
|
|
244
|
+
at the end of the row. */
|
|
245
|
+
.item .top { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 8px; margin-bottom: 6px; }
|
|
246
|
+
.item .top .caret { margin-left: auto; }
|
|
242
247
|
.item .num { background: var(--pin); color: #fff; width: 20px; height: 20px; border-radius: 50%; font-size: 11px; font-weight: 700; display: grid; place-items: center; flex: none; }
|
|
243
248
|
.item .num.detached { background: #9aa0af; }
|
|
244
249
|
.item .num.done { background: #10935a; }
|
|
@@ -612,6 +617,74 @@ var Loupe = (() => {
|
|
|
612
617
|
.menu-ver b { color: var(--ink); font-family: ui-monospace, Menlo, monospace; font-weight: 600; }
|
|
613
618
|
.menu-mode { text-transform: uppercase; letter-spacing: .06em; font-size: 9.5px; }
|
|
614
619
|
|
|
620
|
+
/* ------------------------------------------------ lifecycle chips + review flow */
|
|
621
|
+
.lifechip {
|
|
622
|
+
padding: 1px 6px; border-radius: 999px; white-space: nowrap;
|
|
623
|
+
border: 1px solid var(--line); background: var(--bg); color: var(--muted);
|
|
624
|
+
font-size: 9.5px; font-weight: 700; letter-spacing: .02em; text-transform: uppercase;
|
|
625
|
+
}
|
|
626
|
+
.lifechip.st-sent { border-color: var(--accent); color: var(--accent); }
|
|
627
|
+
.lifechip.st-in_pr { border-color: #3f8ae0; color: #3f8ae0; }
|
|
628
|
+
.lifechip.st-preview { border-color: var(--accent); background: var(--accent); color: #fff; }
|
|
629
|
+
.lifechip.st-reviewed { border-color: #2f9e6a; color: #2f9e6a; }
|
|
630
|
+
.prchip {
|
|
631
|
+
padding: 1px 6px; border-radius: 6px; text-decoration: none; cursor: pointer;
|
|
632
|
+
border: 1px solid #3f8ae0; background: var(--bg); color: #3f8ae0;
|
|
633
|
+
font-family: ui-monospace, Menlo, monospace; font-size: 10px; font-weight: 700;
|
|
634
|
+
}
|
|
635
|
+
.prchip:hover { background: #3f8ae0; color: #fff; }
|
|
636
|
+
.prchip.st-merged { border-color: #8250df; color: #8250df; }
|
|
637
|
+
.prchip.st-merged:hover { background: #8250df; color: #fff; }
|
|
638
|
+
.prchip.st-closed { border-color: var(--line); color: var(--muted); }
|
|
639
|
+
/* checks meter \u2014 a numerator over a thin bar */
|
|
640
|
+
.checks { display: inline-flex; align-items: center; gap: 4px; }
|
|
641
|
+
.checks-n { font-family: ui-monospace, Menlo, monospace; font-size: 9.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
642
|
+
.checks-bar { display: block; width: 26px; height: 3px; border-radius: 2px; background: var(--line); overflow: hidden; }
|
|
643
|
+
.checks-bar i { display: block; height: 100%; background: #2f9e6a; }
|
|
644
|
+
|
|
645
|
+
/* the "N waiting on your review" strip above the list */
|
|
646
|
+
.reviewbar {
|
|
647
|
+
display: flex; align-items: center; gap: 7px; margin: 8px 12px 0; padding: 7px 10px;
|
|
648
|
+
border: 1px solid var(--accent); border-radius: 9px; background: var(--bg-2);
|
|
649
|
+
}
|
|
650
|
+
.rb-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); flex: none; animation: loupe-pulse 1.6s ease-in-out infinite; }
|
|
651
|
+
@media (prefers-reduced-motion: reduce) { .rb-dot { animation: none; } }
|
|
652
|
+
.rb-t { flex: 1; font-size: 11.5px; color: var(--ink); }
|
|
653
|
+
.rb-t b { font-variant-numeric: tabular-nums; }
|
|
654
|
+
.rb-b {
|
|
655
|
+
padding: 3px 9px; border: 1px solid var(--accent); border-radius: 7px;
|
|
656
|
+
background: var(--accent); color: #fff; font-size: 11px; font-weight: 600; cursor: pointer;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/* the review banner inside a thread */
|
|
660
|
+
.revbanner {
|
|
661
|
+
display: flex; align-items: center; gap: 6px; margin: 8px 0; padding: 7px 8px;
|
|
662
|
+
border: 1px solid var(--accent); border-radius: 9px; background: var(--bg-3);
|
|
663
|
+
}
|
|
664
|
+
.rev-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); flex: none; }
|
|
665
|
+
.rev-t { flex: 1; font-size: 11.5px; font-weight: 600; color: var(--ink); }
|
|
666
|
+
.rev-approve {
|
|
667
|
+
padding: 4px 10px; border: 1px solid var(--accent); border-radius: 7px;
|
|
668
|
+
background: var(--accent); color: #fff; font-size: 11px; font-weight: 600; cursor: pointer;
|
|
669
|
+
}
|
|
670
|
+
.rev-approve:disabled { opacity: .6; cursor: default; }
|
|
671
|
+
.rev-comment, .rev-origin {
|
|
672
|
+
padding: 4px 8px; border: 1px solid var(--line); border-radius: 7px;
|
|
673
|
+
background: var(--bg); color: var(--ink); font-size: 11px; cursor: pointer;
|
|
674
|
+
}
|
|
675
|
+
.rev-comment:hover, .rev-origin:hover { border-color: var(--accent); }
|
|
676
|
+
|
|
677
|
+
/* original request beside the proposed change */
|
|
678
|
+
.origin { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px; margin: 8px 0; }
|
|
679
|
+
.or-col { min-width: 0; padding: 7px 8px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg); }
|
|
680
|
+
.or-h { margin-bottom: 4px; font-size: 9.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
|
|
681
|
+
.or-b { font-size: 11px; line-height: 1.45; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
682
|
+
.or-code {
|
|
683
|
+
margin: 6px 0 0; padding: 6px; max-height: 130px; overflow: auto; border-radius: 6px;
|
|
684
|
+
background: var(--bg-2); color: var(--muted); font-family: ui-monospace, Menlo, monospace;
|
|
685
|
+
font-size: 10px; white-space: pre-wrap; overflow-wrap: anywhere;
|
|
686
|
+
}
|
|
687
|
+
|
|
615
688
|
/* ------------------------------------------------------- hint card (once per view) */
|
|
616
689
|
.hint {
|
|
617
690
|
position: relative; margin: 8px 12px 0; padding: 10px 28px 10px 11px;
|
|
@@ -2876,6 +2949,44 @@ var Loupe = (() => {
|
|
|
2876
2949
|
};
|
|
2877
2950
|
}
|
|
2878
2951
|
|
|
2952
|
+
// ../shared/dist/lifecycle.js
|
|
2953
|
+
var LIFECYCLE_LABELS = {
|
|
2954
|
+
sent: "Sent to agent",
|
|
2955
|
+
in_pr: "In PR",
|
|
2956
|
+
preview: "Review preview",
|
|
2957
|
+
reviewed: "Reviewed"
|
|
2958
|
+
};
|
|
2959
|
+
function lifecycle(c) {
|
|
2960
|
+
const hasProposal = !!c.proposal;
|
|
2961
|
+
const pr = c.pr ?? void 0;
|
|
2962
|
+
const withPr = (stage) => {
|
|
2963
|
+
const out = { stage, label: LIFECYCLE_LABELS[stage] };
|
|
2964
|
+
if (pr) {
|
|
2965
|
+
out.pr = pr;
|
|
2966
|
+
if (typeof pr.checksTotal === "number" && pr.checksTotal > 0) {
|
|
2967
|
+
const passed = Math.max(0, Math.min(pr.checksTotal, pr.checksPassed ?? 0));
|
|
2968
|
+
out.checks = {
|
|
2969
|
+
text: `${passed}/${pr.checksTotal}`,
|
|
2970
|
+
ratio: passed / pr.checksTotal
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
return out;
|
|
2975
|
+
};
|
|
2976
|
+
if (c.status === "resolved")
|
|
2977
|
+
return hasProposal || pr ? withPr("reviewed") : null;
|
|
2978
|
+
if (pr)
|
|
2979
|
+
return withPr("in_pr");
|
|
2980
|
+
if (c.status === "in_review")
|
|
2981
|
+
return { stage: "preview", label: LIFECYCLE_LABELS.preview };
|
|
2982
|
+
if (hasProposal)
|
|
2983
|
+
return { stage: "sent", label: LIFECYCLE_LABELS.sent };
|
|
2984
|
+
return null;
|
|
2985
|
+
}
|
|
2986
|
+
function awaitingReview(comments) {
|
|
2987
|
+
return comments.filter((c) => c.status === "in_review");
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2879
2990
|
// ../shared/dist/index.js
|
|
2880
2991
|
var COMMENT_STAGES = ["queue", "todo", "in_progress", "in_review", "resolved"];
|
|
2881
2992
|
var STAGE_LABELS = {
|
|
@@ -2923,7 +3034,7 @@ var Loupe = (() => {
|
|
|
2923
3034
|
}
|
|
2924
3035
|
|
|
2925
3036
|
// src/app.ts
|
|
2926
|
-
var SDK_VERSION = true ? "0.10.
|
|
3037
|
+
var SDK_VERSION = true ? "0.10.15" : "dev";
|
|
2927
3038
|
var ACCENTS = [
|
|
2928
3039
|
{ id: "indigo", dark: "#6b73e6", light: "#4a55d6", soft: "rgba(107,115,230,0.12)" },
|
|
2929
3040
|
{ id: "violet", dark: "#a06be6", light: "#7c3fd4", soft: "rgba(160,107,230,0.14)" },
|
|
@@ -3535,7 +3646,9 @@ var Loupe = (() => {
|
|
|
3535
3646
|
this.homeEl = homeView;
|
|
3536
3647
|
const commentsView = el("div", "view comments-view");
|
|
3537
3648
|
this.commentsHint = el("div", "hint-slot");
|
|
3538
|
-
|
|
3649
|
+
this.reviewBar = el("div", "reviewbar");
|
|
3650
|
+
this.reviewBar.style.display = "none";
|
|
3651
|
+
commentsView.append(this.commentsHint, tools, listHead, this.reviewBar, this.listEl, this.buildIntegrations());
|
|
3539
3652
|
const activityView = el("div", "view activity-view");
|
|
3540
3653
|
this.activityEl = activityView;
|
|
3541
3654
|
const customViews = (this.cfg.tabs ?? []).map((t) => {
|
|
@@ -4889,6 +5002,7 @@ var Loupe = (() => {
|
|
|
4889
5002
|
});
|
|
4890
5003
|
if (this.scope === "all") items = [...items].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
4891
5004
|
this.renderRepoFilter();
|
|
5005
|
+
this.renderReviewBar();
|
|
4892
5006
|
if (!items.length) {
|
|
4893
5007
|
this.listEl.appendChild(el(
|
|
4894
5008
|
"div",
|
|
@@ -4933,7 +5047,32 @@ var Loupe = (() => {
|
|
|
4933
5047
|
top.appendChild(el("span", "device", `${icon} ${kind}`));
|
|
4934
5048
|
}
|
|
4935
5049
|
if (isResolved(c)) top.appendChild(el("span", "badge done", "resolved"));
|
|
4936
|
-
else if (detached)
|
|
5050
|
+
else if (detached) {
|
|
5051
|
+
const b = el("span", "badge detached", "moved");
|
|
5052
|
+
b.title = "element moved or removed";
|
|
5053
|
+
top.appendChild(b);
|
|
5054
|
+
}
|
|
5055
|
+
const lc = lifecycle(c);
|
|
5056
|
+
if (lc) {
|
|
5057
|
+
if (lc.stage !== "reviewed") top.appendChild(el("span", `lifechip st-${lc.stage}`, lc.label));
|
|
5058
|
+
if (lc.pr) {
|
|
5059
|
+
const pr = el(lc.pr.url ? "a" : "span", `prchip${lc.pr.state ? ` st-${lc.pr.state}` : ""}`, `#${lc.pr.number}`);
|
|
5060
|
+
pr.title = `Pull request #${lc.pr.number}${lc.pr.state ? ` \u2014 ${lc.pr.state}` : ""}`;
|
|
5061
|
+
if (lc.pr.url) {
|
|
5062
|
+
pr.href = lc.pr.url;
|
|
5063
|
+
pr.setAttribute("target", "_blank");
|
|
5064
|
+
pr.setAttribute("rel", "noreferrer");
|
|
5065
|
+
pr.addEventListener("click", (e) => e.stopPropagation());
|
|
5066
|
+
}
|
|
5067
|
+
top.appendChild(pr);
|
|
5068
|
+
}
|
|
5069
|
+
if (lc.checks) {
|
|
5070
|
+
const meter = el("span", "checks");
|
|
5071
|
+
meter.title = `Checks \u2014 ${lc.checks.text} passed`;
|
|
5072
|
+
meter.innerHTML = `<span class="checks-n">${escapeHtml(lc.checks.text)}</span><span class="checks-bar"><i style="width:${Math.round(lc.checks.ratio * 100)}%"></i></span>`;
|
|
5073
|
+
top.appendChild(meter);
|
|
5074
|
+
}
|
|
5075
|
+
}
|
|
4937
5076
|
if (this.showPaths && this.scope === "all" && c.url) {
|
|
4938
5077
|
top.appendChild(el("span", "pathtag", shortPath(c.url)));
|
|
4939
5078
|
}
|
|
@@ -4942,6 +5081,8 @@ var Loupe = (() => {
|
|
|
4942
5081
|
const summary = c.title || (c.body.split("\n")[0] ?? "").slice(0, 140) || "(no description)";
|
|
4943
5082
|
item.appendChild(el("div", "summary", summary));
|
|
4944
5083
|
const detail = el("div", "detail");
|
|
5084
|
+
if (c.status === "in_review") detail.appendChild(this.reviewBanner(c));
|
|
5085
|
+
if (c.proposal) detail.appendChild(this.proposalView(c));
|
|
4945
5086
|
if (c.title || c.body.includes("\n")) detail.appendChild(el("div", "body", c.body));
|
|
4946
5087
|
detail.appendChild(el("div", "meta", describeAnchor(c)));
|
|
4947
5088
|
if (c.recording) {
|
|
@@ -5008,6 +5149,87 @@ var Loupe = (() => {
|
|
|
5008
5149
|
};
|
|
5009
5150
|
return item;
|
|
5010
5151
|
}
|
|
5152
|
+
/**
|
|
5153
|
+
* The review banner. This is the one place the panel asks a human to decide
|
|
5154
|
+
* something, so it sits above everything else in the detail.
|
|
5155
|
+
*
|
|
5156
|
+
* Approving resolves the thread. That asymmetry is the rule the whole flow rests
|
|
5157
|
+
* on: an agent moves work to In Review, only a person closes it.
|
|
5158
|
+
*/
|
|
5159
|
+
reviewBanner(c) {
|
|
5160
|
+
const label = c.title || c.body.split("\n")[0] || "this thread";
|
|
5161
|
+
const banner = el("div", "revbanner");
|
|
5162
|
+
banner.innerHTML = `<span class="rev-dot"></span><span class="rev-t">Waiting on your review</span><span class="rev-spacer"></span>`;
|
|
5163
|
+
const approve = el("button", "rev-approve", "Approve");
|
|
5164
|
+
approve.onclick = async (e) => {
|
|
5165
|
+
e.stopPropagation();
|
|
5166
|
+
approve.disabled = true;
|
|
5167
|
+
approve.textContent = "Approving\u2026";
|
|
5168
|
+
await this.store.update(c.id, { status: "resolved" });
|
|
5169
|
+
c.status = "resolved";
|
|
5170
|
+
this.renderPins();
|
|
5171
|
+
this.renderList();
|
|
5172
|
+
this.renderHome();
|
|
5173
|
+
this.addActivity({ kind: "review.approve", label: `Approved \u201C${label}\u201D` });
|
|
5174
|
+
};
|
|
5175
|
+
const discuss = el("button", "rev-comment", "Add comment");
|
|
5176
|
+
discuss.onclick = (e) => {
|
|
5177
|
+
e.stopPropagation();
|
|
5178
|
+
const target = this.resolved.get(c.id);
|
|
5179
|
+
if (target && target.isConnected) {
|
|
5180
|
+
const r = target.getBoundingClientRect();
|
|
5181
|
+
this.openComposer({ kind: "element", element: target }, r.left + r.width / 2, r.top + r.height);
|
|
5182
|
+
} else {
|
|
5183
|
+
this.setMode("free");
|
|
5184
|
+
}
|
|
5185
|
+
};
|
|
5186
|
+
banner.append(approve, discuss);
|
|
5187
|
+
if (c.proposal) {
|
|
5188
|
+
const origin = el("button", "rev-origin", "Show original");
|
|
5189
|
+
origin.onclick = (e) => {
|
|
5190
|
+
e.stopPropagation();
|
|
5191
|
+
const view = banner.parentElement?.querySelector(".origin");
|
|
5192
|
+
if (!view) return;
|
|
5193
|
+
const shown = view.classList.toggle("show");
|
|
5194
|
+
view.style.display = shown ? "" : "none";
|
|
5195
|
+
origin.textContent = shown ? "Hide original" : "Show original";
|
|
5196
|
+
};
|
|
5197
|
+
banner.appendChild(origin);
|
|
5198
|
+
}
|
|
5199
|
+
return banner;
|
|
5200
|
+
}
|
|
5201
|
+
/**
|
|
5202
|
+
* The original request beside Claude's proposed change. Hidden until the review
|
|
5203
|
+
* banner's toggle asks for it — a reviewer who does not care should not pay for
|
|
5204
|
+
* the markup.
|
|
5205
|
+
*/
|
|
5206
|
+
proposalView(c) {
|
|
5207
|
+
const p = c.proposal;
|
|
5208
|
+
const wrap = el("div", "origin");
|
|
5209
|
+
wrap.style.display = "none";
|
|
5210
|
+
wrap.innerHTML = `<div class="or-col"><div class="or-h">Original request</div><div class="or-b">${escapeHtml(c.title ? `${c.title}
|
|
5211
|
+
|
|
5212
|
+
${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escapeHtml(c.context.html)}</pre>` : "") + `</div><div class="or-col"><div class="or-h">Proposed change${p.author ? ` \xB7 ${escapeHtml(p.author)}` : ""}</div>` + (p.notes ? `<div class="or-b">${escapeHtml(p.notes)}</div>` : "") + (p.html ? `<pre class="or-code">${escapeHtml(p.html)}</pre>` : "") + (p.css ? `<pre class="or-code">${escapeHtml(p.css)}</pre>` : "") + `</div>`;
|
|
5213
|
+
return wrap;
|
|
5214
|
+
}
|
|
5215
|
+
/** The strips above the list: how many threads are waiting on a human. */
|
|
5216
|
+
renderReviewBar() {
|
|
5217
|
+
if (!this.reviewBar) return;
|
|
5218
|
+
const waiting = awaitingReview(this.visibleComments);
|
|
5219
|
+
if (!waiting.length) {
|
|
5220
|
+
this.reviewBar.innerHTML = "";
|
|
5221
|
+
this.reviewBar.style.display = "none";
|
|
5222
|
+
return;
|
|
5223
|
+
}
|
|
5224
|
+
const on = this.statFilter === "needs_you";
|
|
5225
|
+
this.reviewBar.style.display = "";
|
|
5226
|
+
this.reviewBar.innerHTML = `<span class="rb-dot"></span><span class="rb-t"><b>${waiting.length}</b> waiting on your review</span><button class="rb-b" data-role="rb-toggle">${on ? "Show all" : "Review"}</button>`;
|
|
5227
|
+
this.reviewBar.querySelector('[data-role="rb-toggle"]').onclick = () => {
|
|
5228
|
+
this.statFilter = on ? "" : "needs_you";
|
|
5229
|
+
this.renderList();
|
|
5230
|
+
this.renderHome();
|
|
5231
|
+
};
|
|
5232
|
+
}
|
|
5011
5233
|
flash(id) {
|
|
5012
5234
|
const c = this.comments.find((x) => x.id === id);
|
|
5013
5235
|
const pin = this.pins.get(id);
|