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