@loupekit/sdk 0.10.22-next.68 → 0.10.23-next.69
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.global.js +299 -6
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +299 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.global.js
CHANGED
|
@@ -794,6 +794,43 @@ var Loupe = (() => {
|
|
|
794
794
|
.voice:disabled { opacity: .45; cursor: not-allowed; }
|
|
795
795
|
.voice.on { border-color: var(--pin); color: var(--pin); background: var(--bg-3); }
|
|
796
796
|
|
|
797
|
+
/* in-app mentions */
|
|
798
|
+
.hnotif { margin-top: 10px; }
|
|
799
|
+
.nf-head { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; font-size: 11.5px; color: var(--ink); }
|
|
800
|
+
.nf-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); flex: none; }
|
|
801
|
+
.nf-i {
|
|
802
|
+
display: flex; align-items: baseline; gap: 7px; width: 100%; text-align: left;
|
|
803
|
+
padding: 6px 8px; margin-bottom: 4px; border: 1px solid var(--accent); border-radius: 8px;
|
|
804
|
+
background: var(--bg-2); color: var(--ink); font-size: 11px; cursor: pointer;
|
|
805
|
+
}
|
|
806
|
+
.nf-i:hover { background: var(--bg-3); }
|
|
807
|
+
.nf-b { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
808
|
+
.nf-w { flex: none; color: var(--muted); font-size: 10.5px; }
|
|
809
|
+
.nf-read {
|
|
810
|
+
margin-top: 2px; padding: 4px 8px; border: 1px solid var(--line); border-radius: 7px;
|
|
811
|
+
background: var(--bg); color: var(--muted); font-size: 10.5px; cursor: pointer;
|
|
812
|
+
}
|
|
813
|
+
.nf-read:hover { border-color: var(--accent); color: var(--ink); }
|
|
814
|
+
|
|
815
|
+
/* mention highlighting + autocomplete */
|
|
816
|
+
.mention { color: var(--accent); font-weight: 600; }
|
|
817
|
+
.mention-list {
|
|
818
|
+
display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px;
|
|
819
|
+
padding: 5px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg-2);
|
|
820
|
+
}
|
|
821
|
+
.mention-pick {
|
|
822
|
+
padding: 3px 8px; border: 1px solid var(--line); border-radius: 999px;
|
|
823
|
+
background: var(--bg); color: var(--ink); font-size: 11px; cursor: pointer;
|
|
824
|
+
}
|
|
825
|
+
.mention-pick:hover { border-color: var(--accent); color: var(--accent); }
|
|
826
|
+
/* "needs you" \u2014 the two reasons the stage cannot express */
|
|
827
|
+
.needsline {
|
|
828
|
+
display: flex; align-items: center; gap: 7px; margin: 8px 0; padding: 7px 9px;
|
|
829
|
+
border: 1px solid #e0a92c; border-radius: 9px; background: var(--bg-2);
|
|
830
|
+
font-size: 11.5px; font-weight: 600; color: var(--ink);
|
|
831
|
+
}
|
|
832
|
+
.needs-dot { width: 7px; height: 7px; border-radius: 50%; background: #e0a92c; flex: none; }
|
|
833
|
+
|
|
797
834
|
/* ------------------------------------------------------------- conversation */
|
|
798
835
|
.convo { margin-top: 8px; }
|
|
799
836
|
.convo-loading { padding: 10px; text-align: center; font-size: 11.5px; color: var(--muted); }
|
|
@@ -2990,6 +3027,25 @@ var Loupe = (() => {
|
|
|
2990
3027
|
localStorage.setItem(this.msgKey(threadId), JSON.stringify(all));
|
|
2991
3028
|
return stored;
|
|
2992
3029
|
}
|
|
3030
|
+
/** Offline: the people are whoever has already commented locally. */
|
|
3031
|
+
async listPeople(projectKey) {
|
|
3032
|
+
const prefix = `loupe:${projectKey}:`;
|
|
3033
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3034
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3035
|
+
const k = localStorage.key(i);
|
|
3036
|
+
if (!k || !k.startsWith(prefix)) continue;
|
|
3037
|
+
for (const c of this.parseList(k)) {
|
|
3038
|
+
if (c.author?.id && !seen.has(c.author.id)) seen.set(c.author.id, c.author);
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
return [...seen.values()];
|
|
3042
|
+
}
|
|
3043
|
+
/** Offline mode has no server to notify anyone from. */
|
|
3044
|
+
async listNotifications() {
|
|
3045
|
+
return [];
|
|
3046
|
+
}
|
|
3047
|
+
async markNotificationsRead() {
|
|
3048
|
+
}
|
|
2993
3049
|
async update(id, patch) {
|
|
2994
3050
|
for (const k of this.commentKeys()) {
|
|
2995
3051
|
const list = this.parseList(k);
|
|
@@ -3100,6 +3156,26 @@ var Loupe = (() => {
|
|
|
3100
3156
|
if (!res.ok) throw new Error(`addMessage failed: ${res.status}`);
|
|
3101
3157
|
return await res.json();
|
|
3102
3158
|
}
|
|
3159
|
+
/** Everyone who has taken part in this project, for mention resolution. */
|
|
3160
|
+
async listPeople(projectKey) {
|
|
3161
|
+
const q = new URLSearchParams({ projectKey });
|
|
3162
|
+
const res = await fetch(`${this.base}/v1/people?${q}`, this.opts({ headers: this.headers() }));
|
|
3163
|
+
if (!res.ok) throw new Error(`listPeople failed: ${res.status}`);
|
|
3164
|
+
return await res.json();
|
|
3165
|
+
}
|
|
3166
|
+
async listNotifications(projectKey, recipient) {
|
|
3167
|
+
const q = new URLSearchParams({ projectKey, recipient });
|
|
3168
|
+
const res = await fetch(`${this.base}/v1/notifications?${q}`, this.opts({ headers: this.headers() }));
|
|
3169
|
+
if (!res.ok) throw new Error(`listNotifications failed: ${res.status}`);
|
|
3170
|
+
return (await res.json()).notifications;
|
|
3171
|
+
}
|
|
3172
|
+
async markNotificationsRead(projectKey, recipient, id) {
|
|
3173
|
+
await fetch(`${this.base}/v1/notifications/read`, this.opts({
|
|
3174
|
+
method: "POST",
|
|
3175
|
+
headers: this.headers(),
|
|
3176
|
+
body: JSON.stringify({ projectKey, recipient, id })
|
|
3177
|
+
}));
|
|
3178
|
+
}
|
|
3103
3179
|
async update(id, patch) {
|
|
3104
3180
|
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(id)}`, this.opts({
|
|
3105
3181
|
method: "PATCH",
|
|
@@ -3384,6 +3460,111 @@ var Loupe = (() => {
|
|
|
3384
3460
|
return lines.join("\n");
|
|
3385
3461
|
}
|
|
3386
3462
|
|
|
3463
|
+
// ../shared/dist/mentions.js
|
|
3464
|
+
var HANDLE = /^[A-Za-z0-9][A-Za-z0-9._-]*/;
|
|
3465
|
+
function excludedRanges(body) {
|
|
3466
|
+
const ranges = [];
|
|
3467
|
+
for (const m of body.matchAll(/```[\s\S]*?```/g))
|
|
3468
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
3469
|
+
const fenced = (i) => ranges.some(([a, b]) => i >= a && i < b);
|
|
3470
|
+
for (const m of body.matchAll(/`[^`\n]*`/g)) {
|
|
3471
|
+
if (!fenced(m.index))
|
|
3472
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
3473
|
+
}
|
|
3474
|
+
return ranges;
|
|
3475
|
+
}
|
|
3476
|
+
function parseMentions(body) {
|
|
3477
|
+
if (!body)
|
|
3478
|
+
return [];
|
|
3479
|
+
const excluded = excludedRanges(body);
|
|
3480
|
+
const inExcluded = (i) => excluded.some(([a, b]) => i >= a && i < b);
|
|
3481
|
+
const out = [];
|
|
3482
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3483
|
+
for (let i = 0; i < body.length; i++) {
|
|
3484
|
+
if (body[i] !== "@")
|
|
3485
|
+
continue;
|
|
3486
|
+
if (inExcluded(i))
|
|
3487
|
+
continue;
|
|
3488
|
+
const prev = i > 0 ? body[i - 1] : "";
|
|
3489
|
+
if (prev && !/[\s([{<"'*_~]/.test(prev))
|
|
3490
|
+
continue;
|
|
3491
|
+
const match = HANDLE.exec(body.slice(i + 1));
|
|
3492
|
+
if (!match)
|
|
3493
|
+
continue;
|
|
3494
|
+
const raw = match[0];
|
|
3495
|
+
const handle = raw.replace(/[._-]+$/, "");
|
|
3496
|
+
if (!handle)
|
|
3497
|
+
continue;
|
|
3498
|
+
const key = handle.toLowerCase();
|
|
3499
|
+
if (seen.has(key))
|
|
3500
|
+
continue;
|
|
3501
|
+
seen.add(key);
|
|
3502
|
+
out.push({ handle, start: i, length: handle.length + 1 });
|
|
3503
|
+
}
|
|
3504
|
+
return out;
|
|
3505
|
+
}
|
|
3506
|
+
function mentionSegments(body, mentions) {
|
|
3507
|
+
if (!mentions.length)
|
|
3508
|
+
return [{ text: body, mention: false }];
|
|
3509
|
+
const sorted = [...mentions].sort((a, b) => a.start - b.start);
|
|
3510
|
+
const out = [];
|
|
3511
|
+
let at = 0;
|
|
3512
|
+
for (const m of sorted) {
|
|
3513
|
+
if (m.start > at)
|
|
3514
|
+
out.push({ text: body.slice(at, m.start), mention: false });
|
|
3515
|
+
out.push({ text: body.slice(m.start, m.start + m.length), mention: true });
|
|
3516
|
+
at = m.start + m.length;
|
|
3517
|
+
}
|
|
3518
|
+
if (at < body.length)
|
|
3519
|
+
out.push({ text: body.slice(at), mention: false });
|
|
3520
|
+
return out;
|
|
3521
|
+
}
|
|
3522
|
+
function mentionSuggestions(body, caret, candidates) {
|
|
3523
|
+
const before = body.slice(0, caret);
|
|
3524
|
+
const at = before.lastIndexOf("@");
|
|
3525
|
+
if (at < 0)
|
|
3526
|
+
return [];
|
|
3527
|
+
const typed = before.slice(at + 1);
|
|
3528
|
+
if (/\s/.test(typed))
|
|
3529
|
+
return [];
|
|
3530
|
+
const q = typed.toLowerCase();
|
|
3531
|
+
return candidates.filter((c) => !q || c.name.toLowerCase().includes(q) || c.id.toLowerCase().includes(q)).slice(0, 6);
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
// ../shared/dist/needs-you.js
|
|
3535
|
+
var NEEDS_YOU_LABELS = {
|
|
3536
|
+
review: "Waiting on your review",
|
|
3537
|
+
question: "The agent asked you something",
|
|
3538
|
+
failed: "The agent's run failed"
|
|
3539
|
+
};
|
|
3540
|
+
function looksLikeQuestion(body) {
|
|
3541
|
+
const lines = body.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3542
|
+
const last = lines[lines.length - 1];
|
|
3543
|
+
if (!last)
|
|
3544
|
+
return false;
|
|
3545
|
+
return /\?["')\]]*$/.test(last);
|
|
3546
|
+
}
|
|
3547
|
+
function needsYou(input) {
|
|
3548
|
+
const stage = normalizeStage2(input.status);
|
|
3549
|
+
if (stage === "resolved")
|
|
3550
|
+
return { needs: false };
|
|
3551
|
+
if (input.agentFailed)
|
|
3552
|
+
return { needs: true, reason: "failed", label: NEEDS_YOU_LABELS.failed };
|
|
3553
|
+
if (input.last?.fromAgent && looksLikeQuestion(input.last.body)) {
|
|
3554
|
+
return { needs: true, reason: "question", label: NEEDS_YOU_LABELS.question };
|
|
3555
|
+
}
|
|
3556
|
+
if (stage === "in_review")
|
|
3557
|
+
return { needs: true, reason: "review", label: NEEDS_YOU_LABELS.review };
|
|
3558
|
+
return { needs: false };
|
|
3559
|
+
}
|
|
3560
|
+
function normalizeStage2(status) {
|
|
3561
|
+
if (status === "open")
|
|
3562
|
+
return "queue";
|
|
3563
|
+
if (status === "done")
|
|
3564
|
+
return "resolved";
|
|
3565
|
+
return status;
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3387
3568
|
// ../shared/dist/index.js
|
|
3388
3569
|
var COMMENT_STAGES = ["queue", "todo", "in_progress", "in_review", "resolved"];
|
|
3389
3570
|
var STAGE_LABELS = {
|
|
@@ -3431,7 +3612,7 @@ var Loupe = (() => {
|
|
|
3431
3612
|
}
|
|
3432
3613
|
|
|
3433
3614
|
// src/app.ts
|
|
3434
|
-
var SDK_VERSION = true ? "0.10.
|
|
3615
|
+
var SDK_VERSION = true ? "0.10.23" : "dev";
|
|
3435
3616
|
var ACCENTS = [
|
|
3436
3617
|
{ id: "indigo", dark: "#6b73e6", light: "#4a55d6", soft: "rgba(107,115,230,0.12)" },
|
|
3437
3618
|
{ id: "violet", dark: "#a06be6", light: "#7c3fd4", soft: "rgba(160,107,230,0.14)" },
|
|
@@ -3620,6 +3801,8 @@ var Loupe = (() => {
|
|
|
3620
3801
|
* rather than a `.tab-<id>` selector, so host-registered ids need no CSS.
|
|
3621
3802
|
*/
|
|
3622
3803
|
this.viewEls = /* @__PURE__ */ new Map();
|
|
3804
|
+
/** In-app notifications: mentions of you, newest first. */
|
|
3805
|
+
this.notifications = [];
|
|
3623
3806
|
// ---- inspector ------------------------------------------------------------
|
|
3624
3807
|
// Pointer Events, not mouse events: a touch drag on a phone never fires
|
|
3625
3808
|
// mousemove/mouseup, so Region/Record selection was impossible on touch devices.
|
|
@@ -3847,6 +4030,8 @@ var Loupe = (() => {
|
|
|
3847
4030
|
/** Optimistic replies the store rejected — offered for retry rather than lost. */
|
|
3848
4031
|
this.msgFailed = /* @__PURE__ */ new Set();
|
|
3849
4032
|
this.msgErr = /* @__PURE__ */ new Map();
|
|
4033
|
+
/** Everyone who has taken part, for mention autocomplete. Fetched once. */
|
|
4034
|
+
this.people = /* @__PURE__ */ new Map();
|
|
3850
4035
|
this.cfg = cfg;
|
|
3851
4036
|
this.store = cfg.apiBase ? new HttpAdapter(cfg.apiBase, cfg.user, cfg.userHmac, cfg.headers, cfg.credentials) : new LocalStorageAdapter();
|
|
3852
4037
|
}
|
|
@@ -3864,6 +4049,7 @@ var Loupe = (() => {
|
|
|
3864
4049
|
this.renderPins();
|
|
3865
4050
|
this.renderList();
|
|
3866
4051
|
this.renderHome();
|
|
4052
|
+
void this.loadNotifications();
|
|
3867
4053
|
if (this.scope === "all") void this.loadAllComments();
|
|
3868
4054
|
this.observe();
|
|
3869
4055
|
this.watchNavigation();
|
|
@@ -4239,7 +4425,7 @@ var Loupe = (() => {
|
|
|
4239
4425
|
*/
|
|
4240
4426
|
buildHomePanel() {
|
|
4241
4427
|
const title = this.cfg.label ?? "Loupe";
|
|
4242
|
-
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-repo"></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="hlabel">Recent</div><div class="hfeed" id="loupe-hfeed"></div><div class="hfoot">${escapeHtml(title)} \xB7 <span class="hver">v${escapeHtml(SDK_VERSION)}</span></div>`;
|
|
4428
|
+
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-repo"></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></div>`;
|
|
4243
4429
|
this.homeEl.querySelector('[data-role="proj-open"]').onclick = (e) => {
|
|
4244
4430
|
e.stopPropagation();
|
|
4245
4431
|
this.setProjectOpen(!this.projOpen);
|
|
@@ -4285,7 +4471,10 @@ var Loupe = (() => {
|
|
|
4285
4471
|
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
4286
4472
|
return [
|
|
4287
4473
|
{ key: "open", label: "Open", n: list.filter((c) => normalizeStatus(c.status) !== "resolved").length },
|
|
4288
|
-
|
|
4474
|
+
// The shared predicate, so the panel and the dashboard cannot disagree about
|
|
4475
|
+
// what "needs you" means. Only the stage is available here — the reasons that
|
|
4476
|
+
// depend on the last message are computed where the messages are loaded.
|
|
4477
|
+
{ key: "needs_you", label: "Needs you", n: list.filter((c) => needsYou({ status: c.status }).needs).length },
|
|
4289
4478
|
{ key: "resolved", label: "Resolved", n: list.filter((c) => normalizeStatus(c.status) === "resolved").length },
|
|
4290
4479
|
{
|
|
4291
4480
|
key: "stale",
|
|
@@ -4322,6 +4511,7 @@ var Loupe = (() => {
|
|
|
4322
4511
|
b.querySelector(".hscope-n").textContent = counts[scope];
|
|
4323
4512
|
});
|
|
4324
4513
|
this.renderProject();
|
|
4514
|
+
this.renderNotifications();
|
|
4325
4515
|
const feed = this.homeEl.querySelector("#loupe-hfeed");
|
|
4326
4516
|
if (!feed) return;
|
|
4327
4517
|
const recent = [...this.visibleComments].sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, 8);
|
|
@@ -4341,6 +4531,50 @@ var Loupe = (() => {
|
|
|
4341
4531
|
};
|
|
4342
4532
|
});
|
|
4343
4533
|
}
|
|
4534
|
+
async loadNotifications() {
|
|
4535
|
+
try {
|
|
4536
|
+
const list = await this.store.listNotifications(this.cfg.projectKey, this.cfg.user.id);
|
|
4537
|
+
this.notifications = Array.isArray(list) ? list : [];
|
|
4538
|
+
} catch {
|
|
4539
|
+
this.notifications = [];
|
|
4540
|
+
}
|
|
4541
|
+
this.renderNotifications();
|
|
4542
|
+
}
|
|
4543
|
+
/**
|
|
4544
|
+
* The mentions block. Rendered only when there are unread ones — a permanent empty
|
|
4545
|
+
* "0 unread" is noise, and the whole point is that it appears when it matters.
|
|
4546
|
+
*/
|
|
4547
|
+
renderNotifications() {
|
|
4548
|
+
const box = this.homeEl?.querySelector("#loupe-hnotif");
|
|
4549
|
+
if (!box) return;
|
|
4550
|
+
const unread = this.notifications.filter((n) => !n.readAt);
|
|
4551
|
+
if (!unread.length) {
|
|
4552
|
+
box.innerHTML = "";
|
|
4553
|
+
box.style.display = "none";
|
|
4554
|
+
return;
|
|
4555
|
+
}
|
|
4556
|
+
box.style.display = "";
|
|
4557
|
+
box.innerHTML = `<div class="nf-head"><span class="nf-dot"></span><b>${unread.length}</b> mention${unread.length === 1 ? "" : "s"} waiting</div>` + unread.slice(0, 3).map((n) => `<button class="nf-i" data-thread="${escapeAttr(n.threadId)}"><span class="nf-b">${escapeHtml(n.body)}</span><span class="nf-w">${escapeHtml(fmtAgo(n.createdAt))}</span></button>`).join("") + `<button class="nf-read">Mark as read</button>`;
|
|
4558
|
+
box.querySelectorAll(".nf-i").forEach((b) => {
|
|
4559
|
+
b.onclick = () => {
|
|
4560
|
+
const id = b.dataset.thread;
|
|
4561
|
+
this.scope = "all";
|
|
4562
|
+
if (!this.allComments.length) void this.loadAllComments();
|
|
4563
|
+
this.statFilter = "";
|
|
4564
|
+
this.setTab("comments");
|
|
4565
|
+
this.expanded.add(id);
|
|
4566
|
+
this.renderList();
|
|
4567
|
+
this.flash(id);
|
|
4568
|
+
};
|
|
4569
|
+
});
|
|
4570
|
+
box.querySelector(".nf-read").onclick = async () => {
|
|
4571
|
+
try {
|
|
4572
|
+
await this.store.markNotificationsRead(this.cfg.projectKey, this.cfg.user.id);
|
|
4573
|
+
} catch {
|
|
4574
|
+
}
|
|
4575
|
+
void this.loadNotifications();
|
|
4576
|
+
};
|
|
4577
|
+
}
|
|
4344
4578
|
// ---- project manager (#60) ------------------------------------------------
|
|
4345
4579
|
/** The repo new comments are filed against: the panel's choice, else the config. */
|
|
4346
4580
|
effectiveRepo() {
|
|
@@ -5444,7 +5678,7 @@ var Loupe = (() => {
|
|
|
5444
5678
|
let items = this.visibleComments.filter((c) => {
|
|
5445
5679
|
const stage = normalizeStatus(c.status);
|
|
5446
5680
|
if (this.statFilter === "open" && stage === "resolved") return false;
|
|
5447
|
-
if (this.statFilter === "needs_you" &&
|
|
5681
|
+
if (this.statFilter === "needs_you" && !needsYou({ status: c.status }).needs) return false;
|
|
5448
5682
|
if (this.statFilter === "resolved" && stage !== "resolved") return false;
|
|
5449
5683
|
if (this.statFilter === "stale" && (stage === "resolved" || !(Date.parse(c.createdAt) < weekAgo))) return false;
|
|
5450
5684
|
if (this.repoFilter && c.repo !== this.repoFilter) return false;
|
|
@@ -5546,6 +5780,18 @@ var Loupe = (() => {
|
|
|
5546
5780
|
const summary = c.title || (c.body.split("\n")[0] ?? "").slice(0, 140) || "(no description)";
|
|
5547
5781
|
item.appendChild(el("div", "summary", summary));
|
|
5548
5782
|
const detail = el("div", "detail");
|
|
5783
|
+
const repliesForState = this.messages.get(c.id) ?? [];
|
|
5784
|
+
const lastMsg = repliesForState.length ? repliesForState[repliesForState.length - 1] : void 0;
|
|
5785
|
+
const attention = needsYou({
|
|
5786
|
+
status: c.status,
|
|
5787
|
+
last: lastMsg ? { fromAgent: lastMsg.author.type === "agent", body: lastMsg.body } : null,
|
|
5788
|
+
agentFailed: this.msgFailed.has(lastMsg?.id ?? "")
|
|
5789
|
+
});
|
|
5790
|
+
if (c.status !== "resolved" && attention.needs && attention.reason !== "review") {
|
|
5791
|
+
const line = el("div", "needsline");
|
|
5792
|
+
line.append(el("span", "needs-dot"), el("span", "", attention.label ?? "Needs you"));
|
|
5793
|
+
detail.appendChild(line);
|
|
5794
|
+
}
|
|
5549
5795
|
if (c.status === "in_review") detail.appendChild(this.reviewBanner(c));
|
|
5550
5796
|
if (c.proposal) detail.appendChild(this.proposalView(c));
|
|
5551
5797
|
if (c.title || c.body.includes("\n")) detail.appendChild(el("div", "body", c.body));
|
|
@@ -6106,7 +6352,12 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6106
6352
|
el("span", "msg-when", fmtAgo(m.createdAt))
|
|
6107
6353
|
);
|
|
6108
6354
|
if (fromAgent) head.appendChild(el("span", "msg-tag", "agent"));
|
|
6109
|
-
|
|
6355
|
+
const body = el("div", "msg-body");
|
|
6356
|
+
for (const seg of mentionSegments(m.body, parseMentions(m.body))) {
|
|
6357
|
+
if (seg.mention) body.appendChild(el("span", "mention", seg.text));
|
|
6358
|
+
else body.appendChild(document.createTextNode(seg.text));
|
|
6359
|
+
}
|
|
6360
|
+
row.append(head, body);
|
|
6110
6361
|
if (this.msgPending.has(m.id)) row.appendChild(el("div", "msg-state", "Sending\u2026"));
|
|
6111
6362
|
if (this.msgFailed.has(m.id)) {
|
|
6112
6363
|
const failed = el("div", "msg-state failed");
|
|
@@ -6143,7 +6394,40 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6143
6394
|
void this.sendReply(c, input.value);
|
|
6144
6395
|
};
|
|
6145
6396
|
foot.append(el("span", "reply-hint", "@ to mention"), send);
|
|
6146
|
-
|
|
6397
|
+
const suggest = el("div", "mention-list");
|
|
6398
|
+
suggest.style.display = "none";
|
|
6399
|
+
const refreshSuggestions = () => {
|
|
6400
|
+
const matches = mentionSuggestions(input.value, input.selectionStart ?? input.value.length, this.people.get(this.cfg.projectKey) ?? []);
|
|
6401
|
+
suggest.textContent = "";
|
|
6402
|
+
if (!matches.length) {
|
|
6403
|
+
suggest.style.display = "none";
|
|
6404
|
+
return;
|
|
6405
|
+
}
|
|
6406
|
+
suggest.style.display = "";
|
|
6407
|
+
for (const person of matches) {
|
|
6408
|
+
const b = el("button", "mention-pick", person.name);
|
|
6409
|
+
b.onclick = (e) => {
|
|
6410
|
+
e.stopPropagation();
|
|
6411
|
+
const before = input.value.slice(0, input.selectionStart ?? input.value.length);
|
|
6412
|
+
const at = before.lastIndexOf("@");
|
|
6413
|
+
const after = input.value.slice(input.selectionStart ?? input.value.length);
|
|
6414
|
+
input.value = `${before.slice(0, at)}@${person.name.replace(/\s+/g, "")} ${after}`;
|
|
6415
|
+
this.msgDrafts.set(c.id, input.value);
|
|
6416
|
+
suggest.style.display = "none";
|
|
6417
|
+
input.focus();
|
|
6418
|
+
};
|
|
6419
|
+
suggest.appendChild(b);
|
|
6420
|
+
}
|
|
6421
|
+
};
|
|
6422
|
+
input.oninput = () => {
|
|
6423
|
+
this.msgDrafts.set(c.id, input.value);
|
|
6424
|
+
refreshSuggestions();
|
|
6425
|
+
};
|
|
6426
|
+
input.onkeyup = () => refreshSuggestions();
|
|
6427
|
+
input.onblur = () => setTimeout(() => {
|
|
6428
|
+
suggest.style.display = "none";
|
|
6429
|
+
}, 150);
|
|
6430
|
+
reply.append(input, suggest, foot);
|
|
6147
6431
|
wrap.appendChild(reply);
|
|
6148
6432
|
const timeline = threadTimeline(c, asRows);
|
|
6149
6433
|
const tl = el("div", "tl");
|
|
@@ -6185,9 +6469,18 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6185
6469
|
wrap.appendChild(copyRow);
|
|
6186
6470
|
return wrap;
|
|
6187
6471
|
}
|
|
6472
|
+
async loadPeople() {
|
|
6473
|
+
if (this.people.has(this.cfg.projectKey)) return;
|
|
6474
|
+
try {
|
|
6475
|
+
this.people.set(this.cfg.projectKey, await this.store.listPeople(this.cfg.projectKey));
|
|
6476
|
+
} catch {
|
|
6477
|
+
this.people.set(this.cfg.projectKey, []);
|
|
6478
|
+
}
|
|
6479
|
+
}
|
|
6188
6480
|
/** Fetch replies once, when a card is first expanded. */
|
|
6189
6481
|
async loadMessages(c) {
|
|
6190
6482
|
if (this.messages.has(c.id)) return;
|
|
6483
|
+
void this.loadPeople();
|
|
6191
6484
|
try {
|
|
6192
6485
|
this.messages.set(c.id, await this.store.listMessages(c.id));
|
|
6193
6486
|
} catch {
|