aimeat 1.15.0 → 1.16.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.
Files changed (49) hide show
  1. package/dist/locales/en.json +39 -1
  2. package/dist/locales/fi.json +39 -1
  3. package/dist/public/css/views/agents-detail.css +135 -0
  4. package/dist/public/js/services/agent-tasks.js +11 -0
  5. package/dist/public/views/profile/agents/agent-card.js +5 -1
  6. package/dist/public/views/profile/agents/tab-integration.js +12 -4
  7. package/dist/public/views/profile/agents/tab-quality.js +159 -0
  8. package/dist/public/views/profile/agents-tasks-subtab.js +76 -2
  9. package/dist/src/generated/api-types.d.ts +172 -3
  10. package/dist/src/generated/api-types.d.ts.map +1 -1
  11. package/dist/src/models/agent-task-schemas.d.ts +28 -0
  12. package/dist/src/models/agent-task-schemas.d.ts.map +1 -1
  13. package/dist/src/models/agent-task-schemas.js +21 -0
  14. package/dist/src/models/agent-task-schemas.js.map +1 -1
  15. package/dist/src/routes/agent-activity.d.ts +2 -0
  16. package/dist/src/routes/agent-activity.d.ts.map +1 -1
  17. package/dist/src/routes/agent-activity.js +41 -0
  18. package/dist/src/routes/agent-activity.js.map +1 -1
  19. package/dist/src/routes/agent-tasks.d.ts +3 -0
  20. package/dist/src/routes/agent-tasks.d.ts.map +1 -1
  21. package/dist/src/routes/agent-tasks.js +91 -1
  22. package/dist/src/routes/agent-tasks.js.map +1 -1
  23. package/dist/src/routes/lib-agents.d.ts +20 -0
  24. package/dist/src/routes/lib-agents.d.ts.map +1 -0
  25. package/dist/src/routes/lib-agents.js +288 -0
  26. package/dist/src/routes/lib-agents.js.map +1 -0
  27. package/dist/src/routes/libs.d.ts +3 -0
  28. package/dist/src/routes/libs.d.ts.map +1 -1
  29. package/dist/src/routes/libs.js +16 -0
  30. package/dist/src/routes/libs.js.map +1 -1
  31. package/dist/src/services/agent-statistics.d.ts +80 -0
  32. package/dist/src/services/agent-statistics.d.ts.map +1 -0
  33. package/dist/src/services/agent-statistics.js +180 -0
  34. package/dist/src/services/agent-statistics.js.map +1 -0
  35. package/dist/src/storage/interface.d.ts +35 -1
  36. package/dist/src/storage/interface.d.ts.map +1 -1
  37. package/dist/src/storage/interface.js +2 -1
  38. package/dist/src/storage/interface.js.map +1 -1
  39. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  40. package/dist/src/storage/providers/mongodb/index.js +3 -0
  41. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  42. package/dist/src/storage/providers/sqlite/repos/agent-task.d.ts.map +1 -1
  43. package/dist/src/storage/providers/sqlite/repos/agent-task.js +6 -4
  44. package/dist/src/storage/providers/sqlite/repos/agent-task.js.map +1 -1
  45. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  46. package/dist/src/storage/providers/sqlite/schema.js +4 -1
  47. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  48. package/package.json +1 -1
  49. package/prisma/schema.prisma +1 -0
@@ -458,6 +458,10 @@
458
458
  "deliverable": "Deliverable",
459
459
  "viewDeliverable": "View",
460
460
  "deliverableGone": "The deliverable entry no longer exists.",
461
+ "cancel": "Cancel",
462
+ "cancelConfirm": "Cancel this task? Work that hasn't started will be skipped",
463
+ "cancelled": "Task cancelled",
464
+ "cancelError": "Failed to cancel task",
461
465
  "revision_requested": "Revision requested",
462
466
  "requestChanges": "Request changes",
463
467
  "requestChangesTitle": "Ask the agent to revise the plan",
@@ -469,6 +473,8 @@
469
473
  "requestChangesError": "Failed to send change request",
470
474
  "revisionWaiting": "Waiting for the agent to propose a revised plan.",
471
475
  "outdatedTodos": "Previously proposed (outdated)",
476
+ "blurTitle": "Hide title (blur for screen recording)",
477
+ "unblurTitle": "Show title",
472
478
  "builder": {
473
479
  "title": "New Task",
474
480
  "placeholder": "What should the agent do? Describe in plain language or technical detail.",
@@ -649,7 +655,39 @@
649
655
  "directives": "Directives",
650
656
  "agent_config": "Agent Config",
651
657
  "activity": "Activity",
652
- "services": "Services"
658
+ "services": "Services",
659
+ "quality": "Quality"
660
+ },
661
+ "quality": {
662
+ "performanceTitle": "Performance",
663
+ "performanceDesc": "How much this agent has done and how fast — computed from its tasks.",
664
+ "reviewsTitle": "Reviews by context",
665
+ "reviewsDesc": "Per-context star ratings of the agent's deliverables. Factual contexts are source-grounded.",
666
+ "customTitle": "Custom metrics",
667
+ "customDesc": "Metrics the agent publishes about itself.",
668
+ "tasksTotal": "Tasks",
669
+ "tasksCompleted": "Completed",
670
+ "tasksFailed": "Failed",
671
+ "successRate": "Success rate",
672
+ "avgTime": "Avg make-time",
673
+ "events": "Events",
674
+ "overall": "Overall",
675
+ "ratings": "{count} ratings",
676
+ "lowConfidence": "Low confidence (small sample)",
677
+ "noReviews": "No reviews yet. Rate a completed task to start building quality stats.",
678
+ "noCustom": "No custom metrics published.",
679
+ "grounded": "{count} source-grounded",
680
+ "seconds": "s",
681
+ "contexts": {
682
+ "factual": "Factual",
683
+ "creative": "Creative",
684
+ "code": "Code",
685
+ "planning": "Planning",
686
+ "summarization": "Summarization",
687
+ "research": "Research",
688
+ "communication": "Communication",
689
+ "other": "Other"
690
+ }
653
691
  },
654
692
  "state": {
655
693
  "new": "New",
@@ -458,6 +458,10 @@
458
458
  "deliverable": "Tuotos",
459
459
  "viewDeliverable": "Näytä",
460
460
  "deliverableGone": "Tuotosmerkintää ei ole enää olemassa.",
461
+ "cancel": "Peruuta",
462
+ "cancelConfirm": "Peruutetaanko tämä tehtävä? Aloittamaton työ ohitetaan",
463
+ "cancelled": "Tehtävä peruutettu",
464
+ "cancelError": "Tehtävän peruutus epäonnistui",
461
465
  "revision_requested": "Korjauspyyntö lähetetty",
462
466
  "requestChanges": "Pyydä muutoksia",
463
467
  "requestChangesTitle": "Pyydä agenttia muokkaamaan suunnitelmaa",
@@ -469,6 +473,8 @@
469
473
  "requestChangesError": "Muutospyynnön lähetys epäonnistui",
470
474
  "revisionWaiting": "Odotetaan agentin korjattua ehdotusta.",
471
475
  "outdatedTodos": "Aiemmin ehdotetut (vanhentuneet)",
476
+ "blurTitle": "Piilota otsikko (sumenna ruutunauhoitusta varten)",
477
+ "unblurTitle": "Näytä otsikko",
472
478
  "builder": {
473
479
  "title": "Uusi tehtävä",
474
480
  "placeholder": "Mitä agentin pitäisi tehdä? Kuvaile selkokielellä tai teknisesti.",
@@ -649,7 +655,39 @@
649
655
  "directives": "Ohjeistukset",
650
656
  "agent_config": "Agenttiasetukset",
651
657
  "activity": "Toiminta",
652
- "services": "Palvelut"
658
+ "services": "Palvelut",
659
+ "quality": "Laatu"
660
+ },
661
+ "quality": {
662
+ "performanceTitle": "Suorituskyky",
663
+ "performanceDesc": "Kuinka paljon agentti on tehnyt ja kuinka nopeasti — laskettu sen tehtävistä.",
664
+ "reviewsTitle": "Arviot konteksteittain",
665
+ "reviewsDesc": "Agentin tuotosten tähtiarviot konteksteittain. Faktakontekstit ovat lähteisiin perustuvia.",
666
+ "customTitle": "Omat mittarit",
667
+ "customDesc": "Mittarit, joita agentti julkaisee itsestään.",
668
+ "tasksTotal": "Tehtäviä",
669
+ "tasksCompleted": "Valmiit",
670
+ "tasksFailed": "Epäonnistuneet",
671
+ "successRate": "Onnistumisaste",
672
+ "avgTime": "Keskim. tekoaika",
673
+ "events": "Tapahtumia",
674
+ "overall": "Kokonaisuus",
675
+ "ratings": "{count} arviota",
676
+ "lowConfidence": "Matala luottamus (pieni otos)",
677
+ "noReviews": "Ei vielä arvioita. Arvioi valmis tehtävä aloittaaksesi laatutilastojen kerryttämisen.",
678
+ "noCustom": "Ei julkaistuja omia mittareita.",
679
+ "grounded": "{count} lähteistettyä",
680
+ "seconds": "s",
681
+ "contexts": {
682
+ "factual": "Faktatieto",
683
+ "creative": "Luova",
684
+ "code": "Koodi",
685
+ "planning": "Suunnittelu",
686
+ "summarization": "Tiivistäminen",
687
+ "research": "Tutkimus",
688
+ "communication": "Viestintä",
689
+ "other": "Muu"
690
+ }
653
691
  },
654
692
  "state": {
655
693
  "new": "Uusi",
@@ -23,8 +23,10 @@
23
23
  * v2.8.0 -- 2026-05-31 -- Add pop-out button (pf-agd-popout-btn), drag-to-reorder rows (pf-agd-dnd-*), and solo pop-out window layout (pf-agd-solo*)
24
24
  * v2.9.0 -- 2026-05-31 -- Collapsed bar two-row layout (pf-agd-collapsed-main + pf-agd-collapsed-meta divider) to stop status overflow; move drag grip into the left margin so the card border stays flush
25
25
  * v2.10.0 -- 2026-05-31 -- Extend the drag-row drop target left over the grip gutter (padding-left + negative margin-left) so dropping on/near the handle registers; card border stays aligned
26
+ * v2.11.0 -- 2026-05-31 -- Add Quality tab styles (per-context review rows, duration chips, custom-metric rows)
26
27
  * v2.11.0 -- 2026-05-31 -- Widen the solo pop-out window (and pf-agd-solo max-width) so the agent card matches the list-view width and the full tab bar fits
27
28
  * v2.12.0 -- 2026-05-31 -- Add completed-task deliverable styles (pf-agd-deliverable*) linking a task to its agent-memory output key
29
+ * v2.13.0 -- 2026-05-31 -- Add per-task blur-title toggle styles (pf-agd-blur-toggle, pf-agd-task-title--blurred) for hiding sensitive titles in screen recordings
28
30
  */
29
31
 
30
32
  /* Header row: title left, connect button right */
@@ -2528,6 +2530,29 @@
2528
2530
  align-items: center;
2529
2531
  gap: 0.5rem;
2530
2532
  }
2533
+
2534
+ /* Per-task "hide title" toggle (eye icon) -- used to blur sensitive titles for
2535
+ screen recordings. Preference is browser-only (localStorage). */
2536
+ .pf .pf-agd-blur-toggle {
2537
+ flex-shrink: 0;
2538
+ background: transparent;
2539
+ border: none;
2540
+ padding: 0;
2541
+ margin: 0;
2542
+ font-size: 0.95rem;
2543
+ line-height: 1;
2544
+ cursor: pointer;
2545
+ opacity: 0.45;
2546
+ transition: opacity 0.15s ease;
2547
+ }
2548
+ .pf .pf-agd-blur-toggle:hover { opacity: 0.9; }
2549
+ .pf .pf-agd-blur-toggle--on { opacity: 0.85; }
2550
+
2551
+ .pf .pf-agd-task-title--blurred {
2552
+ filter: blur(5px);
2553
+ user-select: none;
2554
+ cursor: pointer;
2555
+ }
2531
2556
  .pf .agd-task-group-title ,
2532
2557
  .pf .pf-agd-task-group-title {
2533
2558
  margin-top: 0.5rem;
@@ -2655,3 +2680,113 @@
2655
2680
  border-left: 2px solid var(--info, #3B82F6);
2656
2681
  padding-left: 0.5rem;
2657
2682
  }
2683
+
2684
+ /* ── Quality tab ── */
2685
+
2686
+ .pf .pf-agd-quality-desc {
2687
+ font-size: 0.8rem;
2688
+ color: var(--text-dim, #6B7280);
2689
+ margin: 0.25rem 0 0.75rem;
2690
+ line-height: 1.45;
2691
+ }
2692
+
2693
+ .pf .pf-agd-quality .pf-agd-section-title {
2694
+ margin-top: 1.25rem;
2695
+ }
2696
+
2697
+ /* Duration-by-context chips under the performance cards */
2698
+ .pf .pf-agd-quality-durations {
2699
+ display: flex;
2700
+ flex-wrap: wrap;
2701
+ gap: 0.4rem;
2702
+ margin-bottom: 0.5rem;
2703
+ }
2704
+ .pf .pf-agd-quality-dur-chip {
2705
+ font-size: 0.75rem;
2706
+ color: var(--text-dim, #6B7280);
2707
+ background: var(--bg-dim, #F3F4F6);
2708
+ border: 1px solid var(--border, #E5E7EB);
2709
+ border-radius: 6px;
2710
+ padding: 0.15rem 0.5rem;
2711
+ }
2712
+
2713
+ /* Per-context review rows */
2714
+ .pf .pf-agd-quality-reviews {
2715
+ display: flex;
2716
+ flex-direction: column;
2717
+ gap: 0.5rem;
2718
+ }
2719
+ .pf .pf-agd-quality-review-row {
2720
+ border: 1px solid var(--border, #E5E7EB);
2721
+ border-radius: 8px;
2722
+ padding: 0.6rem 0.75rem;
2723
+ background: var(--card-bg, #FFFFFF);
2724
+ }
2725
+ .pf .pf-agd-quality-review-head {
2726
+ display: flex;
2727
+ align-items: center;
2728
+ flex-wrap: wrap;
2729
+ gap: 0.5rem;
2730
+ }
2731
+ .pf .pf-agd-quality-ctx {
2732
+ font-weight: 600;
2733
+ color: var(--text, #1A1A2E);
2734
+ min-width: 7rem;
2735
+ }
2736
+ .pf .pf-agd-quality-stars {
2737
+ color: var(--accent, #E8564A);
2738
+ letter-spacing: 1px;
2739
+ }
2740
+ .pf .pf-agd-quality-avg {
2741
+ font-weight: 600;
2742
+ color: var(--text, #1A1A2E);
2743
+ }
2744
+ .pf .pf-agd-quality-n {
2745
+ font-size: 0.75rem;
2746
+ color: var(--text-muted, #9CA3AF);
2747
+ }
2748
+ .pf .pf-agd-quality-lowconf {
2749
+ font-size: 0.7rem;
2750
+ color: var(--warning, #D97706);
2751
+ border: 1px solid var(--warning, #D97706);
2752
+ border-radius: 6px;
2753
+ padding: 0.05rem 0.4rem;
2754
+ }
2755
+ .pf .pf-agd-quality-review-meta {
2756
+ font-size: 0.72rem;
2757
+ color: var(--text-muted, #9CA3AF);
2758
+ margin-top: 0.3rem;
2759
+ }
2760
+ .pf .pf-agd-quality-overall {
2761
+ margin-top: 0.4rem;
2762
+ font-size: 0.85rem;
2763
+ color: var(--text-dim, #6B7280);
2764
+ }
2765
+ .pf .pf-agd-quality-overall .pf-agd-quality-stars,
2766
+ .pf .pf-agd-quality-overall {
2767
+ letter-spacing: 0.5px;
2768
+ }
2769
+
2770
+ /* Custom metric rows */
2771
+ .pf .pf-agd-quality-custom {
2772
+ display: flex;
2773
+ flex-direction: column;
2774
+ gap: 0.3rem;
2775
+ }
2776
+ .pf .pf-agd-quality-custom-row {
2777
+ display: flex;
2778
+ justify-content: space-between;
2779
+ gap: 1rem;
2780
+ font-size: 0.8rem;
2781
+ padding: 0.3rem 0;
2782
+ border-bottom: 1px solid var(--border, #E5E7EB);
2783
+ }
2784
+ .pf .pf-agd-quality-custom-key {
2785
+ color: var(--text-dim, #6B7280);
2786
+ font-family: var(--font-mono, monospace);
2787
+ }
2788
+ .pf .pf-agd-quality-custom-val {
2789
+ color: var(--text, #1A1A2E);
2790
+ text-align: right;
2791
+ word-break: break-word;
2792
+ }
@@ -6,6 +6,7 @@
6
6
  * @version-history
7
7
  * v1.0.0 -- 2026-05-21 -- Initial creation for Agent Dashboard Phase 1
8
8
  * v1.1.0 -- 2026-05-29 -- Add requestChanges() for the owner-asks-agent-to-revise-todos flow.
9
+ * v1.2.0 -- 2026-05-31 -- Add rateTask() and getAgentStatistics() for the Quality tab.
9
10
  */
10
11
  import { apiGet, apiPost, apiDelete } from '/js/api.js';
11
12
 
@@ -49,3 +50,13 @@ export async function requestChanges(agentName, taskId, message) {
49
50
  export async function listEvents(agentName, taskId) {
50
51
  return apiGet(`/v1/agents/${encodeURIComponent(agentName)}/tasks/${encodeURIComponent(taskId)}/events`);
51
52
  }
53
+
54
+ /** Review a completed task's deliverable. body: { stars, context, comment?, source_grounded?, unsupported?, evaluated_model? } */
55
+ export async function rateTask(agentName, taskId, body) {
56
+ return apiPost(`/v1/agents/${encodeURIComponent(agentName)}/tasks/${encodeURIComponent(taskId)}/rate`, body);
57
+ }
58
+
59
+ /** Quality tab: recomputed performance + per-context review rollups + custom metrics. */
60
+ export async function getAgentStatistics(agentName) {
61
+ return apiGet(`/v1/agents/${encodeURIComponent(agentName)}/statistics`);
62
+ }
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * @file agent-card.js
3
3
  * @description Agent card component with collapsed/expanded states,
4
- * Two-Zone Header (identity + state-dependent status), and 8-tab bar.
4
+ * Two-Zone Header (identity + state-dependent status), and tab bar.
5
5
  * @version-history
6
+ * v1.8.0 -- 2026-05-31 -- Add Quality tab (per-context reviews + performance + custom stats)
6
7
  * v1.0.0 -- 2026-05-24 -- Initial creation for Agent Detail Tab-View
7
8
  * v1.1.0 -- 2026-05-24 -- Fix C6: remove GAII from Zone 1, M1: add Next prefix, C1: production stats, C2: problem action buttons
8
9
  * v1.3.0 -- 2026-05-24 -- Audit fix: use proper down arrow glyph for collapse icon
@@ -36,6 +37,7 @@ import TabDataAccess from './tab-data-access.js';
36
37
  import TabDirectives from './tab-directives.js';
37
38
  import TabAgentConfig from './tab-agent-config.js';
38
39
  import TabActivity from './tab-activity.js';
40
+ import TabQuality from './tab-quality.js';
39
41
  import TabServices from './tab-services.js';
40
42
 
41
43
  const html = htm.bind(h);
@@ -50,6 +52,7 @@ const TABS = [
50
52
  { id: 'directives', key: 'profile.agents.detail.tabs.directives' },
51
53
  { id: 'agent-config', key: 'profile.agents.detail.tabs.agent_config' },
52
54
  { id: 'activity', key: 'profile.agents.detail.tabs.activity' },
55
+ { id: 'quality', key: 'profile.agents.detail.tabs.quality' },
53
56
  { id: 'services', key: 'profile.agents.detail.tabs.services' },
54
57
  ];
55
58
 
@@ -488,6 +491,7 @@ function renderTabContent(activeTab, agent, onboarding, session, showToast, allA
488
491
  case 'directives': return html`<${TabDirectives} ...${props} />`;
489
492
  case 'agent-config': return html`<${TabAgentConfig} ...${props} />`;
490
493
  case 'activity': return html`<${TabActivity} ...${props} />`;
494
+ case 'quality': return html`<${TabQuality} ...${props} />`;
491
495
  case 'services': return html`<${TabServices} ...${props} />`;
492
496
  default: return null;
493
497
  }
@@ -4,6 +4,10 @@
4
4
  * during onboarding or production status (connection, platform, readiness,
5
5
  * identity, delivery log) after completion.
6
6
  * @version-history
7
+ * v1.5.0 -- 2026-05-31 -- Live-update refresh no longer toggles the full-tab loading
8
+ * placeholder (added a showSpinner option). During Hello Integration the agent
9
+ * posts steps/telemetry rapidly, so the frequent SSE ticks were re-rendering the
10
+ * "Loading..." state on every tick -- the tab flashed like a full reload.
7
11
  * v1.4.0 -- 2026-05-28 -- Treat webhook with empty url as polling: status dot, label, and
8
12
  * the webhook section all key off (webhook && webhook.url), not just
9
13
  * the webhook object existing. A record with url="" is still polling.
@@ -48,8 +52,8 @@ export default function TabIntegration({ agent, onboarding, session, showToast,
48
52
  const [updatingBundle, setUpdatingBundle] = useState(false);
49
53
  const [copiedPrompt, setCopiedPrompt] = useState(false);
50
54
 
51
- async function loadData() {
52
- setLoading(true);
55
+ async function loadData({ showSpinner = true } = {}) {
56
+ if (showSpinner) setLoading(true);
53
57
  try {
54
58
  const [whResp, sbResp, dlResp, obResp] = await Promise.all([
55
59
  getWebhookConfig(agentName).catch(() => null),
@@ -62,7 +66,7 @@ export default function TabIntegration({ agent, onboarding, session, showToast,
62
66
  setDeliveries(dlResp?.data?.deliveries || []);
63
67
  setPostChecklist(obResp?.data?.post_onboarding_checklist || null);
64
68
  } catch { /* silent */ }
65
- setLoading(false);
69
+ if (showSpinner) setLoading(false);
66
70
  }
67
71
 
68
72
  useEffect(() => { loadData(); }, [agentName]);
@@ -70,7 +74,11 @@ export default function TabIntegration({ agent, onboarding, session, showToast,
70
74
  const loadRef = useRef(loadData);
71
75
  loadRef.current = loadData;
72
76
  useEffect(() => {
73
- const handler = () => loadRef.current();
77
+ // Background refresh on every live-update WITHOUT toggling the full-tab
78
+ // "Loading..." placeholder. During Hello Integration the agent posts steps
79
+ // + telemetry rapidly, so this fires often; showing the spinner each time
80
+ // made the whole tab flash like a reload. Initial mount still shows it.
81
+ const handler = () => loadRef.current({ showSpinner: false });
74
82
  window.addEventListener('aimeat-live-update', handler);
75
83
  return () => window.removeEventListener('aimeat-live-update', handler);
76
84
  }, []);
@@ -0,0 +1,159 @@
1
+ /**
2
+ * @file tab-quality.js
3
+ * @description Quality tab for the expanded agent view. Reads the recomputed
4
+ * GET /v1/agents/:name/statistics rollups and renders three sections:
5
+ * Performance (task counts, success rate, make-time, durations by context),
6
+ * Reviews by context (per-context star ratings + sample size + low-confidence
7
+ * flag), and Custom metrics (whatever the agent published under
8
+ * agents.<name>.statistics.custom.*). Quality READS statistics + reviews — it
9
+ * is the evaluative lens over the objective data.
10
+ * @structure default export TabQuality({ agentName })
11
+ * @usage rendered by agent-card.js renderTabContent() for the 'quality' tab
12
+ * @version-history
13
+ * v1.0.0 -- 2026-05-31 -- Initial Quality tab (per-context reviews + performance + custom)
14
+ */
15
+
16
+ import { h } from 'preact';
17
+ import { useState, useEffect, useRef } from 'preact/hooks';
18
+ import htm from 'htm';
19
+ import { t } from '/js/i18n.js';
20
+ import { getAgentStatistics } from '/js/services/agent-tasks.js';
21
+
22
+ const html = htm.bind(h);
23
+
24
+ /** Glyph row for an average star value (rounded to nearest whole star). */
25
+ function starGlyphs(value) {
26
+ const full = Math.max(0, Math.min(5, Math.round(value)));
27
+ return '★★★★★'.slice(0, full) + '☆☆☆☆☆'.slice(0, 5 - full);
28
+ }
29
+
30
+ /** Localised context label, falling back to the raw enum value. */
31
+ function ctxLabel(ctx) {
32
+ const key = `profile.agents.detail.quality.contexts.${ctx}`;
33
+ const label = t(key);
34
+ return label !== key ? label : ctx;
35
+ }
36
+
37
+ function fmtSeconds(secs) {
38
+ return `${Number(secs || 0).toLocaleString()}${t('profile.agents.detail.quality.seconds')}`;
39
+ }
40
+
41
+ export default function TabQuality({ agentName }) {
42
+ const [data, setData] = useState(null);
43
+ const [loading, setLoading] = useState(true);
44
+
45
+ async function loadData() {
46
+ setLoading(true);
47
+ try {
48
+ const resp = await getAgentStatistics(agentName);
49
+ setData(resp?.data || null);
50
+ } catch { setData(null); }
51
+ setLoading(false);
52
+ }
53
+
54
+ useEffect(() => { loadData(); }, [agentName]);
55
+
56
+ // Live updates: refresh when a rating lands or tasks change.
57
+ const loadRef = useRef(loadData);
58
+ loadRef.current = loadData;
59
+ useEffect(() => {
60
+ const handler = () => loadRef.current();
61
+ window.addEventListener('aimeat-live-update', handler);
62
+ return () => window.removeEventListener('aimeat-live-update', handler);
63
+ }, []);
64
+
65
+ if (loading) {
66
+ return html`<div class="pf-agd-empty">${t('profile.loading')}</div>`;
67
+ }
68
+
69
+ const perf = data?.performance || {};
70
+ const reviews = data?.reviews || {};
71
+ const custom = data?.custom || [];
72
+ const byContext = reviews.byContext || {};
73
+ const contextKeys = Object.keys(byContext);
74
+ const durByContext = perf.duration?.byContext || {};
75
+ const durKeys = Object.keys(durByContext);
76
+
77
+ return html`
78
+ <div class="pf-agd-quality">
79
+ <!-- Performance -->
80
+ <div class="pf-agd-section-title">${t('profile.agents.detail.quality.performanceTitle')}</div>
81
+ <div class="pf-agd-quality-desc">${t('profile.agents.detail.quality.performanceDesc')}</div>
82
+ <div class="pf-agd-stats-grid">
83
+ <div class="pf-agd-stat-card">
84
+ <div class="pf-agd-stat-value">${perf.tasks?.total ?? 0}</div>
85
+ <div class="pf-agd-stat-label">${t('profile.agents.detail.quality.tasksTotal')}</div>
86
+ </div>
87
+ <div class="pf-agd-stat-card">
88
+ <div class="pf-agd-stat-value">${perf.tasks?.completed ?? 0}</div>
89
+ <div class="pf-agd-stat-label">${t('profile.agents.detail.quality.tasksCompleted')}</div>
90
+ </div>
91
+ <div class="pf-agd-stat-card">
92
+ <div class="pf-agd-stat-value">${perf.tasks?.successRate != null ? `${Math.round(perf.tasks.successRate * 100)}%` : '-'}</div>
93
+ <div class="pf-agd-stat-label">${t('profile.agents.detail.quality.successRate')}</div>
94
+ </div>
95
+ <div class="pf-agd-stat-card">
96
+ <div class="pf-agd-stat-value">${fmtSeconds(perf.duration?.avgCompletionSeconds)}</div>
97
+ <div class="pf-agd-stat-label">${t('profile.agents.detail.quality.avgTime')}</div>
98
+ </div>
99
+ <div class="pf-agd-stat-card">
100
+ <div class="pf-agd-stat-value">${perf.events?.total ?? 0}</div>
101
+ <div class="pf-agd-stat-label">${t('profile.agents.detail.quality.events')}</div>
102
+ </div>
103
+ </div>
104
+ ${durKeys.length > 0 && html`
105
+ <div class="pf-agd-quality-durations">
106
+ ${durKeys.map(ctx => html`
107
+ <span key=${ctx} class="pf-agd-quality-dur-chip">
108
+ ${ctxLabel(ctx)}: ${fmtSeconds(durByContext[ctx].avgSeconds)} (${durByContext[ctx].count})
109
+ </span>
110
+ `)}
111
+ </div>
112
+ `}
113
+
114
+ <!-- Reviews by context -->
115
+ <div class="pf-agd-section-title">${t('profile.agents.detail.quality.reviewsTitle')}</div>
116
+ <div class="pf-agd-quality-desc">${t('profile.agents.detail.quality.reviewsDesc')}</div>
117
+ ${contextKeys.length === 0
118
+ ? html`<div class="pf-agd-empty">${t('profile.agents.detail.quality.noReviews')}</div>`
119
+ : html`
120
+ <div class="pf-agd-quality-reviews">
121
+ ${contextKeys.map(ctx => {
122
+ const s = byContext[ctx];
123
+ return html`
124
+ <div key=${ctx} class="pf-agd-quality-review-row">
125
+ <div class="pf-agd-quality-review-head">
126
+ <span class="pf-agd-quality-ctx">${ctxLabel(ctx)}</span>
127
+ <span class="pf-agd-quality-stars">${starGlyphs(s.avgStars)}</span>
128
+ <span class="pf-agd-quality-avg">${Number(s.avgStars).toFixed(1)}</span>
129
+ <span class="pf-agd-quality-n">${t('profile.agents.detail.quality.ratings', { count: s.n })}</span>
130
+ ${s.lowConfidence && html`<span class="pf-agd-quality-lowconf">${t('profile.agents.detail.quality.lowConfidence')}</span>`}
131
+ </div>
132
+ <div class="pf-agd-quality-review-meta">${t('profile.agents.detail.quality.grounded', { count: s.sourceGroundedN })}</div>
133
+ </div>
134
+ `;
135
+ })}
136
+ <div class="pf-agd-quality-overall">
137
+ ${t('profile.agents.detail.quality.overall')}: ${starGlyphs(reviews.overall?.avgStars || 0)} ${Number(reviews.overall?.avgStars || 0).toFixed(1)} (${t('profile.agents.detail.quality.ratings', { count: reviews.overall?.n || 0 })})
138
+ </div>
139
+ </div>
140
+ `}
141
+
142
+ <!-- Custom metrics -->
143
+ <div class="pf-agd-section-title">${t('profile.agents.detail.quality.customTitle')}</div>
144
+ <div class="pf-agd-quality-desc">${t('profile.agents.detail.quality.customDesc')}</div>
145
+ ${custom.length === 0
146
+ ? html`<div class="pf-agd-empty">${t('profile.agents.detail.quality.noCustom')}</div>`
147
+ : html`
148
+ <div class="pf-agd-quality-custom">
149
+ ${custom.map(c => html`
150
+ <div key=${c.key} class="pf-agd-quality-custom-row">
151
+ <span class="pf-agd-quality-custom-key">${c.key}</span>
152
+ <span class="pf-agd-quality-custom-val">${typeof c.value === 'object' ? JSON.stringify(c.value) : String(c.value)}</span>
153
+ </div>
154
+ `)}
155
+ </div>
156
+ `}
157
+ </div>
158
+ `;
159
+ }
@@ -9,6 +9,9 @@
9
9
  * - TaskItem -- task row with expand/collapse, todo list, start button
10
10
  * - RequestChangesModal -- inline modal for owner to send a free-text change request
11
11
  * @version-history
12
+ * v4.3.0 -- 2026-05-31 -- Add per-task "blur title" toggle in the collapsed
13
+ * task row (eye icon). Hides a task title behind a CSS blur for screen
14
+ * recordings; preference persists per task ID in localStorage (browser-only).
12
15
  * v3.2.0 -- 2026-05-24 -- Add interactive filter pills, scope/rules display, migrate to pf-agd- CSS prefix
13
16
  * v3.1.0 -- 2026-05-24 -- Fix: use correct locale key for empty state
14
17
  * v3.0.0 -- 2026-05-22 -- Replace TaskCreateForm with TaskCreationBuilder (design spec split-panel)
@@ -17,6 +20,9 @@
17
20
  * v1.0.0 -- 2026-05-21 -- Initial creation for Agent Dashboard Phase 1
18
21
  * v3.3.0 -- 2026-05-29 -- Add delete confirmation, "Request changes" modal for the revise-proposed-todos flow, outdated-todo history rendering, and revision_requested status.
19
22
  * v3.3.1 -- 2026-05-30 -- Re-fetch the event log on every live-update tick while a task card is expanded. Without this, the parent refreshed the task object (todos + status) but the events list under it stayed frozen until the user closed and re-opened the card.
23
+ * v4.2.0 -- 2026-05-31 -- Add owner "Cancel" on active/stalled tasks: writes an
24
+ * agents.cancel.task.<id> marker (worker daemons self-skip before kickoff) +
25
+ * natively pauses the active task for immediate effect.
20
26
  * v4.1.0 -- 2026-05-31 -- Show a completed task's deliverable: link to the
21
27
  * agent-memory key it was published to (task.deliverableKey), fetch + preview
22
28
  * the value on demand, and show "no longer exists" if the entry is gone.
@@ -34,12 +40,35 @@ import htm from 'htm';
34
40
  const html = htm.bind(h);
35
41
  import { t } from '/js/i18n.js';
36
42
  import { timeAgo } from '/js/utils.js';
37
- import { apiGet } from '/js/api.js';
43
+ import { apiGet, apiPost } from '/js/api.js';
38
44
  import { listTasks, deleteTask, startTask, listEvents, requestChanges, createTask } from '/js/services/agent-tasks.js';
39
45
  import { useConfirm, Modal } from '/components/Modal.js';
40
46
 
41
47
  const TASK_FILTERS = ['all', 'active', 'queued', 'completed', 'failed'];
42
48
 
49
+ // Per-browser "blur the title" preference. Used when screen-recording the tab
50
+ // so sensitive task titles can be hidden without affecting other viewers or
51
+ // the server. Stored as an array of task IDs in localStorage; survives reloads
52
+ // but never leaves this browser.
53
+ const BLUR_STORAGE_KEY = 'aimeat.blurredTaskTitles';
54
+
55
+ function readBlurredSet() {
56
+ try {
57
+ const parsed = JSON.parse(localStorage.getItem(BLUR_STORAGE_KEY));
58
+ return new Set(Array.isArray(parsed) ? parsed : []);
59
+ } catch { return new Set(); }
60
+ }
61
+
62
+ function isTaskBlurred(taskId) {
63
+ return readBlurredSet().has(taskId);
64
+ }
65
+
66
+ function setTaskBlurred(taskId, blurred) {
67
+ const set = readBlurredSet();
68
+ if (blurred) set.add(taskId); else set.delete(taskId);
69
+ try { localStorage.setItem(BLUR_STORAGE_KEY, JSON.stringify([...set])); } catch { /* storage full/blocked -- preference just won't persist */ }
70
+ }
71
+
43
72
  // Character limits -- keep in sync with AgentTaskCreateSchema in
44
73
  // src/models/agent-task-schemas.ts (title max 256, description max 10000).
45
74
  // The agent crew reads task.description as its prompt, so that is the field
@@ -183,8 +212,18 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
183
212
  const [showOutdated, setShowOutdated] = useState(false);
184
213
  // Deliverable preview: null = not requested, {loading}|{notFound}|{value}.
185
214
  const [deliverable, setDeliverable] = useState(null);
215
+ // Local-only "hide the title" toggle (for screen recordings). Persisted per
216
+ // task ID in localStorage; see helpers at top of file.
217
+ const [blurred, setBlurred] = useState(() => isTaskBlurred(task.id));
186
218
  const { confirm, ConfirmUI } = useConfirm();
187
219
 
220
+ function handleToggleBlur(e) {
221
+ e.stopPropagation();
222
+ const next = !blurred;
223
+ setBlurred(next);
224
+ setTaskBlurred(task.id, next);
225
+ }
226
+
188
227
  // Fetch the task's published deliverable from the agent's memory namespace.
189
228
  // Uses the owner list endpoint (?agent=<gaii>&prefix=<key>) which returns the
190
229
  // value inline; if the exact key is gone, show that it no longer exists.
@@ -265,6 +304,32 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
265
304
  );
266
305
  }
267
306
 
307
+ // Cooperative cancel for a running/queued subtask: write an
308
+ // `agents.cancel.task.<id>` marker (owner_scope-visible, so the worker daemon
309
+ // self-skips before its next kickoff) AND, for immediate effect, natively
310
+ // pause an active task. Covers the "coordinator over-delegated; stop the work
311
+ // nobody is waiting for" case.
312
+ function handleCancel(e) {
313
+ e.stopPropagation();
314
+ confirm(
315
+ t('profile.agents.tasks.cancelConfirm') + ': ' + (task.title || task.id) + '?',
316
+ async () => {
317
+ try {
318
+ await apiPost('/v1/memory', { key: 'agents.cancel.task.' + task.id, value: [task.id], visibility: 'owner' });
319
+ // Immediate native stop (owner-only): pause active tasks.
320
+ if (task.status === 'active') {
321
+ try { await apiPost(`/v1/agents/${encodeURIComponent(agentName)}/tasks/${encodeURIComponent(task.id)}/pause`, {}); } catch { /* marker still applies */ }
322
+ }
323
+ showToast(t('profile.agents.tasks.cancelled'));
324
+ onRefresh();
325
+ } catch (err) {
326
+ showToast(err.message || t('profile.agents.tasks.cancelError'), true);
327
+ }
328
+ },
329
+ { danger: true },
330
+ );
331
+ }
332
+
268
333
  function handleOpenRevision(e) {
269
334
  e.stopPropagation();
270
335
  setShowRevisionModal(true);
@@ -309,7 +374,13 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
309
374
  <div>
310
375
  <div class="pf-agd-task-item" onClick=${handleExpand}>
311
376
  <div class="pf-agd-task-title-row">
312
- <span class="pf-agd-task-title">${task.title || task.id}</span>
377
+ <button
378
+ class=${`pf-agd-blur-toggle ${blurred ? 'pf-agd-blur-toggle--on' : ''}`}
379
+ onClick=${handleToggleBlur}
380
+ title=${blurred ? t('profile.agents.tasks.unblurTitle') : t('profile.agents.tasks.blurTitle')}
381
+ aria-pressed=${blurred}
382
+ >${blurred ? '🙈' : '👁'}</button>
383
+ <span class=${`pf-agd-task-title ${blurred ? 'pf-agd-task-title--blurred' : ''}`}>${task.title || task.id}</span>
313
384
  ${progress && html`<span class="pf-agd-todo-progress">${progress}</span>`}
314
385
  </div>
315
386
  <div class="pf-agd-task-meta">
@@ -454,6 +525,9 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
454
525
  ${t('profile.agents.tasks.requestChanges')}
455
526
  </button>
456
527
  `}
528
+ ${(isActive || task.status === 'stalled') && html`
529
+ <button class="btn-danger btn-sm" onClick=${handleCancel}>${t('profile.agents.tasks.cancel')}</button>
530
+ `}
457
531
  ${(isQueued || task.status === 'draft' || isRevisionRequested) && html`
458
532
  <button class="btn-danger btn-sm" onClick=${handleDelete}>${t('profile.agents.tasks.delete')}</button>
459
533
  `}