@loupekit/sdk 0.10.24 → 0.10.25
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 +138 -11
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +138 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.global.js
CHANGED
|
@@ -794,6 +794,19 @@ 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
|
+
/* attachments on a reply */
|
|
798
|
+
.reply-attach {
|
|
799
|
+
padding: 3px 7px; border: 1px solid var(--line); border-radius: 7px;
|
|
800
|
+
background: var(--bg); color: var(--muted); font-size: 12px; cursor: pointer; line-height: 1.4;
|
|
801
|
+
}
|
|
802
|
+
.reply-attach:hover { border-color: var(--accent); color: var(--accent); }
|
|
803
|
+
.reply-chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 5px; }
|
|
804
|
+
.msg-atts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
|
|
805
|
+
.msg-att {
|
|
806
|
+
max-width: 190px; max-height: 130px; border: 1px solid var(--line); border-radius: 8px;
|
|
807
|
+
object-fit: cover; cursor: pointer; background: var(--bg-2);
|
|
808
|
+
}
|
|
809
|
+
|
|
797
810
|
/* reactions */
|
|
798
811
|
.rxns { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: 6px; position: relative; }
|
|
799
812
|
.rxn {
|
|
@@ -3137,8 +3150,9 @@ var Loupe = (() => {
|
|
|
3137
3150
|
createdAt: comment.createdAt
|
|
3138
3151
|
};
|
|
3139
3152
|
}
|
|
3140
|
-
function threadConversation(comment, replies) {
|
|
3141
|
-
|
|
3153
|
+
function threadConversation(comment, replies, opts = {}) {
|
|
3154
|
+
const visible = opts.includeDeleted ? replies : replies.filter((m) => !m.deletedAt);
|
|
3155
|
+
return [firstMessageFromComment(comment), ...visible].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
3142
3156
|
}
|
|
3143
3157
|
function iterationLabel(link) {
|
|
3144
3158
|
if (!link.parentThreadId || link.iterationType !== "revision")
|
|
@@ -3514,6 +3528,7 @@ var Loupe = (() => {
|
|
|
3514
3528
|
threadId,
|
|
3515
3529
|
author: message.author,
|
|
3516
3530
|
body: message.body,
|
|
3531
|
+
attachments: message.attachments?.length ? message.attachments : void 0,
|
|
3517
3532
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3518
3533
|
};
|
|
3519
3534
|
const all = await this.listMessages(threadId);
|
|
@@ -3672,7 +3687,7 @@ var Loupe = (() => {
|
|
|
3672
3687
|
return await res.json();
|
|
3673
3688
|
}
|
|
3674
3689
|
async listReactions(threadId) {
|
|
3675
|
-
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(threadId)}/
|
|
3690
|
+
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(threadId)}/reactions`, this.opts({ headers: this.headers() }));
|
|
3676
3691
|
if (res.status === 404) return [];
|
|
3677
3692
|
if (!res.ok) throw new Error(`listReactions failed: ${res.status}`);
|
|
3678
3693
|
return (await res.json()).reactions;
|
|
@@ -3727,7 +3742,7 @@ var Loupe = (() => {
|
|
|
3727
3742
|
};
|
|
3728
3743
|
|
|
3729
3744
|
// src/app.ts
|
|
3730
|
-
var SDK_VERSION = true ? "0.10.
|
|
3745
|
+
var SDK_VERSION = true ? "0.10.25" : "dev";
|
|
3731
3746
|
var ACCENTS = [
|
|
3732
3747
|
{ id: "indigo", dark: "#6b73e6", light: "#4a55d6", soft: "rgba(107,115,230,0.12)" },
|
|
3733
3748
|
{ id: "violet", dark: "#a06be6", light: "#7c3fd4", soft: "rgba(160,107,230,0.14)" },
|
|
@@ -4169,6 +4184,7 @@ var Loupe = (() => {
|
|
|
4169
4184
|
this.renderList();
|
|
4170
4185
|
this.renderHome();
|
|
4171
4186
|
this.startPresence();
|
|
4187
|
+
this.startLiveThreads();
|
|
4172
4188
|
void this.loadNotifications();
|
|
4173
4189
|
if (this.scope === "all") void this.loadAllComments();
|
|
4174
4190
|
this.observe();
|
|
@@ -6511,6 +6527,30 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6511
6527
|
};
|
|
6512
6528
|
pills.appendChild(add);
|
|
6513
6529
|
row.appendChild(pills);
|
|
6530
|
+
if (m.attachments?.length) {
|
|
6531
|
+
const strip = el("div", "msg-atts");
|
|
6532
|
+
for (const a of m.attachments) {
|
|
6533
|
+
if (a.kind === "image") {
|
|
6534
|
+
const img = el("img", "msg-att");
|
|
6535
|
+
img.src = a.url;
|
|
6536
|
+
img.alt = a.name ?? "attachment";
|
|
6537
|
+
img.loading = "lazy";
|
|
6538
|
+
img.onclick = (e) => {
|
|
6539
|
+
e.stopPropagation();
|
|
6540
|
+
window.open(a.url, "_blank", "noopener");
|
|
6541
|
+
};
|
|
6542
|
+
strip.appendChild(img);
|
|
6543
|
+
} else if (a.kind === "video") {
|
|
6544
|
+
const video = el("video", "msg-att");
|
|
6545
|
+
video.src = a.url;
|
|
6546
|
+
video.controls = true;
|
|
6547
|
+
video.preload = "metadata";
|
|
6548
|
+
video.onclick = (e) => e.stopPropagation();
|
|
6549
|
+
strip.appendChild(video);
|
|
6550
|
+
}
|
|
6551
|
+
}
|
|
6552
|
+
if (strip.childElementCount) row.appendChild(strip);
|
|
6553
|
+
}
|
|
6514
6554
|
if (this.msgPending.has(m.id)) row.appendChild(el("div", "msg-state", "Sending\u2026"));
|
|
6515
6555
|
if (this.msgFailed.has(m.id)) {
|
|
6516
6556
|
const failed = el("div", "msg-state failed");
|
|
@@ -6536,7 +6576,7 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6536
6576
|
input.onkeydown = (e) => {
|
|
6537
6577
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
6538
6578
|
e.preventDefault();
|
|
6539
|
-
void this.sendReply(c, input.value);
|
|
6579
|
+
void this.sendReply(c, input.value, [...pending]);
|
|
6540
6580
|
}
|
|
6541
6581
|
};
|
|
6542
6582
|
const foot = el("div", "reply-foot");
|
|
@@ -6544,9 +6584,51 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6544
6584
|
send.setAttribute("aria-label", "Send reply");
|
|
6545
6585
|
send.onclick = (e) => {
|
|
6546
6586
|
e.stopPropagation();
|
|
6547
|
-
void this.sendReply(c, input.value);
|
|
6587
|
+
void this.sendReply(c, input.value, [...pending]);
|
|
6588
|
+
};
|
|
6589
|
+
const fileIn = el("input");
|
|
6590
|
+
fileIn.type = "file";
|
|
6591
|
+
fileIn.multiple = true;
|
|
6592
|
+
fileIn.accept = "image/*,video/*";
|
|
6593
|
+
fileIn.style.display = "none";
|
|
6594
|
+
const attachBtn = el("button", "reply-attach", "\u{1F4CE}");
|
|
6595
|
+
attachBtn.title = "Attach an image or video";
|
|
6596
|
+
attachBtn.setAttribute("aria-label", "Attach a file");
|
|
6597
|
+
attachBtn.onclick = (e) => {
|
|
6598
|
+
e.stopPropagation();
|
|
6599
|
+
fileIn.click();
|
|
6600
|
+
};
|
|
6601
|
+
const pending = [];
|
|
6602
|
+
const chips = el("div", "reply-chips");
|
|
6603
|
+
const repaintChips = () => {
|
|
6604
|
+
chips.textContent = "";
|
|
6605
|
+
pending.forEach((f, i) => {
|
|
6606
|
+
const chip = el("span", "chip", `${attachmentKind(f.type) === "video" ? "\u{1F3AC}" : "\u{1F5BC}"} ${f.name}`);
|
|
6607
|
+
const x = el("button", "chip-x", "\u2715");
|
|
6608
|
+
x.title = "Remove";
|
|
6609
|
+
x.onclick = (ev) => {
|
|
6610
|
+
ev.stopPropagation();
|
|
6611
|
+
pending.splice(i, 1);
|
|
6612
|
+
repaintChips();
|
|
6613
|
+
};
|
|
6614
|
+
chip.appendChild(x);
|
|
6615
|
+
chips.appendChild(chip);
|
|
6616
|
+
});
|
|
6617
|
+
};
|
|
6618
|
+
fileIn.onchange = () => {
|
|
6619
|
+
for (const f of Array.from(fileIn.files ?? [])) {
|
|
6620
|
+
const cap = attachmentKind(f.type) === "video" ? MAX_VIDEO_BYTES : MAX_IMAGE_BYTES;
|
|
6621
|
+
if (f.size > cap) {
|
|
6622
|
+
this.addActivity({ kind: "error", level: "error", label: `${f.name} is too large to attach` });
|
|
6623
|
+
continue;
|
|
6624
|
+
}
|
|
6625
|
+
pending.push(f);
|
|
6626
|
+
}
|
|
6627
|
+
fileIn.value = "";
|
|
6628
|
+
repaintChips();
|
|
6548
6629
|
};
|
|
6549
|
-
|
|
6630
|
+
chips.addEventListener("click", (e) => e.stopPropagation());
|
|
6631
|
+
foot.append(el("span", "reply-hint", "@ to mention"), attachBtn, send);
|
|
6550
6632
|
const suggest = el("div", "mention-list");
|
|
6551
6633
|
suggest.style.display = "none";
|
|
6552
6634
|
const refreshSuggestions = () => {
|
|
@@ -6580,7 +6662,7 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6580
6662
|
input.onblur = () => setTimeout(() => {
|
|
6581
6663
|
suggest.style.display = "none";
|
|
6582
6664
|
}, 150);
|
|
6583
|
-
reply.append(input, suggest, foot);
|
|
6665
|
+
reply.append(input, suggest, chips, fileIn, foot);
|
|
6584
6666
|
wrap.appendChild(reply);
|
|
6585
6667
|
const timeline = threadTimeline(c, asRows);
|
|
6586
6668
|
const tl = el("div", "tl");
|
|
@@ -6691,6 +6773,47 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6691
6773
|
this.peerId = void 0;
|
|
6692
6774
|
this.peers = [];
|
|
6693
6775
|
}
|
|
6776
|
+
/**
|
|
6777
|
+
* Follow the bridge's thread channel while the panel is open.
|
|
6778
|
+
*
|
|
6779
|
+
* The SSE channel lives in the MCP process, so a reply posted by anyone — an agent, a
|
|
6780
|
+
* teammate's browser — arrives here rather than being discovered by the 4 s list
|
|
6781
|
+
* poll. Only the threads whose messages are already loaded are refetched: pulling a
|
|
6782
|
+
* conversation nobody has open would be work for nothing.
|
|
6783
|
+
*/
|
|
6784
|
+
startLiveThreads() {
|
|
6785
|
+
const bridge = this.cfg.bridge;
|
|
6786
|
+
if (!bridge || this.liveSource) return;
|
|
6787
|
+
if (typeof EventSource === "undefined") return;
|
|
6788
|
+
try {
|
|
6789
|
+
const source = new EventSource(`${bridge.replace(/\/$/, "")}/thread-updates`);
|
|
6790
|
+
source.onmessage = (ev) => {
|
|
6791
|
+
let parsed;
|
|
6792
|
+
try {
|
|
6793
|
+
parsed = JSON.parse(ev.data);
|
|
6794
|
+
} catch {
|
|
6795
|
+
return;
|
|
6796
|
+
}
|
|
6797
|
+
if (!parsed || parsed.type !== "thread" || typeof parsed.threadId !== "string") return;
|
|
6798
|
+
if (!this.messages.has(parsed.threadId)) return;
|
|
6799
|
+
void this.refreshMessages(parsed.threadId);
|
|
6800
|
+
};
|
|
6801
|
+
source.onerror = () => {
|
|
6802
|
+
};
|
|
6803
|
+
this.liveSource = source;
|
|
6804
|
+
} catch {
|
|
6805
|
+
}
|
|
6806
|
+
}
|
|
6807
|
+
/** Re-read one thread's replies, replacing the cache. */
|
|
6808
|
+
async refreshMessages(threadId) {
|
|
6809
|
+
try {
|
|
6810
|
+
const list = await this.store.listMessages(threadId);
|
|
6811
|
+
if (!Array.isArray(list)) return;
|
|
6812
|
+
this.messages.set(threadId, list);
|
|
6813
|
+
this.renderList();
|
|
6814
|
+
} catch {
|
|
6815
|
+
}
|
|
6816
|
+
}
|
|
6694
6817
|
reactionsOf(threadId, messageId) {
|
|
6695
6818
|
return this.reactions.get(`${threadId}:${messageId}`) ?? [];
|
|
6696
6819
|
}
|
|
@@ -6760,20 +6883,22 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6760
6883
|
* The row appears immediately; if the store rejects it the row stays with a Retry,
|
|
6761
6884
|
* because losing what someone typed is worse than showing a failed row.
|
|
6762
6885
|
*/
|
|
6763
|
-
async sendReply(c, raw) {
|
|
6886
|
+
async sendReply(c, raw, files = []) {
|
|
6764
6887
|
const body = raw.trim();
|
|
6765
|
-
if (!body) return;
|
|
6888
|
+
if (!body && !files.length) return;
|
|
6766
6889
|
const author = {
|
|
6767
6890
|
id: this.cfg.user.id,
|
|
6768
6891
|
name: this.cfg.user.name,
|
|
6769
6892
|
email: this.cfg.user.email,
|
|
6770
6893
|
type: "user"
|
|
6771
6894
|
};
|
|
6895
|
+
const attachments = files.length ? await this.uploadAttachments(files) : void 0;
|
|
6772
6896
|
const optimistic = {
|
|
6773
6897
|
id: `pending-${Date.now().toString(36)}`,
|
|
6774
6898
|
threadId: c.id,
|
|
6775
6899
|
author,
|
|
6776
6900
|
body,
|
|
6901
|
+
attachments,
|
|
6777
6902
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6778
6903
|
};
|
|
6779
6904
|
this.messages.set(c.id, [...this.messages.get(c.id) ?? [], optimistic]);
|
|
@@ -6781,7 +6906,7 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6781
6906
|
this.msgDrafts.delete(c.id);
|
|
6782
6907
|
this.renderList();
|
|
6783
6908
|
try {
|
|
6784
|
-
const saved = await this.store.addMessage(c.id, { author, body });
|
|
6909
|
+
const saved = await this.store.addMessage(c.id, { author, body, attachments });
|
|
6785
6910
|
this.messages.set(c.id, (this.messages.get(c.id) ?? []).map((m) => m.id === optimistic.id ? saved : m));
|
|
6786
6911
|
this.msgPending.delete(optimistic.id);
|
|
6787
6912
|
this.addActivity({ kind: "message.create", label: `Replied on \u201C${c.title || body.slice(0, 40)}\u201D` });
|
|
@@ -6893,6 +7018,8 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6893
7018
|
if (elx) elx.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
6894
7019
|
}
|
|
6895
7020
|
destroy() {
|
|
7021
|
+
this.liveSource?.close();
|
|
7022
|
+
this.liveSource = void 0;
|
|
6896
7023
|
this.stopPresence();
|
|
6897
7024
|
this.stopRecording?.();
|
|
6898
7025
|
this.setMode("off");
|