aimeat 1.16.0 → 1.16.2

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 (34) hide show
  1. package/dist/locales/en.json +25 -0
  2. package/dist/locales/fi.json +25 -0
  3. package/dist/public/css/views/agents-detail.css +144 -0
  4. package/dist/public/js/services/agents.js +8 -0
  5. package/dist/public/views/profile/agents/rate-modal.js +87 -0
  6. package/dist/public/views/profile/agents/tab-quality.js +58 -4
  7. package/dist/public/views/profile/agents/tab-tasks.js +4 -2
  8. package/dist/public/views/profile/agents-tasks-subtab.js +91 -2
  9. package/dist/src/generated/api-types.d.ts +66 -0
  10. package/dist/src/generated/api-types.d.ts.map +1 -1
  11. package/dist/src/routes/agent-activity.d.ts +1 -0
  12. package/dist/src/routes/agent-activity.d.ts.map +1 -1
  13. package/dist/src/routes/agent-activity.js +6 -3
  14. package/dist/src/routes/agent-activity.js.map +1 -1
  15. package/dist/src/routes/agent-integration.d.ts.map +1 -1
  16. package/dist/src/routes/agent-integration.js +4 -0
  17. package/dist/src/routes/agent-integration.js.map +1 -1
  18. package/dist/src/routes/agents.d.ts.map +1 -1
  19. package/dist/src/routes/agents.js +35 -0
  20. package/dist/src/routes/agents.js.map +1 -1
  21. package/dist/src/storage/interface.d.ts +8 -0
  22. package/dist/src/storage/interface.d.ts.map +1 -1
  23. package/dist/src/storage/interface.js.map +1 -1
  24. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  25. package/dist/src/storage/providers/mongodb/index.js +2 -1
  26. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  27. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  28. package/dist/src/storage/providers/sqlite/index.js +6 -4
  29. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  30. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  31. package/dist/src/storage/providers/sqlite/schema.js +5 -0
  32. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  33. package/package.json +1 -1
  34. package/prisma/schema.prisma +1 -0
@@ -420,6 +420,12 @@
420
420
  "title": "Task Queue",
421
421
  "empty": "No tasks yet",
422
422
  "newTask": "New Task",
423
+ "concurrency": {
424
+ "label": "Max concurrent tasks",
425
+ "hint": "1 = serial. Higher needs a concurrency-capable runner (e.g. a CrewAI daemon).",
426
+ "saved": "Concurrency updated",
427
+ "error": "Failed to update concurrency"
428
+ },
423
429
  "createTask": "Create Task",
424
430
  "cancel": "Cancel",
425
431
  "titleLabel": "Title (optional)",
@@ -458,6 +464,21 @@
458
464
  "deliverable": "Deliverable",
459
465
  "viewDeliverable": "View",
460
466
  "deliverableGone": "The deliverable entry no longer exists.",
467
+ "rate": {
468
+ "button": "Rate deliverable",
469
+ "rerate": "Re-rate",
470
+ "title": "Rate this deliverable",
471
+ "help": "Give the deliverable 1–5 stars and pick the context it should be judged in. Factual contexts should be checked against the sources, not rated on looks alone.",
472
+ "context": "Context",
473
+ "grounded": "I checked this against the sources / inputs",
474
+ "commentPlaceholder": "Optional: what was good or wrong?",
475
+ "submit": "Submit rating",
476
+ "submitting": "Saving...",
477
+ "rated": "Rated",
478
+ "needStars": "Pick a star rating first",
479
+ "success": "Rating saved",
480
+ "error": "Failed to save rating"
481
+ },
461
482
  "cancel": "Cancel",
462
483
  "cancelConfirm": "Cancel this task? Work that hasn't started will be skipped",
463
484
  "cancelled": "Task cancelled",
@@ -663,6 +684,10 @@
663
684
  "performanceDesc": "How much this agent has done and how fast — computed from its tasks.",
664
685
  "reviewsTitle": "Reviews by context",
665
686
  "reviewsDesc": "Per-context star ratings of the agent's deliverables. Factual contexts are source-grounded.",
687
+ "pendingTitle": "Rate deliverables",
688
+ "pendingDesc": "Completed tasks you can rate. Your ratings build the per-context quality picture above.",
689
+ "allRated": "All completed deliverables are rated.",
690
+ "unrated": "Not rated",
666
691
  "customTitle": "Custom metrics",
667
692
  "customDesc": "Metrics the agent publishes about itself.",
668
693
  "tasksTotal": "Tasks",
@@ -420,6 +420,12 @@
420
420
  "title": "Tehtäväjono",
421
421
  "empty": "Ei tehtäviä vielä",
422
422
  "newTask": "Uusi tehtävä",
423
+ "concurrency": {
424
+ "label": "Tehtäviä rinnakkain enintään",
425
+ "hint": "1 = sarjallinen. Suurempi vaatii rinnakkaisuutta tukevan ajurin (esim. CrewAI-daemon).",
426
+ "saved": "Rinnakkaisuus päivitetty",
427
+ "error": "Rinnakkaisuuden päivitys epäonnistui"
428
+ },
423
429
  "createTask": "Luo tehtävä",
424
430
  "cancel": "Peruuta",
425
431
  "titleLabel": "Otsikko (valinnainen)",
@@ -458,6 +464,21 @@
458
464
  "deliverable": "Tuotos",
459
465
  "viewDeliverable": "Näytä",
460
466
  "deliverableGone": "Tuotosmerkintää ei ole enää olemassa.",
467
+ "rate": {
468
+ "button": "Arvioi tuotos",
469
+ "rerate": "Arvioi uudelleen",
470
+ "title": "Arvioi tämä tuotos",
471
+ "help": "Anna tuotokselle 1–5 tähteä ja valitse konteksti jossa sitä arvioidaan. Faktakontekstit tulisi tarkistaa lähteitä vasten, ei arvioida pelkän ulkoasun perusteella.",
472
+ "context": "Konteksti",
473
+ "grounded": "Tarkistin tämän lähteitä / syötteitä vasten",
474
+ "commentPlaceholder": "Valinnainen: mikä oli hyvää tai pielessä?",
475
+ "submit": "Tallenna arvio",
476
+ "submitting": "Tallennetaan...",
477
+ "rated": "Arvioitu",
478
+ "needStars": "Valitse ensin tähtiarvio",
479
+ "success": "Arvio tallennettu",
480
+ "error": "Arvion tallennus epäonnistui"
481
+ },
461
482
  "cancel": "Peruuta",
462
483
  "cancelConfirm": "Peruutetaanko tämä tehtävä? Aloittamaton työ ohitetaan",
463
484
  "cancelled": "Tehtävä peruutettu",
@@ -663,6 +684,10 @@
663
684
  "performanceDesc": "Kuinka paljon agentti on tehnyt ja kuinka nopeasti — laskettu sen tehtävistä.",
664
685
  "reviewsTitle": "Arviot konteksteittain",
665
686
  "reviewsDesc": "Agentin tuotosten tähtiarviot konteksteittain. Faktakontekstit ovat lähteisiin perustuvia.",
687
+ "pendingTitle": "Arvioi tuotokset",
688
+ "pendingDesc": "Valmiit tehtävät joita voit arvioida. Arviosi rakentavat yllä olevan kontekstikohtaisen laatukuvan.",
689
+ "allRated": "Kaikki valmiit tuotokset on arvioitu.",
690
+ "unrated": "Ei arvioitu",
666
691
  "customTitle": "Omat mittarit",
667
692
  "customDesc": "Mittarit, joita agentti julkaisee itsestään.",
668
693
  "tasksTotal": "Tehtäviä",
@@ -24,6 +24,9 @@
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
26
  * v2.11.0 -- 2026-05-31 -- Add Quality tab styles (per-context review rows, duration chips, custom-metric rows)
27
+ * v2.12.0 -- 2026-05-31 -- Add deliverable rating control styles (star picker, current-rating row, rate modal fields)
28
+ * v2.13.0 -- 2026-05-31 -- Add Quality-tab "Rate deliverables" pending-list row styles
29
+ * v2.14.0 -- 2026-06-01 -- Add Tasks-tab max-concurrent-tasks config row styles
27
30
  * 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
28
31
  * v2.12.0 -- 2026-05-31 -- Add completed-task deliverable styles (pf-agd-deliverable*) linking a task to its agent-memory output key
29
32
  * 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
@@ -2790,3 +2793,144 @@
2790
2793
  text-align: right;
2791
2794
  word-break: break-word;
2792
2795
  }
2796
+
2797
+ /* ── Deliverable rating (Tasks tab) ── */
2798
+
2799
+ /* Current rating + rate/re-rate button row under a done task's deliverable */
2800
+ .pf .pf-agd-rate-row {
2801
+ display: flex;
2802
+ align-items: center;
2803
+ flex-wrap: wrap;
2804
+ gap: 0.5rem;
2805
+ margin: 0.5rem 0;
2806
+ }
2807
+ .pf .pf-agd-rate-current {
2808
+ display: inline-flex;
2809
+ align-items: center;
2810
+ flex-wrap: wrap;
2811
+ gap: 0.5rem;
2812
+ font-size: 0.85rem;
2813
+ }
2814
+ .pf .pf-agd-rate-current-stars {
2815
+ color: var(--accent, #E8564A);
2816
+ letter-spacing: 1px;
2817
+ }
2818
+ .pf .pf-agd-rate-current-ctx {
2819
+ color: var(--text-dim, #6B7280);
2820
+ font-weight: 600;
2821
+ }
2822
+ .pf .pf-agd-rate-current-comment {
2823
+ color: var(--text-muted, #9CA3AF);
2824
+ font-style: italic;
2825
+ }
2826
+
2827
+ /* Star picker inside the rate modal */
2828
+ .pf .pf-agd-rate-stars {
2829
+ display: flex;
2830
+ gap: 0.15rem;
2831
+ margin: 0.5rem 0 0.75rem;
2832
+ }
2833
+ .pf .pf-agd-rate-star {
2834
+ background: none;
2835
+ border: none;
2836
+ cursor: pointer;
2837
+ font-size: 1.6rem;
2838
+ line-height: 1;
2839
+ color: var(--border, #E5E7EB);
2840
+ padding: 0;
2841
+ transition: color 0.12s;
2842
+ }
2843
+ .pf .pf-agd-rate-star:hover {
2844
+ color: var(--accent, #E8564A);
2845
+ }
2846
+ .pf .pf-agd-rate-star--on {
2847
+ color: var(--accent, #E8564A);
2848
+ }
2849
+
2850
+ /* Context select + grounded checkbox rows in the rate modal */
2851
+ .pf .pf-agd-rate-field {
2852
+ display: flex;
2853
+ align-items: center;
2854
+ gap: 0.5rem;
2855
+ margin-bottom: 0.6rem;
2856
+ font-size: 0.85rem;
2857
+ color: var(--text-dim, #6B7280);
2858
+ }
2859
+ .pf .pf-agd-rate-field select {
2860
+ flex: 1;
2861
+ }
2862
+ .pf .pf-agd-rate-check {
2863
+ display: flex;
2864
+ align-items: center;
2865
+ gap: 0.5rem;
2866
+ margin-bottom: 0.6rem;
2867
+ font-size: 0.85rem;
2868
+ color: var(--text, #1A1A2E);
2869
+ cursor: pointer;
2870
+ }
2871
+
2872
+ /* Quality tab "Rate deliverables" pending list */
2873
+ .pf .pf-agd-quality-pending {
2874
+ display: flex;
2875
+ flex-direction: column;
2876
+ gap: 0.4rem;
2877
+ }
2878
+ .pf .pf-agd-quality-pending-row {
2879
+ display: flex;
2880
+ align-items: center;
2881
+ gap: 0.5rem;
2882
+ padding: 0.4rem 0.5rem;
2883
+ border: 1px solid var(--border, #E5E7EB);
2884
+ border-radius: 8px;
2885
+ background: var(--card-bg, #FFFFFF);
2886
+ }
2887
+ .pf .pf-agd-quality-pending-title {
2888
+ flex: 1;
2889
+ min-width: 0;
2890
+ overflow: hidden;
2891
+ text-overflow: ellipsis;
2892
+ white-space: nowrap;
2893
+ font-size: 0.85rem;
2894
+ color: var(--text, #1A1A2E);
2895
+ }
2896
+ .pf .pf-agd-quality-pending-rated {
2897
+ font-size: 0.8rem;
2898
+ color: var(--accent, #E8564A);
2899
+ letter-spacing: 0.5px;
2900
+ white-space: nowrap;
2901
+ }
2902
+ .pf .pf-agd-quality-pending-unrated {
2903
+ font-size: 0.72rem;
2904
+ color: var(--text-muted, #9CA3AF);
2905
+ border: 1px solid var(--border, #E5E7EB);
2906
+ border-radius: 6px;
2907
+ padding: 0.05rem 0.4rem;
2908
+ white-space: nowrap;
2909
+ }
2910
+
2911
+ /* Tasks tab — max concurrent tasks runner config */
2912
+ .pf .pf-agd-concurrency {
2913
+ display: flex;
2914
+ align-items: center;
2915
+ flex-wrap: wrap;
2916
+ gap: 0.5rem;
2917
+ margin: 0.25rem 0 0.75rem;
2918
+ font-size: 0.85rem;
2919
+ }
2920
+ .pf .pf-agd-concurrency-label {
2921
+ color: var(--text-dim, #6B7280);
2922
+ font-weight: 600;
2923
+ }
2924
+ .pf .pf-agd-concurrency-input {
2925
+ width: 4rem;
2926
+ padding: 0.25rem 0.4rem;
2927
+ border: 1px solid var(--border, #E5E7EB);
2928
+ border-radius: 6px;
2929
+ background: var(--card-bg, #FFFFFF);
2930
+ color: var(--text, #1A1A2E);
2931
+ font-size: 0.85rem;
2932
+ }
2933
+ .pf .pf-agd-concurrency-hint {
2934
+ color: var(--text-muted, #9CA3AF);
2935
+ font-size: 0.75rem;
2936
+ }
@@ -30,6 +30,14 @@ export async function updateAgentScopes(name, scopes) {
30
30
  });
31
31
  }
32
32
 
33
+ /** Set how many tasks the agent's runner may process concurrently (1–20). */
34
+ export async function setMaxConcurrentTasks(name, max) {
35
+ return api(`/v1/agents/${encodeURIComponent(name)}/max-concurrent-tasks`, {
36
+ method: 'PATCH',
37
+ body: JSON.stringify({ max_concurrent_tasks: max }),
38
+ });
39
+ }
40
+
33
41
  /** Get agent CORS origins. */
34
42
  export async function getAgentCors(name) {
35
43
  const data = await apiGet(`/v1/agents/${encodeURIComponent(name)}/cors`);
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @file rate-modal.js
3
+ * @description Shared "rate a deliverable" modal — 1–5 stars, quality context,
4
+ * an optional "checked against sources" flag, and a comment. Submits the
5
+ * POST /v1/agents/:name/tasks/:id/rate body shape. Used by both the Tasks
6
+ * sub-tab (rate from the task row) and the Quality tab (rate from the
7
+ * pending-deliverables list). Pre-fills from an existing rating so the same
8
+ * modal also does re-rate.
9
+ * @structure
10
+ * - RATE_CONTEXTS -- the RatingContext enum (mirrors src/storage/interface.ts)
11
+ * - RateModal (default export) -- the modal component
12
+ * @usage
13
+ * import RateModal, { RATE_CONTEXTS } from './rate-modal.js';
14
+ * <RateModal open onClose onSubmit submitting existing=${task.rating} />
15
+ * @version-history
16
+ * v1.0.0 -- 2026-05-31 -- Extracted from agents-tasks-subtab.js so the Quality
17
+ * tab can reuse the same rating modal.
18
+ */
19
+ import { h } from 'preact';
20
+ import { useState, useEffect } from 'preact/hooks';
21
+ import htm from 'htm';
22
+ import { t } from '/js/i18n.js';
23
+ import { Modal } from '/components/Modal.js';
24
+
25
+ const html = htm.bind(h);
26
+
27
+ // Quality contexts an owner can pick when rating a deliverable. Mirrors the
28
+ // RatingContext enum in src/storage/interface.ts; labels come from the shared
29
+ // profile.agents.detail.quality.contexts.* i18n block.
30
+ export const RATE_CONTEXTS = ['factual', 'creative', 'code', 'planning', 'summarization', 'research', 'communication', 'other'];
31
+
32
+ export default function RateModal({ open, onClose, onSubmit, submitting, existing }) {
33
+ const [stars, setStars] = useState(existing?.stars || 0);
34
+ const [context, setContext] = useState(existing?.context || 'creative');
35
+ const [grounded, setGrounded] = useState(existing?.sourceGrounded || false);
36
+ const [comment, setComment] = useState(existing?.comment || '');
37
+ useEffect(() => {
38
+ if (open) {
39
+ setStars(existing?.stars || 0);
40
+ setContext(existing?.context || 'creative');
41
+ setGrounded(existing?.sourceGrounded || false);
42
+ setComment(existing?.comment || '');
43
+ }
44
+ }, [open]);
45
+ function handleSend() {
46
+ if (!stars) return;
47
+ const body = { stars, context, source_grounded: grounded };
48
+ const c = comment.trim();
49
+ if (c) body.comment = c;
50
+ onSubmit(body);
51
+ }
52
+ return html`<${Modal} open=${open} onClose=${onClose} title=${t('profile.agents.tasks.rate.title')}>
53
+ <p class="pf-agd-modal-help">${t('profile.agents.tasks.rate.help')}</p>
54
+ <div class="pf-agd-rate-stars" role="radiogroup">
55
+ ${[1, 2, 3, 4, 5].map(n => html`
56
+ <button key=${n}
57
+ class=${`pf-agd-rate-star ${n <= stars ? 'pf-agd-rate-star--on' : ''}`}
58
+ onClick=${() => setStars(n)}
59
+ aria-label=${String(n)}
60
+ aria-pressed=${n <= stars}>★</button>
61
+ `)}
62
+ </div>
63
+ <label class="pf-agd-rate-field">
64
+ <span>${t('profile.agents.tasks.rate.context')}</span>
65
+ <select value=${context} onChange=${e => setContext(e.target.value)}>
66
+ ${RATE_CONTEXTS.map(c => html`<option key=${c} value=${c}>${t(`profile.agents.detail.quality.contexts.${c}`)}</option>`)}
67
+ </select>
68
+ </label>
69
+ <label class="pf-agd-rate-check">
70
+ <input type="checkbox" checked=${grounded} onChange=${e => setGrounded(e.target.checked)} />
71
+ <span>${t('profile.agents.tasks.rate.grounded')}</span>
72
+ </label>
73
+ <textarea
74
+ class="pf-agd-revision-textarea"
75
+ placeholder=${t('profile.agents.tasks.rate.commentPlaceholder')}
76
+ value=${comment}
77
+ onInput=${e => setComment(e.target.value)}
78
+ rows=${3}
79
+ ></textarea>
80
+ <div class="modal-footer">
81
+ <button class="btn-ghost" onClick=${onClose} disabled=${submitting}>${t('common.cancel') || 'Cancel'}</button>
82
+ <button class="btn-primary" onClick=${handleSend} disabled=${submitting || !stars}>
83
+ ${submitting ? t('profile.agents.tasks.rate.submitting') : t('profile.agents.tasks.rate.submit')}
84
+ </button>
85
+ </div>
86
+ <//>`;
87
+ }
@@ -10,6 +10,9 @@
10
10
  * @structure default export TabQuality({ agentName })
11
11
  * @usage rendered by agent-card.js renderTabContent() for the 'quality' tab
12
12
  * @version-history
13
+ * v1.1.0 -- 2026-05-31 -- Add "Rate deliverables" list: completed tasks the owner
14
+ * can rate inline via the shared RateModal (unrated first); refreshes the
15
+ * rollups on submit.
13
16
  * v1.0.0 -- 2026-05-31 -- Initial Quality tab (per-context reviews + performance + custom)
14
17
  */
15
18
 
@@ -17,7 +20,8 @@ import { h } from 'preact';
17
20
  import { useState, useEffect, useRef } from 'preact/hooks';
18
21
  import htm from 'htm';
19
22
  import { t } from '/js/i18n.js';
20
- import { getAgentStatistics } from '/js/services/agent-tasks.js';
23
+ import { getAgentStatistics, listTasks, rateTask } from '/js/services/agent-tasks.js';
24
+ import RateModal from './rate-modal.js';
21
25
 
22
26
  const html = htm.bind(h);
23
27
 
@@ -38,19 +42,40 @@ function fmtSeconds(secs) {
38
42
  return `${Number(secs || 0).toLocaleString()}${t('profile.agents.detail.quality.seconds')}`;
39
43
  }
40
44
 
41
- export default function TabQuality({ agentName }) {
45
+ export default function TabQuality({ agentName, showToast }) {
42
46
  const [data, setData] = useState(null);
43
47
  const [loading, setLoading] = useState(true);
48
+ const [doneTasks, setDoneTasks] = useState([]);
49
+ const [rateTarget, setRateTarget] = useState(null); // task being rated, or null
50
+ const [sendingRate, setSendingRate] = useState(false);
44
51
 
45
52
  async function loadData() {
46
53
  setLoading(true);
47
54
  try {
48
- const resp = await getAgentStatistics(agentName);
49
- setData(resp?.data || null);
55
+ const [statsResp, tasksResp] = await Promise.all([
56
+ getAgentStatistics(agentName),
57
+ listTasks(agentName, { status: 'done', per_page: 100 }).catch(() => null),
58
+ ]);
59
+ setData(statsResp?.data || null);
60
+ setDoneTasks(tasksResp?.data?.tasks || []);
50
61
  } catch { setData(null); }
51
62
  setLoading(false);
52
63
  }
53
64
 
65
+ async function handleSubmitRate(body) {
66
+ if (!rateTarget) return;
67
+ setSendingRate(true);
68
+ try {
69
+ await rateTask(agentName, rateTarget.id, body);
70
+ showToast?.(t('profile.agents.tasks.rate.success'));
71
+ setRateTarget(null);
72
+ await loadData();
73
+ } catch (err) {
74
+ showToast?.(err.message || t('profile.agents.tasks.rate.error'), true);
75
+ }
76
+ setSendingRate(false);
77
+ }
78
+
54
79
  useEffect(() => { loadData(); }, [agentName]);
55
80
 
56
81
  // Live updates: refresh when a rating lands or tasks change.
@@ -139,6 +164,27 @@ export default function TabQuality({ agentName }) {
139
164
  </div>
140
165
  `}
141
166
 
167
+ <!-- Rate deliverables (completed tasks the owner can rate) -->
168
+ <div class="pf-agd-section-title">${t('profile.agents.detail.quality.pendingTitle')}</div>
169
+ <div class="pf-agd-quality-desc">${t('profile.agents.detail.quality.pendingDesc')}</div>
170
+ ${doneTasks.length === 0
171
+ ? html`<div class="pf-agd-empty">${t('profile.agents.detail.quality.allRated')}</div>`
172
+ : html`
173
+ <div class="pf-agd-quality-pending">
174
+ ${[...doneTasks].sort((a, b) => (a.rating ? 1 : 0) - (b.rating ? 1 : 0)).map(task => html`
175
+ <div key=${task.id} class="pf-agd-quality-pending-row">
176
+ <span class="pf-agd-quality-pending-title">${task.title || task.id}</span>
177
+ ${task.rating
178
+ ? html`<span class="pf-agd-quality-pending-rated">${starGlyphs(task.rating.stars)} ${ctxLabel(task.rating.context)}</span>`
179
+ : html`<span class="pf-agd-quality-pending-unrated">${t('profile.agents.detail.quality.unrated')}</span>`}
180
+ <button class="btn-outline btn-sm" onClick=${() => setRateTarget(task)}>
181
+ ${task.rating ? t('profile.agents.tasks.rate.rerate') : t('profile.agents.tasks.rate.button')}
182
+ </button>
183
+ </div>
184
+ `)}
185
+ </div>
186
+ `}
187
+
142
188
  <!-- Custom metrics -->
143
189
  <div class="pf-agd-section-title">${t('profile.agents.detail.quality.customTitle')}</div>
144
190
  <div class="pf-agd-quality-desc">${t('profile.agents.detail.quality.customDesc')}</div>
@@ -154,6 +200,14 @@ export default function TabQuality({ agentName }) {
154
200
  `)}
155
201
  </div>
156
202
  `}
203
+
204
+ <${RateModal}
205
+ open=${!!rateTarget}
206
+ onClose=${() => setRateTarget(null)}
207
+ onSubmit=${handleSubmitRate}
208
+ submitting=${sendingRate}
209
+ existing=${rateTarget?.rating}
210
+ />
157
211
  </div>
158
212
  `;
159
213
  }
@@ -3,6 +3,8 @@
3
3
  * @description Tasks tab wrapper for agent detail tab-view.
4
4
  * Delegates to the existing AgentTasksSubtab component.
5
5
  * @version-history
6
+ * v1.1.0 -- 2026-06-01 -- Forward the `agent` object so the subtab can show the
7
+ * max_concurrent_tasks runner config.
6
8
  * v1.0.0 -- 2026-05-24 -- Initial creation for Agent Detail Tab-View
7
9
  */
8
10
 
@@ -12,6 +14,6 @@ import AgentTasksSubtab from '../agents-tasks-subtab.js';
12
14
 
13
15
  const html = htm.bind(h);
14
16
 
15
- export default function TabTasks({ agentName, session, showToast }) {
16
- return html`<${AgentTasksSubtab} agentName=${agentName} session=${session} showToast=${showToast} />`;
17
+ export default function TabTasks({ agent, agentName, session, showToast }) {
18
+ return html`<${AgentTasksSubtab} agent=${agent} agentName=${agentName} session=${session} showToast=${showToast} />`;
17
19
  }
@@ -9,6 +9,14 @@
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.6.0 -- 2026-06-01 -- Add "max concurrent tasks" runner config (number input,
13
+ * default 1) that PATCHes /v1/agents/:name/max-concurrent-tasks. Consumed by
14
+ * the agent's runner (e.g. a CrewAI daemon) via the integration kit.
15
+ * v4.5.0 -- 2026-05-31 -- Extract RateModal to shared ./agents/rate-modal.js
16
+ * so the Quality tab can reuse it; no behaviour change here.
17
+ * v4.4.0 -- 2026-05-31 -- Add owner "Rate deliverable" control on done tasks
18
+ * (RateModal: stars + context + source-grounded + comment) wired to
19
+ * POST /tasks/:id/rate; shows the current rating with a re-rate affordance.
12
20
  * v4.3.0 -- 2026-05-31 -- Add per-task "blur title" toggle in the collapsed
13
21
  * task row (eye icon). Hides a task title behind a CSS blur for screen
14
22
  * recordings; preference persists per task ID in localStorage (browser-only).
@@ -41,8 +49,10 @@ const html = htm.bind(h);
41
49
  import { t } from '/js/i18n.js';
42
50
  import { timeAgo } from '/js/utils.js';
43
51
  import { apiGet, apiPost } from '/js/api.js';
44
- import { listTasks, deleteTask, startTask, listEvents, requestChanges, createTask } from '/js/services/agent-tasks.js';
52
+ import { listTasks, deleteTask, startTask, listEvents, requestChanges, createTask, rateTask } from '/js/services/agent-tasks.js';
53
+ import { setMaxConcurrentTasks } from '/js/services/agents.js';
45
54
  import { useConfirm, Modal } from '/components/Modal.js';
55
+ import RateModal from './agents/rate-modal.js';
46
56
 
47
57
  const TASK_FILTERS = ['all', 'active', 'queued', 'completed', 'failed'];
48
58
 
@@ -209,6 +219,8 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
209
219
  const [starting, setStarting] = useState(false);
210
220
  const [showRevisionModal, setShowRevisionModal] = useState(false);
211
221
  const [sendingRevision, setSendingRevision] = useState(false);
222
+ const [showRateModal, setShowRateModal] = useState(false);
223
+ const [sendingRate, setSendingRate] = useState(false);
212
224
  const [showOutdated, setShowOutdated] = useState(false);
213
225
  // Deliverable preview: null = not requested, {loading}|{notFound}|{value}.
214
226
  const [deliverable, setDeliverable] = useState(null);
@@ -335,6 +347,24 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
335
347
  setShowRevisionModal(true);
336
348
  }
337
349
 
350
+ function handleOpenRate(e) {
351
+ e.stopPropagation();
352
+ setShowRateModal(true);
353
+ }
354
+
355
+ async function handleSubmitRate(body) {
356
+ setSendingRate(true);
357
+ try {
358
+ await rateTask(agentName, task.id, body);
359
+ showToast(t('profile.agents.tasks.rate.success'));
360
+ setShowRateModal(false);
361
+ onRefresh();
362
+ } catch (err) {
363
+ showToast(err.message || t('profile.agents.tasks.rate.error'), true);
364
+ }
365
+ setSendingRate(false);
366
+ }
367
+
338
368
  async function handleSubmitRevision(message) {
339
369
  setSendingRevision(true);
340
370
  try {
@@ -357,6 +387,8 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
357
387
  const isQueued = task.status === 'queued' || task.status === 'draft';
358
388
  const isRevisionRequested = task.status === 'revision_requested';
359
389
  const isActive = task.status === 'active';
390
+ const isDone = task.status === 'done';
391
+ const rating = task.rating;
360
392
  const canStart = isQueued && hasTodos;
361
393
  const canRequestChanges = task.status === 'queued' && hasTodos;
362
394
  const totalMinutes = todos.reduce((sum, td) => sum + (td.estimateMinutes || 0), 0);
@@ -409,6 +441,21 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
409
441
  `}
410
442
  `}
411
443
 
444
+ ${isDone && html`
445
+ <div class="pf-agd-rate-row">
446
+ ${rating
447
+ ? html`
448
+ <span class="pf-agd-rate-current">
449
+ <span class="pf-agd-rate-current-stars">${'★★★★★'.slice(0, rating.stars)}${'☆☆☆☆☆'.slice(0, 5 - rating.stars)}</span>
450
+ <span class="pf-agd-rate-current-ctx">${t(`profile.agents.detail.quality.contexts.${rating.context}`)}</span>
451
+ ${rating.comment && html`<span class="pf-agd-rate-current-comment">${rating.comment}</span>`}
452
+ </span>
453
+ <button class="btn-ghost btn-sm" onClick=${handleOpenRate}>${t('profile.agents.tasks.rate.rerate')}</button>
454
+ `
455
+ : html`<button class="btn-outline btn-sm" onClick=${handleOpenRate}>${t('profile.agents.tasks.rate.button')}</button>`}
456
+ </div>
457
+ `}
458
+
412
459
  ${task.scope && html`
413
460
  <div class="pf-agd-info-row">
414
461
  <span class="pf-agd-info-label">${t('profile.agents.detail.tasks.scope')}</span>
@@ -539,17 +586,42 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
539
586
  onSubmit=${handleSubmitRevision}
540
587
  submitting=${sendingRevision}
541
588
  />
589
+ <${RateModal}
590
+ open=${showRateModal}
591
+ onClose=${() => setShowRateModal(false)}
592
+ onSubmit=${handleSubmitRate}
593
+ submitting=${sendingRate}
594
+ existing=${rating}
595
+ />
542
596
  </div>
543
597
  `}
544
598
  </div>
545
599
  `;
546
600
  }
547
601
 
548
- export default function AgentTasksSubtab({ agentName, showToast }) {
602
+ export default function AgentTasksSubtab({ agent, agentName, showToast }) {
549
603
  const [tasks, setTasks] = useState(null);
550
604
  const [showCreate, setShowCreate] = useState(false);
551
605
  const [error, setError] = useState(null);
552
606
  const [filter, setFilter] = useState('all');
607
+ // Runner concurrency config (default 1 = serial). Saved to the agent via PATCH.
608
+ const [maxConcurrent, setMaxConcurrent] = useState(agent?.max_concurrent_tasks ?? 1);
609
+ const [savingConcurrency, setSavingConcurrency] = useState(false);
610
+
611
+ async function handleSaveConcurrency(next) {
612
+ const n = Math.max(1, Math.min(20, parseInt(next, 10) || 1));
613
+ setMaxConcurrent(n);
614
+ if (n === (agent?.max_concurrent_tasks ?? 1)) return;
615
+ setSavingConcurrency(true);
616
+ try {
617
+ await setMaxConcurrentTasks(agentName, n);
618
+ if (agent) agent.max_concurrent_tasks = n;
619
+ showToast?.(t('profile.agents.tasks.concurrency.saved'));
620
+ } catch (err) {
621
+ showToast?.(err.message || t('profile.agents.tasks.concurrency.error'), true);
622
+ }
623
+ setSavingConcurrency(false);
624
+ }
553
625
 
554
626
  async function loadTasks() {
555
627
  try {
@@ -605,6 +677,23 @@ export default function AgentTasksSubtab({ agentName, showToast }) {
605
677
  </button>
606
678
  </div>
607
679
 
680
+ <div class="pf-agd-concurrency">
681
+ <label class="pf-agd-concurrency-label" for="pf-agd-mct-${agentName}">
682
+ ${t('profile.agents.tasks.concurrency.label')}
683
+ </label>
684
+ <input
685
+ id="pf-agd-mct-${agentName}"
686
+ class="pf-agd-concurrency-input"
687
+ type="number"
688
+ min="1"
689
+ max="20"
690
+ value=${maxConcurrent}
691
+ disabled=${savingConcurrency}
692
+ onChange=${e => handleSaveConcurrency(e.target.value)}
693
+ />
694
+ <span class="pf-agd-concurrency-hint">${t('profile.agents.tasks.concurrency.hint')}</span>
695
+ </div>
696
+
608
697
  ${showCreate && html`
609
698
  <${TaskCreateForm} agentName=${agentName} showToast=${showToast} onCreated=${handleCreated} onCancel=${() => setShowCreate(false)} />
610
699
  `}