@loupekit/sdk 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -7
- package/dist/index.global.js +184 -135
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +184 -135
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.global.js
CHANGED
|
@@ -378,6 +378,8 @@ var Loupe = (() => {
|
|
|
378
378
|
}
|
|
379
379
|
.tabs .tab:hover { color: var(--ink); background: var(--bg-2); }
|
|
380
380
|
.tabs .tab.on { color: var(--accent); border-bottom-color: var(--accent); }
|
|
381
|
+
.tabs .tab.off { opacity: .45; cursor: not-allowed; }
|
|
382
|
+
.tabs .tab.off:hover { color: inherit; background: transparent; }
|
|
381
383
|
|
|
382
384
|
/* Only the active page shows. Driven by an "on" class rather than a .tab-<id>
|
|
383
385
|
selector, so host-registered tab ids need no CSS of their own. */
|
|
@@ -436,11 +438,6 @@ var Loupe = (() => {
|
|
|
436
438
|
font-size: 10.5px; font-family: ui-monospace, Menlo, monospace;
|
|
437
439
|
max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
438
440
|
}
|
|
439
|
-
.listhead .reposel {
|
|
440
|
-
margin-left: 6px; font-size: 11px; padding: 3px 6px; border: 1px solid var(--line);
|
|
441
|
-
border-radius: 7px; background: var(--bg-2); color: var(--ink);
|
|
442
|
-
text-transform: none; letter-spacing: 0; max-width: 130px;
|
|
443
|
-
}
|
|
444
441
|
|
|
445
442
|
/* recording marker + video in the list */
|
|
446
443
|
.item .rectag { font-size: 10px; font-weight: 700; color: var(--pin); background: var(--bg-3); border-radius: 999px; padding: 1px 7px; white-space: nowrap; }
|
|
@@ -554,6 +551,9 @@ var Loupe = (() => {
|
|
|
554
551
|
.activity-view { padding: 10px 12px 16px; }
|
|
555
552
|
.mon-status { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight: 600; color: var(--ink); }
|
|
556
553
|
.mon-spacer { flex: 1; }
|
|
554
|
+
.mon-live { font-size: 10px; font-weight: 600; color: var(--muted); }
|
|
555
|
+
.mon-live.live { color: #2f9e6a; }
|
|
556
|
+
.mon-live.offline { color: #d9534f; }
|
|
557
557
|
.mon-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: none; }
|
|
558
558
|
.mon-status.st-working .mon-dot { background: var(--accent); animation: loupe-pulse 1.4s ease-in-out infinite; }
|
|
559
559
|
.mon-status.st-error .mon-dot { background: var(--pin); }
|
|
@@ -617,8 +617,7 @@ var Loupe = (() => {
|
|
|
617
617
|
color: var(--ink); font-size: 11.5px; font-family: ui-monospace, Menlo, monospace; cursor: pointer;
|
|
618
618
|
}
|
|
619
619
|
.proj-chip:hover { border-color: var(--accent); }
|
|
620
|
-
.proj-chip .proj-
|
|
621
|
-
.proj-chip.unset .proj-repo { color: var(--muted); font-style: italic; }
|
|
620
|
+
.proj-chip .proj-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; }
|
|
622
621
|
.proj-caret { color: var(--muted); font-family: inherit; }
|
|
623
622
|
.proj-pop {
|
|
624
623
|
position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 25;
|
|
@@ -631,19 +630,17 @@ var Loupe = (() => {
|
|
|
631
630
|
.pp-x:hover { background: var(--bg-3); color: var(--ink); }
|
|
632
631
|
.pp-cur { margin-bottom: 8px; font-size: 11.5px; line-height: 1.45; color: var(--muted); }
|
|
633
632
|
.pp-cur b { color: var(--ink); font-family: ui-monospace, Menlo, monospace; }
|
|
634
|
-
.pp-search {
|
|
635
|
-
width: 100%; margin-bottom: 6px; padding: 6px 8px; border: 1px solid var(--line);
|
|
636
|
-
border-radius: 8px; background: var(--bg); color: var(--ink); font-size: 12px;
|
|
637
|
-
}
|
|
638
|
-
.pp-search:focus { outline: none; border-color: var(--accent); }
|
|
639
633
|
.pp-list { display: flex; flex-direction: column; gap: 2px; max-height: 30vh; overflow-y: auto; }
|
|
640
|
-
.pp-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
634
|
+
.pp-proj {
|
|
635
|
+
display: flex; align-items: center; gap: 6px; padding: 5px 8px; border: 1px solid transparent;
|
|
636
|
+
border-radius: 7px; font-size: 11.5px; color: var(--ink);
|
|
637
|
+
}
|
|
638
|
+
.pp-proj.on { border-color: var(--accent); }
|
|
639
|
+
.pp-proj-n { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
640
|
+
.pp-badge {
|
|
641
|
+
flex: none; padding: 1px 6px; border-radius: 999px; background: var(--bg-3);
|
|
642
|
+
color: var(--muted); font-size: 10px; white-space: nowrap;
|
|
644
643
|
}
|
|
645
|
-
.pp-item:hover { background: var(--bg-3); }
|
|
646
|
-
.pp-item.on { border-color: var(--accent); color: var(--accent); }
|
|
647
644
|
.pp-empty { padding: 8px 6px; font-size: 11px; line-height: 1.5; color: var(--muted); }
|
|
648
645
|
.pp-empty code { font-family: ui-monospace, Menlo, monospace; color: var(--ink); }
|
|
649
646
|
.pp-clear {
|
|
@@ -677,6 +674,15 @@ var Loupe = (() => {
|
|
|
677
674
|
/* The host package's version, shown only when it differs from this bundle's (a stale publish). */
|
|
678
675
|
.ver-stale { color: var(--pin); font-family: ui-monospace, Menlo, monospace; font-weight: 600; cursor: help; }
|
|
679
676
|
|
|
677
|
+
/* ------------------------------------------------ tickets between projects */
|
|
678
|
+
.srcchip, .fwdchip {
|
|
679
|
+
padding: 1px 6px; border-radius: 999px; white-space: nowrap; font-size: 10px;
|
|
680
|
+
border: 1px solid var(--line); background: var(--bg); color: var(--muted);
|
|
681
|
+
max-width: 140px; overflow: hidden; text-overflow: ellipsis;
|
|
682
|
+
}
|
|
683
|
+
.fwdchip { border-color: var(--accent); color: var(--accent); }
|
|
684
|
+
.fwdchip.bad { border-color: #d9534f; color: #d9534f; }
|
|
685
|
+
|
|
680
686
|
/* ------------------------------------------------ lifecycle chips + review flow */
|
|
681
687
|
.lifechip {
|
|
682
688
|
padding: 1px 6px; border-radius: 999px; white-space: nowrap;
|
|
@@ -3796,6 +3802,14 @@ var Loupe = (() => {
|
|
|
3796
3802
|
}
|
|
3797
3803
|
}
|
|
3798
3804
|
}
|
|
3805
|
+
/** Offline: there is no organization to read. */
|
|
3806
|
+
async getOrg() {
|
|
3807
|
+
return null;
|
|
3808
|
+
}
|
|
3809
|
+
/** Offline: no server feed; the panel shows only what this browser reports. */
|
|
3810
|
+
async listActivity(_projectKey, _since) {
|
|
3811
|
+
return null;
|
|
3812
|
+
}
|
|
3799
3813
|
};
|
|
3800
3814
|
|
|
3801
3815
|
// src/http-adapter.ts
|
|
@@ -3931,6 +3945,28 @@ var Loupe = (() => {
|
|
|
3931
3945
|
}));
|
|
3932
3946
|
if (!res.ok) throw new Error(`update failed: ${res.status}`);
|
|
3933
3947
|
}
|
|
3948
|
+
/** The organization, read through the host app. A 404 means the backend has no such endpoint. */
|
|
3949
|
+
async getOrg() {
|
|
3950
|
+
const res = await fetch(`${this.base}/v1/org`, this.opts({ headers: this.headers() }));
|
|
3951
|
+
if (res.status === 404) return null;
|
|
3952
|
+
if (!res.ok) throw new Error(`getOrg failed: ${res.status}`);
|
|
3953
|
+
const body = await res.json();
|
|
3954
|
+
if (!body || typeof body !== "object" || !body.project || !Array.isArray(body.projects)) {
|
|
3955
|
+
throw new Error("getOrg: the answer was not an organization");
|
|
3956
|
+
}
|
|
3957
|
+
return body;
|
|
3958
|
+
}
|
|
3959
|
+
/** The server's activity feed. A 404 means the backend keeps none. */
|
|
3960
|
+
async listActivity(projectKey, since) {
|
|
3961
|
+
const q = new URLSearchParams({ projectKey });
|
|
3962
|
+
if (since) q.set("since", since);
|
|
3963
|
+
const res = await fetch(`${this.base}/v1/activity?${q}`, this.opts({ headers: this.headers() }));
|
|
3964
|
+
if (res.status === 404) return null;
|
|
3965
|
+
if (!res.ok) throw new Error(`listActivity failed: ${res.status}`);
|
|
3966
|
+
const body = await res.json();
|
|
3967
|
+
if (!Array.isArray(body)) throw new Error("listActivity: the answer was not a list");
|
|
3968
|
+
return body;
|
|
3969
|
+
}
|
|
3934
3970
|
async remove(id) {
|
|
3935
3971
|
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(id)}`, this.opts({
|
|
3936
3972
|
method: "DELETE",
|
|
@@ -3941,7 +3977,7 @@ var Loupe = (() => {
|
|
|
3941
3977
|
};
|
|
3942
3978
|
|
|
3943
3979
|
// src/app.ts
|
|
3944
|
-
var SDK_VERSION = true ? "0.
|
|
3980
|
+
var SDK_VERSION = true ? "0.12.0" : "dev";
|
|
3945
3981
|
var FAB_SIZE = 46;
|
|
3946
3982
|
var FAB_DRAG_THRESHOLD = 6;
|
|
3947
3983
|
var LAUNCHER_SHORTCUT = "Alt+Shift+L";
|
|
@@ -3966,7 +4002,7 @@ var Loupe = (() => {
|
|
|
3966
4002
|
sel: ".hscope",
|
|
3967
4003
|
tab: "home",
|
|
3968
4004
|
title: "This page, or the whole project",
|
|
3969
|
-
body: "Switch to All to see every page's feedback as a day-grouped timeline
|
|
4005
|
+
body: "Switch to All to see every page's feedback as a day-grouped timeline."
|
|
3970
4006
|
},
|
|
3971
4007
|
{
|
|
3972
4008
|
sel: ".tools",
|
|
@@ -3978,7 +4014,7 @@ var Loupe = (() => {
|
|
|
3978
4014
|
sel: '.tabs [data-tab="activity"]',
|
|
3979
4015
|
tab: "activity",
|
|
3980
4016
|
title: "Watch the work happen",
|
|
3981
|
-
body: "
|
|
4017
|
+
body: "Comments, status changes and forwarded tickets land here as they happen, with anything a bridge or your app reports. Tool chips filter it."
|
|
3982
4018
|
},
|
|
3983
4019
|
{
|
|
3984
4020
|
sel: '.dctl [data-role="settings"]',
|
|
@@ -4095,8 +4131,6 @@ var Loupe = (() => {
|
|
|
4095
4131
|
this.allLoaded = false;
|
|
4096
4132
|
/** A clicked stat tile, which narrows the list. */
|
|
4097
4133
|
this.statFilter = "";
|
|
4098
|
-
/** Repo filter, offered once the scope is "all". */
|
|
4099
|
-
this.repoFilter = "";
|
|
4100
4134
|
/** Accent preset id (see ACCENTS), applied as inline --accent / --accent-soft. */
|
|
4101
4135
|
this.accent = "indigo";
|
|
4102
4136
|
/** Collapsed to the one-line minimize bar. */
|
|
@@ -4120,8 +4154,16 @@ var Loupe = (() => {
|
|
|
4120
4154
|
this.activityFollow = true;
|
|
4121
4155
|
/** The summary card's key/value rows are behind this toggle. */
|
|
4122
4156
|
this.summaryOpen = false;
|
|
4123
|
-
/** Per-browser project settings:
|
|
4157
|
+
/** Per-browser project settings: the environment URLs and the Local AI endpoint. */
|
|
4124
4158
|
this.project = { environments: [] };
|
|
4159
|
+
/** The organization, read once from the backend. Null offline or before it loads. */
|
|
4160
|
+
this.org = null;
|
|
4161
|
+
/** Whether the server keeps an activity feed, and whether the last read reached it. */
|
|
4162
|
+
this.activityRemote = "unsupported";
|
|
4163
|
+
/** Newest server event seen, so a poll asks only for what is new. */
|
|
4164
|
+
this.activitySince = "";
|
|
4165
|
+
/** The backend answered that it keeps no feed, so the panel stops asking. */
|
|
4166
|
+
this.activityNone = false;
|
|
4125
4167
|
/** Iteration history per thread — generating a change is a stack, not a one-shot. */
|
|
4126
4168
|
this.iterations = /* @__PURE__ */ new Map();
|
|
4127
4169
|
/** The thread whose generate pane is open, if any. */
|
|
@@ -4134,13 +4176,9 @@ var Loupe = (() => {
|
|
|
4134
4176
|
this.consent = emptyConsent();
|
|
4135
4177
|
/** The live SpeechRecognition instance while dictating, if any. */
|
|
4136
4178
|
this.voice = null;
|
|
4137
|
-
this.projSearch = "";
|
|
4138
4179
|
this.projOpen = false;
|
|
4139
|
-
this.projResults = [];
|
|
4140
4180
|
this.projError = "";
|
|
4141
4181
|
this.envDraft = "";
|
|
4142
|
-
/** Guards against a slow repo search overwriting a newer one. */
|
|
4143
|
-
this.repoSeq = 0;
|
|
4144
4182
|
/** Float-mode window geometry; (x<=0 && y<=0) → placed on first layout. */
|
|
4145
4183
|
this.floatRect = { x: 0, y: 0, w: 380, h: 540 };
|
|
4146
4184
|
this.floatDrag = null;
|
|
@@ -4485,6 +4523,10 @@ var Loupe = (() => {
|
|
|
4485
4523
|
get url() {
|
|
4486
4524
|
return location.pathname + location.search;
|
|
4487
4525
|
}
|
|
4526
|
+
/** Chat is experimental: the tab stays dimmed and inert unless the host opts in. */
|
|
4527
|
+
get chatOn() {
|
|
4528
|
+
return this.cfg.chat === true;
|
|
4529
|
+
}
|
|
4488
4530
|
async start() {
|
|
4489
4531
|
this.loadState();
|
|
4490
4532
|
this.loadProject();
|
|
@@ -4501,8 +4543,11 @@ var Loupe = (() => {
|
|
|
4501
4543
|
this.renderHome();
|
|
4502
4544
|
this.startPresence();
|
|
4503
4545
|
this.startLiveThreads();
|
|
4504
|
-
this.startCompanionStream();
|
|
4546
|
+
if (this.chatOn) this.startCompanionStream();
|
|
4505
4547
|
void this.loadNotifications();
|
|
4548
|
+
void this.loadOrg();
|
|
4549
|
+
void this.loadActivity();
|
|
4550
|
+
this.syncActivityPoll();
|
|
4506
4551
|
void this.loadAllComments();
|
|
4507
4552
|
this.observe();
|
|
4508
4553
|
this.watchNavigation();
|
|
@@ -4673,8 +4718,13 @@ var Loupe = (() => {
|
|
|
4673
4718
|
this.tabList = [...BUILTIN_TABS, ...(this.cfg.tabs ?? []).map((t) => ({ id: t.id, label: t.label }))];
|
|
4674
4719
|
const tabs = el("div", "tabs");
|
|
4675
4720
|
for (const t of this.tabList) {
|
|
4676
|
-
const
|
|
4721
|
+
const off = t.id === "chat" && !this.chatOn;
|
|
4722
|
+
const b = el("button", off ? "tab off" : "tab", t.label);
|
|
4677
4723
|
b.dataset.tab = t.id;
|
|
4724
|
+
if (off) {
|
|
4725
|
+
b.setAttribute("aria-disabled", "true");
|
|
4726
|
+
b.title = "Chat \u2014 coming soon";
|
|
4727
|
+
}
|
|
4678
4728
|
b.onclick = () => this.setTab(t.id);
|
|
4679
4729
|
tabs.appendChild(b);
|
|
4680
4730
|
}
|
|
@@ -4714,15 +4764,6 @@ var Loupe = (() => {
|
|
|
4714
4764
|
this.renderList();
|
|
4715
4765
|
};
|
|
4716
4766
|
listHead.appendChild(search);
|
|
4717
|
-
this.repoSel = el("select", "reposel");
|
|
4718
|
-
this.repoSel.title = "Filter by repository";
|
|
4719
|
-
this.repoSel.setAttribute("aria-label", "Filter by repository");
|
|
4720
|
-
this.repoSel.onchange = () => {
|
|
4721
|
-
this.repoFilter = this.repoSel.value;
|
|
4722
|
-
this.renderList();
|
|
4723
|
-
};
|
|
4724
|
-
this.repoSel.style.display = "none";
|
|
4725
|
-
listHead.appendChild(this.repoSel);
|
|
4726
4767
|
this.listEl = el("div", "list");
|
|
4727
4768
|
const homeView = el("div", "view home-view");
|
|
4728
4769
|
this.homeEl = homeView;
|
|
@@ -4808,6 +4849,7 @@ var Loupe = (() => {
|
|
|
4808
4849
|
}
|
|
4809
4850
|
/** The Chat tab label carries the unread count, so it is visible from any page. */
|
|
4810
4851
|
updateChatBadge() {
|
|
4852
|
+
if (!this.chatOn) return;
|
|
4811
4853
|
const tab = this.tabList.findIndex((t) => t.id === "chat");
|
|
4812
4854
|
const buttons = this.shadow?.querySelectorAll(".tabs .tab");
|
|
4813
4855
|
const el2 = buttons?.[tab];
|
|
@@ -5131,7 +5173,7 @@ var Loupe = (() => {
|
|
|
5131
5173
|
}
|
|
5132
5174
|
}
|
|
5133
5175
|
buildActivityPanel() {
|
|
5134
|
-
this.activityEl.innerHTML = `<div class="hint-slot" id="loupe-ahint"></div><div class="mon-status" id="loupe-mon-status"><span class="mon-dot"></span><span class="mon-status-label"></span><span class="mon-spacer"></span><button class="mon-clear" data-role="mon-clear" title="Clear the feed">Clear</button></div><div class="mon-summary" id="loupe-mon-summary"></div><div class="mon-micro" id="loupe-mon-micro"></div><div class="mon-chips" id="loupe-mon-chips"></div><div class="mon-feed" id="loupe-mon-feed"></div>`;
|
|
5176
|
+
this.activityEl.innerHTML = `<div class="hint-slot" id="loupe-ahint"></div><div class="mon-status" id="loupe-mon-status"><span class="mon-dot"></span><span class="mon-status-label"></span><span class="mon-live" id="loupe-mon-live"></span><span class="mon-spacer"></span><button class="mon-clear" data-role="mon-clear" title="Clear the feed">Clear</button></div><div class="mon-summary" id="loupe-mon-summary"></div><div class="mon-micro" id="loupe-mon-micro"></div><div class="mon-chips" id="loupe-mon-chips"></div><div class="mon-feed" id="loupe-mon-feed"></div>`;
|
|
5135
5177
|
this.feedEl = this.activityEl.querySelector("#loupe-mon-feed");
|
|
5136
5178
|
this.activityHint = this.activityEl.querySelector("#loupe-ahint");
|
|
5137
5179
|
this.activityEl.querySelector('[data-role="mon-clear"]').onclick = () => this.clearActivity();
|
|
@@ -5169,6 +5211,52 @@ var Loupe = (() => {
|
|
|
5169
5211
|
this.activityStatus = "idle";
|
|
5170
5212
|
this.renderActivity();
|
|
5171
5213
|
}
|
|
5214
|
+
/**
|
|
5215
|
+
* Read the server's feed and merge it in by id. A backend without a feed answers
|
|
5216
|
+
* null once, and the panel stops asking.
|
|
5217
|
+
*/
|
|
5218
|
+
async loadActivity() {
|
|
5219
|
+
if (this.activityNone) return;
|
|
5220
|
+
let events;
|
|
5221
|
+
try {
|
|
5222
|
+
const read = this.store.listActivity?.bind(this.store);
|
|
5223
|
+
events = read ? await read(this.cfg.projectKey, this.activitySince || void 0) : null;
|
|
5224
|
+
} catch {
|
|
5225
|
+
if (this.activityRemote === "live") {
|
|
5226
|
+
this.activityRemote = "offline";
|
|
5227
|
+
this.renderActivity();
|
|
5228
|
+
}
|
|
5229
|
+
return;
|
|
5230
|
+
}
|
|
5231
|
+
if (events === null) {
|
|
5232
|
+
this.activityRemote = "unsupported";
|
|
5233
|
+
this.activityNone = true;
|
|
5234
|
+
this.syncActivityPoll();
|
|
5235
|
+
this.renderActivity();
|
|
5236
|
+
return;
|
|
5237
|
+
}
|
|
5238
|
+
this.activityRemote = "live";
|
|
5239
|
+
const known = new Set(this.activityEvents.map((e) => e.id));
|
|
5240
|
+
const fresh = events.filter((e) => e && typeof e.id === "string" && !known.has(e.id));
|
|
5241
|
+
if (fresh.length) {
|
|
5242
|
+
this.activityEvents = [...this.activityEvents, ...fresh].sort((a, b) => a.at.localeCompare(b.at)).slice(-500);
|
|
5243
|
+
const newest = fresh.reduce((m, e) => e.at > m ? e.at : m, this.activitySince);
|
|
5244
|
+
this.activitySince = newest;
|
|
5245
|
+
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1e3).toISOString();
|
|
5246
|
+
if (fresh.some((e) => e.level === "error" && e.at >= tenMinutesAgo)) this.activityStatus = "error";
|
|
5247
|
+
}
|
|
5248
|
+
this.renderActivity();
|
|
5249
|
+
}
|
|
5250
|
+
/** Poll the server's feed every 15s, but only while the Activity view is on screen. */
|
|
5251
|
+
syncActivityPoll() {
|
|
5252
|
+
const want = this.tab === "activity" && !this.activityNone && !!this.store.listActivity;
|
|
5253
|
+
if (want && !this.activityPoll) {
|
|
5254
|
+
this.activityPoll = setInterval(() => void this.loadActivity(), 15e3);
|
|
5255
|
+
} else if (!want && this.activityPoll) {
|
|
5256
|
+
clearInterval(this.activityPoll);
|
|
5257
|
+
this.activityPoll = void 0;
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
5172
5260
|
/** The summary, status row, micro-stats and chips — derived, never stored. */
|
|
5173
5261
|
renderActivityChrome() {
|
|
5174
5262
|
if (!this.activityEl) return;
|
|
@@ -5177,6 +5265,11 @@ var Loupe = (() => {
|
|
|
5177
5265
|
const s = summarizeActivity(this.activityEvents, this.activityStatus);
|
|
5178
5266
|
status.querySelector(".mon-status-label").textContent = ACTIVITY_STATUS_LABELS[s.status];
|
|
5179
5267
|
status.className = `mon-status st-${s.status}`;
|
|
5268
|
+
const live = status.querySelector("#loupe-mon-live");
|
|
5269
|
+
if (live) {
|
|
5270
|
+
live.textContent = this.activityRemote === "live" ? "Live" : this.activityRemote === "offline" ? "Offline" : "";
|
|
5271
|
+
live.className = `mon-live ${this.activityRemote}`;
|
|
5272
|
+
}
|
|
5180
5273
|
const summary = this.activityEl.querySelector("#loupe-mon-summary");
|
|
5181
5274
|
if (!s.events) {
|
|
5182
5275
|
summary.innerHTML = "";
|
|
@@ -5230,7 +5323,7 @@ var Loupe = (() => {
|
|
|
5230
5323
|
if (!this.feedEl) return;
|
|
5231
5324
|
const events = this.activityFilter ? this.activityEvents.filter((e) => e.kind === this.activityFilter) : this.activityEvents;
|
|
5232
5325
|
if (!events.length) {
|
|
5233
|
-
this.feedEl.innerHTML = `<div class="mon-empty">` + (this.activityEvents.length ? `Nothing from <b>${escapeHtml(this.activityFilter)}</b> yet.` : `<b>Monitor unavailable.</b> Nothing is feeding this view yet.<br>A bridge or your app can push events with <code>Loupe.trackActivity({ kind, label })</code>, and Loupe reports its own operations here as you work.`) + `</div>`;
|
|
5326
|
+
this.feedEl.innerHTML = `<div class="mon-empty">` + (this.activityEvents.length ? `Nothing from <b>${escapeHtml(this.activityFilter)}</b> yet.` : this.activityRemote === "live" ? `No activity yet. Comments, status changes and forwarded tickets appear here.` : `<b>Monitor unavailable.</b> Nothing is feeding this view yet.<br>A bridge or your app can push events with <code>Loupe.trackActivity({ kind, label })</code>, and Loupe reports its own operations here as you work.`) + `</div>`;
|
|
5234
5327
|
return;
|
|
5235
5328
|
}
|
|
5236
5329
|
this.feedEl.innerHTML = events.map((e) => {
|
|
@@ -5246,7 +5339,7 @@ var Loupe = (() => {
|
|
|
5246
5339
|
*/
|
|
5247
5340
|
buildHomePanel() {
|
|
5248
5341
|
const title = this.cfg.label ?? "Loupe";
|
|
5249
|
-
this.homeEl.innerHTML = `<div class="hint-slot" id="loupe-hhint"></div><div class="hstat" id="loupe-hstats"></div><div class="hscope"><button class="hscope-b" data-scope="page">This page <b class="hscope-n"></b></button><button class="hscope-b" data-scope="all">All <b class="hscope-n"></b></button><button class="hrefresh" title="Refresh" aria-label="Refresh">\u27F3</button></div><button class="hpin" data-role="home-pin">\u271B Pin feedback on this page</button><div class="projbar"><span class="proj-label">Project</span><button class="proj-chip" data-role="proj-open" aria-haspopup="dialog" aria-expanded="false"><span class="proj-
|
|
5342
|
+
this.homeEl.innerHTML = `<div class="hint-slot" id="loupe-hhint"></div><div class="hstat" id="loupe-hstats"></div><div class="hscope"><button class="hscope-b" data-scope="page">This page <b class="hscope-n"></b></button><button class="hscope-b" data-scope="all">All <b class="hscope-n"></b></button><button class="hrefresh" title="Refresh" aria-label="Refresh">\u27F3</button></div><button class="hpin" data-role="home-pin">\u271B Pin feedback on this page</button><div class="projbar"><span class="proj-label">Project</span><button class="proj-chip" data-role="proj-open" aria-haspopup="dialog" aria-expanded="false"><span class="proj-name"></span><span class="proj-caret">\u25BE</span></button><div class="proj-pop" id="loupe-proj" role="dialog" aria-label="Project settings"></div></div><div class="hnotif" id="loupe-hnotif"></div><div class="hlabel">Recent</div><div class="hfeed" id="loupe-hfeed"></div><div class="hfoot">${escapeHtml(title)} \xB7 <span class="hver">v${escapeHtml(SDK_VERSION)}</span>${this.packageVersionNote()}</div>`;
|
|
5250
5343
|
this.homeEl.querySelector('[data-role="proj-open"]').onclick = (e) => {
|
|
5251
5344
|
e.stopPropagation();
|
|
5252
5345
|
this.setProjectOpen(!this.projOpen);
|
|
@@ -5398,65 +5491,36 @@ var Loupe = (() => {
|
|
|
5398
5491
|
};
|
|
5399
5492
|
}
|
|
5400
5493
|
// ---- project manager (#60) ------------------------------------------------
|
|
5401
|
-
/** The repo new comments are filed against: the panel's choice, else the config. */
|
|
5402
|
-
effectiveRepo() {
|
|
5403
|
-
return this.project.repo || this.cfg.repo || void 0;
|
|
5404
|
-
}
|
|
5405
5494
|
setProjectOpen(open) {
|
|
5406
5495
|
this.projOpen = open;
|
|
5407
5496
|
this.projError = "";
|
|
5408
|
-
if (open) {
|
|
5409
|
-
this.projSearch = "";
|
|
5410
|
-
void this.refreshRepos();
|
|
5411
|
-
}
|
|
5412
5497
|
this.renderProject();
|
|
5413
5498
|
}
|
|
5414
|
-
/**
|
|
5415
|
-
async
|
|
5416
|
-
const source = this.cfg.repos;
|
|
5417
|
-
if (!source) return [];
|
|
5418
|
-
if (Array.isArray(source)) {
|
|
5419
|
-
const q = this.projSearch.trim().toLowerCase();
|
|
5420
|
-
const all = q ? source.filter((r) => r.toLowerCase().includes(q)) : source;
|
|
5421
|
-
return all.slice(0, 50);
|
|
5422
|
-
}
|
|
5499
|
+
/** Read the organization once. A backend without the endpoint, or offline mode, leaves it null. */
|
|
5500
|
+
async loadOrg() {
|
|
5423
5501
|
try {
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
this.projError = e instanceof Error ? e.message : "Could not load repositories.";
|
|
5428
|
-
return [];
|
|
5502
|
+
this.org = await this.store.getOrg?.() ?? null;
|
|
5503
|
+
} catch {
|
|
5504
|
+
this.org = null;
|
|
5429
5505
|
}
|
|
5506
|
+
this.renderProject();
|
|
5430
5507
|
}
|
|
5431
|
-
/**
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
this.projError = "";
|
|
5437
|
-
const seq = ++this.repoSeq;
|
|
5438
|
-
const out = await this.repoChoices();
|
|
5439
|
-
if (seq !== this.repoSeq) return;
|
|
5440
|
-
this.projResults = out;
|
|
5441
|
-
this.renderRepoList();
|
|
5442
|
-
this.renderProjectError();
|
|
5508
|
+
/** "Project · Organization", or the project key when there is no organization to show. */
|
|
5509
|
+
projectLabel() {
|
|
5510
|
+
const org = this.org;
|
|
5511
|
+
const name = org?.project.name || this.cfg.projectKey;
|
|
5512
|
+
return org?.organization ? `${name} \xB7 ${org.organization.name}` : name;
|
|
5443
5513
|
}
|
|
5444
|
-
/**
|
|
5445
|
-
|
|
5446
|
-
const
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
this.renderProject();
|
|
5455
|
-
this.renderPins();
|
|
5456
|
-
this.renderList();
|
|
5457
|
-
this.addActivity({ kind: "repo.link", label: `Linked this page to ${b.dataset.repo}` });
|
|
5458
|
-
};
|
|
5459
|
-
});
|
|
5514
|
+
/** The Organization section of the project popover. Read-only: Hub owns this. */
|
|
5515
|
+
orgSectionHtml() {
|
|
5516
|
+
const org = this.org;
|
|
5517
|
+
const head = `<div class="pp-head"><span>Organization</span><button class="pp-x" data-role="pp-close" aria-label="Close">\u2715</button></div>`;
|
|
5518
|
+
if (!org?.organization) {
|
|
5519
|
+
return head + `<div class="pp-cur">` + (org?.error ? "Could not reach Loupe Hub. Your organization's projects will show here when it is back." : "This project is not connected to an organization. Connect it to Loupe Hub to see the organization's projects and where its tickets go.") + `</div>`;
|
|
5520
|
+
}
|
|
5521
|
+
const dest = org.project.destination;
|
|
5522
|
+
const others = org.projects;
|
|
5523
|
+
return head + `<div class="pp-cur"><b>${escapeHtml(org.project.name || this.cfg.projectKey)}</b> in <b>${escapeHtml(org.organization.name)}</b>.</div><div class="pp-cur pp-dest">${dest ? `Tickets go to <b>${escapeHtml(dest.name)}</b>.` : "Tickets stay in this project."}</div><div class="pp-list pp-org">` + (others.length ? others.map((p) => `<div class="pp-proj${p.isDestination ? " on" : ""}"><span class="pp-proj-n">${escapeHtml(p.name)}</span>` + (p.receives ? `<span class="pp-badge">receives tickets</span>` : "") + `</div>`).join("") : `<div class="pp-empty">No other projects in this organization yet.</div>`) + `</div>`;
|
|
5460
5524
|
}
|
|
5461
5525
|
renderProjectError() {
|
|
5462
5526
|
const box = this.homeEl?.querySelector("#loupe-pp-err");
|
|
@@ -5469,9 +5533,8 @@ var Loupe = (() => {
|
|
|
5469
5533
|
const chip = this.homeEl.querySelector(".proj-chip");
|
|
5470
5534
|
const pop = this.homeEl.querySelector("#loupe-proj");
|
|
5471
5535
|
if (!chip || !pop) return;
|
|
5472
|
-
|
|
5473
|
-
chip.
|
|
5474
|
-
chip.classList.toggle("unset", !repo);
|
|
5536
|
+
chip.querySelector(".proj-name").textContent = this.projectLabel();
|
|
5537
|
+
chip.classList.toggle("unset", !this.org?.organization);
|
|
5475
5538
|
chip.setAttribute("aria-expanded", String(this.projOpen));
|
|
5476
5539
|
if (!this.projOpen) {
|
|
5477
5540
|
pop.classList.remove("open");
|
|
@@ -5480,25 +5543,9 @@ var Loupe = (() => {
|
|
|
5480
5543
|
}
|
|
5481
5544
|
pop.classList.add("open");
|
|
5482
5545
|
const envs = this.project.environments;
|
|
5483
|
-
pop.innerHTML =
|
|
5484
|
-
this.renderRepoList();
|
|
5546
|
+
pop.innerHTML = this.orgSectionHtml() + `<div class="pp-sep"></div><div class="pp-head"><span>Environments</span></div><div class="pp-list">` + (envs.length ? envs.map((u, i) => `<div class="pp-env"><span class="pp-env-u" title="${escapeAttr(u)}">${escapeHtml(u)}</span><button class="pp-x" data-env-rm="${i}" aria-label="Remove environment">\u2715</button></div>`).join("") : `<div class="pp-empty">No environment URLs yet.</div>`) + `</div><div class="pp-add"><input class="pp-env-url" type="url" placeholder="https://staging.example.com" value="${escapeAttr(this.envDraft)}" aria-label="Environment URL"><button class="pp-add-b" data-role="env-add">Add</button></div><div class="pp-sep"></div><div class="pp-head"><span>Local AI</span></div><div class="pp-empty">Any OpenAI-compatible server \u2014 used by the Generate pane.</div><div class="pp-add"><input class="pp-ai-url" type="url" placeholder="http://localhost:11434" value="${escapeAttr(this.project.localAi?.url ?? "")}" aria-label="Local AI endpoint"></div><div class="pp-add"><input class="pp-ai-model" type="text" placeholder="llama3.2" value="${escapeAttr(this.project.localAi?.model ?? "")}" aria-label="Model name"><button class="pp-add-b" data-role="ai-save">Save</button></div><div class="pp-add"><button class="pp-clear" data-role="ai-test">Test connection</button></div><div class="pp-ai-out" id="loupe-pp-ai"></div><div class="pp-err" id="loupe-pp-err"></div>`;
|
|
5485
5547
|
this.renderProjectError();
|
|
5486
5548
|
pop.querySelector('[data-role="pp-close"]').onclick = () => this.setProjectOpen(false);
|
|
5487
|
-
pop.querySelector('[data-role="pp-clear"]')?.addEventListener("click", () => {
|
|
5488
|
-
this.project.repo = void 0;
|
|
5489
|
-
this.saveProject();
|
|
5490
|
-
this.renderProject();
|
|
5491
|
-
this.renderPins();
|
|
5492
|
-
this.renderList();
|
|
5493
|
-
});
|
|
5494
|
-
const search = pop.querySelector(".pp-search");
|
|
5495
|
-
if (search) {
|
|
5496
|
-
search.addEventListener("click", (e) => e.stopPropagation());
|
|
5497
|
-
search.oninput = () => {
|
|
5498
|
-
this.projSearch = search.value;
|
|
5499
|
-
void this.refreshRepos();
|
|
5500
|
-
};
|
|
5501
|
-
}
|
|
5502
5549
|
pop.querySelectorAll("[data-env-rm]").forEach((b) => {
|
|
5503
5550
|
b.onclick = () => {
|
|
5504
5551
|
const i = Number(b.dataset.envRm);
|
|
@@ -5562,10 +5609,9 @@ var Loupe = (() => {
|
|
|
5562
5609
|
try {
|
|
5563
5610
|
const raw = localStorage.getItem(`loupe:project:${this.cfg.projectKey}`);
|
|
5564
5611
|
const p = raw ? JSON.parse(raw) : null;
|
|
5565
|
-
const repo = typeof p?.repo === "string" && p.repo ? p.repo : void 0;
|
|
5566
5612
|
const stored = Array.isArray(p?.environments) ? p.environments.filter((u) => typeof u === "string") : null;
|
|
5567
5613
|
const localAi = p?.localAi && typeof p.localAi.url === "string" && typeof p.localAi.model === "string" ? { url: p.localAi.url, model: p.localAi.model } : void 0;
|
|
5568
|
-
this.project = {
|
|
5614
|
+
this.project = { environments: stored ?? fromConfig, localAi };
|
|
5569
5615
|
} catch {
|
|
5570
5616
|
this.project = { environments: fromConfig };
|
|
5571
5617
|
}
|
|
@@ -6164,10 +6210,6 @@ var Loupe = (() => {
|
|
|
6164
6210
|
status: "queue",
|
|
6165
6211
|
priority,
|
|
6166
6212
|
changeType,
|
|
6167
|
-
// Branch-aware threads: the host declares these once in `init()`, and the
|
|
6168
|
-
// panel's project manager can override the repo per browser.
|
|
6169
|
-
repo: this.effectiveRepo(),
|
|
6170
|
-
branch: this.cfg.branch,
|
|
6171
6213
|
kind: target.kind,
|
|
6172
6214
|
anchor,
|
|
6173
6215
|
context,
|
|
@@ -6198,8 +6240,7 @@ var Loupe = (() => {
|
|
|
6198
6240
|
this.addActivity({
|
|
6199
6241
|
kind: "comment.create",
|
|
6200
6242
|
label: `Created \u201C${comment.title || comment.body.split("\n")[0] || "comment"}\u201D`,
|
|
6201
|
-
detail: [normalizePriority(comment.priority), comment.kind ?? "element", comment.
|
|
6202
|
-
files: comment.repo ? [`${comment.repo}/${shortPath(comment.url)}`] : []
|
|
6243
|
+
detail: [normalizePriority(comment.priority), comment.kind ?? "element", shortPath(comment.url)].join(" \xB7 ")
|
|
6203
6244
|
});
|
|
6204
6245
|
}
|
|
6205
6246
|
/** Upload the reporter's picked files. A file that fails is skipped, not fatal. */
|
|
@@ -6224,6 +6265,7 @@ var Loupe = (() => {
|
|
|
6224
6265
|
}
|
|
6225
6266
|
}
|
|
6226
6267
|
this.comments.forEach((c, i) => {
|
|
6268
|
+
if (c.source) return;
|
|
6227
6269
|
let pin = this.pins.get(c.id);
|
|
6228
6270
|
if (!pin) {
|
|
6229
6271
|
pin = el("button", "pin");
|
|
@@ -6305,6 +6347,7 @@ var Loupe = (() => {
|
|
|
6305
6347
|
/** Switch the sidebar page (Home ↔ Comments ↔ Connect Claude). */
|
|
6306
6348
|
setTab(tab) {
|
|
6307
6349
|
if (this.tab === tab) return;
|
|
6350
|
+
if (tab === "chat" && !this.chatOn) return;
|
|
6308
6351
|
if (tab !== "comments") this.setMode("off");
|
|
6309
6352
|
this.tab = tab;
|
|
6310
6353
|
this.hintFor = null;
|
|
@@ -6314,7 +6357,11 @@ var Loupe = (() => {
|
|
|
6314
6357
|
if (this.scope === "all" && !this.allComments.length) void this.loadAllComments();
|
|
6315
6358
|
}
|
|
6316
6359
|
if (tab === "comments") this.renderList();
|
|
6317
|
-
if (tab === "activity")
|
|
6360
|
+
if (tab === "activity") {
|
|
6361
|
+
this.renderActivity();
|
|
6362
|
+
void this.loadActivity();
|
|
6363
|
+
}
|
|
6364
|
+
this.syncActivityPoll();
|
|
6318
6365
|
this.applyDockLayout();
|
|
6319
6366
|
}
|
|
6320
6367
|
toggleTheme() {
|
|
@@ -6501,11 +6548,10 @@ var Loupe = (() => {
|
|
|
6501
6548
|
if (DOCK_MODES.includes(p?.mode)) this.dockMode = p.mode;
|
|
6502
6549
|
if (typeof p?.open === "boolean") this.open = p.open;
|
|
6503
6550
|
if (p?.theme === "light" || p?.theme === "dark") this.theme = p.theme;
|
|
6504
|
-
const known = [...BUILTIN_TABS.map((t) => t.id), ...(this.cfg.tabs ?? []).map((t) => t.id)];
|
|
6551
|
+
const known = [...BUILTIN_TABS.map((t) => t.id).filter((id) => id !== "chat" || this.chatOn), ...(this.cfg.tabs ?? []).map((t) => t.id)];
|
|
6505
6552
|
if (typeof p?.tab === "string" && known.includes(p.tab)) this.tab = p.tab;
|
|
6506
6553
|
if (p?.scope === "page" || p?.scope === "all") this.scope = p.scope;
|
|
6507
6554
|
if (typeof p?.statFilter === "string") this.statFilter = p.statFilter;
|
|
6508
|
-
if (typeof p?.repoFilter === "string") this.repoFilter = p.repoFilter;
|
|
6509
6555
|
if (p?.float && typeof p.float.w === "number") this.floatRect = { ...this.floatRect, ...p.float };
|
|
6510
6556
|
if (typeof p?.markersHidden === "boolean") this.markersHidden = p.markersHidden;
|
|
6511
6557
|
if (typeof p?.launcherHidden === "boolean") this.launcherHidden = p.launcherHidden;
|
|
@@ -6532,7 +6578,6 @@ var Loupe = (() => {
|
|
|
6532
6578
|
fab: this.fabPos,
|
|
6533
6579
|
scope: this.scope,
|
|
6534
6580
|
statFilter: this.statFilter,
|
|
6535
|
-
repoFilter: this.repoFilter,
|
|
6536
6581
|
accent: this.accent,
|
|
6537
6582
|
minimized: this.minimized,
|
|
6538
6583
|
hoverHints: this.hoverHints,
|
|
@@ -6615,11 +6660,9 @@ var Loupe = (() => {
|
|
|
6615
6660
|
if (this.statFilter === "needs_you" && !needsYou({ status: c.status }).needs) return false;
|
|
6616
6661
|
if (this.statFilter === "resolved" && stage !== "resolved") return false;
|
|
6617
6662
|
if (this.statFilter === "stale" && (stage === "resolved" || !(Date.parse(c.createdAt) < weekAgo))) return false;
|
|
6618
|
-
if (this.repoFilter && c.repo !== this.repoFilter) return false;
|
|
6619
6663
|
return !q || `${c.title ?? ""} ${c.body} ${c.author?.name ?? ""}`.toLowerCase().includes(q);
|
|
6620
6664
|
});
|
|
6621
6665
|
if (this.scope === "all") items = [...items].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
6622
|
-
this.renderRepoFilter();
|
|
6623
6666
|
this.renderReviewBar();
|
|
6624
6667
|
if (!items.length) {
|
|
6625
6668
|
this.listEl.appendChild(el(
|
|
@@ -6641,15 +6684,6 @@ var Loupe = (() => {
|
|
|
6641
6684
|
});
|
|
6642
6685
|
this.updateCount(items.length);
|
|
6643
6686
|
}
|
|
6644
|
-
/** Repo filter — only offered in the project scope, where it means something. */
|
|
6645
|
-
renderRepoFilter() {
|
|
6646
|
-
if (!this.repoSel) return;
|
|
6647
|
-
const repos = [...new Set(this.visibleComments.map((c) => c.repo).filter((r) => !!r))].sort();
|
|
6648
|
-
this.repoSel.style.display = this.scope === "all" && repos.length > 1 ? "" : "none";
|
|
6649
|
-
const current2 = this.repoFilter;
|
|
6650
|
-
this.repoSel.innerHTML = `<option value="">All repos</option>` + repos.map((r) => `<option value="${escapeHtml(r)}">${escapeHtml(r)}</option>`).join("");
|
|
6651
|
-
this.repoSel.value = current2;
|
|
6652
|
-
}
|
|
6653
6687
|
itemView(c, i) {
|
|
6654
6688
|
const detached = this.pins.get(c.id)?.classList.contains("detached") && !isResolved(c);
|
|
6655
6689
|
const open = this.expanded.has(c.id);
|
|
@@ -6664,6 +6698,19 @@ var Loupe = (() => {
|
|
|
6664
6698
|
const icon = vw < 768 ? "\u{1F4F1}" : vw < 1024 ? "\u25A6" : "\u{1F5A5}";
|
|
6665
6699
|
top.appendChild(el("span", "device", `${icon} ${kind}`));
|
|
6666
6700
|
}
|
|
6701
|
+
if (c.source) {
|
|
6702
|
+
const from = el("span", "srcchip", `from ${c.source.projectName || c.source.projectId}`);
|
|
6703
|
+
from.title = c.source.reporter ? `Sent by ${c.source.reporter.name || c.source.reporter.email}` : "Sent from another project";
|
|
6704
|
+
top.appendChild(from);
|
|
6705
|
+
}
|
|
6706
|
+
const fwd = c.forwarded;
|
|
6707
|
+
if (fwd && fwd.status !== "none") {
|
|
6708
|
+
const where = fwd.destinationName || (fwd.status === "ok" ? "webhook" : "Hub");
|
|
6709
|
+
const ok = fwd.status === "ok";
|
|
6710
|
+
const chip = el("span", ok ? "fwdchip" : "fwdchip bad", ok ? `\u2192 ${where}` : `\u2192 ${where} failed`);
|
|
6711
|
+
chip.title = ok ? `Sent to ${where}` : `Could not send to ${where}${fwd.error ? `: ${fwd.error}` : ""}`;
|
|
6712
|
+
top.appendChild(chip);
|
|
6713
|
+
}
|
|
6667
6714
|
if (isResolved(c)) top.appendChild(el("span", "badge done", "resolved"));
|
|
6668
6715
|
else if (detached) {
|
|
6669
6716
|
const b = el("span", "badge detached", "moved");
|
|
@@ -7839,6 +7886,8 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
7839
7886
|
this.companionSource?.close();
|
|
7840
7887
|
this.companionSource = void 0;
|
|
7841
7888
|
if (this.companionPoll) clearInterval(this.companionPoll);
|
|
7889
|
+
if (this.activityPoll) clearInterval(this.activityPoll);
|
|
7890
|
+
this.activityPoll = void 0;
|
|
7842
7891
|
this.stopVoice();
|
|
7843
7892
|
this.stopPresence();
|
|
7844
7893
|
this.stopRecording?.();
|