@loupekit/sdk 0.10.24-next.71 → 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.
@@ -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
- return [firstMessageFromComment(comment), ...replies].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
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)}/messages/all/reactions`, this.opts({ headers: this.headers() }));
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.24" : "dev";
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
- foot.append(el("span", "reply-hint", "@ to mention"), send);
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");