relay-companion 0.1.87 → 0.1.89

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.
@@ -504,7 +504,30 @@
504
504
  .cv-chev { flex:0 0 auto; display:flex; align-items:center; }
505
505
  .cv-empty { padding:34px 16px; text-align:center; color:var(--muted-2); font-size:12.5px; line-height:1.5; }
506
506
  /* ---- contact groups (rosters) ---- */
507
- .cvg { border-top:1px solid var(--hair-2); padding-bottom:10px; }
507
+ /* Groups sits ABOVE the people list: a long contact list used to bury it
508
+ entirely (you had to scroll past every contact to discover groups exist).
509
+ Small set first, long list second. */
510
+ .cvg { padding-bottom:6px; }
511
+ /* Segmented island: an inset track with the selected segment lifted out of it —
512
+ depth from light, not borders, matching the open-actions menu. */
513
+ .cv-seg {
514
+ display:inline-flex; gap:2px; padding:2px; border-radius:9px;
515
+ background:rgba(31,26,23,.045);
516
+ }
517
+ .cv-seg-btn {
518
+ appearance:none; border:0; background:transparent; cursor:pointer;
519
+ font-family:var(--sans); font-size:11px; font-weight:500; letter-spacing:-0.01em;
520
+ color:var(--muted-2); height:22px; padding:0 9px; border-radius:7px;
521
+ display:inline-flex; align-items:center; gap:5px;
522
+ transition:background-color .15s var(--settle), color .15s var(--settle), box-shadow .15s var(--settle);
523
+ }
524
+ .cv-seg-btn:hover { color:var(--ink-2); }
525
+ .cv-seg-btn.active {
526
+ background:#fff; color:var(--ink); font-weight:600;
527
+ box-shadow:0 1px 2px rgba(31,26,23,.09), 0 0 0 0.5px rgba(31,26,23,.05);
528
+ }
529
+ .cv-seg-n { font-size:9.5px; font-variant-numeric:tabular-nums; opacity:.55; }
530
+ .cv-seg-btn.active .cv-seg-n { opacity:.65; }
508
531
  .cvg-head { display:flex; align-items:center; justify-content:space-between; padding:12px 16px 6px; }
509
532
  .cvg-newform { display:flex; gap:7px; padding:6px 16px 10px; align-items:center; }
510
533
  .cvg-newform.hidden { display:none; }
@@ -824,8 +847,16 @@
824
847
  <!-- Contacts view -->
825
848
  <section class="view hidden" id="contactsView">
826
849
  <div class="cv-head">
827
- <span class="cv-title">People you relay with.</span>
850
+ <!-- One island, two lists. Groups used to sit below every contact, so a
851
+ long book hid them completely; stacking them the other way would just
852
+ bury people instead. A segment switches the pane and keeps both a
853
+ single tap away, with counts so neither looks empty when it isn't. -->
854
+ <div class="cv-seg" role="tablist" aria-label="Contacts and groups">
855
+ <button class="cv-seg-btn active" type="button" role="tab" data-pane="people" id="cvSegPeople">People <span class="cv-seg-n" id="cvSegPeopleN"></span></button>
856
+ <button class="cv-seg-btn" type="button" role="tab" data-pane="groups" id="cvSegGroups">Groups <span class="cv-seg-n" id="cvSegGroupsN"></span></button>
857
+ </div>
828
858
  <button class="cv-add" type="button" id="cvAdd">+ Add</button>
859
+ <button class="cv-add gone" type="button" id="cvgNew">+ New</button>
829
860
  </div>
830
861
  <form class="cv-form hidden" id="cvForm">
831
862
  <div class="cv-row2">
@@ -848,16 +879,9 @@
848
879
  <button class="cv-save" type="submit" id="cvSave">Save</button>
849
880
  </div>
850
881
  </form>
851
- <div class="cv-list" id="cvList"></div>
852
- <div id="contactsEmpty" class="cv-empty gone">No contacts yet.<br>Add someone, or your agent saves them as you relay.</div>
853
-
854
882
  <!-- Contact groups: saved rosters. A relay to a group fans out to every
855
883
  member under ONE shared thread; replies reach everyone. -->
856
884
  <div class="cvg" id="cvGroups">
857
- <div class="cvg-head">
858
- <span class="cv-title">Groups</span>
859
- <button class="cv-add" type="button" id="cvgNew">+ New group</button>
860
- </div>
861
885
  <form class="cvg-newform hidden" id="cvgNewForm">
862
886
  <input class="cv-input" id="cvgNewName" placeholder="Group name" autocomplete="off" spellcheck="false" />
863
887
  <button class="cv-save" type="submit">Create</button>
@@ -867,6 +891,9 @@
867
891
  <div id="cvgList"></div>
868
892
  <div class="cv-empty" id="cvgEmpty">No groups yet. A group relays to everyone in it as one conversation.</div>
869
893
  </div>
894
+ <div class="cv-list" id="cvList"></div>
895
+ <div id="contactsEmpty" class="cv-empty gone">No contacts yet.<br>Add someone, or your agent saves them as you relay.</div>
896
+
870
897
  </section>
871
898
 
872
899
  <!-- Settings view (account card + sign-out / switch-account; rendered by JS) -->
@@ -2440,7 +2467,8 @@
2440
2467
  }
2441
2468
  function contactKey(c) { return c.id || (contactEmails(c)[0] || "") || c.name; }
2442
2469
  function renderContacts() {
2443
- contactsEmptyEl.classList.toggle("gone", contactsList.length > 0);
2470
+ contactsEmptyEl.classList.toggle("gone", contactsPane !== "people" || contactsList.length > 0);
2471
+ refreshPaneCounts();
2444
2472
  cvListEl.innerHTML = contactsList.map((c) => {
2445
2473
  const emails = contactEmails(c);
2446
2474
  const primary = emails[0] || "";
@@ -2475,6 +2503,35 @@
2475
2503
  const cvgNewFormEl = document.getElementById("cvgNewForm");
2476
2504
  const cvgNewNameEl = document.getElementById("cvgNewName");
2477
2505
  const cvgNewCancelEl = document.getElementById("cvgNewCancel");
2506
+ // ---- contacts / groups pane (the segmented island in the head) ----
2507
+ const cvSegPeopleEl = document.getElementById("cvSegPeople");
2508
+ const cvSegGroupsEl = document.getElementById("cvSegGroups");
2509
+ const cvSegPeopleNEl = document.getElementById("cvSegPeopleN");
2510
+ const cvSegGroupsNEl = document.getElementById("cvSegGroupsN");
2511
+ const cvGroupsEl = document.getElementById("cvGroups");
2512
+ let contactsPane = "people";
2513
+ function applyContactsPane() {
2514
+ const people = contactsPane === "people";
2515
+ cvSegPeopleEl.classList.toggle("active", people);
2516
+ cvSegGroupsEl.classList.toggle("active", !people);
2517
+ cvSegPeopleEl.setAttribute("aria-selected", String(people));
2518
+ cvSegGroupsEl.setAttribute("aria-selected", String(!people));
2519
+ cvListEl.classList.toggle("gone", !people);
2520
+ contactsEmptyEl.classList.toggle("gone", !people || contactsList.length > 0);
2521
+ cvGroupsEl.classList.toggle("gone", people);
2522
+ // The add button follows the pane it belongs to.
2523
+ cvAddEl.classList.toggle("gone", !people);
2524
+ cvgNewEl.classList.toggle("gone", people);
2525
+ if (people) cvgNewFormEl.classList.add("hidden");
2526
+ else cvFormEl.classList.add("hidden");
2527
+ }
2528
+ function refreshPaneCounts() {
2529
+ cvSegPeopleNEl.textContent = contactsList.length ? String(contactsList.length) : "";
2530
+ cvSegGroupsNEl.textContent = groupsList.length ? String(groupsList.length) : "";
2531
+ }
2532
+ cvSegPeopleEl.addEventListener("click", () => { contactsPane = "people"; applyContactsPane(); });
2533
+ cvSegGroupsEl.addEventListener("click", () => { contactsPane = "groups"; applyContactsPane(); loadGroups(); });
2534
+
2478
2535
  const cvgListEl = document.getElementById("cvgList");
2479
2536
  const cvgEmptyEl = document.getElementById("cvgEmpty");
2480
2537
  const cvgErrorEl = document.getElementById("cvgError");
@@ -2515,6 +2572,7 @@
2515
2572
  }
2516
2573
  function renderGroups() {
2517
2574
  cvgEmptyEl.classList.toggle("gone", groupsList.length > 0);
2575
+ refreshPaneCounts();
2518
2576
  cvgListEl.innerHTML = groupsList.map((g) => {
2519
2577
  const open = g.id === cvgExpandedId;
2520
2578
  const count = Number(g.memberCount ?? (g.members || []).length) || 0;
@@ -3195,7 +3253,9 @@
3195
3253
  taskDetailTimer = null;
3196
3254
  }
3197
3255
  if (activeView === "contacts") {
3256
+ applyContactsPane();
3198
3257
  loadContacts();
3258
+ loadGroups(); // counts on the segment must be right before it is tapped
3199
3259
  } else {
3200
3260
  cvFormEl.classList.add("hidden");
3201
3261
  if (window.relay.setFocusable) window.relay.setFocusable(false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-companion",
3
- "version": "0.1.87",
3
+ "version": "0.1.89",
4
4
  "description": "Relay companion for ordinary messages, with dormant coordination features available only by explicit opt-in.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/install.js CHANGED
@@ -943,6 +943,14 @@ ${pillArgs.map((argument) => ` <string>${plistEscape(argument)}</string>`).jo
943
943
  </array>
944
944
  <key>RunAtLoad</key><true/>
945
945
  <key>KeepAlive</key><true/>
946
+ <!-- The pill draws a window the user clicks, so it must be scheduled like an
947
+ app, not like batch work. Without ProcessType, launchd treats an agent as
948
+ ordinary background work: under load macOS starves it, and the field
949
+ symptom is a visible pill whose clicks and animation freeze for seconds
950
+ while it burns no CPU and runs no JavaScript. Interactive opts out of that
951
+ throttling; LowPriorityIO off keeps its disk reads from being deferred. -->
952
+ <key>ProcessType</key><string>Interactive</string>
953
+ <key>LowPriorityIO</key><false/>
946
954
  <key>StandardOutPath</key><string>${plistEscape(logPath)}</string>
947
955
  <key>StandardErrorPath</key><string>${plistEscape(logPath)}</string>
948
956
  <key>EnvironmentVariables</key>