agent-tasks 1.7.1 → 1.9.1
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.
- package/README.md +17 -15
- package/dist/domain/agent-bridge.d.ts.map +1 -1
- package/dist/domain/agent-bridge.js +22 -2
- package/dist/domain/agent-bridge.js.map +1 -1
- package/dist/domain/approvals.d.ts.map +1 -1
- package/dist/domain/approvals.js +4 -1
- package/dist/domain/approvals.js.map +1 -1
- package/dist/domain/cleanup.d.ts +1 -0
- package/dist/domain/cleanup.d.ts.map +1 -1
- package/dist/domain/cleanup.js +36 -4
- package/dist/domain/cleanup.js.map +1 -1
- package/dist/domain/comments.d.ts +1 -0
- package/dist/domain/comments.d.ts.map +1 -1
- package/dist/domain/comments.js +10 -0
- package/dist/domain/comments.js.map +1 -1
- package/dist/domain/rules.js +11 -10
- package/dist/domain/rules.js.map +1 -1
- package/dist/domain/task-validator.d.ts +9 -0
- package/dist/domain/task-validator.d.ts.map +1 -0
- package/dist/domain/task-validator.js +70 -0
- package/dist/domain/task-validator.js.map +1 -0
- package/dist/domain/tasks.d.ts +19 -9
- package/dist/domain/tasks.d.ts.map +1 -1
- package/dist/domain/tasks.js +242 -111
- package/dist/domain/tasks.js.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/storage/database.d.ts.map +1 -1
- package/dist/storage/database.js +11 -3
- package/dist/storage/database.js.map +1 -1
- package/dist/transport/mcp-handlers.d.ts +31 -0
- package/dist/transport/mcp-handlers.d.ts.map +1 -0
- package/dist/transport/mcp-handlers.js +426 -0
- package/dist/transport/mcp-handlers.js.map +1 -0
- package/dist/transport/mcp.d.ts.map +1 -1
- package/dist/transport/mcp.js +207 -656
- package/dist/transport/mcp.js.map +1 -1
- package/dist/transport/rest.d.ts.map +1 -1
- package/dist/transport/rest.js +23 -7
- package/dist/transport/rest.js.map +1 -1
- package/dist/transport/ws.d.ts.map +1 -1
- package/dist/transport/ws.js +6 -4
- package/dist/transport/ws.js.map +1 -1
- package/dist/ui/app.js +186 -1608
- package/dist/ui/board.js +401 -0
- package/dist/ui/drag.js +143 -0
- package/dist/ui/index.html +5 -0
- package/dist/ui/inline-edit.js +242 -0
- package/dist/ui/panel.js +574 -0
- package/dist/ui/styles.css +109 -0
- package/dist/ui/ui-utils.js +323 -0
- package/package.json +1 -1
- package/dist/db.d.ts +0 -10
- package/dist/db.d.ts.map +0 -1
- package/dist/db.js +0 -112
- package/dist/db.js.map +0 -1
- package/dist/event-bus.d.ts +0 -10
- package/dist/event-bus.d.ts.map +0 -1
- package/dist/event-bus.js +0 -38
- package/dist/event-bus.js.map +0 -1
- package/dist/session.d.ts +0 -7
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js +0 -11
- package/dist/session.js.map +0 -1
- package/dist/tasks.d.ts +0 -32
- package/dist/tasks.d.ts.map +0 -1
- package/dist/tasks.js +0 -410
- package/dist/tasks.js.map +0 -1
package/dist/ui/panel.js
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// agent-tasks — Panel Module
|
|
3
|
+
//
|
|
4
|
+
// Side panel detail view, artifact/decision/learning rendering, comments,
|
|
5
|
+
// artifact fullscreen, panel resize.
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
window.TaskBoard = window.TaskBoard || {};
|
|
9
|
+
|
|
10
|
+
// ---- Expandable Artifact Rendering ----
|
|
11
|
+
|
|
12
|
+
function renderArtifactContent(content, name) {
|
|
13
|
+
var isDiff = TaskBoard.isDiff;
|
|
14
|
+
var renderDiff = TaskBoard.renderDiff;
|
|
15
|
+
var detectLanguage = TaskBoard.detectLanguage;
|
|
16
|
+
var highlightSyntax = TaskBoard.highlightSyntax;
|
|
17
|
+
|
|
18
|
+
if (isDiff(content)) return renderDiff(content);
|
|
19
|
+
const lang = detectLanguage(name, content);
|
|
20
|
+
const highlighted = highlightSyntax(content, lang);
|
|
21
|
+
const aLines = content.split('\n');
|
|
22
|
+
const lineNums = aLines.map((_, i) => i + 1).join('\n');
|
|
23
|
+
return (
|
|
24
|
+
'<div class="artifact-lines"><div class="artifact-line-numbers">' +
|
|
25
|
+
lineNums +
|
|
26
|
+
'</div><div class="artifact-line-content"><pre class="artifact-code">' +
|
|
27
|
+
highlighted +
|
|
28
|
+
'</pre></div></div>'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renderArtifactBlock(artifact) {
|
|
33
|
+
var esc = TaskBoard.esc;
|
|
34
|
+
const vLabel = artifact.version > 1 ? ' v' + artifact.version : '';
|
|
35
|
+
const aLines = (artifact.content || '').split('\n');
|
|
36
|
+
const needsCollapse = aLines.length > 8;
|
|
37
|
+
const wrapperClass = needsCollapse
|
|
38
|
+
? 'artifact-wrapper artifact-collapsed'
|
|
39
|
+
: 'artifact-wrapper artifact-expanded';
|
|
40
|
+
const artId = 'artifact-' + artifact.id;
|
|
41
|
+
let html = '<div class="panel-artifact"><div class="artifact-header">';
|
|
42
|
+
html +=
|
|
43
|
+
'<h4><span class="material-symbols-outlined" style="font-size:14px">description</span> ' +
|
|
44
|
+
esc(artifact.name) +
|
|
45
|
+
vLabel +
|
|
46
|
+
' <span style="color:var(--text-dim);font-weight:400">(' +
|
|
47
|
+
esc(artifact.stage) +
|
|
48
|
+
', ' +
|
|
49
|
+
esc(artifact.created_by) +
|
|
50
|
+
')</span></h4>';
|
|
51
|
+
html +=
|
|
52
|
+
'<button class="artifact-fullscreen-btn" data-artifact-id="' +
|
|
53
|
+
artId +
|
|
54
|
+
'" title="Open fullscreen"><span class="material-symbols-outlined">open_in_full</span></button>' +
|
|
55
|
+
'<button class="artifact-copy-btn" data-artifact-id="' +
|
|
56
|
+
artId +
|
|
57
|
+
'" title="Copy to clipboard"><span class="material-symbols-outlined">content_copy</span> Copy</button>';
|
|
58
|
+
html += '</div><div class="' + wrapperClass + '" id="' + artId + '">';
|
|
59
|
+
html += renderArtifactContent(artifact.content || '', artifact.name || '');
|
|
60
|
+
html += '<div class="artifact-fade"></div></div>';
|
|
61
|
+
if (needsCollapse) {
|
|
62
|
+
html +=
|
|
63
|
+
'<button class="artifact-toggle" data-artifact-id="' +
|
|
64
|
+
artId +
|
|
65
|
+
'"><span class="material-symbols-outlined">expand_more</span> Show more (' +
|
|
66
|
+
aLines.length +
|
|
67
|
+
' lines)</button>';
|
|
68
|
+
}
|
|
69
|
+
html += '</div>';
|
|
70
|
+
return html;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- Decision rendering ----
|
|
74
|
+
|
|
75
|
+
function renderDecisionBlock(artifact) {
|
|
76
|
+
var esc = TaskBoard.esc;
|
|
77
|
+
const content = artifact.content || '';
|
|
78
|
+
const choseMatch = content.match(/\*\*Chose:\*\*\s*(.+)/);
|
|
79
|
+
const overMatch = content.match(/\*\*Over:\*\*\s*(.+)/);
|
|
80
|
+
const becauseMatch = content.match(/\*\*Because:\*\*\s*(.+)/);
|
|
81
|
+
const chose = choseMatch ? choseMatch[1].trim() : '';
|
|
82
|
+
const over = overMatch ? overMatch[1].trim() : '';
|
|
83
|
+
const because = becauseMatch ? becauseMatch[1].trim() : '';
|
|
84
|
+
if (!chose) return renderArtifactBlock(artifact);
|
|
85
|
+
const vLabel = artifact.version > 1 ? ' v' + artifact.version : '';
|
|
86
|
+
return `<div class="panel-decision"><div class="decision-header"><span class="material-symbols-outlined">gavel</span> Decision${vLabel} <span style="color:var(--text-dim);font-weight:400">(${esc(artifact.stage)}, ${esc(artifact.created_by)})</span></div><div class="decision-body"><div class="decision-row"><span class="decision-label">Chose</span><span class="decision-value decision-chose">${esc(chose)}</span></div><div class="decision-row"><span class="decision-label">Over</span><span class="decision-value decision-over">${esc(over)}</span></div><div class="decision-row"><span class="decision-label">Because</span><span class="decision-value decision-because">${esc(because)}</span></div></div></div>`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---- Learning rendering ----
|
|
90
|
+
|
|
91
|
+
function renderLearningBlock(artifact) {
|
|
92
|
+
var esc = TaskBoard.esc;
|
|
93
|
+
var renderMarkdown = TaskBoard.renderMarkdown;
|
|
94
|
+
const content = artifact.content || '';
|
|
95
|
+
const categoryMatch = content.match(/^\[(technique|pitfall|decision|pattern)\]\s*/);
|
|
96
|
+
const category = categoryMatch ? categoryMatch[1] : 'technique';
|
|
97
|
+
const body = categoryMatch ? content.slice(categoryMatch[0].length) : content;
|
|
98
|
+
const sourceMatch = body.match(/^Learning from (subtask|sibling) #(\d+):\s*/);
|
|
99
|
+
const sourceLabel = sourceMatch ? `from ${sourceMatch[1]} #${sourceMatch[2]}` : '';
|
|
100
|
+
const displayBody = sourceMatch ? body.slice(sourceMatch[0].length) : body;
|
|
101
|
+
const categoryIcons = {
|
|
102
|
+
technique: 'construction',
|
|
103
|
+
pitfall: 'warning',
|
|
104
|
+
decision: 'gavel',
|
|
105
|
+
pattern: 'pattern',
|
|
106
|
+
};
|
|
107
|
+
const categoryIcon = categoryIcons[category] || 'lightbulb';
|
|
108
|
+
const vLabel = artifact.version > 1 ? ' v' + artifact.version : '';
|
|
109
|
+
return `<div class="panel-learning"><div class="learning-header"><span class="material-symbols-outlined learning-icon">${categoryIcon}</span><span class="learning-category">${esc(category)}</span>${vLabel}${sourceLabel ? `<span class="learning-source">${esc(sourceLabel)}</span>` : ''}<span style="color:var(--text-dim);font-weight:400;margin-left:auto;font-size:11px">${esc(artifact.created_by)}</span></div><div class="learning-body">${renderMarkdown(displayBody)}</div></div>`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---- Panel open/close ----
|
|
113
|
+
|
|
114
|
+
function openPanel(id) {
|
|
115
|
+
var state = TaskBoard.state;
|
|
116
|
+
const task = state.tasks.find((t) => t.id === id);
|
|
117
|
+
if (!task) return;
|
|
118
|
+
|
|
119
|
+
state.panelTaskId = id;
|
|
120
|
+
const wrapper = document.getElementById('board-wrapper');
|
|
121
|
+
wrapper.classList.add('panel-open');
|
|
122
|
+
|
|
123
|
+
const panel = document.getElementById('side-panel');
|
|
124
|
+
const hasArtifacts = (state.artifactCounts[id] || 0) > 0;
|
|
125
|
+
if (hasArtifacts) {
|
|
126
|
+
panel.classList.add('panel-wide');
|
|
127
|
+
} else {
|
|
128
|
+
panel.classList.remove('panel-wide');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
renderPanelContent(task);
|
|
132
|
+
highlightActiveCard(id);
|
|
133
|
+
|
|
134
|
+
showPanelBackdrop();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function closePanel() {
|
|
138
|
+
TaskBoard.state.panelTaskId = null;
|
|
139
|
+
const wrapper = document.getElementById('board-wrapper');
|
|
140
|
+
wrapper.classList.remove('panel-open');
|
|
141
|
+
hidePanelBackdrop();
|
|
142
|
+
highlightActiveCard(null);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function showPanelBackdrop() {
|
|
146
|
+
let backdrop = document.getElementById('panel-backdrop');
|
|
147
|
+
if (!backdrop) {
|
|
148
|
+
backdrop = document.createElement('div');
|
|
149
|
+
backdrop.id = 'panel-backdrop';
|
|
150
|
+
backdrop.className = 'panel-backdrop';
|
|
151
|
+
backdrop.addEventListener('click', closePanel);
|
|
152
|
+
document.body.appendChild(backdrop);
|
|
153
|
+
}
|
|
154
|
+
backdrop.style.display = '';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function hidePanelBackdrop() {
|
|
158
|
+
const backdrop = document.getElementById('panel-backdrop');
|
|
159
|
+
if (backdrop) backdrop.style.display = 'none';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function highlightActiveCard(id) {
|
|
163
|
+
document
|
|
164
|
+
.querySelectorAll('.task-card.active-card')
|
|
165
|
+
.forEach((c) => c.classList.remove('active-card'));
|
|
166
|
+
if (id) {
|
|
167
|
+
const card = document.querySelector(`.task-card[data-task-id="${id}"]`);
|
|
168
|
+
if (card) card.classList.add('active-card');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---- Panel content rendering ----
|
|
173
|
+
|
|
174
|
+
function renderPanelContent(task) {
|
|
175
|
+
var state = TaskBoard.state;
|
|
176
|
+
var esc = TaskBoard.esc;
|
|
177
|
+
var formatDate = TaskBoard.formatDate;
|
|
178
|
+
var relativeTime = TaskBoard.relativeTime;
|
|
179
|
+
var renderMarkdown = TaskBoard.renderMarkdown;
|
|
180
|
+
var renderAvatar = TaskBoard.renderAvatar;
|
|
181
|
+
|
|
182
|
+
const panelBody = document.getElementById('panel-body');
|
|
183
|
+
const panelHeader = document.getElementById('panel-header-content');
|
|
184
|
+
|
|
185
|
+
const stageClass = `stage-${task.stage}`;
|
|
186
|
+
|
|
187
|
+
panelHeader.innerHTML = `
|
|
188
|
+
<div class="panel-header-left">
|
|
189
|
+
<span class="panel-task-id">#${task.id}</span>
|
|
190
|
+
<span class="panel-stage-badge ${stageClass}">${esc(task.stage)}</span>
|
|
191
|
+
</div>
|
|
192
|
+
<button class="panel-close-btn" data-action="close-panel" aria-label="Close panel">
|
|
193
|
+
<span class="material-symbols-outlined">close</span>
|
|
194
|
+
</button>`;
|
|
195
|
+
|
|
196
|
+
const deps = state.dependencies.filter((d) => d.task_id === task.id);
|
|
197
|
+
const blocking = state.dependencies.filter((d) => d.depends_on === task.id);
|
|
198
|
+
|
|
199
|
+
let html = `<div class="panel-title">${esc(task.title)}</div>`;
|
|
200
|
+
|
|
201
|
+
html += '<div class="panel-section">';
|
|
202
|
+
html +=
|
|
203
|
+
'<div class="panel-section-title"><span class="material-symbols-outlined">info</span> Details</div>';
|
|
204
|
+
html += '<div class="panel-grid">';
|
|
205
|
+
|
|
206
|
+
const gridRows = [
|
|
207
|
+
['Status', task.status],
|
|
208
|
+
['Priority', `P${task.priority}`],
|
|
209
|
+
['Created by', task.created_by || '\u2014'],
|
|
210
|
+
['Assigned to', task.assigned_to || '\u2014'],
|
|
211
|
+
['Project', task.project || '\u2014'],
|
|
212
|
+
['Created', formatDate(task.created_at)],
|
|
213
|
+
['Updated', relativeTime(task.updated_at) || formatDate(task.updated_at)],
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
if (task.parent_id) {
|
|
217
|
+
const parent = state.tasks.find((t) => t.id === task.parent_id);
|
|
218
|
+
gridRows.push(['Parent', parent ? `#${parent.id} ${parent.title}` : `#${task.parent_id}`]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (task.tags) {
|
|
222
|
+
try {
|
|
223
|
+
const parsed = JSON.parse(task.tags);
|
|
224
|
+
if (Array.isArray(parsed) && parsed.length) {
|
|
225
|
+
gridRows.push(['Tags', parsed.join(', ')]);
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
/* ignore */
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const [label, value] of gridRows) {
|
|
233
|
+
html += `<span class="panel-label">${esc(label)}</span><span class="panel-value">${esc(String(value))}</span>`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
html += '</div></div>';
|
|
237
|
+
|
|
238
|
+
if (task.description) {
|
|
239
|
+
html += '<div class="panel-section">';
|
|
240
|
+
html +=
|
|
241
|
+
'<div class="panel-section-title"><span class="material-symbols-outlined">notes</span> Description</div>';
|
|
242
|
+
html += `<div class="panel-description">${renderMarkdown(task.description)}</div>`;
|
|
243
|
+
html += '</div>';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (task.result) {
|
|
247
|
+
html += '<div class="panel-section">';
|
|
248
|
+
html +=
|
|
249
|
+
'<div class="panel-section-title"><span class="material-symbols-outlined">output</span> Result</div>';
|
|
250
|
+
html += `<div class="panel-description">${renderMarkdown(task.result)}</div>`;
|
|
251
|
+
html += '</div>';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (deps.length) {
|
|
255
|
+
html += '<div class="panel-section">';
|
|
256
|
+
html +=
|
|
257
|
+
'<div class="panel-section-title"><span class="material-symbols-outlined">link</span> Dependencies</div>';
|
|
258
|
+
for (const d of deps) {
|
|
259
|
+
const t = state.tasks.find((x) => x.id === d.depends_on);
|
|
260
|
+
const name = t ? `${t.title}` : `Task`;
|
|
261
|
+
html += `<div class="panel-subtask" data-subtask-id="${d.depends_on}">
|
|
262
|
+
<span class="subtask-id">#${d.depends_on}</span>
|
|
263
|
+
<span>${esc(name)}</span>
|
|
264
|
+
${t ? `<span class="subtask-stage stage-${t.stage}">${esc(t.stage)}</span>` : ''}
|
|
265
|
+
</div>`;
|
|
266
|
+
}
|
|
267
|
+
html += '</div>';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (blocking.length) {
|
|
271
|
+
html += '<div class="panel-section">';
|
|
272
|
+
html +=
|
|
273
|
+
'<div class="panel-section-title"><span class="material-symbols-outlined">block</span> Blocks</div>';
|
|
274
|
+
for (const d of blocking) {
|
|
275
|
+
const t = state.tasks.find((x) => x.id === d.task_id);
|
|
276
|
+
const name = t ? `${t.title}` : `Task`;
|
|
277
|
+
html += `<div class="panel-subtask" data-subtask-id="${d.task_id}">
|
|
278
|
+
<span class="subtask-id">#${d.task_id}</span>
|
|
279
|
+
<span>${esc(name)}</span>
|
|
280
|
+
${t ? `<span class="subtask-stage stage-${t.stage}">${esc(t.stage)}</span>` : ''}
|
|
281
|
+
</div>`;
|
|
282
|
+
}
|
|
283
|
+
html += '</div>';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const skeletonHTML =
|
|
287
|
+
'<div class="panel-loading">' +
|
|
288
|
+
'<div class="skeleton-line skeleton-wide"></div>' +
|
|
289
|
+
'<div class="skeleton-line"></div>' +
|
|
290
|
+
'<div class="skeleton-line"></div>' +
|
|
291
|
+
'<div class="skeleton-line skeleton-short"></div>' +
|
|
292
|
+
'</div>';
|
|
293
|
+
|
|
294
|
+
panelBody.innerHTML = html + skeletonHTML;
|
|
295
|
+
|
|
296
|
+
Promise.all([
|
|
297
|
+
fetch(`/api/tasks/${task.id}/artifacts`)
|
|
298
|
+
.then((r) => r.json())
|
|
299
|
+
.catch(() => []),
|
|
300
|
+
fetch(`/api/tasks/${task.id}/comments`)
|
|
301
|
+
.then((r) => r.json())
|
|
302
|
+
.catch(() => []),
|
|
303
|
+
fetch(`/api/tasks/${task.id}/subtasks`)
|
|
304
|
+
.then((r) => r.json())
|
|
305
|
+
.catch(() => []),
|
|
306
|
+
]).then(([artifacts, comments, subtasks]) => {
|
|
307
|
+
let extra = '';
|
|
308
|
+
|
|
309
|
+
if (subtasks.length) {
|
|
310
|
+
extra += '<div class="panel-section">';
|
|
311
|
+
extra += `<div class="panel-section-title"><span class="material-symbols-outlined">account_tree</span> Subtasks (${subtasks.length})</div>`;
|
|
312
|
+
for (const s of subtasks) {
|
|
313
|
+
extra += `<div class="panel-subtask" data-subtask-id="${s.id}">
|
|
314
|
+
<span class="subtask-id">#${s.id}</span>
|
|
315
|
+
<span>${esc(s.title)}</span>
|
|
316
|
+
<span class="subtask-stage stage-${s.stage}">${esc(s.stage)}</span>
|
|
317
|
+
</div>`;
|
|
318
|
+
}
|
|
319
|
+
extra += '</div>';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const decisions = artifacts.filter((a) => a.name === 'decision');
|
|
323
|
+
const learnings = artifacts.filter((a) => a.name === 'learning');
|
|
324
|
+
const otherArtifacts = artifacts.filter((a) => a.name !== 'decision' && a.name !== 'learning');
|
|
325
|
+
|
|
326
|
+
if (learnings.length) {
|
|
327
|
+
extra += '<div class="panel-section">';
|
|
328
|
+
extra += `<div class="panel-section-title"><span class="material-symbols-outlined">lightbulb</span> Learnings (${learnings.length})</div>`;
|
|
329
|
+
for (const l of learnings) {
|
|
330
|
+
extra += renderLearningBlock(l);
|
|
331
|
+
}
|
|
332
|
+
extra += '</div>';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (decisions.length) {
|
|
336
|
+
extra += '<div class="panel-section">';
|
|
337
|
+
extra += `<div class="panel-section-title"><span class="material-symbols-outlined">gavel</span> Decisions (${decisions.length})</div>`;
|
|
338
|
+
for (const d of decisions) {
|
|
339
|
+
extra += renderDecisionBlock(d);
|
|
340
|
+
}
|
|
341
|
+
extra += '</div>';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (otherArtifacts.length) {
|
|
345
|
+
extra += '<div class="panel-section">';
|
|
346
|
+
extra += `<div class="panel-section-title"><span class="material-symbols-outlined">inventory_2</span> Artifacts (${otherArtifacts.length})</div>`;
|
|
347
|
+
for (const a of otherArtifacts) {
|
|
348
|
+
extra += renderArtifactBlock(a);
|
|
349
|
+
}
|
|
350
|
+
extra += '</div>';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
extra += '<div class="panel-section panel-comments">';
|
|
354
|
+
extra += `<div class="panel-section-title"><span class="material-symbols-outlined">chat</span> Comments (${comments.length})</div>`;
|
|
355
|
+
for (const c of comments) {
|
|
356
|
+
const isReply = c.parent_comment_id ? ' reply' : '';
|
|
357
|
+
extra += `<div class="comment-item${isReply}">
|
|
358
|
+
<div class="comment-header">
|
|
359
|
+
${renderAvatar(c.agent_id, 'avatar-sm')}
|
|
360
|
+
<span class="comment-agent">${esc(c.agent_id)}</span>
|
|
361
|
+
<span class="comment-time">${relativeTime(c.created_at) || formatDate(c.created_at)}</span>
|
|
362
|
+
</div>
|
|
363
|
+
<div class="comment-body">${renderMarkdown(c.content)}</div>
|
|
364
|
+
</div>`;
|
|
365
|
+
}
|
|
366
|
+
extra += `<div class="comment-form">
|
|
367
|
+
<textarea id="comment-input" placeholder="Add a comment..." rows="1" aria-label="Add a comment"></textarea>
|
|
368
|
+
<button id="comment-send-btn" data-task-id="${task.id}" aria-label="Send comment">Send</button>
|
|
369
|
+
</div></div>`;
|
|
370
|
+
|
|
371
|
+
panelBody.innerHTML = html + extra;
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ---- Comment submission ----
|
|
376
|
+
|
|
377
|
+
function submitComment(taskId) {
|
|
378
|
+
const input = document.getElementById('comment-input');
|
|
379
|
+
const content = input?.value?.trim();
|
|
380
|
+
if (!content) return;
|
|
381
|
+
|
|
382
|
+
fetch(`/api/tasks/${taskId}/comments`, {
|
|
383
|
+
method: 'POST',
|
|
384
|
+
headers: { 'Content-Type': 'application/json' },
|
|
385
|
+
body: JSON.stringify({ content, agent_id: 'dashboard' }),
|
|
386
|
+
})
|
|
387
|
+
.then((r) => r.json())
|
|
388
|
+
.then(() => {
|
|
389
|
+
openPanel(taskId);
|
|
390
|
+
})
|
|
391
|
+
.catch(() => TaskBoard.showToast('Error', 'Failed to post comment', 'error'));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ---- Artifact interactions ----
|
|
395
|
+
|
|
396
|
+
function toggleArtifact(id) {
|
|
397
|
+
const wrapper = document.getElementById(id);
|
|
398
|
+
if (!wrapper) return;
|
|
399
|
+
const isCollapsed = wrapper.classList.contains('artifact-collapsed');
|
|
400
|
+
wrapper.classList.toggle('artifact-collapsed', !isCollapsed);
|
|
401
|
+
wrapper.classList.toggle('artifact-expanded', isCollapsed);
|
|
402
|
+
const btn = wrapper.parentElement.querySelector('.artifact-toggle');
|
|
403
|
+
if (btn) {
|
|
404
|
+
const icon = btn.querySelector('.material-symbols-outlined');
|
|
405
|
+
if (isCollapsed) {
|
|
406
|
+
icon.textContent = 'expand_less';
|
|
407
|
+
btn.childNodes[btn.childNodes.length - 1].textContent = ' Show less';
|
|
408
|
+
} else {
|
|
409
|
+
icon.textContent = 'expand_more';
|
|
410
|
+
const codeEl = wrapper.querySelector('.artifact-code, .diff-viewer');
|
|
411
|
+
const count = codeEl ? codeEl.textContent.split('\n').length : 0;
|
|
412
|
+
btn.childNodes[btn.childNodes.length - 1].textContent = ' Show more (' + count + ' lines)';
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function copyArtifact(btn) {
|
|
418
|
+
const artifactEl = btn.closest('.panel-artifact');
|
|
419
|
+
if (!artifactEl) return;
|
|
420
|
+
const codeEl = artifactEl.querySelector('.artifact-code, .diff-viewer');
|
|
421
|
+
if (!codeEl) return;
|
|
422
|
+
const text = codeEl.textContent || '';
|
|
423
|
+
navigator.clipboard
|
|
424
|
+
.writeText(text)
|
|
425
|
+
.then(function () {
|
|
426
|
+
const origHtml = btn.innerHTML;
|
|
427
|
+
btn.innerHTML = '<span class="material-symbols-outlined">check</span> Copied';
|
|
428
|
+
setTimeout(function () {
|
|
429
|
+
btn.innerHTML = origHtml;
|
|
430
|
+
}, 1500);
|
|
431
|
+
})
|
|
432
|
+
.catch(function () {
|
|
433
|
+
/* fallback */
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ---- Artifact fullscreen ----
|
|
438
|
+
|
|
439
|
+
function openArtifactFullscreen(artId) {
|
|
440
|
+
var esc = TaskBoard.esc;
|
|
441
|
+
const wrapper = document.getElementById(artId);
|
|
442
|
+
if (!wrapper) return;
|
|
443
|
+
const content = wrapper.querySelector('.artifact-code, .diff-viewer');
|
|
444
|
+
if (!content) return;
|
|
445
|
+
|
|
446
|
+
const header = wrapper.closest('.panel-artifact')?.querySelector('h4');
|
|
447
|
+
const title = header ? header.textContent : 'Artifact';
|
|
448
|
+
|
|
449
|
+
const overlay = document.createElement('div');
|
|
450
|
+
overlay.className = 'artifact-fullscreen-overlay';
|
|
451
|
+
overlay.innerHTML =
|
|
452
|
+
'<div class="artifact-fullscreen-header">' +
|
|
453
|
+
'<h3>' +
|
|
454
|
+
esc(title) +
|
|
455
|
+
'</h3>' +
|
|
456
|
+
'<button class="icon-btn" aria-label="Close fullscreen"><span class="material-symbols-outlined">close</span></button>' +
|
|
457
|
+
'</div>' +
|
|
458
|
+
'<div class="artifact-fullscreen-body"></div>';
|
|
459
|
+
|
|
460
|
+
const body = overlay.querySelector('.artifact-fullscreen-body');
|
|
461
|
+
body.innerHTML = wrapper.innerHTML;
|
|
462
|
+
const fade = body.querySelector('.artifact-fade');
|
|
463
|
+
if (fade) fade.remove();
|
|
464
|
+
const artWrapper = body.querySelector('.artifact-wrapper');
|
|
465
|
+
if (artWrapper) {
|
|
466
|
+
artWrapper.classList.remove('artifact-collapsed');
|
|
467
|
+
artWrapper.classList.add('artifact-expanded');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
overlay.querySelector('button').addEventListener('click', () => overlay.remove());
|
|
471
|
+
overlay.addEventListener('keydown', (e) => {
|
|
472
|
+
if (e.key === 'Escape') overlay.remove();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
document.body.appendChild(overlay);
|
|
476
|
+
overlay.querySelector('button').focus();
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ---- Panel resize ----
|
|
480
|
+
|
|
481
|
+
function initPanelResize() {
|
|
482
|
+
const panel = document.getElementById('side-panel');
|
|
483
|
+
if (!panel) return;
|
|
484
|
+
const handle = document.createElement('div');
|
|
485
|
+
handle.className = 'panel-resize-handle';
|
|
486
|
+
panel.appendChild(handle);
|
|
487
|
+
|
|
488
|
+
let isResizing = false;
|
|
489
|
+
let startX = 0;
|
|
490
|
+
let startWidth = 0;
|
|
491
|
+
|
|
492
|
+
handle.addEventListener('mousedown', (e) => {
|
|
493
|
+
isResizing = true;
|
|
494
|
+
startX = e.clientX;
|
|
495
|
+
startWidth = panel.offsetWidth;
|
|
496
|
+
document.body.style.cursor = 'col-resize';
|
|
497
|
+
document.body.style.userSelect = 'none';
|
|
498
|
+
e.preventDefault();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
document.addEventListener('mousemove', (e) => {
|
|
502
|
+
if (!isResizing) return;
|
|
503
|
+
const dx = startX - e.clientX;
|
|
504
|
+
const newWidth = Math.max(400, Math.min(startWidth + dx, window.innerWidth * 0.8));
|
|
505
|
+
panel.style.width = newWidth + 'px';
|
|
506
|
+
panel.style.minWidth = newWidth + 'px';
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
document.addEventListener('mouseup', () => {
|
|
510
|
+
if (!isResizing) return;
|
|
511
|
+
isResizing = false;
|
|
512
|
+
document.body.style.cursor = '';
|
|
513
|
+
document.body.style.userSelect = '';
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ---- Panel event delegation ----
|
|
518
|
+
|
|
519
|
+
function initPanelEvents() {
|
|
520
|
+
document.getElementById('side-panel').addEventListener('click', (e) => {
|
|
521
|
+
const closeBtn = e.target.closest('[data-action="close-panel"]');
|
|
522
|
+
if (closeBtn) {
|
|
523
|
+
closePanel();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const subtask = e.target.closest('[data-subtask-id]');
|
|
528
|
+
if (subtask) {
|
|
529
|
+
openPanel(parseInt(subtask.dataset.subtaskId, 10));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const sendBtn = e.target.closest('#comment-send-btn');
|
|
534
|
+
if (sendBtn) {
|
|
535
|
+
submitComment(parseInt(sendBtn.dataset.taskId, 10));
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const toggleBtn = e.target.closest('.artifact-toggle');
|
|
540
|
+
if (toggleBtn) {
|
|
541
|
+
const artId = toggleBtn.dataset.artifactId;
|
|
542
|
+
if (artId) toggleArtifact(artId);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const copyBtn = e.target.closest('.artifact-copy-btn');
|
|
547
|
+
if (copyBtn) {
|
|
548
|
+
copyArtifact(copyBtn);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const fsBtn = e.target.closest('.artifact-fullscreen-btn');
|
|
553
|
+
if (fsBtn) {
|
|
554
|
+
const artId = fsBtn.dataset.artifactId;
|
|
555
|
+
if (artId) openArtifactFullscreen(artId);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ---- Register on namespace ----
|
|
562
|
+
|
|
563
|
+
TaskBoard.renderArtifactContent = renderArtifactContent;
|
|
564
|
+
TaskBoard.renderArtifactBlock = renderArtifactBlock;
|
|
565
|
+
TaskBoard.renderDecisionBlock = renderDecisionBlock;
|
|
566
|
+
TaskBoard.renderLearningBlock = renderLearningBlock;
|
|
567
|
+
TaskBoard.openPanel = openPanel;
|
|
568
|
+
TaskBoard.closePanel = closePanel;
|
|
569
|
+
TaskBoard.submitComment = submitComment;
|
|
570
|
+
TaskBoard.toggleArtifact = toggleArtifact;
|
|
571
|
+
TaskBoard.copyArtifact = copyArtifact;
|
|
572
|
+
TaskBoard.openArtifactFullscreen = openArtifactFullscreen;
|
|
573
|
+
TaskBoard.initPanelResize = initPanelResize;
|
|
574
|
+
TaskBoard.initPanelEvents = initPanelEvents;
|
package/dist/ui/styles.css
CHANGED
|
@@ -781,6 +781,79 @@ header {
|
|
|
781
781
|
color: var(--text);
|
|
782
782
|
}
|
|
783
783
|
|
|
784
|
+
/* --------------------------------------------------------------------------
|
|
785
|
+
Learnings (side panel)
|
|
786
|
+
-------------------------------------------------------------------------- */
|
|
787
|
+
|
|
788
|
+
.panel-learning {
|
|
789
|
+
border: 1px solid var(--border);
|
|
790
|
+
border-left: 3px solid var(--amber, #9a6700);
|
|
791
|
+
border-radius: var(--radius);
|
|
792
|
+
margin-bottom: 8px;
|
|
793
|
+
overflow: hidden;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
.learning-header {
|
|
797
|
+
display: flex;
|
|
798
|
+
align-items: center;
|
|
799
|
+
gap: 6px;
|
|
800
|
+
padding: 8px 12px;
|
|
801
|
+
font-weight: 600;
|
|
802
|
+
font-size: 13px;
|
|
803
|
+
background: var(--amber-dim, rgba(154, 103, 0, 0.12));
|
|
804
|
+
border-bottom: 1px solid var(--border);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
.learning-icon {
|
|
808
|
+
font-size: 16px;
|
|
809
|
+
color: var(--amber, #9a6700);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
.learning-category {
|
|
813
|
+
font-size: 11px;
|
|
814
|
+
text-transform: uppercase;
|
|
815
|
+
letter-spacing: 0.3px;
|
|
816
|
+
color: var(--amber, #9a6700);
|
|
817
|
+
background: var(--amber-dim, rgba(154, 103, 0, 0.12));
|
|
818
|
+
padding: 1px 6px;
|
|
819
|
+
border-radius: 4px;
|
|
820
|
+
font-weight: 500;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.learning-source {
|
|
824
|
+
font-size: 11px;
|
|
825
|
+
color: var(--text-dim);
|
|
826
|
+
font-weight: 400;
|
|
827
|
+
font-style: italic;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
.learning-body {
|
|
831
|
+
padding: 8px 12px;
|
|
832
|
+
font-size: 13px;
|
|
833
|
+
line-height: 1.5;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
.learning-body .rendered-md {
|
|
837
|
+
font-size: 13px;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/* --------------------------------------------------------------------------
|
|
841
|
+
Affinity indicator (task cards)
|
|
842
|
+
-------------------------------------------------------------------------- */
|
|
843
|
+
|
|
844
|
+
.task-card-affinity {
|
|
845
|
+
display: inline-flex;
|
|
846
|
+
align-items: center;
|
|
847
|
+
gap: 2px;
|
|
848
|
+
font-size: 11px;
|
|
849
|
+
color: var(--indigo, #5856d6);
|
|
850
|
+
margin-left: auto;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
.task-card-affinity .material-symbols-outlined {
|
|
854
|
+
font-size: 14px;
|
|
855
|
+
}
|
|
856
|
+
|
|
784
857
|
.column-body {
|
|
785
858
|
padding: 6px 8px;
|
|
786
859
|
overflow-y: auto;
|
|
@@ -1300,6 +1373,42 @@ header {
|
|
|
1300
1373
|
outline-offset: 2px;
|
|
1301
1374
|
}
|
|
1302
1375
|
|
|
1376
|
+
/* --------------------------------------------------------------------------
|
|
1377
|
+
Show More Button
|
|
1378
|
+
-------------------------------------------------------------------------- */
|
|
1379
|
+
|
|
1380
|
+
.column-show-more-btn {
|
|
1381
|
+
display: flex;
|
|
1382
|
+
align-items: center;
|
|
1383
|
+
justify-content: center;
|
|
1384
|
+
gap: 4px;
|
|
1385
|
+
padding: 8px;
|
|
1386
|
+
margin: 2px 8px 4px;
|
|
1387
|
+
border: none;
|
|
1388
|
+
border-radius: var(--radius-sm);
|
|
1389
|
+
background: var(--bg-elevated);
|
|
1390
|
+
color: var(--text-dim);
|
|
1391
|
+
font-size: 12px;
|
|
1392
|
+
font-weight: 500;
|
|
1393
|
+
font-family: var(--font-sans);
|
|
1394
|
+
cursor: pointer;
|
|
1395
|
+
transition: all var(--transition-fast);
|
|
1396
|
+
width: calc(100% - 16px);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
.column-show-more-btn:hover {
|
|
1400
|
+
color: var(--accent);
|
|
1401
|
+
background: var(--accent-dim);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
.column-show-more-btn .material-symbols-outlined {
|
|
1405
|
+
font-size: 16px;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
.kanban-column.collapsed .column-show-more-btn {
|
|
1409
|
+
display: none;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1303
1412
|
/* --------------------------------------------------------------------------
|
|
1304
1413
|
Inline Task Creation
|
|
1305
1414
|
-------------------------------------------------------------------------- */
|