@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,386 @@
|
|
|
1
|
+
import { marked } from "marked";
|
|
2
|
+
import { layout } from "./layout.js";
|
|
3
|
+
function renderMessage(msg) {
|
|
4
|
+
const isUser = msg.role === "user";
|
|
5
|
+
const rendered = marked.parse(msg.content, { async: false });
|
|
6
|
+
return `<div class="message ${isUser ? "message-user" : "message-assistant"}">
|
|
7
|
+
<div class="message-role">${isUser ? "You" : "Claude"}</div>
|
|
8
|
+
<div class="message-content">${rendered}</div>
|
|
9
|
+
</div>`;
|
|
10
|
+
}
|
|
11
|
+
function typeIcon(type) {
|
|
12
|
+
switch (type) {
|
|
13
|
+
case "task_created": return "\u{1F4DD}";
|
|
14
|
+
case "task_complete": return "\u2705";
|
|
15
|
+
case "agent_spawn": return "\u{1F916}";
|
|
16
|
+
case "agent_complete": return "\u{1F3C1}";
|
|
17
|
+
case "checkpoint": return "\u{1F4BE}";
|
|
18
|
+
case "rejection": return "\u274C";
|
|
19
|
+
case "spec_update": return "\u{1F4C4}";
|
|
20
|
+
default: return "\u26A1";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function relativeTime(timestamp) {
|
|
24
|
+
const diff = Date.now() - new Date(timestamp).getTime();
|
|
25
|
+
const mins = Math.floor(diff / 60000);
|
|
26
|
+
if (mins < 1)
|
|
27
|
+
return "just now";
|
|
28
|
+
if (mins < 60)
|
|
29
|
+
return `${mins}m ago`;
|
|
30
|
+
const hours = Math.floor(mins / 60);
|
|
31
|
+
if (hours < 24)
|
|
32
|
+
return `${hours}h ago`;
|
|
33
|
+
const days = Math.floor(hours / 24);
|
|
34
|
+
return `${days}d ago`;
|
|
35
|
+
}
|
|
36
|
+
function renderActivityFeed(entries) {
|
|
37
|
+
if (entries.length === 0)
|
|
38
|
+
return "";
|
|
39
|
+
const items = entries.map((e) => {
|
|
40
|
+
const icon = typeIcon(e.type);
|
|
41
|
+
const time = relativeTime(e.timestamp);
|
|
42
|
+
let link = "";
|
|
43
|
+
const details = e.details || "";
|
|
44
|
+
// Extract taskId from details like "taskId:5" or summary like "Task #5"
|
|
45
|
+
const taskMatch = e.summary.match(/#(\d+)/) || details.match(/taskId:(\d+)/);
|
|
46
|
+
const runMatch = details.match(/runId:([\w-]+)/);
|
|
47
|
+
if (taskMatch) {
|
|
48
|
+
link = `/tasks/${taskMatch[1]}`;
|
|
49
|
+
}
|
|
50
|
+
if (runMatch) {
|
|
51
|
+
link = `/run/${runMatch[1]}`;
|
|
52
|
+
}
|
|
53
|
+
const summary = link
|
|
54
|
+
? `<a href="${link}" class="activity-link">${e.summary}</a>`
|
|
55
|
+
: e.summary;
|
|
56
|
+
return `<div class="activity-item"><span class="activity-icon">${icon}</span><span class="activity-summary">${summary}</span><span class="activity-time">${time}</span></div>`;
|
|
57
|
+
}).join("\n");
|
|
58
|
+
return `<details class="activity-feed">
|
|
59
|
+
<summary class="activity-header">Recent Activity</summary>
|
|
60
|
+
<div class="activity-list">${items}</div>
|
|
61
|
+
</details>`;
|
|
62
|
+
}
|
|
63
|
+
export function chatPage(messages, _pendingDiffs, agentRunning, currentRunId, chatPending = false, queuedCount = 0, services = [], historyEntries = []) {
|
|
64
|
+
// Filter out agent messages (those with runId) — display-only filtering
|
|
65
|
+
const filteredMessages = messages.filter((m) => !m.runId);
|
|
66
|
+
const messageHtml = filteredMessages.map(renderMessage).join("\n");
|
|
67
|
+
const queueSuffix = queuedCount > 0 ? ` ${queuedCount} queued` : "";
|
|
68
|
+
const bannerHtml = agentRunning && currentRunId
|
|
69
|
+
? `<a href="/agent/${currentRunId}" class="agent-banner">Agent working...${queueSuffix} tap to view progress</a>`
|
|
70
|
+
: "";
|
|
71
|
+
const thinkingHtml = chatPending
|
|
72
|
+
? `<div class="message message-assistant" id="thinking-indicator">
|
|
73
|
+
<div class="message-role">Claude</div>
|
|
74
|
+
<div class="message-content"><em>Thinking...</em></div>
|
|
75
|
+
</div>`
|
|
76
|
+
: "";
|
|
77
|
+
const activityHtml = renderActivityFeed(historyEntries);
|
|
78
|
+
const body = `
|
|
79
|
+
<div class="chat-container">
|
|
80
|
+
${bannerHtml}
|
|
81
|
+
${activityHtml}
|
|
82
|
+
<div class="messages" id="messages">
|
|
83
|
+
${messageHtml}
|
|
84
|
+
${thinkingHtml}
|
|
85
|
+
</div>
|
|
86
|
+
${services.length ? `<div class="service-status-bar" id="service-bar">
|
|
87
|
+
${services.map((s) => {
|
|
88
|
+
const color = s.status === "running" ? "var(--accent-green)" : s.status === "crashed" ? "var(--accent-red)" : "var(--text-muted)";
|
|
89
|
+
return `<button class="service-toggle" data-id="${s.id}" data-status="${s.status}" onclick="toggleService('${s.id}', '${s.status}')">
|
|
90
|
+
<span class="service-dot" style="background:${color}"></span>
|
|
91
|
+
${s.name}
|
|
92
|
+
</button>`;
|
|
93
|
+
}).join("\n")}
|
|
94
|
+
</div>` : ""}
|
|
95
|
+
<div class="chat-input-area">
|
|
96
|
+
<form class="chat-input" id="chat-form">
|
|
97
|
+
<textarea
|
|
98
|
+
name="message"
|
|
99
|
+
id="message-input"
|
|
100
|
+
placeholder="Ask Claude or describe what you want..."
|
|
101
|
+
rows="2"
|
|
102
|
+
autofocus
|
|
103
|
+
></textarea>
|
|
104
|
+
<button type="submit" id="send-btn">Send</button>
|
|
105
|
+
</form>
|
|
106
|
+
<div class="chat-action-buttons">
|
|
107
|
+
<button class="draft-task-btn" id="draft-task-btn">Draft Task</button>
|
|
108
|
+
<button class="draft-task-btn" id="add-service-btn" style="border-color:var(--accent-green);color:var(--accent-green)">Add Service</button>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<script>
|
|
113
|
+
const form = document.getElementById('chat-form');
|
|
114
|
+
const input = document.getElementById('message-input');
|
|
115
|
+
const messagesEl = document.getElementById('messages');
|
|
116
|
+
const sendBtn = document.getElementById('send-btn');
|
|
117
|
+
const draftBtn = document.getElementById('draft-task-btn');
|
|
118
|
+
let messageCount = ${filteredMessages.length};
|
|
119
|
+
let polling = ${chatPending ? "true" : "false"};
|
|
120
|
+
|
|
121
|
+
function scrollToBottom() {
|
|
122
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
123
|
+
}
|
|
124
|
+
scrollToBottom();
|
|
125
|
+
|
|
126
|
+
function escapeHtml(str) {
|
|
127
|
+
const div = document.createElement('div');
|
|
128
|
+
div.textContent = str;
|
|
129
|
+
return div.innerHTML;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
input.addEventListener('input', () => {
|
|
133
|
+
input.style.height = 'auto';
|
|
134
|
+
input.style.height = Math.min(input.scrollHeight, 120) + 'px';
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
input.addEventListener('keydown', (e) => {
|
|
138
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
form.dispatchEvent(new Event('submit'));
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
function showThinking() {
|
|
145
|
+
let indicator = document.getElementById('thinking-indicator');
|
|
146
|
+
if (!indicator) {
|
|
147
|
+
indicator = document.createElement('div');
|
|
148
|
+
indicator.id = 'thinking-indicator';
|
|
149
|
+
indicator.className = 'message message-assistant';
|
|
150
|
+
indicator.innerHTML = '<div class="message-role">Claude</div><div class="message-content"><em>Thinking...</em></div>';
|
|
151
|
+
messagesEl.appendChild(indicator);
|
|
152
|
+
}
|
|
153
|
+
scrollToBottom();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function removeThinking() {
|
|
157
|
+
const indicator = document.getElementById('thinking-indicator');
|
|
158
|
+
if (indicator) indicator.remove();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function pollForResponse() {
|
|
162
|
+
if (polling) return; // already polling
|
|
163
|
+
polling = true;
|
|
164
|
+
showThinking();
|
|
165
|
+
|
|
166
|
+
const poll = async () => {
|
|
167
|
+
try {
|
|
168
|
+
const resp = await fetch('/api/chat/messages?after=' + messageCount);
|
|
169
|
+
const data = await resp.json();
|
|
170
|
+
|
|
171
|
+
if (data.messages.length > 0) {
|
|
172
|
+
removeThinking();
|
|
173
|
+
for (const msg of data.messages) {
|
|
174
|
+
const div = document.createElement('div');
|
|
175
|
+
div.className = 'message ' + (msg.role === 'user' ? 'message-user' : 'message-assistant');
|
|
176
|
+
div.innerHTML = '<div class="message-role">' + (msg.role === 'user' ? 'You' : 'Claude') + '</div><div class="message-content">' + (msg.html || msg.content) + '</div>';
|
|
177
|
+
messagesEl.appendChild(div);
|
|
178
|
+
}
|
|
179
|
+
messageCount = data.total;
|
|
180
|
+
scrollToBottom();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (data.pending) {
|
|
184
|
+
showThinking();
|
|
185
|
+
setTimeout(poll, 2000);
|
|
186
|
+
} else {
|
|
187
|
+
removeThinking();
|
|
188
|
+
polling = false;
|
|
189
|
+
sendBtn.disabled = false;
|
|
190
|
+
sendBtn.textContent = 'Send';
|
|
191
|
+
refreshServiceBar();
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
polling = false;
|
|
195
|
+
removeThinking();
|
|
196
|
+
sendBtn.disabled = false;
|
|
197
|
+
sendBtn.textContent = 'Send';
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
setTimeout(poll, 2000);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// If already pending on page load, start polling
|
|
205
|
+
if (polling) {
|
|
206
|
+
polling = false; // reset so pollForResponse can set it
|
|
207
|
+
pollForResponse();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
form.addEventListener('submit', async (e) => {
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
const message = input.value.trim();
|
|
213
|
+
if (!message || polling) return;
|
|
214
|
+
|
|
215
|
+
// Append user message to DOM immediately
|
|
216
|
+
const userDiv = document.createElement('div');
|
|
217
|
+
userDiv.className = 'message message-user';
|
|
218
|
+
userDiv.innerHTML = '<div class="message-role">You</div><div class="message-content">' + escapeHtml(message) + '</div>';
|
|
219
|
+
messagesEl.appendChild(userDiv);
|
|
220
|
+
|
|
221
|
+
input.value = '';
|
|
222
|
+
input.style.height = 'auto';
|
|
223
|
+
sendBtn.disabled = true;
|
|
224
|
+
sendBtn.textContent = '...';
|
|
225
|
+
scrollToBottom();
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const resp = await fetch('/api/chat/send', {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
headers: { 'Content-Type': 'application/json' },
|
|
231
|
+
body: JSON.stringify({ message }),
|
|
232
|
+
});
|
|
233
|
+
const data = await resp.json();
|
|
234
|
+
|
|
235
|
+
if (resp.ok) {
|
|
236
|
+
messageCount++; // user message was saved server-side
|
|
237
|
+
pollForResponse();
|
|
238
|
+
} else {
|
|
239
|
+
sendBtn.disabled = false;
|
|
240
|
+
sendBtn.textContent = 'Send';
|
|
241
|
+
if (data.error) {
|
|
242
|
+
const errDiv = document.createElement('div');
|
|
243
|
+
errDiv.className = 'error';
|
|
244
|
+
errDiv.textContent = data.error;
|
|
245
|
+
messagesEl.appendChild(errDiv);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
sendBtn.disabled = false;
|
|
250
|
+
sendBtn.textContent = 'Send';
|
|
251
|
+
const errDiv = document.createElement('div');
|
|
252
|
+
errDiv.className = 'error';
|
|
253
|
+
errDiv.textContent = 'Failed to send message';
|
|
254
|
+
messagesEl.appendChild(errDiv);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
input.focus();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Service controls
|
|
261
|
+
async function toggleService(id, status) {
|
|
262
|
+
const action = status === 'running' ? 'stop' : 'start';
|
|
263
|
+
await fetch('/api/services/' + id + '/' + action, { method: 'POST' });
|
|
264
|
+
// Refresh service bar
|
|
265
|
+
refreshServiceBar();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function refreshServiceBar() {
|
|
269
|
+
try {
|
|
270
|
+
const resp = await fetch('/api/services');
|
|
271
|
+
const services = await resp.json();
|
|
272
|
+
const bar = document.getElementById('service-bar');
|
|
273
|
+
if (!bar && services.length === 0) return;
|
|
274
|
+
if (!bar && services.length > 0) {
|
|
275
|
+
location.reload();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (bar) {
|
|
279
|
+
bar.innerHTML = services.map(s => {
|
|
280
|
+
const color = s.status === 'running' ? 'var(--accent-green)' : s.status === 'crashed' ? 'var(--accent-red)' : 'var(--text-muted)';
|
|
281
|
+
return '<button class="service-toggle" data-id="' + s.id + '" data-status="' + s.status + '" onclick="toggleService(\\'' + s.id + '\\', \\'' + s.status + '\\')">' +
|
|
282
|
+
'<span class="service-dot" style="background:' + color + '"></span>' + s.name + '</button>';
|
|
283
|
+
}).join('');
|
|
284
|
+
}
|
|
285
|
+
} catch {}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Poll service status
|
|
289
|
+
setInterval(refreshServiceBar, 5000);
|
|
290
|
+
|
|
291
|
+
const addServiceBtn = document.getElementById('add-service-btn');
|
|
292
|
+
addServiceBtn.addEventListener('click', async () => {
|
|
293
|
+
const message = input.value.trim();
|
|
294
|
+
if (!message || polling) return;
|
|
295
|
+
|
|
296
|
+
// Show user message in DOM immediately
|
|
297
|
+
const userDiv = document.createElement('div');
|
|
298
|
+
userDiv.className = 'message message-user';
|
|
299
|
+
userDiv.innerHTML = '<div class="message-role">You</div><div class="message-content">' + escapeHtml(message) + '</div>';
|
|
300
|
+
messagesEl.appendChild(userDiv);
|
|
301
|
+
|
|
302
|
+
input.value = '';
|
|
303
|
+
input.style.height = 'auto';
|
|
304
|
+
sendBtn.disabled = true;
|
|
305
|
+
sendBtn.textContent = '...';
|
|
306
|
+
scrollToBottom();
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const resp = await fetch('/api/chat/add-service', {
|
|
310
|
+
method: 'POST',
|
|
311
|
+
headers: { 'Content-Type': 'application/json' },
|
|
312
|
+
body: JSON.stringify({ message }),
|
|
313
|
+
});
|
|
314
|
+
const data = await resp.json();
|
|
315
|
+
|
|
316
|
+
if (resp.ok) {
|
|
317
|
+
messageCount++;
|
|
318
|
+
pollForResponse();
|
|
319
|
+
} else {
|
|
320
|
+
sendBtn.disabled = false;
|
|
321
|
+
sendBtn.textContent = 'Send';
|
|
322
|
+
if (data.error) {
|
|
323
|
+
const errDiv = document.createElement('div');
|
|
324
|
+
errDiv.className = 'error';
|
|
325
|
+
errDiv.textContent = data.error;
|
|
326
|
+
messagesEl.appendChild(errDiv);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} catch (err) {
|
|
330
|
+
sendBtn.disabled = false;
|
|
331
|
+
sendBtn.textContent = 'Send';
|
|
332
|
+
const errDiv = document.createElement('div');
|
|
333
|
+
errDiv.className = 'error';
|
|
334
|
+
errDiv.textContent = 'Failed to send message';
|
|
335
|
+
messagesEl.appendChild(errDiv);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
input.focus();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Make functions available globally for onclick handlers
|
|
342
|
+
window.toggleService = toggleService;
|
|
343
|
+
|
|
344
|
+
draftBtn.addEventListener('click', async () => {
|
|
345
|
+
draftBtn.disabled = true;
|
|
346
|
+
draftBtn.textContent = 'Queuing...';
|
|
347
|
+
try {
|
|
348
|
+
const message = input.value.trim();
|
|
349
|
+
if (!message) {
|
|
350
|
+
draftBtn.disabled = false;
|
|
351
|
+
draftBtn.textContent = 'Draft Task';
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
// Save to chat history and show in DOM
|
|
355
|
+
const userDiv = document.createElement('div');
|
|
356
|
+
userDiv.className = 'message message-user';
|
|
357
|
+
userDiv.innerHTML = '<div class="message-role">You</div><div class="message-content">' + escapeHtml(message) + '</div>';
|
|
358
|
+
messagesEl.appendChild(userDiv);
|
|
359
|
+
input.value = '';
|
|
360
|
+
input.style.height = 'auto';
|
|
361
|
+
await fetch('/chat', {
|
|
362
|
+
method: 'POST',
|
|
363
|
+
headers: { 'Content-Type': 'application/json' },
|
|
364
|
+
body: JSON.stringify({ message }),
|
|
365
|
+
});
|
|
366
|
+
// Enqueue draft task
|
|
367
|
+
const resp = await fetch('/agent/draft-task', {
|
|
368
|
+
method: 'POST',
|
|
369
|
+
headers: { 'Content-Type': 'application/json' },
|
|
370
|
+
body: JSON.stringify({ message }),
|
|
371
|
+
});
|
|
372
|
+
const data = await resp.json();
|
|
373
|
+
if (data.ok) {
|
|
374
|
+
window.location.href = '/tasks';
|
|
375
|
+
} else {
|
|
376
|
+
draftBtn.disabled = false;
|
|
377
|
+
draftBtn.textContent = 'Draft Task';
|
|
378
|
+
}
|
|
379
|
+
} catch (err) {
|
|
380
|
+
draftBtn.disabled = false;
|
|
381
|
+
draftBtn.textContent = 'Draft Task';
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
</script>`;
|
|
385
|
+
return layout("Chat", "chat", body);
|
|
386
|
+
}
|