@longshot/cli 0.0.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/LICENSE +8 -0
- package/dist/agent.js +214 -0
- package/dist/cli.js +172 -0
- package/dist/git.js +291 -0
- package/dist/index.js +1250 -0
- package/dist/profile.js +79 -0
- package/dist/projects.js +337 -0
- package/dist/queue.js +868 -0
- package/dist/services.js +194 -0
- package/dist/store.js +612 -0
- package/dist/views/agent-progress.js +242 -0
- package/dist/views/branches.js +191 -0
- package/dist/views/chat.js +386 -0
- package/dist/views/diff.js +321 -0
- package/dist/views/history.js +124 -0
- package/dist/views/layout.js +121 -0
- package/dist/views/run.js +92 -0
- package/dist/views/services.js +230 -0
- package/dist/views/spec.js +18 -0
- package/dist/views/tasks.js +898 -0
- package/dist/views/verify.js +209 -0
- package/package.json +36 -0
- package/public/style.css +2088 -0
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
import { marked } from "marked";
|
|
2
|
+
import { hasRealSpec } from "../store.js";
|
|
3
|
+
import { layout } from "./layout.js";
|
|
4
|
+
const statusColors = {
|
|
5
|
+
drafting: "status-drafting",
|
|
6
|
+
ready: "status-ready",
|
|
7
|
+
in_progress: "status-progress",
|
|
8
|
+
complete: "status-complete",
|
|
9
|
+
rejected: "status-rejected",
|
|
10
|
+
queued: "status-queued",
|
|
11
|
+
approved: "status-approved",
|
|
12
|
+
refining: "status-refining",
|
|
13
|
+
fixing: "status-fixing",
|
|
14
|
+
conflict: "status-conflict",
|
|
15
|
+
};
|
|
16
|
+
const statusLabels = {
|
|
17
|
+
drafting: "Drafting",
|
|
18
|
+
ready: "Ready",
|
|
19
|
+
in_progress: "In Progress",
|
|
20
|
+
complete: "Complete",
|
|
21
|
+
rejected: "Rejected",
|
|
22
|
+
queued: "Queued",
|
|
23
|
+
approved: "Approved",
|
|
24
|
+
refining: "Refining",
|
|
25
|
+
fixing: "Fixing",
|
|
26
|
+
conflict: "Conflict",
|
|
27
|
+
};
|
|
28
|
+
// Sort groups: agent-active tasks first, then other active, then terminal
|
|
29
|
+
const statusGroup = {
|
|
30
|
+
in_progress: 0,
|
|
31
|
+
queued: 0,
|
|
32
|
+
approved: 0,
|
|
33
|
+
refining: 0,
|
|
34
|
+
fixing: 0,
|
|
35
|
+
conflict: 0,
|
|
36
|
+
ready: 1,
|
|
37
|
+
drafting: 1,
|
|
38
|
+
complete: 2,
|
|
39
|
+
rejected: 2,
|
|
40
|
+
};
|
|
41
|
+
function taskSortKey(t) {
|
|
42
|
+
const group = statusGroup[t.status] ?? 2;
|
|
43
|
+
return [group, t.id];
|
|
44
|
+
}
|
|
45
|
+
function renderTaskCard(task, ctx) {
|
|
46
|
+
const colorClass = statusColors[task.status] || "";
|
|
47
|
+
const label = statusLabels[task.status] || task.status;
|
|
48
|
+
// Build action buttons
|
|
49
|
+
const actions = [];
|
|
50
|
+
if (task.status === "drafting" && ctx.pendingQueueTaskIds.has(task.id) && ctx.agentTaskId !== task.id) {
|
|
51
|
+
actions.push(`<span class="btn btn-task-action btn-queued">Drafting...</span>`);
|
|
52
|
+
}
|
|
53
|
+
if (task.status === "ready") {
|
|
54
|
+
actions.push(`<button class="btn btn-task-action btn-primary" onclick="event.preventDefault(); startWork(${task.id})">Start Work</button>`);
|
|
55
|
+
}
|
|
56
|
+
if (task.status === "in_progress" && ctx.pendingDiffId) {
|
|
57
|
+
actions.push(`<a href="/diff/${ctx.pendingDiffId}" class="btn btn-task-action btn-approve" onclick="event.stopPropagation()">Review Diff</a>`);
|
|
58
|
+
}
|
|
59
|
+
if ((task.status === "in_progress" || task.status === "drafting" || task.status === "refining" || task.status === "approved" || task.status === "fixing" || task.status === "conflict") && ctx.agentTaskId === task.id && ctx.currentRunId) {
|
|
60
|
+
actions.push(`<a href="/agent/${ctx.currentRunId}" class="btn btn-task-action btn-task-progress" onclick="event.stopPropagation()">View Progress</a>`);
|
|
61
|
+
}
|
|
62
|
+
const actionsHtml = actions.length
|
|
63
|
+
? `<div class="task-card-actions">${actions.join("\n")}</div>`
|
|
64
|
+
: "";
|
|
65
|
+
return `<a href="/tasks/${task.id}" class="task-card ${colorClass}">
|
|
66
|
+
<div class="task-card-top">
|
|
67
|
+
<span class="task-status-badge ${colorClass}">${label}</span>
|
|
68
|
+
<span class="task-id">#${task.id}</span>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="task-title">${escapeHtml(task.title)}</div>
|
|
71
|
+
${actionsHtml}
|
|
72
|
+
</a>`;
|
|
73
|
+
}
|
|
74
|
+
export function tasksListPage(tasks, taskDiffs = new Map(), agentRunning = false, currentRunId = null, agentTaskId = null, pendingQueueTaskIds = new Set()) {
|
|
75
|
+
const ctx = { agentRunning, currentRunId, agentTaskId, pendingQueueTaskIds };
|
|
76
|
+
const active = tasks
|
|
77
|
+
.filter((t) => t.status !== "complete" && t.status !== "rejected")
|
|
78
|
+
.sort((a, b) => {
|
|
79
|
+
const [aGroup, aId] = taskSortKey(a);
|
|
80
|
+
const [bGroup, bId] = taskSortKey(b);
|
|
81
|
+
if (aGroup !== bGroup)
|
|
82
|
+
return aGroup - bGroup;
|
|
83
|
+
// Group 0 (agent-active): ID ascending (oldest first)
|
|
84
|
+
// Group 1 (pending): ID descending (newest first)
|
|
85
|
+
return aGroup === 0 ? Number(aId) - Number(bId) : Number(bId) - Number(aId);
|
|
86
|
+
});
|
|
87
|
+
const done = tasks
|
|
88
|
+
.filter((t) => t.status === "complete" || t.status === "rejected")
|
|
89
|
+
.sort((a, b) => Number(b.id) - Number(a.id));
|
|
90
|
+
const body = `
|
|
91
|
+
<div class="tasks-container">
|
|
92
|
+
<div class="tasks-header">
|
|
93
|
+
<h1>Tasks</h1>
|
|
94
|
+
</div>
|
|
95
|
+
<div id="git-status-bar"></div>
|
|
96
|
+
<div id="task-list-container">
|
|
97
|
+
${active.length ? `<div class="tasks-section">
|
|
98
|
+
<h2>Active</h2>
|
|
99
|
+
<div class="task-list">${active.map((t) => renderTaskCard(t, { ...ctx, pendingDiffId: taskDiffs.get(t.id) })).join("\n")}</div>
|
|
100
|
+
</div>` : ""}
|
|
101
|
+
${done.length ? `<div class="tasks-section">
|
|
102
|
+
<h2>Done</h2>
|
|
103
|
+
<div class="task-list">${done.map((t) => renderTaskCard(t, { ...ctx, pendingDiffId: taskDiffs.get(t.id) })).join("\n")}</div>
|
|
104
|
+
</div>` : ""}
|
|
105
|
+
${!tasks.length ? '<p class="empty-state">No tasks yet. Chat to create one.</p>' : ""}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<script>
|
|
109
|
+
const statusColors = ${JSON.stringify(statusColors)};
|
|
110
|
+
const statusLabels = ${JSON.stringify(statusLabels)};
|
|
111
|
+
const statusGroup = ${JSON.stringify(statusGroup)};
|
|
112
|
+
|
|
113
|
+
function escapeHtmlClient(str) {
|
|
114
|
+
const div = document.createElement('div');
|
|
115
|
+
div.textContent = str;
|
|
116
|
+
return div.innerHTML;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function renderTaskCardClient(task, taskDiffs, agentRunning, currentRunId, agentTaskId, pendingQueueTaskIds) {
|
|
120
|
+
const colorClass = statusColors[task.status] || '';
|
|
121
|
+
const label = statusLabels[task.status] || task.status;
|
|
122
|
+
const pendingDiffId = taskDiffs[task.id];
|
|
123
|
+
const pendingQueue = pendingQueueTaskIds && pendingQueueTaskIds.indexOf(task.id) >= 0;
|
|
124
|
+
const actions = [];
|
|
125
|
+
|
|
126
|
+
if (task.status === 'drafting' && pendingQueue && agentTaskId !== task.id) {
|
|
127
|
+
actions.push('<span class="btn btn-task-action btn-queued">Drafting...</span>');
|
|
128
|
+
}
|
|
129
|
+
if (task.status === 'ready') {
|
|
130
|
+
actions.push('<button class="btn btn-task-action btn-primary" onclick="event.preventDefault(); startWork(' + task.id + ')">Start Work</button>');
|
|
131
|
+
}
|
|
132
|
+
if (task.status === 'in_progress' && pendingDiffId) {
|
|
133
|
+
actions.push('<a href="/diff/' + pendingDiffId + '" class="btn btn-task-action btn-approve" onclick="event.stopPropagation()">Review Diff</a>');
|
|
134
|
+
}
|
|
135
|
+
if ((task.status === 'in_progress' || task.status === 'drafting' || task.status === 'refining' || task.status === 'approved' || task.status === 'fixing' || task.status === 'conflict') && agentTaskId === task.id && currentRunId) {
|
|
136
|
+
actions.push('<a href="/agent/' + currentRunId + '" class="btn btn-task-action btn-task-progress" onclick="event.stopPropagation()">View Progress</a>');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const actionsHtml = actions.length ? '<div class="task-card-actions">' + actions.join('') + '</div>' : '';
|
|
140
|
+
|
|
141
|
+
return '<a href="/tasks/' + task.id + '" class="task-card ' + colorClass + '">' +
|
|
142
|
+
'<div class="task-card-top">' +
|
|
143
|
+
'<span class="task-status-badge ' + colorClass + '">' + label + '</span>' +
|
|
144
|
+
'<span class="task-id">#' + task.id + '</span>' +
|
|
145
|
+
'</div>' +
|
|
146
|
+
'<div class="task-title">' + escapeHtmlClient(task.title) + '</div>' +
|
|
147
|
+
actionsHtml +
|
|
148
|
+
'</a>';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function renderTaskList(tasks, taskDiffs, agentRunning, currentRunId, agentTaskId, pendingQueueTaskIds) {
|
|
152
|
+
const active = tasks
|
|
153
|
+
.filter(t => t.status !== 'complete' && t.status !== 'rejected')
|
|
154
|
+
.sort((a, b) => {
|
|
155
|
+
const aGroup = a.status in statusGroup ? statusGroup[a.status] : 2;
|
|
156
|
+
const bGroup = b.status in statusGroup ? statusGroup[b.status] : 2;
|
|
157
|
+
if (aGroup !== bGroup) return aGroup - bGroup;
|
|
158
|
+
return aGroup === 0 ? a.id - b.id : b.id - a.id;
|
|
159
|
+
});
|
|
160
|
+
const done = tasks
|
|
161
|
+
.filter(t => t.status === 'complete' || t.status === 'rejected')
|
|
162
|
+
.sort((a, b) => b.id - a.id);
|
|
163
|
+
|
|
164
|
+
let html = '';
|
|
165
|
+
if (active.length) {
|
|
166
|
+
html += '<div class="tasks-section"><h2>Active</h2><div class="task-list">' +
|
|
167
|
+
active.map(t => renderTaskCardClient(t, taskDiffs, agentRunning, currentRunId, agentTaskId, pendingQueueTaskIds)).join('') +
|
|
168
|
+
'</div></div>';
|
|
169
|
+
}
|
|
170
|
+
if (done.length) {
|
|
171
|
+
html += '<div class="tasks-section"><h2>Done</h2><div class="task-list">' +
|
|
172
|
+
done.map(t => renderTaskCardClient(t, taskDiffs, agentRunning, currentRunId, agentTaskId, pendingQueueTaskIds)).join('') +
|
|
173
|
+
'</div></div>';
|
|
174
|
+
}
|
|
175
|
+
if (!tasks.length) {
|
|
176
|
+
html = '<p class="empty-state">No tasks yet. Chat to create one.</p>';
|
|
177
|
+
}
|
|
178
|
+
return html;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function pollTasks() {
|
|
182
|
+
try {
|
|
183
|
+
const resp = await fetch('/api/tasks');
|
|
184
|
+
if (!resp.ok) return;
|
|
185
|
+
const data = await resp.json();
|
|
186
|
+
const container = document.getElementById('task-list-container');
|
|
187
|
+
if (container) {
|
|
188
|
+
container.innerHTML = renderTaskList(data.tasks, data.taskDiffs, data.agentRunning, data.currentRunId, data.agentTaskId, data.pendingQueueTaskIds || []);
|
|
189
|
+
}
|
|
190
|
+
} catch {}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
setInterval(pollTasks, 3000);
|
|
194
|
+
|
|
195
|
+
async function startWork(id) {
|
|
196
|
+
const btn = event.target;
|
|
197
|
+
btn.disabled = true;
|
|
198
|
+
btn.textContent = 'Queuing...';
|
|
199
|
+
try {
|
|
200
|
+
const resp = await fetch('/tasks/' + id + '/status', {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers: { 'Content-Type': 'application/json' },
|
|
203
|
+
body: JSON.stringify({ status: 'in_progress' }),
|
|
204
|
+
});
|
|
205
|
+
if (resp.ok) {
|
|
206
|
+
pollTasks();
|
|
207
|
+
} else {
|
|
208
|
+
btn.disabled = false;
|
|
209
|
+
btn.textContent = 'Start Work';
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
btn.disabled = false;
|
|
213
|
+
btn.textContent = 'Start Work';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- Git sync ---
|
|
218
|
+
let gitPullFailed = false;
|
|
219
|
+
let gitCommitsExpanded = false;
|
|
220
|
+
|
|
221
|
+
function renderGitStatus(data) {
|
|
222
|
+
const bar = document.getElementById('git-status-bar');
|
|
223
|
+
if (!bar) return;
|
|
224
|
+
|
|
225
|
+
if (!data.remote) {
|
|
226
|
+
bar.innerHTML = '';
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (data.ahead === 0 && data.behind === 0 && !gitPullFailed) {
|
|
231
|
+
bar.innerHTML = '';
|
|
232
|
+
gitCommitsExpanded = false;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let html = '<div class="git-sync-bar">';
|
|
237
|
+
|
|
238
|
+
if (gitPullFailed) {
|
|
239
|
+
html += '<div class="git-sync-error" id="git-error">' + escapeHtmlClient(gitPullFailed) + '</div>';
|
|
240
|
+
html += '<div class="git-sync-actions">';
|
|
241
|
+
html += '<button class="btn btn-small git-btn" onclick="gitPullRebase()">Pull (rebase)</button>';
|
|
242
|
+
html += '<button class="btn btn-small git-btn" onclick="gitDismissError()">Dismiss</button>';
|
|
243
|
+
html += '</div>';
|
|
244
|
+
} else {
|
|
245
|
+
html += '<div class="git-sync-info">';
|
|
246
|
+
if (data.ahead > 0) {
|
|
247
|
+
html += '<span class="git-ahead git-ahead-toggle" onclick="toggleCommitList()">' + data.ahead + ' commit' + (data.ahead === 1 ? '' : 's') + ' to push</span>';
|
|
248
|
+
}
|
|
249
|
+
if (data.behind > 0) {
|
|
250
|
+
html += '<span class="git-behind">' + data.behind + ' behind</span>';
|
|
251
|
+
}
|
|
252
|
+
html += '</div>';
|
|
253
|
+
html += '<div class="git-sync-actions">';
|
|
254
|
+
if (data.behind > 0) {
|
|
255
|
+
html += '<button class="btn btn-small git-btn git-pull-btn" onclick="gitPull()">Pull</button>';
|
|
256
|
+
}
|
|
257
|
+
html += '</div>';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
html += '</div>';
|
|
261
|
+
|
|
262
|
+
// Expandable commit list
|
|
263
|
+
if (data.ahead > 0 && data.outgoing && data.outgoing.length > 0) {
|
|
264
|
+
html += '<div class="git-commit-list" id="git-commit-list" style="display:' + (gitCommitsExpanded ? 'block' : 'none') + '">';
|
|
265
|
+
html += '<div class="git-commit-entries">';
|
|
266
|
+
for (var i = 0; i < data.outgoing.length; i++) {
|
|
267
|
+
var c = data.outgoing[i];
|
|
268
|
+
html += '<div class="git-commit-entry"><span class="git-commit-hash">' + escapeHtmlClient(c.hash) + '</span> <span class="git-commit-subject">' + escapeHtmlClient(c.subject) + '</span></div>';
|
|
269
|
+
}
|
|
270
|
+
html += '</div>';
|
|
271
|
+
html += '<div class="git-commit-list-actions"><button class="btn btn-small git-btn git-push-btn" onclick="gitPush()">Push</button></div>';
|
|
272
|
+
html += '</div>';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
bar.innerHTML = html;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function toggleCommitList() {
|
|
279
|
+
gitCommitsExpanded = !gitCommitsExpanded;
|
|
280
|
+
var el = document.getElementById('git-commit-list');
|
|
281
|
+
if (el) {
|
|
282
|
+
el.style.display = gitCommitsExpanded ? 'block' : 'none';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function pollGitStatus() {
|
|
287
|
+
try {
|
|
288
|
+
const resp = await fetch('/api/git/status');
|
|
289
|
+
if (!resp.ok) return;
|
|
290
|
+
const data = await resp.json();
|
|
291
|
+
renderGitStatus(data);
|
|
292
|
+
} catch {}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function gitPush() {
|
|
296
|
+
const btns = document.querySelectorAll('.git-btn');
|
|
297
|
+
btns.forEach(b => { b.disabled = true; });
|
|
298
|
+
try {
|
|
299
|
+
const resp = await fetch('/api/git/push', { method: 'POST' });
|
|
300
|
+
const data = await resp.json();
|
|
301
|
+
if (!data.success) {
|
|
302
|
+
alert('Push failed: ' + (data.error || 'Unknown error'));
|
|
303
|
+
}
|
|
304
|
+
pollGitStatus();
|
|
305
|
+
} catch {
|
|
306
|
+
alert('Push failed');
|
|
307
|
+
}
|
|
308
|
+
btns.forEach(b => { b.disabled = false; });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function gitPull() {
|
|
312
|
+
const btns = document.querySelectorAll('.git-btn');
|
|
313
|
+
btns.forEach(b => { b.disabled = true; });
|
|
314
|
+
try {
|
|
315
|
+
const resp = await fetch('/api/git/pull', {
|
|
316
|
+
method: 'POST',
|
|
317
|
+
headers: { 'Content-Type': 'application/json' },
|
|
318
|
+
body: JSON.stringify({}),
|
|
319
|
+
});
|
|
320
|
+
const data = await resp.json();
|
|
321
|
+
if (!data.success) {
|
|
322
|
+
gitPullFailed = data.error || 'Pull failed';
|
|
323
|
+
pollGitStatus();
|
|
324
|
+
} else {
|
|
325
|
+
gitPullFailed = false;
|
|
326
|
+
pollGitStatus();
|
|
327
|
+
}
|
|
328
|
+
} catch {
|
|
329
|
+
alert('Pull failed');
|
|
330
|
+
}
|
|
331
|
+
btns.forEach(b => { b.disabled = false; });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function gitPullRebase() {
|
|
335
|
+
const btns = document.querySelectorAll('.git-btn');
|
|
336
|
+
btns.forEach(b => { b.disabled = true; });
|
|
337
|
+
try {
|
|
338
|
+
const resp = await fetch('/api/git/pull', {
|
|
339
|
+
method: 'POST',
|
|
340
|
+
headers: { 'Content-Type': 'application/json' },
|
|
341
|
+
body: JSON.stringify({ rebase: true }),
|
|
342
|
+
});
|
|
343
|
+
const data = await resp.json();
|
|
344
|
+
if (!data.success) {
|
|
345
|
+
gitPullFailed = data.error || 'Pull (rebase) failed';
|
|
346
|
+
} else {
|
|
347
|
+
gitPullFailed = false;
|
|
348
|
+
}
|
|
349
|
+
pollGitStatus();
|
|
350
|
+
} catch {
|
|
351
|
+
alert('Pull (rebase) failed');
|
|
352
|
+
}
|
|
353
|
+
btns.forEach(b => { b.disabled = false; });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function gitDismissError() {
|
|
357
|
+
gitPullFailed = false;
|
|
358
|
+
pollGitStatus();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
pollGitStatus();
|
|
362
|
+
setInterval(pollGitStatus, 10000);
|
|
363
|
+
</script>`;
|
|
364
|
+
return layout("Tasks", "tasks", body);
|
|
365
|
+
}
|
|
366
|
+
function renderMessage(msg) {
|
|
367
|
+
const isUser = msg.role === "user";
|
|
368
|
+
const rendered = marked.parse(msg.content, { async: false });
|
|
369
|
+
return `<div class="message ${isUser ? "message-user" : "message-assistant"}">
|
|
370
|
+
<div class="message-role">${isUser ? "You" : "Claude"}</div>
|
|
371
|
+
<div class="message-content">${rendered}</div>
|
|
372
|
+
</div>`;
|
|
373
|
+
}
|
|
374
|
+
export function taskDetailPage(task, spec, agentRunning = false, currentRunId = null, chatMessages = [], chatPending = false, report = null, agentTaskId = null, pendingDiffId, hasPendingQueueItem = false, conflictError) {
|
|
375
|
+
const rendered = spec
|
|
376
|
+
? marked.parse(spec, { async: false })
|
|
377
|
+
: '<p class="empty-state">No task spec written yet.</p>';
|
|
378
|
+
const colorClass = statusColors[task.status] || "";
|
|
379
|
+
const isDrafting = task.status === "drafting" || task.status === "refining";
|
|
380
|
+
const isConflict = task.status === "conflict";
|
|
381
|
+
const agentWorkingOnThis = agentTaskId === task.id && agentRunning;
|
|
382
|
+
const isComplete = task.status === "complete";
|
|
383
|
+
const draftPending = hasPendingQueueItem && !agentWorkingOnThis;
|
|
384
|
+
const actions = [];
|
|
385
|
+
// Drafting (no agent, no pending queue item): Mark Ready (only if spec has real content)
|
|
386
|
+
if (isDrafting && !agentWorkingOnThis && !draftPending && hasRealSpec(spec)) {
|
|
387
|
+
actions.push(`<button class="btn btn-approve" onclick="updateTask(${task.id}, 'ready')">Mark Ready</button>`);
|
|
388
|
+
}
|
|
389
|
+
// Show "Drafting..." indicator when queue item is pending
|
|
390
|
+
if (isDrafting && draftPending) {
|
|
391
|
+
actions.push(`<span class="btn btn-queued">Drafting...</span>`);
|
|
392
|
+
}
|
|
393
|
+
// Ready: Start Work
|
|
394
|
+
if (task.status === "ready") {
|
|
395
|
+
actions.push(`<button class="btn btn-primary" id="start-work-btn">Start Work</button>`);
|
|
396
|
+
}
|
|
397
|
+
// In Progress with pending diff: Review Diff
|
|
398
|
+
if (task.status === "in_progress" && pendingDiffId) {
|
|
399
|
+
actions.push(`<a href="/diff/${pendingDiffId}" class="btn btn-task-action btn-approve">Review Diff</a>`);
|
|
400
|
+
}
|
|
401
|
+
// In Progress with no diff and no agent: Done — Review Diff (after manual conflict resolution)
|
|
402
|
+
if (task.status === "in_progress" && !pendingDiffId && !agentWorkingOnThis) {
|
|
403
|
+
actions.push(`<button class="btn btn-approve" onclick="conflictDone()">Done — Review Diff</button>`);
|
|
404
|
+
}
|
|
405
|
+
// Agent working on this task: View Progress
|
|
406
|
+
if (agentWorkingOnThis && currentRunId) {
|
|
407
|
+
actions.push(`<a href="/agent/${currentRunId}" class="btn btn-task-action btn-task-progress">View Progress</a>`);
|
|
408
|
+
}
|
|
409
|
+
const bannerHtml = agentWorkingOnThis && currentRunId
|
|
410
|
+
? `<a href="/agent/${currentRunId}" class="agent-banner">Agent working... tap to view progress</a>`
|
|
411
|
+
: "";
|
|
412
|
+
// Completion report for completed tasks
|
|
413
|
+
const reportHtml = isComplete && report
|
|
414
|
+
? `<div class="task-report">
|
|
415
|
+
<h2>Completion Report</h2>
|
|
416
|
+
<div class="report-content">${marked.parse(report, { async: false })}</div>
|
|
417
|
+
</div>`
|
|
418
|
+
: "";
|
|
419
|
+
// Chat UI for drafting tasks — history always visible, input hidden while agent is running
|
|
420
|
+
const chatHistoryHtml = isDrafting ? `
|
|
421
|
+
<div class="task-chat">
|
|
422
|
+
<div class="messages" id="task-messages">
|
|
423
|
+
${chatMessages.map(renderMessage).join("\n")}
|
|
424
|
+
${chatPending ? `<div class="message message-assistant" id="thinking-indicator">
|
|
425
|
+
<div class="message-role">Claude</div>
|
|
426
|
+
<div class="message-content"><em>Thinking...</em></div>
|
|
427
|
+
</div>` : ""}
|
|
428
|
+
</div>
|
|
429
|
+
${!agentWorkingOnThis && !draftPending ? `<div class="chat-input-area">
|
|
430
|
+
<form class="chat-input" id="task-chat-form">
|
|
431
|
+
<textarea
|
|
432
|
+
name="message"
|
|
433
|
+
id="task-message-input"
|
|
434
|
+
placeholder="Describe changes to the spec..."
|
|
435
|
+
rows="2"
|
|
436
|
+
autofocus
|
|
437
|
+
></textarea>
|
|
438
|
+
<div class="task-chat-buttons">
|
|
439
|
+
<button type="button" id="task-refine-btn" class="btn btn-primary">Refine</button>
|
|
440
|
+
</div>
|
|
441
|
+
</form>
|
|
442
|
+
</div>` : `<div class="chat-busy">
|
|
443
|
+
<p>Claude is working on this task...${currentRunId ? ` <a href="/agent/${currentRunId}">View progress</a>` : ""}</p>
|
|
444
|
+
</div>`}
|
|
445
|
+
</div>` : "";
|
|
446
|
+
const chatHtml = chatHistoryHtml;
|
|
447
|
+
const body = `
|
|
448
|
+
<div class="task-detail">
|
|
449
|
+
<div id="task-banner">${bannerHtml}</div>
|
|
450
|
+
<div class="task-detail-header">
|
|
451
|
+
<a href="/tasks" class="back-link">Tasks</a>
|
|
452
|
+
<h1>${escapeHtml(task.title)}</h1>
|
|
453
|
+
<div class="task-meta">
|
|
454
|
+
<span class="task-status-badge ${colorClass}" id="task-status-badge">${statusLabels[task.status] || task.status}</span>
|
|
455
|
+
<span class="task-id">#${task.id}</span>
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
<div id="task-primary-actions">${actions.length ? `<div class="task-actions">${actions.join("\n")}</div>` : ""}</div>
|
|
459
|
+
${task.status !== "complete" && task.status !== "rejected" ? `<div class="task-actions" style="margin-top: 8px;">
|
|
460
|
+
<button class="btn btn-reject" style="opacity: 0.7; font-size: 0.85em;" id="cancel-task-btn" onclick="cancelTask(this, ${task.id})">Cancel Task</button>
|
|
461
|
+
</div>` : ""}
|
|
462
|
+
${isConflict ? `<div class="conflict-section">
|
|
463
|
+
<div class="conflict-error">
|
|
464
|
+
<strong>Merge Conflict</strong>
|
|
465
|
+
<p>${escapeHtml(conflictError || "Stash apply failed due to merge conflicts.")}</p>
|
|
466
|
+
</div>
|
|
467
|
+
<div class="conflict-actions">
|
|
468
|
+
<button class="btn btn-primary" onclick="resolveConflict('restart')">Restart Task</button>
|
|
469
|
+
<button class="btn btn-approve" onclick="resolveConflict('agent-resolve')">Agent Resolve</button>
|
|
470
|
+
<button class="btn btn-task-action" onclick="resolveConflict('manual')">Manual Resolve</button>
|
|
471
|
+
</div>
|
|
472
|
+
</div>` : ""}
|
|
473
|
+
<details class="spec-collapsible" ${isDrafting ? "" : "open"}>
|
|
474
|
+
<summary>Task Spec</summary>
|
|
475
|
+
<div class="spec-content">
|
|
476
|
+
${rendered}
|
|
477
|
+
</div>
|
|
478
|
+
</details>
|
|
479
|
+
${reportHtml}
|
|
480
|
+
${chatHtml}
|
|
481
|
+
</div>
|
|
482
|
+
<script>
|
|
483
|
+
const statusColors = ${JSON.stringify(statusColors)};
|
|
484
|
+
const statusLabels = ${JSON.stringify(statusLabels)};
|
|
485
|
+
const agentBusy = ${agentRunning};
|
|
486
|
+
const taskId = ${task.id};
|
|
487
|
+
const isDrafting = ${isDrafting};
|
|
488
|
+
let currentStatus = '${task.status}';
|
|
489
|
+
let lastPendingQueueItem = ${draftPending};
|
|
490
|
+
|
|
491
|
+
async function updateTask(id, status) {
|
|
492
|
+
const btns = document.querySelectorAll('.task-actions .btn');
|
|
493
|
+
btns.forEach(b => { b.disabled = true; });
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
const resp = await fetch('/tasks/' + id + '/status', {
|
|
497
|
+
method: 'POST',
|
|
498
|
+
headers: { 'Content-Type': 'application/json' },
|
|
499
|
+
body: JSON.stringify({ status }),
|
|
500
|
+
});
|
|
501
|
+
if (!resp.ok) {
|
|
502
|
+
const data = await resp.json();
|
|
503
|
+
alert(data.error || 'Failed to update task');
|
|
504
|
+
btns.forEach(b => { b.disabled = false; });
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
location.reload();
|
|
508
|
+
} catch {
|
|
509
|
+
btns.forEach(b => { b.disabled = false; });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function resolveConflict(action) {
|
|
514
|
+
const btns = document.querySelectorAll('.conflict-actions .btn');
|
|
515
|
+
btns.forEach(b => { b.disabled = true; });
|
|
516
|
+
|
|
517
|
+
try {
|
|
518
|
+
const resp = await fetch('/api/tasks/' + taskId + '/conflict/' + action, { method: 'POST' });
|
|
519
|
+
if (!resp.ok) {
|
|
520
|
+
const data = await resp.json();
|
|
521
|
+
alert(data.error || 'Failed to resolve conflict');
|
|
522
|
+
btns.forEach(b => { b.disabled = false; });
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const data = await resp.json();
|
|
526
|
+
if (action === 'manual' && data.conflicts && data.conflicts.length > 0) {
|
|
527
|
+
alert('Conflict markers written to disk in: ' + data.conflicts.join(', ') + '\\n\\nResolve the conflicts locally, then come back and click "Done — Review Diff".');
|
|
528
|
+
}
|
|
529
|
+
location.reload();
|
|
530
|
+
} catch {
|
|
531
|
+
btns.forEach(b => { b.disabled = false; });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function conflictDone() {
|
|
536
|
+
try {
|
|
537
|
+
const resp = await fetch('/api/tasks/' + taskId + '/conflict/done', { method: 'POST' });
|
|
538
|
+
if (!resp.ok) {
|
|
539
|
+
const data = await resp.json();
|
|
540
|
+
alert(data.error || 'No changes to review');
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const data = await resp.json();
|
|
544
|
+
if (data.diffId) {
|
|
545
|
+
location.href = '/diff/' + data.diffId;
|
|
546
|
+
} else {
|
|
547
|
+
location.reload();
|
|
548
|
+
}
|
|
549
|
+
} catch {
|
|
550
|
+
alert('Failed to create diff');
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function cancelTask(btn, id) {
|
|
555
|
+
if (!confirm('Cancel this task?')) return;
|
|
556
|
+
const origText = btn.textContent;
|
|
557
|
+
btn.disabled = true;
|
|
558
|
+
btn.textContent = 'Cancelling...';
|
|
559
|
+
try {
|
|
560
|
+
const resp = await fetch('/tasks/' + id + '/status', {
|
|
561
|
+
method: 'POST',
|
|
562
|
+
headers: { 'Content-Type': 'application/json' },
|
|
563
|
+
body: JSON.stringify({ status: 'rejected' }),
|
|
564
|
+
});
|
|
565
|
+
if (resp.ok) {
|
|
566
|
+
location.href = '/tasks';
|
|
567
|
+
} else {
|
|
568
|
+
btn.disabled = false;
|
|
569
|
+
btn.textContent = origText;
|
|
570
|
+
}
|
|
571
|
+
} catch {
|
|
572
|
+
btn.disabled = false;
|
|
573
|
+
btn.textContent = origText;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Poll for task status changes
|
|
578
|
+
async function pollTaskStatus() {
|
|
579
|
+
try {
|
|
580
|
+
const resp = await fetch('/api/tasks/' + taskId + '/status');
|
|
581
|
+
if (!resp.ok) return;
|
|
582
|
+
const data = await resp.json();
|
|
583
|
+
|
|
584
|
+
// Update status badge
|
|
585
|
+
const badge = document.getElementById('task-status-badge');
|
|
586
|
+
if (badge) {
|
|
587
|
+
const newColorClass = statusColors[data.status] || '';
|
|
588
|
+
const newLabel = statusLabels[data.status] || data.status;
|
|
589
|
+
badge.className = 'task-status-badge ' + newColorClass;
|
|
590
|
+
badge.textContent = newLabel;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const agentWorkingOnThis = data.agentTaskId === taskId && data.agentRunning;
|
|
594
|
+
|
|
595
|
+
// Update banner
|
|
596
|
+
const bannerEl = document.getElementById('task-banner');
|
|
597
|
+
if (bannerEl) {
|
|
598
|
+
if (agentWorkingOnThis && data.currentRunId) {
|
|
599
|
+
bannerEl.innerHTML = '<a href="/agent/' + data.currentRunId + '" class="agent-banner">Agent working... tap to view progress</a>';
|
|
600
|
+
} else {
|
|
601
|
+
bannerEl.innerHTML = '';
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Update action buttons
|
|
606
|
+
const actionsEl = document.getElementById('task-primary-actions');
|
|
607
|
+
if (actionsEl) {
|
|
608
|
+
const actions = [];
|
|
609
|
+
const taskIsDrafting = data.status === 'drafting' || data.status === 'refining';
|
|
610
|
+
const draftPending = data.hasPendingQueueItem && !agentWorkingOnThis;
|
|
611
|
+
if (taskIsDrafting && !agentWorkingOnThis && !draftPending && data.hasSpec) {
|
|
612
|
+
actions.push('<button class="btn btn-approve" onclick="updateTask(' + taskId + ', \\'ready\\')">Mark Ready</button>');
|
|
613
|
+
}
|
|
614
|
+
if (taskIsDrafting && draftPending) {
|
|
615
|
+
actions.push('<span class="btn btn-queued">Drafting...</span>');
|
|
616
|
+
}
|
|
617
|
+
if (data.status === 'ready') {
|
|
618
|
+
actions.push('<button class="btn btn-primary" id="start-work-btn" onclick="updateTask(' + taskId + ', \\'in_progress\\')">Start Work</button>');
|
|
619
|
+
}
|
|
620
|
+
if (data.status === 'in_progress' && data.pendingDiffId) {
|
|
621
|
+
actions.push('<a href="/diff/' + data.pendingDiffId + '" class="btn btn-task-action btn-approve">Review Diff</a>');
|
|
622
|
+
}
|
|
623
|
+
if (agentWorkingOnThis && data.currentRunId) {
|
|
624
|
+
actions.push('<a href="/agent/' + data.currentRunId + '" class="btn btn-task-action btn-task-progress">View Progress</a>');
|
|
625
|
+
}
|
|
626
|
+
actionsEl.innerHTML = actions.length ? '<div class="task-actions">' + actions.join('') + '</div>' : '';
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Update spec content in-place (preserves <details> open state)
|
|
630
|
+
if (data.specHtml) {
|
|
631
|
+
const specContent = document.querySelector('.spec-content');
|
|
632
|
+
if (specContent) specContent.innerHTML = data.specHtml;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Check for state changes
|
|
636
|
+
const pendingQueueChanged = !!data.hasPendingQueueItem !== lastPendingQueueItem;
|
|
637
|
+
if (pendingQueueChanged) lastPendingQueueItem = !!data.hasPendingQueueItem;
|
|
638
|
+
|
|
639
|
+
if (data.status !== currentStatus || pendingQueueChanged) {
|
|
640
|
+
const staysDraftingOrRefining =
|
|
641
|
+
(data.status === 'drafting' || data.status === 'refining') &&
|
|
642
|
+
(currentStatus === 'drafting' || currentStatus === 'refining');
|
|
643
|
+
|
|
644
|
+
if (staysDraftingOrRefining) {
|
|
645
|
+
// Handle refining <-> drafting transitions in-place
|
|
646
|
+
const wasRefining = currentStatus === 'refining';
|
|
647
|
+
const nowRefining = data.status === 'refining';
|
|
648
|
+
currentStatus = data.status;
|
|
649
|
+
|
|
650
|
+
if (wasRefining && !nowRefining) {
|
|
651
|
+
// refining → drafting: show chat input, hide busy message, refresh chat messages
|
|
652
|
+
const busyEl = document.querySelector('.chat-busy');
|
|
653
|
+
if (busyEl) {
|
|
654
|
+
const inputArea = document.createElement('div');
|
|
655
|
+
inputArea.className = 'chat-input-area';
|
|
656
|
+
inputArea.innerHTML = '<form class="chat-input" id="task-chat-form">' +
|
|
657
|
+
'<textarea name="message" id="task-message-input" placeholder="Describe changes to the spec..." rows="2" autofocus></textarea>' +
|
|
658
|
+
'<div class="task-chat-buttons"><button type="button" id="task-refine-btn" class="btn btn-primary">Refine</button></div>' +
|
|
659
|
+
'</form>';
|
|
660
|
+
busyEl.replaceWith(inputArea);
|
|
661
|
+
// Re-bind chat input handlers
|
|
662
|
+
rebindChatInput();
|
|
663
|
+
}
|
|
664
|
+
// Fetch any new chat messages added during refining
|
|
665
|
+
refreshChatMessages();
|
|
666
|
+
} else if (!wasRefining && nowRefining) {
|
|
667
|
+
// drafting → refining: hide chat input, show busy message
|
|
668
|
+
const inputArea = document.querySelector('.chat-input-area');
|
|
669
|
+
if (inputArea) {
|
|
670
|
+
const busyEl = document.createElement('div');
|
|
671
|
+
busyEl.className = 'chat-busy';
|
|
672
|
+
busyEl.innerHTML = '<p>Claude is working on this task...' +
|
|
673
|
+
(data.currentRunId ? ' <a href="/agent/' + data.currentRunId + '">View progress</a>' : '') + '</p>';
|
|
674
|
+
inputArea.replaceWith(busyEl);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
// pendingQueueChanged within drafting/refining — no reload needed
|
|
678
|
+
} else {
|
|
679
|
+
// For non-drafting/refining transitions, reload as before
|
|
680
|
+
const needsReload = (data.status === 'complete') ||
|
|
681
|
+
(data.status === 'conflict') ||
|
|
682
|
+
(currentStatus === 'conflict') ||
|
|
683
|
+
(data.status === 'drafting' && currentStatus !== 'drafting' && currentStatus !== 'refining') ||
|
|
684
|
+
(currentStatus === 'drafting' && data.status !== 'drafting' && data.status !== 'refining') ||
|
|
685
|
+
pendingQueueChanged;
|
|
686
|
+
currentStatus = data.status;
|
|
687
|
+
if (needsReload) {
|
|
688
|
+
location.reload();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
} catch {}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
setInterval(pollTaskStatus, 3000);
|
|
696
|
+
|
|
697
|
+
// Start Work button
|
|
698
|
+
const startBtn = document.getElementById('start-work-btn');
|
|
699
|
+
if (startBtn) {
|
|
700
|
+
startBtn.addEventListener('click', () => {
|
|
701
|
+
updateTask(${task.id}, 'in_progress');
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Task chat logic (drafting/refining — history always visible, input only when not refining)
|
|
706
|
+
var messagesEl = document.getElementById('task-messages');
|
|
707
|
+
var chatMessageCount = ${chatMessages.length};
|
|
708
|
+
var chatPolling = ${chatPending ? "true" : "false"};
|
|
709
|
+
|
|
710
|
+
function scrollToBottom() {
|
|
711
|
+
if (messagesEl) messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function escapeHtml(str) {
|
|
715
|
+
const div = document.createElement('div');
|
|
716
|
+
div.textContent = str;
|
|
717
|
+
return div.innerHTML;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function showThinking() {
|
|
721
|
+
if (!messagesEl) return;
|
|
722
|
+
let indicator = document.getElementById('thinking-indicator');
|
|
723
|
+
if (!indicator) {
|
|
724
|
+
indicator = document.createElement('div');
|
|
725
|
+
indicator.id = 'thinking-indicator';
|
|
726
|
+
indicator.className = 'message message-assistant';
|
|
727
|
+
indicator.innerHTML = '<div class="message-role">Claude</div><div class="message-content"><em>Thinking...</em></div>';
|
|
728
|
+
messagesEl.appendChild(indicator);
|
|
729
|
+
}
|
|
730
|
+
scrollToBottom();
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function removeThinking() {
|
|
734
|
+
const indicator = document.getElementById('thinking-indicator');
|
|
735
|
+
if (indicator) indicator.remove();
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async function pollForResponse() {
|
|
739
|
+
if (chatPolling) return;
|
|
740
|
+
chatPolling = true;
|
|
741
|
+
showThinking();
|
|
742
|
+
|
|
743
|
+
const poll = async () => {
|
|
744
|
+
try {
|
|
745
|
+
const resp = await fetch('/api/tasks/' + taskId + '/chat?after=' + chatMessageCount);
|
|
746
|
+
const data = await resp.json();
|
|
747
|
+
|
|
748
|
+
if (data.messages.length > 0) {
|
|
749
|
+
removeThinking();
|
|
750
|
+
for (const msg of data.messages) {
|
|
751
|
+
const div = document.createElement('div');
|
|
752
|
+
div.className = 'message ' + (msg.role === 'user' ? 'message-user' : 'message-assistant');
|
|
753
|
+
div.innerHTML = '<div class="message-role">' + (msg.role === 'user' ? 'You' : 'Claude') + '</div><div class="message-content">' + (msg.html || msg.content) + '</div>';
|
|
754
|
+
messagesEl.appendChild(div);
|
|
755
|
+
}
|
|
756
|
+
chatMessageCount = data.total;
|
|
757
|
+
scrollToBottom();
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (data.pending) {
|
|
761
|
+
showThinking();
|
|
762
|
+
setTimeout(poll, 2000);
|
|
763
|
+
} else {
|
|
764
|
+
removeThinking();
|
|
765
|
+
chatPolling = false;
|
|
766
|
+
const btn = document.getElementById('task-refine-btn');
|
|
767
|
+
if (btn) {
|
|
768
|
+
btn.disabled = false;
|
|
769
|
+
btn.textContent = 'Refine';
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
} catch {
|
|
773
|
+
chatPolling = false;
|
|
774
|
+
removeThinking();
|
|
775
|
+
const btn = document.getElementById('task-refine-btn');
|
|
776
|
+
if (btn) {
|
|
777
|
+
btn.disabled = false;
|
|
778
|
+
btn.textContent = 'Refine';
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
setTimeout(poll, 2000);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function refreshChatMessages() {
|
|
787
|
+
try {
|
|
788
|
+
const resp = await fetch('/api/tasks/' + taskId + '/chat?after=' + chatMessageCount);
|
|
789
|
+
const data = await resp.json();
|
|
790
|
+
if (data.messages.length > 0 && messagesEl) {
|
|
791
|
+
removeThinking();
|
|
792
|
+
for (const msg of data.messages) {
|
|
793
|
+
const div = document.createElement('div');
|
|
794
|
+
div.className = 'message ' + (msg.role === 'user' ? 'message-user' : 'message-assistant');
|
|
795
|
+
div.innerHTML = '<div class="message-role">' + (msg.role === 'user' ? 'You' : 'Claude') + '</div><div class="message-content">' + (msg.html || msg.content) + '</div>';
|
|
796
|
+
messagesEl.appendChild(div);
|
|
797
|
+
}
|
|
798
|
+
chatMessageCount = data.total;
|
|
799
|
+
scrollToBottom();
|
|
800
|
+
}
|
|
801
|
+
} catch {}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function rebindChatInput() {
|
|
805
|
+
const input = document.getElementById('task-message-input');
|
|
806
|
+
const refineBtn = document.getElementById('task-refine-btn');
|
|
807
|
+
|
|
808
|
+
if (input) {
|
|
809
|
+
input.addEventListener('input', () => {
|
|
810
|
+
input.style.height = 'auto';
|
|
811
|
+
input.style.height = Math.min(input.scrollHeight, 120) + 'px';
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
input.addEventListener('keydown', (e) => {
|
|
815
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
816
|
+
e.preventDefault();
|
|
817
|
+
if (refineBtn) refineBtn.click();
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (refineBtn) {
|
|
823
|
+
refineBtn.addEventListener('click', async () => {
|
|
824
|
+
const inp = document.getElementById('task-message-input');
|
|
825
|
+
const message = inp ? inp.value.trim() : '';
|
|
826
|
+
if (!message || chatPolling) return;
|
|
827
|
+
|
|
828
|
+
// Add user message to DOM
|
|
829
|
+
const userDiv = document.createElement('div');
|
|
830
|
+
userDiv.className = 'message message-user';
|
|
831
|
+
userDiv.innerHTML = '<div class="message-role">You</div><div class="message-content">' + escapeHtml(message) + '</div>';
|
|
832
|
+
if (messagesEl) messagesEl.appendChild(userDiv);
|
|
833
|
+
|
|
834
|
+
inp.value = '';
|
|
835
|
+
inp.style.height = 'auto';
|
|
836
|
+
refineBtn.disabled = true;
|
|
837
|
+
refineBtn.textContent = 'Starting...';
|
|
838
|
+
scrollToBottom();
|
|
839
|
+
|
|
840
|
+
try {
|
|
841
|
+
// Save message to task conversation first
|
|
842
|
+
await fetch('/api/tasks/' + taskId + '/chat', {
|
|
843
|
+
method: 'POST',
|
|
844
|
+
headers: { 'Content-Type': 'application/json' },
|
|
845
|
+
body: JSON.stringify({ message }),
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
// Then enqueue refine via the agent route
|
|
849
|
+
const resp = await fetch('/agent/refine-task/' + taskId, {
|
|
850
|
+
method: 'POST',
|
|
851
|
+
headers: { 'Content-Type': 'application/json' },
|
|
852
|
+
body: JSON.stringify({ message }),
|
|
853
|
+
});
|
|
854
|
+
if (resp.ok) {
|
|
855
|
+
// Transition will be handled by polling — just update state
|
|
856
|
+
currentStatus = 'refining';
|
|
857
|
+
lastPendingQueueItem = true;
|
|
858
|
+
// Hide input, show busy
|
|
859
|
+
const inputArea = document.querySelector('.chat-input-area');
|
|
860
|
+
if (inputArea) {
|
|
861
|
+
const busyEl = document.createElement('div');
|
|
862
|
+
busyEl.className = 'chat-busy';
|
|
863
|
+
busyEl.innerHTML = '<p>Claude is working on this task...</p>';
|
|
864
|
+
inputArea.replaceWith(busyEl);
|
|
865
|
+
}
|
|
866
|
+
} else {
|
|
867
|
+
refineBtn.disabled = false;
|
|
868
|
+
refineBtn.textContent = 'Refine';
|
|
869
|
+
}
|
|
870
|
+
} catch {
|
|
871
|
+
refineBtn.disabled = false;
|
|
872
|
+
refineBtn.textContent = 'Refine';
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (isDrafting) {
|
|
879
|
+
scrollToBottom();
|
|
880
|
+
|
|
881
|
+
// If already pending on page load, start polling
|
|
882
|
+
if (chatPolling) {
|
|
883
|
+
chatPolling = false;
|
|
884
|
+
pollForResponse();
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
rebindChatInput();
|
|
888
|
+
}
|
|
889
|
+
</script>`;
|
|
890
|
+
return layout("Task #" + task.id, "tasks", body);
|
|
891
|
+
}
|
|
892
|
+
function escapeHtml(str) {
|
|
893
|
+
return str
|
|
894
|
+
.replace(/&/g, "&")
|
|
895
|
+
.replace(/</g, "<")
|
|
896
|
+
.replace(/>/g, ">")
|
|
897
|
+
.replace(/"/g, """);
|
|
898
|
+
}
|