@loupekit/sdk 0.10.24 → 0.10.25-next.72
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.js
CHANGED
|
@@ -760,6 +760,19 @@ 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
|
+
/* attachments on a reply */
|
|
764
|
+
.reply-attach {
|
|
765
|
+
padding: 3px 7px; border: 1px solid var(--line); border-radius: 7px;
|
|
766
|
+
background: var(--bg); color: var(--muted); font-size: 12px; cursor: pointer; line-height: 1.4;
|
|
767
|
+
}
|
|
768
|
+
.reply-attach:hover { border-color: var(--accent); color: var(--accent); }
|
|
769
|
+
.reply-chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 5px; }
|
|
770
|
+
.msg-atts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
|
|
771
|
+
.msg-att {
|
|
772
|
+
max-width: 190px; max-height: 130px; border: 1px solid var(--line); border-radius: 8px;
|
|
773
|
+
object-fit: cover; cursor: pointer; background: var(--bg-2);
|
|
774
|
+
}
|
|
775
|
+
|
|
763
776
|
/* reactions */
|
|
764
777
|
.rxns { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: 6px; position: relative; }
|
|
765
778
|
.rxn {
|
|
@@ -3103,8 +3116,9 @@ function firstMessageFromComment(comment) {
|
|
|
3103
3116
|
createdAt: comment.createdAt
|
|
3104
3117
|
};
|
|
3105
3118
|
}
|
|
3106
|
-
function threadConversation(comment, replies) {
|
|
3107
|
-
|
|
3119
|
+
function threadConversation(comment, replies, opts = {}) {
|
|
3120
|
+
const visible = opts.includeDeleted ? replies : replies.filter((m) => !m.deletedAt);
|
|
3121
|
+
return [firstMessageFromComment(comment), ...visible].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
3108
3122
|
}
|
|
3109
3123
|
function iterationLabel(link) {
|
|
3110
3124
|
if (!link.parentThreadId || link.iterationType !== "revision")
|
|
@@ -3480,6 +3494,7 @@ var LocalStorageAdapter = class {
|
|
|
3480
3494
|
threadId,
|
|
3481
3495
|
author: message.author,
|
|
3482
3496
|
body: message.body,
|
|
3497
|
+
attachments: message.attachments?.length ? message.attachments : void 0,
|
|
3483
3498
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3484
3499
|
};
|
|
3485
3500
|
const all = await this.listMessages(threadId);
|
|
@@ -3638,7 +3653,7 @@ var HttpAdapter = class {
|
|
|
3638
3653
|
return await res.json();
|
|
3639
3654
|
}
|
|
3640
3655
|
async listReactions(threadId) {
|
|
3641
|
-
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(threadId)}/
|
|
3656
|
+
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(threadId)}/reactions`, this.opts({ headers: this.headers() }));
|
|
3642
3657
|
if (res.status === 404) return [];
|
|
3643
3658
|
if (!res.ok) throw new Error(`listReactions failed: ${res.status}`);
|
|
3644
3659
|
return (await res.json()).reactions;
|
|
@@ -3693,7 +3708,7 @@ var HttpAdapter = class {
|
|
|
3693
3708
|
};
|
|
3694
3709
|
|
|
3695
3710
|
// src/app.ts
|
|
3696
|
-
var SDK_VERSION = true ? "0.10.
|
|
3711
|
+
var SDK_VERSION = true ? "0.10.25" : "dev";
|
|
3697
3712
|
var ACCENTS = [
|
|
3698
3713
|
{ id: "indigo", dark: "#6b73e6", light: "#4a55d6", soft: "rgba(107,115,230,0.12)" },
|
|
3699
3714
|
{ id: "violet", dark: "#a06be6", light: "#7c3fd4", soft: "rgba(160,107,230,0.14)" },
|
|
@@ -4135,6 +4150,7 @@ var LoupeApp = class {
|
|
|
4135
4150
|
this.renderList();
|
|
4136
4151
|
this.renderHome();
|
|
4137
4152
|
this.startPresence();
|
|
4153
|
+
this.startLiveThreads();
|
|
4138
4154
|
void this.loadNotifications();
|
|
4139
4155
|
if (this.scope === "all") void this.loadAllComments();
|
|
4140
4156
|
this.observe();
|
|
@@ -6477,6 +6493,30 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6477
6493
|
};
|
|
6478
6494
|
pills.appendChild(add);
|
|
6479
6495
|
row.appendChild(pills);
|
|
6496
|
+
if (m.attachments?.length) {
|
|
6497
|
+
const strip = el("div", "msg-atts");
|
|
6498
|
+
for (const a of m.attachments) {
|
|
6499
|
+
if (a.kind === "image") {
|
|
6500
|
+
const img = el("img", "msg-att");
|
|
6501
|
+
img.src = a.url;
|
|
6502
|
+
img.alt = a.name ?? "attachment";
|
|
6503
|
+
img.loading = "lazy";
|
|
6504
|
+
img.onclick = (e) => {
|
|
6505
|
+
e.stopPropagation();
|
|
6506
|
+
window.open(a.url, "_blank", "noopener");
|
|
6507
|
+
};
|
|
6508
|
+
strip.appendChild(img);
|
|
6509
|
+
} else if (a.kind === "video") {
|
|
6510
|
+
const video = el("video", "msg-att");
|
|
6511
|
+
video.src = a.url;
|
|
6512
|
+
video.controls = true;
|
|
6513
|
+
video.preload = "metadata";
|
|
6514
|
+
video.onclick = (e) => e.stopPropagation();
|
|
6515
|
+
strip.appendChild(video);
|
|
6516
|
+
}
|
|
6517
|
+
}
|
|
6518
|
+
if (strip.childElementCount) row.appendChild(strip);
|
|
6519
|
+
}
|
|
6480
6520
|
if (this.msgPending.has(m.id)) row.appendChild(el("div", "msg-state", "Sending\u2026"));
|
|
6481
6521
|
if (this.msgFailed.has(m.id)) {
|
|
6482
6522
|
const failed = el("div", "msg-state failed");
|
|
@@ -6502,7 +6542,7 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6502
6542
|
input.onkeydown = (e) => {
|
|
6503
6543
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
6504
6544
|
e.preventDefault();
|
|
6505
|
-
void this.sendReply(c, input.value);
|
|
6545
|
+
void this.sendReply(c, input.value, [...pending]);
|
|
6506
6546
|
}
|
|
6507
6547
|
};
|
|
6508
6548
|
const foot = el("div", "reply-foot");
|
|
@@ -6510,9 +6550,51 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6510
6550
|
send.setAttribute("aria-label", "Send reply");
|
|
6511
6551
|
send.onclick = (e) => {
|
|
6512
6552
|
e.stopPropagation();
|
|
6513
|
-
void this.sendReply(c, input.value);
|
|
6553
|
+
void this.sendReply(c, input.value, [...pending]);
|
|
6554
|
+
};
|
|
6555
|
+
const fileIn = el("input");
|
|
6556
|
+
fileIn.type = "file";
|
|
6557
|
+
fileIn.multiple = true;
|
|
6558
|
+
fileIn.accept = "image/*,video/*";
|
|
6559
|
+
fileIn.style.display = "none";
|
|
6560
|
+
const attachBtn = el("button", "reply-attach", "\u{1F4CE}");
|
|
6561
|
+
attachBtn.title = "Attach an image or video";
|
|
6562
|
+
attachBtn.setAttribute("aria-label", "Attach a file");
|
|
6563
|
+
attachBtn.onclick = (e) => {
|
|
6564
|
+
e.stopPropagation();
|
|
6565
|
+
fileIn.click();
|
|
6566
|
+
};
|
|
6567
|
+
const pending = [];
|
|
6568
|
+
const chips = el("div", "reply-chips");
|
|
6569
|
+
const repaintChips = () => {
|
|
6570
|
+
chips.textContent = "";
|
|
6571
|
+
pending.forEach((f, i) => {
|
|
6572
|
+
const chip = el("span", "chip", `${attachmentKind(f.type) === "video" ? "\u{1F3AC}" : "\u{1F5BC}"} ${f.name}`);
|
|
6573
|
+
const x = el("button", "chip-x", "\u2715");
|
|
6574
|
+
x.title = "Remove";
|
|
6575
|
+
x.onclick = (ev) => {
|
|
6576
|
+
ev.stopPropagation();
|
|
6577
|
+
pending.splice(i, 1);
|
|
6578
|
+
repaintChips();
|
|
6579
|
+
};
|
|
6580
|
+
chip.appendChild(x);
|
|
6581
|
+
chips.appendChild(chip);
|
|
6582
|
+
});
|
|
6583
|
+
};
|
|
6584
|
+
fileIn.onchange = () => {
|
|
6585
|
+
for (const f of Array.from(fileIn.files ?? [])) {
|
|
6586
|
+
const cap = attachmentKind(f.type) === "video" ? MAX_VIDEO_BYTES : MAX_IMAGE_BYTES;
|
|
6587
|
+
if (f.size > cap) {
|
|
6588
|
+
this.addActivity({ kind: "error", level: "error", label: `${f.name} is too large to attach` });
|
|
6589
|
+
continue;
|
|
6590
|
+
}
|
|
6591
|
+
pending.push(f);
|
|
6592
|
+
}
|
|
6593
|
+
fileIn.value = "";
|
|
6594
|
+
repaintChips();
|
|
6514
6595
|
};
|
|
6515
|
-
|
|
6596
|
+
chips.addEventListener("click", (e) => e.stopPropagation());
|
|
6597
|
+
foot.append(el("span", "reply-hint", "@ to mention"), attachBtn, send);
|
|
6516
6598
|
const suggest = el("div", "mention-list");
|
|
6517
6599
|
suggest.style.display = "none";
|
|
6518
6600
|
const refreshSuggestions = () => {
|
|
@@ -6546,7 +6628,7 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6546
6628
|
input.onblur = () => setTimeout(() => {
|
|
6547
6629
|
suggest.style.display = "none";
|
|
6548
6630
|
}, 150);
|
|
6549
|
-
reply.append(input, suggest, foot);
|
|
6631
|
+
reply.append(input, suggest, chips, fileIn, foot);
|
|
6550
6632
|
wrap.appendChild(reply);
|
|
6551
6633
|
const timeline = threadTimeline(c, asRows);
|
|
6552
6634
|
const tl = el("div", "tl");
|
|
@@ -6657,6 +6739,47 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6657
6739
|
this.peerId = void 0;
|
|
6658
6740
|
this.peers = [];
|
|
6659
6741
|
}
|
|
6742
|
+
/**
|
|
6743
|
+
* Follow the bridge's thread channel while the panel is open.
|
|
6744
|
+
*
|
|
6745
|
+
* The SSE channel lives in the MCP process, so a reply posted by anyone — an agent, a
|
|
6746
|
+
* teammate's browser — arrives here rather than being discovered by the 4 s list
|
|
6747
|
+
* poll. Only the threads whose messages are already loaded are refetched: pulling a
|
|
6748
|
+
* conversation nobody has open would be work for nothing.
|
|
6749
|
+
*/
|
|
6750
|
+
startLiveThreads() {
|
|
6751
|
+
const bridge = this.cfg.bridge;
|
|
6752
|
+
if (!bridge || this.liveSource) return;
|
|
6753
|
+
if (typeof EventSource === "undefined") return;
|
|
6754
|
+
try {
|
|
6755
|
+
const source = new EventSource(`${bridge.replace(/\/$/, "")}/thread-updates`);
|
|
6756
|
+
source.onmessage = (ev) => {
|
|
6757
|
+
let parsed;
|
|
6758
|
+
try {
|
|
6759
|
+
parsed = JSON.parse(ev.data);
|
|
6760
|
+
} catch {
|
|
6761
|
+
return;
|
|
6762
|
+
}
|
|
6763
|
+
if (!parsed || parsed.type !== "thread" || typeof parsed.threadId !== "string") return;
|
|
6764
|
+
if (!this.messages.has(parsed.threadId)) return;
|
|
6765
|
+
void this.refreshMessages(parsed.threadId);
|
|
6766
|
+
};
|
|
6767
|
+
source.onerror = () => {
|
|
6768
|
+
};
|
|
6769
|
+
this.liveSource = source;
|
|
6770
|
+
} catch {
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
/** Re-read one thread's replies, replacing the cache. */
|
|
6774
|
+
async refreshMessages(threadId) {
|
|
6775
|
+
try {
|
|
6776
|
+
const list = await this.store.listMessages(threadId);
|
|
6777
|
+
if (!Array.isArray(list)) return;
|
|
6778
|
+
this.messages.set(threadId, list);
|
|
6779
|
+
this.renderList();
|
|
6780
|
+
} catch {
|
|
6781
|
+
}
|
|
6782
|
+
}
|
|
6660
6783
|
reactionsOf(threadId, messageId) {
|
|
6661
6784
|
return this.reactions.get(`${threadId}:${messageId}`) ?? [];
|
|
6662
6785
|
}
|
|
@@ -6726,20 +6849,22 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6726
6849
|
* The row appears immediately; if the store rejects it the row stays with a Retry,
|
|
6727
6850
|
* because losing what someone typed is worse than showing a failed row.
|
|
6728
6851
|
*/
|
|
6729
|
-
async sendReply(c, raw) {
|
|
6852
|
+
async sendReply(c, raw, files = []) {
|
|
6730
6853
|
const body = raw.trim();
|
|
6731
|
-
if (!body) return;
|
|
6854
|
+
if (!body && !files.length) return;
|
|
6732
6855
|
const author = {
|
|
6733
6856
|
id: this.cfg.user.id,
|
|
6734
6857
|
name: this.cfg.user.name,
|
|
6735
6858
|
email: this.cfg.user.email,
|
|
6736
6859
|
type: "user"
|
|
6737
6860
|
};
|
|
6861
|
+
const attachments = files.length ? await this.uploadAttachments(files) : void 0;
|
|
6738
6862
|
const optimistic = {
|
|
6739
6863
|
id: `pending-${Date.now().toString(36)}`,
|
|
6740
6864
|
threadId: c.id,
|
|
6741
6865
|
author,
|
|
6742
6866
|
body,
|
|
6867
|
+
attachments,
|
|
6743
6868
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6744
6869
|
};
|
|
6745
6870
|
this.messages.set(c.id, [...this.messages.get(c.id) ?? [], optimistic]);
|
|
@@ -6747,7 +6872,7 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6747
6872
|
this.msgDrafts.delete(c.id);
|
|
6748
6873
|
this.renderList();
|
|
6749
6874
|
try {
|
|
6750
|
-
const saved = await this.store.addMessage(c.id, { author, body });
|
|
6875
|
+
const saved = await this.store.addMessage(c.id, { author, body, attachments });
|
|
6751
6876
|
this.messages.set(c.id, (this.messages.get(c.id) ?? []).map((m) => m.id === optimistic.id ? saved : m));
|
|
6752
6877
|
this.msgPending.delete(optimistic.id);
|
|
6753
6878
|
this.addActivity({ kind: "message.create", label: `Replied on \u201C${c.title || body.slice(0, 40)}\u201D` });
|
|
@@ -6859,6 +6984,8 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6859
6984
|
if (elx) elx.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
6860
6985
|
}
|
|
6861
6986
|
destroy() {
|
|
6987
|
+
this.liveSource?.close();
|
|
6988
|
+
this.liveSource = void 0;
|
|
6862
6989
|
this.stopPresence();
|
|
6863
6990
|
this.stopRecording?.();
|
|
6864
6991
|
this.setMode("off");
|