aimeat 1.7.0 → 1.8.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 (95) hide show
  1. package/dist/bin/aimeat.js +0 -0
  2. package/dist/locales/en.json +46 -3
  3. package/dist/locales/fi.json +46 -3
  4. package/dist/public/css/views/agents-detail.css +150 -0
  5. package/dist/public/css/views/profile.css +22 -2
  6. package/dist/public/js/services/generator-spec-validate.js +23 -0
  7. package/dist/public/js/services/memory.js +39 -0
  8. package/dist/public/views/admin/federation-tab.js +5 -5
  9. package/dist/public/views/admin/prompts-tab.js +9 -10
  10. package/dist/public/views/profile/access-tab.js +18 -0
  11. package/dist/public/views/profile/agents-messages-subtab.js +3 -2
  12. package/dist/public/views/profile/agents-tab.js +10 -6
  13. package/dist/public/views/profile/agents-tasks-subtab.js +123 -64
  14. package/dist/public/views/profile/generator-detail.js +8 -1
  15. package/dist/public/views/profile/memory-tab.js +371 -7
  16. package/dist/public/views/profile/wallet-tab.js +14 -0
  17. package/dist/src/models/agent-capabilities-schemas.d.ts +3 -0
  18. package/dist/src/models/agent-capabilities-schemas.d.ts.map +1 -1
  19. package/dist/src/models/agent-capabilities-schemas.js +3 -0
  20. package/dist/src/models/agent-capabilities-schemas.js.map +1 -1
  21. package/dist/src/routes/admin-monitoring.js +12 -7
  22. package/dist/src/routes/admin-monitoring.js.map +1 -1
  23. package/dist/src/routes/agent-capabilities.d.ts +1 -0
  24. package/dist/src/routes/agent-capabilities.d.ts.map +1 -1
  25. package/dist/src/routes/agent-capabilities.js +9 -0
  26. package/dist/src/routes/agent-capabilities.js.map +1 -1
  27. package/dist/src/routes/agent-tasks.d.ts +1 -0
  28. package/dist/src/routes/agent-tasks.d.ts.map +1 -1
  29. package/dist/src/routes/agent-tasks.js +9 -7
  30. package/dist/src/routes/agent-tasks.js.map +1 -1
  31. package/dist/src/routes/federation-auth.d.ts.map +1 -1
  32. package/dist/src/routes/federation-auth.js +3 -1
  33. package/dist/src/routes/federation-auth.js.map +1 -1
  34. package/dist/src/routes/federation-peer.d.ts.map +1 -1
  35. package/dist/src/routes/federation-peer.js +11 -2
  36. package/dist/src/routes/federation-peer.js.map +1 -1
  37. package/dist/src/routes/federation-sync.d.ts +1 -0
  38. package/dist/src/routes/federation-sync.d.ts.map +1 -1
  39. package/dist/src/routes/federation-sync.js +29 -0
  40. package/dist/src/routes/federation-sync.js.map +1 -1
  41. package/dist/src/routes/memory.d.ts +9 -0
  42. package/dist/src/routes/memory.d.ts.map +1 -1
  43. package/dist/src/routes/memory.js +304 -9
  44. package/dist/src/routes/memory.js.map +1 -1
  45. package/dist/src/routes/prompts.d.ts +1 -0
  46. package/dist/src/routes/prompts.d.ts.map +1 -1
  47. package/dist/src/routes/prompts.js +40 -1
  48. package/dist/src/routes/prompts.js.map +1 -1
  49. package/dist/src/routes/sharing-groups.d.ts.map +1 -1
  50. package/dist/src/routes/sharing-groups.js +8 -2
  51. package/dist/src/routes/sharing-groups.js.map +1 -1
  52. package/dist/src/routes/wallet.d.ts.map +1 -1
  53. package/dist/src/routes/wallet.js +2 -0
  54. package/dist/src/routes/wallet.js.map +1 -1
  55. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  56. package/dist/src/server-bootstrap/routes-loader.js +31 -0
  57. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  58. package/dist/src/services/federation-helpers.d.ts +1 -0
  59. package/dist/src/services/federation-helpers.d.ts.map +1 -1
  60. package/dist/src/services/federation-helpers.js +1 -1
  61. package/dist/src/services/federation-helpers.js.map +1 -1
  62. package/dist/src/services/generator-autopilot.d.ts.map +1 -1
  63. package/dist/src/services/generator-autopilot.js +125 -26
  64. package/dist/src/services/generator-autopilot.js.map +1 -1
  65. package/dist/src/services/generator-prompts/index.d.ts +1 -1
  66. package/dist/src/services/generator-prompts/index.d.ts.map +1 -1
  67. package/dist/src/services/generator-prompts/index.js +1 -1
  68. package/dist/src/services/generator-prompts/index.js.map +1 -1
  69. package/dist/src/services/generator-prompts/spec-validate.d.ts +1 -0
  70. package/dist/src/services/generator-prompts/spec-validate.d.ts.map +1 -1
  71. package/dist/src/services/generator-prompts/spec-validate.js +20 -0
  72. package/dist/src/services/generator-prompts/spec-validate.js.map +1 -1
  73. package/dist/src/services/generator-prompts/validate.d.ts +7 -1
  74. package/dist/src/services/generator-prompts/validate.d.ts.map +1 -1
  75. package/dist/src/services/generator-prompts/validate.js +70 -0
  76. package/dist/src/services/generator-prompts/validate.js.map +1 -1
  77. package/dist/src/services/prompt-defaults.d.ts +1 -0
  78. package/dist/src/services/prompt-defaults.d.ts.map +1 -1
  79. package/dist/src/services/prompt-defaults.js +830 -8
  80. package/dist/src/services/prompt-defaults.js.map +1 -1
  81. package/dist/src/services/prompt-seeder.js +1 -1
  82. package/dist/src/services/prompt-seeder.js.map +1 -1
  83. package/dist/src/storage/interface.d.ts +2 -0
  84. package/dist/src/storage/interface.d.ts.map +1 -1
  85. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  86. package/dist/src/storage/providers/mongodb/index.js +5 -2
  87. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  88. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  89. package/dist/src/storage/providers/sqlite/index.js +7 -2
  90. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  91. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  92. package/dist/src/storage/providers/sqlite/schema.js +2 -0
  93. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  94. package/package.json +77 -97
  95. package/prisma/schema.prisma +2 -0
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * @file agents-tasks-subtab.js
3
3
  * @description Task queue sub-tab for agent detail view.
4
- * Shows queued, active, and completed tasks with inline creation form.
5
- * Listens for live updates to refresh task data automatically.
4
+ * Shows queued, active, and completed tasks with todo plans,
5
+ * "Start this task" approval button, and progress tracking.
6
6
  * @structure
7
7
  * - AgentTasksSubtab (default export) -- main component
8
- * - TaskItem -- individual task row with expand/collapse
8
+ * - TaskItem -- task row with expand/collapse, todo list, start button
9
9
  * - TaskCreateForm -- inline task creation form
10
10
  * @version-history
11
+ * v2.0.0 -- 2026-05-22 -- Add todo rendering, Start button, progress tracking
11
12
  * v1.0.0 -- 2026-05-21 -- Initial creation for Agent Dashboard Phase 1
12
13
  */
13
14
  import { h } from 'preact';
@@ -16,54 +17,89 @@ import htm from 'htm';
16
17
  const html = htm.bind(h);
17
18
  import { t } from '/js/i18n.js';
18
19
  import { timeAgo } from '/js/utils.js';
19
- import { listTasks, createTask, deleteTask, listEvents } from '/js/services/agent-tasks.js';
20
+ import { listTasks, createTask, deleteTask, startTask, listEvents } from '/js/services/agent-tasks.js';
20
21
 
21
22
  function statusLabel(status) {
22
23
  const key = `profile.agents.tasks.status.${status}`;
23
24
  const val = t(key);
24
- // Fallback to capitalized status if translation key not found
25
25
  return val !== key ? val : status.charAt(0).toUpperCase() + status.slice(1);
26
26
  }
27
27
 
28
- function TaskItem({ task, agentName, showToast }) {
28
+ function todoStatusIcon(status) {
29
+ if (status === 'done') return '✅';
30
+ if (status === 'failed') return '❌';
31
+ if (status === 'skipped') return '⏭';
32
+ if (status === 'active') return '▶';
33
+ return '⬜';
34
+ }
35
+
36
+ function todoProgress(todos) {
37
+ if (!todos || todos.length === 0) return null;
38
+ const done = todos.filter(td => td.status === 'done').length;
39
+ return `${done}/${todos.length}`;
40
+ }
41
+
42
+ function TaskItem({ task, agentName, showToast, onRefresh }) {
29
43
  const [expanded, setExpanded] = useState(false);
30
44
  const [events, setEvents] = useState(null);
31
45
  const [loadingEvents, setLoadingEvents] = useState(false);
46
+ const [starting, setStarting] = useState(false);
32
47
 
33
48
  async function handleExpand(e) {
34
49
  e.stopPropagation();
35
- if (expanded) {
36
- setExpanded(false);
37
- return;
38
- }
50
+ if (expanded) { setExpanded(false); return; }
39
51
  setExpanded(true);
40
52
  if (!events) {
41
53
  setLoadingEvents(true);
42
54
  try {
43
55
  const resp = await listEvents(agentName, task.id);
44
56
  setEvents(resp?.data?.events || []);
45
- } catch {
46
- setEvents([]);
47
- }
57
+ } catch { setEvents([]); }
48
58
  setLoadingEvents(false);
49
59
  }
50
60
  }
51
61
 
62
+ async function handleStart(e) {
63
+ e.stopPropagation();
64
+ setStarting(true);
65
+ try {
66
+ await startTask(agentName, task.id);
67
+ showToast('Task started');
68
+ onRefresh();
69
+ } catch (err) {
70
+ showToast(err.message || 'Failed to start task', true);
71
+ }
72
+ setStarting(false);
73
+ }
74
+
52
75
  async function handleDelete(e) {
53
76
  e.stopPropagation();
54
77
  try {
55
78
  await deleteTask(agentName, task.id);
56
- const msg = t('profile.agents.tasks.deleted');
57
- showToast(msg !== 'profile.agents.tasks.deleted' ? msg : 'Task deleted');
79
+ showToast('Task deleted');
80
+ onRefresh();
58
81
  } catch (err) {
59
82
  showToast(err.message || 'Failed to delete task', true);
60
83
  }
61
84
  }
62
85
 
86
+ const todos = task.todos || [];
87
+ const hasTodos = todos.length > 0;
88
+ const progress = todoProgress(todos);
89
+ const isQueued = task.status === 'queued' || task.status === 'draft';
90
+ const isActive = task.status === 'active';
91
+ const canStart = isQueued && hasTodos;
92
+ const totalMinutes = todos.reduce((sum, td) => sum + (td.estimateMinutes || td.estimate_minutes || 0), 0);
93
+ const aimeatSteps = todos.filter(td => td.environment === 'aimeat').length;
94
+ const agentSteps = todos.filter(td => td.environment === 'agent').length;
95
+
63
96
  return html`
64
97
  <div>
65
98
  <div class="agd-task-item" onClick=${handleExpand}>
66
- <span class="agd-task-title">${task.title || task.id}</span>
99
+ <div style="flex:1;display:flex;align-items:center;gap:0.5rem">
100
+ <span class="agd-task-title">${task.title || task.id}</span>
101
+ ${progress && html`<span class="agd-todo-progress">${progress}</span>`}
102
+ </div>
67
103
  <div class="agd-task-meta">
68
104
  ${task.created_at && html`<span class="agd-task-time">${timeAgo(task.created_at)}</span>`}
69
105
  <span class="agd-status agd-status-${task.status || 'draft'}">${statusLabel(task.status || 'draft')}</span>
@@ -72,30 +108,72 @@ function TaskItem({ task, agentName, showToast }) {
72
108
  ${expanded && html`
73
109
  <div class="agd-task-expanded">
74
110
  ${task.description && html`<div class="agd-task-desc">${task.description}</div>`}
75
- ${task.scope && html`
76
- <div class="agd-task-desc">
77
- <strong>${t('profile.agents.tasks.scope') !== 'profile.agents.tasks.scope' ? t('profile.agents.tasks.scope') : 'Scope'}:</strong> ${Array.isArray(task.scope) ? task.scope.join(', ') : task.scope}
111
+
112
+ ${hasTodos && html`
113
+ <div class="agd-todo-section">
114
+ <div class="agd-todo-header">
115
+ <strong>TODO</strong>
116
+ <span class="agd-todo-summary">
117
+ ${aimeatSteps > 0 && html`<span class="agd-env-badge agd-env-aimeat">AIMEAT: ${aimeatSteps}</span>`}
118
+ ${agentSteps > 0 && html`<span class="agd-env-badge agd-env-agent">Agent: ${agentSteps}</span>`}
119
+ ${totalMinutes > 0 && html`<span class="agd-todo-time">~${totalMinutes} min</span>`}
120
+ </span>
121
+ </div>
122
+ <div class="agd-todo-list">
123
+ ${todos.map((td, i) => html`
124
+ <div class="agd-todo-item agd-todo-${td.status || 'pending'}" key=${td.id || i}>
125
+ <span class="agd-todo-icon">${todoStatusIcon(td.status || 'pending')}</span>
126
+ <div class="agd-todo-content">
127
+ <div class="agd-todo-title">
128
+ ${td.title}
129
+ <span class="agd-env-badge agd-env-${td.environment || 'agent'}">${(td.environment || 'agent').toUpperCase()}</span>
130
+ ${(td.estimateMinutes || td.estimate_minutes) && html`
131
+ <span class="agd-todo-est">${td.estimateMinutes || td.estimate_minutes} min</span>
132
+ `}
133
+ </div>
134
+ ${td.description && html`<div class="agd-todo-desc">${td.description}</div>`}
135
+ ${(td.environmentReason || td.environment_reason) && html`
136
+ <div class="agd-todo-reason">${td.environmentReason || td.environment_reason}</div>
137
+ `}
138
+ ${td.verification && html`<div class="agd-todo-verify">${td.verification}</div>`}
139
+ </div>
140
+ </div>
141
+ `)}
142
+ </div>
143
+ </div>
144
+ `}
145
+
146
+ ${!hasTodos && isQueued && html`
147
+ <div class="agd-todo-waiting">
148
+ Waiting for agent to propose a plan...
78
149
  </div>
79
150
  `}
151
+
80
152
  ${loadingEvents && html`<div class="agd-empty">${t('profile.loading')}</div>`}
81
153
  ${events && events.length > 0 && html`
82
154
  <div class="agd-event-log">
83
155
  ${events.map(ev => html`
84
156
  <div class="agd-event-item" key=${ev.id || ev.timestamp}>
85
157
  <span class="agd-event-time">${ev.timestamp ? timeAgo(ev.timestamp) : ''}</span>
86
- <span class="agd-event-type">${ev.type || ev.event || ''}</span>
87
- <span>${ev.message || ev.data || ''}</span>
158
+ <span class="agd-event-type">${ev.type || ''}</span>
159
+ <span>${ev.message || ''}</span>
88
160
  </div>
89
161
  `)}
90
162
  </div>
91
163
  `}
92
- ${events && events.length === 0 && html`
93
- <div class="agd-empty">${t('profile.agents.tasks.noEvents') !== 'profile.agents.tasks.noEvents' ? t('profile.agents.tasks.noEvents') : 'No events recorded'}</div>
164
+ ${events && events.length === 0 && !isQueued && html`
165
+ <div class="agd-empty">No events recorded</div>
94
166
  `}
167
+
95
168
  <div class="agd-task-actions">
96
- <button class="btn-danger btn-sm" onClick=${handleDelete}>
97
- ${t('profile.agents.tasks.delete') !== 'profile.agents.tasks.delete' ? t('profile.agents.tasks.delete') : 'Delete'}
98
- </button>
169
+ ${canStart && html`
170
+ <button class="btn-primary btn-sm" onClick=${handleStart} disabled=${starting}>
171
+ ${starting ? 'Starting...' : 'Start this task'}
172
+ </button>
173
+ `}
174
+ ${(isQueued || task.status === 'draft') && html`
175
+ <button class="btn-danger btn-sm" onClick=${handleDelete}>Delete</button>
176
+ `}
99
177
  </div>
100
178
  </div>
101
179
  `}
@@ -119,8 +197,7 @@ function TaskCreateForm({ agentName, showToast, onCreated, onCancel }) {
119
197
  description: description.trim() || undefined,
120
198
  status,
121
199
  });
122
- const msg = t('profile.agents.tasks.created');
123
- showToast(msg !== 'profile.agents.tasks.created' ? msg : 'Task created');
200
+ showToast('Task created');
124
201
  onCreated();
125
202
  } catch (err) {
126
203
  showToast(err.message || 'Failed to create task', true);
@@ -131,18 +208,17 @@ function TaskCreateForm({ agentName, showToast, onCreated, onCancel }) {
131
208
  return html`
132
209
  <form class="agd-create-form" onSubmit=${handleSubmit}>
133
210
  <div class="agd-form-field">
134
- <label>${t('profile.agents.tasks.titleLabel') !== 'profile.agents.tasks.titleLabel' ? t('profile.agents.tasks.titleLabel') : 'Title'}</label>
211
+ <label>Title</label>
135
212
  <input type="text" value=${title} onInput=${(e) => setTitle(e.target.value)}
136
- placeholder=${t('profile.agents.tasks.titlePlaceholder') !== 'profile.agents.tasks.titlePlaceholder' ? t('profile.agents.tasks.titlePlaceholder') : 'What should the agent do?'}
137
- required />
213
+ placeholder="What should the agent do?" required />
138
214
  </div>
139
215
  <div class="agd-form-field">
140
- <label>${t('profile.agents.tasks.descriptionLabel') !== 'profile.agents.tasks.descriptionLabel' ? t('profile.agents.tasks.descriptionLabel') : 'Description'}</label>
216
+ <label>Description</label>
141
217
  <textarea value=${description} onInput=${(e) => setDescription(e.target.value)}
142
- placeholder=${t('profile.agents.tasks.descriptionPlaceholder') !== 'profile.agents.tasks.descriptionPlaceholder' ? t('profile.agents.tasks.descriptionPlaceholder') : 'Detailed instructions (optional)'}></textarea>
218
+ placeholder="Detailed instructions (optional)"></textarea>
143
219
  </div>
144
220
  <div class="agd-form-field">
145
- <label>${t('profile.agents.tasks.initialStatus') !== 'profile.agents.tasks.initialStatus' ? t('profile.agents.tasks.initialStatus') : 'Initial status'}</label>
221
+ <label>Initial status</label>
146
222
  <select value=${status} onChange=${(e) => setStatus(e.target.value)}>
147
223
  <option value="draft">${statusLabel('draft')}</option>
148
224
  <option value="queued">${statusLabel('queued')}</option>
@@ -150,13 +226,9 @@ function TaskCreateForm({ agentName, showToast, onCreated, onCancel }) {
150
226
  </div>
151
227
  <div class="agd-form-actions">
152
228
  <button type="submit" class="btn-primary btn-sm" disabled=${saving || !title.trim()}>
153
- ${saving
154
- ? (t('profile.agents.tasks.creating') !== 'profile.agents.tasks.creating' ? t('profile.agents.tasks.creating') : 'Creating...')
155
- : (t('profile.agents.tasks.createBtn') !== 'profile.agents.tasks.createBtn' ? t('profile.agents.tasks.createBtn') : 'Create Task')}
156
- </button>
157
- <button type="button" class="btn-outline btn-sm" onClick=${onCancel}>
158
- ${t('profile.agents.scopeUi.cancel')}
229
+ ${saving ? 'Creating...' : 'Create Task'}
159
230
  </button>
231
+ <button type="button" class="btn-outline btn-sm" onClick=${onCancel}>Cancel</button>
160
232
  </div>
161
233
  </form>
162
234
  `;
@@ -178,11 +250,8 @@ export default function AgentTasksSubtab({ agentName, session, showToast }) {
178
250
  }
179
251
  }
180
252
 
181
- useEffect(() => {
182
- loadTasks();
183
- }, [agentName]);
253
+ useEffect(() => { loadTasks(); }, [agentName]);
184
254
 
185
- // Live update listener
186
255
  const loadRef = useRef(loadTasks);
187
256
  loadRef.current = loadTasks;
188
257
  useEffect(() => {
@@ -200,7 +269,6 @@ export default function AgentTasksSubtab({ agentName, session, showToast }) {
200
269
  return html`<div class="agd-empty">${t('profile.loading')}</div>`;
201
270
  }
202
271
 
203
- // Group tasks by status category
204
272
  const active = tasks.filter(t => t.status === 'active');
205
273
  const queued = tasks.filter(t => t.status === 'queued' || t.status === 'draft');
206
274
  const completed = tasks.filter(t => t.status === 'done' || t.status === 'failed' || t.status === 'stalled');
@@ -209,11 +277,10 @@ export default function AgentTasksSubtab({ agentName, session, showToast }) {
209
277
  <div>
210
278
  <div class="agd-section-header">
211
279
  <span class="agd-section-title">
212
- ${t('profile.agents.tasks.title') !== 'profile.agents.tasks.title' ? t('profile.agents.tasks.title') : 'Tasks'}
213
- ${tasks.length > 0 && html` (${tasks.length})`}
280
+ TASK QUEUE${tasks.length > 0 ? ` (${tasks.length})` : ''}
214
281
  </span>
215
- <button class="btn-outline btn-sm" onClick=${(e) => { e.stopPropagation(); setShowCreate(!showCreate); }}>
216
- ${showCreate ? '-' : '+'} ${t('profile.agents.tasks.newTask') !== 'profile.agents.tasks.newTask' ? t('profile.agents.tasks.newTask') : 'New Task'}
282
+ <button class="btn-outline btn-sm" onClick=${() => setShowCreate(!showCreate)}>
283
+ ${showCreate ? '-' : '+'} New Task
217
284
  </button>
218
285
  </div>
219
286
 
@@ -224,30 +291,22 @@ export default function AgentTasksSubtab({ agentName, session, showToast }) {
224
291
  ${error && html`<div class="agd-empty">${error}</div>`}
225
292
 
226
293
  ${active.length > 0 && html`
227
- <div class="agd-section-title" style="margin-top:0.5rem;margin-bottom:0.25rem">
228
- ${t('profile.agents.tasks.activeSection') !== 'profile.agents.tasks.activeSection' ? t('profile.agents.tasks.activeSection') : 'Active'}
229
- </div>
230
- ${active.map(task => html`<${TaskItem} key=${task.id} task=${task} agentName=${agentName} showToast=${showToast} />`)}
294
+ <div class="agd-section-title" style="margin-top:0.5rem;margin-bottom:0.25rem">ACTIVE</div>
295
+ ${active.map(task => html`<${TaskItem} key=${task.id} task=${task} agentName=${agentName} showToast=${showToast} onRefresh=${loadTasks} />`)}
231
296
  `}
232
297
 
233
298
  ${queued.length > 0 && html`
234
- <div class="agd-section-title" style="margin-top:0.5rem;margin-bottom:0.25rem">
235
- ${t('profile.agents.tasks.queuedSection') !== 'profile.agents.tasks.queuedSection' ? t('profile.agents.tasks.queuedSection') : 'Queued'}
236
- </div>
237
- ${queued.map(task => html`<${TaskItem} key=${task.id} task=${task} agentName=${agentName} showToast=${showToast} />`)}
299
+ <div class="agd-section-title" style="margin-top:0.5rem;margin-bottom:0.25rem">QUEUED</div>
300
+ ${queued.map(task => html`<${TaskItem} key=${task.id} task=${task} agentName=${agentName} showToast=${showToast} onRefresh=${loadTasks} />`)}
238
301
  `}
239
302
 
240
303
  ${completed.length > 0 && html`
241
- <div class="agd-section-title" style="margin-top:0.5rem;margin-bottom:0.25rem">
242
- ${t('profile.agents.tasks.completedSection') !== 'profile.agents.tasks.completedSection' ? t('profile.agents.tasks.completedSection') : 'Completed'}
243
- </div>
244
- ${completed.map(task => html`<${TaskItem} key=${task.id} task=${task} agentName=${agentName} showToast=${showToast} />`)}
304
+ <div class="agd-section-title" style="margin-top:0.5rem;margin-bottom:0.25rem">COMPLETED</div>
305
+ ${completed.map(task => html`<${TaskItem} key=${task.id} task=${task} agentName=${agentName} showToast=${showToast} onRefresh=${loadTasks} />`)}
245
306
  `}
246
307
 
247
308
  ${tasks.length === 0 && !showCreate && html`
248
- <div class="agd-empty">
249
- ${t('profile.agents.tasks.empty') !== 'profile.agents.tasks.empty' ? t('profile.agents.tasks.empty') : 'No tasks yet. Create one to get started.'}
250
- </div>
309
+ <div class="agd-empty">No tasks yet. Create one to get started.</div>
251
310
  `}
252
311
  </div>
253
312
  `;
@@ -32,7 +32,7 @@ import {
32
32
  // DEPRECATED: browser-side prompt builders no longer used. Prompts loaded from database via API.
33
33
  // import { buildComponentPrompt, buildFixPrompt, buildReflectionPrompt, buildTestPrompt } from '/js/services/generator-prompts.js';
34
34
  import { validateComponent } from '/js/services/generator-validate.js';
35
- import { validateExtensionSpec, validateDataApiSpec, validateComponentSpec, validateAppDomainSpec } from '/js/services/generator-spec-validate.js';
35
+ import { validateExtensionSpec, validateDataApiSpec, validateComponentSpec, validateAppDomainSpec, validateAppSpec } from '/js/services/generator-spec-validate.js';
36
36
  import { verifyContract } from '/js/services/generator-contract.js';
37
37
  import { smokeTest } from '/js/services/generator-smoke.js';
38
38
  import { createBundle } from '/js/services/generator-context-bundle.js';
@@ -672,6 +672,7 @@ export function ComponentDetail({ component, project, components, projectId, int
672
672
  // Validate spec structure
673
673
  let sv;
674
674
  if (component.type === 'extension') sv = validateExtensionSpec(parsed);
675
+ else if (component.type === 'app') sv = validateAppSpec(parsed);
675
676
  else if (component.subtype === 'data') sv = validateDataApiSpec(parsed);
676
677
  else if (component.subtype === 'component') sv = validateComponentSpec(parsed);
677
678
  else if (component.subtype === 'app-domain') sv = validateAppDomainSpec(parsed);
@@ -705,6 +706,12 @@ export function ComponentDetail({ component, project, components, projectId, int
705
706
  try {
706
707
  const parsed = JSON.parse(specResult);
707
708
  await saveSpec(projectId, component.id, parsed);
709
+ // Rebuild code prompt now that the spec is saved
710
+ const freshPrompt = await loadPromptFromBackend(projectId, component.id, 'code');
711
+ setGeneratedPrompt(freshPrompt);
712
+ if (component.prompt) {
713
+ await saveComponent(projectId, { ...component, prompt: freshPrompt, spec: parsed });
714
+ }
708
715
  showToast?.('Spec saved');
709
716
  onUpdate();
710
717
  } catch (e) {