@loupekit/sdk 0.10.22 → 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.js
CHANGED
|
@@ -760,6 +760,43 @@ var STYLES = (
|
|
|
760
760
|
.voice:disabled { opacity: .45; cursor: not-allowed; }
|
|
761
761
|
.voice.on { border-color: var(--pin); color: var(--pin); background: var(--bg-3); }
|
|
762
762
|
|
|
763
|
+
/* in-app mentions */
|
|
764
|
+
.hnotif { margin-top: 10px; }
|
|
765
|
+
.nf-head { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; font-size: 11.5px; color: var(--ink); }
|
|
766
|
+
.nf-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); flex: none; }
|
|
767
|
+
.nf-i {
|
|
768
|
+
display: flex; align-items: baseline; gap: 7px; width: 100%; text-align: left;
|
|
769
|
+
padding: 6px 8px; margin-bottom: 4px; border: 1px solid var(--accent); border-radius: 8px;
|
|
770
|
+
background: var(--bg-2); color: var(--ink); font-size: 11px; cursor: pointer;
|
|
771
|
+
}
|
|
772
|
+
.nf-i:hover { background: var(--bg-3); }
|
|
773
|
+
.nf-b { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
774
|
+
.nf-w { flex: none; color: var(--muted); font-size: 10.5px; }
|
|
775
|
+
.nf-read {
|
|
776
|
+
margin-top: 2px; padding: 4px 8px; border: 1px solid var(--line); border-radius: 7px;
|
|
777
|
+
background: var(--bg); color: var(--muted); font-size: 10.5px; cursor: pointer;
|
|
778
|
+
}
|
|
779
|
+
.nf-read:hover { border-color: var(--accent); color: var(--ink); }
|
|
780
|
+
|
|
781
|
+
/* mention highlighting + autocomplete */
|
|
782
|
+
.mention { color: var(--accent); font-weight: 600; }
|
|
783
|
+
.mention-list {
|
|
784
|
+
display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px;
|
|
785
|
+
padding: 5px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg-2);
|
|
786
|
+
}
|
|
787
|
+
.mention-pick {
|
|
788
|
+
padding: 3px 8px; border: 1px solid var(--line); border-radius: 999px;
|
|
789
|
+
background: var(--bg); color: var(--ink); font-size: 11px; cursor: pointer;
|
|
790
|
+
}
|
|
791
|
+
.mention-pick:hover { border-color: var(--accent); color: var(--accent); }
|
|
792
|
+
/* "needs you" \u2014 the two reasons the stage cannot express */
|
|
793
|
+
.needsline {
|
|
794
|
+
display: flex; align-items: center; gap: 7px; margin: 8px 0; padding: 7px 9px;
|
|
795
|
+
border: 1px solid #e0a92c; border-radius: 9px; background: var(--bg-2);
|
|
796
|
+
font-size: 11.5px; font-weight: 600; color: var(--ink);
|
|
797
|
+
}
|
|
798
|
+
.needs-dot { width: 7px; height: 7px; border-radius: 50%; background: #e0a92c; flex: none; }
|
|
799
|
+
|
|
763
800
|
/* ------------------------------------------------------------- conversation */
|
|
764
801
|
.convo { margin-top: 8px; }
|
|
765
802
|
.convo-loading { padding: 10px; text-align: center; font-size: 11.5px; color: var(--muted); }
|
|
@@ -2956,6 +2993,25 @@ var LocalStorageAdapter = class {
|
|
|
2956
2993
|
localStorage.setItem(this.msgKey(threadId), JSON.stringify(all));
|
|
2957
2994
|
return stored;
|
|
2958
2995
|
}
|
|
2996
|
+
/** Offline: the people are whoever has already commented locally. */
|
|
2997
|
+
async listPeople(projectKey) {
|
|
2998
|
+
const prefix = `loupe:${projectKey}:`;
|
|
2999
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3000
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3001
|
+
const k = localStorage.key(i);
|
|
3002
|
+
if (!k || !k.startsWith(prefix)) continue;
|
|
3003
|
+
for (const c of this.parseList(k)) {
|
|
3004
|
+
if (c.author?.id && !seen.has(c.author.id)) seen.set(c.author.id, c.author);
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
return [...seen.values()];
|
|
3008
|
+
}
|
|
3009
|
+
/** Offline mode has no server to notify anyone from. */
|
|
3010
|
+
async listNotifications() {
|
|
3011
|
+
return [];
|
|
3012
|
+
}
|
|
3013
|
+
async markNotificationsRead() {
|
|
3014
|
+
}
|
|
2959
3015
|
async update(id, patch) {
|
|
2960
3016
|
for (const k of this.commentKeys()) {
|
|
2961
3017
|
const list = this.parseList(k);
|
|
@@ -3066,6 +3122,26 @@ var HttpAdapter = class {
|
|
|
3066
3122
|
if (!res.ok) throw new Error(`addMessage failed: ${res.status}`);
|
|
3067
3123
|
return await res.json();
|
|
3068
3124
|
}
|
|
3125
|
+
/** Everyone who has taken part in this project, for mention resolution. */
|
|
3126
|
+
async listPeople(projectKey) {
|
|
3127
|
+
const q = new URLSearchParams({ projectKey });
|
|
3128
|
+
const res = await fetch(`${this.base}/v1/people?${q}`, this.opts({ headers: this.headers() }));
|
|
3129
|
+
if (!res.ok) throw new Error(`listPeople failed: ${res.status}`);
|
|
3130
|
+
return await res.json();
|
|
3131
|
+
}
|
|
3132
|
+
async listNotifications(projectKey, recipient) {
|
|
3133
|
+
const q = new URLSearchParams({ projectKey, recipient });
|
|
3134
|
+
const res = await fetch(`${this.base}/v1/notifications?${q}`, this.opts({ headers: this.headers() }));
|
|
3135
|
+
if (!res.ok) throw new Error(`listNotifications failed: ${res.status}`);
|
|
3136
|
+
return (await res.json()).notifications;
|
|
3137
|
+
}
|
|
3138
|
+
async markNotificationsRead(projectKey, recipient, id) {
|
|
3139
|
+
await fetch(`${this.base}/v1/notifications/read`, this.opts({
|
|
3140
|
+
method: "POST",
|
|
3141
|
+
headers: this.headers(),
|
|
3142
|
+
body: JSON.stringify({ projectKey, recipient, id })
|
|
3143
|
+
}));
|
|
3144
|
+
}
|
|
3069
3145
|
async update(id, patch) {
|
|
3070
3146
|
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(id)}`, this.opts({
|
|
3071
3147
|
method: "PATCH",
|
|
@@ -3350,6 +3426,111 @@ function threadAsText(input, messages = []) {
|
|
|
3350
3426
|
return lines.join("\n");
|
|
3351
3427
|
}
|
|
3352
3428
|
|
|
3429
|
+
// ../shared/dist/mentions.js
|
|
3430
|
+
var HANDLE = /^[A-Za-z0-9][A-Za-z0-9._-]*/;
|
|
3431
|
+
function excludedRanges(body) {
|
|
3432
|
+
const ranges = [];
|
|
3433
|
+
for (const m of body.matchAll(/```[\s\S]*?```/g))
|
|
3434
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
3435
|
+
const fenced = (i) => ranges.some(([a, b]) => i >= a && i < b);
|
|
3436
|
+
for (const m of body.matchAll(/`[^`\n]*`/g)) {
|
|
3437
|
+
if (!fenced(m.index))
|
|
3438
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
3439
|
+
}
|
|
3440
|
+
return ranges;
|
|
3441
|
+
}
|
|
3442
|
+
function parseMentions(body) {
|
|
3443
|
+
if (!body)
|
|
3444
|
+
return [];
|
|
3445
|
+
const excluded = excludedRanges(body);
|
|
3446
|
+
const inExcluded = (i) => excluded.some(([a, b]) => i >= a && i < b);
|
|
3447
|
+
const out = [];
|
|
3448
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3449
|
+
for (let i = 0; i < body.length; i++) {
|
|
3450
|
+
if (body[i] !== "@")
|
|
3451
|
+
continue;
|
|
3452
|
+
if (inExcluded(i))
|
|
3453
|
+
continue;
|
|
3454
|
+
const prev = i > 0 ? body[i - 1] : "";
|
|
3455
|
+
if (prev && !/[\s([{<"'*_~]/.test(prev))
|
|
3456
|
+
continue;
|
|
3457
|
+
const match = HANDLE.exec(body.slice(i + 1));
|
|
3458
|
+
if (!match)
|
|
3459
|
+
continue;
|
|
3460
|
+
const raw = match[0];
|
|
3461
|
+
const handle = raw.replace(/[._-]+$/, "");
|
|
3462
|
+
if (!handle)
|
|
3463
|
+
continue;
|
|
3464
|
+
const key = handle.toLowerCase();
|
|
3465
|
+
if (seen.has(key))
|
|
3466
|
+
continue;
|
|
3467
|
+
seen.add(key);
|
|
3468
|
+
out.push({ handle, start: i, length: handle.length + 1 });
|
|
3469
|
+
}
|
|
3470
|
+
return out;
|
|
3471
|
+
}
|
|
3472
|
+
function mentionSegments(body, mentions) {
|
|
3473
|
+
if (!mentions.length)
|
|
3474
|
+
return [{ text: body, mention: false }];
|
|
3475
|
+
const sorted = [...mentions].sort((a, b) => a.start - b.start);
|
|
3476
|
+
const out = [];
|
|
3477
|
+
let at = 0;
|
|
3478
|
+
for (const m of sorted) {
|
|
3479
|
+
if (m.start > at)
|
|
3480
|
+
out.push({ text: body.slice(at, m.start), mention: false });
|
|
3481
|
+
out.push({ text: body.slice(m.start, m.start + m.length), mention: true });
|
|
3482
|
+
at = m.start + m.length;
|
|
3483
|
+
}
|
|
3484
|
+
if (at < body.length)
|
|
3485
|
+
out.push({ text: body.slice(at), mention: false });
|
|
3486
|
+
return out;
|
|
3487
|
+
}
|
|
3488
|
+
function mentionSuggestions(body, caret, candidates) {
|
|
3489
|
+
const before = body.slice(0, caret);
|
|
3490
|
+
const at = before.lastIndexOf("@");
|
|
3491
|
+
if (at < 0)
|
|
3492
|
+
return [];
|
|
3493
|
+
const typed = before.slice(at + 1);
|
|
3494
|
+
if (/\s/.test(typed))
|
|
3495
|
+
return [];
|
|
3496
|
+
const q = typed.toLowerCase();
|
|
3497
|
+
return candidates.filter((c) => !q || c.name.toLowerCase().includes(q) || c.id.toLowerCase().includes(q)).slice(0, 6);
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
// ../shared/dist/needs-you.js
|
|
3501
|
+
var NEEDS_YOU_LABELS = {
|
|
3502
|
+
review: "Waiting on your review",
|
|
3503
|
+
question: "The agent asked you something",
|
|
3504
|
+
failed: "The agent's run failed"
|
|
3505
|
+
};
|
|
3506
|
+
function looksLikeQuestion(body) {
|
|
3507
|
+
const lines = body.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3508
|
+
const last = lines[lines.length - 1];
|
|
3509
|
+
if (!last)
|
|
3510
|
+
return false;
|
|
3511
|
+
return /\?["')\]]*$/.test(last);
|
|
3512
|
+
}
|
|
3513
|
+
function needsYou(input) {
|
|
3514
|
+
const stage = normalizeStage2(input.status);
|
|
3515
|
+
if (stage === "resolved")
|
|
3516
|
+
return { needs: false };
|
|
3517
|
+
if (input.agentFailed)
|
|
3518
|
+
return { needs: true, reason: "failed", label: NEEDS_YOU_LABELS.failed };
|
|
3519
|
+
if (input.last?.fromAgent && looksLikeQuestion(input.last.body)) {
|
|
3520
|
+
return { needs: true, reason: "question", label: NEEDS_YOU_LABELS.question };
|
|
3521
|
+
}
|
|
3522
|
+
if (stage === "in_review")
|
|
3523
|
+
return { needs: true, reason: "review", label: NEEDS_YOU_LABELS.review };
|
|
3524
|
+
return { needs: false };
|
|
3525
|
+
}
|
|
3526
|
+
function normalizeStage2(status) {
|
|
3527
|
+
if (status === "open")
|
|
3528
|
+
return "queue";
|
|
3529
|
+
if (status === "done")
|
|
3530
|
+
return "resolved";
|
|
3531
|
+
return status;
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3353
3534
|
// ../shared/dist/index.js
|
|
3354
3535
|
var COMMENT_STAGES = ["queue", "todo", "in_progress", "in_review", "resolved"];
|
|
3355
3536
|
var STAGE_LABELS = {
|
|
@@ -3397,7 +3578,7 @@ function normalizePriority(value) {
|
|
|
3397
3578
|
}
|
|
3398
3579
|
|
|
3399
3580
|
// src/app.ts
|
|
3400
|
-
var SDK_VERSION = true ? "0.10.
|
|
3581
|
+
var SDK_VERSION = true ? "0.10.23" : "dev";
|
|
3401
3582
|
var ACCENTS = [
|
|
3402
3583
|
{ id: "indigo", dark: "#6b73e6", light: "#4a55d6", soft: "rgba(107,115,230,0.12)" },
|
|
3403
3584
|
{ id: "violet", dark: "#a06be6", light: "#7c3fd4", soft: "rgba(160,107,230,0.14)" },
|
|
@@ -3586,6 +3767,8 @@ var LoupeApp = class {
|
|
|
3586
3767
|
* rather than a `.tab-<id>` selector, so host-registered ids need no CSS.
|
|
3587
3768
|
*/
|
|
3588
3769
|
this.viewEls = /* @__PURE__ */ new Map();
|
|
3770
|
+
/** In-app notifications: mentions of you, newest first. */
|
|
3771
|
+
this.notifications = [];
|
|
3589
3772
|
// ---- inspector ------------------------------------------------------------
|
|
3590
3773
|
// Pointer Events, not mouse events: a touch drag on a phone never fires
|
|
3591
3774
|
// mousemove/mouseup, so Region/Record selection was impossible on touch devices.
|
|
@@ -3813,6 +3996,8 @@ var LoupeApp = class {
|
|
|
3813
3996
|
/** Optimistic replies the store rejected — offered for retry rather than lost. */
|
|
3814
3997
|
this.msgFailed = /* @__PURE__ */ new Set();
|
|
3815
3998
|
this.msgErr = /* @__PURE__ */ new Map();
|
|
3999
|
+
/** Everyone who has taken part, for mention autocomplete. Fetched once. */
|
|
4000
|
+
this.people = /* @__PURE__ */ new Map();
|
|
3816
4001
|
this.cfg = cfg;
|
|
3817
4002
|
this.store = cfg.apiBase ? new HttpAdapter(cfg.apiBase, cfg.user, cfg.userHmac, cfg.headers, cfg.credentials) : new LocalStorageAdapter();
|
|
3818
4003
|
}
|
|
@@ -3830,6 +4015,7 @@ var LoupeApp = class {
|
|
|
3830
4015
|
this.renderPins();
|
|
3831
4016
|
this.renderList();
|
|
3832
4017
|
this.renderHome();
|
|
4018
|
+
void this.loadNotifications();
|
|
3833
4019
|
if (this.scope === "all") void this.loadAllComments();
|
|
3834
4020
|
this.observe();
|
|
3835
4021
|
this.watchNavigation();
|
|
@@ -4205,7 +4391,7 @@ var LoupeApp = class {
|
|
|
4205
4391
|
*/
|
|
4206
4392
|
buildHomePanel() {
|
|
4207
4393
|
const title = this.cfg.label ?? "Loupe";
|
|
4208
|
-
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>`;
|
|
4394
|
+
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>`;
|
|
4209
4395
|
this.homeEl.querySelector('[data-role="proj-open"]').onclick = (e) => {
|
|
4210
4396
|
e.stopPropagation();
|
|
4211
4397
|
this.setProjectOpen(!this.projOpen);
|
|
@@ -4251,7 +4437,10 @@ var LoupeApp = class {
|
|
|
4251
4437
|
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
4252
4438
|
return [
|
|
4253
4439
|
{ key: "open", label: "Open", n: list.filter((c) => normalizeStatus(c.status) !== "resolved").length },
|
|
4254
|
-
|
|
4440
|
+
// The shared predicate, so the panel and the dashboard cannot disagree about
|
|
4441
|
+
// what "needs you" means. Only the stage is available here — the reasons that
|
|
4442
|
+
// depend on the last message are computed where the messages are loaded.
|
|
4443
|
+
{ key: "needs_you", label: "Needs you", n: list.filter((c) => needsYou({ status: c.status }).needs).length },
|
|
4255
4444
|
{ key: "resolved", label: "Resolved", n: list.filter((c) => normalizeStatus(c.status) === "resolved").length },
|
|
4256
4445
|
{
|
|
4257
4446
|
key: "stale",
|
|
@@ -4288,6 +4477,7 @@ var LoupeApp = class {
|
|
|
4288
4477
|
b.querySelector(".hscope-n").textContent = counts[scope];
|
|
4289
4478
|
});
|
|
4290
4479
|
this.renderProject();
|
|
4480
|
+
this.renderNotifications();
|
|
4291
4481
|
const feed = this.homeEl.querySelector("#loupe-hfeed");
|
|
4292
4482
|
if (!feed) return;
|
|
4293
4483
|
const recent = [...this.visibleComments].sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, 8);
|
|
@@ -4307,6 +4497,50 @@ var LoupeApp = class {
|
|
|
4307
4497
|
};
|
|
4308
4498
|
});
|
|
4309
4499
|
}
|
|
4500
|
+
async loadNotifications() {
|
|
4501
|
+
try {
|
|
4502
|
+
const list = await this.store.listNotifications(this.cfg.projectKey, this.cfg.user.id);
|
|
4503
|
+
this.notifications = Array.isArray(list) ? list : [];
|
|
4504
|
+
} catch {
|
|
4505
|
+
this.notifications = [];
|
|
4506
|
+
}
|
|
4507
|
+
this.renderNotifications();
|
|
4508
|
+
}
|
|
4509
|
+
/**
|
|
4510
|
+
* The mentions block. Rendered only when there are unread ones — a permanent empty
|
|
4511
|
+
* "0 unread" is noise, and the whole point is that it appears when it matters.
|
|
4512
|
+
*/
|
|
4513
|
+
renderNotifications() {
|
|
4514
|
+
const box = this.homeEl?.querySelector("#loupe-hnotif");
|
|
4515
|
+
if (!box) return;
|
|
4516
|
+
const unread = this.notifications.filter((n) => !n.readAt);
|
|
4517
|
+
if (!unread.length) {
|
|
4518
|
+
box.innerHTML = "";
|
|
4519
|
+
box.style.display = "none";
|
|
4520
|
+
return;
|
|
4521
|
+
}
|
|
4522
|
+
box.style.display = "";
|
|
4523
|
+
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>`;
|
|
4524
|
+
box.querySelectorAll(".nf-i").forEach((b) => {
|
|
4525
|
+
b.onclick = () => {
|
|
4526
|
+
const id = b.dataset.thread;
|
|
4527
|
+
this.scope = "all";
|
|
4528
|
+
if (!this.allComments.length) void this.loadAllComments();
|
|
4529
|
+
this.statFilter = "";
|
|
4530
|
+
this.setTab("comments");
|
|
4531
|
+
this.expanded.add(id);
|
|
4532
|
+
this.renderList();
|
|
4533
|
+
this.flash(id);
|
|
4534
|
+
};
|
|
4535
|
+
});
|
|
4536
|
+
box.querySelector(".nf-read").onclick = async () => {
|
|
4537
|
+
try {
|
|
4538
|
+
await this.store.markNotificationsRead(this.cfg.projectKey, this.cfg.user.id);
|
|
4539
|
+
} catch {
|
|
4540
|
+
}
|
|
4541
|
+
void this.loadNotifications();
|
|
4542
|
+
};
|
|
4543
|
+
}
|
|
4310
4544
|
// ---- project manager (#60) ------------------------------------------------
|
|
4311
4545
|
/** The repo new comments are filed against: the panel's choice, else the config. */
|
|
4312
4546
|
effectiveRepo() {
|
|
@@ -5410,7 +5644,7 @@ var LoupeApp = class {
|
|
|
5410
5644
|
let items = this.visibleComments.filter((c) => {
|
|
5411
5645
|
const stage = normalizeStatus(c.status);
|
|
5412
5646
|
if (this.statFilter === "open" && stage === "resolved") return false;
|
|
5413
|
-
if (this.statFilter === "needs_you" &&
|
|
5647
|
+
if (this.statFilter === "needs_you" && !needsYou({ status: c.status }).needs) return false;
|
|
5414
5648
|
if (this.statFilter === "resolved" && stage !== "resolved") return false;
|
|
5415
5649
|
if (this.statFilter === "stale" && (stage === "resolved" || !(Date.parse(c.createdAt) < weekAgo))) return false;
|
|
5416
5650
|
if (this.repoFilter && c.repo !== this.repoFilter) return false;
|
|
@@ -5512,6 +5746,18 @@ var LoupeApp = class {
|
|
|
5512
5746
|
const summary = c.title || (c.body.split("\n")[0] ?? "").slice(0, 140) || "(no description)";
|
|
5513
5747
|
item.appendChild(el("div", "summary", summary));
|
|
5514
5748
|
const detail = el("div", "detail");
|
|
5749
|
+
const repliesForState = this.messages.get(c.id) ?? [];
|
|
5750
|
+
const lastMsg = repliesForState.length ? repliesForState[repliesForState.length - 1] : void 0;
|
|
5751
|
+
const attention = needsYou({
|
|
5752
|
+
status: c.status,
|
|
5753
|
+
last: lastMsg ? { fromAgent: lastMsg.author.type === "agent", body: lastMsg.body } : null,
|
|
5754
|
+
agentFailed: this.msgFailed.has(lastMsg?.id ?? "")
|
|
5755
|
+
});
|
|
5756
|
+
if (c.status !== "resolved" && attention.needs && attention.reason !== "review") {
|
|
5757
|
+
const line = el("div", "needsline");
|
|
5758
|
+
line.append(el("span", "needs-dot"), el("span", "", attention.label ?? "Needs you"));
|
|
5759
|
+
detail.appendChild(line);
|
|
5760
|
+
}
|
|
5515
5761
|
if (c.status === "in_review") detail.appendChild(this.reviewBanner(c));
|
|
5516
5762
|
if (c.proposal) detail.appendChild(this.proposalView(c));
|
|
5517
5763
|
if (c.title || c.body.includes("\n")) detail.appendChild(el("div", "body", c.body));
|
|
@@ -6072,7 +6318,12 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6072
6318
|
el("span", "msg-when", fmtAgo(m.createdAt))
|
|
6073
6319
|
);
|
|
6074
6320
|
if (fromAgent) head.appendChild(el("span", "msg-tag", "agent"));
|
|
6075
|
-
|
|
6321
|
+
const body = el("div", "msg-body");
|
|
6322
|
+
for (const seg of mentionSegments(m.body, parseMentions(m.body))) {
|
|
6323
|
+
if (seg.mention) body.appendChild(el("span", "mention", seg.text));
|
|
6324
|
+
else body.appendChild(document.createTextNode(seg.text));
|
|
6325
|
+
}
|
|
6326
|
+
row.append(head, body);
|
|
6076
6327
|
if (this.msgPending.has(m.id)) row.appendChild(el("div", "msg-state", "Sending\u2026"));
|
|
6077
6328
|
if (this.msgFailed.has(m.id)) {
|
|
6078
6329
|
const failed = el("div", "msg-state failed");
|
|
@@ -6109,7 +6360,40 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6109
6360
|
void this.sendReply(c, input.value);
|
|
6110
6361
|
};
|
|
6111
6362
|
foot.append(el("span", "reply-hint", "@ to mention"), send);
|
|
6112
|
-
|
|
6363
|
+
const suggest = el("div", "mention-list");
|
|
6364
|
+
suggest.style.display = "none";
|
|
6365
|
+
const refreshSuggestions = () => {
|
|
6366
|
+
const matches = mentionSuggestions(input.value, input.selectionStart ?? input.value.length, this.people.get(this.cfg.projectKey) ?? []);
|
|
6367
|
+
suggest.textContent = "";
|
|
6368
|
+
if (!matches.length) {
|
|
6369
|
+
suggest.style.display = "none";
|
|
6370
|
+
return;
|
|
6371
|
+
}
|
|
6372
|
+
suggest.style.display = "";
|
|
6373
|
+
for (const person of matches) {
|
|
6374
|
+
const b = el("button", "mention-pick", person.name);
|
|
6375
|
+
b.onclick = (e) => {
|
|
6376
|
+
e.stopPropagation();
|
|
6377
|
+
const before = input.value.slice(0, input.selectionStart ?? input.value.length);
|
|
6378
|
+
const at = before.lastIndexOf("@");
|
|
6379
|
+
const after = input.value.slice(input.selectionStart ?? input.value.length);
|
|
6380
|
+
input.value = `${before.slice(0, at)}@${person.name.replace(/\s+/g, "")} ${after}`;
|
|
6381
|
+
this.msgDrafts.set(c.id, input.value);
|
|
6382
|
+
suggest.style.display = "none";
|
|
6383
|
+
input.focus();
|
|
6384
|
+
};
|
|
6385
|
+
suggest.appendChild(b);
|
|
6386
|
+
}
|
|
6387
|
+
};
|
|
6388
|
+
input.oninput = () => {
|
|
6389
|
+
this.msgDrafts.set(c.id, input.value);
|
|
6390
|
+
refreshSuggestions();
|
|
6391
|
+
};
|
|
6392
|
+
input.onkeyup = () => refreshSuggestions();
|
|
6393
|
+
input.onblur = () => setTimeout(() => {
|
|
6394
|
+
suggest.style.display = "none";
|
|
6395
|
+
}, 150);
|
|
6396
|
+
reply.append(input, suggest, foot);
|
|
6113
6397
|
wrap.appendChild(reply);
|
|
6114
6398
|
const timeline = threadTimeline(c, asRows);
|
|
6115
6399
|
const tl = el("div", "tl");
|
|
@@ -6151,9 +6435,18 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6151
6435
|
wrap.appendChild(copyRow);
|
|
6152
6436
|
return wrap;
|
|
6153
6437
|
}
|
|
6438
|
+
async loadPeople() {
|
|
6439
|
+
if (this.people.has(this.cfg.projectKey)) return;
|
|
6440
|
+
try {
|
|
6441
|
+
this.people.set(this.cfg.projectKey, await this.store.listPeople(this.cfg.projectKey));
|
|
6442
|
+
} catch {
|
|
6443
|
+
this.people.set(this.cfg.projectKey, []);
|
|
6444
|
+
}
|
|
6445
|
+
}
|
|
6154
6446
|
/** Fetch replies once, when a card is first expanded. */
|
|
6155
6447
|
async loadMessages(c) {
|
|
6156
6448
|
if (this.messages.has(c.id)) return;
|
|
6449
|
+
void this.loadPeople();
|
|
6157
6450
|
try {
|
|
6158
6451
|
this.messages.set(c.id, await this.store.listMessages(c.id));
|
|
6159
6452
|
} catch {
|