@loupekit/sdk 0.11.1 → 0.12.0-next.78
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.js
CHANGED
|
@@ -341,6 +341,8 @@ var STYLES = (
|
|
|
341
341
|
}
|
|
342
342
|
.tabs .tab:hover { color: var(--ink); background: var(--bg-2); }
|
|
343
343
|
.tabs .tab.on { color: var(--accent); border-bottom-color: var(--accent); }
|
|
344
|
+
.tabs .tab.off { opacity: .45; cursor: not-allowed; }
|
|
345
|
+
.tabs .tab.off:hover { color: inherit; background: transparent; }
|
|
344
346
|
|
|
345
347
|
/* Only the active page shows. Driven by an "on" class rather than a .tab-<id>
|
|
346
348
|
selector, so host-registered tab ids need no CSS of their own. */
|
|
@@ -399,11 +401,6 @@ var STYLES = (
|
|
|
399
401
|
font-size: 10.5px; font-family: ui-monospace, Menlo, monospace;
|
|
400
402
|
max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
401
403
|
}
|
|
402
|
-
.listhead .reposel {
|
|
403
|
-
margin-left: 6px; font-size: 11px; padding: 3px 6px; border: 1px solid var(--line);
|
|
404
|
-
border-radius: 7px; background: var(--bg-2); color: var(--ink);
|
|
405
|
-
text-transform: none; letter-spacing: 0; max-width: 130px;
|
|
406
|
-
}
|
|
407
404
|
|
|
408
405
|
/* recording marker + video in the list */
|
|
409
406
|
.item .rectag { font-size: 10px; font-weight: 700; color: var(--pin); background: var(--bg-3); border-radius: 999px; padding: 1px 7px; white-space: nowrap; }
|
|
@@ -517,6 +514,9 @@ var STYLES = (
|
|
|
517
514
|
.activity-view { padding: 10px 12px 16px; }
|
|
518
515
|
.mon-status { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight: 600; color: var(--ink); }
|
|
519
516
|
.mon-spacer { flex: 1; }
|
|
517
|
+
.mon-live { font-size: 10px; font-weight: 600; color: var(--muted); }
|
|
518
|
+
.mon-live.live { color: #2f9e6a; }
|
|
519
|
+
.mon-live.offline { color: #d9534f; }
|
|
520
520
|
.mon-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: none; }
|
|
521
521
|
.mon-status.st-working .mon-dot { background: var(--accent); animation: loupe-pulse 1.4s ease-in-out infinite; }
|
|
522
522
|
.mon-status.st-error .mon-dot { background: var(--pin); }
|
|
@@ -580,8 +580,7 @@ var STYLES = (
|
|
|
580
580
|
color: var(--ink); font-size: 11.5px; font-family: ui-monospace, Menlo, monospace; cursor: pointer;
|
|
581
581
|
}
|
|
582
582
|
.proj-chip:hover { border-color: var(--accent); }
|
|
583
|
-
.proj-chip .proj-
|
|
584
|
-
.proj-chip.unset .proj-repo { color: var(--muted); font-style: italic; }
|
|
583
|
+
.proj-chip .proj-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; }
|
|
585
584
|
.proj-caret { color: var(--muted); font-family: inherit; }
|
|
586
585
|
.proj-pop {
|
|
587
586
|
position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 25;
|
|
@@ -594,19 +593,17 @@ var STYLES = (
|
|
|
594
593
|
.pp-x:hover { background: var(--bg-3); color: var(--ink); }
|
|
595
594
|
.pp-cur { margin-bottom: 8px; font-size: 11.5px; line-height: 1.45; color: var(--muted); }
|
|
596
595
|
.pp-cur b { color: var(--ink); font-family: ui-monospace, Menlo, monospace; }
|
|
597
|
-
.pp-search {
|
|
598
|
-
width: 100%; margin-bottom: 6px; padding: 6px 8px; border: 1px solid var(--line);
|
|
599
|
-
border-radius: 8px; background: var(--bg); color: var(--ink); font-size: 12px;
|
|
600
|
-
}
|
|
601
|
-
.pp-search:focus { outline: none; border-color: var(--accent); }
|
|
602
596
|
.pp-list { display: flex; flex-direction: column; gap: 2px; max-height: 30vh; overflow-y: auto; }
|
|
603
|
-
.pp-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
597
|
+
.pp-proj {
|
|
598
|
+
display: flex; align-items: center; gap: 6px; padding: 5px 8px; border: 1px solid transparent;
|
|
599
|
+
border-radius: 7px; font-size: 11.5px; color: var(--ink);
|
|
600
|
+
}
|
|
601
|
+
.pp-proj.on { border-color: var(--accent); }
|
|
602
|
+
.pp-proj-n { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
603
|
+
.pp-badge {
|
|
604
|
+
flex: none; padding: 1px 6px; border-radius: 999px; background: var(--bg-3);
|
|
605
|
+
color: var(--muted); font-size: 10px; white-space: nowrap;
|
|
607
606
|
}
|
|
608
|
-
.pp-item:hover { background: var(--bg-3); }
|
|
609
|
-
.pp-item.on { border-color: var(--accent); color: var(--accent); }
|
|
610
607
|
.pp-empty { padding: 8px 6px; font-size: 11px; line-height: 1.5; color: var(--muted); }
|
|
611
608
|
.pp-empty code { font-family: ui-monospace, Menlo, monospace; color: var(--ink); }
|
|
612
609
|
.pp-clear {
|
|
@@ -640,6 +637,15 @@ var STYLES = (
|
|
|
640
637
|
/* The host package's version, shown only when it differs from this bundle's (a stale publish). */
|
|
641
638
|
.ver-stale { color: var(--pin); font-family: ui-monospace, Menlo, monospace; font-weight: 600; cursor: help; }
|
|
642
639
|
|
|
640
|
+
/* ------------------------------------------------ tickets between projects */
|
|
641
|
+
.srcchip, .fwdchip {
|
|
642
|
+
padding: 1px 6px; border-radius: 999px; white-space: nowrap; font-size: 10px;
|
|
643
|
+
border: 1px solid var(--line); background: var(--bg); color: var(--muted);
|
|
644
|
+
max-width: 140px; overflow: hidden; text-overflow: ellipsis;
|
|
645
|
+
}
|
|
646
|
+
.fwdchip { border-color: var(--accent); color: var(--accent); }
|
|
647
|
+
.fwdchip.bad { border-color: #d9534f; color: #d9534f; }
|
|
648
|
+
|
|
643
649
|
/* ------------------------------------------------ lifecycle chips + review flow */
|
|
644
650
|
.lifechip {
|
|
645
651
|
padding: 1px 6px; border-radius: 999px; white-space: nowrap;
|
|
@@ -3759,6 +3765,14 @@ var LocalStorageAdapter = class {
|
|
|
3759
3765
|
}
|
|
3760
3766
|
}
|
|
3761
3767
|
}
|
|
3768
|
+
/** Offline: there is no organization to read. */
|
|
3769
|
+
async getOrg() {
|
|
3770
|
+
return null;
|
|
3771
|
+
}
|
|
3772
|
+
/** Offline: no server feed; the panel shows only what this browser reports. */
|
|
3773
|
+
async listActivity(_projectKey, _since) {
|
|
3774
|
+
return null;
|
|
3775
|
+
}
|
|
3762
3776
|
};
|
|
3763
3777
|
|
|
3764
3778
|
// src/http-adapter.ts
|
|
@@ -3894,6 +3908,28 @@ var HttpAdapter = class {
|
|
|
3894
3908
|
}));
|
|
3895
3909
|
if (!res.ok) throw new Error(`update failed: ${res.status}`);
|
|
3896
3910
|
}
|
|
3911
|
+
/** The organization, read through the host app. A 404 means the backend has no such endpoint. */
|
|
3912
|
+
async getOrg() {
|
|
3913
|
+
const res = await fetch(`${this.base}/v1/org`, this.opts({ headers: this.headers() }));
|
|
3914
|
+
if (res.status === 404) return null;
|
|
3915
|
+
if (!res.ok) throw new Error(`getOrg failed: ${res.status}`);
|
|
3916
|
+
const body = await res.json();
|
|
3917
|
+
if (!body || typeof body !== "object" || !body.project || !Array.isArray(body.projects)) {
|
|
3918
|
+
throw new Error("getOrg: the answer was not an organization");
|
|
3919
|
+
}
|
|
3920
|
+
return body;
|
|
3921
|
+
}
|
|
3922
|
+
/** The server's activity feed. A 404 means the backend keeps none. */
|
|
3923
|
+
async listActivity(projectKey, since) {
|
|
3924
|
+
const q = new URLSearchParams({ projectKey });
|
|
3925
|
+
if (since) q.set("since", since);
|
|
3926
|
+
const res = await fetch(`${this.base}/v1/activity?${q}`, this.opts({ headers: this.headers() }));
|
|
3927
|
+
if (res.status === 404) return null;
|
|
3928
|
+
if (!res.ok) throw new Error(`listActivity failed: ${res.status}`);
|
|
3929
|
+
const body = await res.json();
|
|
3930
|
+
if (!Array.isArray(body)) throw new Error("listActivity: the answer was not a list");
|
|
3931
|
+
return body;
|
|
3932
|
+
}
|
|
3897
3933
|
async remove(id) {
|
|
3898
3934
|
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(id)}`, this.opts({
|
|
3899
3935
|
method: "DELETE",
|
|
@@ -3904,7 +3940,7 @@ var HttpAdapter = class {
|
|
|
3904
3940
|
};
|
|
3905
3941
|
|
|
3906
3942
|
// src/app.ts
|
|
3907
|
-
var SDK_VERSION = true ? "0.
|
|
3943
|
+
var SDK_VERSION = true ? "0.12.0" : "dev";
|
|
3908
3944
|
var FAB_SIZE = 46;
|
|
3909
3945
|
var FAB_DRAG_THRESHOLD = 6;
|
|
3910
3946
|
var LAUNCHER_SHORTCUT = "Alt+Shift+L";
|
|
@@ -3929,7 +3965,7 @@ var TOUR = [
|
|
|
3929
3965
|
sel: ".hscope",
|
|
3930
3966
|
tab: "home",
|
|
3931
3967
|
title: "This page, or the whole project",
|
|
3932
|
-
body: "Switch to All to see every page's feedback as a day-grouped timeline
|
|
3968
|
+
body: "Switch to All to see every page's feedback as a day-grouped timeline."
|
|
3933
3969
|
},
|
|
3934
3970
|
{
|
|
3935
3971
|
sel: ".tools",
|
|
@@ -3941,7 +3977,7 @@ var TOUR = [
|
|
|
3941
3977
|
sel: '.tabs [data-tab="activity"]',
|
|
3942
3978
|
tab: "activity",
|
|
3943
3979
|
title: "Watch the work happen",
|
|
3944
|
-
body: "
|
|
3980
|
+
body: "Comments, status changes and forwarded tickets land here as they happen, with anything a bridge or your app reports. Tool chips filter it."
|
|
3945
3981
|
},
|
|
3946
3982
|
{
|
|
3947
3983
|
sel: '.dctl [data-role="settings"]',
|
|
@@ -4058,8 +4094,6 @@ var LoupeApp = class {
|
|
|
4058
4094
|
this.allLoaded = false;
|
|
4059
4095
|
/** A clicked stat tile, which narrows the list. */
|
|
4060
4096
|
this.statFilter = "";
|
|
4061
|
-
/** Repo filter, offered once the scope is "all". */
|
|
4062
|
-
this.repoFilter = "";
|
|
4063
4097
|
/** Accent preset id (see ACCENTS), applied as inline --accent / --accent-soft. */
|
|
4064
4098
|
this.accent = "indigo";
|
|
4065
4099
|
/** Collapsed to the one-line minimize bar. */
|
|
@@ -4083,8 +4117,16 @@ var LoupeApp = class {
|
|
|
4083
4117
|
this.activityFollow = true;
|
|
4084
4118
|
/** The summary card's key/value rows are behind this toggle. */
|
|
4085
4119
|
this.summaryOpen = false;
|
|
4086
|
-
/** Per-browser project settings:
|
|
4120
|
+
/** Per-browser project settings: the environment URLs and the Local AI endpoint. */
|
|
4087
4121
|
this.project = { environments: [] };
|
|
4122
|
+
/** The organization, read once from the backend. Null offline or before it loads. */
|
|
4123
|
+
this.org = null;
|
|
4124
|
+
/** Whether the server keeps an activity feed, and whether the last read reached it. */
|
|
4125
|
+
this.activityRemote = "unsupported";
|
|
4126
|
+
/** Newest server event seen, so a poll asks only for what is new. */
|
|
4127
|
+
this.activitySince = "";
|
|
4128
|
+
/** The backend answered that it keeps no feed, so the panel stops asking. */
|
|
4129
|
+
this.activityNone = false;
|
|
4088
4130
|
/** Iteration history per thread — generating a change is a stack, not a one-shot. */
|
|
4089
4131
|
this.iterations = /* @__PURE__ */ new Map();
|
|
4090
4132
|
/** The thread whose generate pane is open, if any. */
|
|
@@ -4097,13 +4139,9 @@ var LoupeApp = class {
|
|
|
4097
4139
|
this.consent = emptyConsent();
|
|
4098
4140
|
/** The live SpeechRecognition instance while dictating, if any. */
|
|
4099
4141
|
this.voice = null;
|
|
4100
|
-
this.projSearch = "";
|
|
4101
4142
|
this.projOpen = false;
|
|
4102
|
-
this.projResults = [];
|
|
4103
4143
|
this.projError = "";
|
|
4104
4144
|
this.envDraft = "";
|
|
4105
|
-
/** Guards against a slow repo search overwriting a newer one. */
|
|
4106
|
-
this.repoSeq = 0;
|
|
4107
4145
|
/** Float-mode window geometry; (x<=0 && y<=0) → placed on first layout. */
|
|
4108
4146
|
this.floatRect = { x: 0, y: 0, w: 380, h: 540 };
|
|
4109
4147
|
this.floatDrag = null;
|
|
@@ -4448,6 +4486,10 @@ var LoupeApp = class {
|
|
|
4448
4486
|
get url() {
|
|
4449
4487
|
return location.pathname + location.search;
|
|
4450
4488
|
}
|
|
4489
|
+
/** Chat is experimental: the tab stays dimmed and inert unless the host opts in. */
|
|
4490
|
+
get chatOn() {
|
|
4491
|
+
return this.cfg.chat === true;
|
|
4492
|
+
}
|
|
4451
4493
|
async start() {
|
|
4452
4494
|
this.loadState();
|
|
4453
4495
|
this.loadProject();
|
|
@@ -4464,8 +4506,11 @@ var LoupeApp = class {
|
|
|
4464
4506
|
this.renderHome();
|
|
4465
4507
|
this.startPresence();
|
|
4466
4508
|
this.startLiveThreads();
|
|
4467
|
-
this.startCompanionStream();
|
|
4509
|
+
if (this.chatOn) this.startCompanionStream();
|
|
4468
4510
|
void this.loadNotifications();
|
|
4511
|
+
void this.loadOrg();
|
|
4512
|
+
void this.loadActivity();
|
|
4513
|
+
this.syncActivityPoll();
|
|
4469
4514
|
void this.loadAllComments();
|
|
4470
4515
|
this.observe();
|
|
4471
4516
|
this.watchNavigation();
|
|
@@ -4636,8 +4681,13 @@ var LoupeApp = class {
|
|
|
4636
4681
|
this.tabList = [...BUILTIN_TABS, ...(this.cfg.tabs ?? []).map((t) => ({ id: t.id, label: t.label }))];
|
|
4637
4682
|
const tabs = el("div", "tabs");
|
|
4638
4683
|
for (const t of this.tabList) {
|
|
4639
|
-
const
|
|
4684
|
+
const off = t.id === "chat" && !this.chatOn;
|
|
4685
|
+
const b = el("button", off ? "tab off" : "tab", t.label);
|
|
4640
4686
|
b.dataset.tab = t.id;
|
|
4687
|
+
if (off) {
|
|
4688
|
+
b.setAttribute("aria-disabled", "true");
|
|
4689
|
+
b.title = "Chat \u2014 coming soon";
|
|
4690
|
+
}
|
|
4641
4691
|
b.onclick = () => this.setTab(t.id);
|
|
4642
4692
|
tabs.appendChild(b);
|
|
4643
4693
|
}
|
|
@@ -4677,15 +4727,6 @@ var LoupeApp = class {
|
|
|
4677
4727
|
this.renderList();
|
|
4678
4728
|
};
|
|
4679
4729
|
listHead.appendChild(search);
|
|
4680
|
-
this.repoSel = el("select", "reposel");
|
|
4681
|
-
this.repoSel.title = "Filter by repository";
|
|
4682
|
-
this.repoSel.setAttribute("aria-label", "Filter by repository");
|
|
4683
|
-
this.repoSel.onchange = () => {
|
|
4684
|
-
this.repoFilter = this.repoSel.value;
|
|
4685
|
-
this.renderList();
|
|
4686
|
-
};
|
|
4687
|
-
this.repoSel.style.display = "none";
|
|
4688
|
-
listHead.appendChild(this.repoSel);
|
|
4689
4730
|
this.listEl = el("div", "list");
|
|
4690
4731
|
const homeView = el("div", "view home-view");
|
|
4691
4732
|
this.homeEl = homeView;
|
|
@@ -4771,6 +4812,7 @@ var LoupeApp = class {
|
|
|
4771
4812
|
}
|
|
4772
4813
|
/** The Chat tab label carries the unread count, so it is visible from any page. */
|
|
4773
4814
|
updateChatBadge() {
|
|
4815
|
+
if (!this.chatOn) return;
|
|
4774
4816
|
const tab = this.tabList.findIndex((t) => t.id === "chat");
|
|
4775
4817
|
const buttons = this.shadow?.querySelectorAll(".tabs .tab");
|
|
4776
4818
|
const el2 = buttons?.[tab];
|
|
@@ -5094,7 +5136,7 @@ var LoupeApp = class {
|
|
|
5094
5136
|
}
|
|
5095
5137
|
}
|
|
5096
5138
|
buildActivityPanel() {
|
|
5097
|
-
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>`;
|
|
5139
|
+
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>`;
|
|
5098
5140
|
this.feedEl = this.activityEl.querySelector("#loupe-mon-feed");
|
|
5099
5141
|
this.activityHint = this.activityEl.querySelector("#loupe-ahint");
|
|
5100
5142
|
this.activityEl.querySelector('[data-role="mon-clear"]').onclick = () => this.clearActivity();
|
|
@@ -5132,6 +5174,52 @@ var LoupeApp = class {
|
|
|
5132
5174
|
this.activityStatus = "idle";
|
|
5133
5175
|
this.renderActivity();
|
|
5134
5176
|
}
|
|
5177
|
+
/**
|
|
5178
|
+
* Read the server's feed and merge it in by id. A backend without a feed answers
|
|
5179
|
+
* null once, and the panel stops asking.
|
|
5180
|
+
*/
|
|
5181
|
+
async loadActivity() {
|
|
5182
|
+
if (this.activityNone) return;
|
|
5183
|
+
let events;
|
|
5184
|
+
try {
|
|
5185
|
+
const read = this.store.listActivity?.bind(this.store);
|
|
5186
|
+
events = read ? await read(this.cfg.projectKey, this.activitySince || void 0) : null;
|
|
5187
|
+
} catch {
|
|
5188
|
+
if (this.activityRemote === "live") {
|
|
5189
|
+
this.activityRemote = "offline";
|
|
5190
|
+
this.renderActivity();
|
|
5191
|
+
}
|
|
5192
|
+
return;
|
|
5193
|
+
}
|
|
5194
|
+
if (events === null) {
|
|
5195
|
+
this.activityRemote = "unsupported";
|
|
5196
|
+
this.activityNone = true;
|
|
5197
|
+
this.syncActivityPoll();
|
|
5198
|
+
this.renderActivity();
|
|
5199
|
+
return;
|
|
5200
|
+
}
|
|
5201
|
+
this.activityRemote = "live";
|
|
5202
|
+
const known = new Set(this.activityEvents.map((e) => e.id));
|
|
5203
|
+
const fresh = events.filter((e) => e && typeof e.id === "string" && !known.has(e.id));
|
|
5204
|
+
if (fresh.length) {
|
|
5205
|
+
this.activityEvents = [...this.activityEvents, ...fresh].sort((a, b) => a.at.localeCompare(b.at)).slice(-500);
|
|
5206
|
+
const newest = fresh.reduce((m, e) => e.at > m ? e.at : m, this.activitySince);
|
|
5207
|
+
this.activitySince = newest;
|
|
5208
|
+
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1e3).toISOString();
|
|
5209
|
+
if (fresh.some((e) => e.level === "error" && e.at >= tenMinutesAgo)) this.activityStatus = "error";
|
|
5210
|
+
}
|
|
5211
|
+
this.renderActivity();
|
|
5212
|
+
}
|
|
5213
|
+
/** Poll the server's feed every 15s, but only while the Activity view is on screen. */
|
|
5214
|
+
syncActivityPoll() {
|
|
5215
|
+
const want = this.tab === "activity" && !this.activityNone && !!this.store.listActivity;
|
|
5216
|
+
if (want && !this.activityPoll) {
|
|
5217
|
+
this.activityPoll = setInterval(() => void this.loadActivity(), 15e3);
|
|
5218
|
+
} else if (!want && this.activityPoll) {
|
|
5219
|
+
clearInterval(this.activityPoll);
|
|
5220
|
+
this.activityPoll = void 0;
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
5135
5223
|
/** The summary, status row, micro-stats and chips — derived, never stored. */
|
|
5136
5224
|
renderActivityChrome() {
|
|
5137
5225
|
if (!this.activityEl) return;
|
|
@@ -5140,6 +5228,11 @@ var LoupeApp = class {
|
|
|
5140
5228
|
const s = summarizeActivity(this.activityEvents, this.activityStatus);
|
|
5141
5229
|
status.querySelector(".mon-status-label").textContent = ACTIVITY_STATUS_LABELS[s.status];
|
|
5142
5230
|
status.className = `mon-status st-${s.status}`;
|
|
5231
|
+
const live = status.querySelector("#loupe-mon-live");
|
|
5232
|
+
if (live) {
|
|
5233
|
+
live.textContent = this.activityRemote === "live" ? "Live" : this.activityRemote === "offline" ? "Offline" : "";
|
|
5234
|
+
live.className = `mon-live ${this.activityRemote}`;
|
|
5235
|
+
}
|
|
5143
5236
|
const summary = this.activityEl.querySelector("#loupe-mon-summary");
|
|
5144
5237
|
if (!s.events) {
|
|
5145
5238
|
summary.innerHTML = "";
|
|
@@ -5193,7 +5286,7 @@ var LoupeApp = class {
|
|
|
5193
5286
|
if (!this.feedEl) return;
|
|
5194
5287
|
const events = this.activityFilter ? this.activityEvents.filter((e) => e.kind === this.activityFilter) : this.activityEvents;
|
|
5195
5288
|
if (!events.length) {
|
|
5196
|
-
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>`;
|
|
5289
|
+
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>`;
|
|
5197
5290
|
return;
|
|
5198
5291
|
}
|
|
5199
5292
|
this.feedEl.innerHTML = events.map((e) => {
|
|
@@ -5209,7 +5302,7 @@ var LoupeApp = class {
|
|
|
5209
5302
|
*/
|
|
5210
5303
|
buildHomePanel() {
|
|
5211
5304
|
const title = this.cfg.label ?? "Loupe";
|
|
5212
|
-
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-
|
|
5305
|
+
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>`;
|
|
5213
5306
|
this.homeEl.querySelector('[data-role="proj-open"]').onclick = (e) => {
|
|
5214
5307
|
e.stopPropagation();
|
|
5215
5308
|
this.setProjectOpen(!this.projOpen);
|
|
@@ -5361,65 +5454,36 @@ var LoupeApp = class {
|
|
|
5361
5454
|
};
|
|
5362
5455
|
}
|
|
5363
5456
|
// ---- project manager (#60) ------------------------------------------------
|
|
5364
|
-
/** The repo new comments are filed against: the panel's choice, else the config. */
|
|
5365
|
-
effectiveRepo() {
|
|
5366
|
-
return this.project.repo || this.cfg.repo || void 0;
|
|
5367
|
-
}
|
|
5368
5457
|
setProjectOpen(open) {
|
|
5369
5458
|
this.projOpen = open;
|
|
5370
5459
|
this.projError = "";
|
|
5371
|
-
if (open) {
|
|
5372
|
-
this.projSearch = "";
|
|
5373
|
-
void this.refreshRepos();
|
|
5374
|
-
}
|
|
5375
5460
|
this.renderProject();
|
|
5376
5461
|
}
|
|
5377
|
-
/**
|
|
5378
|
-
async
|
|
5379
|
-
const source = this.cfg.repos;
|
|
5380
|
-
if (!source) return [];
|
|
5381
|
-
if (Array.isArray(source)) {
|
|
5382
|
-
const q = this.projSearch.trim().toLowerCase();
|
|
5383
|
-
const all = q ? source.filter((r) => r.toLowerCase().includes(q)) : source;
|
|
5384
|
-
return all.slice(0, 50);
|
|
5385
|
-
}
|
|
5462
|
+
/** Read the organization once. A backend without the endpoint, or offline mode, leaves it null. */
|
|
5463
|
+
async loadOrg() {
|
|
5386
5464
|
try {
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
this.projError = e instanceof Error ? e.message : "Could not load repositories.";
|
|
5391
|
-
return [];
|
|
5465
|
+
this.org = await this.store.getOrg?.() ?? null;
|
|
5466
|
+
} catch {
|
|
5467
|
+
this.org = null;
|
|
5392
5468
|
}
|
|
5469
|
+
this.renderProject();
|
|
5393
5470
|
}
|
|
5394
|
-
/**
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
this.projError = "";
|
|
5400
|
-
const seq = ++this.repoSeq;
|
|
5401
|
-
const out = await this.repoChoices();
|
|
5402
|
-
if (seq !== this.repoSeq) return;
|
|
5403
|
-
this.projResults = out;
|
|
5404
|
-
this.renderRepoList();
|
|
5405
|
-
this.renderProjectError();
|
|
5471
|
+
/** "Project · Organization", or the project key when there is no organization to show. */
|
|
5472
|
+
projectLabel() {
|
|
5473
|
+
const org = this.org;
|
|
5474
|
+
const name = org?.project.name || this.cfg.projectKey;
|
|
5475
|
+
return org?.organization ? `${name} \xB7 ${org.organization.name}` : name;
|
|
5406
5476
|
}
|
|
5407
|
-
/**
|
|
5408
|
-
|
|
5409
|
-
const
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
this.renderProject();
|
|
5418
|
-
this.renderPins();
|
|
5419
|
-
this.renderList();
|
|
5420
|
-
this.addActivity({ kind: "repo.link", label: `Linked this page to ${b.dataset.repo}` });
|
|
5421
|
-
};
|
|
5422
|
-
});
|
|
5477
|
+
/** The Organization section of the project popover. Read-only: Hub owns this. */
|
|
5478
|
+
orgSectionHtml() {
|
|
5479
|
+
const org = this.org;
|
|
5480
|
+
const head = `<div class="pp-head"><span>Organization</span><button class="pp-x" data-role="pp-close" aria-label="Close">\u2715</button></div>`;
|
|
5481
|
+
if (!org?.organization) {
|
|
5482
|
+
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>`;
|
|
5483
|
+
}
|
|
5484
|
+
const dest = org.project.destination;
|
|
5485
|
+
const others = org.projects;
|
|
5486
|
+
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>`;
|
|
5423
5487
|
}
|
|
5424
5488
|
renderProjectError() {
|
|
5425
5489
|
const box = this.homeEl?.querySelector("#loupe-pp-err");
|
|
@@ -5432,9 +5496,8 @@ var LoupeApp = class {
|
|
|
5432
5496
|
const chip = this.homeEl.querySelector(".proj-chip");
|
|
5433
5497
|
const pop = this.homeEl.querySelector("#loupe-proj");
|
|
5434
5498
|
if (!chip || !pop) return;
|
|
5435
|
-
|
|
5436
|
-
chip.
|
|
5437
|
-
chip.classList.toggle("unset", !repo);
|
|
5499
|
+
chip.querySelector(".proj-name").textContent = this.projectLabel();
|
|
5500
|
+
chip.classList.toggle("unset", !this.org?.organization);
|
|
5438
5501
|
chip.setAttribute("aria-expanded", String(this.projOpen));
|
|
5439
5502
|
if (!this.projOpen) {
|
|
5440
5503
|
pop.classList.remove("open");
|
|
@@ -5443,25 +5506,9 @@ var LoupeApp = class {
|
|
|
5443
5506
|
}
|
|
5444
5507
|
pop.classList.add("open");
|
|
5445
5508
|
const envs = this.project.environments;
|
|
5446
|
-
pop.innerHTML =
|
|
5447
|
-
this.renderRepoList();
|
|
5509
|
+
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>`;
|
|
5448
5510
|
this.renderProjectError();
|
|
5449
5511
|
pop.querySelector('[data-role="pp-close"]').onclick = () => this.setProjectOpen(false);
|
|
5450
|
-
pop.querySelector('[data-role="pp-clear"]')?.addEventListener("click", () => {
|
|
5451
|
-
this.project.repo = void 0;
|
|
5452
|
-
this.saveProject();
|
|
5453
|
-
this.renderProject();
|
|
5454
|
-
this.renderPins();
|
|
5455
|
-
this.renderList();
|
|
5456
|
-
});
|
|
5457
|
-
const search = pop.querySelector(".pp-search");
|
|
5458
|
-
if (search) {
|
|
5459
|
-
search.addEventListener("click", (e) => e.stopPropagation());
|
|
5460
|
-
search.oninput = () => {
|
|
5461
|
-
this.projSearch = search.value;
|
|
5462
|
-
void this.refreshRepos();
|
|
5463
|
-
};
|
|
5464
|
-
}
|
|
5465
5512
|
pop.querySelectorAll("[data-env-rm]").forEach((b) => {
|
|
5466
5513
|
b.onclick = () => {
|
|
5467
5514
|
const i = Number(b.dataset.envRm);
|
|
@@ -5525,10 +5572,9 @@ var LoupeApp = class {
|
|
|
5525
5572
|
try {
|
|
5526
5573
|
const raw = localStorage.getItem(`loupe:project:${this.cfg.projectKey}`);
|
|
5527
5574
|
const p = raw ? JSON.parse(raw) : null;
|
|
5528
|
-
const repo = typeof p?.repo === "string" && p.repo ? p.repo : void 0;
|
|
5529
5575
|
const stored = Array.isArray(p?.environments) ? p.environments.filter((u) => typeof u === "string") : null;
|
|
5530
5576
|
const localAi = p?.localAi && typeof p.localAi.url === "string" && typeof p.localAi.model === "string" ? { url: p.localAi.url, model: p.localAi.model } : void 0;
|
|
5531
|
-
this.project = {
|
|
5577
|
+
this.project = { environments: stored ?? fromConfig, localAi };
|
|
5532
5578
|
} catch {
|
|
5533
5579
|
this.project = { environments: fromConfig };
|
|
5534
5580
|
}
|
|
@@ -6127,10 +6173,6 @@ var LoupeApp = class {
|
|
|
6127
6173
|
status: "queue",
|
|
6128
6174
|
priority,
|
|
6129
6175
|
changeType,
|
|
6130
|
-
// Branch-aware threads: the host declares these once in `init()`, and the
|
|
6131
|
-
// panel's project manager can override the repo per browser.
|
|
6132
|
-
repo: this.effectiveRepo(),
|
|
6133
|
-
branch: this.cfg.branch,
|
|
6134
6176
|
kind: target.kind,
|
|
6135
6177
|
anchor,
|
|
6136
6178
|
context,
|
|
@@ -6161,8 +6203,7 @@ var LoupeApp = class {
|
|
|
6161
6203
|
this.addActivity({
|
|
6162
6204
|
kind: "comment.create",
|
|
6163
6205
|
label: `Created \u201C${comment.title || comment.body.split("\n")[0] || "comment"}\u201D`,
|
|
6164
|
-
detail: [normalizePriority(comment.priority), comment.kind ?? "element", comment.
|
|
6165
|
-
files: comment.repo ? [`${comment.repo}/${shortPath(comment.url)}`] : []
|
|
6206
|
+
detail: [normalizePriority(comment.priority), comment.kind ?? "element", shortPath(comment.url)].join(" \xB7 ")
|
|
6166
6207
|
});
|
|
6167
6208
|
}
|
|
6168
6209
|
/** Upload the reporter's picked files. A file that fails is skipped, not fatal. */
|
|
@@ -6187,6 +6228,7 @@ var LoupeApp = class {
|
|
|
6187
6228
|
}
|
|
6188
6229
|
}
|
|
6189
6230
|
this.comments.forEach((c, i) => {
|
|
6231
|
+
if (c.source) return;
|
|
6190
6232
|
let pin = this.pins.get(c.id);
|
|
6191
6233
|
if (!pin) {
|
|
6192
6234
|
pin = el("button", "pin");
|
|
@@ -6268,6 +6310,7 @@ var LoupeApp = class {
|
|
|
6268
6310
|
/** Switch the sidebar page (Home ↔ Comments ↔ Connect Claude). */
|
|
6269
6311
|
setTab(tab) {
|
|
6270
6312
|
if (this.tab === tab) return;
|
|
6313
|
+
if (tab === "chat" && !this.chatOn) return;
|
|
6271
6314
|
if (tab !== "comments") this.setMode("off");
|
|
6272
6315
|
this.tab = tab;
|
|
6273
6316
|
this.hintFor = null;
|
|
@@ -6277,7 +6320,11 @@ var LoupeApp = class {
|
|
|
6277
6320
|
if (this.scope === "all" && !this.allComments.length) void this.loadAllComments();
|
|
6278
6321
|
}
|
|
6279
6322
|
if (tab === "comments") this.renderList();
|
|
6280
|
-
if (tab === "activity")
|
|
6323
|
+
if (tab === "activity") {
|
|
6324
|
+
this.renderActivity();
|
|
6325
|
+
void this.loadActivity();
|
|
6326
|
+
}
|
|
6327
|
+
this.syncActivityPoll();
|
|
6281
6328
|
this.applyDockLayout();
|
|
6282
6329
|
}
|
|
6283
6330
|
toggleTheme() {
|
|
@@ -6464,11 +6511,10 @@ var LoupeApp = class {
|
|
|
6464
6511
|
if (DOCK_MODES.includes(p?.mode)) this.dockMode = p.mode;
|
|
6465
6512
|
if (typeof p?.open === "boolean") this.open = p.open;
|
|
6466
6513
|
if (p?.theme === "light" || p?.theme === "dark") this.theme = p.theme;
|
|
6467
|
-
const known = [...BUILTIN_TABS.map((t) => t.id), ...(this.cfg.tabs ?? []).map((t) => t.id)];
|
|
6514
|
+
const known = [...BUILTIN_TABS.map((t) => t.id).filter((id) => id !== "chat" || this.chatOn), ...(this.cfg.tabs ?? []).map((t) => t.id)];
|
|
6468
6515
|
if (typeof p?.tab === "string" && known.includes(p.tab)) this.tab = p.tab;
|
|
6469
6516
|
if (p?.scope === "page" || p?.scope === "all") this.scope = p.scope;
|
|
6470
6517
|
if (typeof p?.statFilter === "string") this.statFilter = p.statFilter;
|
|
6471
|
-
if (typeof p?.repoFilter === "string") this.repoFilter = p.repoFilter;
|
|
6472
6518
|
if (p?.float && typeof p.float.w === "number") this.floatRect = { ...this.floatRect, ...p.float };
|
|
6473
6519
|
if (typeof p?.markersHidden === "boolean") this.markersHidden = p.markersHidden;
|
|
6474
6520
|
if (typeof p?.launcherHidden === "boolean") this.launcherHidden = p.launcherHidden;
|
|
@@ -6495,7 +6541,6 @@ var LoupeApp = class {
|
|
|
6495
6541
|
fab: this.fabPos,
|
|
6496
6542
|
scope: this.scope,
|
|
6497
6543
|
statFilter: this.statFilter,
|
|
6498
|
-
repoFilter: this.repoFilter,
|
|
6499
6544
|
accent: this.accent,
|
|
6500
6545
|
minimized: this.minimized,
|
|
6501
6546
|
hoverHints: this.hoverHints,
|
|
@@ -6578,11 +6623,9 @@ var LoupeApp = class {
|
|
|
6578
6623
|
if (this.statFilter === "needs_you" && !needsYou({ status: c.status }).needs) return false;
|
|
6579
6624
|
if (this.statFilter === "resolved" && stage !== "resolved") return false;
|
|
6580
6625
|
if (this.statFilter === "stale" && (stage === "resolved" || !(Date.parse(c.createdAt) < weekAgo))) return false;
|
|
6581
|
-
if (this.repoFilter && c.repo !== this.repoFilter) return false;
|
|
6582
6626
|
return !q || `${c.title ?? ""} ${c.body} ${c.author?.name ?? ""}`.toLowerCase().includes(q);
|
|
6583
6627
|
});
|
|
6584
6628
|
if (this.scope === "all") items = [...items].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
6585
|
-
this.renderRepoFilter();
|
|
6586
6629
|
this.renderReviewBar();
|
|
6587
6630
|
if (!items.length) {
|
|
6588
6631
|
this.listEl.appendChild(el(
|
|
@@ -6604,15 +6647,6 @@ var LoupeApp = class {
|
|
|
6604
6647
|
});
|
|
6605
6648
|
this.updateCount(items.length);
|
|
6606
6649
|
}
|
|
6607
|
-
/** Repo filter — only offered in the project scope, where it means something. */
|
|
6608
|
-
renderRepoFilter() {
|
|
6609
|
-
if (!this.repoSel) return;
|
|
6610
|
-
const repos = [...new Set(this.visibleComments.map((c) => c.repo).filter((r) => !!r))].sort();
|
|
6611
|
-
this.repoSel.style.display = this.scope === "all" && repos.length > 1 ? "" : "none";
|
|
6612
|
-
const current2 = this.repoFilter;
|
|
6613
|
-
this.repoSel.innerHTML = `<option value="">All repos</option>` + repos.map((r) => `<option value="${escapeHtml(r)}">${escapeHtml(r)}</option>`).join("");
|
|
6614
|
-
this.repoSel.value = current2;
|
|
6615
|
-
}
|
|
6616
6650
|
itemView(c, i) {
|
|
6617
6651
|
const detached = this.pins.get(c.id)?.classList.contains("detached") && !isResolved(c);
|
|
6618
6652
|
const open = this.expanded.has(c.id);
|
|
@@ -6627,6 +6661,19 @@ var LoupeApp = class {
|
|
|
6627
6661
|
const icon = vw < 768 ? "\u{1F4F1}" : vw < 1024 ? "\u25A6" : "\u{1F5A5}";
|
|
6628
6662
|
top.appendChild(el("span", "device", `${icon} ${kind}`));
|
|
6629
6663
|
}
|
|
6664
|
+
if (c.source) {
|
|
6665
|
+
const from = el("span", "srcchip", `from ${c.source.projectName || c.source.projectId}`);
|
|
6666
|
+
from.title = c.source.reporter ? `Sent by ${c.source.reporter.name || c.source.reporter.email}` : "Sent from another project";
|
|
6667
|
+
top.appendChild(from);
|
|
6668
|
+
}
|
|
6669
|
+
const fwd = c.forwarded;
|
|
6670
|
+
if (fwd && fwd.status !== "none") {
|
|
6671
|
+
const where = fwd.destinationName || (fwd.status === "ok" ? "webhook" : "Hub");
|
|
6672
|
+
const ok = fwd.status === "ok";
|
|
6673
|
+
const chip = el("span", ok ? "fwdchip" : "fwdchip bad", ok ? `\u2192 ${where}` : `\u2192 ${where} failed`);
|
|
6674
|
+
chip.title = ok ? `Sent to ${where}` : `Could not send to ${where}${fwd.error ? `: ${fwd.error}` : ""}`;
|
|
6675
|
+
top.appendChild(chip);
|
|
6676
|
+
}
|
|
6630
6677
|
if (isResolved(c)) top.appendChild(el("span", "badge done", "resolved"));
|
|
6631
6678
|
else if (detached) {
|
|
6632
6679
|
const b = el("span", "badge detached", "moved");
|
|
@@ -7802,6 +7849,8 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
7802
7849
|
this.companionSource?.close();
|
|
7803
7850
|
this.companionSource = void 0;
|
|
7804
7851
|
if (this.companionPoll) clearInterval(this.companionPoll);
|
|
7852
|
+
if (this.activityPoll) clearInterval(this.activityPoll);
|
|
7853
|
+
this.activityPoll = void 0;
|
|
7805
7854
|
this.stopVoice();
|
|
7806
7855
|
this.stopPresence();
|
|
7807
7856
|
this.stopRecording?.();
|