roger-roger 0.1.3 → 0.1.8

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.
@@ -141,6 +141,40 @@
141
141
  .slider input[type="range"] { flex: 1; accent-color: var(--accent); padding: 0; border: 0; background: transparent; }
142
142
  .slider output { width: 40px; text-align: right; font-size: 11px; color: var(--text-dim); font-variant-numeric: tabular-nums; }
143
143
  .flash.is-ok { border-color: #7fd1a0; }
144
+
145
+ /* The log: who said what, newest first. The agent's colour runs down the left edge. */
146
+ .row.is-agent { cursor: pointer; }
147
+ .filters { display: flex; gap: 4px; flex-wrap: wrap; padding: 6px 4px 8px; }
148
+ .entry { position: relative; padding: 8px 8px 8px 12px; border-bottom: 1px solid var(--border-subtle); cursor: pointer; }
149
+ .entry::before { content: ""; position: absolute; left: 0; top: 8px; bottom: 8px; width: 2px; background: var(--edge, var(--text-muted)); }
150
+ .entry.from-user::before { background: var(--accent); }
151
+ .entry:hover { background: color-mix(in srgb, var(--bg-section) 60%, transparent); }
152
+ .entry .top { gap: 6px; font-size: 11px; color: var(--text-dim); }
153
+ .entry .name { font-weight: 600; color: var(--text); }
154
+ .entry .arrow { color: var(--text-muted); }
155
+ .entry .kind { padding: 0 5px; border: 1px solid var(--border-subtle); font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted); }
156
+ .entry .kind.k-blocked, .entry .kind.k-error, .entry .kind.k-question { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 50%, transparent); }
157
+ .entry .kind.k-done, .entry .kind.k-answer { color: #7fd1a0; border-color: #7fd1a055; }
158
+ .entry .text { margin-top: 4px; line-height: 1.4; color: var(--text); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }
159
+ .entry.is-open .text { display: block; }
160
+ .entry .detail { margin-top: 3px; font-size: 11px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
161
+ .entry.is-open .detail { white-space: normal; }
162
+ .day { padding: 12px 8px 4px; font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); }
163
+
164
+ /* On air: shown only while a voice or sound plays, with the one thing you might want — stop it. */
165
+ .onair { display: flex; align-items: center; gap: 10px; padding: 8px 10px 8px 14px; border-bottom: 1px solid var(--border-subtle); background: color-mix(in srgb, var(--accent) 12%, transparent); }
166
+ .onair[hidden] { display: none; }
167
+ .onair .bars { display: flex; align-items: flex-end; gap: 2px; height: 14px; }
168
+ .onair .bars i { width: 3px; background: var(--accent); animation: level 900ms ease-in-out infinite; }
169
+ .onair .bars i:nth-child(2) { animation-delay: -300ms; }
170
+ .onair .bars i:nth-child(3) { animation-delay: -600ms; }
171
+ @keyframes level { 0%, 100% { height: 4px; } 50% { height: 14px; } }
172
+ @media (prefers-reduced-motion: reduce) { .onair .bars i { animation: none; height: 10px; } }
173
+ .onair .label { flex: 1; min-width: 0; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
174
+ .onair .label strong { font-weight: 600; }
175
+ .onair button { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; font-size: 11px; font-weight: 600; color: #101317; background: var(--accent); border-color: var(--accent); }
176
+ .onair button:hover { filter: brightness(1.08); }
177
+ .onair button svg { width: 11px; height: 11px; fill: currentColor; stroke: none; }
144
178
  svg { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
145
179
  </style>
146
180
  </head>
@@ -151,6 +185,9 @@
151
185
  <span class="count" id="count"></span>
152
186
  <span class="spacer"></span>
153
187
  <span class="badge" id="badge" hidden></span>
188
+ <button class="icon-btn icon-btn-sm" id="log" title="Log: who said what">
189
+ <svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path><path d="M8 9h8"></path><path d="M8 13h5"></path></svg>
190
+ </button>
154
191
  <button class="icon-btn icon-btn-sm" id="pin" title="Keep open when I click elsewhere">
155
192
  <svg viewBox="0 0 24 24"><path d="M12 17v5"></path><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"></path></svg>
156
193
  </button>
@@ -158,6 +195,13 @@
158
195
  <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6h.09A1.65 1.65 0 0 0 10 3.09V3a2 2 0 1 1 4 0v.09A1.65 1.65 0 0 0 15 4.6a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9v.09a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
159
196
  </button>
160
197
  </header>
198
+ <div class="onair" id="onair" hidden>
199
+ <span class="bars" aria-hidden="true"><i></i><i></i><i></i></span>
200
+ <span class="label" id="onair-label"></span>
201
+ <button data-do="hush" title="Stop this announcement (the Slack message stays)">
202
+ <svg viewBox="0 0 24 24"><rect x="5" y="5" width="14" height="14"></rect></svg>Stop
203
+ </button>
204
+ </div>
161
205
  <main id="body"></main>
162
206
  <div class="flash" id="flash" hidden></div>
163
207
  <footer id="actions"></footer>
@@ -168,9 +212,15 @@
168
212
  const plural = (n, one, many) => n + " " + (n === 1 ? one : many);
169
213
  const send = (body) => window.ipc.postMessage(JSON.stringify(body));
170
214
 
171
- let latest = { agents: [], questions: [], working: [] };
215
+ let latest = { agents: [], questions: [], working: [], log: [] };
172
216
  let settingsOpen = false;
173
217
  let advancedOpen = false;
218
+ // The log view, which agent it is narrowed to (null: everyone), and which entries are opened out.
219
+ let logOpen = false;
220
+ let logFilter = null;
221
+ // Stop was pressed: hide the strip at once rather than waiting for the daemon to say it stopped.
222
+ let hushedAt = 0;
223
+ const openEntries = new Set();
174
224
 
175
225
  function ago(ms) {
176
226
  const s = Math.round((ms ?? 0) / 1000);
@@ -197,7 +247,7 @@
197
247
  if (a.queued) bits.push(plural(a.queued, "message", "messages") + " unread");
198
248
  if (a.owes) bits.push(plural(a.owes, "answer", "answers") + " to collect");
199
249
  return (
200
- '<div class="row"><div class="top">' +
250
+ '<div class="row is-agent" data-agent="' + esc(a.nickname) + '" title="Show what ' + esc(a.nickname) + ' said"><div class="top">' +
201
251
  '<span class="status-dot"' + colour + "></span>" +
202
252
  '<span class="who">' + esc(a.nickname) + "</span>" +
203
253
  '<span class="state">' + esc(a.state) + "</span>" +
@@ -217,6 +267,70 @@
217
267
  );
218
268
  }
219
269
 
270
+ // ---------------------------------------------------------------- log
271
+
272
+ const KIND_LABEL = { done: "done", blocked: "blocked", input: "needs you", milestone: "milestone", error: "error", info: "info", question: "question", answer: "answer", message: "message", cancelled: "cancelled", expired: "expired" };
273
+
274
+ function clock(iso) {
275
+ const d = new Date(iso);
276
+ return isFinite(d) ? d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
277
+ }
278
+
279
+ function dayOf(iso) {
280
+ const d = new Date(iso);
281
+ if (!isFinite(d)) return "";
282
+ const today = new Date();
283
+ const yesterday = new Date(today.getTime() - 864e5);
284
+ if (d.toDateString() === today.toDateString()) return "Today";
285
+ if (d.toDateString() === yesterday.toDateString()) return "Yesterday";
286
+ return d.toLocaleDateString([], { weekday: "long", day: "numeric", month: "short" });
287
+ }
288
+
289
+ function entryRow(e, i) {
290
+ const name = (a) => '<span class="name">' + esc(a ? (a.swatch ? a.swatch + " " : "") + a.nickname : "an agent") + "</span>";
291
+ const who = e.from === "user"
292
+ ? '<span class="name">you</span>' + (e.agent ? '<span class="arrow">→</span>' + name(e.agent) : "")
293
+ : name(e.agent) + '<span class="arrow">→</span><span class="name">you</span>';
294
+ const key = e.at + "|" + i;
295
+ const edge = e.agent && e.agent.colour ? ' style="--edge:' + esc(e.agent.colour) + '"' : "";
296
+ const detail = e.kind === "answer" && e.detail ? "to: " + e.detail : e.detail;
297
+ return (
298
+ '<div class="entry' + (e.from === "user" ? " from-user" : "") + (openEntries.has(key) ? " is-open" : "") + '" data-entry="' + esc(key) + '"' + edge + ">" +
299
+ '<div class="top">' + who +
300
+ '<span class="kind k-' + esc(e.kind) + '">' + esc(KIND_LABEL[e.kind] || e.kind) + "</span>" +
301
+ '<span class="age" title="' + esc(new Date(e.at).toLocaleString()) + '">' + clock(e.at) + "</span></div>" +
302
+ (e.text ? '<div class="text">' + esc(e.text) + "</div>" : "") +
303
+ (detail ? '<div class="detail">' + esc(detail) + "</div>" : "") +
304
+ "</div>"
305
+ );
306
+ }
307
+
308
+ function logView(log) {
309
+ const names = [...new Set(log.map((e) => e.agent && e.agent.nickname).filter(Boolean))];
310
+ const shown = logFilter ? log.filter((e) => e.agent && e.agent.nickname === logFilter) : log;
311
+ const parts = [];
312
+ if (names.length > 1 || logFilter) {
313
+ parts.push('<div class="filters">' +
314
+ '<button class="chip' + (logFilter ? "" : " is-active") + '" data-filter="">Everyone</button>' +
315
+ names.map((n) => '<button class="chip' + (logFilter === n ? " is-active" : "") + '" data-filter="' + esc(n) + '">' + esc(n) + "</button>").join("") +
316
+ "</div>");
317
+ }
318
+ if (!shown.length) {
319
+ parts.push('<p class="empty">' + (logFilter ? "Nothing from " + esc(logFilter) + " yet." : "Nothing said yet. Notifications, questions, your answers and your messages show up here.") + "</p>");
320
+ return parts.join("");
321
+ }
322
+ let day = "";
323
+ shown.forEach((e, i) => {
324
+ const d = dayOf(e.at);
325
+ if (d !== day) {
326
+ day = d;
327
+ parts.push('<p class="day">' + esc(d) + "</p>");
328
+ }
329
+ parts.push(entryRow(e, i));
330
+ });
331
+ return parts.join("");
332
+ }
333
+
220
334
  // ---------------------------------------------------------------- settings
221
335
 
222
336
  const setting = (label, control, hint) =>
@@ -363,6 +477,8 @@
363
477
 
364
478
  document.getElementById("count").textContent = settingsOpen
365
479
  ? "settings"
480
+ : logOpen
481
+ ? "log" + (logFilter ? " · " + logFilter : "")
366
482
  : agents.length
367
483
  ? plural(agents.length, "agent", "agents") + (questions.length ? " · " + plural(questions.length, "question", "questions") : "")
368
484
  : "nothing running";
@@ -371,12 +487,27 @@
371
487
  badge.hidden = !snoozed && !quiet;
372
488
  badge.textContent = snoozed ? "snoozed" : "quiet hours";
373
489
  document.getElementById("gear").classList.toggle("is-active", settingsOpen);
490
+ document.getElementById("log").classList.toggle("is-active", logOpen && !settingsOpen);
491
+
492
+ // Who is speaking, and the Stop button, only while something is.
493
+ const speaking = state.speaking;
494
+ const onair = document.getElementById("onair");
495
+ onair.hidden = !speaking || hushedAt > Date.now() - 2500;
496
+ if (speaking) {
497
+ const who = speaking.who;
498
+ const agent = agents.find((a) => a.nickname === who);
499
+ document.getElementById("onair-label").innerHTML = who && agent
500
+ ? "<strong>" + esc((agent.swatch ? agent.swatch + " " : "") + who) + "</strong> is speaking"
501
+ : who ? "Playing a " + esc(who) : "Speaking";
502
+ }
374
503
 
375
504
  const parts = [];
376
505
  // The tray says so when the file it reads has stopped being kept up to date.
377
506
  if (state.stale) parts.push('<p class="stale">Daemon not running — state may be old.</p>');
378
507
  if (settingsOpen) {
379
508
  parts.push(settingsView(state.settings, state.options || {}));
509
+ } else if (logOpen) {
510
+ parts.push(logView(state.log ?? []));
380
511
  } else if (!agents.length && !questions.length && !working.length) {
381
512
  parts.push('<p class="empty">Nothing is running.</p>');
382
513
  } else {
@@ -455,10 +586,40 @@
455
586
 
456
587
  document.getElementById("gear").addEventListener("click", () => {
457
588
  settingsOpen = !settingsOpen;
589
+ if (settingsOpen) logOpen = false;
590
+ draw();
591
+ });
592
+
593
+ document.getElementById("log").addEventListener("click", () => {
594
+ logOpen = settingsOpen ? true : !logOpen;
595
+ settingsOpen = false;
596
+ logFilter = null;
597
+ document.getElementById("body").scrollTop = 0;
458
598
  draw();
459
599
  });
460
600
 
461
601
  document.addEventListener("click", (e) => {
602
+ // An agent's row opens its log; a filter narrows the log; an entry opens out to its full text.
603
+ const agent = e.target.closest("[data-agent]");
604
+ if (agent) {
605
+ logOpen = true;
606
+ logFilter = agent.dataset.agent;
607
+ document.getElementById("body").scrollTop = 0;
608
+ return draw();
609
+ }
610
+ const filter = e.target.closest("[data-filter]");
611
+ if (filter) {
612
+ logFilter = filter.dataset.filter || null;
613
+ return draw();
614
+ }
615
+ const entry = e.target.closest("[data-entry]");
616
+ if (entry) {
617
+ const key = entry.dataset.entry;
618
+ if (openEntries.has(key)) openEntries.delete(key);
619
+ else openEntries.add(key);
620
+ entry.classList.toggle("is-open");
621
+ return;
622
+ }
462
623
  const action = e.target.closest("[data-do]");
463
624
  if (action) {
464
625
  e.preventDefault();
@@ -466,6 +627,10 @@
466
627
  action.disabled = true;
467
628
  action.textContent = "Speaking…";
468
629
  }
630
+ if (action.dataset.do === "hush") {
631
+ hushedAt = Date.now();
632
+ document.getElementById("onair").hidden = true;
633
+ }
469
634
  return send({ do: action.dataset.do, value: action.dataset.value });
470
635
  }
471
636
  const chip = e.target.closest("[data-chip]");