@lovenyberg/ove 0.7.0 → 0.9.0

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.
@@ -332,6 +332,48 @@
332
332
  ::-webkit-scrollbar-track { background: transparent; }
333
333
  ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
334
334
  ::-webkit-scrollbar-thumb:hover { background: var(--border-light); }
335
+
336
+ /* ── Mobile layout ── */
337
+ @media (max-width: 768px) {
338
+ header {
339
+ flex-wrap: wrap;
340
+ padding: 0.4rem 0.5rem;
341
+ gap: 0.4rem;
342
+ }
343
+
344
+ .header-left {
345
+ gap: 0.5rem;
346
+ }
347
+
348
+ .header-left h1 {
349
+ font-size: 0.75rem;
350
+ }
351
+
352
+ .header-right {
353
+ width: 100%;
354
+ justify-content: flex-end;
355
+ }
356
+
357
+ .content {
358
+ padding: 0.75rem;
359
+ }
360
+
361
+ .adapter-grid {
362
+ grid-template-columns: 1fr;
363
+ }
364
+
365
+ .queue-grid {
366
+ grid-template-columns: repeat(2, 1fr);
367
+ }
368
+
369
+ .queue-count {
370
+ font-size: 1.1rem;
371
+ }
372
+
373
+ .pairing-code-value {
374
+ font-size: 1.1rem;
375
+ }
376
+ }
335
377
  </style>
336
378
  </head>
337
379
  <body>
@@ -341,6 +383,7 @@
341
383
  <h1><span>ove</span> status</h1>
342
384
  <a href="/" class="nav-link">chat</a>
343
385
  <a href="/trace" class="nav-link">traces</a>
386
+ <a href="/metrics" class="nav-link">metrics</a>
344
387
  </div>
345
388
  <div class="header-right">
346
389
  <span class="last-updated" id="lastUpdated"></span>
package/public/trace.html CHANGED
@@ -225,6 +225,9 @@
225
225
  .badge-completed { background: var(--green-dim); color: var(--green); }
226
226
  .badge-failed { background: var(--red-dim); color: var(--red); }
227
227
 
228
+ .badge-priority-2 { background: var(--red-dim); color: var(--red); }
229
+ .badge-priority-1 { background: var(--amber-dim); color: var(--amber); }
230
+
228
231
  @keyframes pulse {
229
232
  0%, 100% { opacity: 1; }
230
233
  50% { opacity: 0.6; }
@@ -297,6 +300,23 @@
297
300
  .copy-btn:hover { color: var(--text); border-color: var(--border-light); }
298
301
  .copy-btn.copied { color: var(--green); border-color: var(--green-dim); }
299
302
 
303
+ .cancel-btn {
304
+ background: var(--red-dim);
305
+ border: 1px solid var(--red);
306
+ color: var(--red);
307
+ font-family: inherit;
308
+ font-size: 0.6rem;
309
+ padding: 0.2rem 0.6rem;
310
+ border-radius: 3px;
311
+ cursor: pointer;
312
+ transition: all 0.15s;
313
+ text-transform: uppercase;
314
+ letter-spacing: 0.03em;
315
+ font-weight: 600;
316
+ }
317
+ .cancel-btn:hover { background: var(--red); color: #fff; }
318
+ .cancel-btn:disabled { opacity: 0.5; cursor: not-allowed; }
319
+
300
320
  /* ── Task context card ── */
301
321
  .task-context {
302
322
  padding: 0.75rem 1rem;
@@ -521,6 +541,66 @@
521
541
  ::-webkit-scrollbar-track { background: transparent; }
522
542
  ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
523
543
  ::-webkit-scrollbar-thumb:hover { background: var(--border-light); }
544
+
545
+ /* ── Mobile layout ── */
546
+ @media (max-width: 768px) {
547
+ header {
548
+ flex-wrap: wrap;
549
+ padding: 0.4rem 0.5rem;
550
+ gap: 0.4rem;
551
+ }
552
+
553
+ .header-left {
554
+ gap: 0.5rem;
555
+ }
556
+
557
+ .header-left h1 {
558
+ font-size: 0.75rem;
559
+ }
560
+
561
+ .header-right {
562
+ flex-wrap: wrap;
563
+ gap: 0.4rem;
564
+ width: 100%;
565
+ justify-content: flex-start;
566
+ }
567
+
568
+ .layout {
569
+ flex-direction: column;
570
+ }
571
+
572
+ .task-panel {
573
+ width: 100%;
574
+ min-width: 0;
575
+ max-height: 40vh;
576
+ border-right: none;
577
+ border-bottom: 1px solid var(--border);
578
+ flex-shrink: 0;
579
+ }
580
+
581
+ .trace-panel {
582
+ flex: 1;
583
+ min-height: 0;
584
+ }
585
+
586
+ .trace-event {
587
+ padding: 0.35rem 0.5rem;
588
+ }
589
+
590
+ .trace-ts {
591
+ width: 4rem;
592
+ font-size: 0.55rem;
593
+ }
594
+
595
+ .ctx-meta {
596
+ gap: 0.75rem;
597
+ }
598
+
599
+ .filter-select {
600
+ font-size: 0.6rem;
601
+ padding: 0.15rem 0.3rem;
602
+ }
603
+ }
524
604
  </style>
525
605
  </head>
526
606
  <body>
@@ -530,6 +610,7 @@
530
610
  <h1><span>ove</span> trace</h1>
531
611
  <a href="/" class="nav-link">chat</a>
532
612
  <a href="/status" class="nav-link">status</a>
613
+ <a href="/metrics" class="nav-link">metrics</a>
533
614
  </div>
534
615
  <div class="header-right">
535
616
  <select class="filter-select" id="repoFilter">
@@ -745,6 +826,11 @@
745
826
 
746
827
  var rowTop = el("div", "task-row-top");
747
828
  rowTop.appendChild(el("span", "badge badge-" + task.status, task.status));
829
+ if (task.priority === 2) {
830
+ rowTop.appendChild(el("span", "badge badge-priority-2", "urgent"));
831
+ } else if (task.priority === 1) {
832
+ rowTop.appendChild(el("span", "badge badge-priority-1", "high"));
833
+ }
748
834
  rowTop.appendChild(el("span", "task-repo", task.repo));
749
835
  rowTop.appendChild(el("span", "task-time", fmtTime(task.createdAt)));
750
836
  item.appendChild(rowTop);
@@ -768,10 +854,12 @@
768
854
 
769
855
  // Meta row: user, repo, status, timestamps
770
856
  var meta = el("div", "ctx-meta");
857
+ var priorityLabel = task.priority === 2 ? "urgent" : task.priority === 1 ? "high" : "normal";
771
858
  var items = [
772
859
  ["user", task.userId],
773
860
  ["repo", task.repo],
774
861
  ["status", task.status],
862
+ ["priority", priorityLabel],
775
863
  ["created", fmtTimeFull(task.createdAt)],
776
864
  ];
777
865
  if (task.completedAt) {
@@ -808,6 +896,18 @@
808
896
  ctx.appendChild(resultRow);
809
897
  }
810
898
 
899
+ // Cancel button for running/pending tasks
900
+ if (task.status === "running" || task.status === "pending") {
901
+ var cancelRow = el("div", "task-context-row");
902
+ cancelRow.style.marginTop = "0.5rem";
903
+ var cancelBtn = el("button", "cancel-btn", "Cancel task");
904
+ cancelBtn.addEventListener("click", function () {
905
+ cancelTask(task.id, cancelBtn);
906
+ });
907
+ cancelRow.appendChild(cancelBtn);
908
+ ctx.appendChild(cancelRow);
909
+ }
910
+
811
911
  taskContextEl.appendChild(ctx);
812
912
  }
813
913
 
@@ -895,6 +995,33 @@
895
995
  traceContentEl.appendChild(timeline);
896
996
  }
897
997
 
998
+ // Cancel task
999
+ async function cancelTask(taskId, btn) {
1000
+ btn.disabled = true;
1001
+ btn.textContent = "Cancelling...";
1002
+ try {
1003
+ var res = await fetch("/api/tasks/" + encodeURIComponent(taskId) + "/cancel", {
1004
+ method: "POST",
1005
+ headers: apiHeaders(),
1006
+ });
1007
+ if (res.ok) {
1008
+ btn.textContent = "Cancelled";
1009
+ // Refresh task list and trace
1010
+ await fetchTasks();
1011
+ if (selectedTaskId === taskId) {
1012
+ await selectTask(taskId);
1013
+ }
1014
+ } else {
1015
+ var err = await res.json();
1016
+ btn.textContent = "Failed: " + (err.error || res.status);
1017
+ setTimeout(function () { btn.textContent = "Cancel task"; btn.disabled = false; }, 2000);
1018
+ }
1019
+ } catch (e) {
1020
+ btn.textContent = "Error";
1021
+ setTimeout(function () { btn.textContent = "Cancel task"; btn.disabled = false; }, 2000);
1022
+ }
1023
+ }
1024
+
898
1025
  // Auto-refresh
899
1026
  var lastSelectedStatus = null;
900
1027
  autoRefreshEl.addEventListener("change", function () {
@@ -18,4 +18,12 @@ describe("GitHubAdapter", () => {
18
18
  expect(parseMention("hey @ove-bot review PR #5", "ove-bot")).toBe("hey review PR #5");
19
19
  expect(parseMention("no mention here", "ove-bot")).toBeNull();
20
20
  });
21
+
22
+ test("parseMention handles botName with regex metacharacters", async () => {
23
+ const { parseMention } = await import("./github");
24
+ // botName like "bot[1]" would break if used in a regex without escaping
25
+ expect(parseMention("@bot[1] fix this", "bot[1]")).toBe("fix this");
26
+ expect(parseMention("@bot.star do it", "bot.star")).toBe("do it");
27
+ expect(parseMention("@bot(x) hello", "bot(x)")).toBe("hello");
28
+ });
21
29
  });
@@ -2,8 +2,9 @@ import type { EventAdapter, IncomingEvent, EventSource, AdapterStatus } from "./
2
2
  import { logger } from "../logger";
3
3
 
4
4
  export function parseMention(body: string, botName: string): string | null {
5
- if (!body.includes(`@${botName}`)) return null;
6
- return body.replace(new RegExp(`@${botName}`, "g"), "").trim();
5
+ const mention = "@" + botName;
6
+ if (!body.includes(mention)) return null;
7
+ return body.replaceAll(mention, "").trim();
7
8
  }
8
9
 
9
10
  interface GitHubComment {