@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/dist/index.js CHANGED
@@ -207,7 +207,12 @@ var STYLES = (
207
207
 
208
208
  .item { border: 1px solid var(--line); border-radius: 10px; padding: 10px; cursor: pointer; background: var(--bg-2); }
209
209
  .item:hover { border-color: var(--accent); }
210
- .item .top { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
210
+ /* The header row now carries the number, a detached badge, a lifecycle chip, a PR
211
+ chip, a checks meter and the caret \u2014 more than fits on one line in a narrow
212
+ panel, so it wraps rather than clipping the last chip. The caret keeps its place
213
+ at the end of the row. */
214
+ .item .top { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 8px; margin-bottom: 6px; }
215
+ .item .top .caret { margin-left: auto; }
211
216
  .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; }
212
217
  .item .num.detached { background: #9aa0af; }
213
218
  .item .num.done { background: #10935a; }
@@ -581,6 +586,74 @@ var STYLES = (
581
586
  .menu-ver b { color: var(--ink); font-family: ui-monospace, Menlo, monospace; font-weight: 600; }
582
587
  .menu-mode { text-transform: uppercase; letter-spacing: .06em; font-size: 9.5px; }
583
588
 
589
+ /* ------------------------------------------------ lifecycle chips + review flow */
590
+ .lifechip {
591
+ padding: 1px 6px; border-radius: 999px; white-space: nowrap;
592
+ border: 1px solid var(--line); background: var(--bg); color: var(--muted);
593
+ font-size: 9.5px; font-weight: 700; letter-spacing: .02em; text-transform: uppercase;
594
+ }
595
+ .lifechip.st-sent { border-color: var(--accent); color: var(--accent); }
596
+ .lifechip.st-in_pr { border-color: #3f8ae0; color: #3f8ae0; }
597
+ .lifechip.st-preview { border-color: var(--accent); background: var(--accent); color: #fff; }
598
+ .lifechip.st-reviewed { border-color: #2f9e6a; color: #2f9e6a; }
599
+ .prchip {
600
+ padding: 1px 6px; border-radius: 6px; text-decoration: none; cursor: pointer;
601
+ border: 1px solid #3f8ae0; background: var(--bg); color: #3f8ae0;
602
+ font-family: ui-monospace, Menlo, monospace; font-size: 10px; font-weight: 700;
603
+ }
604
+ .prchip:hover { background: #3f8ae0; color: #fff; }
605
+ .prchip.st-merged { border-color: #8250df; color: #8250df; }
606
+ .prchip.st-merged:hover { background: #8250df; color: #fff; }
607
+ .prchip.st-closed { border-color: var(--line); color: var(--muted); }
608
+ /* checks meter \u2014 a numerator over a thin bar */
609
+ .checks { display: inline-flex; align-items: center; gap: 4px; }
610
+ .checks-n { font-family: ui-monospace, Menlo, monospace; font-size: 9.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
611
+ .checks-bar { display: block; width: 26px; height: 3px; border-radius: 2px; background: var(--line); overflow: hidden; }
612
+ .checks-bar i { display: block; height: 100%; background: #2f9e6a; }
613
+
614
+ /* the "N waiting on your review" strip above the list */
615
+ .reviewbar {
616
+ display: flex; align-items: center; gap: 7px; margin: 8px 12px 0; padding: 7px 10px;
617
+ border: 1px solid var(--accent); border-radius: 9px; background: var(--bg-2);
618
+ }
619
+ .rb-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); flex: none; animation: loupe-pulse 1.6s ease-in-out infinite; }
620
+ @media (prefers-reduced-motion: reduce) { .rb-dot { animation: none; } }
621
+ .rb-t { flex: 1; font-size: 11.5px; color: var(--ink); }
622
+ .rb-t b { font-variant-numeric: tabular-nums; }
623
+ .rb-b {
624
+ padding: 3px 9px; border: 1px solid var(--accent); border-radius: 7px;
625
+ background: var(--accent); color: #fff; font-size: 11px; font-weight: 600; cursor: pointer;
626
+ }
627
+
628
+ /* the review banner inside a thread */
629
+ .revbanner {
630
+ display: flex; align-items: center; gap: 6px; margin: 8px 0; padding: 7px 8px;
631
+ border: 1px solid var(--accent); border-radius: 9px; background: var(--bg-3);
632
+ }
633
+ .rev-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); flex: none; }
634
+ .rev-t { flex: 1; font-size: 11.5px; font-weight: 600; color: var(--ink); }
635
+ .rev-approve {
636
+ padding: 4px 10px; border: 1px solid var(--accent); border-radius: 7px;
637
+ background: var(--accent); color: #fff; font-size: 11px; font-weight: 600; cursor: pointer;
638
+ }
639
+ .rev-approve:disabled { opacity: .6; cursor: default; }
640
+ .rev-comment, .rev-origin {
641
+ padding: 4px 8px; border: 1px solid var(--line); border-radius: 7px;
642
+ background: var(--bg); color: var(--ink); font-size: 11px; cursor: pointer;
643
+ }
644
+ .rev-comment:hover, .rev-origin:hover { border-color: var(--accent); }
645
+
646
+ /* original request beside the proposed change */
647
+ .origin { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px; margin: 8px 0; }
648
+ .or-col { min-width: 0; padding: 7px 8px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg); }
649
+ .or-h { margin-bottom: 4px; font-size: 9.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
650
+ .or-b { font-size: 11px; line-height: 1.45; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
651
+ .or-code {
652
+ margin: 6px 0 0; padding: 6px; max-height: 130px; overflow: auto; border-radius: 6px;
653
+ background: var(--bg-2); color: var(--muted); font-family: ui-monospace, Menlo, monospace;
654
+ font-size: 10px; white-space: pre-wrap; overflow-wrap: anywhere;
655
+ }
656
+
584
657
  /* ------------------------------------------------------- hint card (once per view) */
585
658
  .hint {
586
659
  position: relative; margin: 8px 12px 0; padding: 10px 28px 10px 11px;
@@ -2845,6 +2918,44 @@ function summarizeActivity(events, status = "idle", now = Date.now()) {
2845
2918
  };
2846
2919
  }
2847
2920
 
2921
+ // ../shared/dist/lifecycle.js
2922
+ var LIFECYCLE_LABELS = {
2923
+ sent: "Sent to agent",
2924
+ in_pr: "In PR",
2925
+ preview: "Review preview",
2926
+ reviewed: "Reviewed"
2927
+ };
2928
+ function lifecycle(c) {
2929
+ const hasProposal = !!c.proposal;
2930
+ const pr = c.pr ?? void 0;
2931
+ const withPr = (stage) => {
2932
+ const out = { stage, label: LIFECYCLE_LABELS[stage] };
2933
+ if (pr) {
2934
+ out.pr = pr;
2935
+ if (typeof pr.checksTotal === "number" && pr.checksTotal > 0) {
2936
+ const passed = Math.max(0, Math.min(pr.checksTotal, pr.checksPassed ?? 0));
2937
+ out.checks = {
2938
+ text: `${passed}/${pr.checksTotal}`,
2939
+ ratio: passed / pr.checksTotal
2940
+ };
2941
+ }
2942
+ }
2943
+ return out;
2944
+ };
2945
+ if (c.status === "resolved")
2946
+ return hasProposal || pr ? withPr("reviewed") : null;
2947
+ if (pr)
2948
+ return withPr("in_pr");
2949
+ if (c.status === "in_review")
2950
+ return { stage: "preview", label: LIFECYCLE_LABELS.preview };
2951
+ if (hasProposal)
2952
+ return { stage: "sent", label: LIFECYCLE_LABELS.sent };
2953
+ return null;
2954
+ }
2955
+ function awaitingReview(comments) {
2956
+ return comments.filter((c) => c.status === "in_review");
2957
+ }
2958
+
2848
2959
  // ../shared/dist/index.js
2849
2960
  var COMMENT_STAGES = ["queue", "todo", "in_progress", "in_review", "resolved"];
2850
2961
  var STAGE_LABELS = {
@@ -2892,7 +3003,7 @@ function normalizePriority(value) {
2892
3003
  }
2893
3004
 
2894
3005
  // src/app.ts
2895
- var SDK_VERSION = true ? "0.10.14" : "dev";
3006
+ var SDK_VERSION = true ? "0.10.15" : "dev";
2896
3007
  var ACCENTS = [
2897
3008
  { id: "indigo", dark: "#6b73e6", light: "#4a55d6", soft: "rgba(107,115,230,0.12)" },
2898
3009
  { id: "violet", dark: "#a06be6", light: "#7c3fd4", soft: "rgba(160,107,230,0.14)" },
@@ -3504,7 +3615,9 @@ var LoupeApp = class {
3504
3615
  this.homeEl = homeView;
3505
3616
  const commentsView = el("div", "view comments-view");
3506
3617
  this.commentsHint = el("div", "hint-slot");
3507
- commentsView.append(this.commentsHint, tools, listHead, this.listEl, this.buildIntegrations());
3618
+ this.reviewBar = el("div", "reviewbar");
3619
+ this.reviewBar.style.display = "none";
3620
+ commentsView.append(this.commentsHint, tools, listHead, this.reviewBar, this.listEl, this.buildIntegrations());
3508
3621
  const activityView = el("div", "view activity-view");
3509
3622
  this.activityEl = activityView;
3510
3623
  const customViews = (this.cfg.tabs ?? []).map((t) => {
@@ -4858,6 +4971,7 @@ var LoupeApp = class {
4858
4971
  });
4859
4972
  if (this.scope === "all") items = [...items].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
4860
4973
  this.renderRepoFilter();
4974
+ this.renderReviewBar();
4861
4975
  if (!items.length) {
4862
4976
  this.listEl.appendChild(el(
4863
4977
  "div",
@@ -4902,7 +5016,32 @@ var LoupeApp = class {
4902
5016
  top.appendChild(el("span", "device", `${icon} ${kind}`));
4903
5017
  }
4904
5018
  if (isResolved(c)) top.appendChild(el("span", "badge done", "resolved"));
4905
- else if (detached) top.appendChild(el("span", "badge detached", "element moved/removed"));
5019
+ else if (detached) {
5020
+ const b = el("span", "badge detached", "moved");
5021
+ b.title = "element moved or removed";
5022
+ top.appendChild(b);
5023
+ }
5024
+ const lc = lifecycle(c);
5025
+ if (lc) {
5026
+ if (lc.stage !== "reviewed") top.appendChild(el("span", `lifechip st-${lc.stage}`, lc.label));
5027
+ if (lc.pr) {
5028
+ const pr = el(lc.pr.url ? "a" : "span", `prchip${lc.pr.state ? ` st-${lc.pr.state}` : ""}`, `#${lc.pr.number}`);
5029
+ pr.title = `Pull request #${lc.pr.number}${lc.pr.state ? ` \u2014 ${lc.pr.state}` : ""}`;
5030
+ if (lc.pr.url) {
5031
+ pr.href = lc.pr.url;
5032
+ pr.setAttribute("target", "_blank");
5033
+ pr.setAttribute("rel", "noreferrer");
5034
+ pr.addEventListener("click", (e) => e.stopPropagation());
5035
+ }
5036
+ top.appendChild(pr);
5037
+ }
5038
+ if (lc.checks) {
5039
+ const meter = el("span", "checks");
5040
+ meter.title = `Checks \u2014 ${lc.checks.text} passed`;
5041
+ 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>`;
5042
+ top.appendChild(meter);
5043
+ }
5044
+ }
4906
5045
  if (this.showPaths && this.scope === "all" && c.url) {
4907
5046
  top.appendChild(el("span", "pathtag", shortPath(c.url)));
4908
5047
  }
@@ -4911,6 +5050,8 @@ var LoupeApp = class {
4911
5050
  const summary = c.title || (c.body.split("\n")[0] ?? "").slice(0, 140) || "(no description)";
4912
5051
  item.appendChild(el("div", "summary", summary));
4913
5052
  const detail = el("div", "detail");
5053
+ if (c.status === "in_review") detail.appendChild(this.reviewBanner(c));
5054
+ if (c.proposal) detail.appendChild(this.proposalView(c));
4914
5055
  if (c.title || c.body.includes("\n")) detail.appendChild(el("div", "body", c.body));
4915
5056
  detail.appendChild(el("div", "meta", describeAnchor(c)));
4916
5057
  if (c.recording) {
@@ -4977,6 +5118,87 @@ var LoupeApp = class {
4977
5118
  };
4978
5119
  return item;
4979
5120
  }
5121
+ /**
5122
+ * The review banner. This is the one place the panel asks a human to decide
5123
+ * something, so it sits above everything else in the detail.
5124
+ *
5125
+ * Approving resolves the thread. That asymmetry is the rule the whole flow rests
5126
+ * on: an agent moves work to In Review, only a person closes it.
5127
+ */
5128
+ reviewBanner(c) {
5129
+ const label = c.title || c.body.split("\n")[0] || "this thread";
5130
+ const banner = el("div", "revbanner");
5131
+ banner.innerHTML = `<span class="rev-dot"></span><span class="rev-t">Waiting on your review</span><span class="rev-spacer"></span>`;
5132
+ const approve = el("button", "rev-approve", "Approve");
5133
+ approve.onclick = async (e) => {
5134
+ e.stopPropagation();
5135
+ approve.disabled = true;
5136
+ approve.textContent = "Approving\u2026";
5137
+ await this.store.update(c.id, { status: "resolved" });
5138
+ c.status = "resolved";
5139
+ this.renderPins();
5140
+ this.renderList();
5141
+ this.renderHome();
5142
+ this.addActivity({ kind: "review.approve", label: `Approved \u201C${label}\u201D` });
5143
+ };
5144
+ const discuss = el("button", "rev-comment", "Add comment");
5145
+ discuss.onclick = (e) => {
5146
+ e.stopPropagation();
5147
+ const target = this.resolved.get(c.id);
5148
+ if (target && target.isConnected) {
5149
+ const r = target.getBoundingClientRect();
5150
+ this.openComposer({ kind: "element", element: target }, r.left + r.width / 2, r.top + r.height);
5151
+ } else {
5152
+ this.setMode("free");
5153
+ }
5154
+ };
5155
+ banner.append(approve, discuss);
5156
+ if (c.proposal) {
5157
+ const origin = el("button", "rev-origin", "Show original");
5158
+ origin.onclick = (e) => {
5159
+ e.stopPropagation();
5160
+ const view = banner.parentElement?.querySelector(".origin");
5161
+ if (!view) return;
5162
+ const shown = view.classList.toggle("show");
5163
+ view.style.display = shown ? "" : "none";
5164
+ origin.textContent = shown ? "Hide original" : "Show original";
5165
+ };
5166
+ banner.appendChild(origin);
5167
+ }
5168
+ return banner;
5169
+ }
5170
+ /**
5171
+ * The original request beside Claude's proposed change. Hidden until the review
5172
+ * banner's toggle asks for it — a reviewer who does not care should not pay for
5173
+ * the markup.
5174
+ */
5175
+ proposalView(c) {
5176
+ const p = c.proposal;
5177
+ const wrap = el("div", "origin");
5178
+ wrap.style.display = "none";
5179
+ wrap.innerHTML = `<div class="or-col"><div class="or-h">Original request</div><div class="or-b">${escapeHtml(c.title ? `${c.title}
5180
+
5181
+ ${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>`;
5182
+ return wrap;
5183
+ }
5184
+ /** The strips above the list: how many threads are waiting on a human. */
5185
+ renderReviewBar() {
5186
+ if (!this.reviewBar) return;
5187
+ const waiting = awaitingReview(this.visibleComments);
5188
+ if (!waiting.length) {
5189
+ this.reviewBar.innerHTML = "";
5190
+ this.reviewBar.style.display = "none";
5191
+ return;
5192
+ }
5193
+ const on = this.statFilter === "needs_you";
5194
+ this.reviewBar.style.display = "";
5195
+ 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>`;
5196
+ this.reviewBar.querySelector('[data-role="rb-toggle"]').onclick = () => {
5197
+ this.statFilter = on ? "" : "needs_you";
5198
+ this.renderList();
5199
+ this.renderHome();
5200
+ };
5201
+ }
4980
5202
  flash(id) {
4981
5203
  const c = this.comments.find((x) => x.id === id);
4982
5204
  const pin = this.pins.get(id);