relay-companion 0.1.34 → 0.1.35

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.
@@ -452,6 +452,14 @@
452
452
  .cv-input::placeholder { color:var(--muted-3); }
453
453
  .cv-input:focus { border-color:rgba(48,85,102,.42); box-shadow:0 0 0 3px rgba(48,85,102,.08); }
454
454
  .cv-form-actions { display:flex; align-items:center; justify-content:flex-end; gap:14px; margin-top:1px; }
455
+ .cv-delete {
456
+ appearance:none; border:0; background:transparent; color:#b4332a; font-family:var(--sans);
457
+ font-size:12.5px; font-weight:600; cursor:pointer; transition:opacity .15s var(--settle), color .15s var(--settle);
458
+ margin-right:auto; padding:0;
459
+ }
460
+ .cv-delete:hover { opacity:.72; }
461
+ .cv-delete.armed { color:#8f261f; }
462
+ .cv-delete:disabled { opacity:.4; cursor:default; }
455
463
  .cv-cancel { appearance:none; border:0; background:transparent; color:var(--muted); font-family:var(--sans); font-size:12.5px; font-weight:500; cursor:pointer; transition:color .15s var(--settle); }
456
464
  .cv-cancel:hover { color:var(--ink-2); }
457
465
  .cv-save {
@@ -601,6 +609,7 @@
601
609
  <div class="cv-hint" id="cvHint"></div>
602
610
  <div class="cv-error" id="cvError"></div>
603
611
  <div class="cv-form-actions">
612
+ <button class="cv-delete gone" type="button" id="cvDelete">Delete</button>
604
613
  <button class="cv-cancel" type="button" id="cvCancel">Cancel</button>
605
614
  <button class="cv-save" type="submit" id="cvSave">Save</button>
606
615
  </div>
@@ -643,6 +652,7 @@
643
652
  const cvErrorEl = document.getElementById("cvError");
644
653
  const cvSaveEl = document.getElementById("cvSave");
645
654
  const cvCancelEl = document.getElementById("cvCancel");
655
+ const cvDeleteEl = document.getElementById("cvDelete");
646
656
 
647
657
  const REDUCED = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
648
658
  const EXPANDED = { w: 344, h: 524 };
@@ -1848,6 +1858,8 @@
1848
1858
  const COMMON_DOMAINS = ["gmail.com","outlook.com","hotmail.com","yahoo.com","icloud.com","me.com","proton.me","protonmail.com","live.com","aol.com"];
1849
1859
  let cvEmailsList = []; // committed, lowercased, deduped addresses (the chips)
1850
1860
  let cvSuggIdx = 0; // highlighted suggestion row
1861
+ let cvDeleteArmed = false;
1862
+ let cvDeleteTimer = null;
1851
1863
  function isValidEmail(s) { return EMAIL_RE.test(String(s).trim()); }
1852
1864
  function cvSuggestions() {
1853
1865
  const draft = cvEmailEl.value;
@@ -1907,6 +1919,21 @@
1907
1919
  cvEmailsList = cvEmailsList.filter((x) => x !== e);
1908
1920
  renderEmailChips();
1909
1921
  }
1922
+ function editingContactId() {
1923
+ return cvEditing && (cvEditing.id || cvEditing.contactId) ? String(cvEditing.id || cvEditing.contactId) : "";
1924
+ }
1925
+ function resetDeleteButton() {
1926
+ cvDeleteArmed = false;
1927
+ if (cvDeleteTimer) { clearTimeout(cvDeleteTimer); cvDeleteTimer = null; }
1928
+ cvDeleteEl.textContent = "Delete";
1929
+ cvDeleteEl.classList.remove("armed");
1930
+ cvDeleteEl.disabled = false;
1931
+ }
1932
+ function syncDeleteButton() {
1933
+ resetDeleteButton();
1934
+ const canDelete = Boolean(editingContactId());
1935
+ cvDeleteEl.classList.toggle("gone", !canDelete);
1936
+ }
1910
1937
  cvEmailsEl.addEventListener("mousedown", (e) => {
1911
1938
  if (e.target === cvEmailsEl) { e.preventDefault(); cvEmailEl.focus(); }
1912
1939
  });
@@ -1944,6 +1971,7 @@
1944
1971
  cvErrorEl.textContent = "";
1945
1972
  cvSuggestEl.classList.add("hidden"); cvSuggestEl.innerHTML = "";
1946
1973
  renderEmailChips();
1974
+ syncDeleteButton();
1947
1975
  cvFormEl.classList.remove("hidden");
1948
1976
  if (window.relay.setFocusable) window.relay.setFocusable(true);
1949
1977
  setInteractive(true);
@@ -1957,10 +1985,37 @@
1957
1985
  cvErrorEl.textContent = "";
1958
1986
  cvHintEl.textContent = "";
1959
1987
  cvSuggestEl.classList.add("hidden"); cvSuggestEl.innerHTML = "";
1988
+ cvDeleteEl.classList.add("gone");
1989
+ resetDeleteButton();
1960
1990
  if (window.relay.setFocusable) window.relay.setFocusable(false);
1961
1991
  }
1962
1992
  cvAddEl.addEventListener("click", () => openContactForm(null));
1963
1993
  cvCancelEl.addEventListener("click", closeContactForm);
1994
+ cvDeleteEl.addEventListener("click", async () => {
1995
+ const contactId = editingContactId();
1996
+ if (!contactId || cvDeleteEl.disabled) return;
1997
+ if (!cvDeleteArmed) {
1998
+ cvDeleteArmed = true;
1999
+ cvDeleteEl.textContent = "Delete?";
2000
+ cvDeleteEl.classList.add("armed");
2001
+ cvDeleteTimer = setTimeout(resetDeleteButton, 3500);
2002
+ return;
2003
+ }
2004
+ cvDeleteEl.disabled = true;
2005
+ cvSaveEl.disabled = true;
2006
+ cvErrorEl.textContent = "";
2007
+ try {
2008
+ const next = await window.relay.contactDelete({ contactId });
2009
+ if (Array.isArray(next)) contactsList = next;
2010
+ closeContactForm();
2011
+ renderContacts();
2012
+ } catch (err) {
2013
+ cvErrorEl.textContent = (err && err.message) ? err.message : "Could not delete.";
2014
+ cvDeleteEl.disabled = false;
2015
+ cvSaveEl.disabled = false;
2016
+ resetDeleteButton();
2017
+ }
2018
+ });
1964
2019
  cvFormEl.addEventListener("submit", async (e) => {
1965
2020
  e.preventDefault();
1966
2021
  const name = [cvFirstEl.value, cvLastEl.value].map((s) => s.trim()).filter(Boolean).join(" ");
@@ -1973,7 +2028,8 @@
1973
2028
  cvErrorEl.textContent = "";
1974
2029
  try {
1975
2030
  // Match the website's ContactWrite shape: emails[] is the identity, email = primary.
1976
- const next = await window.relay.contactSave({ name, emails, email: emails[0] });
2031
+ const contactId = editingContactId();
2032
+ const next = await window.relay.contactSave({ contactId, name, emails, email: emails[0] });
1977
2033
  if (Array.isArray(next)) contactsList = next;
1978
2034
  closeContactForm();
1979
2035
  renderContacts();
package/overlay/main.cjs CHANGED
@@ -309,10 +309,11 @@ async function readContacts() {
309
309
  .map((c) => {
310
310
  const emails = Array.isArray(c.emails) ? c.emails.filter(Boolean) : c.email ? [c.email] : [];
311
311
  return {
312
- id: c.id || "",
312
+ id: c.id || c.contactId || "",
313
313
  name: c.name || emails[0] || "",
314
314
  email: emails[0] || c.email || "",
315
315
  emails,
316
+ onRelay: Boolean(c.onRelay),
316
317
  updatedAt: c.updatedAt || null,
317
318
  };
318
319
  })
@@ -324,6 +325,7 @@ async function readContacts() {
324
325
  }
325
326
 
326
327
  async function saveContact(input) {
328
+ const contactId = String((input && input.contactId) || "").trim();
327
329
  const name = String((input && input.name) || "").trim();
328
330
  const emails = Array.isArray(input && input.emails)
329
331
  ? input.emails.map((e) => String(e || "").trim()).filter(Boolean)
@@ -333,7 +335,16 @@ async function saveContact(input) {
333
335
  if (!name) throw new Error("A name is required");
334
336
  for (const e of emails) if (!e.includes("@")) throw new Error("That email looks off.");
335
337
  const client = await relayClient();
336
- await client.upsertContact({ name, emails, email: emails[0] || "" });
338
+ if (contactId) await client.updateContact(contactId, { name, emails, email: emails[0] || "" });
339
+ else await client.upsertContact({ name, emails, email: emails[0] || "" });
340
+ return readContacts();
341
+ }
342
+
343
+ async function deleteContactFromBook(input) {
344
+ const contactId = String((input && input.contactId) || input || "").trim();
345
+ if (!contactId) throw new Error("Missing contact id.");
346
+ const client = await relayClient();
347
+ await client.deleteContact(contactId);
337
348
  return readContacts();
338
349
  }
339
350
 
@@ -1188,6 +1199,7 @@ ipcMain.handle("relay:decline", (_e, taskId, approvalId) => declineShare(taskId,
1188
1199
  ipcMain.handle("relay:answer", (_e, taskId, messageId, text, packetId) => answerQuestion(taskId, messageId, text, packetId));
1189
1200
  ipcMain.handle("relay:contacts", () => readContacts());
1190
1201
  ipcMain.handle("relay:contactSave", (_e, input) => saveContact(input));
1202
+ ipcMain.handle("relay:contactDelete", (_e, input) => deleteContactFromBook(input));
1191
1203
 
1192
1204
  // The overlay is normally focusable:false so it never steals keyboard focus from
1193
1205
  // Claude/Codex. Text fields (the contact form, quick reply) need focus, so the renderer
@@ -32,6 +32,7 @@ contextBridge.exposeInMainWorld("relay", {
32
32
  // contacts
33
33
  contacts: () => ipcRenderer.invoke("relay:contacts"),
34
34
  contactSave: (input) => ipcRenderer.invoke("relay:contactSave", input),
35
+ contactDelete: (input) => ipcRenderer.invoke("relay:contactDelete", input),
35
36
 
36
37
  // window plumbing
37
38
  setFocusable: (v) => ipcRenderer.send("relay:setFocusable", v),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-companion",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "description": "Relay companion: connects local coding agents to Relay tasks, approvals, and connector tools.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/client.js CHANGED
@@ -207,6 +207,10 @@ export class RelayClient {
207
207
  });
208
208
  }
209
209
 
210
+ deleteContact(contactId) {
211
+ return this.#req("DELETE", `/v1/contacts/${encodeURIComponent(contactId)}`);
212
+ }
213
+
210
214
  importContacts(contacts, source = "imported") {
211
215
  return this.#req("POST", "/v1/contacts/import", { contacts, source });
212
216
  }