aimeat 1.15.1 → 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 (52) hide show
  1. package/dist/locales/en.json +60 -1
  2. package/dist/locales/fi.json +60 -1
  3. package/dist/public/css/views/agents-detail.css +279 -0
  4. package/dist/public/js/services/agent-tasks.js +11 -0
  5. package/dist/public/js/services/agents.js +8 -0
  6. package/dist/public/views/profile/agents/agent-card.js +5 -1
  7. package/dist/public/views/profile/agents/rate-modal.js +87 -0
  8. package/dist/public/views/profile/agents/tab-quality.js +213 -0
  9. package/dist/public/views/profile/agents/tab-tasks.js +4 -2
  10. package/dist/public/views/profile/agents-tasks-subtab.js +134 -3
  11. package/dist/src/generated/api-types.d.ts +238 -3
  12. package/dist/src/generated/api-types.d.ts.map +1 -1
  13. package/dist/src/models/agent-task-schemas.d.ts +28 -0
  14. package/dist/src/models/agent-task-schemas.d.ts.map +1 -1
  15. package/dist/src/models/agent-task-schemas.js +21 -0
  16. package/dist/src/models/agent-task-schemas.js.map +1 -1
  17. package/dist/src/routes/agent-activity.d.ts +3 -0
  18. package/dist/src/routes/agent-activity.d.ts.map +1 -1
  19. package/dist/src/routes/agent-activity.js +44 -0
  20. package/dist/src/routes/agent-activity.js.map +1 -1
  21. package/dist/src/routes/agent-integration.d.ts.map +1 -1
  22. package/dist/src/routes/agent-integration.js +4 -0
  23. package/dist/src/routes/agent-integration.js.map +1 -1
  24. package/dist/src/routes/agent-tasks.d.ts +3 -0
  25. package/dist/src/routes/agent-tasks.d.ts.map +1 -1
  26. package/dist/src/routes/agent-tasks.js +91 -1
  27. package/dist/src/routes/agent-tasks.js.map +1 -1
  28. package/dist/src/routes/agents.d.ts.map +1 -1
  29. package/dist/src/routes/agents.js +35 -0
  30. package/dist/src/routes/agents.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 +43 -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 +5 -1
  41. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  42. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  43. package/dist/src/storage/providers/sqlite/index.js +6 -4
  44. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  45. package/dist/src/storage/providers/sqlite/repos/agent-task.d.ts.map +1 -1
  46. package/dist/src/storage/providers/sqlite/repos/agent-task.js +6 -4
  47. package/dist/src/storage/providers/sqlite/repos/agent-task.js.map +1 -1
  48. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  49. package/dist/src/storage/providers/sqlite/schema.js +9 -1
  50. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  51. package/package.json +1 -1
  52. package/prisma/schema.prisma +2 -0
@@ -0,0 +1,213 @@
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.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.
16
+ * v1.0.0 -- 2026-05-31 -- Initial Quality tab (per-context reviews + performance + custom)
17
+ */
18
+
19
+ import { h } from 'preact';
20
+ import { useState, useEffect, useRef } from 'preact/hooks';
21
+ import htm from 'htm';
22
+ import { t } from '/js/i18n.js';
23
+ import { getAgentStatistics, listTasks, rateTask } from '/js/services/agent-tasks.js';
24
+ import RateModal from './rate-modal.js';
25
+
26
+ const html = htm.bind(h);
27
+
28
+ /** Glyph row for an average star value (rounded to nearest whole star). */
29
+ function starGlyphs(value) {
30
+ const full = Math.max(0, Math.min(5, Math.round(value)));
31
+ return '★★★★★'.slice(0, full) + '☆☆☆☆☆'.slice(0, 5 - full);
32
+ }
33
+
34
+ /** Localised context label, falling back to the raw enum value. */
35
+ function ctxLabel(ctx) {
36
+ const key = `profile.agents.detail.quality.contexts.${ctx}`;
37
+ const label = t(key);
38
+ return label !== key ? label : ctx;
39
+ }
40
+
41
+ function fmtSeconds(secs) {
42
+ return `${Number(secs || 0).toLocaleString()}${t('profile.agents.detail.quality.seconds')}`;
43
+ }
44
+
45
+ export default function TabQuality({ agentName, showToast }) {
46
+ const [data, setData] = useState(null);
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);
51
+
52
+ async function loadData() {
53
+ setLoading(true);
54
+ try {
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 || []);
61
+ } catch { setData(null); }
62
+ setLoading(false);
63
+ }
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
+
79
+ useEffect(() => { loadData(); }, [agentName]);
80
+
81
+ // Live updates: refresh when a rating lands or tasks change.
82
+ const loadRef = useRef(loadData);
83
+ loadRef.current = loadData;
84
+ useEffect(() => {
85
+ const handler = () => loadRef.current();
86
+ window.addEventListener('aimeat-live-update', handler);
87
+ return () => window.removeEventListener('aimeat-live-update', handler);
88
+ }, []);
89
+
90
+ if (loading) {
91
+ return html`<div class="pf-agd-empty">${t('profile.loading')}</div>`;
92
+ }
93
+
94
+ const perf = data?.performance || {};
95
+ const reviews = data?.reviews || {};
96
+ const custom = data?.custom || [];
97
+ const byContext = reviews.byContext || {};
98
+ const contextKeys = Object.keys(byContext);
99
+ const durByContext = perf.duration?.byContext || {};
100
+ const durKeys = Object.keys(durByContext);
101
+
102
+ return html`
103
+ <div class="pf-agd-quality">
104
+ <!-- Performance -->
105
+ <div class="pf-agd-section-title">${t('profile.agents.detail.quality.performanceTitle')}</div>
106
+ <div class="pf-agd-quality-desc">${t('profile.agents.detail.quality.performanceDesc')}</div>
107
+ <div class="pf-agd-stats-grid">
108
+ <div class="pf-agd-stat-card">
109
+ <div class="pf-agd-stat-value">${perf.tasks?.total ?? 0}</div>
110
+ <div class="pf-agd-stat-label">${t('profile.agents.detail.quality.tasksTotal')}</div>
111
+ </div>
112
+ <div class="pf-agd-stat-card">
113
+ <div class="pf-agd-stat-value">${perf.tasks?.completed ?? 0}</div>
114
+ <div class="pf-agd-stat-label">${t('profile.agents.detail.quality.tasksCompleted')}</div>
115
+ </div>
116
+ <div class="pf-agd-stat-card">
117
+ <div class="pf-agd-stat-value">${perf.tasks?.successRate != null ? `${Math.round(perf.tasks.successRate * 100)}%` : '-'}</div>
118
+ <div class="pf-agd-stat-label">${t('profile.agents.detail.quality.successRate')}</div>
119
+ </div>
120
+ <div class="pf-agd-stat-card">
121
+ <div class="pf-agd-stat-value">${fmtSeconds(perf.duration?.avgCompletionSeconds)}</div>
122
+ <div class="pf-agd-stat-label">${t('profile.agents.detail.quality.avgTime')}</div>
123
+ </div>
124
+ <div class="pf-agd-stat-card">
125
+ <div class="pf-agd-stat-value">${perf.events?.total ?? 0}</div>
126
+ <div class="pf-agd-stat-label">${t('profile.agents.detail.quality.events')}</div>
127
+ </div>
128
+ </div>
129
+ ${durKeys.length > 0 && html`
130
+ <div class="pf-agd-quality-durations">
131
+ ${durKeys.map(ctx => html`
132
+ <span key=${ctx} class="pf-agd-quality-dur-chip">
133
+ ${ctxLabel(ctx)}: ${fmtSeconds(durByContext[ctx].avgSeconds)} (${durByContext[ctx].count})
134
+ </span>
135
+ `)}
136
+ </div>
137
+ `}
138
+
139
+ <!-- Reviews by context -->
140
+ <div class="pf-agd-section-title">${t('profile.agents.detail.quality.reviewsTitle')}</div>
141
+ <div class="pf-agd-quality-desc">${t('profile.agents.detail.quality.reviewsDesc')}</div>
142
+ ${contextKeys.length === 0
143
+ ? html`<div class="pf-agd-empty">${t('profile.agents.detail.quality.noReviews')}</div>`
144
+ : html`
145
+ <div class="pf-agd-quality-reviews">
146
+ ${contextKeys.map(ctx => {
147
+ const s = byContext[ctx];
148
+ return html`
149
+ <div key=${ctx} class="pf-agd-quality-review-row">
150
+ <div class="pf-agd-quality-review-head">
151
+ <span class="pf-agd-quality-ctx">${ctxLabel(ctx)}</span>
152
+ <span class="pf-agd-quality-stars">${starGlyphs(s.avgStars)}</span>
153
+ <span class="pf-agd-quality-avg">${Number(s.avgStars).toFixed(1)}</span>
154
+ <span class="pf-agd-quality-n">${t('profile.agents.detail.quality.ratings', { count: s.n })}</span>
155
+ ${s.lowConfidence && html`<span class="pf-agd-quality-lowconf">${t('profile.agents.detail.quality.lowConfidence')}</span>`}
156
+ </div>
157
+ <div class="pf-agd-quality-review-meta">${t('profile.agents.detail.quality.grounded', { count: s.sourceGroundedN })}</div>
158
+ </div>
159
+ `;
160
+ })}
161
+ <div class="pf-agd-quality-overall">
162
+ ${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 })})
163
+ </div>
164
+ </div>
165
+ `}
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
+
188
+ <!-- Custom metrics -->
189
+ <div class="pf-agd-section-title">${t('profile.agents.detail.quality.customTitle')}</div>
190
+ <div class="pf-agd-quality-desc">${t('profile.agents.detail.quality.customDesc')}</div>
191
+ ${custom.length === 0
192
+ ? html`<div class="pf-agd-empty">${t('profile.agents.detail.quality.noCustom')}</div>`
193
+ : html`
194
+ <div class="pf-agd-quality-custom">
195
+ ${custom.map(c => html`
196
+ <div key=${c.key} class="pf-agd-quality-custom-row">
197
+ <span class="pf-agd-quality-custom-key">${c.key}</span>
198
+ <span class="pf-agd-quality-custom-val">${typeof c.value === 'object' ? JSON.stringify(c.value) : String(c.value)}</span>
199
+ </div>
200
+ `)}
201
+ </div>
202
+ `}
203
+
204
+ <${RateModal}
205
+ open=${!!rateTarget}
206
+ onClose=${() => setRateTarget(null)}
207
+ onSubmit=${handleSubmitRate}
208
+ submitting=${sendingRate}
209
+ existing=${rateTarget?.rating}
210
+ />
211
+ </div>
212
+ `;
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,17 @@
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.
20
+ * v4.3.0 -- 2026-05-31 -- Add per-task "blur title" toggle in the collapsed
21
+ * task row (eye icon). Hides a task title behind a CSS blur for screen
22
+ * recordings; preference persists per task ID in localStorage (browser-only).
12
23
  * v3.2.0 -- 2026-05-24 -- Add interactive filter pills, scope/rules display, migrate to pf-agd- CSS prefix
13
24
  * v3.1.0 -- 2026-05-24 -- Fix: use correct locale key for empty state
14
25
  * v3.0.0 -- 2026-05-22 -- Replace TaskCreateForm with TaskCreationBuilder (design spec split-panel)
@@ -38,11 +49,36 @@ const html = htm.bind(h);
38
49
  import { t } from '/js/i18n.js';
39
50
  import { timeAgo } from '/js/utils.js';
40
51
  import { apiGet, apiPost } from '/js/api.js';
41
- 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';
42
54
  import { useConfirm, Modal } from '/components/Modal.js';
55
+ import RateModal from './agents/rate-modal.js';
43
56
 
44
57
  const TASK_FILTERS = ['all', 'active', 'queued', 'completed', 'failed'];
45
58
 
59
+ // Per-browser "blur the title" preference. Used when screen-recording the tab
60
+ // so sensitive task titles can be hidden without affecting other viewers or
61
+ // the server. Stored as an array of task IDs in localStorage; survives reloads
62
+ // but never leaves this browser.
63
+ const BLUR_STORAGE_KEY = 'aimeat.blurredTaskTitles';
64
+
65
+ function readBlurredSet() {
66
+ try {
67
+ const parsed = JSON.parse(localStorage.getItem(BLUR_STORAGE_KEY));
68
+ return new Set(Array.isArray(parsed) ? parsed : []);
69
+ } catch { return new Set(); }
70
+ }
71
+
72
+ function isTaskBlurred(taskId) {
73
+ return readBlurredSet().has(taskId);
74
+ }
75
+
76
+ function setTaskBlurred(taskId, blurred) {
77
+ const set = readBlurredSet();
78
+ if (blurred) set.add(taskId); else set.delete(taskId);
79
+ try { localStorage.setItem(BLUR_STORAGE_KEY, JSON.stringify([...set])); } catch { /* storage full/blocked -- preference just won't persist */ }
80
+ }
81
+
46
82
  // Character limits -- keep in sync with AgentTaskCreateSchema in
47
83
  // src/models/agent-task-schemas.ts (title max 256, description max 10000).
48
84
  // The agent crew reads task.description as its prompt, so that is the field
@@ -183,11 +219,23 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
183
219
  const [starting, setStarting] = useState(false);
184
220
  const [showRevisionModal, setShowRevisionModal] = useState(false);
185
221
  const [sendingRevision, setSendingRevision] = useState(false);
222
+ const [showRateModal, setShowRateModal] = useState(false);
223
+ const [sendingRate, setSendingRate] = useState(false);
186
224
  const [showOutdated, setShowOutdated] = useState(false);
187
225
  // Deliverable preview: null = not requested, {loading}|{notFound}|{value}.
188
226
  const [deliverable, setDeliverable] = useState(null);
227
+ // Local-only "hide the title" toggle (for screen recordings). Persisted per
228
+ // task ID in localStorage; see helpers at top of file.
229
+ const [blurred, setBlurred] = useState(() => isTaskBlurred(task.id));
189
230
  const { confirm, ConfirmUI } = useConfirm();
190
231
 
232
+ function handleToggleBlur(e) {
233
+ e.stopPropagation();
234
+ const next = !blurred;
235
+ setBlurred(next);
236
+ setTaskBlurred(task.id, next);
237
+ }
238
+
191
239
  // Fetch the task's published deliverable from the agent's memory namespace.
192
240
  // Uses the owner list endpoint (?agent=<gaii>&prefix=<key>) which returns the
193
241
  // value inline; if the exact key is gone, show that it no longer exists.
@@ -299,6 +347,24 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
299
347
  setShowRevisionModal(true);
300
348
  }
301
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
+
302
368
  async function handleSubmitRevision(message) {
303
369
  setSendingRevision(true);
304
370
  try {
@@ -321,6 +387,8 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
321
387
  const isQueued = task.status === 'queued' || task.status === 'draft';
322
388
  const isRevisionRequested = task.status === 'revision_requested';
323
389
  const isActive = task.status === 'active';
390
+ const isDone = task.status === 'done';
391
+ const rating = task.rating;
324
392
  const canStart = isQueued && hasTodos;
325
393
  const canRequestChanges = task.status === 'queued' && hasTodos;
326
394
  const totalMinutes = todos.reduce((sum, td) => sum + (td.estimateMinutes || 0), 0);
@@ -338,7 +406,13 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
338
406
  <div>
339
407
  <div class="pf-agd-task-item" onClick=${handleExpand}>
340
408
  <div class="pf-agd-task-title-row">
341
- <span class="pf-agd-task-title">${task.title || task.id}</span>
409
+ <button
410
+ class=${`pf-agd-blur-toggle ${blurred ? 'pf-agd-blur-toggle--on' : ''}`}
411
+ onClick=${handleToggleBlur}
412
+ title=${blurred ? t('profile.agents.tasks.unblurTitle') : t('profile.agents.tasks.blurTitle')}
413
+ aria-pressed=${blurred}
414
+ >${blurred ? '🙈' : '👁'}</button>
415
+ <span class=${`pf-agd-task-title ${blurred ? 'pf-agd-task-title--blurred' : ''}`}>${task.title || task.id}</span>
342
416
  ${progress && html`<span class="pf-agd-todo-progress">${progress}</span>`}
343
417
  </div>
344
418
  <div class="pf-agd-task-meta">
@@ -367,6 +441,21 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
367
441
  `}
368
442
  `}
369
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
+
370
459
  ${task.scope && html`
371
460
  <div class="pf-agd-info-row">
372
461
  <span class="pf-agd-info-label">${t('profile.agents.detail.tasks.scope')}</span>
@@ -497,17 +586,42 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
497
586
  onSubmit=${handleSubmitRevision}
498
587
  submitting=${sendingRevision}
499
588
  />
589
+ <${RateModal}
590
+ open=${showRateModal}
591
+ onClose=${() => setShowRateModal(false)}
592
+ onSubmit=${handleSubmitRate}
593
+ submitting=${sendingRate}
594
+ existing=${rating}
595
+ />
500
596
  </div>
501
597
  `}
502
598
  </div>
503
599
  `;
504
600
  }
505
601
 
506
- export default function AgentTasksSubtab({ agentName, showToast }) {
602
+ export default function AgentTasksSubtab({ agent, agentName, showToast }) {
507
603
  const [tasks, setTasks] = useState(null);
508
604
  const [showCreate, setShowCreate] = useState(false);
509
605
  const [error, setError] = useState(null);
510
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
+ }
511
625
 
512
626
  async function loadTasks() {
513
627
  try {
@@ -563,6 +677,23 @@ export default function AgentTasksSubtab({ agentName, showToast }) {
563
677
  </button>
564
678
  </div>
565
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
+
566
697
  ${showCreate && html`
567
698
  <${TaskCreateForm} agentName=${agentName} showToast=${showToast} onCreated=${handleCreated} onCancel=${() => setShowCreate(false)} />
568
699
  `}